dingdanquanliucheng/backend/app/api/orders.py
wsb1224 a0d1e63ada fix: 修复wuliu角色无法访问创建订单和批量匹配物流页面
- 后端API require_roles添加wuliu角色白名单
- 批量匹配接口权限从order:update改为order:batch-match
- bootstrap默认权限修正格式错误并补全order:create
- auth_service兜底默认配置补全创建订单菜单和权限码

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 07:31:48 +08:00

478 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
订单管理路由模块
职责:
处理订单的全生命周期管理接口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)