client_cart.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. """客户购物车 CRUD"""
  2. from decimal import Decimal
  3. from typing import List, Optional, Dict, Any
  4. from collections import defaultdict
  5. from datetime import datetime
  6. from sqlalchemy.orm import Session
  7. from sqlalchemy import and_
  8. from app.models.models import BizClientCart, BizClientCartHis
  9. from app.core.utils import generate_id
  10. from app.crud.price_list import price_list_crud, price_list_item_crud
  11. from app.crud.unit import unit_crud
  12. from app.crud.product import product_crud
  13. from app.crud.category import category_crud
  14. class ClientCartCRUD:
  15. def list_by_client(self, db: Session, client_id: str) -> List[BizClientCart]:
  16. return (
  17. db.query(BizClientCart)
  18. .filter(and_(BizClientCart.client_id == client_id, BizClientCart.is_del == 0))
  19. .order_by(BizClientCart.create_at.desc())
  20. .all()
  21. )
  22. def sync_to_active_price_list(
  23. self,
  24. db: Session,
  25. client_id: str,
  26. region_id: str,
  27. operator: dict,
  28. rows: Optional[List[BizClientCart]] = None,
  29. ) -> bool:
  30. """
  31. 将购物车中仍属于当前生效价格清单的商品行,同步为区域最新生效清单的单价、单位与清单 ID。
  32. 同一商品多条行(历史清单 ID 不同)合并数量并保留一行。
  33. 无生效清单时返回 False,不修改数据。
  34. """
  35. pl = price_list_crud.get_active(db, region_id)
  36. if not pl:
  37. return False
  38. rows = rows if rows is not None else self.list_by_client(db, client_id)
  39. if not rows:
  40. return True
  41. # 快速路径:如果购物车所有行已经属于当前生效价格清单,则无需做任何同步。
  42. # 这能避免在“频繁加购/改数量”场景下对整车做 update + his insert,导致卡顿。
  43. if all(getattr(r, "price_list_id", None) == pl.id for r in rows):
  44. return True
  45. price_items = price_list_item_crud.get_by_price_list(db, pl.id)
  46. pmap: Dict[str, Any] = {i.product_id: i for i in price_items}
  47. by_product: Dict[str, List[BizClientCart]] = defaultdict(list)
  48. for r in rows:
  49. if r.product_id in pmap:
  50. by_product[r.product_id].append(r)
  51. op_uid = operator.get("user_id")
  52. now = datetime.now()
  53. changed = False
  54. for product_id, rel in by_product.items():
  55. pi = pmap[product_id]
  56. total_qty = sum(Decimal(str(r.quantity)) for r in rel)
  57. if total_qty <= 0:
  58. continue
  59. new_unit_price = Decimal(str(pi.price))
  60. new_amount = (total_qty * new_unit_price).quantize(Decimal("0.01"))
  61. keep = next((r for r in rel if r.price_list_id == pl.id), rel[0])
  62. for r in rel:
  63. if r.id != keep.id:
  64. self._copy_to_his(db, r)
  65. r.is_del = 1
  66. r.update_by = op_uid
  67. r.update_at = now
  68. changed = True
  69. # 只有发生实际变化时才写历史并更新主表,避免“重复同步”导致的历史膨胀与写放大
  70. product = product_crud.get(db, product_id)
  71. unit = unit_crud.get(db, pi.unit_id) if pi.unit_id else None
  72. cat_name = keep.category_name or ""
  73. if product and product.category_id:
  74. cat = category_crud.get(db, product.category_id)
  75. cat_name = cat.name if cat else cat_name
  76. need_update = (
  77. keep.price_list_id != pl.id
  78. or keep.unit_id != pi.unit_id
  79. or Decimal(str(keep.quantity)) != total_qty
  80. or Decimal(str(keep.unit_price)) != new_unit_price
  81. or Decimal(str(keep.amount)) != new_amount
  82. or (product and (keep.product_name != product.name or keep.image_url != product.image_url))
  83. or keep.unit_name != (unit.name if unit else keep.unit_name)
  84. or keep.category_name != (cat_name or keep.category_name)
  85. )
  86. if need_update:
  87. self._copy_to_his(db, keep)
  88. keep.price_list_id = pl.id
  89. keep.unit_id = pi.unit_id
  90. keep.quantity = total_qty
  91. keep.unit_price = new_unit_price
  92. keep.amount = new_amount
  93. keep.product_name = product.name if product else keep.product_name
  94. keep.unit_name = unit.name if unit else keep.unit_name
  95. keep.category_name = cat_name or ""
  96. keep.image_url = product.image_url if product else keep.image_url
  97. keep.update_by = op_uid
  98. keep.update_at = now
  99. changed = True
  100. if changed:
  101. db.commit()
  102. return True
  103. def get(self, db: Session, cart_id: str) -> Optional[BizClientCart]:
  104. return (
  105. db.query(BizClientCart)
  106. .filter(and_(BizClientCart.id == cart_id, BizClientCart.is_del == 0))
  107. .first()
  108. )
  109. def _copy_to_his(self, db: Session, row: BizClientCart) -> None:
  110. his = BizClientCartHis(
  111. id=row.id,
  112. client_id=row.client_id,
  113. price_list_id=row.price_list_id,
  114. product_id=row.product_id,
  115. unit_id=row.unit_id,
  116. quantity=row.quantity,
  117. unit_price=row.unit_price,
  118. amount=row.amount,
  119. product_name=row.product_name,
  120. unit_name=row.unit_name,
  121. category_name=row.category_name,
  122. image_url=row.image_url,
  123. create_at=row.create_at,
  124. create_by=row.create_by,
  125. create_name=row.create_name,
  126. update_at=row.update_at,
  127. update_by=row.update_by,
  128. his_create_at=datetime.now(),
  129. )
  130. db.add(his)
  131. def add_or_update(
  132. self,
  133. db: Session,
  134. client_id: str,
  135. price_list_id: str,
  136. product_id: str,
  137. unit_id: str,
  138. quantity: Decimal,
  139. unit_price: Decimal,
  140. product_name: str,
  141. unit_name: str,
  142. category_name: str,
  143. image_url: str,
  144. operator: dict,
  145. ) -> BizClientCart:
  146. matched = (
  147. db.query(BizClientCart)
  148. .filter(
  149. and_(
  150. BizClientCart.client_id == client_id,
  151. BizClientCart.product_id == product_id,
  152. BizClientCart.price_list_id == price_list_id,
  153. BizClientCart.is_del == 0,
  154. )
  155. )
  156. .all()
  157. )
  158. op_uid = operator.get("user_id")
  159. now = datetime.now()
  160. if matched:
  161. # 兼容历史脏数据:同客户+同价目表+同商品可能存在多行,先合并再做增量更新。
  162. keep = matched[0]
  163. merged_qty = sum(Decimal(str(r.quantity or 0)) for r in matched)
  164. if merged_qty < 0:
  165. merged_qty = Decimal("0")
  166. for row in matched[1:]:
  167. self._copy_to_his(db, row)
  168. row.is_del = 1
  169. row.update_by = op_uid
  170. row.update_at = now
  171. self._copy_to_his(db, keep)
  172. next_qty = merged_qty + Decimal(str(quantity))
  173. amount = (next_qty * unit_price).quantize(Decimal("0.01"))
  174. keep.quantity = next_qty
  175. keep.unit_price = unit_price
  176. keep.amount = amount
  177. keep.product_name = product_name
  178. keep.unit_name = unit_name
  179. keep.category_name = category_name
  180. keep.image_url = image_url
  181. keep.update_by = op_uid
  182. keep.update_at = now
  183. db.commit()
  184. return keep
  185. amount = (quantity * unit_price).quantize(Decimal("0.01"))
  186. row = BizClientCart(
  187. id=generate_id(),
  188. client_id=client_id,
  189. price_list_id=price_list_id,
  190. product_id=product_id,
  191. unit_id=unit_id,
  192. quantity=quantity,
  193. unit_price=unit_price,
  194. amount=amount,
  195. product_name=product_name,
  196. unit_name=unit_name,
  197. category_name=category_name or "",
  198. image_url=image_url,
  199. is_del=0,
  200. create_by=op_uid,
  201. create_name=operator.get("real_name"),
  202. update_by=op_uid,
  203. )
  204. db.add(row)
  205. db.commit()
  206. return row
  207. def update_quantity(
  208. self,
  209. db: Session,
  210. cart_id: str,
  211. client_id: str,
  212. quantity: Decimal,
  213. operator: dict,
  214. current_row: Optional[BizClientCart] = None,
  215. ) -> Optional[BizClientCart]:
  216. row = current_row or self.get(db, cart_id)
  217. if not row or row.client_id != client_id:
  218. return None
  219. self._copy_to_his(db, row)
  220. row.quantity = quantity
  221. row.amount = (quantity * row.unit_price).quantize(Decimal("0.01"))
  222. row.update_by = operator.get("user_id")
  223. row.update_at = datetime.now()
  224. db.commit()
  225. return row
  226. def soft_delete(self, db: Session, cart_id: str, client_id: str, operator: dict) -> bool:
  227. row = self.get(db, cart_id)
  228. if not row or row.client_id != client_id:
  229. return False
  230. self._copy_to_his(db, row)
  231. row.is_del = 1
  232. row.update_by = operator.get("user_id")
  233. row.update_at = datetime.now()
  234. db.commit()
  235. return True
  236. def clear(self, db: Session, client_id: str, operator: dict) -> int:
  237. rows = self.list_by_client(db, client_id)
  238. n = 0
  239. for row in rows:
  240. self._copy_to_his(db, row)
  241. row.is_del = 1
  242. row.update_by = operator.get("user_id")
  243. row.update_at = datetime.now()
  244. n += 1
  245. if n:
  246. db.commit()
  247. return n
  248. def batch_delete(self, db: Session, client_id: str, cart_ids: List[str], operator: dict) -> int:
  249. n = 0
  250. for cid in cart_ids:
  251. if self.soft_delete(db, cid, client_id, operator):
  252. n += 1
  253. return n
  254. client_cart_crud = ClientCartCRUD()