| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117 |
- """配送员端接口"""
- 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.core.request_payload import resolve_request_payload
- 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 _resolve_request_payload(request_body: dict, current_user: dict) -> dict:
- return resolve_request_payload(request_body, current_user, source="mini_deliverer")
- 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, "当前状态不支持取货(需供应商完成备货)")
- payload = _resolve_request_payload(request, current_user)
- remark = payload.get("remark")
- image_urls = _normalize_image_urls(payload.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, "当前状态不支持送达操作(请先确认取货)")
- payload = _resolve_request_payload(request, current_user)
- items_data = payload.get("items", [])
- remark = payload.get("remark")
- image_urls = _normalize_image_urls(payload.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, "用户不存在")
- payload = _resolve_request_payload(request, current_user)
- 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"), payload, 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
- })
|