| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 |
- """客户购物车 CRUD"""
- from decimal import Decimal
- from typing import List, Optional, Dict, Any
- from collections import defaultdict
- from datetime import datetime
- from sqlalchemy.orm import Session
- from sqlalchemy import and_
- from app.models.models import BizClientCart, BizClientCartHis
- from app.core.utils import generate_id
- from app.crud.price_list import price_list_crud, price_list_item_crud
- from app.crud.unit import unit_crud
- from app.crud.product import product_crud
- from app.crud.category import category_crud
- class ClientCartCRUD:
- def list_by_client(self, db: Session, client_id: str) -> List[BizClientCart]:
- return (
- db.query(BizClientCart)
- .filter(and_(BizClientCart.client_id == client_id, BizClientCart.is_del == 0))
- .order_by(BizClientCart.create_at.desc())
- .all()
- )
- def sync_to_active_price_list(
- self,
- db: Session,
- client_id: str,
- region_id: str,
- operator: dict,
- rows: Optional[List[BizClientCart]] = None,
- ) -> bool:
- """
- 将购物车中仍属于当前生效价格清单的商品行,同步为区域最新生效清单的单价、单位与清单 ID。
- 同一商品多条行(历史清单 ID 不同)合并数量并保留一行。
- 无生效清单时返回 False,不修改数据。
- """
- pl = price_list_crud.get_active(db, region_id)
- if not pl:
- return False
- rows = rows if rows is not None else self.list_by_client(db, client_id)
- if not rows:
- return True
- # 快速路径:如果购物车所有行已经属于当前生效价格清单,则无需做任何同步。
- # 这能避免在“频繁加购/改数量”场景下对整车做 update + his insert,导致卡顿。
- if all(getattr(r, "price_list_id", None) == pl.id for r in rows):
- return True
- price_items = price_list_item_crud.get_by_price_list(db, pl.id)
- pmap: Dict[str, Any] = {i.product_id: i for i in price_items}
- by_product: Dict[str, List[BizClientCart]] = defaultdict(list)
- for r in rows:
- if r.product_id in pmap:
- by_product[r.product_id].append(r)
- op_uid = operator.get("user_id")
- now = datetime.now()
- changed = False
- for product_id, rel in by_product.items():
- pi = pmap[product_id]
- total_qty = sum(Decimal(str(r.quantity)) for r in rel)
- if total_qty <= 0:
- continue
- new_unit_price = Decimal(str(pi.price))
- new_amount = (total_qty * new_unit_price).quantize(Decimal("0.01"))
- keep = next((r for r in rel if r.price_list_id == pl.id), rel[0])
- for r in rel:
- if r.id != keep.id:
- self._copy_to_his(db, r)
- r.is_del = 1
- r.update_by = op_uid
- r.update_at = now
- changed = True
- # 只有发生实际变化时才写历史并更新主表,避免“重复同步”导致的历史膨胀与写放大
- product = product_crud.get(db, product_id)
- unit = unit_crud.get(db, pi.unit_id) if pi.unit_id else None
- cat_name = keep.category_name or ""
- if product and product.category_id:
- cat = category_crud.get(db, product.category_id)
- cat_name = cat.name if cat else cat_name
- need_update = (
- keep.price_list_id != pl.id
- or keep.unit_id != pi.unit_id
- or Decimal(str(keep.quantity)) != total_qty
- or Decimal(str(keep.unit_price)) != new_unit_price
- or Decimal(str(keep.amount)) != new_amount
- or (product and (keep.product_name != product.name or keep.image_url != product.image_url))
- or keep.unit_name != (unit.name if unit else keep.unit_name)
- or keep.category_name != (cat_name or keep.category_name)
- )
- if need_update:
- self._copy_to_his(db, keep)
- keep.price_list_id = pl.id
- keep.unit_id = pi.unit_id
- keep.quantity = total_qty
- keep.unit_price = new_unit_price
- keep.amount = new_amount
- keep.product_name = product.name if product else keep.product_name
- keep.unit_name = unit.name if unit else keep.unit_name
- keep.category_name = cat_name or ""
- keep.image_url = product.image_url if product else keep.image_url
- keep.update_by = op_uid
- keep.update_at = now
- changed = True
- if changed:
- db.commit()
- return True
- def get(self, db: Session, cart_id: str) -> Optional[BizClientCart]:
- return (
- db.query(BizClientCart)
- .filter(and_(BizClientCart.id == cart_id, BizClientCart.is_del == 0))
- .first()
- )
- def _copy_to_his(self, db: Session, row: BizClientCart) -> None:
- his = BizClientCartHis(
- id=row.id,
- client_id=row.client_id,
- price_list_id=row.price_list_id,
- product_id=row.product_id,
- unit_id=row.unit_id,
- quantity=row.quantity,
- unit_price=row.unit_price,
- amount=row.amount,
- product_name=row.product_name,
- unit_name=row.unit_name,
- category_name=row.category_name,
- image_url=row.image_url,
- create_at=row.create_at,
- create_by=row.create_by,
- create_name=row.create_name,
- update_at=row.update_at,
- update_by=row.update_by,
- his_create_at=datetime.now(),
- )
- db.add(his)
- def add_or_update(
- self,
- db: Session,
- client_id: str,
- price_list_id: str,
- product_id: str,
- unit_id: str,
- quantity: Decimal,
- unit_price: Decimal,
- product_name: str,
- unit_name: str,
- category_name: str,
- image_url: str,
- operator: dict,
- ) -> BizClientCart:
- matched = (
- db.query(BizClientCart)
- .filter(
- and_(
- BizClientCart.client_id == client_id,
- BizClientCart.product_id == product_id,
- BizClientCart.price_list_id == price_list_id,
- BizClientCart.is_del == 0,
- )
- )
- .all()
- )
- op_uid = operator.get("user_id")
- now = datetime.now()
- if matched:
- # 兼容历史脏数据:同客户+同价目表+同商品可能存在多行,先合并再做增量更新。
- keep = matched[0]
- merged_qty = sum(Decimal(str(r.quantity or 0)) for r in matched)
- if merged_qty < 0:
- merged_qty = Decimal("0")
- for row in matched[1:]:
- self._copy_to_his(db, row)
- row.is_del = 1
- row.update_by = op_uid
- row.update_at = now
- self._copy_to_his(db, keep)
- next_qty = merged_qty + Decimal(str(quantity))
- amount = (next_qty * unit_price).quantize(Decimal("0.01"))
- keep.quantity = next_qty
- keep.unit_price = unit_price
- keep.amount = amount
- keep.product_name = product_name
- keep.unit_name = unit_name
- keep.category_name = category_name
- keep.image_url = image_url
- keep.update_by = op_uid
- keep.update_at = now
- db.commit()
- return keep
- amount = (quantity * unit_price).quantize(Decimal("0.01"))
- row = BizClientCart(
- id=generate_id(),
- client_id=client_id,
- price_list_id=price_list_id,
- product_id=product_id,
- unit_id=unit_id,
- quantity=quantity,
- unit_price=unit_price,
- amount=amount,
- product_name=product_name,
- unit_name=unit_name,
- category_name=category_name or "",
- image_url=image_url,
- is_del=0,
- create_by=op_uid,
- create_name=operator.get("real_name"),
- update_by=op_uid,
- )
- db.add(row)
- db.commit()
- return row
- def update_quantity(
- self,
- db: Session,
- cart_id: str,
- client_id: str,
- quantity: Decimal,
- operator: dict,
- current_row: Optional[BizClientCart] = None,
- ) -> Optional[BizClientCart]:
- row = current_row or self.get(db, cart_id)
- if not row or row.client_id != client_id:
- return None
- self._copy_to_his(db, row)
- row.quantity = quantity
- row.amount = (quantity * row.unit_price).quantize(Decimal("0.01"))
- row.update_by = operator.get("user_id")
- row.update_at = datetime.now()
- db.commit()
- return row
- def soft_delete(self, db: Session, cart_id: str, client_id: str, operator: dict) -> bool:
- row = self.get(db, cart_id)
- if not row or row.client_id != client_id:
- return False
- self._copy_to_his(db, row)
- row.is_del = 1
- row.update_by = operator.get("user_id")
- row.update_at = datetime.now()
- db.commit()
- return True
- def clear(self, db: Session, client_id: str, operator: dict) -> int:
- rows = self.list_by_client(db, client_id)
- n = 0
- for row in rows:
- self._copy_to_his(db, row)
- row.is_del = 1
- row.update_by = operator.get("user_id")
- row.update_at = datetime.now()
- n += 1
- if n:
- db.commit()
- return n
- def batch_delete(self, db: Session, client_id: str, cart_ids: List[str], operator: dict) -> int:
- n = 0
- for cid in cart_ids:
- if self.soft_delete(db, cid, client_id, operator):
- n += 1
- return n
- client_cart_crud = ClientCartCRUD()
|