- 后端API require_roles添加wuliu角色白名单 - 批量匹配接口权限从order:update改为order:batch-match - bootstrap默认权限修正格式错误并补全order:create - auth_service兜底默认配置补全创建订单菜单和权限码 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
478 lines
23 KiB
Python
478 lines
23 KiB
Python
"""
|
||
订单管理路由模块
|
||
|
||
职责:
|
||
处理订单的全生命周期管理接口,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)
|