order.py 33 KB

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