""" 订单管理路由模块 职责: 处理订单的全生命周期管理接口,URL 前缀为 /api/orders。 包括:订单列表查询、创建、更新、详情查看、提交、取消、 状态变更、审批、撤销审批、供应商文本发送与确认。 """ from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session from backend.app.api.deps import get_order_service, require_permissions, require_roles from backend.app.core.error_codes import ErrorCode from backend.app.core.exceptions import AppException from backend.app.db import get_db_session from backend.app.schemas.common import success_payload from backend.app.schemas.orders import ( ApproveOrderRequest, BatchMatchConfirmRequest, BatchMatchLogisticsRequest, CancelOrderRequest, ChangeOrderStatusRequest, ConfirmSupplierTextRequest, CreateOrderRequest, ReviseOrderRequest, SupplierTextRequest, UpdateOrderRequest, UpdateOrderTrackingRequest, ) from backend.app.services.order_service import OrderService router = APIRouter(prefix="/api/orders", tags=["orders"]) @router.get("") def list_orders( order_no: str | None = Query(default=None), # 订单编号模糊搜索 order_status: str | None = Query(default=None), # 订单状态筛选 customer_name: str | None = Query(default=None), # 客户名称模糊搜索 customer_mobile: str | None = Query(default=None), # 客户手机号搜索 salesman_id: int | None = Query(default=None), # 业务员 ID 筛选 factory_id: int | None = Query(default=None), # 工厂 ID 筛选 order_source: str | None = Query(default=None), # 订单来源筛选 order_type: str | None = Query(default=None), # 订单类型筛选(industry/daily) need_invoice: int | None = Query(default=None), # 是否需要发票筛选 start_time: str | None = Query(default=None), # 查询起始时间 end_time: str | None = Query(default=None), # 查询结束时间 page_no: int = Query(default=1), # 页码,默认第 1 页 page_size: int = Query(default=20), # 每页条数,默认 20 条 order_service: OrderService = Depends(get_order_service), # 注入订单服务 session: Session = Depends(get_db_session), # 注入数据库会话 current_user: dict = Depends(require_roles("salesman", "manager", "admin", "wuliu")), # 角色鉴权 _permission_user: dict = Depends(require_permissions("order:list")), # 权限鉴权:订单查看 ) -> dict: """分页查询订单列表 用途:根据多维度筛选条件获取订单列表,支持按订单号、状态、客户、业务员、工厂、来源、时间范围筛选。 请求参数:Query 参数组合筛选 + 分页参数。 返回值:分页订单列表,包含 total、page_no、page_size、list。 权限要求:业务员/经理/管理员,且需 order:list 权限。 """ result = order_service.list_orders( session, order_service.normalize_list_filters( { "order_no": order_no, "order_status": order_status, "customer_name": customer_name, "customer_mobile": customer_mobile, "salesman_id": salesman_id, "factory_id": factory_id, "order_source": order_source, "order_type": order_type, "need_invoice": need_invoice, "start_time": start_time, "end_time": end_time, "page_no": page_no, "page_size": page_size, }, current_user, ), current_user=current_user, ) return success_payload( { "total": result["total"], "page_no": page_no, "page_size": page_size, "list": result["list"], } ) @router.get("/stats") def order_stats( order_service: OrderService = Depends(get_order_service), session: Session = Depends(get_db_session), current_user: dict = Depends(require_roles("salesman", "manager", "admin", "wuliu")), _permission_user: dict = Depends(require_permissions("order:list")), ) -> dict: """获取订单统计数据 返回各状态的订单数量统计,用于工作台展示。 返回值:各状态订单数量的字典 """ from sqlalchemy import func, select from backend.app.models.business import SalesOrder # 统计各状态的订单数量 stmt = ( select(SalesOrder.order_status, func.count(SalesOrder.id)) .where(SalesOrder.deleted == 0) .group_by(SalesOrder.order_status) ) rows = session.execute(stmt).all() stats = {row[0]: row[1] for row in rows} return success_payload({ "total": sum(stats.values()), "draft": stats.get("draft", 0), "pending_approve": stats.get("pending_approve", 0), "approved": stats.get("approved", 0), "rejected": stats.get("rejected", 0), "pending_driver": stats.get("pending_driver", 0), "accepted": stats.get("accepted", 0), "picked_up": stats.get("picked_up", 0), "pending_logistics": stats.get("pending_logistics", 0), "in_transit": stats.get("in_transit", 0), "shipped": stats.get("shipped", 0), "completed": stats.get("completed", 0), "pending_settle": stats.get("pending_settle", 0), "settled": stats.get("settled", 0), "canceled": stats.get("canceled", 0), "cancel_pending": stats.get("cancel_pending", 0), "cancel_fulfillment_pending": stats.get("cancel_fulfillment_pending", 0), }) @router.post("") def create_order( payload: CreateOrderRequest, # 创建订单的请求体 order_service: OrderService = Depends(get_order_service), # 注入订单服务 session: Session = Depends(get_db_session), # 注入数据库会话 current_user: dict = Depends(require_roles("salesman", "admin", "wuliu")), # 角色鉴权:业务员/管理员/物流 _permission_user: dict = Depends(require_permissions("order:create")), # 权限鉴权:订单创建 ) -> dict: """创建新订单 用途:新建一个订单,需包含至少一条订单明细。 请求参数:CreateOrderRequest(客户信息、明细列表 items 等)。 返回值:创建成功后的订单信息。 权限要求:业务员(salesman)/管理员(admin)/物流(wuliu),且需 order:create 权限。 """ if not payload.items: raise AppException(code=ErrorCode.PARAM_ERROR, message="订单明细不能为空", status_code=400) order = order_service.create_order(payload.model_dump(), session, current_user) return success_payload(order) @router.put("/{order_id}") def update_order( order_id: int, # 订单 ID(路径参数) payload: UpdateOrderRequest, # 更新订单的请求体 order_service: OrderService = Depends(get_order_service), # 注入订单服务 session: Session = Depends(get_db_session), # 注入数据库会话 current_user: dict = Depends(require_roles("salesman", "manager", "admin")), # 角色鉴权:业务员/经理/管理员 _permission_user: dict = Depends(require_permissions("order:update")), # 权限鉴权:订单更新 ) -> dict: """更新订单信息 用途:修改已有订单的基本信息和明细,需包含至少一条订单明细。 请求参数:order_id(路径参数)+ UpdateOrderRequest(更新字段)。 返回值:更新后的订单信息。 权限要求:业务员(salesman)或管理员(admin),且需 order:update 权限。 """ if not payload.items: raise AppException(code=ErrorCode.PARAM_ERROR, message="订单明细不能为空", status_code=400) order = order_service.update_order(order_id, payload.model_dump(), session, current_user) if not order: raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404) return success_payload(order) @router.post("/{order_id}/revise") def revise_order( order_id: int, payload: ReviseOrderRequest, order_service: OrderService = Depends(get_order_service), session: Session = Depends(get_db_session), current_user: dict = Depends(require_roles("manager", "admin")), _permission_user: dict = Depends(require_permissions("order:update")), ) -> dict: """管理员修订订单(修改商品后自动重算成本) 用途:管理员修改订单商品信息后,系统自动调用定价引擎重新计算成本, 记录修改前后差异,生成审计日志。 请求参数:order_id(路径参数)+ ReviseOrderRequest(修改后商品列表和修订原因)。 返回值:修订结果,包含修改前后差异。 权限要求:经理或管理员,且需 order:update 权限。 """ result = order_service.revise_order(order_id, payload.model_dump(), session, current_user) if not result: raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404) return success_payload(result) @router.get("/{order_id}") def get_order( order_id: int, # 订单 ID(路径参数) order_service: OrderService = Depends(get_order_service), # 注入订单服务 session: Session = Depends(get_db_session), # 注入数据库会话 current_user: dict = Depends(require_roles("salesman", "manager", "admin", "wuliu")), # 角色鉴权 _permission_user: dict = Depends(require_permissions("order:list")), # 权限鉴权:订单查看 ) -> dict: """查询订单详情 用途:根据订单 ID 获取单个订单的完整信息(含明细、状态历史等)。 请求参数:order_id - 订单 ID(路径参数)。 返回值:订单详情信息。 权限要求:业务员/经理/管理员,且需 order:list 权限。 """ order = order_service.get_order(order_id, session, current_user) if not order: raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404) return success_payload(order) @router.post("/{order_id}/submit") def submit_order( order_id: int, # 订单 ID(路径参数) order_service: OrderService = Depends(get_order_service), # 注入订单服务 session: Session = Depends(get_db_session), # 注入数据库会话 current_user: dict = Depends(require_roles("salesman", "admin")), # 角色鉴权:业务员/管理员 _permission_user: dict = Depends(require_permissions("order:submit")), # 权限鉴权:订单提交 ) -> dict: """提交订单 用途:将草稿状态的订单提交审批,进入审批流程。 请求参数:order_id - 订单 ID(路径参数)。 返回值:提交后的订单信息。 权限要求:业务员(salesman)或管理员(admin),且需 order:submit 权限。 """ order = order_service.submit_order(order_id, session, current_user) if not order: raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404) return success_payload(order) @router.post("/{order_id}/cancel") def cancel_order( order_id: int, # 订单 ID(路径参数) payload: CancelOrderRequest, # 取消原因请求体 order_service: OrderService = Depends(get_order_service), # 注入订单服务 session: Session = Depends(get_db_session), # 注入数据库会话 current_user: dict = Depends(require_roles("salesman", "admin")), # 角色鉴权:业务员/管理员 _permission_user: dict = Depends(require_permissions("order:list")), # 权限鉴权:订单查看 ) -> dict: """取消订单 用途:取消一个未完成的订单,需提供取消原因。 请求参数:order_id(路径参数)+ CancelOrderRequest(取消原因)。 返回值:取消后的订单信息。 权限要求:业务员(salesman)或管理员(admin),且需 order:list 权限。 """ order = order_service.cancel_order(order_id, payload.model_dump(), session, current_user) if not order: raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404) return success_payload(order) @router.post("/{order_id}/status") def change_order_status( order_id: int, # 订单 ID(路径参数) payload: ChangeOrderStatusRequest, # 状态变更请求体 order_service: OrderService = Depends(get_order_service), # 注入订单服务 session: Session = Depends(get_db_session), # 注入数据库会话 current_user: dict = Depends(require_roles("manager", "admin")), # 角色鉴权:经理/管理员 _permission_user: dict = Depends(require_permissions("logistics:task:create")), # 权限鉴权:物流任务创建 ) -> dict: """变更订单状态 用途:手动变更订单的当前状态(如发货、完成等),需经理或管理员操作。 请求参数:order_id(路径参数)+ ChangeOrderStatusRequest(目标状态)。 返回值:状态变更后的订单信息。 权限要求:经理(manager)或管理员(admin),且需 logistics:task:create 权限。 """ order = order_service.change_order_status(order_id, payload.model_dump(), session, current_user) if not order: raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404) return success_payload(order) @router.post("/{order_id}/approve") def approve_order( order_id: int, # 订单 ID(路径参数) payload: ApproveOrderRequest, # 审批意见请求体 order_service: OrderService = Depends(get_order_service), # 注入订单服务 session: Session = Depends(get_db_session), # 注入数据库会话 current_user: dict = Depends(require_roles("manager", "admin")), # 角色鉴权:经理/管理员 _permission_user: dict = Depends(require_permissions("order:approve")), # 权限鉴权:订单审批 ) -> dict: """审批订单 用途:对已提交的订单进行审批通过操作。 请求参数:order_id(路径参数)+ ApproveOrderRequest(审批意见)。 返回值:审批后的订单信息。 权限要求:经理(manager)或管理员(admin),且需 order:approve 权限。 """ order = order_service.approve_order(order_id, payload.model_dump(), session, current_user) if not order: raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404) return success_payload(order) @router.post("/{order_id}/cancel-approve") def cancel_approve_order( order_id: int, # 订单 ID(路径参数) payload: ApproveOrderRequest, # 撤销审批意见请求体 order_service: OrderService = Depends(get_order_service), # 注入订单服务 session: Session = Depends(get_db_session), # 注入数据库会话 current_user: dict = Depends(require_roles("manager", "admin")), # 角色鉴权:经理/管理员 _permission_user: dict = Depends(require_permissions("order:cancel-approve")), # 权限鉴权:撤销审批 ) -> dict: """撤销订单审批 用途:撤销已通过的订单审批,将订单回退到待审批状态。 请求参数:order_id(路径参数)+ ApproveOrderRequest(撤销原因)。 返回值:撤销审批后的订单信息。 权限要求:经理(manager)或管理员(admin),且需 order:cancel-approve 权限。 """ order = order_service.cancel_approve_order(order_id, payload.model_dump(), session, current_user) if not order: raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404) return success_payload(order) @router.post("/{order_id}/supplier-text") def supplier_text( order_id: int, # 订单 ID(路径参数) payload: SupplierTextRequest, # 供应商文本请求体 order_service: OrderService = Depends(get_order_service), # 注入订单服务 session: Session = Depends(get_db_session), # 注入数据库会话 current_user: dict = Depends(require_roles("manager", "admin")), # 角色鉴权:经理/管理员 _permission_user: dict = Depends(require_permissions("order:supplier-text")), # 权限鉴权:供应商文本 ) -> dict: """生成/发送供应商文本 用途:根据订单信息生成供应商沟通文本,供内部确认后发送给供应商。 请求参数:order_id(路径参数)+ SupplierTextRequest(文本参数)。 返回值:包含供应商文本内容的订单信息。 权限要求:经理(manager)或管理员(admin),且需 order:supplier-text 权限。 """ order = order_service.get_supplier_text(order_id, payload.model_dump(), session, current_user) if not order: raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404) return success_payload(order) @router.post("/{order_id}/supplier-text/confirm") def confirm_supplier_text( order_id: int, # 订单 ID(路径参数) payload: ConfirmSupplierTextRequest, # 确认文本请求体 order_service: OrderService = Depends(get_order_service), # 注入订单服务 session: Session = Depends(get_db_session), # 注入数据库会话 current_user: dict = Depends(require_roles("manager", "admin")), # 角色鉴权:经理/管理员 _permission_user: dict = Depends(require_permissions("order:supplier-text")), # 权限鉴权:供应商文本 ) -> dict: """确认供应商文本 用途:确认已生成的供应商沟通文本,触发实际发送流程。 请求参数:order_id(路径参数)+ ConfirmSupplierTextRequest(确认内容)。 返回值:确认后的订单信息。 权限要求:经理(manager)或管理员(admin),且需 order:supplier-text 权限。 """ order = order_service.confirm_supplier_text(order_id, payload.model_dump(), session, current_user) if not order: raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404) return success_payload(order) @router.post("/{order_id}/settle") def settle_order( order_id: int, order_service: OrderService = Depends(get_order_service), session: Session = Depends(get_db_session), current_user: dict = Depends(require_roles("manager", "admin")), ) -> dict: """结算订单提成 用途:将待结算状态的订单标记为已结算。 请求参数:order_id(路径参数)。 返回值:结算后的订单信息。 权限要求:经理(manager)或管理员(admin)。 """ order = order_service.settle_order(order_id, session, current_user) if not order: raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404) return success_payload(order) @router.put("/{order_id}/tracking") def update_order_tracking( order_id: int, payload: UpdateOrderTrackingRequest, order_service: OrderService = Depends(get_order_service), session: Session = Depends(get_db_session), current_user: dict = Depends(require_roles("salesman", "manager", "admin")), _permission_user: dict = Depends(require_permissions("order:update")), ) -> dict: """修改自发订单快递单号 用途:审批通过后仍可修改自发订单的快递单号。 请求参数:order_id(路径参数)+ UpdateOrderTrackingRequest(快递单号、修改原因)。 返回值:更新后的订单信息。 权限要求:业务员/经理/管理员,且需 order:update 权限。 """ order = order_service.update_tracking_number(order_id, payload.model_dump(), session, current_user) if not order: raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404) return success_payload(order) @router.post("/batch-match-logistics") def batch_match_logistics( payload: BatchMatchLogisticsRequest, order_service: OrderService = Depends(get_order_service), session: Session = Depends(get_db_session), current_user: dict = Depends(require_roles("manager", "admin", "wuliu")), _permission_user: dict = Depends(require_permissions("order:batch-match")), ) -> dict: """批量匹配物流预览:解析文本/图片,返回物流条目及匹配的订单候选。""" # 1. 根据模式解析物流条目 if payload.mode == "text": if not payload.text or not payload.text.strip(): raise AppException(code=ErrorCode.PARAM_ERROR, message="文本内容不能为空", status_code=400) items = order_service.parse_logistics_text(payload.text) elif payload.mode == "image_table": if not payload.image_urls: raise AppException(code=ErrorCode.PARAM_ERROR, message="请上传物流表格图片", status_code=400) items = order_service.parse_logistics_table_images(payload.image_urls, session) elif payload.mode == "express_images": if not payload.image_urls: raise AppException(code=ErrorCode.PARAM_ERROR, message="请上传快递面单照片", status_code=400) items = order_service.parse_express_images(payload.image_urls, session) else: raise AppException(code=ErrorCode.PARAM_ERROR, message=f"不支持的模式: {payload.mode}", status_code=400) # 2. 对有收件人的条目自动匹配订单 for item in items: recipient = item.get("recipient_name") if recipient: candidates = order_service._match_orders_by_name(recipient, session) item["candidates"] = candidates else: item["candidates"] = [] return success_payload({"items": items}) @router.post("/batch-match-logistics/confirm") def batch_match_logistics_confirm( payload: BatchMatchConfirmRequest, order_service: OrderService = Depends(get_order_service), session: Session = Depends(get_db_session), current_user: dict = Depends(require_roles("manager", "admin", "wuliu")), _permission_user: dict = Depends(require_permissions("order:batch-match")), ) -> dict: """批量确认物流匹配结果,将快递单号写入订单。""" matches = [m.model_dump() for m in payload.matches] # 过滤掉未选择订单的条目(order_id 为 null) valid_matches = [m for m in matches if m.get("order_id")] if not valid_matches: raise AppException(code=ErrorCode.PARAM_ERROR, message="请至少选择一条匹配记录", status_code=400) result = order_service.confirm_batch_match(valid_matches, session) session.commit() return success_payload(result)