"""配送员端接口""" from typing import Optional 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.crud.attachment import attachment_crud router = APIRouter(prefix="/api/mini/deliverer", tags=["小程序端-配送员接口"]) 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 _numeric_to_float_optional(v) -> Optional[float]: if v is None: return None try: return float(v) except (TypeError, ValueError): return None def _normalize_image_urls(raw) -> list[str]: out: list[str] = [] if isinstance(raw, list): for x in raw: s = str(x or "").strip() if s: out.append(s) return out def _save_task_attachments( db: Session, *, task_id: str, business_type: str, image_urls: list[str], uploader_id: Optional[str], uploader_name: Optional[str], ): for idx, url in enumerate(image_urls, start=1): attachment_crud.create_attachment( db=db, business_type=business_type, business_id=task_id, file_name=f"{business_type}_{idx}.jpg", file_url=url, file_type="image/jpeg", upload_by=uploader_id, upload_by_name=uploader_name, ) def require_deliverer(current_user: dict = Depends(get_current_user)): """验证配送员权限""" if current_user.get("role") != "deliver": return error_response(403, "需要配送员权限") return current_user # 配送任务状态映射 DELIVERY_STATUS_MAP = { "pending": "待取货", "assigned": "已分配", "picking": "取货中", "delivering": "配送中", "completed": "已完成", "cancelled": "已取消" } @router.get("/products/{product_id}", response_model=dict) def get_deliverer_product_detail( product_id: str, db: Session = Depends(get_db), current_user: dict = Depends(require_deliverer), ): """配送员查看商品详情:轮播、基础信息、参数、富文本、详情媒体(与供应商端一致,不依赖客户价目表/区域绑定)""" from app.crud.product_banner import product_banner from app.crud.product_parameter_value import product_parameter_value from app.crud.product_detail_media import product_detail_media product = product_crud.get(db, product_id) if not product or getattr(product, "is_del", 0) == 1: return error_response(404, "商品不存在") unit = unit_crud.get(db, product.unit_id) if product.unit_id else None banners_raw = product_banner.get_by_product(db, product_id) banners = [{ "mediaType": b.media_type, "fileUrl": b.file_url, "thumbnailUrl": b.thumbnail_url, "sortOrder": b.sort_order, } for b in banners_raw] pv_rows = product_parameter_value.get_by_product_with_detail(db, product_id) parameter_values = [] for row in pv_rows: val = row.get("param_value") if val is None or str(val).strip() == "": continue parameter_values.append({ "parameterId": row.get("parameter_id"), "parameterName": row.get("parameter_name") or "", "paramValue": str(val), }) media_raw = product_detail_media.get_by_product(db, product_id) detail_media = [] for m in media_raw: if m.content_type not in ("image", "video"): continue if not m.file_url: continue detail_media.append({ "id": m.id, "contentType": m.content_type, "fileUrl": m.file_url, "thumbnailUrl": m.thumbnail_url, "description": m.description, "sortOrder": m.sort_order, }) return success_response({ "product": { "id": product.id, "name": product.name, "unitId": product.unit_id, "unitName": unit.name if unit else None, "measureValue": float(product.measure_value) if product.measure_value is not None else None, "detailContent": product.detail_content or "", }, "banners": banners, "parameterValues": parameter_values, "detailMedia": detail_media, }) @router.get("/tasks", response_model=dict) def list_deliverer_tasks( status: Optional[str] = Query(None, description="状态"), page: int = Query(1, ge=1, description="页码"), pageSize: int = Query(10, ge=1, le=100, description="每页条数"), db: Session = Depends(get_db), current_user: dict = Depends(require_deliverer) ): """获取配送员的任务列表""" from app.crud.delivery import delivery_task_crud, delivery_task_item_crud deliver_id = current_user.get("user_id") # 小程序 Tab:按配送员状态 deliverer_status 筛选(与 statistics 口径一致) deliverer_st = None legacy_st = None if status in (None, "", "pending", "assigned"): deliverer_st = "pending_pickup" elif status == "delivering": deliverer_st = "delivering" elif status == "completed": deliverer_st = "delivered" else: legacy_st = status tasks, total = delivery_task_crud.get_by_deliver( db, deliver_id=deliver_id, status=legacy_st, deliverer_status=deliverer_st, skip=(page - 1) * pageSize, limit=pageSize ) result = [] for task in tasks: # 获取供应商信息 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} pickup_addr = _supplier_pickup_address_block(db, task.supplier_id) # 获取订单信息(配送员端不返回金额) order_info: dict = {} if task.order_id: order = order_crud.get(db, task.order_id) if order: order_info = {"orderNo": order.order_no} # 获取任务明细(卡片展示取货/送货摘要) items = delivery_task_item_crud.get_by_task(db, task.id) total_quantity = sum(float(item.quantity or 0) for item in items) item_preview = [] for item in items: item_preview.append({ "clientName": item.contact_person, "clientPhone": item.contact_phone, "clientAddress": item.delivery_address, }) # 计算剩余时间 remaining_minutes = 0 if task.effective_time: remaining = task.effective_time - datetime.now() remaining_minutes = max(0, int(remaining.total_seconds() / 60)) result.append({ "id": task.id, "taskNo": task.task_no, "orderNo": order_info.get("orderNo"), "supplierName": supplier_info.get("name"), "supplierPhone": supplier_info.get("phone"), "supplierAddress": pickup_addr.get("fullAddress"), "supplierPickupContact": pickup_addr.get("contactPerson"), "supplierPickupPhone": pickup_addr.get("contactPhone"), "clientCount": len(set(item.client_id for item in items if item.client_id)), "itemCount": len(items), "totalQuantity": total_quantity, "items": item_preview, "status": task.status, "delivererStatus": getattr(task, "deliverer_status", None), "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None), "createTime": str(task.create_at) if task.create_at else None, "pickupDeadline": str(task.effective_time) if task.effective_time else None, "remainingMinutes": remaining_minutes }) return success_response({ "list": result, "total": total, "page": page, "pageSize": pageSize, "totalPages": (total + pageSize - 1) // pageSize }) # 以下静态路径必须注册在 /tasks/{task_id} 之前,否则 statistics、history 等会被当成 task_id @router.get("/tasks/statistics", response_model=dict) def get_deliverer_statistics( db: Session = Depends(get_db), current_user: dict = Depends(require_deliverer) ): """获取配送员统计""" from app.crud.delivery import delivery_task_crud deliver_id = current_user.get("user_id") from app.crud.deliverer_review import deliverer_review_crud # 获取今日数据 today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) # 待取货:与列表「待取货」Tab 一致——全部 deliverer_status=pending_pickup(含历史创建未取货),不按创建日截断 pending_tasks, _ = delivery_task_crud.get_by_deliver( db, deliver_id=deliver_id, deliverer_status="pending_pickup", skip=0, limit=500 ) # 配送中数量(配送员状态) delivering_tasks, _ = delivery_task_crud.get_by_deliver( db, deliver_id=deliver_id, deliverer_status="delivering", skip=0, limit=500 ) # 今日已完成(配送员已送达) completed_tasks, _ = delivery_task_crud.get_by_deliver( db, deliver_id=deliver_id, deliverer_status="delivered", skip=0, limit=2000 ) today_completed = [t for t in completed_tasks if t.delivery_time and t.delivery_time >= today] # 本周已完成 week_start = today.replace(day=today.day - today.weekday()) week_completed = [t for t in completed_tasks if t.delivery_time and t.delivery_time >= week_start] # 本月已完成 month_start = today.replace(day=1) month_completed = [t for t in completed_tasks if t.delivery_time and t.delivery_time >= month_start] review_stats = deliverer_review_crud.get_deliverer_rating_stats(db, deliver_id) return success_response({ "todayPending": len(pending_tasks), "todayDelivering": len(delivering_tasks), "todayCompleted": len(today_completed), "weekCompleted": len(week_completed), "monthCompleted": len(month_completed), "rating": review_stats.get("avgScore", 0), "ratingCount": review_stats.get("total", 0) }) @router.get("/tasks/history", response_model=dict) def get_deliverer_task_history( startDate: Optional[str] = Query(None, description="开始日期 YYYY-MM-DD"), endDate: Optional[str] = Query(None, description="结束日期 YYYY-MM-DD"), page: int = Query(1, ge=1, description="页码"), pageSize: int = Query(10, ge=1, le=50, description="每页条数"), db: Session = Depends(get_db), current_user: dict = Depends(require_deliverer) ): """获取配送员历史任务列表""" from app.crud.delivery import delivery_task_crud, delivery_task_item_crud deliver_id = current_user.get("user_id") tasks, total = delivery_task_crud.get_history_by_deliver( db, deliver_id=deliver_id, start_date=startDate, end_date=endDate, skip=(page - 1) * pageSize, limit=pageSize ) result = [] for task in tasks: # 获取供应商信息 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} # 获取任务项统计 items = delivery_task_item_crud.get_by_task(db, task.id) total_quantity = sum(float(item.quantity or 0) for item in items) delivered_quantity = sum(float(item.delivered_quantity or 0) for item in items) # 计算配送时效(分钟) delivery_minutes = None if task.pickup_time and task.delivery_time: delta = task.delivery_time - task.pickup_time delivery_minutes = int(delta.total_seconds() / 60) result.append({ "id": task.id, "taskNo": task.task_no, "supplierName": supplier_info.get("name"), "supplierPhone": supplier_info.get("phone"), "status": task.status, "statusText": "已完成" if task.status == "completed" else "已取消" if task.status == "cancelled" else task.status, "pickupTime": str(task.pickup_time) if task.pickup_time else None, "deliveryTime": str(task.delivery_time) if task.delivery_time else None, "totalQuantity": total_quantity, "deliveredQuantity": delivered_quantity, "deliveryMinutes": delivery_minutes, "createAt": str(task.create_at) if task.create_at else None }) return success_response({ "list": result, "total": total, "page": page, "pageSize": pageSize }) @router.get("/tasks/warnings", response_model=dict) def list_deliverer_task_warnings( db: Session = Depends(get_db), current_user: dict = Depends(require_deliverer) ): """配送员关联任务的时效预警列表""" from app.models.models import BizDeliveryTask, BizDeliveryWarning from sqlalchemy import and_ did = current_user.get("user_id") task_ids = [ t[0] for t in db.query(BizDeliveryTask.id) .filter(and_(BizDeliveryTask.deliver_id == did, BizDeliveryTask.is_del == 0)) .all() ] if not task_ids: return success_response({"list": []}) rows = ( db.query(BizDeliveryWarning) .filter( and_( BizDeliveryWarning.task_id.in_(task_ids), BizDeliveryWarning.is_del == 0, BizDeliveryWarning.is_resolved == 0, ) ) .order_by(BizDeliveryWarning.create_at.desc()) .limit(50) .all() ) out = [ { "id": r.id, "taskId": r.task_id, "warningType": r.warning_type, "urgencyLevel": r.urgency_level, "remainingMinutes": r.remaining_minutes, "message": r.message, "createAt": str(r.create_at) if r.create_at else None, } for r in rows ] return success_response({"list": out}) @router.get("/tasks/expiring", response_model=dict) def list_deliverer_expiring_tasks( db: Session = Depends(get_db), current_user: dict = Depends(require_deliverer), withinMinutes: int = Query(60, ge=1, le=24 * 60, description="多少分钟内视为即将超时"), ): """即将超时的配送任务(基于 effective_time)""" from app.crud.delivery import delivery_task_crud did = current_user.get("user_id") now = datetime.now() tasks, _ = delivery_task_crud.get_by_deliver(db, deliver_id=did, status=None, skip=0, limit=200) result = [] for t in tasks: if t.status in ("completed", "cancelled"): continue if (getattr(t, "deliverer_status", None) or "") == "delivered": continue eff = getattr(t, "effective_time", None) if not eff: continue remaining = (eff - now).total_seconds() / 60 if 0 <= remaining <= withinMinutes: result.append({ "id": t.id, "taskNo": t.task_no, "status": t.status, "effectiveTime": str(eff) if eff else None, "remainingMinutes": max(0, int(remaining)), }) return success_response({"list": result}) @router.get("/tasks/{task_id}", response_model=dict) def get_deliverer_task( task_id: str, db: Session = Depends(get_db), current_user: dict = Depends(require_deliverer) ): """获取配送员任务详情""" 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, "配送任务不存在") if task.deliver_id != current_user.get("user_id"): return error_response(403, "无权查看此任务") # 获取供应商信息 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} pickup_addr = _supplier_pickup_address_block(db, task.supplier_id) # 获取订单信息(配送员端不返回金额) order_info: dict = {} if task.order_id: order = order_crud.get(db, task.order_id) if order: order_info = { "orderNo": order.order_no, "contactPerson": order.contact_person, "contactPhone": order.contact_phone, "deliveryAddress": order.delivery_address, "orderRemark": order.remark, } # 获取任务明细(对齐运营端商品展示字段,不含价格) items = delivery_task_item_crud.get_by_task(db, task_id) item_details = [] total_quantity = 0.0 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 measure_value = _numeric_to_float_optional(getattr(product, "measure_value", None)) if product else None qty = float(item.quantity) if item.quantity else 0 total_quantity += qty item_details.append({ "id": item.id, "productId": item.product_id, "productName": product.name if product else None, "imageUrl": image_url, "measureValue": measure_value, "unitName": unit_row.name if unit_row else None, "productUnitName": unit_row.name if unit_row else None, "quantity": qty, "deliveredQuantity": float(item.delivered_quantity) if item.delivered_quantity else 0, "clientName": item.contact_person, "clientPhone": item.contact_phone, "clientAddress": 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(",") supplier_prepare_images = [ a.file_url for a in attachment_crud.get_by_business(db, "delivery_prepare", task_id) if getattr(a, "file_url", None) ] return success_response({ "id": task.id, "taskNo": task.task_no, "orderId": task.order_id, "orderNo": order_info.get("orderNo"), "orderRemark": order_info.get("orderRemark"), "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"), }, "status": task.status, "delivererStatus": getattr(task, "deliverer_status", None), "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None), "totalQuantity": total_quantity, "createTime": str(task.create_at) if task.create_at else None, "pickupTime": str(task.pickup_time) if task.pickup_time else None, "pickupRemark": task.pickup_remark, "pickupImages": pickup_images, "supplierPrepareImages": supplier_prepare_images, "deliveryTime": str(task.delivery_time) if task.delivery_time else None, "deliveryRemark": task.delivery_remark, "deliveryImages": delivery_images, "expireTime": str(task.effective_time) if task.effective_time else None, "dispatchRemark": task.dispatch_remark, "items": item_details }) @router.post("/tasks/{task_id}/pickup", response_model=dict) def confirm_pickup( task_id: str, request: dict, db: Session = Depends(get_db), current_user: dict = Depends(require_deliverer) ): """确认取货""" from app.crud.delivery import delivery_task_crud task = delivery_task_crud.get(db, task_id) if not task: return error_response(404, "配送任务不存在") if task.deliver_id != current_user.get("user_id"): return error_response(403, "无权操作此任务") supplier_prepare_status = getattr(task, "supplier_prepare_status", None) or "pending_prepare" deliverer_status = getattr(task, "deliverer_status", None) or "pending_pickup" if deliverer_status != "pending_pickup" or supplier_prepare_status != "prepared": return error_response(400, "当前状态不支持取货(需供应商完成备货)") remark = request.get("remark") image_urls = _normalize_image_urls(request.get("imageUrls", [])) if not image_urls: return error_response(400, "请上传取货凭证照片") task = delivery_task_crud.update_status( db, task_id=task_id, new_status="picking", operator_id=current_user.get("user_id"), remark=remark, image_urls=image_urls, operator_real_name=current_user.get("real_name"), ) if not task: return error_response(400, "操作失败") _save_task_attachments( db, task_id=task_id, business_type="delivery_pickup", image_urls=image_urls, uploader_id=current_user.get("user_id"), uploader_name=current_user.get("real_name"), ) return success_response({ "id": task.id, "status": task.status, "delivererStatus": getattr(task, "deliverer_status", None), "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None), "message": "取货成功" }) @router.post("/tasks/{task_id}/deliver", response_model=dict) def confirm_deliver( task_id: str, request: dict, db: Session = Depends(get_db), current_user: dict = Depends(require_deliverer) ): """确认送达""" from app.crud.delivery import delivery_task_crud, delivery_task_item_crud from app.crud.order import order_crud from app.core.order_status_compat import derive_legacy_status_from_three task = delivery_task_crud.get(db, task_id) if not task: return error_response(404, "配送任务不存在") if task.deliver_id != current_user.get("user_id"): return error_response(403, "无权操作此任务") ds = (getattr(task, "deliverer_status", None) or "").strip() or "pending_pickup" ss = (getattr(task, "supplier_prepare_status", None) or "").strip() # 正常:已取货后为 delivering;兼容历史 bug:pending_pickup + 备货侧已 picked_up 仍允许送达 if ds != "delivering" and not (ds == "pending_pickup" and ss == "picked_up"): return error_response(400, "当前状态不支持送达操作(请先确认取货)") items_data = request.get("items", []) remark = request.get("remark") image_urls = _normalize_image_urls(request.get("imageUrls", [])) if not image_urls: return error_response(400, "请上传送达凭证照片") # 更新任务状态 task = delivery_task_crud.update_status( db, task_id=task_id, new_status="completed", operator_id=current_user.get("user_id"), remark=remark, image_urls=image_urls, operator_real_name=current_user.get("real_name"), ) # 更新任务明细状态 for item_data in items_data: item_id = item_data.get("taskItemId") delivered_quantity = item_data.get("deliveredQuantity", 0) delivery_task_item_crud.update_item_status( db, item_id=item_id, delivered_quantity=delivered_quantity, status="delivered" ) # 配送员送货成功后,订单配送状态变为配送完成 order = order_crud.get(db, task.order_id) if task.order_id else None if order and getattr(order, "order_status", None) != "cancelled": order.delivery_status = "delivered" if not getattr(order, "delivered_time", None): order.delivered_time = datetime.now() if not getattr(order, "order_status", None): 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() _save_task_attachments( db, task_id=task_id, business_type="delivery_deliver", image_urls=image_urls, uploader_id=current_user.get("user_id"), uploader_name=current_user.get("real_name"), ) return success_response({ "id": task.id, "status": task.status, "delivererStatus": getattr(task, "deliverer_status", None), "supplierPrepareStatus": getattr(task, "supplier_prepare_status", None), "message": "送达确认成功" }) @router.get("/statistics/efficiency", response_model=dict) def get_deliverer_efficiency_statistics( db: Session = Depends(get_db), current_user: dict = Depends(require_deliverer) ): """获取配送员效率统计(小程序端工作台用)""" from app.crud.delivery import delivery_task_crud from app.crud.deliverer_review import deliverer_review_crud deliver_id = current_user.get("user_id") # 时间范围 now = datetime.now() today = now.replace(hour=0, minute=0, second=0, microsecond=0) week_start = today.replace(day=today.day - today.weekday()) month_start = today.replace(day=1) # 获取已完成任务(按配送员状态) completed_tasks, _ = delivery_task_crud.get_by_deliver( db, deliver_id=deliver_id, deliverer_status="delivered", skip=0, limit=5000 ) # 今日数据 today_completed = [t for t in completed_tasks if t.delivery_time and t.delivery_time >= today] # 本周数据 week_completed = [t for t in completed_tasks if t.delivery_time and t.delivery_time >= week_start] # 本月数据 month_completed = [t for t in completed_tasks if t.delivery_time and t.delivery_time >= month_start] # 计算平均配送时长 def calc_avg_delivery_minutes(tasks): minutes_list = [] for task in tasks: if task.pickup_time and task.delivery_time: delta = task.delivery_time - task.pickup_time minutes_list.append(int(delta.total_seconds() / 60)) return sum(minutes_list) / len(minutes_list) if minutes_list else 0 # 计算准时率(假设承诺时长为60分钟) def calc_on_time_rate(tasks): if not tasks: return 100.0 on_time_count = sum(1 for t in tasks if t.pickup_time and t.delivery_time) return round((on_time_count / len(tasks)) * 100, 1) if tasks else 100.0 # 获取评价统计 review_stats = deliverer_review_crud.get_deliverer_rating_stats(db, deliver_id) return success_response({ "today": { "completedCount": len(today_completed), "avgDeliveryMinutes": round(calc_avg_delivery_minutes(today_completed), 1), "onTimeRate": calc_on_time_rate(today_completed) }, "week": { "completedCount": len(week_completed), "avgDeliveryMinutes": round(calc_avg_delivery_minutes(week_completed), 1), "onTimeRate": calc_on_time_rate(week_completed) }, "month": { "completedCount": len(month_completed), "avgDeliveryMinutes": round(calc_avg_delivery_minutes(month_completed), 1), "onTimeRate": calc_on_time_rate(month_completed) }, "rating": round(review_stats.get("avgScore", 0), 1), "totalCompleted": len(completed_tasks), "totalDistance": len(completed_tasks) * 5 # 估算配送距离 }) # ========================================= # 个人资料管理 # ========================================= @router.get("/profile", response_model=dict) def get_deliverer_profile( db: Session = Depends(get_db), current_user: dict = Depends(require_deliverer) ): """获取配送员个人资料""" user = user_crud.get(db, current_user.get("user_id")) if not user: return error_response(404, "用户不存在") region_name = None if user.region_id: region = region_crud.get(db, user.region_id) region_name = region.name if region else None return success_response({ "id": user.id, "username": user.username, "realName": user.real_name, "phone": user.phone, "role": user.role, "regionId": user.region_id, "regionName": region_name, "isActive": user.is_active }) @router.put("/profile", response_model=dict) def update_deliverer_profile( request: dict, db: Session = Depends(get_db), current_user: dict = Depends(require_deliverer) ): """更新配送员个人资料""" user = user_crud.get(db, current_user.get("user_id")) if not user: return error_response(404, "用户不存在") updater = {"user_id": current_user.get("user_id"), "real_name": current_user.get("real_name")} user = user_crud.update_with_history(db, current_user.get("user_id"), request, updater) return success_response({ "id": user.id, "realName": user.real_name, "phone": user.phone }) # ========================================= # 抢单功能 # ========================================= @router.get("/snatch/pending", response_model=dict) def list_pending_snatch_tasks( page: int = Query(1, ge=1, description="页码"), pageSize: int = Query(10, ge=1, le=50, description="每页条数"), db: Session = Depends(get_db), current_user: dict = Depends(require_deliverer) ): """获取待抢单的任务列表 返回所有未分配给特定配送员的任务,配送员可以抢单 """ from app.crud.delivery import delivery_task_crud, delivery_task_item_crud # 获取待抢单的任务(status=pending 且 deliver_id 为空) tasks, total = delivery_task_crud.get_pending_snatch_tasks( db, skip=(page - 1) * pageSize, limit=pageSize ) result = [] for task in tasks: # 获取供应商信息 supplier_info = {"name": None, "phone": None, "address": None} if task.supplier_id: supplier = user_crud.get(db, task.supplier_id) if supplier: supplier_info = { "name": supplier.real_name, "phone": supplier.phone, "address": getattr(supplier, 'address', None) } # 获取任务明细 items = delivery_task_item_crud.get_by_task(db, task.id) # 计算配送地址数量和商品数量 client_count = len(set(item.client_id for item in items if item.client_id)) item_count = len(items) total_quantity = sum(float(item.quantity or 0) for item in items) # 计算剩余时间 remaining_minutes = 0 if task.effective_time: remaining = task.effective_time - datetime.now() remaining_minutes = max(0, int(remaining.total_seconds() / 60)) result.append({ "id": task.id, "taskNo": task.task_no, "supplierName": supplier_info.get("name"), "supplierPhone": supplier_info.get("phone"), "supplierAddress": supplier_info.get("address"), "clientCount": client_count, "itemCount": item_count, "totalQuantity": total_quantity, "status": task.status, "createTime": str(task.create_at) if task.create_at else None, "pickupDeadline": str(task.effective_time) if task.effective_time else None, "remainingMinutes": remaining_minutes }) return success_response({ "list": result, "total": total, "page": page, "pageSize": pageSize, "totalPages": (total + pageSize - 1) // pageSize if total > 0 else 0 }) @router.post("/snatch/{task_id}", response_model=dict) def snatch_task( task_id: str, db: Session = Depends(get_db), current_user: dict = Depends(require_deliverer) ): """抢单 配送员抢单成功后,该任务会分配给当前配送员 """ from app.crud.delivery import delivery_task_crud task = delivery_task_crud.get(db, task_id) if not task: return error_response(404, "配送任务不存在") # 检查任务是否可抢 if task.status != "pending": return error_response(400, "该任务已无法抢单") if task.deliver_id: return error_response(400, "该任务已被其他配送员抢单") # 执行抢单 deliver_id = current_user.get("user_id") deliver_name = current_user.get("real_name") or current_user.get("username", "") success = delivery_task_crud.assign_to_deliver( db, task_id=task_id, deliver_id=deliver_id, deliver_name=deliver_name ) if not success: return error_response(400, "抢单失败,请稍后重试") return success_response({ "id": task_id, "message": "抢单成功" }) @router.get("/snatch/my", response_model=dict) def get_my_snatched_tasks( page: int = Query(1, ge=1, description="页码"), pageSize: int = Query(10, ge=1, le=100, description="每页条数"), db: Session = Depends(get_db), current_user: dict = Depends(require_deliverer) ): """获取我抢到的任务列表 返回当前配送员已抢但尚未开始配送的任务 """ from app.crud.delivery import delivery_task_crud, delivery_task_item_crud deliver_id = current_user.get("user_id") # 获取已抢到的任务(状态为 pending/assigned 但已分配给当前配送员) tasks, total = delivery_task_crud.get_snatched_by_me( db, deliver_id=deliver_id, skip=(page - 1) * pageSize, limit=pageSize ) result = [] for task in tasks: # 获取供应商信息 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} # 获取任务明细 items = delivery_task_item_crud.get_by_task(db, task.id) # 计算剩余时间 remaining_minutes = 0 if task.effective_time: remaining = task.effective_time - datetime.now() remaining_minutes = max(0, int(remaining.total_seconds() / 60)) result.append({ "id": task.id, "taskNo": task.task_no, "supplierName": supplier_info.get("name"), "supplierPhone": supplier_info.get("phone"), "clientCount": len(set(item.client_id for item in items if item.client_id)), "itemCount": len(items), "status": task.status, "createTime": str(task.create_at) if task.create_at else None, "pickupDeadline": str(task.effective_time) if task.effective_time else None, "remainingMinutes": remaining_minutes }) return success_response({ "list": result, "total": total, "page": page, "pageSize": pageSize, "totalPages": (total + pageSize - 1) // pageSize if total > 0 else 0 }) @router.delete("/snatch/{task_id}", response_model=dict) def cancel_snatched_task( task_id: str, db: Session = Depends(get_db), current_user: dict = Depends(require_deliverer) ): """取消抢单 配送员可以取消自己抢到但尚未开始配送的任务 """ from app.crud.delivery import delivery_task_crud task = delivery_task_crud.get(db, task_id) if not task: return error_response(404, "配送任务不存在") # 检查权限 if task.deliver_id != current_user.get("user_id"): return error_response(403, "无权操作此任务") # 检查状态 if task.status not in ["pending", "assigned"]: return error_response(400, "该任务已开始配送,无法取消") # 取消抢单 success = delivery_task_crud.cancel_snatch(db, task_id) if not success: return error_response(400, "取消失败,请稍后重试") return success_response({"message": "取消成功"}) @router.get("/snatch/statistics", response_model=dict) def get_snatch_statistics( db: Session = Depends(get_db), current_user: dict = Depends(require_deliverer) ): """获取抢单统计""" from app.crud.delivery import delivery_task_crud deliver_id = current_user.get("user_id") # 获取今日可抢单数量 pending_count = delivery_task_crud.count_pending_snatch_tasks(db) # 获取我抢到的任务数量 snatched_count = delivery_task_crud.count_snatched_by_me(db, deliver_id) # 获取今日抢单成功数量 today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) today_snatched_count = delivery_task_crud.count_today_snatched(db, deliver_id, today) return success_response({ "pendingCount": pending_count, "mySnatchedCount": snatched_count, "todaySnatchedCount": today_snatched_count })