order.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038
  1. """订单CRUD"""
  2. from typing import Optional, List
  3. import json
  4. from datetime import datetime
  5. from decimal import Decimal
  6. from sqlalchemy.orm import Session
  7. from sqlalchemy import and_, or_, func, select, exists
  8. from app.models.models import BizOrder, BizOrderItem, BizOrderStatusLog, BizOrderHis, BizOrderAdjust, SysUser
  9. from app.crud.base import CRUDBase
  10. from app.core.utils import generate_id
  11. from app.core.order_status_compat import derive_three_status_from_legacy, derive_legacy_status_from_three
  12. # 订单编辑锁超时(秒);超时后允许他人抢锁
  13. ORDER_EDIT_LOCK_TTL_SECONDS = 300
  14. class OrderCRUD(CRUDBase[BizOrder]):
  15. @staticmethod
  16. def normalize_payment_method(payment_method: Optional[str]) -> str:
  17. """统一支付方式值,仅保留 online/offline。"""
  18. method = (payment_method or "").strip().lower()
  19. if method in ("wechat", "wx", "online"):
  20. return "online"
  21. if method == "offline":
  22. return "offline"
  23. return "online"
  24. def sync_pay_fields_from_amounts(self, order: BizOrder) -> None:
  25. """根据已付/应收同步 pay_status、payment_time 与兼容字段 status(付清后才是 paid)。"""
  26. pa = order.paid_amount or Decimal("0")
  27. ra = order.receivable_amount or Decimal("0")
  28. if ra > 0 and pa >= ra:
  29. order.pay_status = "paid"
  30. if not order.payment_time:
  31. order.payment_time = datetime.now()
  32. else:
  33. order.pay_status = "pending_payment"
  34. order.status = derive_legacy_status_from_three(
  35. order_status=getattr(order, "order_status", None) or "pending_confirm",
  36. delivery_status=getattr(order, "delivery_status", None) or "pending_dispatch",
  37. pay_status=order.pay_status,
  38. )
  39. """订单CRUD操作"""
  40. def _sync_three_status_fields_from_legacy(self, order: BizOrder):
  41. """阶段A:保持三字段与旧 status 一致(先兼容,再逐步迁移逻辑)。"""
  42. ts = derive_three_status_from_legacy(order.status, paid_amount=order.paid_amount)
  43. order.order_status = ts.order_status
  44. order.delivery_status = ts.delivery_status
  45. order.pay_status = ts.pay_status
  46. def _apply_three_status(self, order: BizOrder, *, order_status: str = None, delivery_status: str = None, pay_status: str = None):
  47. """阶段D收尾:优先写三字段,再回写兼容 status。"""
  48. if order_status is not None:
  49. order.order_status = order_status
  50. if delivery_status is not None:
  51. order.delivery_status = delivery_status
  52. if pay_status is not None:
  53. order.pay_status = pay_status
  54. order.status = derive_legacy_status_from_three(
  55. order_status=order.order_status,
  56. delivery_status=order.delivery_status,
  57. pay_status=order.pay_status,
  58. )
  59. def get_by_client(self, db: Session, client_id: str) -> List[BizOrder]:
  60. """根据客户获取订单列表"""
  61. return db.query(BizOrder).filter(
  62. and_(
  63. BizOrder.client_id == client_id,
  64. BizOrder.is_del == 0
  65. )
  66. ).order_by(BizOrder.create_at.desc()).all()
  67. def get_multi_with_filters(
  68. self,
  69. db: Session,
  70. client_id: str = None,
  71. region_id: str = None,
  72. status: str = None,
  73. order_status: str = None,
  74. delivery_status: str = None,
  75. pay_status: str = None,
  76. payment_method: str = None,
  77. cancel_status: str = None,
  78. order_no: str = None,
  79. keyword: str = None,
  80. start_date: str = None,
  81. end_date: str = None,
  82. skip: int = 0,
  83. limit: int = 10,
  84. list_tab: str = None,
  85. ) -> tuple[List[BizOrder], int]:
  86. """多条件查询订单"""
  87. query = db.query(BizOrder).filter(BizOrder.is_del == 0)
  88. if client_id:
  89. query = query.filter(BizOrder.client_id == client_id)
  90. if region_id:
  91. query = query.filter(BizOrder.region_id == region_id)
  92. if status:
  93. query = query.filter(BizOrder.status == status)
  94. if order_status:
  95. query = query.filter(BizOrder.order_status == order_status)
  96. if delivery_status:
  97. query = query.filter(BizOrder.delivery_status == delivery_status)
  98. if pay_status:
  99. query = query.filter(BizOrder.pay_status == pay_status)
  100. if payment_method:
  101. pm = self.normalize_payment_method(payment_method)
  102. if pm == "offline":
  103. query = query.filter(BizOrder.payment_method == "offline")
  104. else:
  105. query = query.filter(
  106. or_(
  107. BizOrder.payment_method == "online",
  108. BizOrder.payment_method.is_(None),
  109. BizOrder.payment_method == "",
  110. )
  111. )
  112. if cancel_status:
  113. query = query.filter(BizOrder.cancel_status == cancel_status)
  114. if order_no:
  115. query = query.filter(BizOrder.order_no.like(f"%{order_no}%"))
  116. if keyword:
  117. kw = (keyword or "").strip()
  118. if kw:
  119. pattern = f"%{kw}%"
  120. client_user_match = exists(
  121. select(1).select_from(SysUser).where(
  122. and_(
  123. SysUser.id == BizOrder.client_id,
  124. SysUser.is_del == 0,
  125. or_(
  126. SysUser.real_name.like(pattern),
  127. SysUser.username.like(pattern),
  128. SysUser.phone.like(pattern),
  129. ),
  130. )
  131. )
  132. )
  133. query = query.filter(
  134. or_(
  135. BizOrder.order_no.like(pattern),
  136. BizOrder.contact_person.like(pattern),
  137. BizOrder.contact_phone.like(pattern),
  138. client_user_match,
  139. )
  140. )
  141. if start_date:
  142. query = query.filter(BizOrder.create_at >= start_date)
  143. if end_date:
  144. query = query.filter(BizOrder.create_at <= end_date)
  145. # 客户端订单列表 Tab(与小程序 order.vue 分段一致,保证分页有效)
  146. if list_tab == "pending_payment":
  147. query = query.filter(
  148. and_(
  149. BizOrder.order_status == "pending_payment",
  150. BizOrder.pay_status == "pending_payment",
  151. BizOrder.cancel_status != "pending_review",
  152. )
  153. )
  154. elif list_tab == "pending_confirm":
  155. query = query.filter(
  156. and_(
  157. BizOrder.order_status == "pending_confirm",
  158. BizOrder.cancel_status != "pending_review",
  159. )
  160. )
  161. elif list_tab == "completed":
  162. query = query.filter(BizOrder.order_status == "completed")
  163. elif list_tab == "cancelled":
  164. query = query.filter(BizOrder.order_status == "cancelled")
  165. total = query.count()
  166. orders = query.order_by(BizOrder.create_at.desc()).offset(skip).limit(limit).all()
  167. return orders, total
  168. def get_by_order_no(self, db: Session, order_no: str) -> Optional[BizOrder]:
  169. """根据订单号获取订单"""
  170. return db.query(BizOrder).filter(
  171. and_(
  172. BizOrder.order_no == order_no,
  173. BizOrder.is_del == 0
  174. )
  175. ).first()
  176. def create_order(
  177. self,
  178. db: Session,
  179. client_id: str,
  180. region_id: str,
  181. price_list_id: str,
  182. price_version: int,
  183. items: List[dict],
  184. address_id: str,
  185. address_info: dict,
  186. payment_method: str,
  187. remark: str = None,
  188. creator: dict = None
  189. ) -> BizOrder:
  190. """创建订单"""
  191. # 生成订单号
  192. order_no = self._generate_order_no(db)
  193. # 计算订单总金额
  194. total_amount = sum(
  195. Decimal(str(item.get("quantity", 0))) * Decimal(str(item.get("unitPrice", 0)))
  196. for item in items
  197. )
  198. normalized_payment_method = self.normalize_payment_method(payment_method)
  199. # 线上:未支付前主状态为待支付;线下:未收款前 pay_status 仍为待支付,但主状态进入待收货/履约阶段,可与 pending_dispatch 一并参与分配配送
  200. if normalized_payment_method == "offline":
  201. init_order_status = "pending_confirm"
  202. else:
  203. init_order_status = "pending_payment"
  204. init_delivery_status = "pending_dispatch"
  205. init_pay_status = "pending_payment"
  206. order = BizOrder(
  207. id=generate_id(),
  208. order_no=order_no,
  209. region_id=region_id,
  210. client_id=client_id,
  211. price_list_id=price_list_id,
  212. price_version=price_version,
  213. status=derive_legacy_status_from_three(
  214. order_status=init_order_status,
  215. delivery_status=init_delivery_status,
  216. pay_status=init_pay_status,
  217. ),
  218. order_status=init_order_status,
  219. delivery_status=init_delivery_status,
  220. pay_status=init_pay_status,
  221. cancel_status="none",
  222. payment_method=normalized_payment_method,
  223. client_settlement_status="settled" if normalized_payment_method == "online" else "pending",
  224. supplier_settlement_status="pending",
  225. client_settlement_at=datetime.now() if normalized_payment_method == "online" else None,
  226. supplier_settlement_at=None,
  227. total_amount=total_amount,
  228. paid_amount=Decimal("0"),
  229. receivable_amount=total_amount,
  230. refund_amount=Decimal("0"),
  231. contact_person=address_info.get("contactPerson"),
  232. contact_phone=address_info.get("contactPhone"),
  233. delivery_address=address_info.get("fullAddress"),
  234. remark=remark,
  235. payment_time=None,
  236. create_by=creator.get("user_id") if creator else None,
  237. create_name=creator.get("real_name") if creator else None
  238. )
  239. db.add(order)
  240. db.flush()
  241. # 创建订单项
  242. for item in items:
  243. amount = Decimal(str(item.get("quantity", 0))) * Decimal(str(item.get("unitPrice", 0)))
  244. order_item = BizOrderItem(
  245. id=generate_id(),
  246. order_id=order.id,
  247. product_id=item.get("productId"),
  248. unit_id=item.get("unitId"),
  249. quantity=Decimal(str(item.get("quantity", 0))),
  250. unit_price=Decimal(str(item.get("unitPrice", 0))),
  251. amount=amount,
  252. delivered_quantity=Decimal("0"),
  253. status="normal",
  254. note=item.get("note"),
  255. create_by=creator.get("user_id") if creator else None,
  256. create_name=creator.get("real_name") if creator else None
  257. )
  258. db.add(order_item)
  259. # 创建订单状态日志
  260. self._create_status_log(
  261. db,
  262. order.id,
  263. None,
  264. order.status,
  265. creator.get("user_id") if creator else client_id,
  266. "client",
  267. "创建订单"
  268. )
  269. db.commit()
  270. db.refresh(order)
  271. return order
  272. def update_status(
  273. self,
  274. db: Session,
  275. order_id: str,
  276. new_status: str,
  277. operator_id: str,
  278. operator_role: str,
  279. reason: str = None,
  280. note: str = None
  281. ) -> Optional[BizOrder]:
  282. """更新订单状态"""
  283. order = self.get(db, order_id)
  284. if not order:
  285. return None
  286. old_status = order.status
  287. order.status = new_status
  288. order.update_at = datetime.now()
  289. # 特殊状态处理
  290. if new_status == "paid":
  291. order.payment_time = datetime.now()
  292. elif new_status == "completed":
  293. order.completed_time = datetime.now()
  294. order.completed_by = operator_id
  295. # 阶段A:同步三字段
  296. self._sync_three_status_fields_from_legacy(order)
  297. # 创建状态变更日志
  298. self._create_status_log(
  299. db,
  300. order_id,
  301. old_status,
  302. new_status,
  303. operator_id,
  304. operator_role,
  305. reason,
  306. note
  307. )
  308. db.commit()
  309. db.refresh(order)
  310. return order
  311. def cancel_order(
  312. self,
  313. db: Session,
  314. order_id: str,
  315. cancel_type: str,
  316. reason: str,
  317. operator_id: str,
  318. operator_role: str
  319. ) -> Optional[BizOrder]:
  320. """取消订单"""
  321. order = self.get(db, order_id)
  322. if not order:
  323. return None
  324. # 待支付订单直接取消
  325. if order.status == "pending_payment":
  326. return self.update_status(
  327. db,
  328. order_id,
  329. "cancelled",
  330. operator_id,
  331. operator_role,
  332. reason=reason
  333. )
  334. # 已支付订单申请取消
  335. if order.delivery_status in ["pending_dispatch", "delivering"]:
  336. self._apply_three_status(order, order_status="pending_confirm")
  337. order.cancel_status = "pending_review"
  338. order.cancel_reason = reason
  339. order.update_at = datetime.now()
  340. self._create_status_log(
  341. db,
  342. order_id,
  343. order.status,
  344. "pending_review_cancel", # 兼容历史日志标识
  345. operator_id,
  346. operator_role,
  347. reason=reason
  348. )
  349. db.commit()
  350. db.refresh(order)
  351. return order
  352. return None
  353. def review_cancel(
  354. self,
  355. db: Session,
  356. order_id: str,
  357. approved: bool,
  358. reviewer_id: str,
  359. review_note: str = None
  360. ) -> Optional[BizOrder]:
  361. """审核取消订单"""
  362. order = self.get(db, order_id)
  363. if not order or order.cancel_status != "pending_review":
  364. return None
  365. if approved:
  366. order.cancel_status = "approved"
  367. self._apply_three_status(order, order_status="cancelled")
  368. new_status = order.status
  369. else:
  370. # 从状态日志获取取消前的状态
  371. from app.models.models import BizOrderStatusLog
  372. cancel_log = db.query(BizOrderStatusLog).filter(
  373. BizOrderStatusLog.order_id == order_id,
  374. BizOrderStatusLog.to_status == "pending_review_cancel"
  375. ).order_by(BizOrderStatusLog.create_at.desc()).first()
  376. # 恢复为取消前的状态(pending_delivery, delivering, partial_delivered)
  377. if cancel_log and cancel_log.from_status:
  378. # 兼容历史:尽量从旧状态恢复到三字段
  379. order.status = cancel_log.from_status
  380. self._sync_three_status_fields_from_legacy(order)
  381. else:
  382. self._apply_three_status(order, order_status="pending_confirm", delivery_status="pending_dispatch")
  383. order.cancel_status = "rejected"
  384. new_status = order.status
  385. order.cancel_review_time = datetime.now()
  386. order.cancel_reviewed_by = reviewer_id
  387. order.cancel_review_note = review_note
  388. order.update_at = datetime.now()
  389. self._create_status_log(
  390. db,
  391. order_id,
  392. "pending_review_cancel",
  393. new_status,
  394. reviewer_id,
  395. "admin",
  396. reason=f"审核{'通过' if approved else '拒绝'}: {review_note}"
  397. )
  398. db.commit()
  399. db.refresh(order)
  400. return order
  401. def confirm_receive(
  402. self,
  403. db: Session,
  404. order_id: str,
  405. operator_id: str
  406. ) -> Optional[BizOrder]:
  407. """确认收货"""
  408. order = self.get(db, order_id)
  409. if not order:
  410. return None
  411. # 仅待确认收货状态支持确认收货
  412. if order.status == "pending_confirm":
  413. return self.update_status(
  414. db,
  415. order_id,
  416. "completed",
  417. operator_id,
  418. "client",
  419. reason="客户确认收货"
  420. )
  421. return None
  422. def auto_confirm_receive_overdue(
  423. self,
  424. db: Session,
  425. *,
  426. before_threshold: datetime,
  427. configured_days: int,
  428. ) -> int:
  429. """送达后超过 configured_days 未确认收货的订单,系统自动确认(与 force_complete 一致写三字段与日志)。"""
  430. threshold_expr = func.coalesce(BizOrder.delivered_time, BizOrder.update_at)
  431. orders = (
  432. db.query(BizOrder)
  433. .filter(
  434. BizOrder.is_del == 0,
  435. BizOrder.order_status == "pending_confirm",
  436. BizOrder.delivery_status == "delivered",
  437. BizOrder.cancel_status.in_(("none", "rejected")),
  438. threshold_expr <= before_threshold,
  439. )
  440. .all()
  441. )
  442. if not orders:
  443. return 0
  444. note = f"送达后超过{configured_days}天未确认收货,系统自动确认"
  445. count = 0
  446. for order in orders:
  447. old_status = order.status
  448. self._apply_three_status(order, order_status="completed")
  449. order.completed_time = datetime.now()
  450. order.completed_by = "system"
  451. order.update_at = datetime.now()
  452. self._create_status_log(
  453. db,
  454. order.id,
  455. old_status,
  456. "completed",
  457. "system",
  458. "system",
  459. reason=note,
  460. )
  461. count += 1
  462. db.commit()
  463. return count
  464. def _generate_order_no(self, db: Session) -> str:
  465. """生成订单号"""
  466. today = datetime.now().strftime("%Y%m%d")
  467. prefix = f"ORD{today}"
  468. # 查询当天最大订单号
  469. last_order = db.query(BizOrder).filter(
  470. BizOrder.order_no.like(f"{prefix}%")
  471. ).order_by(BizOrder.order_no.desc()).first()
  472. if last_order:
  473. last_no = int(last_order.order_no[-4:])
  474. new_no = last_no + 1
  475. else:
  476. new_no = 1
  477. return f"{prefix}{str(new_no).zfill(4)}"
  478. def _create_status_log(
  479. self,
  480. db: Session,
  481. order_id: str,
  482. from_status: str,
  483. to_status: str,
  484. operator_id: str,
  485. operator_role: str,
  486. reason: str = None,
  487. note: str = None,
  488. create_name: str = None,
  489. log_create_at: Optional[datetime] = None,
  490. ):
  491. """创建订单状态变更日志"""
  492. ts = log_create_at or datetime.now()
  493. log = BizOrderStatusLog(
  494. id=generate_id(),
  495. order_id=order_id,
  496. from_status=from_status,
  497. to_status=to_status,
  498. operator_id=operator_id,
  499. operator_role=operator_role,
  500. reason=reason,
  501. note=note,
  502. create_by=operator_id,
  503. create_name=create_name or None,
  504. create_at=ts,
  505. update_at=ts,
  506. )
  507. db.add(log)
  508. def close_unpaid_online_orders(
  509. self,
  510. db: Session,
  511. *,
  512. before_time: datetime,
  513. timeout_note: str,
  514. ) -> int:
  515. """关闭超时未支付的线上订单。"""
  516. candidates = db.query(BizOrder).filter(
  517. BizOrder.is_del == 0,
  518. BizOrder.payment_method == "online",
  519. BizOrder.pay_status == "pending_payment",
  520. BizOrder.order_status == "pending_payment",
  521. BizOrder.create_at <= before_time,
  522. ).all()
  523. count = 0
  524. for order in candidates:
  525. self._apply_three_status(order, order_status="cancelled", delivery_status="pending_dispatch", pay_status="pending_payment")
  526. order.cancel_status = "approved"
  527. order.cancel_reason = "线上支付超时自动关闭"
  528. order.cancel_review_note = "系统自动关闭"
  529. order.update_at = datetime.now()
  530. self._create_status_log(
  531. db,
  532. order.id,
  533. "pending_payment",
  534. "cancelled",
  535. "system",
  536. "system",
  537. reason="线上支付超时自动关闭",
  538. note=timeout_note,
  539. )
  540. count += 1
  541. if count:
  542. db.commit()
  543. return count
  544. def admin_cancel(
  545. self,
  546. db: Session,
  547. *,
  548. order_id: str,
  549. operator_id: str,
  550. operator_name: str,
  551. reason: str,
  552. note: Optional[str] = None,
  553. evidence_image_urls: Optional[List[str]] = None,
  554. ) -> Optional[BizOrder]:
  555. """运营取消订单。"""
  556. order = self.get(db, order_id)
  557. if not order:
  558. return None
  559. old_status = order.status
  560. self._apply_three_status(order, order_status="cancelled")
  561. order.cancel_status = "approved"
  562. order.cancel_reason = reason
  563. if note:
  564. order.cancel_review_note = note
  565. order.cancel_review_time = datetime.now()
  566. order.cancel_reviewed_by = operator_id
  567. order.update_at = datetime.now()
  568. self._create_status_log(
  569. db,
  570. order.id,
  571. old_status,
  572. "cancelled",
  573. operator_id,
  574. "admin",
  575. reason=reason,
  576. note=json.dumps({"note": note or "", "evidenceImageUrls": evidence_image_urls or []}, ensure_ascii=False),
  577. )
  578. db.commit()
  579. db.refresh(order)
  580. return order
  581. def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BizOrder]:
  582. """更新订单并记录历史"""
  583. original = self.get(db, id)
  584. if not original:
  585. return None
  586. his_data = {
  587. "id": original.id,
  588. "order_no": original.order_no,
  589. "region_id": original.region_id,
  590. "client_id": original.client_id,
  591. "price_list_id": original.price_list_id,
  592. "price_version": original.price_version,
  593. "status": original.status,
  594. "order_status": getattr(original, "order_status", None),
  595. "delivery_status": getattr(original, "delivery_status", None),
  596. "pay_status": getattr(original, "pay_status", None),
  597. "cancel_status": getattr(original, "cancel_status", None),
  598. "payment_method": original.payment_method,
  599. "client_settlement_status": getattr(original, "client_settlement_status", "pending"),
  600. "supplier_settlement_status": getattr(original, "supplier_settlement_status", "pending"),
  601. "client_settlement_at": getattr(original, "client_settlement_at", None),
  602. "supplier_settlement_at": getattr(original, "supplier_settlement_at", None),
  603. "total_amount": original.total_amount,
  604. "paid_amount": original.paid_amount,
  605. "receivable_amount": original.receivable_amount,
  606. "refund_amount": original.refund_amount,
  607. "contact_person": original.contact_person,
  608. "contact_phone": original.contact_phone,
  609. "delivery_address": original.delivery_address,
  610. "cancel_reason": original.cancel_reason,
  611. "cancel_review_time": original.cancel_review_time,
  612. "cancel_reviewed_by": original.cancel_reviewed_by,
  613. "cancel_review_note": original.cancel_review_note,
  614. "completed_time": original.completed_time,
  615. "completed_by": original.completed_by,
  616. "remark": original.remark,
  617. "payment_time": original.payment_time,
  618. "delivery_assign_time": original.delivery_assign_time,
  619. "delivered_time": getattr(original, "delivered_time", None),
  620. "create_at": original.create_at,
  621. "create_by": original.create_by,
  622. "create_name": original.create_name,
  623. "update_at": original.update_at,
  624. "update_by": original.update_by,
  625. }
  626. his = BizOrderHis(**his_data)
  627. db.add(his)
  628. return self.update(db, id, obj_in, updater)
  629. def soft_delete_with_history(self, db: Session, id: str) -> bool:
  630. """删除订单并记录历史"""
  631. original = self.get(db, id)
  632. if not original:
  633. return False
  634. his_data = {
  635. "id": original.id,
  636. "order_no": original.order_no,
  637. "region_id": original.region_id,
  638. "client_id": original.client_id,
  639. "price_list_id": original.price_list_id,
  640. "price_version": original.price_version,
  641. "status": original.status,
  642. "order_status": getattr(original, "order_status", None),
  643. "delivery_status": getattr(original, "delivery_status", None),
  644. "pay_status": getattr(original, "pay_status", None),
  645. "cancel_status": getattr(original, "cancel_status", None),
  646. "payment_method": original.payment_method,
  647. "client_settlement_status": getattr(original, "client_settlement_status", "pending"),
  648. "supplier_settlement_status": getattr(original, "supplier_settlement_status", "pending"),
  649. "client_settlement_at": getattr(original, "client_settlement_at", None),
  650. "supplier_settlement_at": getattr(original, "supplier_settlement_at", None),
  651. "total_amount": original.total_amount,
  652. "paid_amount": original.paid_amount,
  653. "receivable_amount": original.receivable_amount,
  654. "refund_amount": original.refund_amount,
  655. "contact_person": original.contact_person,
  656. "contact_phone": original.contact_phone,
  657. "delivery_address": original.delivery_address,
  658. "delivered_time": getattr(original, "delivered_time", None),
  659. "create_at": original.create_at,
  660. "create_by": original.create_by,
  661. "create_name": original.create_name,
  662. "update_at": original.update_at,
  663. "update_by": original.update_by,
  664. }
  665. his = BizOrderHis(**his_data)
  666. db.add(his)
  667. return self.delete(db, id)
  668. def is_order_lock_expired(
  669. self,
  670. order: BizOrder,
  671. ttl_seconds: int = ORDER_EDIT_LOCK_TTL_SECONDS,
  672. ) -> bool:
  673. """锁是否已过期(无锁或无锁定时间视为过期/可抢)。"""
  674. if not getattr(order, "lock_operator_id", None):
  675. return True
  676. if not getattr(order, "lock_time", None):
  677. return True
  678. return (datetime.now() - order.lock_time).total_seconds() > ttl_seconds
  679. def clear_order_lock(self, db: Session, order: BizOrder) -> BizOrder:
  680. """清空锁字段(调用方负责 commit,或本方法内 commit)。"""
  681. order.lock_operator_id = None
  682. order.lock_time = None
  683. order.update_at = datetime.now()
  684. db.commit()
  685. db.refresh(order)
  686. return order
  687. def lock_order(
  688. self,
  689. db: Session,
  690. order_id: str,
  691. operator_id: str,
  692. ttl_seconds: int = ORDER_EDIT_LOCK_TTL_SECONDS,
  693. ) -> Optional[BizOrder]:
  694. """锁定订单,防止并发写操作;他人锁超 TTL 后可抢。"""
  695. order = self.get(db, order_id)
  696. if not order:
  697. return None
  698. if order.lock_operator_id and order.lock_operator_id != operator_id:
  699. if not self.is_order_lock_expired(order, ttl_seconds):
  700. lock_time = (
  701. order.lock_time.strftime("%Y-%m-%d %H:%M:%S")
  702. if order.lock_time
  703. else "未知"
  704. )
  705. raise ValueError(f"订单已被用户 {order.lock_operator_id} 于 {lock_time} 锁定")
  706. order.lock_operator_id = operator_id
  707. order.lock_time = datetime.now()
  708. order.update_at = datetime.now()
  709. db.commit()
  710. db.refresh(order)
  711. return order
  712. def unlock_order(
  713. self,
  714. db: Session,
  715. order_id: str,
  716. operator_id: str,
  717. force: bool = False,
  718. ) -> Optional[BizOrder]:
  719. """解锁订单;force=True 可解他人锁;无锁时幂等成功。"""
  720. order = self.get(db, order_id)
  721. if not order:
  722. return None
  723. if not order.lock_operator_id:
  724. return order
  725. if not force and order.lock_operator_id != operator_id:
  726. raise ValueError("只能解锁自己锁定的订单")
  727. order.lock_operator_id = None
  728. order.lock_time = None
  729. order.update_at = datetime.now()
  730. db.commit()
  731. db.refresh(order)
  732. return order
  733. def force_complete(
  734. self,
  735. db: Session,
  736. order_id: str,
  737. operator_id: str,
  738. operator_role: str,
  739. reason: str = None
  740. ) -> Optional[BizOrder]:
  741. """运营强制完结订单"""
  742. order = self.get(db, order_id)
  743. if not order:
  744. return None
  745. # 只有待确认收货状态可以强制完结
  746. if order.order_status not in ["pending_confirm"]:
  747. raise ValueError("只有待确认收货状态的订单可以强制完结")
  748. old_status = order.status
  749. self._apply_three_status(order, order_status="completed")
  750. order.completed_time = datetime.now()
  751. order.completed_by = operator_id
  752. order.update_at = datetime.now()
  753. # 记录状态变更
  754. self._create_status_log(
  755. db,
  756. order_id,
  757. old_status,
  758. "completed",
  759. operator_id,
  760. operator_role,
  761. reason=reason or "运营强制完结"
  762. )
  763. db.commit()
  764. db.refresh(order)
  765. return order
  766. def update_to_partial_delivered(
  767. self,
  768. db: Session,
  769. order_id: str,
  770. operator_id: str,
  771. operator_role: str,
  772. reason: str = None
  773. ) -> Optional[BizOrder]:
  774. """已废弃:旧的部分送达流程不再支持。"""
  775. raise ValueError("部分送达流程已下线")
  776. def update_to_confirm(
  777. self,
  778. db: Session,
  779. order_id: str,
  780. operator_id: str,
  781. operator_role: str,
  782. reason: str = None
  783. ) -> Optional[BizOrder]:
  784. """已废弃:旧的部分送达确认流程不再支持。"""
  785. raise ValueError("部分送达确认流程已下线")
  786. class OrderItemCRUD(CRUDBase[BizOrderItem]):
  787. """订单项CRUD操作"""
  788. def get_by_order(self, db: Session, order_id: str) -> List[BizOrderItem]:
  789. """获取订单的所有订单项"""
  790. return db.query(BizOrderItem).filter(
  791. and_(
  792. BizOrderItem.order_id == order_id,
  793. BizOrderItem.is_del == 0
  794. )
  795. ).all()
  796. def get_count_by_order(self, db: Session, order_id: str) -> int:
  797. """获取订单项数量"""
  798. return db.query(BizOrderItem).filter(
  799. and_(
  800. BizOrderItem.order_id == order_id,
  801. BizOrderItem.is_del == 0
  802. )
  803. ).count()
  804. def update_item_adjustment(
  805. self,
  806. db: Session,
  807. item_id: str,
  808. new_quantity: Decimal = None,
  809. new_amount: Decimal = None,
  810. new_status: str = None,
  811. note: str = None,
  812. updater_id: str = None,
  813. updater_name: str = None
  814. ) -> Optional[BizOrderItem]:
  815. """更新订单项(调整后)"""
  816. item = self.get(db, item_id)
  817. if not item:
  818. return None
  819. if new_quantity is not None:
  820. item.quantity = new_quantity
  821. if new_amount is not None:
  822. item.amount = new_amount
  823. if new_status is not None:
  824. item.status = new_status
  825. if note is not None:
  826. item.note = note
  827. item.update_at = datetime.now()
  828. item.update_by = updater_id
  829. db.commit()
  830. db.refresh(item)
  831. return item
  832. def mark_as_reduced(
  833. self,
  834. db: Session,
  835. item_id: str,
  836. new_quantity: Decimal,
  837. new_amount: Decimal,
  838. note: str = None
  839. ) -> Optional[BizOrderItem]:
  840. """标记为已减量"""
  841. return self.update_item_adjustment(
  842. db, item_id,
  843. new_quantity=new_quantity,
  844. new_amount=new_amount,
  845. new_status="reduced",
  846. note=note
  847. )
  848. def mark_as_deleted(
  849. self,
  850. db: Session,
  851. item_id: str,
  852. note: str = None
  853. ) -> Optional[BizOrderItem]:
  854. """标记为已删除"""
  855. return self.update_item_adjustment(
  856. db, item_id,
  857. new_status="deleted",
  858. note=note
  859. )
  860. def mark_as_substituted(
  861. self,
  862. db: Session,
  863. item_id: str,
  864. new_product_id: str,
  865. new_quantity: Decimal,
  866. new_amount: Decimal,
  867. note: str = None
  868. ) -> Optional[BizOrderItem]:
  869. """标记为已替代"""
  870. item = self.update_item_adjustment(
  871. db, item_id,
  872. new_status="substituted",
  873. note=note
  874. )
  875. return item
  876. class OrderStatusLogCRUD(CRUDBase[BizOrderStatusLog]):
  877. """订单状态日志CRUD操作"""
  878. def get_by_order(self, db: Session, order_id: str) -> List[BizOrderStatusLog]:
  879. """获取订单的状态变更日志"""
  880. return db.query(BizOrderStatusLog).filter(
  881. BizOrderStatusLog.order_id == order_id
  882. ).order_by(BizOrderStatusLog.create_at.asc()).all()
  883. class OrderAdjustCRUD(CRUDBase[BizOrderAdjust]):
  884. """订单调整记录CRUD操作"""
  885. def get_by_order(self, db: Session, order_id: str) -> List[BizOrderAdjust]:
  886. """获取订单的所有调整记录"""
  887. return db.query(BizOrderAdjust).filter(
  888. and_(
  889. BizOrderAdjust.order_id == order_id,
  890. BizOrderAdjust.is_del == 0
  891. )
  892. ).order_by(BizOrderAdjust.create_at.desc()).all()
  893. def get_by_order_item(self, db: Session, order_item_id: str) -> List[BizOrderAdjust]:
  894. """获取订单项的调整记录"""
  895. return db.query(BizOrderAdjust).filter(
  896. and_(
  897. BizOrderAdjust.order_item_id == order_item_id,
  898. BizOrderAdjust.is_del == 0
  899. )
  900. ).order_by(BizOrderAdjust.create_at.desc()).all()
  901. def create_adjust_record(
  902. self,
  903. db: Session,
  904. order_id: str,
  905. adjust_type: str,
  906. operator_id: str,
  907. operator_name: str,
  908. order_item_id: str = None,
  909. before_quantity: Decimal = None,
  910. after_quantity: Decimal = None,
  911. before_amount: Decimal = None,
  912. after_amount: Decimal = None,
  913. difference_amount: Decimal = None,
  914. reason: str = None,
  915. substitute_product_id: str = None,
  916. snapshot_items_json: Optional[str] = None,
  917. ) -> BizOrderAdjust:
  918. """创建调整记录"""
  919. adjust_record = BizOrderAdjust(
  920. id=generate_id(),
  921. order_id=order_id,
  922. order_item_id=order_item_id,
  923. adjust_type=adjust_type,
  924. before_quantity=before_quantity,
  925. after_quantity=after_quantity,
  926. before_amount=before_amount,
  927. after_amount=after_amount,
  928. difference_amount=difference_amount or Decimal("0"),
  929. reason=reason,
  930. substitute_product_id=substitute_product_id,
  931. snapshot_items_json=snapshot_items_json,
  932. operator_id=operator_id,
  933. operator_name=operator_name,
  934. create_by=operator_id,
  935. create_name=operator_name
  936. )
  937. db.add(adjust_record)
  938. db.commit()
  939. db.refresh(adjust_record)
  940. return adjust_record
  941. order_crud = OrderCRUD(BizOrder)
  942. order_item_crud = OrderItemCRUD(BizOrderItem)
  943. order_status_log_crud = OrderStatusLogCRUD(BizOrderStatusLog)
  944. order_adjust_crud = OrderAdjustCRUD(BizOrderAdjust)