# Design:错误可解释性与付款就绪治理 ## 1. 总体边界 本任务不把全部 `VALID_001` 重构成新异常体系。保留 HTTP status、顶层 code 和既有 details,在已迁移错误中增加稳定 `details.reasonCode`,用注册表、遗留指纹基线和 focused 测试形成只增不退的 ratchet。 ```text 服务端业务事实 -> 原因码注册表 / domain validator -> AppError { code, details: { reasonCode, ...legacyDetails } } -> ApiError(保留 requestId) -> 原因码中文映射 / 页面专用投影 -> 就近提示 + 动作禁用 + 安全兜底 ``` ## 2. 原因码注册表 服务端建立唯一原因码注册表模块,例如 `server/src/constants/errorReasons.js`。每项至少包含: ```js { code: "payment_batch.legal_entity_required", domain: "payment_batch", audience: "user", defaultMessage: "报销人尚未维护所属公司,请联系管理员补充后再安排付款。", recovery: "contact_admin", } ``` 客户端维护显式、可测试的 reasonCode 映射。静态提示可使用注册表默认中文;涉及数量、页面定位或权限差异的提示由窄化 renderer 处理,但必须有默认安全提示。服务端使用常量或经注册表校验的 builder,不散落裸原因码。 兼容响应继续使用 `VALID_001`,并同时保留 legacy details: ```json { "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 或行号做字符串计数: 1. 注册表唯一、命名和元数据完整;`audience=user` 必须有客户端映射。 2. reason-aware builder 的原因均已注册;客户端无孤儿映射。 3. 泛化 `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 和当前配置;输出只含安全原因: ```ts type PaymentReadiness = { ready: boolean; blockers: Array<{ reasonCode: PaymentBatchReasonCode; entityId?: string; // 只用于关联,不直接渲染 entityCode?: string; }>; }; ``` 解析器复用快照、部门 gate、活动占用、付款账户和 reviewer 事实。敏感账户只做存在性、银行机构和可解密判断,响应不带密文、完整账号或原始 employee。 ```text 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/中文及失败副作用断言。当前只完成清单,不新增迁移范围或任何线上行为。