PayrollManagementSystem/backend/app/repositories/product_repository.py
liuwenxin 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 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""产品主数据数据访问。
本层只 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()