1637 lines
73 KiB
Python
1637 lines
73 KiB
Python
"""销售订单服务层。
|
||
|
||
负责销售订单的全生命周期管理,包括创建、编辑、提交审核、审批、取消、
|
||
发厂文案生成、状态流转等核心业务逻辑。
|
||
依赖多个 Repository 进行数据持久化,依赖 audit_service 记录操作审计日志,
|
||
依赖 arrears_service 同步欠款数据,依赖 ReminderRepository 发送状态变更通知。
|
||
"""
|
||
|
||
from datetime import datetime
|
||
import json
|
||
|
||
from sqlalchemy.exc import SQLAlchemyError
|
||
from sqlalchemy.orm import Session
|
||
|
||
from backend.app.core.cache import cache_delete, cache_delete_pattern, cache_get, cache_set, make_cache_key
|
||
from backend.app.core.error_codes import ErrorCode
|
||
from backend.app.core.exceptions import AppException
|
||
from backend.app.repositories.customer_repository import CustomerRepository
|
||
from backend.app.repositories.config_repository import ConfigRepository
|
||
from backend.app.repositories.file_repository import FileRepository
|
||
from backend.app.repositories.logistics_repository import LogisticsRepository
|
||
from backend.app.repositories.order_approve_log_repository import OrderApproveLogRepository
|
||
from backend.app.repositories.order_repository import OrderRepository
|
||
from backend.app.repositories.reminder_repository import ReminderRepository
|
||
from backend.app.services.arrears_service import arrears_service
|
||
from backend.app.services.audit_service import audit_service
|
||
from backend.app.services.event_bus import event_bus
|
||
|
||
|
||
class OrderService:
|
||
"""订单服务,封装销售订单的完整业务流程。
|
||
|
||
依赖:
|
||
OrderRepository - 订单数据访问
|
||
CustomerRepository - 客户数据访问(订单录入时自动创建客户)
|
||
ConfigRepository - 系统配置读取
|
||
OrderApproveLogRepository - 审批日志
|
||
LogisticsRepository - 物流任务和轨迹
|
||
FileRepository - 附件查询
|
||
ReminderRepository - 状态变更提醒通知
|
||
arrears_service - 欠款同步
|
||
audit_service - 操作审计日志
|
||
"""
|
||
|
||
def __init__(self) -> None:
|
||
self.order_repository = OrderRepository()
|
||
self.customer_repository = CustomerRepository()
|
||
self.config_repository = ConfigRepository()
|
||
self.order_approve_log_repository = OrderApproveLogRepository()
|
||
self.logistics_repository = LogisticsRepository()
|
||
self.file_repository = FileRepository()
|
||
self.reminder_repository = ReminderRepository()
|
||
|
||
def list_orders(self, session: Session | None = None, filters: dict | None = None, current_user: dict | None = None) -> dict:
|
||
"""查询订单列表。
|
||
|
||
根据过滤条件查询订单,并根据用户角色过滤敏感字段(如利润、佣金对业务员不可见)。
|
||
|
||
Args:
|
||
session: 数据库会话
|
||
filters: 查询过滤条件
|
||
current_user: 当前登录用户信息,用于角色级数据过滤
|
||
|
||
Returns:
|
||
包含 total 和 list 的字典
|
||
|
||
被调用路由: orders.py - GET /orders, salesman.py - GET /salesman/orders
|
||
"""
|
||
if session is not None:
|
||
# 读缓存
|
||
cache_key = make_cache_key("order:list", user_id=(current_user or {}).get("user_id"), **(filters or {}))
|
||
cached = cache_get(cache_key)
|
||
if cached is not None:
|
||
return cached
|
||
try:
|
||
result = self.order_repository.list_orders_by_filters(session, filters or {})
|
||
total = result["total"] or 0
|
||
orders = result["list"]
|
||
role_code = (current_user or {}).get("role_code")
|
||
rows = []
|
||
for item in orders:
|
||
# 获取订单明细摘要
|
||
order_items = self.order_repository.list_order_items(session, item.id)
|
||
items_summary = ""
|
||
if order_items:
|
||
summary_parts = [f"{oi.product_name}×{int(oi.quantity or 0)}" for oi in order_items[:2]]
|
||
items_summary = "、".join(summary_parts)
|
||
if len(order_items) > 2:
|
||
items_summary += "..."
|
||
|
||
# 获取物流信息
|
||
task = self.logistics_repository.get_task_by_order_id(session, item.id)
|
||
logistics_info = ""
|
||
if task:
|
||
logistics_info = task.express_company + " " + (task.tracking_number or "") if task.express_company else task.status or ""
|
||
|
||
row = self._filter_order_row(
|
||
{
|
||
"order_id": item.id,
|
||
"order_no": item.order_no,
|
||
"customer_name": item.customer_name,
|
||
"customer_mobile": item.customer_mobile,
|
||
"salesman_name": "",
|
||
"order_status": item.order_status,
|
||
"order_source": item.order_source,
|
||
"sale_price_total": float(item.sale_price_total or 0),
|
||
"profit_total": float(item.profit_total or 0),
|
||
"commission_amount": float(item.commission_amount or 0),
|
||
"created_at": item.created_at.strftime("%Y-%m-%d %H:%M:%S") if item.created_at else "",
|
||
"items_summary": items_summary,
|
||
"logistics_info": logistics_info,
|
||
},
|
||
role_code,
|
||
)
|
||
rows.append(row)
|
||
# 写缓存
|
||
result_data = {"total": total, "list": rows}
|
||
cache_set(cache_key, result_data, ttl=30)
|
||
return result_data
|
||
except SQLAlchemyError as exc:
|
||
raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库异常", status_code=500) from exc
|
||
return {"total": 0, "list": []}
|
||
|
||
def get_order(
|
||
self,
|
||
order_id: int,
|
||
session: Session | None = None,
|
||
current_user: dict | None = None,
|
||
) -> dict | None:
|
||
"""查询单个订单详情。
|
||
|
||
聚合订单主档、明细、审批日志、物流任务、物流轨迹、附件、计算明细等信息。
|
||
根据用户角色过滤敏感字段。
|
||
|
||
Args:
|
||
order_id: 订单 ID
|
||
session: 数据库会话
|
||
current_user: 当前登录用户信息
|
||
|
||
Returns:
|
||
订单完整详情字典,订单不存在时返回 None
|
||
|
||
被调用路由: orders.py - GET /orders/{order_id}
|
||
"""
|
||
if session is not None:
|
||
# 读缓存
|
||
cache_key = f"order:detail:{order_id}"
|
||
cached = cache_get(cache_key)
|
||
if cached is not None:
|
||
return self._filter_order_detail_by_role(cached, current_user)
|
||
try:
|
||
order = self.order_repository.get_order(session, order_id)
|
||
if order is None:
|
||
return None
|
||
self._ensure_order_access(order, current_user)
|
||
|
||
supplier = self.order_repository.get_supplier(session, order.factory_id)
|
||
items = self.order_repository.list_order_items(session, order.id)
|
||
approve_logs = self.order_approve_log_repository.list_by_order_id(session, order.id)
|
||
task = self.logistics_repository.get_task_by_order_id(session, order.id)
|
||
traces = self.logistics_repository.list_traces_by_order_id(session, order.id)
|
||
order_attachments = self.file_repository.list_attachments(session, "sales_order", order.id)
|
||
task_attachments = self.file_repository.list_attachments(session, "logistics_task", task.id) if task else []
|
||
|
||
result = {
|
||
"order_id": order.id,
|
||
"order_no": order.order_no,
|
||
"customer_id": order.customer_id,
|
||
"customer_name": order.customer_name,
|
||
"customer_mobile": order.customer_mobile,
|
||
"customer_address": order.customer_address,
|
||
"salesman_id": order.salesman_id,
|
||
"salesman_name": "",
|
||
"order_source": order.order_source,
|
||
"order_status": order.order_status,
|
||
"delivery_type": order.delivery_type,
|
||
"factory_id": order.factory_id,
|
||
"factory_name": supplier.supplier_name if supplier else "",
|
||
"sale_price_total": float(order.sale_price_total or 0),
|
||
"cost_price_total": float(order.cost_price_total or 0),
|
||
"rebate_total": float(order.rebate_total or 0),
|
||
"freight_total": float(order.freight_total or 0),
|
||
"tax_total": float(order.tax_total or 0),
|
||
"other_fee_total": float(order.other_fee_total or 0),
|
||
"profit_total": float(order.profit_total or 0),
|
||
"profit_rate": float(order.profit_rate or 0),
|
||
"commission_amount": float(order.commission_amount or 0),
|
||
"payment_method": order.payment_method,
|
||
"tax_amount": float(order.tax_amount or 0),
|
||
"items": [
|
||
{
|
||
"item_id": item.id,
|
||
"product_id": item.product_id,
|
||
"product_name": item.product_name,
|
||
"specification": item.specification,
|
||
"unit": item.unit,
|
||
"quantity": float(item.quantity or 0),
|
||
"sale_price": float(item.sale_price or 0),
|
||
"cost_price": float(item.cost_price or 0),
|
||
"rebate_amount": float(item.rebate_amount or 0),
|
||
"freight_amount": float(item.freight_amount or 0),
|
||
"tax_amount": float(item.tax_amount or 0),
|
||
"other_fee_amount": float(item.other_fee_amount or 0),
|
||
"remark": item.remark,
|
||
"pricing_type": getattr(item, "pricing_type", None),
|
||
"length_m": float(item.length_m) if getattr(item, "length_m", None) else None,
|
||
"width_m": float(item.width_m) if getattr(item, "width_m", None) else None,
|
||
"area_sqm": float(item.area_sqm) if getattr(item, "area_sqm", None) else None,
|
||
"surcharge_detail": getattr(item, "surcharge_detail", None),
|
||
"processing_detail": getattr(item, "processing_detail", None),
|
||
"supplier_id": getattr(item, "supplier_id", None),
|
||
"supplier_model": getattr(item, "supplier_model", None),
|
||
"price_tier": getattr(item, "price_tier", None),
|
||
}
|
||
for item in items
|
||
],
|
||
"approve_logs": [
|
||
{
|
||
"log_id": log.id,
|
||
"approve_type": log.approve_type,
|
||
"approve_result": log.approve_result,
|
||
"before_status": log.before_status,
|
||
"after_status": log.after_status,
|
||
"approve_opinion": log.approve_opinion,
|
||
"operator_id": log.operator_id,
|
||
"approve_time": log.created_at.strftime("%Y-%m-%d %H:%M:%S") if log.created_at else "",
|
||
}
|
||
for log in approve_logs
|
||
],
|
||
"logistics_info": {
|
||
"task": self._build_task_summary(task),
|
||
"trace_list": [
|
||
{
|
||
"task_id": item.task_id,
|
||
"node_time": item.node_time.strftime("%Y-%m-%d %H:%M:%S") if item.node_time else "",
|
||
"node_desc": item.node_desc,
|
||
"node_type": item.node_type,
|
||
"source_platform": item.source_platform,
|
||
"remark": item.remark,
|
||
}
|
||
for item in traces
|
||
],
|
||
},
|
||
"attachments": [
|
||
*[self._build_attachment_row(item) for item in order_attachments],
|
||
*[self._build_attachment_row(item) for item in task_attachments],
|
||
],
|
||
"calculation_detail": self._build_calculation_detail(order, items),
|
||
"profit_alert_threshold": float(self._get_config_value(session, "profit_alert_threshold", "0")),
|
||
"remark": order.remark,
|
||
}
|
||
# 写缓存(缓存完整结果,角色过滤在读取后执行)
|
||
cache_set(cache_key, result, ttl=60)
|
||
return self._filter_order_detail_by_role(result, current_user)
|
||
except SQLAlchemyError as exc:
|
||
raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库异常", status_code=500) from exc
|
||
return None
|
||
|
||
def update_order(
|
||
self,
|
||
order_id: int,
|
||
payload: dict,
|
||
session: Session | None = None,
|
||
current_user: dict | None = None,
|
||
) -> dict | None:
|
||
"""编辑订单。
|
||
|
||
仅草稿和已退回状态的订单允许编辑。编辑时若客户不存在会自动创建。
|
||
重新计算销售总额、成本总额、利润等汇总数据。
|
||
|
||
Args:
|
||
order_id: 订单 ID
|
||
payload: 包含订单主档和明细的请求体
|
||
session: 数据库会话
|
||
current_user: 当前登录用户信息
|
||
|
||
Returns:
|
||
更新结果字典,包含订单汇总数据
|
||
|
||
被调用路由: orders.py - PUT /orders/{order_id}
|
||
"""
|
||
if session is not None:
|
||
try:
|
||
order = self.order_repository.get_order(session, order_id)
|
||
if order is None:
|
||
return None
|
||
self._ensure_order_access(order, current_user, owner_only=True)
|
||
self._ensure_status(order.order_status, {"draft", "rejected", "pending_approve"}, "当前状态不允许编辑订单")
|
||
if not payload["customer_name"].strip():
|
||
raise AppException(code=ErrorCode.PARAM_ERROR, message="客户姓名不能为空", status_code=400)
|
||
if not payload["customer_mobile"].strip():
|
||
raise AppException(code=ErrorCode.PARAM_ERROR, message="客户手机号不能为空", status_code=400)
|
||
customer = self.customer_repository.find_by_name_and_mobile(
|
||
session,
|
||
payload["customer_name"],
|
||
payload["customer_mobile"],
|
||
)
|
||
if customer is None:
|
||
customer = self.customer_repository.create_customer(
|
||
session,
|
||
{
|
||
"customer_name": payload["customer_name"],
|
||
"mobile": payload["customer_mobile"],
|
||
"address": payload.get("customer_address"),
|
||
"settlement_type": None,
|
||
"settlement_days": 0,
|
||
"customer_type": None,
|
||
"salesman_id": current_user.get("user_id") if current_user else payload.get("salesman_id"),
|
||
"credit_limit": 0,
|
||
"remark": "订单编辑时自动创建",
|
||
},
|
||
)
|
||
items = payload["items"]
|
||
if not items:
|
||
raise AppException(code=ErrorCode.PARAM_ERROR, message="订单明细不能为空", status_code=400)
|
||
sale_total = sum(item["quantity"] * item["sale_price"] for item in items)
|
||
cost_total = sum(item["quantity"] * item["cost_price"] for item in items)
|
||
profit_total = sale_total - cost_total - payload["rebate_total"] - payload["freight_total"] - payload["tax_total"] - payload["other_fee_total"]
|
||
profit_rate = round((profit_total / sale_total) * 100, 2) if sale_total else 0
|
||
before_status = order.order_status
|
||
self.order_repository.update_order_with_items(
|
||
session,
|
||
order,
|
||
{
|
||
"customer_id": customer.id,
|
||
"customer_name": payload["customer_name"],
|
||
"customer_mobile": payload["customer_mobile"],
|
||
"customer_address": payload.get("customer_address"),
|
||
"salesman_id": current_user.get("user_id") if current_user else payload.get("salesman_id"),
|
||
"order_source": payload.get("order_source"),
|
||
"delivery_type": payload.get("delivery_type"),
|
||
"factory_id": payload.get("factory_id"),
|
||
"sale_price_total": sale_total,
|
||
"cost_price_total": cost_total,
|
||
"rebate_total": payload["rebate_total"],
|
||
"freight_total": payload["freight_total"],
|
||
"tax_total": payload["tax_total"],
|
||
"other_fee_total": payload["other_fee_total"],
|
||
"profit_total": profit_total,
|
||
"profit_rate": profit_rate,
|
||
"commission_amount": payload["commission_amount"],
|
||
"payment_method": payload.get("payment_method"),
|
||
"tax_amount": payload.get("tax_amount", 0),
|
||
"remark": payload.get("remark"),
|
||
},
|
||
items,
|
||
)
|
||
audit_service.write_log(
|
||
session,
|
||
{
|
||
"operate_type": "order_update",
|
||
"biz_type": "sales_order",
|
||
"biz_id": order.id,
|
||
"before_value": {"order_status": before_status},
|
||
"after_value": {
|
||
"order_no": order.order_no,
|
||
"order_status": order.order_status,
|
||
"sale_price_total": round(sale_total, 2),
|
||
"cost_price_total": round(cost_total, 2),
|
||
"profit_total": round(profit_total, 2),
|
||
"profit_rate": profit_rate,
|
||
"commission_amount": payload["commission_amount"],
|
||
},
|
||
"remark": f"更新订单 {order.order_no}",
|
||
},
|
||
)
|
||
session.commit()
|
||
self._invalidate_order_cache(order.id)
|
||
return {
|
||
"order_id": order.id,
|
||
"order_no": order.order_no,
|
||
"order_status": order.order_status,
|
||
"sale_price_total": round(sale_total, 2),
|
||
"cost_price_total": round(cost_total, 2),
|
||
"profit_total": round(profit_total, 2),
|
||
"profit_rate": profit_rate,
|
||
"commission_amount": payload["commission_amount"],
|
||
"payment_method": payload.get("payment_method"),
|
||
"tax_amount": payload.get("tax_amount", 0),
|
||
}
|
||
except AppException:
|
||
session.rollback()
|
||
raise
|
||
except SQLAlchemyError as exc:
|
||
session.rollback()
|
||
raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库异常", status_code=500) from exc
|
||
|
||
raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库连接不可用", status_code=500)
|
||
|
||
def submit_order(
|
||
self,
|
||
order_id: int,
|
||
session: Session | None = None,
|
||
current_user: dict | None = None,
|
||
) -> dict | None:
|
||
"""提交订单审核。
|
||
|
||
仅草稿和已退回状态的订单允许提交。提交后状态变为 pending_approve,
|
||
并向经理角色发送审批提醒通知。
|
||
|
||
Args:
|
||
order_id: 订单 ID
|
||
session: 数据库会话
|
||
current_user: 当前登录用户信息
|
||
|
||
Returns:
|
||
提交结果字典
|
||
|
||
被调用路由: orders.py - POST /orders/{order_id}/submit
|
||
"""
|
||
if session is not None:
|
||
try:
|
||
order = self.order_repository.get_order(session, order_id)
|
||
if order is None:
|
||
return None
|
||
self._ensure_order_access(order, current_user, owner_only=True)
|
||
self._ensure_status(order.order_status, {"draft", "rejected", "pending_approve"}, "当前状态不允许提交审核")
|
||
|
||
before_status = order.order_status
|
||
self.order_repository.update_order_status(session, order, "pending_approve")
|
||
audit_service.write_log(
|
||
session,
|
||
{
|
||
"operate_type": "order_submit",
|
||
"biz_type": "sales_order",
|
||
"biz_id": order.id,
|
||
"before_value": {"order_status": before_status},
|
||
"after_value": {"order_status": order.order_status},
|
||
"remark": f"提交订单审核 {order.order_no}",
|
||
},
|
||
)
|
||
self._notify_status_change(session, order, before_status, order.order_status, current_user.get("user_id") if current_user else None)
|
||
session.commit()
|
||
self._invalidate_order_cache(order.id)
|
||
return {
|
||
"order_id": order.id,
|
||
"order_status": order.order_status,
|
||
"submitted_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
}
|
||
except AppException:
|
||
session.rollback()
|
||
raise
|
||
except SQLAlchemyError as exc:
|
||
session.rollback()
|
||
raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库异常", status_code=500) from exc
|
||
return None
|
||
|
||
def cancel_order(
|
||
self,
|
||
order_id: int,
|
||
payload: dict,
|
||
session: Session | None = None,
|
||
current_user: dict | None = None,
|
||
) -> dict | None:
|
||
"""发起订单取消。
|
||
|
||
根据订单当前状态决定取消方式:草稿/待审核可直接取消,
|
||
已通过/履约中的订单需走取消审批流程。必须填写取消原因。
|
||
|
||
Args:
|
||
order_id: 订单 ID
|
||
payload: 包含 cancel_reason、cancel_opinion 的请求体
|
||
session: 数据库会话
|
||
current_user: 当前登录用户信息
|
||
|
||
Returns:
|
||
取消结果字典,包含状态变更信息
|
||
|
||
被调用路由: orders.py - POST /orders/{order_id}/cancel
|
||
"""
|
||
self._validate_cancel_payload(payload)
|
||
|
||
if session is not None:
|
||
try:
|
||
order = self.order_repository.get_order(session, order_id)
|
||
if order is None:
|
||
return None
|
||
self._ensure_order_access(order, current_user, owner_only=True)
|
||
|
||
if order.order_status in {"canceled", "settled"}:
|
||
raise AppException(code=ErrorCode.INVALID_STATUS, message="当前状态不允许取消订单", status_code=400)
|
||
|
||
allow_cancel_config = self._get_config_value(session, "allow_cancel_statuses", "draft,pending_approve,approved,pending_factory")
|
||
allow_cancel_statuses = {s.strip() for s in allow_cancel_config.split(",") if s.strip()}
|
||
if order.order_status not in allow_cancel_statuses:
|
||
raise AppException(code=ErrorCode.INVALID_STATUS, message=f"当前状态「{order.order_status}」不允许取消", status_code=400)
|
||
|
||
_DIRECT_CANCEL_STATUSES = {"draft", "pending_approve"}
|
||
_APPROVED_CANCEL_STATUSES = {"approved"}
|
||
_FULFILLMENT_CANCEL_STATUSES = {"pending_factory", "pending_driver", "accepted", "picked_up", "delivered", "production", "shipped"}
|
||
|
||
if order.order_status in _DIRECT_CANCEL_STATUSES:
|
||
next_status = "canceled"
|
||
elif order.order_status in _APPROVED_CANCEL_STATUSES:
|
||
next_status = "cancel_pending"
|
||
elif order.order_status in _FULFILLMENT_CANCEL_STATUSES:
|
||
next_status = "cancel_fulfillment_pending"
|
||
else:
|
||
next_status = "cancel_pending"
|
||
previous_status = order.order_status
|
||
self.order_repository.update_cancel_fields(
|
||
session,
|
||
order,
|
||
{
|
||
"order_status": next_status,
|
||
"cancel_requested_by": current_user.get("user_id") if current_user else None,
|
||
"cancel_requested_at": datetime.now(),
|
||
"cancel_reason": payload["cancel_reason"].strip(),
|
||
"cancel_opinion": payload.get("cancel_opinion"),
|
||
"cancel_previous_status": None if next_status == "canceled" else previous_status,
|
||
},
|
||
)
|
||
if next_status == "canceled":
|
||
self.order_repository.clear_cancel_request(session, order)
|
||
self.order_repository.update_order_status(session, order, "canceled")
|
||
audit_service.write_log(
|
||
session,
|
||
{
|
||
"operate_type": "order_cancel",
|
||
"biz_type": "sales_order",
|
||
"biz_id": order.id,
|
||
"before_value": {"order_status": previous_status},
|
||
"after_value": {
|
||
"order_status": order.order_status,
|
||
"cancel_reason": order.cancel_reason,
|
||
"cancel_previous_status": order.cancel_previous_status,
|
||
"cancel_requested_by": order.cancel_requested_by,
|
||
},
|
||
"remark": f"发起订单取消 {order.order_no}",
|
||
},
|
||
)
|
||
self._notify_status_change(session, order, previous_status, order.order_status, current_user.get("user_id") if current_user else None)
|
||
session.commit()
|
||
self._invalidate_order_cache(order.id)
|
||
return {
|
||
"order_id": order.id,
|
||
"order_status": order.order_status,
|
||
"previous_status": previous_status,
|
||
"canceled_at": order.cancel_requested_at.strftime("%Y-%m-%d %H:%M:%S") if order.cancel_requested_at else None,
|
||
}
|
||
except AppException:
|
||
session.rollback()
|
||
raise
|
||
except SQLAlchemyError as exc:
|
||
session.rollback()
|
||
raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库异常", status_code=500) from exc
|
||
return None
|
||
|
||
def approve_order(
|
||
self,
|
||
order_id: int,
|
||
payload: dict,
|
||
session: Session | None = None,
|
||
current_user: dict | None = None,
|
||
) -> dict | None:
|
||
"""审批订单(通过或退回)。
|
||
|
||
仅待审核状态的订单允许审批。通过后进入待下发工厂状态,
|
||
退回后状态恢复为 rejected。驳回时必须填写审批意见。
|
||
自动记录审批日志并通知业务员。
|
||
|
||
Args:
|
||
order_id: 订单 ID
|
||
payload: 包含 approve_result(pass/reject)和 approve_opinion 的请求体
|
||
session: 数据库会话
|
||
current_user: 当前登录用户信息
|
||
|
||
Returns:
|
||
审批结果字典
|
||
|
||
被调用路由: orders.py - POST /orders/{order_id}/approve
|
||
"""
|
||
self._validate_approve_payload(payload)
|
||
|
||
if session is not None:
|
||
try:
|
||
order = self.order_repository.get_order(session, order_id)
|
||
if order is None:
|
||
return None
|
||
self._ensure_order_access(order, current_user)
|
||
self._ensure_status(order.order_status, {"pending_approve"}, "当前状态不允许审批")
|
||
|
||
previous_status = order.order_status
|
||
target_status = "approved" if payload["approve_result"] == "pass" else "rejected"
|
||
self.order_repository.update_order_status(session, order, target_status)
|
||
self.order_approve_log_repository.create_log(
|
||
session,
|
||
{
|
||
"order_id": order.id,
|
||
"approve_type": "order",
|
||
"approve_result": payload["approve_result"],
|
||
"before_status": previous_status,
|
||
"after_status": target_status,
|
||
"approve_opinion": payload.get("approve_opinion"),
|
||
"operator_id": current_user.get("user_id") if current_user else None,
|
||
},
|
||
)
|
||
audit_service.write_log(
|
||
session,
|
||
{
|
||
"operate_type": "order_approve",
|
||
"biz_type": "sales_order",
|
||
"biz_id": order.id,
|
||
"before_value": {"order_status": previous_status},
|
||
"after_value": {
|
||
"order_status": order.order_status,
|
||
"approve_result": payload["approve_result"],
|
||
"approve_opinion": payload.get("approve_opinion"),
|
||
},
|
||
"remark": f"订单审批 {order.order_no}",
|
||
},
|
||
)
|
||
self._notify_status_change(session, order, previous_status, order.order_status, current_user.get("user_id") if current_user else None, {"approve_opinion": payload.get("approve_opinion")})
|
||
session.commit()
|
||
|
||
# 触发审批事件,通知业务员
|
||
if payload["approve_result"] == "pass":
|
||
event_bus.emit("order_approved", {
|
||
"order_id": order.id,
|
||
"order_no": order.order_no,
|
||
"salesman_id": order.salesman_id,
|
||
"biz_type": "sales_order",
|
||
"biz_id": order.id,
|
||
}, session)
|
||
else:
|
||
event_bus.emit("order_rejected", {
|
||
"order_id": order.id,
|
||
"order_no": order.order_no,
|
||
"salesman_id": order.salesman_id,
|
||
"reason": payload.get("approve_opinion", ""),
|
||
"biz_type": "sales_order",
|
||
"biz_id": order.id,
|
||
}, session)
|
||
|
||
self._invalidate_order_cache(order.id)
|
||
return {
|
||
"order_id": order.id,
|
||
"order_status": order.order_status,
|
||
"profit_total": float(order.profit_total or 0),
|
||
"profit_rate": float(order.profit_rate or 0),
|
||
"approve_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
"approve_result": payload["approve_result"],
|
||
"approve_opinion": payload.get("approve_opinion"),
|
||
}
|
||
except AppException:
|
||
session.rollback()
|
||
raise
|
||
except SQLAlchemyError as exc:
|
||
session.rollback()
|
||
raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库异常", status_code=500) from exc
|
||
return None
|
||
|
||
def cancel_approve_order(
|
||
self,
|
||
order_id: int,
|
||
payload: dict,
|
||
session: Session | None = None,
|
||
current_user: dict | None = None,
|
||
) -> dict | None:
|
||
"""审批订单取消申请(通过或驳回)。
|
||
|
||
仅取消审批中或履约取消审批中的订单允许操作。
|
||
通过则订单变为已取消,驳回则恢复为取消前的状态。
|
||
驳回时必须填写审批意见。
|
||
|
||
Args:
|
||
order_id: 订单 ID
|
||
payload: 包含 approve_result(pass/refuse)和 approve_opinion 的请求体
|
||
session: 数据库会话
|
||
current_user: 当前登录用户信息
|
||
|
||
Returns:
|
||
审批结果字典
|
||
|
||
被调用路由: orders.py - POST /orders/{order_id}/cancel-approve
|
||
"""
|
||
self._validate_cancel_approve_payload(payload)
|
||
|
||
if session is not None:
|
||
try:
|
||
order = self.order_repository.get_order(session, order_id)
|
||
if order is None:
|
||
return None
|
||
self._ensure_order_access(order, current_user)
|
||
self._ensure_status(order.order_status, {"cancel_pending", "cancel_fulfillment_pending"}, "当前状态不允许取消审批")
|
||
|
||
previous_status = order.order_status
|
||
target_status = "canceled" if payload["approve_result"] == "pass" else (order.cancel_previous_status or "approved")
|
||
self.order_repository.update_order_status(session, order, target_status)
|
||
if target_status != "canceled":
|
||
self.order_repository.clear_cancel_request(session, order)
|
||
self.order_approve_log_repository.create_log(
|
||
session,
|
||
{
|
||
"order_id": order.id,
|
||
"approve_type": "cancel_order",
|
||
"approve_result": payload["approve_result"],
|
||
"before_status": previous_status,
|
||
"after_status": target_status,
|
||
"approve_opinion": payload.get("approve_opinion"),
|
||
"operator_id": current_user.get("user_id") if current_user else None,
|
||
},
|
||
)
|
||
audit_service.write_log(
|
||
session,
|
||
{
|
||
"operate_type": "order_cancel_approve",
|
||
"biz_type": "sales_order",
|
||
"biz_id": order.id,
|
||
"before_value": {
|
||
"order_status": previous_status,
|
||
"cancel_previous_status": order.cancel_previous_status,
|
||
},
|
||
"after_value": {
|
||
"order_status": order.order_status,
|
||
"approve_result": payload["approve_result"],
|
||
"approve_opinion": payload.get("approve_opinion"),
|
||
},
|
||
"remark": f"订单取消审批 {order.order_no}",
|
||
},
|
||
)
|
||
self._notify_status_change(session, order, previous_status, order.order_status, current_user.get("user_id") if current_user else None)
|
||
session.commit()
|
||
self._invalidate_order_cache(order.id)
|
||
return {
|
||
"order_id": order.id,
|
||
"order_status": order.order_status,
|
||
"approve_result": payload["approve_result"],
|
||
"approve_opinion": payload.get("approve_opinion"),
|
||
}
|
||
except AppException:
|
||
session.rollback()
|
||
raise
|
||
except SQLAlchemyError as exc:
|
||
session.rollback()
|
||
raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库异常", status_code=500) from exc
|
||
return None
|
||
|
||
def get_supplier_text(
|
||
self,
|
||
order_id: int,
|
||
payload: dict,
|
||
session: Session | None = None,
|
||
current_user: dict | None = None,
|
||
) -> dict | None:
|
||
"""生成发厂文案。
|
||
|
||
根据订单明细和供应商模板类型生成文本,仅已通过状态的订单允许操作。
|
||
|
||
Args:
|
||
order_id: 订单 ID
|
||
payload: 包含 supplier_id 的请求体(可选)
|
||
session: 数据库会话
|
||
current_user: 当前登录用户信息
|
||
|
||
Returns:
|
||
包含 text_content 的发厂文案字典
|
||
|
||
被调用路由: orders.py - POST /orders/{order_id}/supplier-text
|
||
"""
|
||
if session is not None:
|
||
try:
|
||
order = self.order_repository.get_order(session, order_id)
|
||
if order is None:
|
||
return None
|
||
self._ensure_order_access(order, current_user)
|
||
self._ensure_status(order.order_status, {"approved"}, "当前状态不允许生成发厂文案")
|
||
|
||
supplier_id = payload.get("supplier_id") or order.factory_id
|
||
if not supplier_id:
|
||
raise AppException(code=ErrorCode.NOT_FOUND, message="订单未绑定工厂", status_code=404)
|
||
|
||
supplier = self.order_repository.get_supplier(session, supplier_id)
|
||
if supplier is None:
|
||
raise AppException(code=ErrorCode.NOT_FOUND, message="工厂不存在", status_code=404)
|
||
|
||
items = self.order_repository.list_order_items(session, order.id)
|
||
template_type = supplier.template_type or "default"
|
||
text_content = self._build_supplier_text(order, items, template_type)
|
||
return {
|
||
"order_id": order.id,
|
||
"supplier_id": supplier.id,
|
||
"template_type": template_type,
|
||
"text_content": text_content,
|
||
}
|
||
except SQLAlchemyError as exc:
|
||
raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库异常", status_code=500) from exc
|
||
return None
|
||
|
||
def confirm_supplier_text(
|
||
self,
|
||
order_id: int,
|
||
payload: dict,
|
||
session: Session | None = None,
|
||
current_user: dict | None = None,
|
||
) -> dict | None:
|
||
"""确认发厂文案。
|
||
|
||
确认后订单状态从 approved 变更为 pending_factory,进入履约阶段。
|
||
自动记录发厂文案日志和审计日志。
|
||
|
||
Args:
|
||
order_id: 订单 ID
|
||
payload: 包含 text_content、supplier_id 等的请求体
|
||
session: 数据库会话
|
||
current_user: 当前登录用户信息
|
||
|
||
Returns:
|
||
确认结果字典
|
||
|
||
被调用路由: orders.py - POST /orders/{order_id}/confirm-supplier-text
|
||
"""
|
||
if not (payload.get("text_content") or "").strip():
|
||
raise AppException(code=ErrorCode.PARAM_ERROR, message="发厂文案不能为空", status_code=400)
|
||
|
||
if session is not None:
|
||
try:
|
||
order = self.order_repository.get_order(session, order_id)
|
||
if order is None:
|
||
return None
|
||
self._ensure_order_access(order, current_user)
|
||
self._ensure_status(order.order_status, {"approved"}, "当前状态不允许确认发厂")
|
||
|
||
supplier_id = payload.get("supplier_id") or order.factory_id
|
||
if not supplier_id:
|
||
raise AppException(code=ErrorCode.NOT_FOUND, message="订单未绑定工厂", status_code=404)
|
||
|
||
supplier = self.order_repository.get_supplier(session, supplier_id)
|
||
if supplier is None:
|
||
raise AppException(code=ErrorCode.NOT_FOUND, message="工厂不存在", status_code=404)
|
||
|
||
before_status = order.order_status
|
||
self.order_repository.update_supplier_text_confirm(session, order, current_user.get("user_id") if current_user else None)
|
||
self._record_supplier_text_log(session, order, {**payload, "supplier_id": supplier_id}, current_user)
|
||
audit_service.write_log(
|
||
session,
|
||
{
|
||
"operate_type": "order_supplier_text_confirm",
|
||
"biz_type": "sales_order",
|
||
"biz_id": order.id,
|
||
"before_value": {"order_status": before_status},
|
||
"after_value": {
|
||
"order_status": order.order_status,
|
||
"supplier_id": supplier_id,
|
||
"text_content": payload["text_content"],
|
||
"remark": payload.get("remark"),
|
||
},
|
||
"remark": f"确认发厂文案 {order.order_no}",
|
||
},
|
||
)
|
||
session.commit()
|
||
|
||
# 触发订单发厂事件,通知业务员
|
||
event_bus.emit("order_shipped", {
|
||
"order_id": order.id,
|
||
"order_no": order.order_no,
|
||
"salesman_id": order.salesman_id,
|
||
"supplier_name": supplier.supplier_name if supplier else "",
|
||
"biz_type": "sales_order",
|
||
"biz_id": order.id,
|
||
}, session)
|
||
|
||
self._invalidate_order_cache(order.id)
|
||
return {
|
||
"order_id": order.id,
|
||
"order_status": order.order_status,
|
||
"confirmed_at": order.supplier_text_confirmed_at.strftime("%Y-%m-%d %H:%M:%S")
|
||
if order.supplier_text_confirmed_at
|
||
else datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
}
|
||
except AppException:
|
||
session.rollback()
|
||
raise
|
||
except SQLAlchemyError as exc:
|
||
session.rollback()
|
||
raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库异常", status_code=500) from exc
|
||
return None
|
||
|
||
def change_order_status(
|
||
self,
|
||
order_id: int,
|
||
payload: dict,
|
||
session: Session | None = None,
|
||
current_user: dict | None = None,
|
||
) -> dict | None:
|
||
"""手动变更订单状态。
|
||
|
||
校验状态流转合法性(基于 _VALID_STATUS_TRANSITIONS),同步欠款数据。
|
||
|
||
Args:
|
||
order_id: 订单 ID
|
||
payload: 包含 target_status 的请求体
|
||
session: 数据库会话
|
||
current_user: 当前登录用户信息
|
||
|
||
Returns:
|
||
状态变更结果字典
|
||
|
||
被调用路由: orders.py - POST /orders/{order_id}/status
|
||
"""
|
||
if session is not None:
|
||
try:
|
||
order = self.order_repository.get_order(session, order_id)
|
||
if order is None:
|
||
return None
|
||
self._ensure_order_access(order, current_user)
|
||
|
||
target_status = (payload.get("target_status") or "").strip()
|
||
if not target_status:
|
||
raise AppException(code=ErrorCode.PARAM_ERROR, message="目标状态不能为空", status_code=400)
|
||
|
||
allowed_statuses = {
|
||
"approved",
|
||
"pending_factory",
|
||
"pending_driver",
|
||
"accepted",
|
||
"picked_up",
|
||
"delivered",
|
||
"production",
|
||
"shipped",
|
||
"completed",
|
||
"settled",
|
||
"canceled",
|
||
}
|
||
if target_status not in allowed_statuses:
|
||
raise AppException(code=ErrorCode.PARAM_ERROR, message="目标状态不支持", status_code=400)
|
||
|
||
self._validate_status_transition(order.order_status, target_status)
|
||
|
||
before_status = order.order_status
|
||
self.order_repository.update_order_status(session, order, target_status)
|
||
arrears_service.sync_order_arrears(session, order)
|
||
audit_service.write_log(
|
||
session,
|
||
{
|
||
"operate_type": "order_status_change",
|
||
"biz_type": "sales_order",
|
||
"biz_id": order.id,
|
||
"before_value": {"order_status": before_status},
|
||
"after_value": {"order_status": order.order_status},
|
||
"remark": payload.get("remark") or f"订单状态变更为 {target_status}",
|
||
},
|
||
)
|
||
self._notify_status_change(session, order, before_status, order.order_status, current_user.get("user_id") if current_user else None)
|
||
session.commit()
|
||
self._invalidate_order_cache(order.id)
|
||
return {
|
||
"order_id": order.id,
|
||
"order_status": order.order_status,
|
||
}
|
||
except AppException:
|
||
session.rollback()
|
||
raise
|
||
except SQLAlchemyError as exc:
|
||
session.rollback()
|
||
raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库异常", status_code=500) from exc
|
||
return None
|
||
|
||
def create_order(
|
||
self,
|
||
payload: dict,
|
||
session: Session | None = None,
|
||
current_user: dict | None = None,
|
||
) -> dict:
|
||
"""创建新订单。
|
||
|
||
若客户不存在会自动创建(基于姓名+手机号匹配)。
|
||
生成唯一订单号(SO + 时间戳),计算销售/成本/利润汇总。
|
||
初始状态为 pending_approve,直接进入审批流程。
|
||
|
||
Args:
|
||
payload: 包含客户信息、订单明细、费用等的请求体
|
||
session: 数据库会话
|
||
current_user: 当前登录用户信息
|
||
|
||
Returns:
|
||
创建结果字典,包含订单号、状态和汇总金额
|
||
|
||
被调用路由: orders.py - POST /orders
|
||
"""
|
||
if session is not None:
|
||
try:
|
||
if not payload["customer_name"].strip():
|
||
raise AppException(code=ErrorCode.PARAM_ERROR, message="客户姓名不能为空", status_code=400)
|
||
if not payload["customer_mobile"].strip():
|
||
raise AppException(code=ErrorCode.PARAM_ERROR, message="客户手机号不能为空", status_code=400)
|
||
|
||
customer = self.customer_repository.find_by_name_and_mobile(
|
||
session,
|
||
payload["customer_name"],
|
||
payload["customer_mobile"],
|
||
)
|
||
if customer is None:
|
||
customer = self.customer_repository.create_customer(
|
||
session,
|
||
{
|
||
"customer_name": payload["customer_name"],
|
||
"mobile": payload["customer_mobile"],
|
||
"address": payload.get("customer_address"),
|
||
"settlement_type": None,
|
||
"settlement_days": 0,
|
||
"customer_type": None,
|
||
"salesman_id": current_user.get("user_id") if current_user else payload.get("salesman_id"),
|
||
"credit_limit": 0,
|
||
"remark": "订单录入时自动创建",
|
||
},
|
||
)
|
||
|
||
items = payload["items"]
|
||
if not items:
|
||
raise AppException(code=ErrorCode.PARAM_ERROR, message="订单明细不能为空", status_code=400)
|
||
|
||
sale_total = sum(item["quantity"] * item["sale_price"] for item in items)
|
||
cost_total = sum(item["quantity"] * item["cost_price"] for item in items)
|
||
profit_total = (
|
||
sale_total
|
||
- cost_total
|
||
- payload["rebate_total"]
|
||
- payload["freight_total"]
|
||
- payload["tax_total"]
|
||
- payload["other_fee_total"]
|
||
)
|
||
profit_rate = round((profit_total / sale_total) * 100, 2) if sale_total else 0
|
||
|
||
order = self.order_repository.create_order(
|
||
session,
|
||
{
|
||
"order_no": f"SO{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||
"customer_id": customer.id,
|
||
"customer_name": payload["customer_name"],
|
||
"customer_mobile": payload["customer_mobile"],
|
||
"customer_address": payload.get("customer_address"),
|
||
"salesman_id": current_user.get("user_id") if current_user else payload.get("salesman_id"),
|
||
"order_status": "pending_approve",
|
||
"order_source": payload.get("order_source"),
|
||
"delivery_type": payload.get("delivery_type"),
|
||
"factory_id": payload.get("factory_id"),
|
||
"sale_price_total": sale_total,
|
||
"cost_price_total": cost_total,
|
||
"rebate_total": payload["rebate_total"],
|
||
"freight_total": payload["freight_total"],
|
||
"tax_total": payload["tax_total"],
|
||
"other_fee_total": payload["other_fee_total"],
|
||
"profit_total": profit_total,
|
||
"profit_rate": profit_rate,
|
||
"commission_amount": payload["commission_amount"],
|
||
"payment_method": payload.get("payment_method"),
|
||
"tax_amount": payload.get("tax_amount", 0),
|
||
"remark": payload.get("remark"),
|
||
},
|
||
items,
|
||
)
|
||
audit_service.write_log(
|
||
session,
|
||
{
|
||
"operate_type": "order_create",
|
||
"biz_type": "sales_order",
|
||
"biz_id": order.id,
|
||
"before_value": None,
|
||
"after_value": {
|
||
"order_no": order.order_no,
|
||
"order_status": order.order_status,
|
||
"sale_price_total": round(sale_total, 2),
|
||
"cost_price_total": round(cost_total, 2),
|
||
"profit_total": round(profit_total, 2),
|
||
"profit_rate": profit_rate,
|
||
"commission_amount": payload["commission_amount"],
|
||
},
|
||
"remark": f"创建订单 {order.order_no}",
|
||
},
|
||
)
|
||
session.commit()
|
||
|
||
# 触发订单创建事件,通知管理层审批
|
||
salesman_name = ""
|
||
if current_user:
|
||
salesman_name = current_user.get("real_name", "")
|
||
event_bus.emit("order_created", {
|
||
"order_id": order.id,
|
||
"order_no": order.order_no,
|
||
"salesman_id": order.salesman_id,
|
||
"salesman_name": salesman_name,
|
||
"customer_name": payload["customer_name"],
|
||
"amount": round(sale_total, 2),
|
||
"biz_type": "sales_order",
|
||
"biz_id": order.id,
|
||
}, session)
|
||
|
||
self._invalidate_order_cache(order.id)
|
||
return {
|
||
"order_id": order.id,
|
||
"order_no": order.order_no,
|
||
"order_status": order.order_status,
|
||
"sale_price_total": round(sale_total, 2),
|
||
"cost_price_total": round(cost_total, 2),
|
||
"profit_total": round(profit_total, 2),
|
||
"profit_rate": profit_rate,
|
||
"commission_amount": payload["commission_amount"],
|
||
"payment_method": payload.get("payment_method"),
|
||
"tax_amount": payload.get("tax_amount", 0),
|
||
}
|
||
except AppException:
|
||
session.rollback()
|
||
raise
|
||
except SQLAlchemyError as exc:
|
||
session.rollback()
|
||
raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库异常", status_code=500) from exc
|
||
raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库连接不可用", status_code=500)
|
||
|
||
def normalize_list_filters(self, filters: dict | None, current_user: dict | None = None) -> dict:
|
||
"""标准化订单查询过滤条件。
|
||
|
||
对业务员角色自动注入 salesman_id,确保只能查看自己的订单。
|
||
|
||
Args:
|
||
filters: 原始过滤条件
|
||
current_user: 当前登录用户信息
|
||
|
||
Returns:
|
||
标准化后的过滤条件字典
|
||
|
||
被调用路由: orders.py - GET /orders
|
||
"""
|
||
normalized = dict(filters or {})
|
||
if not current_user:
|
||
return normalized
|
||
if current_user.get("role_code") == "salesman":
|
||
normalized["salesman_id"] = current_user.get("user_id")
|
||
return normalized
|
||
|
||
_STATUS_LABELS = {
|
||
"draft": "草稿",
|
||
"pending_approve": "待审核",
|
||
"approved": "已通过",
|
||
"rejected": "已退回",
|
||
"pending_factory": "待下发工厂",
|
||
"pending_driver": "待司机接单",
|
||
"accepted": "已接单",
|
||
"picked_up": "已揽货",
|
||
"delivered": "已送达",
|
||
"canceled": "已取消",
|
||
"cancel_pending": "取消审批中",
|
||
"cancel_fulfillment_pending": "履约取消审批中",
|
||
"production": "生产中",
|
||
"shipped": "已发货",
|
||
"completed": "已完成",
|
||
"settled": "已结算",
|
||
}
|
||
|
||
def _notify_status_change(self, session: Session, order, before_status: str, after_status: str, operator_id: int | None, extra_info: dict | None = None) -> None:
|
||
"""订单状态变更时生成提醒通知。
|
||
|
||
向业务员发送状态变更通知,待审核/取消审批类状态额外通知所有经理。
|
||
|
||
Args:
|
||
session: 数据库会话
|
||
order: 订单 ORM 对象
|
||
before_status: 变更前状态
|
||
after_status: 变更后状态
|
||
operator_id: 操作人 ID
|
||
extra_info: 额外信息(如审批意见)
|
||
"""
|
||
before_label = self._STATUS_LABELS.get(before_status, before_status)
|
||
after_label = self._STATUS_LABELS.get(after_status, after_status)
|
||
_NOTIFY_MANAGER_STATUSES = {"pending_approve", "cancel_pending", "cancel_fulfillment_pending"}
|
||
_NOTIFY_SALESMAN_STATUSES = {"approved", "rejected", "canceled", "delivered", "completed", "settled"}
|
||
|
||
receiver = order.salesman_id or 0
|
||
if receiver > 0 and after_status in _NOTIFY_SALESMAN_STATUSES | _NOTIFY_MANAGER_STATUSES:
|
||
reminder_content = f"订单 {order.order_no} 状态由「{before_label}」变更为「{after_label}」。"
|
||
if after_status == "rejected" and extra_info and extra_info.get("approve_opinion"):
|
||
reminder_content += f" 退回原因:{extra_info['approve_opinion']}"
|
||
self.reminder_repository.create_reminder(
|
||
session,
|
||
{
|
||
"reminder_type": "order_status_change",
|
||
"biz_type": "sales_order",
|
||
"biz_id": order.id,
|
||
"receiver_user_id": receiver,
|
||
"reminder_title": f"订单状态变更 - {order.order_no}",
|
||
"reminder_content": reminder_content,
|
||
"status": "pending",
|
||
"sent_at": datetime.now(),
|
||
},
|
||
)
|
||
|
||
if after_status in _NOTIFY_MANAGER_STATUSES:
|
||
managers = self._find_manager_user_ids(session)
|
||
for manager_id in managers:
|
||
self.reminder_repository.create_reminder(
|
||
session,
|
||
{
|
||
"reminder_type": "order_approval_needed",
|
||
"biz_type": "sales_order",
|
||
"biz_id": order.id,
|
||
"receiver_user_id": manager_id,
|
||
"reminder_title": f"订单待审批 - {order.order_no}",
|
||
"reminder_content": f"订单 {order.order_no}(客户:{order.customer_name})需要审批处理。",
|
||
"status": "pending",
|
||
"sent_at": datetime.now(),
|
||
},
|
||
)
|
||
|
||
# 推送 WebSocket 实时通知
|
||
event_bus.emit("order_status_changed", {
|
||
"order_no": order.order_no,
|
||
"salesman_id": order.salesman_id or 0,
|
||
"operator_id": operator_id or 0,
|
||
})
|
||
|
||
def _find_manager_user_ids(self, session: Session) -> list[int]:
|
||
"""查询所有经理角色的用户 ID 列表。
|
||
|
||
Args:
|
||
session: 数据库会话
|
||
|
||
Returns:
|
||
经理用户 ID 列表,查询失败时返回空列表
|
||
"""
|
||
try:
|
||
from backend.app.models.system import SysUser, SysRole
|
||
from sqlalchemy import select
|
||
stmt = (
|
||
select(SysUser.id)
|
||
.join(SysRole, SysRole.id == SysUser.role_id)
|
||
.where(SysRole.role_code == "manager", SysUser.status == 1, SysUser.deleted == 0)
|
||
)
|
||
return list(session.execute(stmt).scalars())
|
||
except Exception:
|
||
return []
|
||
|
||
def _ensure_order_access(self, order: object, current_user: dict | None, owner_only: bool = False) -> None:
|
||
"""校验当前用户是否有权访问指定订单。
|
||
|
||
管理员和经理可访问所有订单,业务员只能访问自己负责的订单。
|
||
|
||
Args:
|
||
order: 订单 ORM 对象
|
||
current_user: 当前登录用户信息
|
||
owner_only: 是否仅允许所有者访问(当前未使用)
|
||
|
||
Raises:
|
||
AppException: 无权限时抛出 FORBIDDEN
|
||
"""
|
||
if not current_user:
|
||
return
|
||
role_code = current_user.get("role_code")
|
||
if role_code in {"admin", "manager"}:
|
||
return
|
||
if role_code == "salesman":
|
||
if order.salesman_id != current_user.get("user_id"):
|
||
raise AppException(code=ErrorCode.FORBIDDEN, message="无权限访问他人订单", status_code=403)
|
||
return
|
||
raise AppException(code=ErrorCode.FORBIDDEN, message="无权限访问", status_code=403)
|
||
|
||
def _filter_order_detail_by_role(self, order: dict, current_user: dict | None) -> dict:
|
||
"""根据用户角色过滤订单详情中的敏感字段。
|
||
|
||
司机只能看到物流相关信息,业务员看不到成本和利润信息。
|
||
|
||
Args:
|
||
order: 订单详情字典
|
||
current_user: 当前登录用户信息
|
||
|
||
Returns:
|
||
过滤后的订单详情字典
|
||
"""
|
||
if not current_user:
|
||
return order
|
||
role_code = current_user.get("role_code")
|
||
if role_code == "driver":
|
||
filtered = dict(order)
|
||
filtered["customer_name"] = None
|
||
filtered["customer_mobile"] = None
|
||
filtered["customer_address"] = None
|
||
filtered["salesman_id"] = None
|
||
filtered["salesman_name"] = ""
|
||
filtered["sale_price_total"] = None
|
||
filtered["cost_price_total"] = None
|
||
filtered["rebate_total"] = None
|
||
filtered["freight_total"] = None
|
||
filtered["tax_total"] = None
|
||
filtered["other_fee_total"] = None
|
||
filtered["profit_total"] = None
|
||
filtered["profit_rate"] = None
|
||
filtered["commission_amount"] = None
|
||
filtered["order_source"] = None
|
||
filtered["items"] = []
|
||
filtered["approve_logs"] = []
|
||
filtered["remark"] = None
|
||
return filtered
|
||
if role_code == "salesman":
|
||
filtered = dict(order)
|
||
filtered["cost_price_total"] = None
|
||
filtered["rebate_total"] = None
|
||
filtered["profit_total"] = None
|
||
filtered["profit_rate"] = None
|
||
filtered["calculation_detail"] = None
|
||
filtered["items"] = [
|
||
{
|
||
**item,
|
||
"cost_price": None,
|
||
"rebate_amount": None,
|
||
}
|
||
for item in order.get("items", [])
|
||
]
|
||
return filtered
|
||
return order
|
||
|
||
def _invalidate_order_cache(self, order_id: int | None = None) -> None:
|
||
"""清除订单相关缓存。"""
|
||
if order_id:
|
||
cache_delete(f"order:detail:{order_id}")
|
||
cache_delete_pattern("order:list:*")
|
||
cache_delete_pattern("dashboard:*")
|
||
|
||
def _filter_order_row(self, row: dict, role_code: str | None) -> dict:
|
||
"""根据用户角色过滤订单列表行中的敏感字段。
|
||
|
||
业务员看不到利润和佣金字段。
|
||
|
||
Args:
|
||
row: 订单行数据字典
|
||
role_code: 用户角色编码
|
||
|
||
Returns:
|
||
过滤后的订单行数据字典
|
||
"""
|
||
if role_code == "salesman":
|
||
filtered = dict(row)
|
||
filtered["profit_total"] = None
|
||
filtered["commission_amount"] = None
|
||
return filtered
|
||
return row
|
||
|
||
# 需求 6.1 定义的合法状态流转路径
|
||
_VALID_STATUS_TRANSITIONS = {
|
||
"draft": {"pending_approve", "canceled"},
|
||
"pending_approve": {"approved", "rejected", "canceled"},
|
||
"rejected": {"draft", "pending_approve"},
|
||
"approved": {"pending_factory", "cancel_pending", "canceled"},
|
||
"pending_factory": {"pending_driver", "production", "cancel_fulfillment_pending", "cancel_pending"},
|
||
"pending_driver": {"accepted", "cancel_fulfillment_pending"},
|
||
"accepted": {"picked_up", "cancel_fulfillment_pending"},
|
||
"picked_up": {"delivered", "cancel_fulfillment_pending"},
|
||
"delivered": {"completed", "cancel_fulfillment_pending"},
|
||
"production": {"shipped", "cancel_fulfillment_pending"},
|
||
"shipped": {"delivered", "completed", "cancel_fulfillment_pending"},
|
||
"completed": {"settled"},
|
||
"settled": set(),
|
||
"canceled": set(),
|
||
"cancel_pending": {"canceled"},
|
||
"cancel_fulfillment_pending": {"canceled"},
|
||
}
|
||
|
||
def _validate_status_transition(self, from_status: str, to_status: str) -> None:
|
||
"""校验订单状态流转是否合法。
|
||
|
||
基于 _VALID_STATUS_TRANSITIONS 白名单校验。
|
||
|
||
Args:
|
||
from_status: 当前状态
|
||
to_status: 目标状态
|
||
|
||
Raises:
|
||
AppException: 非法流转时抛出 INVALID_STATUS
|
||
"""
|
||
allowed = self._VALID_STATUS_TRANSITIONS.get(from_status, set())
|
||
if to_status not in allowed:
|
||
raise AppException(
|
||
code=ErrorCode.INVALID_STATUS,
|
||
message=f"不允许从「{from_status}」变更为「{to_status}」",
|
||
status_code=400,
|
||
)
|
||
|
||
def _ensure_status(self, current_status: str, allowed_statuses: set[str], message: str) -> None:
|
||
"""校验订单当前状态是否在允许的操作状态集合中。
|
||
|
||
Args:
|
||
current_status: 当前订单状态
|
||
allowed_statuses: 允许的操作状态集合
|
||
message: 不满足时的错误提示信息
|
||
|
||
Raises:
|
||
AppException: 状态不匹配时抛出 INVALID_STATUS
|
||
"""
|
||
if current_status not in allowed_statuses:
|
||
raise AppException(code=ErrorCode.INVALID_STATUS, message=message, status_code=400)
|
||
|
||
def _get_config_value(self, session: Session, config_key: str, default: str) -> str:
|
||
"""从系统配置表中读取配置值。
|
||
|
||
Args:
|
||
session: 数据库会话
|
||
config_key: 配置键名
|
||
default: 默认值
|
||
|
||
Returns:
|
||
配置值或默认值
|
||
"""
|
||
config = self.config_repository.get_by_key(session, config_key)
|
||
return config.config_value if config is not None and config.config_value else default
|
||
|
||
def _validate_cancel_payload(self, payload: dict) -> None:
|
||
"""校验取消订单的请求参数。
|
||
|
||
Args:
|
||
payload: 请求参数
|
||
|
||
Raises:
|
||
AppException: 取消原因为空时抛出 PARAM_ERROR
|
||
"""
|
||
if not (payload.get("cancel_reason") or "").strip():
|
||
raise AppException(code=ErrorCode.PARAM_ERROR, message="取消原因不能为空", status_code=400)
|
||
|
||
def _validate_approve_payload(self, payload: dict) -> None:
|
||
"""校验订单审批的请求参数。
|
||
|
||
审批结果仅支持 pass 或 reject,驳回时必须填写意见。
|
||
|
||
Args:
|
||
payload: 请求参数
|
||
|
||
Raises:
|
||
AppException: 参数不合法时抛出 PARAM_ERROR
|
||
"""
|
||
approve_result = (payload.get("approve_result") or "").strip()
|
||
if approve_result not in {"pass", "reject"}:
|
||
raise AppException(code=ErrorCode.PARAM_ERROR, message="审批结果仅支持 pass 或 reject", status_code=400)
|
||
if approve_result == "reject" and not (payload.get("approve_opinion") or "").strip():
|
||
raise AppException(code=ErrorCode.PARAM_ERROR, message="审批驳回时必须填写意见", status_code=400)
|
||
|
||
def _validate_cancel_approve_payload(self, payload: dict) -> None:
|
||
"""校验取消审批的请求参数。
|
||
|
||
审批结果仅支持 pass 或 refuse,驳回时必须填写意见。
|
||
|
||
Args:
|
||
payload: 请求参数
|
||
|
||
Raises:
|
||
AppException: 参数不合法时抛出 PARAM_ERROR
|
||
"""
|
||
approve_result = (payload.get("approve_result") or "").strip()
|
||
if approve_result not in {"pass", "refuse"}:
|
||
raise AppException(code=ErrorCode.PARAM_ERROR, message="取消审批结果仅支持 pass 或 refuse", status_code=400)
|
||
if approve_result == "refuse" and not (payload.get("approve_opinion") or "").strip():
|
||
raise AppException(code=ErrorCode.PARAM_ERROR, message="取消审批驳回时必须填写意见", status_code=400)
|
||
|
||
def _build_calculation_detail(self, order, items) -> dict:
|
||
"""构建订单利润计算明细。
|
||
|
||
包含公式说明、各费用汇总和每条明细的金额拆分,支持报价引擎快照数据。
|
||
|
||
Args:
|
||
order: 订单 ORM 对象
|
||
items: 订单明细 ORM 对象列表
|
||
|
||
Returns:
|
||
包含计算公式、汇总金额和明细的字典
|
||
"""
|
||
sale_total = float(order.sale_price_total or 0)
|
||
cost_total = float(order.cost_price_total or 0)
|
||
rebate_total = float(order.rebate_total or 0)
|
||
freight_total = float(order.freight_total or 0)
|
||
tax_total = float(order.tax_total or 0)
|
||
other_fee_total = float(order.other_fee_total or 0)
|
||
profit_total = float(order.profit_total or 0)
|
||
profit_rate = float(order.profit_rate or 0)
|
||
|
||
item_details = []
|
||
for item in items:
|
||
qty = float(item.quantity or 0)
|
||
sale_price = float(item.sale_price or 0)
|
||
cost_price = float(item.cost_price or 0)
|
||
item_sale_amount = round(qty * sale_price, 2)
|
||
item_cost_amount = round(qty * cost_price, 2)
|
||
detail_row = {
|
||
"product_name": item.product_name,
|
||
"quantity": qty,
|
||
"sale_price": sale_price,
|
||
"cost_price": cost_price,
|
||
"sale_amount": item_sale_amount,
|
||
"cost_amount": item_cost_amount,
|
||
"tax_amount": float(item.tax_amount or 0),
|
||
}
|
||
# 报价引擎快照
|
||
pricing_type = getattr(item, "pricing_type", None)
|
||
if pricing_type:
|
||
detail_row["pricing_type"] = pricing_type
|
||
detail_row["length_m"] = float(item.length_m) if getattr(item, "length_m", None) else None
|
||
detail_row["width_m"] = float(item.width_m) if getattr(item, "width_m", None) else None
|
||
detail_row["area_sqm"] = float(item.area_sqm) if getattr(item, "area_sqm", None) else None
|
||
surcharge_detail = getattr(item, "surcharge_detail", None)
|
||
if surcharge_detail:
|
||
try:
|
||
detail_row["surcharge_items"] = json.loads(surcharge_detail)
|
||
except (json.JSONDecodeError, TypeError):
|
||
detail_row["surcharge_items"] = []
|
||
detail_row["supplier_model"] = getattr(item, "supplier_model", None)
|
||
detail_row["price_tier"] = getattr(item, "price_tier", None)
|
||
item_details.append(detail_row)
|
||
|
||
# 税计算说明
|
||
tax_detail = None
|
||
if tax_total > 0:
|
||
# 反推税率:tax_total / (sale_total - tax_total) 近似
|
||
base_for_tax = sale_total - tax_total if sale_total > tax_total else sale_total
|
||
approx_rate = round(tax_total / base_for_tax * 100, 2) if base_for_tax else 0
|
||
tax_detail = {
|
||
"tax_total": tax_total,
|
||
"approx_tax_rate": approx_rate,
|
||
"desc": f"税费 ¥{tax_total:.2f}(约占销售额 {approx_rate}%)",
|
||
}
|
||
|
||
return {
|
||
"formula": "利润 = 销售价 - 成本价 - 回扣 - 运费 - 税费 - 其他费用",
|
||
"rate_formula": "利润率 = 利润 / 销售价 x 100%",
|
||
"sale_total": sale_total,
|
||
"cost_total": cost_total,
|
||
"rebate_total": rebate_total,
|
||
"freight_total": freight_total,
|
||
"tax_total": tax_total,
|
||
"tax_detail": tax_detail,
|
||
"other_fee_total": other_fee_total,
|
||
"profit_total": profit_total,
|
||
"profit_rate": profit_rate,
|
||
"item_details": item_details,
|
||
}
|
||
|
||
def _build_task_summary(self, task) -> dict:
|
||
"""构建物流任务摘要字典。
|
||
|
||
Args:
|
||
task: 物流任务 ORM 对象,可为 None
|
||
|
||
Returns:
|
||
任务摘要字典,task 为 None 时返回空字典
|
||
"""
|
||
if task is None:
|
||
return {}
|
||
return {
|
||
"task_id": task.id,
|
||
"task_no": task.task_no,
|
||
"driver_id": task.driver_id,
|
||
"factory_id": task.factory_id,
|
||
"pickup_address": task.pickup_address,
|
||
"delivery_address": task.delivery_address,
|
||
"pickup_content": task.pickup_content,
|
||
"quantity": float(task.quantity or 0),
|
||
"status": task.status,
|
||
"remark": task.remark,
|
||
"created_at": task.created_at.strftime("%Y-%m-%d %H:%M:%S") if task.created_at else "",
|
||
}
|
||
|
||
def _build_attachment_row(self, attachment) -> dict:
|
||
"""构建附件行数据字典。
|
||
|
||
Args:
|
||
attachment: 附件 ORM 对象
|
||
|
||
Returns:
|
||
附件摘要字典
|
||
"""
|
||
return {
|
||
"attachment_id": attachment.id,
|
||
"biz_type": attachment.biz_type,
|
||
"biz_id": attachment.biz_id,
|
||
"file_name": attachment.file_name,
|
||
"file_url": attachment.file_url,
|
||
"file_type": attachment.file_type,
|
||
"file_size": attachment.file_size,
|
||
"file_category": attachment.file_category,
|
||
"object_key": attachment.object_key,
|
||
"created_at": attachment.created_at.strftime("%Y-%m-%d %H:%M:%S") if attachment.created_at else "",
|
||
}
|
||
|
||
def _record_supplier_text_log(self, session: Session, order, payload: dict, current_user: dict | None) -> None:
|
||
"""记录发厂文案确认日志。
|
||
|
||
Args:
|
||
session: 数据库会话
|
||
order: 订单 ORM 对象
|
||
payload: 包含 supplier_id、text_content 等的请求体
|
||
current_user: 当前登录用户信息
|
||
"""
|
||
self.order_repository.create_supplier_text_log(
|
||
session,
|
||
{
|
||
"order_id": order.id,
|
||
"supplier_id": payload["supplier_id"],
|
||
"template_type": payload.get("template_type") or "default",
|
||
"text_content": payload["text_content"],
|
||
"confirmed": 1,
|
||
"confirmed_by": current_user.get("user_id") if current_user else None,
|
||
"confirmed_at": datetime.now(),
|
||
"created_by": current_user.get("user_id") if current_user else None,
|
||
},
|
||
)
|
||
|
||
def _build_supplier_text(self, order, items, template_type: str) -> str:
|
||
"""根据模板类型生成发厂文案文本。
|
||
|
||
支持 default(完整模板)和 brief(简略模板)两种格式。
|
||
|
||
Args:
|
||
order: 订单 ORM 对象
|
||
items: 订单明细 ORM 对象列表
|
||
template_type: 模板类型(default 或 brief)
|
||
|
||
Returns:
|
||
格式化后的发厂文案字符串
|
||
"""
|
||
item_lines = []
|
||
for item in items:
|
||
qty = float(item.quantity or 0)
|
||
spec = item.specification or ""
|
||
unit = item.unit or ""
|
||
item_lines.append(f" {item.product_name} {spec} {qty}{unit}")
|
||
items_text = "\n".join(item_lines) if item_lines else " (无明细)"
|
||
|
||
if template_type == "brief":
|
||
total_qty = sum(float(item.quantity or 0) for item in items)
|
||
return f"订单 {order.order_no}\n客户:{order.customer_name}\n总数量:{total_qty}\n请安排发货。"
|
||
|
||
return (
|
||
f"订单编号:{order.order_no}\n"
|
||
f"客户:{order.customer_name}\n"
|
||
f"发货方式:{order.delivery_type or '待定'}\n"
|
||
f"产品明细:\n{items_text}\n"
|
||
f"备注:{order.remark or '无'}\n"
|
||
f"请安排生产与发货。"
|
||
)
|
||
|
||
|
||
order_service = OrderService()
|