delivery.py 27 KB

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