从孤儿 worktree(artifacts/isolated-worktrees/20260909-ops-preparation)合并 回填 2026-09-09 从未提交的到期豁免分流工作: - 对 591 条错误原因码豁免基线(scripts/qa/error-reason-legacy-baseline.json, 全部 2026-10-05 到期)做完整 AST inventory 核对:673 条基线、695 次调用、 73 文件,逐条 observedCount 与基线一致、0 漂移。 - 73 文件按 A(16 文件/优先迁移)/B(25 文件/待确认业务影响)/C(32 文件/ 逐模块审查延期理由)三档分流,逐文件列出指纹与调用数,均未裁决。 - 首批 4 个具体调用已设计好迁移方案(AP 台账开关/查询校验、todo 开关、 portal 开关的 reasonCode 与中文文案),尚未落码。 - 明确 09-11/09-18/09-25/09-30/10-02 的分阶段裁决节奏;10-05 00:00 UTC (北京时间 08:00)即为门禁到期时点。 无产品代码改动、无原因码源码/注册表变更;仅补齐分流台账文档,为处理 10-05 到期风险提供已有基础,避免从零重新盘点 73 个文件。 Co-Authored-By: Claude Sonnet 5 <[email protected]>
6.9 KiB
Design:错误可解释性与付款就绪治理
1. 总体边界
本任务不把全部 VALID_001 重构成新异常体系。保留 HTTP status、顶层 code 和既有 details,在已迁移错误中增加稳定 details.reasonCode,用注册表、遗留指纹基线和 focused 测试形成只增不退的 ratchet。
服务端业务事实
-> 原因码注册表 / domain validator
-> AppError { code, details: { reasonCode, ...legacyDetails } }
-> ApiError(保留 requestId)
-> 原因码中文映射 / 页面专用投影
-> 就近提示 + 动作禁用 + 安全兜底
2. 原因码注册表
服务端建立唯一原因码注册表模块,例如 server/src/constants/errorReasons.js。每项至少包含:
{
code: "payment_batch.legal_entity_required",
domain: "payment_batch",
audience: "user",
defaultMessage: "报销人尚未维护所属公司,请联系管理员补充后再安排付款。",
recovery: "contact_admin",
}
客户端维护显式、可测试的 reasonCode 映射。静态提示可使用注册表默认中文;涉及数量、页面定位或权限差异的提示由窄化 renderer 处理,但必须有默认安全提示。服务端使用常量或经注册表校验的 builder,不散落裸原因码。
兼容响应继续使用 VALID_001,并同时保留 legacy details:
{
"error": {
"code": "VALID_001",
"message": "参数校验失败",
"details": {
"reasonCode": "payment_batch.legal_entity_required",
"legalEntity": "single_legal_entity_required"
}
},
"requestId": "..."
}
新 UI 不使用该 message 作为用户文案来源。未知 reasonCode 走安全兜底,并由现有 ApiError.requestId 附加排障编号。
3. Parity 与遗留基线
新增 scripts/qa/check-error-reason-contract.mjs,使用仓库已有 parser 解析 JS/TS,而不是按 key 或行号做字符串计数:
- 注册表唯一、命名和元数据完整;
audience=user必须有客户端映射。 - reason-aware builder 的原因均已注册;客户端无孤儿映射。
- 泛化
buildValidationError/new AppError(...VALID_001...)调用若没有 reasonCode,必须命中版本化遗留调用指纹。
遗留基线放在 QA 合同附近,例如 scripts/qa/error-reason-legacy-baseline.json。按业务域/文件组记录 owner、reason、expiresOn 和规范化 AST 节点形状指纹;指纹必须忽略空白、缩进、换行和纯格式化差异,源码文本与行号仅用于报告,不参与身份计算。首批主链路 operation 单列为 zero-exception,命中遗留也失败。self-test 用临时 fixture 验证新增、删除、拼写、结构性指纹变化、纯格式化不变、过期和同 key 跨域。
这避免两类假门禁:只比数量可被“一增一删”绕过;只比 details key 会把不同业务语义误判为同一覆盖。
4. 首批迁移边界
首批原因域:
reimbursement.*:提交、草稿编辑、票据关联/恢复。payment_batch.*:候选/组合 readiness、创建、复核、回单和登记。leave.*:员工提交路径。attendance_correction.*:员工/授权应急提交路径。
同一业务后果跨入口复用同一码;不同业务后果即使 legacy key 相同也使用不同码。20 处旧字符串先形成 task-local 处置表:客户端 mock、主链路服务测试、内部/持久化摘要、延期域分别处理。迁移测试断言 exact reasonCode、HTTP status、兼容字段和准确中文,不使用模糊 message 正则。
5. 付款 readiness 服务
抽取无 HTTP 展示依赖的 resolvePaymentBatchCreationReadiness。输入包括 entity type、entity IDs、付款渠道、maker ID 和当前配置;输出只含安全原因:
type PaymentReadiness = {
ready: boolean;
blockers: Array<{
reasonCode: PaymentBatchReasonCode;
entityId?: string; // 只用于关联,不直接渲染
entityCode?: string;
}>;
};
解析器复用快照、部门 gate、活动占用、付款账户和 reviewer 事实。敏感账户只做存在性、银行机构和可解密判断,响应不带密文、完整账号或原始 employee。
GET candidates
-> 单条 resolver
-> readiness/blockers 决定可选性和提示
POST readiness(只读)
-> 当前多选集合的组合 resolver
-> 跨公司/占用/复核人等组合提示
POST create
-> 事务 + 锁后复用同一 resolver
-> ready=false 抛相同 reasonCode
-> ready=true 才创建 batch/items/audit/notification
候选查询必须批量载入必要关系、占用、付款账户和复核人事实,避免在候选循环中产生 N+1。readiness 不落库、不进入长期缓存;资料补齐后刷新查询即可得到新事实。
6. UI 投影
依据 ui-ux-pro-max 的表单反馈规则:错误就近、提供恢复路径、toast 不作唯一反馈。
- 正常候选保持紧凑,不显示“资料已齐全”。
- 异常候选仍出现在列表中,避免再次形成不可见状态空隙;复选框 disabled,行内展示必要缺项。
- 缺员工资料时说明需管理员维护;仅有维护权限者看到对应入口。
- 非建行时说明当前银行付款渠道限制,不引导伪填。
- 组合 readiness 紧贴主动作,使用
role=alert/aria-live,按钮以aria-describedby关联原因。 - create 并发失败复用相同映射留在动作附近;toast 只补充“未创建付款安排”。
- 未知错误显示可行动兜底及可用 request ID,不回显原始 details。
7. 兼容、隐私与回滚
- API 是加法变更;旧 code/details 保留,旧客户端继续工作。
- 不记录银行卡、密文、完整账号或真实 payload。原因码是否进入脱敏服务端日志延期评估。
- 不改变付款状态机、maker-checker、角色、通知对象、回单门禁或 B1/B2。
- 无 migration/schema/seed/flag/routing。生产资料补录不属于部署步骤。
- 建议按“原因码/parity、首批迁移、付款 readiness/UI”形成逻辑提交;付款提交可独立回滚且无数据库补偿。
- 部署另行批准;公网只用合成 fixture 验证。不得自动创建、复核或付款 0014。
8. 到期清单的后续分流合同(2026-09-09)
按已有 scanner 输出的 file/operation/fingerprint/count 与源文件摘要建立逐条台账,文件表仅作索引。未知使用状况记为待复核,不推导无需维护或默认低频;未证明删除/迁移成功的记录不得核销。保留原 expiresOn 与 owner,不把新 reviewDue 当作门禁新期限;拟延期日期和批准记录必须独立且逐模块说明。
共用 validation builder 的一个指纹可能影响多个业务分支,迁移必须在调用语义层选择原因;不能批量将所有旧错误映射成一个“通用参数错误”或只更新计数。每个获批迁移分支先定位入口与旧 HTTP status/code/details,再增加精确 reasonCode/中文及失败副作用断言。当前只完成清单,不新增迁移范围或任何线上行为。