refactor(test): 集成工厂上提共用,submit_change 拆校验段,审计重试断言对齐全局兜底

- tests/integration/factories.py:回款/发货两文件逐字重复的
  _customer/_bitem/_bpayload/_create_business 上提(t_ 前缀与默认行
  口径单点维护);test_audit_retry 的持续冲突断言由 500 更新为
  400「数据完整性冲突」——全局 IntegrityError handler(批 6)收敛
  行为属预期改进,非回归
- param_service.submit_change 拆出 _normalize_items(校验段独立,
  主函数降至 50 行约定内);e2e 六个列表页 helper 收口 common.py
  (base_data 版为准:分组参数化、:text-is 精确匹配),两脚本变薄
- 全量回归 625 passed(基线 603 + 新增 22);e2e 四链路(登录 8/
  系统管理 13/基础数据 13/业务 16)全过
- 测试 README 补 2026-10-15 运行记录与 10 条评审缺陷(预览千分比/
  审计假成功/字典序 MAX/无锁编辑/删产品 500/schema 口径/quantize/
  负天数外插/评级并发/大小写不对称)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
liuwenxinandClaude Code committed 2026-10-15 10:58:44 +08:00
1 parent 0ef3adec21
commit f75a9d534e
6 files changed
+141 -147

No files matched your search

+64 -55
View File
@@ -35,6 +35,7 @@ from app.schemas.param import (
ParamCatalogItemOut,
ParamChangeBatchOut,
ParamChangeIn,
ParamChangeItemIn,
ParamDiffOut,
ParamDimensionValueOut,
ParamHistoryOut,
@@ -188,6 +189,67 @@ def get_catalog(session: Session, *, as_of: date | None = None) -> list[ParamCat
# ==================== 变更与版本管理 ====================
def _normalize_items(
session: Session, items: list[ParamChangeItemIn]
) -> list[tuple[ParamDefinition, str, str]]:
"""全量校验条目并归一为 (定义, 规范维度值, 规范值),任何一项不合法整批拒绝。
定义与岗位各一条 IN 批量查询预载(逐条查询在批次扩大时是写侧 N+1);
岗位预载收集全部非空维度值 —— 集合可能略大于实际所需,但省去按维度
类型先分类的第二轮遍历,无害多取由校验逻辑自行过滤。
:raises BadRequestError: 参数未定义 / 维度不合法 / 值校验失败 / 同批次重复条目
"""
codes = list({item.param_code for item in items})
definitions = param_repository.list_definitions_by_codes(session, codes)
dimension_values = list({item.dimension_value for item in items if item.dimension_value})
# 预载字典键归一为 casefold:单条版 get_by_code 走 SQL ci 校对(大小写
# 变形可命中),批量版改字典查找后须自行等价归一,否则变形大小写会
# 被误判「不是已存在的岗位」;岗位编码均为 ascii 小写蛇形,casefold 足够
roles = {
code.casefold(): role
for code, role in business_role_repository.list_by_codes(session, dimension_values).items()
}
normalized: list[tuple[ParamDefinition, str, str]] = []
seen: set[tuple[str, str]] = set()
for item in items:
definition = definitions.get(item.param_code)
if definition is None:
# 用户输入先行查库给 400;未注册编码出现在库里属数据与注册表漂移
raise BadRequestError(f"参数 {item.param_code} 未在系统中定义")
# ci 校对规则下大小写变形也能命中库,此后一律用库里的规范值:
# 注册表 get_spec 与快照字典均按精确键匹配,原始大小写入库会导致
# 取值链静默不命中(按错误参数算钱且无报错)
spec = get_spec(definition.param_code)
dimension = item.dimension_value
if spec.dimension_type == "NONE" and dimension != "":
raise BadRequestError(f"参数 {item.param_code} 不带维度,维度值须为空")
if spec.dimension_type == "BUSINESS_ROLE" and dimension:
role = roles.get(dimension.casefold())
if role is None:
raise BadRequestError(
f"参数 {item.param_code} 的维度值 {dimension!r} 不是已存在的岗位"
)
dimension = role.role_code
if (
spec.dimension_type == "PRODUCT_CATEGORY"
and dimension
and dimension not in PRODUCT_CATEGORIES
):
raise BadRequestError(
f"参数 {item.param_code} 的维度值 {dimension!r} 不是合法的产品类别"
)
value = validate_and_normalize(spec, item.value)
key = (item.param_code, dimension)
if key in seen:
raise BadRequestError(
f"同一批次内重复提交参数 {item.param_code}({dimension or '默认'})"
)
seen.add(key)
normalized.append((definition, dimension, value))
return normalized
def submit_change(
session: Session,
payload: ParamChangeIn,
@@ -218,61 +280,8 @@ def submit_change(
if payload.change_type == 2 and effective < date.today():
raise BadRequestError("指定生效日期不得早于今日")
# 先全量校验、再写入:任何一项不合法整批拒绝(不留半批脏数据)。
# 定义与岗位各一条 IN 批量查询预载(逐条查询在批次扩大时是写侧 N+1);
# 岗位预载收集全部非空维度值 —— 集合可能略大于实际所需,但省去按
# 维度类型先分类的第二轮遍历,无害多取由校验逻辑自行过滤
codes = list({item.param_code for item in payload.items})
definitions = param_repository.list_definitions_by_codes(session, codes)
dimension_values = list(
{item.dimension_value for item in payload.items if item.dimension_value}
)
# 预载字典键归一为 casefold:单条版 get_by_code 走 SQL ci 校对(大小写
# 变形可命中),批量版改字典查找后须自行等价归一,否则变形大小写会
# 被误判「不是已存在的岗位」;岗位编码均为 ascii 小写蛇形,casefold 足够
roles = {
code.casefold(): role
for code, role in business_role_repository.list_by_codes(
session, dimension_values
).items()
}
normalized: list[tuple[ParamDefinition, str, str]] = []
seen: set[tuple[str, str]] = set()
for item in payload.items:
definition = definitions.get(item.param_code)
if definition is None:
# 用户输入先行查库给 400;未注册编码出现在库里属数据与注册表漂移
raise BadRequestError(f"参数 {item.param_code} 未在系统中定义")
# ci 校对规则下大小写变形也能命中库,此后一律用库里的规范值:
# 注册表 get_spec 与快照字典均按精确键匹配,原始大小写入库会导致
# 取值链静默不命中(按错误参数算钱且无报错)
spec = get_spec(definition.param_code)
dimension = item.dimension_value
if spec.dimension_type == "NONE" and dimension != "":
raise BadRequestError(f"参数 {item.param_code} 不带维度,维度值须为空")
if spec.dimension_type == "BUSINESS_ROLE" and dimension:
role = roles.get(dimension.casefold())
if role is None:
raise BadRequestError(
f"参数 {item.param_code} 的维度值 {dimension!r} 不是已存在的岗位"
)
dimension = role.role_code
if (
spec.dimension_type == "PRODUCT_CATEGORY"
and dimension
and dimension not in PRODUCT_CATEGORIES
):
raise BadRequestError(
f"参数 {item.param_code} 的维度值 {dimension!r} 不是合法的产品类别"
)
value = validate_and_normalize(spec, item.value)
key = (item.param_code, dimension)
if key in seen:
raise BadRequestError(
f"同一批次内重复提交参数 {item.param_code}({dimension or '默认'})"
)
seen.add(key)
normalized.append((definition, dimension, value))
# 先全量校验、再写入:任何一项不合法整批拒绝(不留半批脏数据)
normalized = _normalize_items(session, payload.items)
try:
batch = param_repository.create_batch(