delivery.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826
  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="订单号(兼容:仅按配送单号 task_no 模糊匹配)"),
  148. keyword: Optional[str] = Query(None, description="关键词:配送单号/订单编号/联系人/电话等(与运营端筛选一致)"),
  149. db: Session = Depends(get_db),
  150. current_user: dict = Depends(require_admin)
  151. ):
  152. """获取配送任务列表"""
  153. from app.crud.delivery import delivery_task_crud
  154. tasks, total = delivery_task_crud.get_multi_with_filters(
  155. db,
  156. region_id=regionId,
  157. status=status,
  158. order_no=orderNo,
  159. keyword=keyword,
  160. skip=(page - 1) * pageSize,
  161. limit=pageSize
  162. )
  163. region_map = {}
  164. supplier_map = {}
  165. deliver_map = {}
  166. client_map = {}
  167. task_ids = [t.id for t in tasks if getattr(t, "id", None)]
  168. region_ids = list({t.region_id for t in tasks if getattr(t, "region_id", None)})
  169. supplier_ids = list({t.supplier_id for t in tasks if getattr(t, "supplier_id", None)})
  170. deliver_ids = list({t.deliver_id for t in tasks if getattr(t, "deliver_id", None)})
  171. order_ids = list({t.order_id for t in tasks if getattr(t, "order_id", None)})
  172. # 批量加载区域
  173. if region_ids:
  174. regions = db.query(SysRegion).filter(SysRegion.id.in_(region_ids), SysRegion.is_del == 0).all()
  175. region_map = {r.id: r.name for r in regions}
  176. # 批量加载供应商/配送员
  177. user_ids = list({*supplier_ids, *deliver_ids})
  178. if user_ids:
  179. users = db.query(SysUser).filter(SysUser.id.in_(user_ids), SysUser.is_del == 0).all()
  180. user_map = {u.id: u for u in users}
  181. for sid in supplier_ids:
  182. u = user_map.get(sid)
  183. supplier_map[sid] = {"name": u.real_name if u else None, "phone": u.phone if u else None}
  184. for did in deliver_ids:
  185. u = user_map.get(did)
  186. deliver_map[did] = {"name": u.real_name if u else None, "phone": u.phone if u else None}
  187. # 批量加载订单与客户
  188. order_map = {}
  189. if order_ids:
  190. orders = db.query(BizOrder).filter(BizOrder.id.in_(order_ids), BizOrder.is_del == 0).all()
  191. order_map = {o.id: o for o in orders}
  192. client_ids = list({o.client_id for o in orders if getattr(o, "client_id", None)})
  193. if client_ids:
  194. clients = db.query(SysUser).filter(SysUser.id.in_(client_ids), SysUser.is_del == 0).all()
  195. client_map = {u.id: {"name": u.real_name if u else None, "phone": u.phone if u else None} for u in clients}
  196. # 批量聚合任务明细数量
  197. item_count_map = {}
  198. if task_ids:
  199. rows = (
  200. db.query(BizDeliveryTaskItem.task_id, func.count(BizDeliveryTaskItem.id))
  201. .filter(BizDeliveryTaskItem.task_id.in_(task_ids), BizDeliveryTaskItem.is_del == 0)
  202. .group_by(BizDeliveryTaskItem.task_id)
  203. .all()
  204. )
  205. item_count_map = {task_id: int(cnt or 0) for task_id, cnt in rows}
  206. # 批量供应商取货地址
  207. pickup_addr_map = _batch_supplier_pickup_address_map(db, supplier_ids)
  208. result = []
  209. for task in tasks:
  210. # 获取订单信息
  211. order_info = {}
  212. if task.order_id:
  213. order = order_map.get(task.order_id)
  214. if order:
  215. order_info = {
  216. "orderNo": order.order_no,
  217. "totalAmount": float(order.total_amount) if order.total_amount else 0,
  218. "clientId": order.client_id,
  219. "clientName": client_map.get(order.client_id, {}).get("name") if order.client_id else None,
  220. "contactPerson": order.contact_person,
  221. "contactPhone": order.contact_phone,
  222. "deliveryAddress": order.delivery_address
  223. }
  224. item_count = item_count_map.get(task.id, 0)
  225. # 供应商取货地址(用于列表展示)
  226. supplier_pickup = pickup_addr_map.get(task.supplier_id, {"fullAddress": None, "contactPerson": None, "contactPhone": None})
  227. result.append({
  228. "id": task.id,
  229. "taskNo": task.task_no,
  230. "orderId": task.order_id,
  231. "orderNo": order_info.get("orderNo"),
  232. "regionId": task.region_id,
  233. "regionName": region_map.get(task.region_id),
  234. "supplierId": task.supplier_id,
  235. "supplierName": supplier_map.get(task.supplier_id, {}).get("name"),
  236. "supplierAddress": supplier_pickup.get("fullAddress"),
  237. "deliverId": task.deliver_id,
  238. "deliverName": deliver_map.get(task.deliver_id, {}).get("name"),
  239. "deliverPhone": deliver_map.get(task.deliver_id, {}).get("phone"),
  240. "status": task.status,
  241. "totalAmount": order_info.get("totalAmount"),
  242. "itemCount": item_count,
  243. "clientId": order_info.get("clientId"),
  244. "clientName": order_info.get("clientName"),
  245. "contactPerson": order_info.get("contactPerson"),
  246. "contactPhone": order_info.get("contactPhone"),
  247. "deliveryAddress": order_info.get("deliveryAddress"),
  248. "dispatchRemark": task.dispatch_remark,
  249. "createAt": str(task.create_at) if task.create_at else None,
  250. "assignTime": str(task.create_at) if task.deliver_id else None,
  251. "pickupTime": str(task.pickup_time) if task.pickup_time else None,
  252. "deliveryTime": str(task.delivery_time) if task.delivery_time else None,
  253. "expireTime": str(task.effective_time) if task.effective_time else None,
  254. "isExpiring": False
  255. })
  256. return success_response({
  257. "list": result,
  258. "total": total,
  259. "page": page,
  260. "pageSize": pageSize,
  261. "totalPages": (total + pageSize - 1) // pageSize
  262. })
  263. @router.get("/delivery-tasks/{task_id}", response_model=dict)
  264. def get_delivery_task(
  265. task_id: str,
  266. db: Session = Depends(get_db),
  267. current_user: dict = Depends(require_admin)
  268. ):
  269. """获取配送任务详情"""
  270. from app.crud.delivery import delivery_task_crud, delivery_task_item_crud
  271. task = delivery_task_crud.get(db, task_id)
  272. if not task:
  273. return error_response(404, "配送任务不存在")
  274. # 获取任务明细
  275. items = delivery_task_item_crud.get_by_task(db, task_id)
  276. # 获取关联信息
  277. region_name = None
  278. if task.region_id:
  279. region = region_crud.get(db, task.region_id)
  280. region_name = region.name if region else None
  281. supplier_info = {"name": None, "phone": None}
  282. if task.supplier_id:
  283. supplier = user_crud.get(db, task.supplier_id)
  284. if supplier:
  285. supplier_info = {"name": supplier.real_name, "phone": supplier.phone}
  286. deliver_info = {"name": None, "phone": None}
  287. if task.deliver_id:
  288. deliver = user_crud.get(db, task.deliver_id)
  289. if deliver:
  290. deliver_info = {"name": deliver.real_name, "phone": deliver.phone}
  291. # 获取订单信息
  292. order_info = {}
  293. order_price_list_id: Optional[str] = None
  294. if task.order_id:
  295. order = order_crud.get(db, task.order_id)
  296. if order:
  297. order_info = {
  298. "orderNo": order.order_no,
  299. "totalAmount": float(order.total_amount) if order.total_amount else 0,
  300. "contactPerson": order.contact_person,
  301. "contactPhone": order.contact_phone,
  302. "deliveryAddress": order.delivery_address
  303. }
  304. order_price_list_id = getattr(order, "price_list_id", None)
  305. pickup_addr = _supplier_pickup_address_block(db, task.supplier_id)
  306. # 处理任务明细(商品展示字段对齐运营端订单明细)
  307. item_details = []
  308. pli_map = _price_list_item_map(db, [order_price_list_id]) if order_price_list_id else {}
  309. for item in items:
  310. product = product_crud.get(db, item.product_id)
  311. order_line = order_item_crud.get(db, item.order_item_id) if item.order_item_id else None
  312. unit_row = None
  313. if order_line and order_line.unit_id:
  314. unit_row = unit_crud.get(db, order_line.unit_id)
  315. elif product and product.unit_id:
  316. unit_row = unit_crud.get(db, product.unit_id)
  317. image_url = getattr(product, "image_url", None) if product else None
  318. category_path = ""
  319. if product and getattr(product, "category_id", None):
  320. category_path = _category_path_display(db, str(product.category_id))
  321. measure_value = _numeric_to_float_optional(getattr(product, "measure_value", None)) if product else None
  322. qty = float(item.quantity) if item.quantity else 0.0
  323. unit_price = float(order_line.unit_price) if order_line and order_line.unit_price is not None else None
  324. amount = float(order_line.amount) if order_line and order_line.amount is not None else None
  325. if amount is None and unit_price is not None:
  326. amount = round(qty * float(unit_price), 2)
  327. client = user_crud.get(db, item.client_id) if item.client_id else None
  328. # 供货/售价字段:对齐「订单详情」逻辑:优先使用价格清单计算供货单价;无价格清单时兜底订单行售价
  329. selling_unit_price = unit_price
  330. selling_amount = amount
  331. unit_id_for_price = None
  332. if order_line and getattr(order_line, "unit_id", None):
  333. unit_id_for_price = order_line.unit_id
  334. elif product and getattr(product, "unit_id", None):
  335. unit_id_for_price = product.unit_id
  336. supplier_unit_price = None
  337. if order_price_list_id and item.product_id and unit_id_for_price:
  338. pli = pli_map.get((order_price_list_id, item.product_id, unit_id_for_price))
  339. supplier_unit_price = _supplier_unit_yuan_from_pli(pli)
  340. if supplier_unit_price is None:
  341. supplier_unit_price = unit_price
  342. supplier_amount = None
  343. if supplier_unit_price is not None:
  344. supplier_amount = round(qty * float(supplier_unit_price), 2)
  345. item_details.append({
  346. "id": item.id,
  347. "orderItemId": item.order_item_id,
  348. "productId": item.product_id,
  349. "productName": product.name if product else None,
  350. "imageUrl": image_url,
  351. "categoryName": category_path or None,
  352. "measureValue": measure_value,
  353. "unitName": unit_row.name if unit_row else None,
  354. "unitPrice": unit_price,
  355. "amount": amount,
  356. "sellingUnitPrice": selling_unit_price,
  357. "sellingAmount": selling_amount,
  358. "supplierId": task.supplier_id,
  359. "supplierName": supplier_info.get("name"),
  360. "supplierUnitPrice": supplier_unit_price,
  361. "supplierAmount": supplier_amount,
  362. "quantity": qty,
  363. "deliveredQuantity": float(item.delivered_quantity) if item.delivered_quantity else 0,
  364. "clientName": client.real_name if client else None,
  365. "clientPhone": client.phone if client else None,
  366. "clientAddress": item.delivery_address,
  367. "contactPerson": item.contact_person,
  368. "contactPhone": item.contact_phone,
  369. "deliveryAddress": item.delivery_address,
  370. "status": item.status
  371. })
  372. # 凭证图片:与运营端订单详情一致(附件表优先,任务字段兜底)
  373. from app.crud.attachment import attachment_crud as _attachment_crud
  374. prepare_images = [
  375. a.file_url for a in _attachment_crud.get_by_business(db, "delivery_prepare", task.id) if a.file_url
  376. ]
  377. pickup_images = [
  378. a.file_url for a in _attachment_crud.get_by_business(db, "delivery_pickup", task.id) if a.file_url
  379. ]
  380. delivery_images = [
  381. a.file_url for a in _attachment_crud.get_by_business(db, "delivery_deliver", task.id) if a.file_url
  382. ]
  383. if not pickup_images and getattr(task, "pickup_image_url", None):
  384. pickup_images = [
  385. s.strip() for s in str(task.pickup_image_url).split(",") if s and str(s).strip()
  386. ]
  387. if not delivery_images and getattr(task, "delivery_image_url", None):
  388. delivery_images = [
  389. s.strip() for s in str(task.delivery_image_url).split(",") if s and str(s).strip()
  390. ]
  391. return success_response({
  392. "id": task.id,
  393. "taskNo": task.task_no,
  394. "orderId": task.order_id,
  395. "orderNo": order_info.get("orderNo"),
  396. "regionId": task.region_id,
  397. "regionName": region_name,
  398. "supplierId": task.supplier_id,
  399. "supplierName": supplier_info.get("name"),
  400. "supplierPhone": supplier_info.get("phone"),
  401. "supplierAddress": pickup_addr.get("fullAddress"),
  402. "supplierPickup": {
  403. "address": pickup_addr.get("fullAddress"),
  404. "contactPerson": pickup_addr.get("contactPerson"),
  405. "contactPhone": pickup_addr.get("contactPhone"),
  406. },
  407. "customerDelivery": {
  408. "address": order_info.get("deliveryAddress"),
  409. "contactPerson": order_info.get("contactPerson"),
  410. "contactPhone": order_info.get("contactPhone"),
  411. },
  412. "deliverId": task.deliver_id,
  413. "deliverName": deliver_info.get("name"),
  414. "deliverPhone": deliver_info.get("phone"),
  415. "status": task.status,
  416. "totalAmount": order_info.get("totalAmount"),
  417. "createAt": str(task.create_at) if task.create_at else None,
  418. "assignTime": str(task.create_at) if task.deliver_id else None,
  419. "pickupTime": str(task.pickup_time) if task.pickup_time else None,
  420. "pickupRemark": task.pickup_remark,
  421. "prepareImages": prepare_images,
  422. "pickupImages": pickup_images,
  423. "deliveryTime": str(task.delivery_time) if task.delivery_time else None,
  424. "deliveryRemark": task.delivery_remark,
  425. "deliveryImages": delivery_images,
  426. "dispatchRemark": task.dispatch_remark,
  427. "items": item_details
  428. })
  429. @router.post("/delivery-tasks/assign", response_model=dict)
  430. def assign_delivery_task(
  431. request: dict,
  432. db: Session = Depends(get_db),
  433. current_user: dict = Depends(require_admin)
  434. ):
  435. """分配配送任务"""
  436. request, req_err = _resolve_admin_payload_with_crypto_fallback(request, current_user)
  437. if req_err is not None:
  438. return req_err
  439. from app.crud.delivery import delivery_task_crud
  440. from app.crud.order import order_crud
  441. order_id = request.get("orderId")
  442. deliver_id = request.get("deliverId")
  443. if not order_id:
  444. return error_response(400, "请选择订单")
  445. if not deliver_id:
  446. return error_response(400, "请选择配送员")
  447. # 验证配送员存在
  448. deliver = user_crud.get(db, deliver_id)
  449. if not deliver:
  450. return error_response(404, "配送员不存在")
  451. if deliver.role != "deliver":
  452. return error_response(400, "该用户不是配送员")
  453. task = delivery_task_crud.assign_deliver(
  454. db,
  455. order_id=order_id,
  456. deliver_id=deliver_id,
  457. operator_id=current_user.get("user_id")
  458. )
  459. if not task:
  460. return error_response(400, "分配失败,请检查订单状态")
  461. # 阶段D前收尾:分配配送优先维护 delivery_status
  462. order = order_crud.get(db, order_id)
  463. if order:
  464. order.delivery_status = "delivering"
  465. if order.order_status != "cancelled":
  466. order.order_status = "pending_confirm"
  467. order.status = derive_legacy_status_from_three(
  468. order_status=order.order_status,
  469. delivery_status=order.delivery_status,
  470. pay_status=order.pay_status,
  471. )
  472. order.update_at = datetime.now()
  473. db.commit()
  474. return success_response({
  475. "id": task.id,
  476. "status": task.status,
  477. "message": "分配成功"
  478. })
  479. @router.post("/delivery-tasks/assign-batch", response_model=dict)
  480. def assign_delivery_task_batch(
  481. request: dict,
  482. db: Session = Depends(get_db),
  483. current_user: dict = Depends(require_admin)
  484. ):
  485. """批量分配配送任务"""
  486. request, req_err = _resolve_admin_payload_with_crypto_fallback(request, current_user)
  487. if req_err is not None:
  488. return req_err
  489. from app.crud.delivery import delivery_task_crud
  490. from app.crud.order import order_crud
  491. assignments = request.get("assignments", [])
  492. if not assignments:
  493. return error_response(400, "请选择要分配的订单")
  494. results = []
  495. for assignment in assignments:
  496. order_id = assignment.get("orderId")
  497. deliver_id = assignment.get("deliverId")
  498. if order_id and deliver_id:
  499. task = delivery_task_crud.assign_deliver(
  500. db,
  501. order_id=order_id,
  502. deliver_id=deliver_id,
  503. operator_id=current_user.get("user_id")
  504. )
  505. if task:
  506. # 阶段D前收尾:分配配送优先维护 delivery_status
  507. order = order_crud.get(db, order_id)
  508. if order:
  509. order.delivery_status = "delivering"
  510. if order.order_status != "cancelled":
  511. order.order_status = "pending_confirm"
  512. order.status = derive_legacy_status_from_three(
  513. order_status=order.order_status,
  514. delivery_status=order.delivery_status,
  515. pay_status=order.pay_status,
  516. )
  517. order.update_at = datetime.now()
  518. db.commit()
  519. results.append({"orderId": order_id, "success": True})
  520. else:
  521. results.append({"orderId": order_id, "success": False, "reason": "分配失败"})
  522. success_count = sum(1 for r in results if r.get("success"))
  523. return success_response({
  524. "total": len(results),
  525. "success": success_count,
  526. "failed": len(results) - success_count,
  527. "results": results
  528. })
  529. @router.get("/delivery-tasks/warnings", response_model=dict)
  530. def list_delivery_warnings(
  531. page: int = Query(1, ge=1, description="页码"),
  532. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  533. regionId: Optional[str] = Query(None, description="区域ID"),
  534. withinMinutes: int = Query(60, description="多少分钟内即将过期"),
  535. db: Session = Depends(get_db),
  536. current_user: dict = Depends(require_admin)
  537. ):
  538. """获取配送时效预警列表"""
  539. from app.crud.delivery import delivery_task_crud
  540. tasks, total, warning_count = delivery_task_crud.get_warning_tasks(
  541. db,
  542. region_id=regionId,
  543. within_minutes=withinMinutes,
  544. skip=(page - 1) * pageSize,
  545. limit=pageSize
  546. )
  547. region_map = {}
  548. deliver_map = {}
  549. result = []
  550. for task in tasks:
  551. if task.region_id and task.region_id not in region_map:
  552. region = region_crud.get(db, task.region_id)
  553. region_map[task.region_id] = region.name if region else None
  554. if task.deliver_id and task.deliver_id not in deliver_map:
  555. deliver = user_crud.get(db, task.deliver_id)
  556. deliver_map[task.deliver_id] = deliver.real_name if deliver else None
  557. order_info = {}
  558. if task.order_id:
  559. order = order_crud.get(db, task.order_id)
  560. if order:
  561. order_info = {"orderNo": order.order_no}
  562. # 计算剩余时间和预警级别
  563. remaining_minutes = 0
  564. warning_level = "normal"
  565. is_overdue = False
  566. if task.effective_time:
  567. now = datetime.now()
  568. remaining = task.effective_time - now
  569. remaining_minutes = int(remaining.total_seconds() / 60)
  570. if remaining_minutes < 0:
  571. is_overdue = True
  572. warning_level = "overdue"
  573. elif remaining_minutes <= 15:
  574. warning_level = "urgent"
  575. elif remaining_minutes <= 30:
  576. warning_level = "warning"
  577. result.append({
  578. "id": task.id,
  579. "taskNo": task.task_no,
  580. "orderId": task.order_id,
  581. "orderNo": order_info.get("orderNo"),
  582. "regionId": task.region_id,
  583. "regionName": region_map.get(task.region_id),
  584. "deliverId": task.deliver_id,
  585. "deliverName": deliver_map.get(task.deliver_id),
  586. "status": task.status,
  587. "expireTime": str(task.effective_time) if task.effective_time else None,
  588. "remainingMinutes": remaining_minutes,
  589. "isOverdue": is_overdue,
  590. "warningLevel": warning_level
  591. })
  592. return success_response({
  593. "list": result,
  594. "total": total,
  595. "warningCount": warning_count,
  596. "page": page,
  597. "pageSize": pageSize,
  598. "totalPages": (total + pageSize - 1) // pageSize
  599. })
  600. @router.post("/delivery-tasks/{task_id}/cancel", response_model=dict)
  601. def cancel_delivery_task(
  602. task_id: str,
  603. request: dict,
  604. db: Session = Depends(get_db),
  605. current_user: dict = Depends(require_admin)
  606. ):
  607. """取消配送任务"""
  608. request, req_err = _resolve_admin_payload_with_crypto_fallback(request, current_user)
  609. if req_err is not None:
  610. return req_err
  611. from app.crud.delivery import delivery_task_crud
  612. task = delivery_task_crud.get(db, task_id)
  613. if not task:
  614. return error_response(404, "配送任务不存在")
  615. reason = request.get("reason")
  616. task = delivery_task_crud.cancel_task(
  617. db,
  618. task_id=task_id,
  619. reason=reason,
  620. operator_id=current_user.get("user_id"),
  621. operator_real_name=current_user.get("real_name"),
  622. )
  623. if not task:
  624. return error_response(400, "当前状态不支持取消")
  625. return success_response({
  626. "id": task.id,
  627. "status": task.status,
  628. "message": "取消成功"
  629. })
  630. @router.post("/delivery-tasks/{task_id}/reassign", response_model=dict)
  631. def reassign_delivery_task(
  632. task_id: str,
  633. request: dict,
  634. db: Session = Depends(get_db),
  635. current_user: dict = Depends(require_admin)
  636. ):
  637. """重新分配配送任务"""
  638. request, req_err = _resolve_admin_payload_with_crypto_fallback(request, current_user)
  639. if req_err is not None:
  640. return req_err
  641. from app.crud.delivery import delivery_task_crud
  642. task = delivery_task_crud.get(db, task_id)
  643. if not task:
  644. return error_response(404, "配送任务不存在")
  645. new_deliver_id = request.get("deliverId")
  646. if not new_deliver_id:
  647. return error_response(400, "请选择新配送员")
  648. # 验证新配送员
  649. new_deliver = user_crud.get(db, new_deliver_id)
  650. if not new_deliver:
  651. return error_response(404, "配送员不存在")
  652. if new_deliver.role != "deliver":
  653. return error_response(400, "该用户不是配送员")
  654. task = delivery_task_crud.reassign(
  655. db,
  656. task_id=task_id,
  657. new_deliver_id=new_deliver_id,
  658. operator_id=current_user.get("user_id"),
  659. operator_real_name=current_user.get("real_name"),
  660. )
  661. if not task:
  662. return error_response(400, "当前状态不支持重新分配")
  663. return success_response({
  664. "id": task.id,
  665. "deliverId": task.deliver_id,
  666. "message": "重新分配成功"
  667. })
  668. @router.get("/delivery-tasks/pending", response_model=dict)
  669. def list_pending_delivery_tasks(
  670. page: int = Query(1, ge=1, description="页码"),
  671. pageSize: int = Query(10, ge=1, le=100, description="每页条数"),
  672. regionId: Optional[str] = Query(None, description="区域ID"),
  673. db: Session = Depends(get_db),
  674. current_user: dict = Depends(require_admin)
  675. ):
  676. """获取待分配的配送任务"""
  677. from app.crud.delivery import delivery_task_crud, delivery_task_item_crud
  678. tasks, total = delivery_task_crud.get_pending_list(
  679. db,
  680. region_id=regionId,
  681. skip=(page - 1) * pageSize,
  682. limit=pageSize
  683. )
  684. region_map = {}
  685. result = []
  686. for task in tasks:
  687. if task.region_id and task.region_id not in region_map:
  688. region = region_crud.get(db, task.region_id)
  689. region_map[task.region_id] = region.name if region else None
  690. order_info = {}
  691. if task.order_id:
  692. order = order_crud.get(db, task.order_id)
  693. if order:
  694. order_info = {
  695. "orderNo": order.order_no,
  696. "totalAmount": float(order.total_amount) if order.total_amount else 0,
  697. "contactPerson": order.contact_person,
  698. "contactPhone": order.contact_phone,
  699. "deliveryAddress": order.delivery_address
  700. }
  701. items = delivery_task_item_crud.get_by_task(db, task.id)
  702. result.append({
  703. "id": task.id,
  704. "taskNo": task.task_no,
  705. "orderId": task.order_id,
  706. "orderNo": order_info.get("orderNo"),
  707. "regionId": task.region_id,
  708. "regionName": region_map.get(task.region_id),
  709. "status": task.status,
  710. "totalAmount": order_info.get("totalAmount"),
  711. "itemCount": len(items),
  712. "createAt": str(task.create_at) if task.create_at else None
  713. })
  714. return success_response({
  715. "list": result,
  716. "total": total,
  717. "page": page,
  718. "pageSize": pageSize,
  719. "totalPages": (total + pageSize - 1) // pageSize
  720. })