delivery.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872
  1. """配送任务CRUD"""
  2. import json
  3. from typing import Optional, List
  4. from datetime import datetime, timedelta
  5. from decimal import Decimal
  6. from sqlalchemy.orm import Session
  7. from sqlalchemy import and_, func, or_, select, exists
  8. from app.models.models import BizDeliveryTask, BizDeliveryTaskItem, BizDeliveryTaskHis, BizOrder
  9. from app.crud.base import CRUDBase
  10. from app.core.utils import generate_id
  11. class DeliveryTaskCRUD(CRUDBase[BizDeliveryTask]):
  12. """配送任务CRUD操作"""
  13. @staticmethod
  14. def _sync_legacy_status_from_dual(task: BizDeliveryTask) -> None:
  15. """根据双状态回写兼容字段 status。"""
  16. ds = (getattr(task, "deliverer_status", None) or "").strip()
  17. ss = (getattr(task, "supplier_prepare_status", None) or "").strip()
  18. if ds == "delivered":
  19. task.status = "completed"
  20. elif ds == "delivering":
  21. task.status = "delivering"
  22. elif ss == "stocking":
  23. task.status = "supplier_stocking"
  24. elif ss == "prepared":
  25. task.status = "supplier_ready"
  26. else:
  27. task.status = "assigned" if task.deliver_id else "pending"
  28. def get_by_order(self, db: Session, order_id: str) -> Optional[BizDeliveryTask]:
  29. """根据订单ID获取配送任务"""
  30. return db.query(BizDeliveryTask).filter(
  31. and_(
  32. BizDeliveryTask.order_id == order_id,
  33. BizDeliveryTask.is_del == 0
  34. )
  35. ).first()
  36. def get_multi_with_filters(
  37. self,
  38. db: Session,
  39. region_id: str = None,
  40. supplier_id: str = None,
  41. status: str = None,
  42. supplier_prepare_status: str = None,
  43. order_no: str = None,
  44. deliver_id: str = None,
  45. keyword: str = None,
  46. skip: int = 0,
  47. limit: int = 10
  48. ) -> tuple[List[BizDeliveryTask], int]:
  49. """多条件查询配送任务"""
  50. query = db.query(BizDeliveryTask).filter(BizDeliveryTask.is_del == 0)
  51. if region_id:
  52. query = query.filter(BizDeliveryTask.region_id == region_id)
  53. if supplier_id:
  54. query = query.filter(BizDeliveryTask.supplier_id == supplier_id)
  55. if supplier_prepare_status:
  56. parts = [s.strip() for s in supplier_prepare_status.split(",") if s.strip()]
  57. if parts:
  58. if len(parts) > 1:
  59. query = query.filter(BizDeliveryTask.supplier_prepare_status.in_(parts))
  60. else:
  61. p0 = parts[0]
  62. if p0 == "pending_prepare":
  63. query = query.filter(
  64. or_(
  65. BizDeliveryTask.supplier_prepare_status == "pending_prepare",
  66. BizDeliveryTask.supplier_prepare_status.is_(None),
  67. )
  68. )
  69. else:
  70. query = query.filter(BizDeliveryTask.supplier_prepare_status == p0)
  71. elif status:
  72. parts = [s.strip() for s in status.split(",") if s.strip()]
  73. if parts:
  74. if len(parts) > 1:
  75. query = query.filter(BizDeliveryTask.status.in_(parts))
  76. else:
  77. query = query.filter(BizDeliveryTask.status == parts[0])
  78. if order_no:
  79. query = query.filter(BizDeliveryTask.task_no.like(f"%{order_no}%"))
  80. if deliver_id:
  81. query = query.filter(BizDeliveryTask.deliver_id == deliver_id)
  82. if keyword:
  83. kw = (keyword or "").strip()
  84. if kw:
  85. pattern = f"%{kw}%"
  86. order_match = exists(
  87. select(1).select_from(BizOrder).where(
  88. and_(
  89. BizOrder.id == BizDeliveryTask.order_id,
  90. BizOrder.is_del == 0,
  91. or_(
  92. BizOrder.order_no.like(pattern),
  93. BizOrder.contact_person.like(pattern),
  94. BizOrder.contact_phone.like(pattern),
  95. ),
  96. )
  97. )
  98. )
  99. query = query.filter(
  100. or_(
  101. BizDeliveryTask.task_no.like(pattern),
  102. BizDeliveryTask.order_id.like(pattern),
  103. order_match,
  104. )
  105. )
  106. total = query.count()
  107. tasks = query.order_by(BizDeliveryTask.create_at.desc()).offset(skip).limit(limit).all()
  108. return tasks, total
  109. def get_by_deliver(
  110. self,
  111. db: Session,
  112. deliver_id: str,
  113. status: str = None,
  114. deliverer_status: str = None,
  115. skip: int = 0,
  116. limit: int = 10
  117. ) -> tuple[List[BizDeliveryTask], int]:
  118. """根据配送员获取配送任务。优先 deliverer_status(小程序);否则按兼容字段 status。"""
  119. query = db.query(BizDeliveryTask).filter(
  120. and_(
  121. BizDeliveryTask.deliver_id == deliver_id,
  122. BizDeliveryTask.is_del == 0,
  123. BizDeliveryTask.status != "cancelled",
  124. )
  125. )
  126. if deliverer_status:
  127. parts = [s.strip() for s in deliverer_status.split(",") if s.strip()]
  128. if parts:
  129. if len(parts) > 1:
  130. query = query.filter(BizDeliveryTask.deliverer_status.in_(parts))
  131. elif parts[0] == "pending_pickup":
  132. query = query.filter(
  133. or_(
  134. BizDeliveryTask.deliverer_status == "pending_pickup",
  135. BizDeliveryTask.deliverer_status.is_(None),
  136. )
  137. )
  138. else:
  139. query = query.filter(BizDeliveryTask.deliverer_status == parts[0])
  140. elif status:
  141. parts = [s.strip() for s in status.split(",") if s.strip()]
  142. if parts:
  143. if len(parts) > 1:
  144. query = query.filter(BizDeliveryTask.status.in_(parts))
  145. else:
  146. query = query.filter(BizDeliveryTask.status == parts[0])
  147. total = query.count()
  148. tasks = query.order_by(BizDeliveryTask.create_at.desc()).offset(skip).limit(limit).all()
  149. return tasks, total
  150. def get_by_supplier(
  151. self,
  152. db: Session,
  153. supplier_id: str,
  154. status: str = None,
  155. skip: int = 0,
  156. limit: int = 10
  157. ) -> tuple[List[BizDeliveryTask], int]:
  158. """根据供应商获取配送任务"""
  159. query = db.query(BizDeliveryTask).filter(
  160. and_(
  161. BizDeliveryTask.supplier_id == supplier_id,
  162. BizDeliveryTask.is_del == 0
  163. )
  164. )
  165. if status:
  166. query = query.filter(BizDeliveryTask.status == status)
  167. total = query.count()
  168. tasks = query.order_by(BizDeliveryTask.create_at.desc()).offset(skip).limit(limit).all()
  169. return tasks, total
  170. def get_pending_list(
  171. self,
  172. db: Session,
  173. region_id: str = None,
  174. skip: int = 0,
  175. limit: int = 10
  176. ) -> tuple[List[BizDeliveryTask], int]:
  177. """获取待分配配送的任务"""
  178. query = db.query(BizDeliveryTask).filter(
  179. and_(
  180. BizDeliveryTask.status == "pending",
  181. BizDeliveryTask.is_del == 0
  182. )
  183. )
  184. if region_id:
  185. query = query.filter(BizDeliveryTask.region_id == region_id)
  186. total = query.count()
  187. tasks = query.order_by(BizDeliveryTask.create_at.asc()).offset(skip).limit(limit).all()
  188. return tasks, total
  189. def assign_deliver(
  190. self,
  191. db: Session,
  192. order_id: str,
  193. deliver_id: str,
  194. operator_id: str = None
  195. ) -> Optional[BizDeliveryTask]:
  196. """分配配送员"""
  197. task = self.get_by_order(db, order_id)
  198. if not task:
  199. return None
  200. if task.status != "pending":
  201. return None
  202. task.deliver_id = deliver_id
  203. task.deliverer_status = "pending_pickup"
  204. task.supplier_prepare_status = "pending_prepare"
  205. self._sync_legacy_status_from_dual(task)
  206. task.update_at = datetime.now()
  207. db.commit()
  208. db.refresh(task)
  209. return task
  210. def assign_deliver_batch(
  211. self,
  212. db: Session,
  213. assignments: List[dict]
  214. ) -> List[BizDeliveryTask]:
  215. """批量分配配送员"""
  216. results = []
  217. for assignment in assignments:
  218. order_id = assignment.get("orderId")
  219. deliver_id = assignment.get("deliverId")
  220. task = self.assign_deliver(db, order_id, deliver_id)
  221. if task:
  222. results.append(task)
  223. return results
  224. def update_status(
  225. self,
  226. db: Session,
  227. task_id: str,
  228. new_status: str,
  229. operator_id: str = None,
  230. remark: str = None,
  231. image_urls: List[str] = None,
  232. operator_real_name: str = None,
  233. operator_role: str = "deliver",
  234. ) -> Optional[BizDeliveryTask]:
  235. """更新配送任务状态"""
  236. task = self.get(db, task_id)
  237. if not task:
  238. return None
  239. old_status = task.status
  240. task.status = new_status
  241. task.update_at = datetime.now()
  242. if new_status == "picking":
  243. # 配送员已取货:进入配送中(勿再保留 pending_pickup,否则列表/标题仍显示「待取货」)
  244. task.deliverer_status = "delivering"
  245. task.supplier_prepare_status = "picked_up"
  246. task.pickup_time = datetime.now()
  247. if remark:
  248. task.pickup_remark = remark
  249. if image_urls:
  250. task.pickup_image_url = ",".join(image_urls) if len(image_urls) > 0 else None
  251. elif new_status == "supplier_stocking":
  252. task.supplier_prepare_status = "stocking"
  253. elif new_status == "supplier_ready":
  254. task.supplier_prepare_status = "prepared"
  255. # 备货完成凭证(与配送员取货凭证共用字段,时间线区分为供应商操作)
  256. if remark:
  257. task.pickup_remark = remark
  258. if image_urls:
  259. task.pickup_image_url = ",".join(image_urls) if len(image_urls) > 0 else None
  260. elif new_status == "delivering":
  261. task.deliverer_status = "delivering"
  262. if remark:
  263. task.delivery_remark = remark
  264. if image_urls:
  265. task.delivery_image_url = ",".join(image_urls) if len(image_urls) > 0 else None
  266. elif new_status == "completed":
  267. task.deliverer_status = "delivered"
  268. # 送达确认时间:与模型注释一致;小程序取货后直达 completed,必须在此写入
  269. task.delivery_time = datetime.now()
  270. if remark:
  271. task.delivery_remark = remark
  272. if image_urls:
  273. task.delivery_image_url = ",".join(image_urls) if len(image_urls) > 0 else None
  274. self._sync_legacy_status_from_dual(task)
  275. # 写入订单「操作记录」:legacy task.status 若已为 picking,会与取货入参 new_status 相同而漏记;
  276. # 配送员确认取货时必须仍写入一条。
  277. if task.order_id:
  278. from app.crud.order import order_crud
  279. should_log_order = (old_status != new_status) or new_status == "picking"
  280. if should_log_order:
  281. order_crud._create_status_log(
  282. db,
  283. task.order_id,
  284. f"dt_{old_status}" if old_status is not None else "",
  285. f"dt_{new_status}",
  286. operator_id or "system",
  287. operator_role or "deliver",
  288. reason=remark,
  289. create_name=(operator_real_name or "").strip() or None,
  290. )
  291. db.commit()
  292. db.refresh(task)
  293. if new_status == "delivering":
  294. from app.crud.delivery_warning_resolve import auto_resolve_open_warnings_for_task
  295. auto_resolve_open_warnings_for_task(db, task_id)
  296. return task
  297. def cancel_task(
  298. self,
  299. db: Session,
  300. task_id: str,
  301. reason: str = None,
  302. operator_id: str = None,
  303. operator_real_name: str = None,
  304. ) -> Optional[BizDeliveryTask]:
  305. """取消配送任务"""
  306. task = self.get(db, task_id)
  307. if not task:
  308. return None
  309. if task.status in ["completed", "cancelled"]:
  310. return None
  311. old_status = task.status
  312. task.status = "cancelled"
  313. task.update_at = datetime.now()
  314. if task.order_id:
  315. from app.crud.order import order_crud
  316. order_crud._create_status_log(
  317. db,
  318. task.order_id,
  319. f"dt_{old_status}",
  320. "dt_cancelled",
  321. operator_id or "system",
  322. "admin",
  323. reason=reason,
  324. create_name=(operator_real_name or "").strip() or None,
  325. )
  326. db.commit()
  327. db.refresh(task)
  328. from app.crud.delivery_warning_resolve import auto_resolve_open_warnings_for_task
  329. auto_resolve_open_warnings_for_task(db, task_id)
  330. return task
  331. def reassign(
  332. self,
  333. db: Session,
  334. task_id: str,
  335. new_deliver_id: str,
  336. operator_id: str = None,
  337. operator_real_name: str = None,
  338. ) -> Optional[BizDeliveryTask]:
  339. """重新分配配送员"""
  340. from app.crud.order import order_crud
  341. from app.crud.user import user_crud
  342. task = self.get(db, task_id)
  343. if not task:
  344. return None
  345. if task.status in ["completed", "cancelled"]:
  346. return None
  347. old_deliver_id = task.deliver_id
  348. if old_deliver_id == new_deliver_id:
  349. return task
  350. updater = None
  351. if operator_id:
  352. updater = {"user_id": operator_id, "real_name": operator_real_name}
  353. self.update_with_history(
  354. db,
  355. task_id,
  356. {"deliver_id": new_deliver_id},
  357. updater=updater,
  358. )
  359. task = self.get(db, task_id)
  360. if not task:
  361. return None
  362. def _dn(uid: Optional[str]) -> Optional[str]:
  363. if not uid:
  364. return None
  365. u = user_crud.get(db, uid)
  366. return (u.real_name if u else None) or uid
  367. nu = user_crud.get(db, new_deliver_id)
  368. order_crud._create_status_log(
  369. db,
  370. task.order_id,
  371. "",
  372. "delivery.reassign",
  373. operator_id or "system",
  374. "admin",
  375. reason=json.dumps(
  376. {
  377. "fromDeliverId": old_deliver_id,
  378. "toDeliverId": new_deliver_id,
  379. "fromDeliverName": _dn(old_deliver_id),
  380. "toDeliverName": nu.real_name if nu else new_deliver_id,
  381. },
  382. ensure_ascii=False,
  383. ),
  384. create_name=(operator_real_name or "").strip() or None,
  385. )
  386. db.commit()
  387. db.refresh(task)
  388. return task
  389. def get_warning_tasks(
  390. self,
  391. db: Session,
  392. region_id: str = None,
  393. within_minutes: int = 60,
  394. skip: int = 0,
  395. limit: int = 10
  396. ) -> tuple[List[BizDeliveryTask], int, int]:
  397. """获取时效预警任务"""
  398. now = datetime.now()
  399. expire_time_threshold = now + timedelta(minutes=within_minutes)
  400. query = db.query(BizDeliveryTask).filter(
  401. and_(
  402. BizDeliveryTask.status.in_(["pending", "assigned", "picking"]),
  403. BizDeliveryTask.is_del == 0,
  404. BizDeliveryTask.effective_time <= expire_time_threshold
  405. )
  406. )
  407. if region_id:
  408. query = query.filter(BizDeliveryTask.region_id == region_id)
  409. total = query.count()
  410. warning_count = query.filter(
  411. BizDeliveryTask.effective_time <= now + timedelta(minutes=30)
  412. ).count()
  413. tasks = query.order_by(BizDeliveryTask.effective_time.asc()).offset(skip).limit(limit).all()
  414. return tasks, total, warning_count
  415. def _generate_task_no(self, db: Session) -> str:
  416. """生成配送单号"""
  417. today = datetime.now().strftime("%Y%m%d")
  418. prefix = f"DLV{today}"
  419. last_task = db.query(BizDeliveryTask).filter(
  420. BizDeliveryTask.task_no.like(f"{prefix}%")
  421. ).order_by(BizDeliveryTask.task_no.desc()).first()
  422. if last_task:
  423. last_no = int(last_task.task_no[-4:])
  424. new_no = last_no + 1
  425. else:
  426. new_no = 1
  427. return f"{prefix}{str(new_no).zfill(4)}"
  428. def create_from_order(
  429. self,
  430. db: Session,
  431. order_id: str,
  432. supplier_id: str,
  433. region_id: str,
  434. creator: dict = None
  435. ) -> BizDeliveryTask:
  436. """从订单创建配送任务"""
  437. task_no = self._generate_task_no(db)
  438. # 设置配送截止时间(默认4小时)
  439. effective_time = datetime.now() + timedelta(hours=4)
  440. task = BizDeliveryTask(
  441. id=generate_id(),
  442. task_no=task_no,
  443. order_id=order_id,
  444. region_id=region_id,
  445. supplier_id=supplier_id,
  446. status="pending",
  447. deliverer_status="pending_pickup",
  448. supplier_prepare_status="pending_prepare",
  449. effective_time=effective_time,
  450. create_by=creator.get("user_id") if creator else None,
  451. create_name=creator.get("real_name") if creator else None
  452. )
  453. db.add(task)
  454. db.flush()
  455. return task
  456. def create_delivery_task_from_order(
  457. self,
  458. db: Session,
  459. order_id: str,
  460. order_items: List[dict],
  461. supplier_id: str,
  462. region_id: str,
  463. creator: dict = None
  464. ) -> tuple:
  465. """从订单创建完整的配送任务(包含明细)"""
  466. from app.crud.order import order_item_crud
  467. # 创建配送任务
  468. task = self.create_from_order(
  469. db, order_id, supplier_id, region_id, creator
  470. )
  471. # 创建配送任务明细
  472. task_items = []
  473. for item_data in order_items:
  474. # 获取订单项信息
  475. order_item = order_item_crud.get(db, item_data.get("order_item_id"))
  476. if not order_item:
  477. continue
  478. task_item = BizDeliveryTaskItem(
  479. id=generate_id(),
  480. task_id=task.id,
  481. order_id=order_id,
  482. order_item_id=item_data.get("order_item_id"),
  483. product_id=item_data.get("product_id"),
  484. client_id=item_data.get("client_id"),
  485. delivery_address=item_data.get("delivery_address"),
  486. contact_person=item_data.get("contact_person"),
  487. contact_phone=item_data.get("contact_phone"),
  488. quantity=Decimal(str(item_data.get("quantity", 0))),
  489. status="pending",
  490. create_by=creator.get("user_id") if creator else None,
  491. create_name=creator.get("real_name") if creator else None
  492. )
  493. db.add(task_item)
  494. task_items.append(task_item)
  495. db.commit()
  496. db.refresh(task)
  497. for item in task_items:
  498. db.refresh(item)
  499. return task, task_items
  500. def get_expiring_tasks(
  501. self,
  502. db: Session,
  503. within_minutes: int = 60,
  504. region_id: str = None
  505. ) -> List[BizDeliveryTask]:
  506. """获取即将过期的配送任务(用于定时任务)"""
  507. now = datetime.now()
  508. expire_threshold = now + timedelta(minutes=within_minutes)
  509. query = db.query(BizDeliveryTask).filter(
  510. and_(
  511. BizDeliveryTask.status.in_(["pending", "assigned", "picking"]),
  512. BizDeliveryTask.is_del == 0,
  513. BizDeliveryTask.effective_time <= expire_threshold,
  514. BizDeliveryTask.effective_time >= now
  515. )
  516. )
  517. if region_id:
  518. query = query.filter(BizDeliveryTask.region_id == region_id)
  519. return query.order_by(BizDeliveryTask.effective_time.asc()).all()
  520. def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BizDeliveryTask]:
  521. """更新配送任务并记录历史"""
  522. original = self.get(db, id)
  523. if not original:
  524. return None
  525. his_data = {
  526. "id": generate_id(),
  527. "task_no": original.task_no,
  528. "order_id": original.order_id,
  529. "region_id": original.region_id,
  530. "supplier_id": original.supplier_id,
  531. "deliver_id": original.deliver_id,
  532. "status": original.status,
  533. "deliverer_status": getattr(original, "deliverer_status", None),
  534. "supplier_prepare_status": getattr(original, "supplier_prepare_status", None),
  535. "pickup_time": original.pickup_time,
  536. "pickup_remark": original.pickup_remark,
  537. "pickup_image_url": original.pickup_image_url,
  538. "delivery_time": original.delivery_time,
  539. "delivery_remark": original.delivery_remark,
  540. "delivery_image_url": original.delivery_image_url,
  541. "expire_warning_sent": getattr(original, "expire_warning_sent", False),
  542. "effective_time": original.effective_time,
  543. "dispatch_remark": getattr(original, "dispatch_remark", None),
  544. "create_at": original.create_at,
  545. "create_by": original.create_by,
  546. "create_name": original.create_name,
  547. "update_at": original.update_at,
  548. "update_by": original.update_by,
  549. }
  550. his = BizDeliveryTaskHis(**his_data)
  551. db.add(his)
  552. return self.update(db, id, obj_in, updater)
  553. # =========================================
  554. # 抢单相关方法
  555. # =========================================
  556. def get_pending_snatch_tasks(
  557. self,
  558. db: Session,
  559. skip: int = 0,
  560. limit: int = 10
  561. ) -> tuple[List[BizDeliveryTask], int]:
  562. """获取待抢单的任务(状态为pending且未分配配送员)"""
  563. query = db.query(BizDeliveryTask).filter(
  564. and_(
  565. BizDeliveryTask.status == "pending",
  566. BizDeliveryTask.is_del == 0
  567. )
  568. )
  569. total = query.count()
  570. tasks = query.order_by(BizDeliveryTask.effective_time.asc()).offset(skip).limit(limit).all()
  571. return tasks, total
  572. def count_pending_snatch_tasks(self, db: Session) -> int:
  573. """统计待抢单的任务数量"""
  574. return db.query(BizDeliveryTask).filter(
  575. and_(
  576. BizDeliveryTask.status == "pending",
  577. BizDeliveryTask.is_del == 0
  578. )
  579. ).count()
  580. def assign_to_deliver(
  581. self,
  582. db: Session,
  583. task_id: str,
  584. deliver_id: str,
  585. deliver_name: str = None
  586. ) -> bool:
  587. """分配任务给配送员(用于抢单)"""
  588. task = self.get(db, task_id)
  589. if not task:
  590. return False
  591. if task.status != "pending":
  592. return False
  593. if task.deliver_id is not None:
  594. return False
  595. task.deliver_id = deliver_id
  596. task.deliver_name = deliver_name
  597. task.deliverer_status = "pending_pickup"
  598. task.supplier_prepare_status = "pending_prepare"
  599. self._sync_legacy_status_from_dual(task)
  600. task.update_at = datetime.now()
  601. db.commit()
  602. db.refresh(task)
  603. return True
  604. def get_snatched_by_me(
  605. self,
  606. db: Session,
  607. deliver_id: str,
  608. skip: int = 0,
  609. limit: int = 10
  610. ) -> tuple[List[BizDeliveryTask], int]:
  611. """获取我抢到的任务"""
  612. query = db.query(BizDeliveryTask).filter(
  613. and_(
  614. BizDeliveryTask.deliver_id == deliver_id,
  615. BizDeliveryTask.status.in_(["pending", "assigned"]),
  616. BizDeliveryTask.is_del == 0
  617. )
  618. )
  619. total = query.count()
  620. tasks = query.order_by(BizDeliveryTask.effective_time.asc()).offset(skip).limit(limit).all()
  621. return tasks, total
  622. def count_snatched_by_me(self, db: Session, deliver_id: str) -> int:
  623. """统计我抢到的任务数量"""
  624. return db.query(BizDeliveryTask).filter(
  625. and_(
  626. BizDeliveryTask.deliver_id == deliver_id,
  627. BizDeliveryTask.status.in_(["pending", "assigned"]),
  628. BizDeliveryTask.is_del == 0
  629. )
  630. ).count()
  631. def count_today_snatched(self, db: Session, deliver_id: str, start_time: datetime) -> int:
  632. """统计今日抢单成功数量"""
  633. return db.query(BizDeliveryTask).filter(
  634. and_(
  635. BizDeliveryTask.deliver_id == deliver_id,
  636. BizDeliveryTask.status.in_(["pending", "assigned", "picking", "delivering", "completed"]),
  637. BizDeliveryTask.is_del == 0,
  638. BizDeliveryTask.update_at >= start_time
  639. )
  640. ).count()
  641. def cancel_snatch(self, db: Session, task_id: str) -> bool:
  642. """取消抢单"""
  643. task = self.get(db, task_id)
  644. if not task:
  645. return False
  646. if task.status not in ["pending", "assigned"]:
  647. return False
  648. task.deliver_id = None
  649. task.deliver_name = None
  650. task.status = "pending"
  651. task.update_at = datetime.now()
  652. db.commit()
  653. db.refresh(task)
  654. return True
  655. def get_history_by_deliver(
  656. self,
  657. db: Session,
  658. deliver_id: str,
  659. start_date: str = None,
  660. end_date: str = None,
  661. skip: int = 0,
  662. limit: int = 10
  663. ) -> tuple[List[BizDeliveryTask], int]:
  664. """获取配送员历史任务(已完成和已取消的)"""
  665. query = db.query(BizDeliveryTask).filter(
  666. and_(
  667. BizDeliveryTask.deliver_id == deliver_id,
  668. BizDeliveryTask.status.in_(["completed", "cancelled"]),
  669. BizDeliveryTask.is_del == 0
  670. )
  671. )
  672. if start_date:
  673. query = query.filter(BizDeliveryTask.create_at >= start_date)
  674. if end_date:
  675. query = query.filter(BizDeliveryTask.create_at <= end_date + " 23:59:59")
  676. total = query.count()
  677. tasks = query.order_by(BizDeliveryTask.delivery_time.desc()).offset(skip).limit(limit).all()
  678. return tasks, total
  679. class DeliveryTaskItemCRUD(CRUDBase[BizDeliveryTaskItem]):
  680. """配送任务明细CRUD操作"""
  681. def get_by_task(self, db: Session, task_id: str) -> List[BizDeliveryTaskItem]:
  682. """获取配送任务的所有明细"""
  683. return db.query(BizDeliveryTaskItem).filter(
  684. and_(
  685. BizDeliveryTaskItem.task_id == task_id,
  686. BizDeliveryTaskItem.is_del == 0
  687. )
  688. ).all()
  689. def get_delivered_quantity_by_order_item(
  690. self, db: Session, order_id: str
  691. ) -> dict[str, float]:
  692. """按订单行汇总当前配送任务明细上的已送达数量(与 biz_order_item.delivered_quantity 对齐展示用)"""
  693. task = db.query(BizDeliveryTask).filter(
  694. and_(BizDeliveryTask.order_id == order_id, BizDeliveryTask.is_del == 0)
  695. ).first()
  696. if not task:
  697. return {}
  698. out: dict[str, float] = {}
  699. for ti in self.get_by_task(db, task.id):
  700. oid = ti.order_item_id
  701. if not oid:
  702. continue
  703. q = float(ti.delivered_quantity or 0)
  704. prev = out.get(oid, 0.0)
  705. if q > prev:
  706. out[oid] = q
  707. return out
  708. def create_items(
  709. self,
  710. db: Session,
  711. task_id: str,
  712. items: List[dict],
  713. creator: dict = None
  714. ) -> List[BizDeliveryTaskItem]:
  715. """批量创建配送任务明细"""
  716. result = []
  717. for item in items:
  718. task_item = BizDeliveryTaskItem(
  719. id=generate_id(),
  720. task_id=task_id,
  721. order_id=item.get("order_id"),
  722. order_item_id=item.get("order_item_id"),
  723. product_id=item.get("product_id"),
  724. client_id=item.get("client_id"),
  725. delivery_address=item.get("delivery_address"),
  726. contact_person=item.get("contact_person"),
  727. contact_phone=item.get("contact_phone"),
  728. quantity=Decimal(str(item.get("quantity", 0))),
  729. status="pending",
  730. create_by=creator.get("user_id") if creator else None,
  731. create_name=creator.get("real_name") if creator else None
  732. )
  733. db.add(task_item)
  734. result.append(task_item)
  735. db.commit()
  736. for item in result:
  737. db.refresh(item)
  738. return result
  739. def update_item_status(
  740. self,
  741. db: Session,
  742. item_id: str,
  743. delivered_quantity: Decimal,
  744. status: str = "delivered"
  745. ) -> Optional[BizDeliveryTaskItem]:
  746. """更新配送明细状态"""
  747. item = self.get(db, item_id)
  748. if not item:
  749. return None
  750. item.delivered_quantity = delivered_quantity
  751. item.status = status
  752. if status == "delivered":
  753. item.delivery_time = datetime.now()
  754. if item.order_item_id:
  755. from app.crud.order import order_item_crud
  756. oi = order_item_crud.get(db, item.order_item_id)
  757. if oi is not None:
  758. oi.delivered_quantity = delivered_quantity
  759. oi.update_at = datetime.now()
  760. db.commit()
  761. db.refresh(item)
  762. return item
  763. delivery_task_crud = DeliveryTaskCRUD(BizDeliveryTask)
  764. delivery_task_item_crud = DeliveryTaskItemCRUD(BizDeliveryTaskItem)