| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694 |
- """配送管理接口"""
- 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
- })
|