"""配送管理接口""" from typing import Optional, List from datetime import datetime from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session from app.core.database import get_db from app.core.auth import get_current_user from app.core.response import success_response, error_response from app.crud.region import region_crud from app.crud.user import user_crud from app.crud.order import order_crud, order_item_crud from app.crud.product import product_crud from app.crud.unit import unit_crud from app.crud.address import address_crud from app.core.order_status_compat import derive_legacy_status_from_three router = APIRouter(prefix="/web", tags=["运营端-配送管理"]) def _numeric_to_float_optional(v) -> Optional[float]: if v is None: return None return float(v) def _category_path_display(db: Session, category_id: Optional[str]) -> str: """一级/二级分类用 / 连接(与订单详情一致)。""" from app.crud.category import category_crud if not category_id: return "" names: List[str] = [] cid = str(category_id).strip() seen = set() depth = 0 while cid and depth < 24 and cid not in seen: seen.add(cid) cat = category_crud.get(db, cid) if not cat: break names.insert(0, (getattr(cat, "name", None) or "").strip()) pid = getattr(cat, "parent_id", None) cid = str(pid).strip() if pid else "" depth += 1 return "/".join([n for n in names if n]) def _supplier_pickup_address_block(db: Session, supplier_id: Optional[str]) -> dict: """供应商默认取货地址;无默认时取第一条 pickup 类型地址。""" empty = {"fullAddress": None, "contactPerson": None, "contactPhone": None} if not supplier_id: return empty addr = address_crud.get_default(db, supplier_id, "pickup") if not addr: rows = address_crud.get_by_user_and_type(db, supplier_id, "pickup") addr = rows[0] if rows else None if not addr: return empty segs = [ getattr(addr, "province", None), getattr(addr, "city", None), getattr(addr, "district", None), getattr(addr, "detail_address", None), ] full = "".join(s for s in segs if s) return { "fullAddress": full or None, "contactPerson": addr.contact_person, "contactPhone": addr.contact_phone, } def require_admin(current_user: dict = Depends(get_current_user)): """验证运营人员权限""" if current_user.get("role") != "admin": return error_response(403, "需要运营人员权限") return current_user # 配送任务状态映射 DELIVERY_STATUS_MAP = { "pending": "待取货", "assigned": "已分配", "picking": "取货中", "delivering": "配送中", "completed": "已完成", "cancelled": "已取消" } @router.get("/delivery-tasks", response_model=dict) def list_delivery_tasks( page: int = Query(1, ge=1, description="页码"), pageSize: int = Query(10, ge=1, le=100, description="每页条数"), regionId: Optional[str] = Query(None, description="区域ID"), status: Optional[str] = Query(None, description="状态"), orderNo: Optional[str] = Query(None, description="订单号"), db: Session = Depends(get_db), current_user: dict = Depends(require_admin) ): """获取配送任务列表""" from app.crud.delivery import delivery_task_crud, delivery_task_item_crud tasks, total = delivery_task_crud.get_multi_with_filters( db, region_id=regionId, status=status, order_no=orderNo, skip=(page - 1) * pageSize, limit=pageSize ) region_map = {} supplier_map = {} deliver_map = {} client_map = {} result = [] for task in tasks: # 获取区域名称 if task.region_id and task.region_id not in region_map: region = region_crud.get(db, task.region_id) region_map[task.region_id] = region.name if region else None # 获取供应商信息 if task.supplier_id and task.supplier_id not in supplier_map: supplier = user_crud.get(db, task.supplier_id) supplier_map[task.supplier_id] = { "name": supplier.real_name if supplier else None, "phone": supplier.phone if supplier else None } # 获取配送员信息 if task.deliver_id and task.deliver_id not in deliver_map: deliver = user_crud.get(db, task.deliver_id) deliver_map[task.deliver_id] = { "name": deliver.real_name if deliver else None, "phone": deliver.phone if deliver else None } # 获取订单信息 order_info = {} if task.order_id: order = order_crud.get(db, task.order_id) if order: # 获取客户信息(列表页需要客户名称) if order.client_id and order.client_id not in client_map: client = user_crud.get(db, order.client_id) client_map[order.client_id] = { "name": client.real_name if client else None, "phone": client.phone if client else None } order_info = { "orderNo": order.order_no, "totalAmount": float(order.total_amount) if order.total_amount else 0, "clientId": order.client_id, "clientName": client_map.get(order.client_id, {}).get("name") if order.client_id else None, "contactPerson": order.contact_person, "contactPhone": order.contact_phone, "deliveryAddress": order.delivery_address } # 获取任务明细数量 items = delivery_task_item_crud.get_by_task(db, task.id) item_count = len(items) # 供应商取货地址(用于列表展示) supplier_pickup = _supplier_pickup_address_block(db, task.supplier_id) result.append({ "id": task.id, "taskNo": task.task_no, "orderId": task.order_id, "orderNo": order_info.get("orderNo"), "regionId": task.region_id, "regionName": region_map.get(task.region_id), "supplierId": task.supplier_id, "supplierName": supplier_map.get(task.supplier_id, {}).get("name"), "supplierAddress": supplier_pickup.get("fullAddress"), "deliverId": task.deliver_id, "deliverName": deliver_map.get(task.deliver_id, {}).get("name"), "deliverPhone": deliver_map.get(task.deliver_id, {}).get("phone"), "status": task.status, "totalAmount": order_info.get("totalAmount"), "itemCount": item_count, "clientId": order_info.get("clientId"), "clientName": order_info.get("clientName"), "contactPerson": order_info.get("contactPerson"), "contactPhone": order_info.get("contactPhone"), "deliveryAddress": order_info.get("deliveryAddress"), "dispatchRemark": task.dispatch_remark, "createAt": str(task.create_at) if task.create_at else None, "assignTime": str(task.create_at) if task.deliver_id else None, "pickupTime": str(task.pickup_time) if task.pickup_time else None, "deliveryTime": str(task.delivery_time) if task.delivery_time else None, "expireTime": str(task.effective_time) if task.effective_time else None, "isExpiring": False }) return success_response({ "list": result, "total": total, "page": page, "pageSize": pageSize, "totalPages": (total + pageSize - 1) // pageSize }) @router.get("/delivery-tasks/{task_id}", response_model=dict) def get_delivery_task( task_id: str, db: Session = Depends(get_db), current_user: dict = Depends(require_admin) ): """获取配送任务详情""" from app.crud.delivery import delivery_task_crud, delivery_task_item_crud task = delivery_task_crud.get(db, task_id) if not task: return error_response(404, "配送任务不存在") # 获取任务明细 items = delivery_task_item_crud.get_by_task(db, task_id) # 获取关联信息 region_name = None if task.region_id: region = region_crud.get(db, task.region_id) region_name = region.name if region else None supplier_info = {"name": None, "phone": None} if task.supplier_id: supplier = user_crud.get(db, task.supplier_id) if supplier: supplier_info = {"name": supplier.real_name, "phone": supplier.phone} deliver_info = {"name": None, "phone": None} if task.deliver_id: deliver = user_crud.get(db, task.deliver_id) if deliver: deliver_info = {"name": deliver.real_name, "phone": deliver.phone} # 获取订单信息 order_info = {} if task.order_id: order = order_crud.get(db, task.order_id) if order: order_info = { "orderNo": order.order_no, "totalAmount": float(order.total_amount) if order.total_amount else 0, "contactPerson": order.contact_person, "contactPhone": order.contact_phone, "deliveryAddress": order.delivery_address } pickup_addr = _supplier_pickup_address_block(db, task.supplier_id) # 处理任务明细(商品展示字段对齐运营端订单明细) item_details = [] for item in items: product = product_crud.get(db, item.product_id) order_line = order_item_crud.get(db, item.order_item_id) if item.order_item_id else None unit_row = None if order_line and order_line.unit_id: unit_row = unit_crud.get(db, order_line.unit_id) elif product and product.unit_id: unit_row = unit_crud.get(db, product.unit_id) image_url = getattr(product, "image_url", None) if product else None category_path = "" if product and getattr(product, "category_id", None): category_path = _category_path_display(db, str(product.category_id)) measure_value = _numeric_to_float_optional(getattr(product, "measure_value", None)) if product else None unit_price = float(order_line.unit_price) if order_line and order_line.unit_price is not None else None amount = float(order_line.amount) if order_line and order_line.amount is not None else None client = user_crud.get(db, item.client_id) if item.client_id else None # 供货/售价字段:配送任务明细接口目前不计算价格清单的供货价,先以订单行售价兜底, # 确保运营端「配送列表」展开明细对齐「订单列表」列结构时有数据可展示。 selling_unit_price = unit_price selling_amount = amount supplier_unit_price = unit_price supplier_amount = amount item_details.append({ "id": item.id, "orderItemId": item.order_item_id, "productId": item.product_id, "productName": product.name if product else None, "imageUrl": image_url, "categoryName": category_path or None, "measureValue": measure_value, "unitName": unit_row.name if unit_row else None, "unitPrice": unit_price, "amount": amount, "sellingUnitPrice": selling_unit_price, "sellingAmount": selling_amount, "supplierId": task.supplier_id, "supplierName": supplier_info.get("name"), "supplierUnitPrice": supplier_unit_price, "supplierAmount": supplier_amount, "quantity": float(item.quantity) if item.quantity else 0, "deliveredQuantity": float(item.delivered_quantity) if item.delivered_quantity else 0, "clientName": client.real_name if client else None, "clientPhone": client.phone if client else None, "clientAddress": item.delivery_address, "contactPerson": item.contact_person, "contactPhone": item.contact_phone, "deliveryAddress": item.delivery_address, "status": item.status }) # 处理图片 pickup_images = [] if task.pickup_image_url: pickup_images = task.pickup_image_url.split(",") delivery_images = [] if task.delivery_image_url: delivery_images = task.delivery_image_url.split(",") return success_response({ "id": task.id, "taskNo": task.task_no, "orderId": task.order_id, "orderNo": order_info.get("orderNo"), "regionId": task.region_id, "regionName": region_name, "supplierId": task.supplier_id, "supplierName": supplier_info.get("name"), "supplierPhone": supplier_info.get("phone"), "supplierAddress": pickup_addr.get("fullAddress"), "supplierPickup": { "address": pickup_addr.get("fullAddress"), "contactPerson": pickup_addr.get("contactPerson"), "contactPhone": pickup_addr.get("contactPhone"), }, "customerDelivery": { "address": order_info.get("deliveryAddress"), "contactPerson": order_info.get("contactPerson"), "contactPhone": order_info.get("contactPhone"), }, "deliverId": task.deliver_id, "deliverName": deliver_info.get("name"), "deliverPhone": deliver_info.get("phone"), "status": task.status, "totalAmount": order_info.get("totalAmount"), "createAt": str(task.create_at) if task.create_at else None, "assignTime": str(task.create_at) if task.deliver_id else None, "pickupTime": str(task.pickup_time) if task.pickup_time else None, "pickupRemark": task.pickup_remark, "pickupImages": pickup_images, "deliveryTime": str(task.delivery_time) if task.delivery_time else None, "deliveryRemark": task.delivery_remark, "deliveryImages": delivery_images, "dispatchRemark": task.dispatch_remark, "items": item_details }) @router.post("/delivery-tasks/assign", response_model=dict) def assign_delivery_task( request: dict, db: Session = Depends(get_db), current_user: dict = Depends(require_admin) ): """分配配送任务""" from app.crud.delivery import delivery_task_crud from app.crud.order import order_crud order_id = request.get("orderId") deliver_id = request.get("deliverId") if not order_id: return error_response(400, "请选择订单") if not deliver_id: return error_response(400, "请选择配送员") # 验证配送员存在 deliver = user_crud.get(db, deliver_id) if not deliver: return error_response(404, "配送员不存在") if deliver.role != "deliver": return error_response(400, "该用户不是配送员") task = delivery_task_crud.assign_deliver( db, order_id=order_id, deliver_id=deliver_id, operator_id=current_user.get("user_id") ) if not task: return error_response(400, "分配失败,请检查订单状态") # 阶段D前收尾:分配配送优先维护 delivery_status order = order_crud.get(db, order_id) if order: order.delivery_status = "delivering" if order.order_status != "cancelled": order.order_status = "pending_confirm" order.status = derive_legacy_status_from_three( order_status=order.order_status, delivery_status=order.delivery_status, pay_status=order.pay_status, ) order.update_at = datetime.now() db.commit() return success_response({ "id": task.id, "status": task.status, "message": "分配成功" }) @router.post("/delivery-tasks/assign-batch", response_model=dict) def assign_delivery_task_batch( request: dict, db: Session = Depends(get_db), current_user: dict = Depends(require_admin) ): """批量分配配送任务""" from app.crud.delivery import delivery_task_crud from app.crud.order import order_crud assignments = request.get("assignments", []) if not assignments: return error_response(400, "请选择要分配的订单") results = [] for assignment in assignments: order_id = assignment.get("orderId") deliver_id = assignment.get("deliverId") if order_id and deliver_id: task = delivery_task_crud.assign_deliver( db, order_id=order_id, deliver_id=deliver_id, operator_id=current_user.get("user_id") ) if task: # 阶段D前收尾:分配配送优先维护 delivery_status order = order_crud.get(db, order_id) if order: order.delivery_status = "delivering" if order.order_status != "cancelled": order.order_status = "pending_confirm" order.status = derive_legacy_status_from_three( order_status=order.order_status, delivery_status=order.delivery_status, pay_status=order.pay_status, ) order.update_at = datetime.now() db.commit() results.append({"orderId": order_id, "success": True}) else: results.append({"orderId": order_id, "success": False, "reason": "分配失败"}) success_count = sum(1 for r in results if r.get("success")) return success_response({ "total": len(results), "success": success_count, "failed": len(results) - success_count, "results": results }) @router.get("/delivery-tasks/warnings", response_model=dict) def list_delivery_warnings( page: int = Query(1, ge=1, description="页码"), pageSize: int = Query(10, ge=1, le=100, description="每页条数"), regionId: Optional[str] = Query(None, description="区域ID"), withinMinutes: int = Query(60, description="多少分钟内即将过期"), db: Session = Depends(get_db), current_user: dict = Depends(require_admin) ): """获取配送时效预警列表""" from app.crud.delivery import delivery_task_crud tasks, total, warning_count = delivery_task_crud.get_warning_tasks( db, region_id=regionId, within_minutes=withinMinutes, skip=(page - 1) * pageSize, limit=pageSize ) region_map = {} deliver_map = {} result = [] for task in tasks: if task.region_id and task.region_id not in region_map: region = region_crud.get(db, task.region_id) region_map[task.region_id] = region.name if region else None if task.deliver_id and task.deliver_id not in deliver_map: deliver = user_crud.get(db, task.deliver_id) deliver_map[task.deliver_id] = deliver.real_name if deliver else None order_info = {} if task.order_id: order = order_crud.get(db, task.order_id) if order: order_info = {"orderNo": order.order_no} # 计算剩余时间和预警级别 remaining_minutes = 0 warning_level = "normal" is_overdue = False if task.effective_time: now = datetime.now() remaining = task.effective_time - now remaining_minutes = int(remaining.total_seconds() / 60) if remaining_minutes < 0: is_overdue = True warning_level = "overdue" elif remaining_minutes <= 15: warning_level = "urgent" elif remaining_minutes <= 30: warning_level = "warning" result.append({ "id": task.id, "taskNo": task.task_no, "orderId": task.order_id, "orderNo": order_info.get("orderNo"), "regionId": task.region_id, "regionName": region_map.get(task.region_id), "deliverId": task.deliver_id, "deliverName": deliver_map.get(task.deliver_id), "status": task.status, "expireTime": str(task.effective_time) if task.effective_time else None, "remainingMinutes": remaining_minutes, "isOverdue": is_overdue, "warningLevel": warning_level }) return success_response({ "list": result, "total": total, "warningCount": warning_count, "page": page, "pageSize": pageSize, "totalPages": (total + pageSize - 1) // pageSize }) @router.post("/delivery-tasks/{task_id}/cancel", response_model=dict) def cancel_delivery_task( task_id: str, request: dict, db: Session = Depends(get_db), current_user: dict = Depends(require_admin) ): """取消配送任务""" from app.crud.delivery import delivery_task_crud task = delivery_task_crud.get(db, task_id) if not task: return error_response(404, "配送任务不存在") reason = request.get("reason") task = delivery_task_crud.cancel_task( db, task_id=task_id, reason=reason, operator_id=current_user.get("user_id"), operator_real_name=current_user.get("real_name"), ) if not task: return error_response(400, "当前状态不支持取消") return success_response({ "id": task.id, "status": task.status, "message": "取消成功" }) @router.post("/delivery-tasks/{task_id}/reassign", response_model=dict) def reassign_delivery_task( task_id: str, request: dict, db: Session = Depends(get_db), current_user: dict = Depends(require_admin) ): """重新分配配送任务""" from app.crud.delivery import delivery_task_crud task = delivery_task_crud.get(db, task_id) if not task: return error_response(404, "配送任务不存在") new_deliver_id = request.get("deliverId") if not new_deliver_id: return error_response(400, "请选择新配送员") # 验证新配送员 new_deliver = user_crud.get(db, new_deliver_id) if not new_deliver: return error_response(404, "配送员不存在") if new_deliver.role != "deliver": return error_response(400, "该用户不是配送员") task = delivery_task_crud.reassign( db, task_id=task_id, new_deliver_id=new_deliver_id, operator_id=current_user.get("user_id"), operator_real_name=current_user.get("real_name"), ) if not task: return error_response(400, "当前状态不支持重新分配") return success_response({ "id": task.id, "deliverId": task.deliver_id, "message": "重新分配成功" }) @router.get("/delivery-tasks/pending", response_model=dict) def list_pending_delivery_tasks( page: int = Query(1, ge=1, description="页码"), pageSize: int = Query(10, ge=1, le=100, description="每页条数"), regionId: Optional[str] = Query(None, description="区域ID"), db: Session = Depends(get_db), current_user: dict = Depends(require_admin) ): """获取待分配的配送任务""" from app.crud.delivery import delivery_task_crud, delivery_task_item_crud tasks, total = delivery_task_crud.get_pending_list( db, region_id=regionId, skip=(page - 1) * pageSize, limit=pageSize ) region_map = {} result = [] for task in tasks: if task.region_id and task.region_id not in region_map: region = region_crud.get(db, task.region_id) region_map[task.region_id] = region.name if region else None order_info = {} if task.order_id: order = order_crud.get(db, task.order_id) if order: order_info = { "orderNo": order.order_no, "totalAmount": float(order.total_amount) if order.total_amount else 0, "contactPerson": order.contact_person, "contactPhone": order.contact_phone, "deliveryAddress": order.delivery_address } items = delivery_task_item_crud.get_by_task(db, task.id) result.append({ "id": task.id, "taskNo": task.task_no, "orderId": task.order_id, "orderNo": order_info.get("orderNo"), "regionId": task.region_id, "regionName": region_map.get(task.region_id), "status": task.status, "totalAmount": order_info.get("totalAmount"), "itemCount": len(items), "createAt": str(task.create_at) if task.create_at else None }) return success_response({ "list": result, "total": total, "page": page, "pageSize": pageSize, "totalPages": (total + pageSize - 1) // pageSize })