文件
hyb-oa/.trellis/tasks/09-05-error-explainability-payment-readiness/design.md
T
wangsli和Claude Sonnet 5 dea0303485 回填10-05到期例外的73文件分流台账 [09-05-error-explainability-payment-readiness]
从孤儿 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]>
2026-09-25 19:49:07 +08:00

6.9 KiB
原始文件 Blame 文件历史

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 或行号做字符串计数:

  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 和当前配置;输出只含安全原因:

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/中文及失败副作用断言。当前只完成清单,不新增迁移范围或任何线上行为。