文件
PayrollManagementSystem/backend/app/repositories/product_repository.py
T
liuwenxin和Claude Code 5ece9f6dab refactor(backend): 仓储层散参数收敛为查询 DTO
仓储层的 list_*/count_* 此前以 keyword/status/offset/limit 四五个散参数
传参,调用点重复铺开、易漏传且不可扩展;按项目「参数 >4 封装 DTO」的
约定统一收敛为各模块的 Query 对象(如 RoleQuery、BusinessQuery),
服务层与测试调用同步调整,行为不变。

- repositories 层查询方法签名改为 (session, query),分页与筛选条件
  由 Query 承载,_apply_filters 同步收口
- schemas 层补齐各模块 Query 模型(approval/business_role/report 等)
- services 层调用点同步改为传 Query
- tests 随之调整;新增用例:待办角色流转(财务通过后领导出现待办)、
  参数预览顺序稳定

验证:全量 pytest 900 passed(11:08);单元 526 passed;ruff check 绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 19:43:20 +08:00

107 行
4.2 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
"""产品主数据数据访问。
本层只 flush 不 commit —— 事务边界由 services 层控制。
"""
from sqlalchemy import Select, func, or_, select
from sqlalchemy.orm import Session
from app.models import BusinessItem, Product, ShipmentItem
from app.schemas.product import ProductQuery
# ==================== 读取 ====================
def get_by_id(session: Session, product_id: int, *, for_update: bool = False) -> Product | None:
"""按主键查询。删除校验走 ``for_update`` 锁行(同 business_role_repository
的理由:校验与删除之间若发生并发引用,锁行使并发写入可见地失败,
而非引用被静默悬空)。"""
if for_update:
stmt = select(Product).where(Product.id == product_id).with_for_update()
return session.execute(stmt).scalar_one_or_none()
return session.get(Product, product_id)
def get_by_name_spec(session: Session, product_name: str, spec_model: str) -> Product | None:
"""按 (名称, 规格型号) 组合查询。
组合是 uk_products_name_spec 唯一键的键值,先查用于给出即时业务提示;
并发窗口仍存在,最终由 IntegrityError 兜底(见 product_service)。
"""
stmt = select(Product).where(
Product.product_name == product_name,
Product.spec_model == spec_model,
)
return session.execute(stmt).scalar_one_or_none()
def list_products(session: Session, query: ProductQuery) -> list[Product]:
"""分页查询产品(含停用)。按 id 升序,与 M1 岗位列表的次序口径一致。"""
stmt = _apply_filters(select(Product), query)
stmt = stmt.order_by(Product.id).offset(query.offset).limit(query.page_size)
return list(session.execute(stmt).scalars())
def count_products(session: Session, query: ProductQuery) -> int:
"""统计符合条件的产品总数,供分页使用。"""
stmt = _apply_filters(select(func.count(Product.id)), query)
return int(session.execute(stmt).scalar_one())
def _apply_filters(stmt: Select, query: ProductQuery) -> Select:
"""应用通用筛选条件。列表与计数共用,避免两处口径不一致。
关键词对名称/规格两列取 OR 命中。转义与 query_utils.fuzzy 同一机制
(``contains(autoescape=True)`` 转义 LIKE 通配符并附加 ESCAPE 子句);
因 fuzzy 是单列 where 语义(两次调用会叠加成 AND),双列 OR 在此直接
写 contains —— M1 business_role_repository 的双列关键词同款写法。
规格型号常含 ``_``(如 DN300_II),不转义会当单字符通配执行。
"""
if query.keyword:
stmt = stmt.where(
or_(
Product.product_name.contains(query.keyword, autoescape=True),
Product.spec_model.contains(query.keyword, autoescape=True),
)
)
if query.product_category:
stmt = stmt.where(Product.product_category == query.product_category)
if query.status is not None:
stmt = stmt.where(Product.status == query.status)
return stmt
def count_refs(session: Session, product_id: int) -> int:
"""统计引用该产品的业务明细与发货明细行数(删除保护,business_id 先例)。
两张明细表对 products 均 RESTRICT 外键:先查给出可读的 400;校验与
删除之间的并发引用由锁行 + FK 兜底(service 层再翻译 IntegrityError)。
"""
stmt = select(
select(func.count())
.select_from(BusinessItem)
.where(BusinessItem.product_id == product_id)
.scalar_subquery()
+ select(func.count())
.select_from(ShipmentItem)
.where(ShipmentItem.product_id == product_id)
.scalar_subquery()
)
return int(session.execute(stmt).scalar_one())
# ==================== 写入 ====================
def create(session: Session, product: Product) -> Product:
"""新增产品。只 flush 以取得自增 id。"""
session.add(product)
session.flush()
return product
def delete(session: Session, product: Product) -> None:
"""物理删除产品。调用方须先完成引用保护校验(count_refs + FK 兜底)。"""
session.delete(product)
session.flush()