仓储层的 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>
107 lines
4.2 KiB
Python
107 lines
4.2 KiB
Python
"""产品主数据数据访问。
|
||
|
||
本层只 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()
|