delivery.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811
  1. """配送管理接口"""
  2. from typing import Optional, List
  3. from datetime import datetime
  4. from decimal import Decimal
  5. from fastapi import APIRouter, Depends, Query
  6. from sqlalchemy.orm import Session
  7. from sqlalchemy import and_, desc, func
  8. from app.core.database import get_db
  9. from app.core.auth import get_current_user
  10. from app.core.response import success_response, error_response
  11. from app.crud.region import region_crud
  12. from app.crud.user import user_crud
  13. from app.crud.order import order_crud, order_item_crud
  14. from app.crud.product import product_crud
  15. from app.crud.unit import unit_crud
  16. from app.crud.address import address_crud
  17. from app.core.order_status_compat import derive_legacy_status_from_three
  18. from app.models.models import BizPriceListItem, SysRegion, SysUser, BizOrder, BizDeliveryTaskItem, BasAddress
  19. from app.api.admin import _resolve_admin_payload_with_crypto_fallback
  20. router = APIRouter(prefix="/web", tags=["运营端-配送管理"])
  21. def _numeric_to_float_optional(v) -> Optional[float]:
  22. if v is None:
  23. return None
  24. return float(v)
  25. def _category_path_display(db: Session, category_id: Optional[str]) -> str:
  26. """一级/二级分类用 / 连接(与订单详情一致)。"""
  27. from app.crud.category import category_crud
  28. if not category_id:
  29. return ""
  30. names: List[str] = []
  31. cid = str(category_id).strip()
  32. seen = set()
  33. depth = 0
  34. while cid and depth < 24 and cid not in seen:
  35. seen.add(cid)
  36. cat = category_crud.get(db, cid)
  37. if not cat:
  38. break
  39. names.insert(0, (getattr(cat, "name", None) or "").strip())
  40. pid = getattr(cat, "parent_id", None)
  41. cid = str(pid).strip() if pid else ""
  42. depth += 1
  43. return "/".join([n for n in names if n])
  44. def _supplier_unit_yuan_from_pli(pli: Optional[BizPriceListItem]) -> Optional[float]:
  45. """价格清单行 price 为面向客户的最终单价;pricing_coefficient 为调价系数(%),供货单价 = price / (1 + 系数/100)。"""
  46. if pli is None or getattr(pli, "price", None) is None:
  47. return None
  48. final_price = Decimal(str(pli.price))
  49. raw_coeff = getattr(pli, "pricing_coefficient", None)
  50. if raw_coeff is None:
  51. coeff_pct = Decimal("0")
  52. else:
  53. try:
  54. coeff_pct = Decimal(str(raw_coeff))
  55. except Exception:
  56. coeff_pct = Decimal("0")
  57. factor = Decimal("1") + coeff_pct / Decimal("100")
  58. if factor <= Decimal("0"):
  59. factor = Decimal("1")
  60. supplier_unit = (final_price / factor).quantize(Decimal("0.01"))
  61. return float(supplier_unit)
  62. def _price_list_item_map(db: Session, price_list_ids: List[str]) -> dict:
  63. """(price_list_id, product_id, unit_id) -> BizPriceListItem"""
  64. if not price_list_ids:
  65. return {}
  66. rows = (
  67. db.query(BizPriceListItem)
  68. .filter(BizPriceListItem.price_list_id.in_(price_list_ids), BizPriceListItem.is_del == 0)
  69. .all()
  70. )
  71. return {(r.price_list_id, r.product_id, r.unit_id): r for r in rows}
  72. def _supplier_pickup_address_block(db: Session, supplier_id: Optional[str]) -> dict:
  73. """供应商默认取货地址;无默认时取第一条 pickup 类型地址。"""
  74. empty = {"fullAddress": None, "contactPerson": None, "contactPhone": None}
  75. if not supplier_id:
  76. return empty
  77. addr = address_crud.get_default(db, supplier_id, "pickup")
  78. if not addr:
  79. rows = address_crud.get_by_user_and_type(db, supplier_id, "pickup")
  80. addr = rows[0] if rows else None
  81. if not addr:
  82. return empty
  83. segs = [
  84. getattr(addr, "province", None),
  85. getattr(addr, "city", None),
  86. getattr(addr, "district", None),
  87. getattr(addr, "detail_address", None),
  88. ]
  89. full = "".join(s for s in segs if s)
  90. return {
  91. "fullAddress": full or None,
  92. "contactPerson": addr.contact_person,
  93. "contactPhone": addr.contact_phone,
  94. }
  95. def _batch_supplier_pickup_address_map(db: Session, supplier_ids: List[str]) -> dict:
  96. """批量获取供应商取货地址,避免列表接口逐条查询。"""
  97. if not supplier_ids:
  98. return {}
  99. rows = (
  100. db.query(BasAddress)
  101. .filter(
  102. BasAddress.user_id.in_(supplier_ids),
  103. BasAddress.address_type == "pickup",
  104. BasAddress.is_del == 0,
  105. )
  106. .order_by(BasAddress.user_id.asc(), BasAddress.is_default.desc(), BasAddress.create_at.desc(), BasAddress.id.desc())
  107. .all()
  108. )
  109. addr_map = {}
  110. for addr in rows:
  111. uid = str(getattr(addr, "user_id", "") or "")
  112. if not uid or uid in addr_map:
  113. continue
  114. segs = [
  115. getattr(addr, "province", None),
  116. getattr(addr, "city", None),
  117. getattr(addr, "district", None),
  118. getattr(addr, "detail_address", None),
  119. ]
  120. full = "".join(s for s in segs if s)
  121. addr_map[uid] = {
  122. "fullAddress": full or None,
  123. "contactPerson": getattr(addr, "contact_person", None),
  124. "contactPhone": getattr(addr, "contact_phone", None),
  125. }
  126. return addr_map
  127. def require_admin(current_user: dict = Depends(get_current_user)):
  128. """验证运营人员权限"""
  129. if current_user.get("role") not in {"admin", "super_admin"}:
  130. return error_response(403, "需要运营人员权限")
  131. return current_user
  132. # 配送任务状态映射
  133. DELIVERY_STATUS_MAP = {
  134. "pending": "待取货",
  135. "assigned": "已分配",
  136. "picking": "取货中",
  137. "delivering": "配送中",
  138. "completed": "已完成",
  139. "cancelled": "已取消"
  140. }
  141. @router.get("/delivery-tasks", response_model=dict)
  142. def list_delivery_tasks(
  143. page: int = Query(1, ge=1, description="页码"),
  144. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  145. regionId: Optional[str] = Query(None, description="区域ID"),
  146. status: Optional[str] = Query(None, description="状态"),
  147. orderNo: Optional[str] = Query(None, description="订单号"),
  148. db: Session = Depends(get_db),
  149. current_user: dict = Depends(require_admin)
  150. ):
  151. """获取配送任务列表"""
  152. from app.crud.delivery import delivery_task_crud
  153. tasks, total = delivery_task_crud.get_multi_with_filters(
  154. db,
  155. region_id=regionId,
  156. status=status,
  157. order_no=orderNo,
  158. skip=(page - 1) * pageSize,
  159. limit=pageSize
  160. )
  161. region_map = {}
  162. supplier_map = {}
  163. deliver_map = {}
  164. client_map = {}
  165. task_ids = [t.id for t in tasks if getattr(t, "id", None)]
  166. region_ids = list({t.region_id for t in tasks if getattr(t, "region_id", None)})
  167. supplier_ids = list({t.supplier_id for t in tasks if getattr(t, "supplier_id", None)})
  168. deliver_ids = list({t.deliver_id for t in tasks if getattr(t, "deliver_id", None)})
  169. order_ids = list({t.order_id for t in tasks if getattr(t, "order_id", None)})
  170. # 批量加载区域
  171. if region_ids:
  172. regions = db.query(SysRegion).filter(SysRegion.id.in_(region_ids), SysRegion.is_del == 0).all()
  173. region_map = {r.id: r.name for r in regions}
  174. # 批量加载供应商/配送员
  175. user_ids = list({*supplier_ids, *deliver_ids})
  176. if user_ids:
  177. users = db.query(SysUser).filter(SysUser.id.in_(user_ids), SysUser.is_del == 0).all()
  178. user_map = {u.id: u for u in users}
  179. for sid in supplier_ids:
  180. u = user_map.get(sid)
  181. supplier_map[sid] = {"name": u.real_name if u else None, "phone": u.phone if u else None}
  182. for did in deliver_ids:
  183. u = user_map.get(did)
  184. deliver_map[did] = {"name": u.real_name if u else None, "phone": u.phone if u else None}
  185. # 批量加载订单与客户
  186. order_map = {}
  187. if order_ids:
  188. orders = db.query(BizOrder).filter(BizOrder.id.in_(order_ids), BizOrder.is_del == 0).all()
  189. order_map = {o.id: o for o in orders}
  190. client_ids = list({o.client_id for o in orders if getattr(o, "client_id", None)})
  191. if client_ids:
  192. clients = db.query(SysUser).filter(SysUser.id.in_(client_ids), SysUser.is_del == 0).all()
  193. client_map = {u.id: {"name": u.real_name if u else None, "phone": u.phone if u else None} for u in clients}
  194. # 批量聚合任务明细数量
  195. item_count_map = {}
  196. if task_ids:
  197. rows = (
  198. db.query(BizDeliveryTaskItem.task_id, func.count(BizDeliveryTaskItem.id))
  199. .filter(BizDeliveryTaskItem.task_id.in_(task_ids), BizDeliveryTaskItem.is_del == 0)
  200. .group_by(BizDeliveryTaskItem.task_id)
  201. .all()
  202. )
  203. item_count_map = {task_id: int(cnt or 0) for task_id, cnt in rows}
  204. # 批量供应商取货地址
  205. pickup_addr_map = _batch_supplier_pickup_address_map(db, supplier_ids)
  206. result = []
  207. for task in tasks:
  208. # 获取订单信息
  209. order_info = {}
  210. if task.order_id:
  211. order = order_map.get(task.order_id)
  212. if order:
  213. order_info = {
  214. "orderNo": order.order_no,
  215. "totalAmount": float(order.total_amount) if order.total_amount else 0,
  216. "clientId": order.client_id,
  217. "clientName": client_map.get(order.client_id, {}).get("name") if order.client_id else None,
  218. "contactPerson": order.contact_person,
  219. "contactPhone": order.contact_phone,
  220. "deliveryAddress": order.delivery_address
  221. }
  222. item_count = item_count_map.get(task.id, 0)
  223. # 供应商取货地址(用于列表展示)
  224. supplier_pickup = pickup_addr_map.get(task.supplier_id, {"fullAddress": None, "contactPerson": None, "contactPhone": None})
  225. result.append({
  226. "id": task.id,
  227. "taskNo": task.task_no,
  228. "orderId": task.order_id,
  229. "orderNo": order_info.get("orderNo"),
  230. "regionId": task.region_id,
  231. "regionName": region_map.get(task.region_id),
  232. "supplierId": task.supplier_id,
  233. "supplierName": supplier_map.get(task.supplier_id, {}).get("name"),
  234. "supplierAddress": supplier_pickup.get("fullAddress"),
  235. "deliverId": task.deliver_id,
  236. "deliverName": deliver_map.get(task.deliver_id, {}).get("name"),
  237. "deliverPhone": deliver_map.get(task.deliver_id, {}).get("phone"),
  238. "status": task.status,
  239. "totalAmount": order_info.get("totalAmount"),
  240. "itemCount": item_count,
  241. "clientId": order_info.get("clientId"),
  242. "clientName": order_info.get("clientName"),
  243. "contactPerson": order_info.get("contactPerson"),
  244. "contactPhone": order_info.get("contactPhone"),
  245. "deliveryAddress": order_info.get("deliveryAddress"),
  246. "dispatchRemark": task.dispatch_remark,
  247. "createAt": str(task.create_at) if task.create_at else None,
  248. "assignTime": str(task.create_at) if task.deliver_id else None,
  249. "pickupTime": str(task.pickup_time) if task.pickup_time else None,
  250. "deliveryTime": str(task.delivery_time) if task.delivery_time else None,
  251. "expireTime": str(task.effective_time) if task.effective_time else None,
  252. "isExpiring": False
  253. })
  254. return success_response({
  255. "list": result,
  256. "total": total,
  257. "page": page,
  258. "pageSize": pageSize,
  259. "totalPages": (total + pageSize - 1) // pageSize
  260. })
  261. @router.get("/delivery-tasks/{task_id}", response_model=dict)
  262. def get_delivery_task(
  263. task_id: str,
  264. db: Session = Depends(get_db),
  265. current_user: dict = Depends(require_admin)
  266. ):
  267. """获取配送任务详情"""
  268. from app.crud.delivery import delivery_task_crud, delivery_task_item_crud
  269. task = delivery_task_crud.get(db, task_id)
  270. if not task:
  271. return error_response(404, "配送任务不存在")
  272. # 获取任务明细
  273. items = delivery_task_item_crud.get_by_task(db, task_id)
  274. # 获取关联信息
  275. region_name = None
  276. if task.region_id:
  277. region = region_crud.get(db, task.region_id)
  278. region_name = region.name if region else None
  279. supplier_info = {"name": None, "phone": None}
  280. if task.supplier_id:
  281. supplier = user_crud.get(db, task.supplier_id)
  282. if supplier:
  283. supplier_info = {"name": supplier.real_name, "phone": supplier.phone}
  284. deliver_info = {"name": None, "phone": None}
  285. if task.deliver_id:
  286. deliver = user_crud.get(db, task.deliver_id)
  287. if deliver:
  288. deliver_info = {"name": deliver.real_name, "phone": deliver.phone}
  289. # 获取订单信息
  290. order_info = {}
  291. order_price_list_id: Optional[str] = None
  292. if task.order_id:
  293. order = order_crud.get(db, task.order_id)
  294. if order:
  295. order_info = {
  296. "orderNo": order.order_no,
  297. "totalAmount": float(order.total_amount) if order.total_amount else 0,
  298. "contactPerson": order.contact_person,
  299. "contactPhone": order.contact_phone,
  300. "deliveryAddress": order.delivery_address
  301. }
  302. order_price_list_id = getattr(order, "price_list_id", None)
  303. pickup_addr = _supplier_pickup_address_block(db, task.supplier_id)
  304. # 处理任务明细(商品展示字段对齐运营端订单明细)
  305. item_details = []
  306. pli_map = _price_list_item_map(db, [order_price_list_id]) if order_price_list_id else {}
  307. for item in items:
  308. product = product_crud.get(db, item.product_id)
  309. order_line = order_item_crud.get(db, item.order_item_id) if item.order_item_id else None
  310. unit_row = None
  311. if order_line and order_line.unit_id:
  312. unit_row = unit_crud.get(db, order_line.unit_id)
  313. elif product and product.unit_id:
  314. unit_row = unit_crud.get(db, product.unit_id)
  315. image_url = getattr(product, "image_url", None) if product else None
  316. category_path = ""
  317. if product and getattr(product, "category_id", None):
  318. category_path = _category_path_display(db, str(product.category_id))
  319. measure_value = _numeric_to_float_optional(getattr(product, "measure_value", None)) if product else None
  320. qty = float(item.quantity) if item.quantity else 0.0
  321. unit_price = float(order_line.unit_price) if order_line and order_line.unit_price is not None else None
  322. amount = float(order_line.amount) if order_line and order_line.amount is not None else None
  323. if amount is None and unit_price is not None:
  324. amount = round(qty * float(unit_price), 2)
  325. client = user_crud.get(db, item.client_id) if item.client_id else None
  326. # 供货/售价字段:对齐「订单详情」逻辑:优先使用价格清单计算供货单价;无价格清单时兜底订单行售价
  327. selling_unit_price = unit_price
  328. selling_amount = amount
  329. unit_id_for_price = None
  330. if order_line and getattr(order_line, "unit_id", None):
  331. unit_id_for_price = order_line.unit_id
  332. elif product and getattr(product, "unit_id", None):
  333. unit_id_for_price = product.unit_id
  334. supplier_unit_price = None
  335. if order_price_list_id and item.product_id and unit_id_for_price:
  336. pli = pli_map.get((order_price_list_id, item.product_id, unit_id_for_price))
  337. supplier_unit_price = _supplier_unit_yuan_from_pli(pli)
  338. if supplier_unit_price is None:
  339. supplier_unit_price = unit_price
  340. supplier_amount = None
  341. if supplier_unit_price is not None:
  342. supplier_amount = round(qty * float(supplier_unit_price), 2)
  343. item_details.append({
  344. "id": item.id,
  345. "orderItemId": item.order_item_id,
  346. "productId": item.product_id,
  347. "productName": product.name if product else None,
  348. "imageUrl": image_url,
  349. "categoryName": category_path or None,
  350. "measureValue": measure_value,
  351. "unitName": unit_row.name if unit_row else None,
  352. "unitPrice": unit_price,
  353. "amount": amount,
  354. "sellingUnitPrice": selling_unit_price,
  355. "sellingAmount": selling_amount,
  356. "supplierId": task.supplier_id,
  357. "supplierName": supplier_info.get("name"),
  358. "supplierUnitPrice": supplier_unit_price,
  359. "supplierAmount": supplier_amount,
  360. "quantity": qty,
  361. "deliveredQuantity": float(item.delivered_quantity) if item.delivered_quantity else 0,
  362. "clientName": client.real_name if client else None,
  363. "clientPhone": client.phone if client else None,
  364. "clientAddress": item.delivery_address,
  365. "contactPerson": item.contact_person,
  366. "contactPhone": item.contact_phone,
  367. "deliveryAddress": item.delivery_address,
  368. "status": item.status
  369. })
  370. # 处理图片
  371. pickup_images = []
  372. if task.pickup_image_url:
  373. pickup_images = task.pickup_image_url.split(",")
  374. delivery_images = []
  375. if task.delivery_image_url:
  376. delivery_images = task.delivery_image_url.split(",")
  377. return success_response({
  378. "id": task.id,
  379. "taskNo": task.task_no,
  380. "orderId": task.order_id,
  381. "orderNo": order_info.get("orderNo"),
  382. "regionId": task.region_id,
  383. "regionName": region_name,
  384. "supplierId": task.supplier_id,
  385. "supplierName": supplier_info.get("name"),
  386. "supplierPhone": supplier_info.get("phone"),
  387. "supplierAddress": pickup_addr.get("fullAddress"),
  388. "supplierPickup": {
  389. "address": pickup_addr.get("fullAddress"),
  390. "contactPerson": pickup_addr.get("contactPerson"),
  391. "contactPhone": pickup_addr.get("contactPhone"),
  392. },
  393. "customerDelivery": {
  394. "address": order_info.get("deliveryAddress"),
  395. "contactPerson": order_info.get("contactPerson"),
  396. "contactPhone": order_info.get("contactPhone"),
  397. },
  398. "deliverId": task.deliver_id,
  399. "deliverName": deliver_info.get("name"),
  400. "deliverPhone": deliver_info.get("phone"),
  401. "status": task.status,
  402. "totalAmount": order_info.get("totalAmount"),
  403. "createAt": str(task.create_at) if task.create_at else None,
  404. "assignTime": str(task.create_at) if task.deliver_id else None,
  405. "pickupTime": str(task.pickup_time) if task.pickup_time else None,
  406. "pickupRemark": task.pickup_remark,
  407. "pickupImages": pickup_images,
  408. "deliveryTime": str(task.delivery_time) if task.delivery_time else None,
  409. "deliveryRemark": task.delivery_remark,
  410. "deliveryImages": delivery_images,
  411. "dispatchRemark": task.dispatch_remark,
  412. "items": item_details
  413. })
  414. @router.post("/delivery-tasks/assign", response_model=dict)
  415. def assign_delivery_task(
  416. request: dict,
  417. db: Session = Depends(get_db),
  418. current_user: dict = Depends(require_admin)
  419. ):
  420. """分配配送任务"""
  421. request, req_err = _resolve_admin_payload_with_crypto_fallback(request, current_user)
  422. if req_err is not None:
  423. return req_err
  424. from app.crud.delivery import delivery_task_crud
  425. from app.crud.order import order_crud
  426. order_id = request.get("orderId")
  427. deliver_id = request.get("deliverId")
  428. if not order_id:
  429. return error_response(400, "请选择订单")
  430. if not deliver_id:
  431. return error_response(400, "请选择配送员")
  432. # 验证配送员存在
  433. deliver = user_crud.get(db, deliver_id)
  434. if not deliver:
  435. return error_response(404, "配送员不存在")
  436. if deliver.role != "deliver":
  437. return error_response(400, "该用户不是配送员")
  438. task = delivery_task_crud.assign_deliver(
  439. db,
  440. order_id=order_id,
  441. deliver_id=deliver_id,
  442. operator_id=current_user.get("user_id")
  443. )
  444. if not task:
  445. return error_response(400, "分配失败,请检查订单状态")
  446. # 阶段D前收尾:分配配送优先维护 delivery_status
  447. order = order_crud.get(db, order_id)
  448. if order:
  449. order.delivery_status = "delivering"
  450. if order.order_status != "cancelled":
  451. order.order_status = "pending_confirm"
  452. order.status = derive_legacy_status_from_three(
  453. order_status=order.order_status,
  454. delivery_status=order.delivery_status,
  455. pay_status=order.pay_status,
  456. )
  457. order.update_at = datetime.now()
  458. db.commit()
  459. return success_response({
  460. "id": task.id,
  461. "status": task.status,
  462. "message": "分配成功"
  463. })
  464. @router.post("/delivery-tasks/assign-batch", response_model=dict)
  465. def assign_delivery_task_batch(
  466. request: dict,
  467. db: Session = Depends(get_db),
  468. current_user: dict = Depends(require_admin)
  469. ):
  470. """批量分配配送任务"""
  471. request, req_err = _resolve_admin_payload_with_crypto_fallback(request, current_user)
  472. if req_err is not None:
  473. return req_err
  474. from app.crud.delivery import delivery_task_crud
  475. from app.crud.order import order_crud
  476. assignments = request.get("assignments", [])
  477. if not assignments:
  478. return error_response(400, "请选择要分配的订单")
  479. results = []
  480. for assignment in assignments:
  481. order_id = assignment.get("orderId")
  482. deliver_id = assignment.get("deliverId")
  483. if order_id and deliver_id:
  484. task = delivery_task_crud.assign_deliver(
  485. db,
  486. order_id=order_id,
  487. deliver_id=deliver_id,
  488. operator_id=current_user.get("user_id")
  489. )
  490. if task:
  491. # 阶段D前收尾:分配配送优先维护 delivery_status
  492. order = order_crud.get(db, order_id)
  493. if order:
  494. order.delivery_status = "delivering"
  495. if order.order_status != "cancelled":
  496. order.order_status = "pending_confirm"
  497. order.status = derive_legacy_status_from_three(
  498. order_status=order.order_status,
  499. delivery_status=order.delivery_status,
  500. pay_status=order.pay_status,
  501. )
  502. order.update_at = datetime.now()
  503. db.commit()
  504. results.append({"orderId": order_id, "success": True})
  505. else:
  506. results.append({"orderId": order_id, "success": False, "reason": "分配失败"})
  507. success_count = sum(1 for r in results if r.get("success"))
  508. return success_response({
  509. "total": len(results),
  510. "success": success_count,
  511. "failed": len(results) - success_count,
  512. "results": results
  513. })
  514. @router.get("/delivery-tasks/warnings", response_model=dict)
  515. def list_delivery_warnings(
  516. page: int = Query(1, ge=1, description="页码"),
  517. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  518. regionId: Optional[str] = Query(None, description="区域ID"),
  519. withinMinutes: int = Query(60, description="多少分钟内即将过期"),
  520. db: Session = Depends(get_db),
  521. current_user: dict = Depends(require_admin)
  522. ):
  523. """获取配送时效预警列表"""
  524. from app.crud.delivery import delivery_task_crud
  525. tasks, total, warning_count = delivery_task_crud.get_warning_tasks(
  526. db,
  527. region_id=regionId,
  528. within_minutes=withinMinutes,
  529. skip=(page - 1) * pageSize,
  530. limit=pageSize
  531. )
  532. region_map = {}
  533. deliver_map = {}
  534. result = []
  535. for task in tasks:
  536. if task.region_id and task.region_id not in region_map:
  537. region = region_crud.get(db, task.region_id)
  538. region_map[task.region_id] = region.name if region else None
  539. if task.deliver_id and task.deliver_id not in deliver_map:
  540. deliver = user_crud.get(db, task.deliver_id)
  541. deliver_map[task.deliver_id] = deliver.real_name if deliver else None
  542. order_info = {}
  543. if task.order_id:
  544. order = order_crud.get(db, task.order_id)
  545. if order:
  546. order_info = {"orderNo": order.order_no}
  547. # 计算剩余时间和预警级别
  548. remaining_minutes = 0
  549. warning_level = "normal"
  550. is_overdue = False
  551. if task.effective_time:
  552. now = datetime.now()
  553. remaining = task.effective_time - now
  554. remaining_minutes = int(remaining.total_seconds() / 60)
  555. if remaining_minutes < 0:
  556. is_overdue = True
  557. warning_level = "overdue"
  558. elif remaining_minutes <= 15:
  559. warning_level = "urgent"
  560. elif remaining_minutes <= 30:
  561. warning_level = "warning"
  562. result.append({
  563. "id": task.id,
  564. "taskNo": task.task_no,
  565. "orderId": task.order_id,
  566. "orderNo": order_info.get("orderNo"),
  567. "regionId": task.region_id,
  568. "regionName": region_map.get(task.region_id),
  569. "deliverId": task.deliver_id,
  570. "deliverName": deliver_map.get(task.deliver_id),
  571. "status": task.status,
  572. "expireTime": str(task.effective_time) if task.effective_time else None,
  573. "remainingMinutes": remaining_minutes,
  574. "isOverdue": is_overdue,
  575. "warningLevel": warning_level
  576. })
  577. return success_response({
  578. "list": result,
  579. "total": total,
  580. "warningCount": warning_count,
  581. "page": page,
  582. "pageSize": pageSize,
  583. "totalPages": (total + pageSize - 1) // pageSize
  584. })
  585. @router.post("/delivery-tasks/{task_id}/cancel", response_model=dict)
  586. def cancel_delivery_task(
  587. task_id: str,
  588. request: dict,
  589. db: Session = Depends(get_db),
  590. current_user: dict = Depends(require_admin)
  591. ):
  592. """取消配送任务"""
  593. request, req_err = _resolve_admin_payload_with_crypto_fallback(request, current_user)
  594. if req_err is not None:
  595. return req_err
  596. from app.crud.delivery import delivery_task_crud
  597. task = delivery_task_crud.get(db, task_id)
  598. if not task:
  599. return error_response(404, "配送任务不存在")
  600. reason = request.get("reason")
  601. task = delivery_task_crud.cancel_task(
  602. db,
  603. task_id=task_id,
  604. reason=reason,
  605. operator_id=current_user.get("user_id"),
  606. operator_real_name=current_user.get("real_name"),
  607. )
  608. if not task:
  609. return error_response(400, "当前状态不支持取消")
  610. return success_response({
  611. "id": task.id,
  612. "status": task.status,
  613. "message": "取消成功"
  614. })
  615. @router.post("/delivery-tasks/{task_id}/reassign", response_model=dict)
  616. def reassign_delivery_task(
  617. task_id: str,
  618. request: dict,
  619. db: Session = Depends(get_db),
  620. current_user: dict = Depends(require_admin)
  621. ):
  622. """重新分配配送任务"""
  623. request, req_err = _resolve_admin_payload_with_crypto_fallback(request, current_user)
  624. if req_err is not None:
  625. return req_err
  626. from app.crud.delivery import delivery_task_crud
  627. task = delivery_task_crud.get(db, task_id)
  628. if not task:
  629. return error_response(404, "配送任务不存在")
  630. new_deliver_id = request.get("deliverId")
  631. if not new_deliver_id:
  632. return error_response(400, "请选择新配送员")
  633. # 验证新配送员
  634. new_deliver = user_crud.get(db, new_deliver_id)
  635. if not new_deliver:
  636. return error_response(404, "配送员不存在")
  637. if new_deliver.role != "deliver":
  638. return error_response(400, "该用户不是配送员")
  639. task = delivery_task_crud.reassign(
  640. db,
  641. task_id=task_id,
  642. new_deliver_id=new_deliver_id,
  643. operator_id=current_user.get("user_id"),
  644. operator_real_name=current_user.get("real_name"),
  645. )
  646. if not task:
  647. return error_response(400, "当前状态不支持重新分配")
  648. return success_response({
  649. "id": task.id,
  650. "deliverId": task.deliver_id,
  651. "message": "重新分配成功"
  652. })
  653. @router.get("/delivery-tasks/pending", response_model=dict)
  654. def list_pending_delivery_tasks(
  655. page: int = Query(1, ge=1, description="页码"),
  656. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  657. regionId: Optional[str] = Query(None, description="区域ID"),
  658. db: Session = Depends(get_db),
  659. current_user: dict = Depends(require_admin)
  660. ):
  661. """获取待分配的配送任务"""
  662. from app.crud.delivery import delivery_task_crud, delivery_task_item_crud
  663. tasks, total = delivery_task_crud.get_pending_list(
  664. db,
  665. region_id=regionId,
  666. skip=(page - 1) * pageSize,
  667. limit=pageSize
  668. )
  669. region_map = {}
  670. result = []
  671. for task in tasks:
  672. if task.region_id and task.region_id not in region_map:
  673. region = region_crud.get(db, task.region_id)
  674. region_map[task.region_id] = region.name if region else None
  675. order_info = {}
  676. if task.order_id:
  677. order = order_crud.get(db, task.order_id)
  678. if order:
  679. order_info = {
  680. "orderNo": order.order_no,
  681. "totalAmount": float(order.total_amount) if order.total_amount else 0,
  682. "contactPerson": order.contact_person,
  683. "contactPhone": order.contact_phone,
  684. "deliveryAddress": order.delivery_address
  685. }
  686. items = delivery_task_item_crud.get_by_task(db, task.id)
  687. result.append({
  688. "id": task.id,
  689. "taskNo": task.task_no,
  690. "orderId": task.order_id,
  691. "orderNo": order_info.get("orderNo"),
  692. "regionId": task.region_id,
  693. "regionName": region_map.get(task.region_id),
  694. "status": task.status,
  695. "totalAmount": order_info.get("totalAmount"),
  696. "itemCount": len(items),
  697. "createAt": str(task.create_at) if task.create_at else None
  698. })
  699. return success_response({
  700. "list": result,
  701. "total": total,
  702. "page": page,
  703. "pageSize": pageSize,
  704. "totalPages": (total + pageSize - 1) // pageSize
  705. })