feat: 完成错误原因合同与付款就绪提示(09-05-error-explainability-payment-readiness)
这个提交包含在:
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,126 @@
|
||||
# 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。
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,150 @@
|
||||
# Implementation Plan:错误可解释性与付款就绪治理
|
||||
|
||||
## 0. 开工门
|
||||
|
||||
- [x] 用户复核并批准 PRD、design、implement。
|
||||
- [x] 批准后运行 `task.py start`;inline 模式实施前加载 `trellis-before-dev`,不派 implement/check sub-agent。
|
||||
- [x] 记录开工基线、工作区状态和当前 20 处旧字符串测试命中清单。
|
||||
|
||||
## 1. 原因码合同与 parity
|
||||
|
||||
1. [x] 新增原因码注册表和 reason-aware validation helper;保留顶层 `VALID_001` 与 legacy details。
|
||||
2. [x] 新增客户端 reasonCode 映射与未知错误安全兜底,接入 `ApiError.requestId`。
|
||||
3. [x] 建立 AST/结构化 parity 检查和版本化遗留指纹:基于规范化 AST 节点形状而非源码文本,纯格式化不得改变指纹;注册唯一性、服务端使用、客户端覆盖、孤儿映射、新增遗留、过期例外全部 fail closed。
|
||||
4. [x] self-test 覆盖同 key 跨域、未注册、拼写、缺映射、一增一删、结构性指纹变化、纯格式化不变、到期和主链路试图加例外。
|
||||
5. [x] 接入既有治理入口;报告只输出相对文件、原因码/域、计数和稳定 finding。
|
||||
|
||||
## 2. 首批主链路迁移
|
||||
|
||||
1. [x] 迁移报销提交/编辑/票据关联的用户可达校验。
|
||||
2. [x] 迁移付款创建/复核/回单/登记的用户可达校验。
|
||||
3. [x] 迁移请假提交、考勤修正提交的用户可达校验。
|
||||
4. [x] 主链路达到零例外;其他存量按域记录 owner/reason/expiresOn,不使用永久目录豁免。
|
||||
5. [x] 逐项处置 20 处旧字符串:
|
||||
- 客户端 4 个 mock 改为 reasonCode,并断言具体提示/安全兜底;
|
||||
- 主链路服务测试断言 exact reasonCode、status 和兼容 details;
|
||||
- workflow utility、ERP procurement、budget 等逐项迁移或登记有效遗留;
|
||||
- `failure_message`、批处理 reason 等持久化/内部摘要单独验证兼容,不与 UI 文案混同。
|
||||
|
||||
## 3. 付款 readiness
|
||||
|
||||
1. [x] 抽取共享 resolver,复用快照、部门 gate、账户、占用和 reviewer 事实;搬移 create 条件而非复制。
|
||||
2. [x] 候选批量计算单条 readiness,避免 N+1;异常候选仍返回但不可选择。
|
||||
3. [x] 增加多选组合只读 readiness API;严格输入,响应仅含安全 blocker。
|
||||
4. [x] create 在锁和事务内复用 resolver;失败零 batch/item/成功 audit/notification 副作用。
|
||||
5. [x] 覆盖资料或账户变化后的即时重算,不新增持久字段或长期缓存。
|
||||
|
||||
## 4. 付款页面
|
||||
|
||||
1. [x] 扩展 API types;候选行显示就近 blocker,正常行无解释噪音。
|
||||
2. [x] 异常候选不可选择并具备可访问原因;有维护权限才显示维护入口。
|
||||
3. [x] 多选变化查询组合 readiness;提示紧贴主按钮,按钮禁用、handler 和 create 二次拦截消费同一原因映射。
|
||||
4. [x] 并发失败留在动作附近;toast 为补充;未知原因使用 request ID 安全兜底。
|
||||
5. [x] PC 覆盖主路径;移动端保持现有能力边界且不显示技术错误。
|
||||
|
||||
## 5. 验证
|
||||
|
||||
- [x] `node scripts/qa/check-error-reason-contract.mjs --self-test` 与 strict 正常扫描。
|
||||
- [x] Server:registry/helper、付款 readiness/route、报销/B2、请假、考勤 focused Vitest;涉及 workflow/状态机时做只读 review。
|
||||
- [x] Client:error mapping 测试;付款 Playwright 覆盖正常、三类 0014 缺项、非建行、跨公司、占用、无 reviewer、并发、未知 request ID fallback、键盘/ARIA。
|
||||
- [x] `cd server && npm run prisma:validate`;触达 JS 运行 `node --check`。
|
||||
- [x] `cd client && npm run typecheck && npm run build`。
|
||||
- [x] UI strict/self-test、Trellis governance、敏感信息扫描、`git diff --check`。
|
||||
- [x] 核对 schema/migration/seed/flag/routing 和生产数据修改为零。
|
||||
|
||||
## 6. Spec、提交与部署
|
||||
|
||||
- [x] 实现验证稳定后,把“原因码 + 遗留指纹 + 主链路零例外”和“共享 readiness 多消费者”沉淀到现有 spec,不新建泛化设计文档。
|
||||
- [x] spec 明确迁移一条遗留原因码的完整合同:服务端注册、客户端映射、具体内容断言三件缺一不可。
|
||||
- [ ] 提交建议:A 原因码/parity;B 首批迁移;C 付款 readiness/UI;task 元数据另行回填。
|
||||
- [ ] commit message 使用中文并引用 `[09-05-error-explainability-payment-readiness]`。
|
||||
- [ ] commit、push、provenance、部署分别批准;切换前再次运行 UI strict。
|
||||
- [ ] 部署保持 `SYNC_ENV=0`、existing 数据合同,无 migration/schema/seed/flag/routing/资料补录。
|
||||
- [ ] 部署后核版本、公网文案、合成 readiness/错误投影、B2、maker-checker 和真人单据状态。
|
||||
- [ ] 不自动创建 0014 批次、不复核、不登记付款;真实业务由授权人员完成。
|
||||
- [x] 回滚无需数据库补偿;新客户端遇旧服务端未知码仍须显示可行动兜底。
|
||||
|
||||
## 7. 开工基线与 20 处旧断言处置
|
||||
|
||||
2026-09-05 恢复实施;开工 HEAD 为 `ab82fe3`,仅当前 task 目录未跟踪并已保留。
|
||||
依据交接中的明确批准幂等执行 `task.py start`;本会话主会话实施/检查,没有派 implement/check agent。
|
||||
原生 `update_plan` 未在工具集中提供,未创建替代 plan;继续以原已批准清单执行。
|
||||
|
||||
下表行号是开工前定位,不以当前字符串总数作为完成证据。兼容响应的原始 message
|
||||
仍可以出现在测试 fixture;用户展示断言必须具体,不能用宽泛正则掩盖。
|
||||
延期域均由 `shunli` 负责,遗留 AST 基线到期日为 **2026-10-05(UTC)**;未增添主链路例外。
|
||||
|
||||
| 编号 | 开工前测试定位 | 逐项处置 |
|
||||
| --- | --- | --- |
|
||||
| T01 | `client/tests/e2e/r103-cross-page-ui-consistency.spec.ts:544` | 采购附件 mock 增加 `purchase_attachment.unsupported_type`;对应服务端分支、固定中文断言和安全上传浏览器断言一并保留。 |
|
||||
| T02 | `client/tests/e2e/r111-invoice-reimbursement-guard.spec.ts:448` | 提交 mock 改为 `reimbursement.items_invoice_item_guard_failed`;保留四类精确数量解释、异常字段安全兜底与双端就近提示断言。 |
|
||||
| T03 | `client/tests/e2e/r111-invoice-reimbursement-guard.spec.ts:481` | 明细关联 mock 改为 `reimbursement.invoice_invalid_unlinked_item_state`;保留准确中文和原子关联断言。 |
|
||||
| T04 | `client/tests/e2e/pc-finance-admin-visual-wave4.spec.ts:83` | 核验 mock 分别登记频控/次数上限原因码;服务端注册、客户端映射、固定内容和 route details 精确断言齐全;旧 `VALID_001:` UI 前缀断言改为无技术前缀的准确中文,不用正则。 |
|
||||
| T05 | `server/tests/unit/utils/workflow.test.js:64` | workflow 配置工具延期;保留原准确消息断言,对应 `utils/workflow.js` 到期指纹。 |
|
||||
| T06 | `server/tests/unit/utils/workflow.test.js:65` | workflow 空步骤配置延期;保留原断言,对应同文件独立调用指纹。 |
|
||||
| T07 | `server/tests/unit/utils/workflow.test.js:87` | approval-rule 配置工具延期;保留原断言及到期指纹。 |
|
||||
| T08 | `server/tests/unit/utils/workflow.test.js:94` | approval-rule 无效配置分支延期;保留原断言及到期指纹。 |
|
||||
| T09 | `server/tests/unit/utils/workflow.test.js:112` | approval-rule 结构配置分支延期;保留原断言及到期指纹。 |
|
||||
| T10 | `server/tests/unit/utils/workflow.test.js:331` | 缺少 assignee rule 配置延期;保留原断言及到期指纹。 |
|
||||
| T11 | `server/tests/unit/utils/workflow.test.js:332` | 未知 assignee rule 类型延期;保留原断言及到期指纹。 |
|
||||
| T12 | `server/tests/unit/services/leave.test.js:50` | 改为精确 `VALID_001`、422、`leave.leave_invalid_range` 与兼容 `leave: invalid_range`。 |
|
||||
| T13 | `server/tests/unit/services/leave.test.js:98` | 改为精确 `VALID_001`、422、`leave.leave_non_working_range` 与兼容 `leave: non_working_range`。 |
|
||||
| T14 | `server/tests/unit/routes/integrations.erp.procurement.test.js:49` | ERP procurement 非首批域延期;保留原消息断言与 integrations 模块到期指纹。 |
|
||||
| T15 | `server/tests/unit/routes/integrations.erp.procurement.test.js:57` | ERP procurement 另一资格分支延期;保留原断言,不扩展集成能力。 |
|
||||
| T16 | `server/tests/unit/routes/paymentBatches.reimbursement.test.js:2341` | 内部持久化 `failure_code/failure_message` 及 API 字段兼容断言保留;执行风险既有 JSON 仅附加注册原因码、刷新安全说明,不改风险状态;明细/风险卡经专用 helper 展示,历史与未知原因安全兜底,不直接展示原摘要。 |
|
||||
| T17 | `server/tests/unit/routes/workflow.batch.test.js:377` | 内部批处理 reason 兼容断言原样保留;不把批处理摘要冒充用户恢复文案。 |
|
||||
| T18 | `server/tests/unit/routes/workflow.permissions.test.js:172` | workflow 管理查询域延期;保留准确消息与 `routes/workflow.js` 到期指纹。 |
|
||||
| T19 | `server/tests/unit/budgetPool.service.test.js:28` | budget scope 配置域延期;保留原断言与 `services/budgetPool.js` 到期指纹。 |
|
||||
| T20 | `server/tests/unit/budgetPool.service.test.js:75` | budget pool 候选配置域延期;保留原断言及到期指纹,不变更预算规则。 |
|
||||
|
||||
## 8. 实施与只读复核记录
|
||||
|
||||
- 原因码合同接入既有 `run-release-gate.sh` static 阶段;主链路包含 invoice bind/unbind
|
||||
与共享 item resolver,不借整目录永久豁免绕过。
|
||||
- 候选、组合预检和创建事务复用 `loadPaymentCreationFacts` 与
|
||||
`resolvePaymentBatchCreationReadiness`;候选关系、占用、公司账户、复核人批量读取。
|
||||
部门 gate 仅在单次事实加载内复用底层 flag 查询,仍调用原判定实现;没有修改 flag。
|
||||
- 只读 reviewer 首轮发现两项 P2:未选中时无法切换付款渠道、失效已选项无法取消。
|
||||
已分别移出渠道选择器的选中条件、允许失效已选项取消,并补浏览器回归。
|
||||
第二轮只读 review 确认这两项闭环及请求内 gate 查询复用;无新增高置信问题。
|
||||
- 系统 Python 缺 `xlwt` 导致既有银行模板导出测试失败;使用现有隔离测试环境提供的
|
||||
Python、通过 `PAYMENT_ORDER_PYTHON_BIN` 运行后恢复通过。未改依赖清单或生产配置。
|
||||
- 首轮扩大浏览器运行另包含一个合同只读用例超时,未完成该项归因,未改合同产品逻辑或该断言;
|
||||
付款、B2、采购与本轮核验反馈改为明确 focused 集重跑,不把无关失败标成通过。
|
||||
- 已更新现有 backend/frontend spec;根因归类为跨层合同和传播缺失,而非真实资料补录。
|
||||
- 本轮不提交、不 push、不部署、不归档;真实资料和 0014 的任何业务写入均未执行。
|
||||
部署后 AC12 仍须独立批准与公网证据,不能用本地合成结果替代。
|
||||
- 最终检查进一步发现两处付款明细直出旧失败摘要,已按 T16 补齐安全展示转换;
|
||||
新原因只保存在既有执行风险 JSON 元数据中,未新增 schema、readiness 持久字段或日志机制。
|
||||
追加只读 review 确认历史兼容、事务和风险状态边界,无新增高置信问题。
|
||||
|
||||
## 9. 本地验证结果(2026-09-05)
|
||||
|
||||
| 验证 | 结果与边界 |
|
||||
| --- | --- |
|
||||
| AST strict | PASS;142 注册原因、199 已迁移使用点、695 遗留调用、主链路例外 0、finding 0。遗留身份同时包含相对文件、operation 和规范化 AST 形状,不能靠移动操作或一增一删抵消。 |
|
||||
| AST self-test | 20 项 PASS;正常跨域与纯格式化退出 0,其余拒绝场景退出 1,包括六类必查失败、主链路加例外、同数替换、别名、具体内容断言缺失、operation 移动、条件式通用码和不安全文案。 |
|
||||
| 扩展服务端 focused | 23 文件 / 407 用例 PASS:报销、票据、请假、考勤、付款、readiness、registry/helper 和客户端投影。 |
|
||||
| 后续兼容/新增边界 focused | 8 文件 / 309 用例 PASS:包含当轮 75 个付款 route 用例、149 个具体内容/安全投影用例、采购附件和延期域/内部摘要兼容测试;与上一行有交集,不合并虚报数量。 |
|
||||
| 最终摘要投影 focused | 4 文件 / 244 用例 PASS:76 付款 route、150 内容/投影、16 readiness、2 helper;新增既有风险元数据更新与历史摘要保护,和前述测试有交集。 |
|
||||
| Playwright focused | 18 个不同用例 PASS(原 17 项 + 最后追加的失败摘要保护 1 项):银行正常提交、现金渠道、三类缺项、非建行、跨公司、占用、无 reviewer、创建并发、移动未知原因排障编号、键盘/ARIA、渠道可达与失效项取消、B2 双端、采购附件、核验错误、PC/移动视口历史失败明细及风险保护。仅本地全合成 API mock。 |
|
||||
| 客户端工程 | `npm run typecheck`、`npm run build` PASS;未部署构建产物。 |
|
||||
| 服务端工程 | Prisma validate、触达 JS/MJS `node --check` PASS;没有执行 migration 或数据库重置。 |
|
||||
| 治理 | UI strict/self-test、process baseline、docs registry、Trellis validate/governance、敏感扫描、`git diff --check` PASS。 |
|
||||
| 只读 review | 首轮两项 P2 已修复;第二轮确认闭环;追加摘要投影与风险元数据复核无新增高置信问题。Reviewer 未写文件、未操作真实数据。 |
|
||||
| 边界核对 | schema/migration/seed/flag 实现、前端 routing、真实资料与真实付款写入均为 0;0014 未操作。 |
|
||||
|
||||
复跑入口:`node scripts/qa/check-error-reason-contract.mjs --strict` 与 `--self-test`;
|
||||
服务端测试中的银行模板导出需把 `PAYMENT_ORDER_PYTHON_BIN` 指向满足现有锁定依赖的隔离测试 Python。
|
||||
浏览器 focused 文件为 `payment-reviewer-permissions.spec.ts`、`r111-invoice-reimbursement-guard.spec.ts`、
|
||||
`r103-cross-page-ui-consistency.spec.ts` 和 `pc-finance-admin-visual-wave4.spec.ts`,筛选本表相关用例;
|
||||
不得对公网或真实账户套用这些 mock 场景。
|
||||
|
||||
## 10. 独立复核、遗留验证事项与发布授权(2026-09-05)
|
||||
|
||||
- 用户独立复核 PASS:真实调用点注入未注册原因码时 strict 退出 1;向主链路新增例外时 strict 退出 1;确认 readiness 三处共用、20 项 self-test 及格式化指纹稳定。用户确认探针全部还原,基线校验与备份一致。
|
||||
- 两个 task 已按完整文件物理分离;本目录仅保留错误可解释性与付款 readiness,Trellis 升级在独立 worktree 待单独审查。原治理文件均与开工 HEAD 一致,分离后原因码及治理门禁通过。
|
||||
- 未解决验证事项:首轮扩大浏览器运行的合同只读用例超时,**未归因、不计入通过结果**,不是已确认的产品缺陷或可永久忽略的 flaky。后续合同域验证前必须单独复现,补充精确用例、失败步骤及环境证据后归类;本任务不得通过放宽断言、重试至绿或混入 focused PASS 关闭它。
|
||||
- 用户明确批准本任务提交、push、部署;部署后核验 `/ready` 与 bundle marker、文案扫描零 findings、0014 候选禁用及缺项原因、创建接口 fail-closed、B2 保持 force_on + global。
|
||||
- 生产核验只使用最小脱敏结果,不读取或修改真实银行卡资料;不补录 0014,不执行真实付款。若前置条件变化可能导致创建成功,停止强行创建探针。
|
||||
- Task 保持 `in_progress`;独立复核通过不等于公网 AC12 完成。发布和公网证据待实际执行后回填,不提前标记 PASS,不归档 Trellis 升级任务。
|
||||
@@ -0,0 +1,94 @@
|
||||
# 错误可解释性与付款就绪治理
|
||||
|
||||
## Goal
|
||||
|
||||
把“系统知道为什么不能继续、真人只看到参数校验失败”的缺陷从逐页补文案改成可持续的错误合同,并以付款批次为第一个完整样板:财务在点击前就知道付款资料是否齐全、缺什么、由谁补;点击后的服务端二次校验与页面提示必须来自同一份实时判断。
|
||||
|
||||
## Background
|
||||
|
||||
- 2026-09-05,真实单据 `0014` 在生产付款页已能进入待处理队列,但财务选择银行转账并点击“创建并提交复核”后只看到“参数校验失败”。只读诊断确认请求未创建付款批次;报销人缺所属公司、开户银行和银行卡号,公司侧已有有效建行付款账户,经理/CEO 复核人存在。
|
||||
- 服务端创建前已检查来源状态、公司归属、收款账户、公司付款账户、活动批次占用和复核人;客户端未覆盖付款原因,因此回退到通用技术文案。
|
||||
- 这是十一天内第三次“参数校验失败”阻断真人主流程。此前 B2 已验证“一份判断同时驱动展示、点击拦截和 mutation 二次拦截”能防漂移;本任务把该模式推广到付款就绪。
|
||||
- 当前文本基线:服务端同一通用校验文案 365 处,`buildValidationError(` 518 次,测试目录中该字符串 20 次。数量用于确定渐进迁移边界,不代表独立业务原因数量。
|
||||
- `details key` 不能作为原因合同:同一个 `legalEntity`、`status` 或 `review` 在不同业务域含义不同。稳定合同必须是带业务域的原因码,例如 `payment_batch.legal_entity_required`。
|
||||
- 生产资料补录和真实付款继续由授权人员操作;本任务不得读取、记录或修改真实银行卡号,不把伪填建行资料作为恢复办法。
|
||||
|
||||
## Scope
|
||||
|
||||
本 task 包含三项互相支撑但可独立回滚的交付:稳定业务原因码及增量 parity 门禁、首批真人主链路错误迁移、付款批次实时 readiness 与就近反馈。它不建设统一待办,也不扩展银行付款能力。
|
||||
|
||||
## Requirements
|
||||
|
||||
### R1 稳定原因码与渐进 parity 门禁
|
||||
|
||||
1. 新增服务端业务错误原因注册表。每个原因至少记录稳定 `reasonCode`、业务域、普通用户是否可达、默认安全中文提示/恢复方向;原因码采用 `<domain>.<reason>`,不得复用任意 `details key` 冒充合同。
|
||||
2. 原有 HTTP status、顶层 `error.code` 和兼容 `details` 保持;已迁移错误在 `details.reasonCode` 增加稳定原因码。旧客户端可继续依赖既有字段,新客户端优先消费原因码。
|
||||
3. 新增 parity 检查:已注册用户原因必须有客户端安全映射;服务端使用的原因必须已注册;新增或修改通用校验不得绕过原因码;客户端不得有孤儿/拼写漂移;未知原因走安全兜底且不展示原始 details、内部路径或 UUID。
|
||||
4. 存量通用错误使用版本化遗留基线渐进收敛。例外按业务域/文件组记录原因、负责人、到期日和行号无关的调用指纹;新增指纹默认失败。不得以整目录永久忽略、只比较数量或模糊正则代替指纹门禁。
|
||||
5. 首批真人主链路不得进入例外:报销提交/编辑/票据关联,付款创建/复核/登记,请假提交和考勤修正提交。范围内每个用户可达校验分支必须有注册原因码、准确中文提示和测试。
|
||||
6. 遗留例外是迁移债,不是管理页永久展示技术文案的许可。到期后门禁必须失败;续期需要新理由和明确期限。
|
||||
|
||||
### R2 用户可行动的错误反馈
|
||||
|
||||
1. `VALID_001` 未映射时不得再把服务端的“参数校验失败”直接展示给普通用户。兜底必须说明操作未完成、建议核对页面提示或联系管理员,并附现有 request ID;没有 request ID 时不展示空编号。
|
||||
2. 具体提示必须回答“哪里不满足、下一步怎么恢复”,并位于相关字段、条目或动作附近。页面顶部错误和 toast 只作补充。
|
||||
3. 错误不得只靠红色表达;动态错误使用可访问告警语义。禁用动作的原因必须在按钮附近持续可见,并通过 `aria-describedby` 或等价语义关联。
|
||||
4. 20 处现有测试字符串逐处处置,不用宽泛正则消除失败:客户端 4 处模拟响应改为原因码 fixture;首批主链路改断言 reasonCode、兼容 details 和 HTTP status;非本批路径明确对应遗留例外和到期日;持久化失败摘要/批处理 reason 单独判断兼容性。
|
||||
|
||||
### R3 付款批次实时 readiness
|
||||
|
||||
1. 抽取唯一服务端付款就绪解析器,返回 `{ ready, blockers }`。blocker 使用 `payment_batch.*` 原因码和最小安全定位信息,不返回完整银行卡、密文或原始员工对象。
|
||||
2. readiness 至少覆盖:来源状态/已付款、付款能力部门门禁、活动批次占用、单一公司归属、收款人银行资料、公司付款账户及解密可用性、非制单人的合格经理/CEO复核人。
|
||||
3. 候选列表对单条候选实时计算 readiness;缺资料的记录仍可被发现,但不可选择,并就近显示缺项与恢复责任。财务无员工维护权限时只提示联系管理员,不暴露无权入口。
|
||||
4. 多选付款必须用同一解析器计算组合 readiness,覆盖跨公司、并发占用和复核人等组合条件;不能因为每条单独就绪就推断组合一定就绪。
|
||||
5. 创建接口在事务、必要锁和最新数据下再次调用同一解析器并 fail closed。候选、组合预检和创建不得各写一套判断;并发变化返回同一原因码。
|
||||
6. readiness 每次按当前事实派生,不新增数据库字段、不落库、不长期缓存。资料或账户变化后,下次查询自然反映最新结果。
|
||||
7. 银行转账只按真实资料判断。开户行不是建行时不得建议伪填;是否支持跨行付款属于独立产品能力。
|
||||
|
||||
### R4 付款页面反馈
|
||||
|
||||
1. 正常候选不增加解释性噪音;有 blocker 时在候选行展示精确缺项,复选框不可用且原因可被键盘和读屏取得。
|
||||
2. 已选组合 readiness 位于主动作紧邻上方;阻断时主动作禁用,提示与服务端二次拦截来自同一组 reasonCode 投影。
|
||||
3. 正常银行付款 fixture 保持可选择、可创建并提交复核;新增门禁不得误伤资料与复核人完整的单据。
|
||||
4. PC 是批量付款主路径;移动端不新增复杂组批能力,但已有移动付款路径不得显示原始技术错误或回归。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] AC1:存在可机器读取的原因码注册表;同一 details key 在两个业务域使用不同 reasonCode,parity 分别验证,证明不是按 key 虚假覆盖。
|
||||
- [x] AC2:未注册原因、缺客户端映射、拼错 reasonCode、新增无原因码调用、遗留指纹变化、例外过期六类 self-test 均非零退出;正常注册与有效遗留通过。
|
||||
- [x] AC3:报销提交/编辑/票据、付款创建/复核/登记、请假提交、考勤修正提交的用户可达错误为零例外;报告按域输出注册、已迁移、遗留和过期数量。
|
||||
- [x] AC4:未知 `VALID_001` 在 PC/移动显示安全可行动兜底;有 request ID 时展示编号,无编号时无空占位;“参数校验失败”、原始 details、UUID、接口路径不出现在 UI。
|
||||
- [x] AC5:20 处既有测试字符串有逐项处置清单;主链路断言具体原因码/中文,非本批项对应有效例外,无宽泛正则掩盖差异。
|
||||
- [x] AC6:付款单条与组合 readiness 覆盖正常、状态异常、活动批次、缺公司、缺/非建行收款资料、缺/不可解密公司账户、跨公司、无复核人;准确返回原因码,正常 fixture `ready=true`。
|
||||
- [x] AC7:候选、组合预检和创建事务调用同一 readiness 实现;并发变化时创建再次阻断且不生成 batch、item、成功 audit 或通知。
|
||||
- [x] AC8:正常候选无 readiness 噪音;异常候选不可选择且缺项就近可读;组合原因紧贴禁用主动作,toast 非唯一反馈,键盘/读屏语义通过。
|
||||
- [x] AC9:员工资料或公司账户变化后下一次 readiness 立即变化;schema、migration 和持久化 readiness 字段为零。
|
||||
- [x] AC10:用受控 fixture 复现 0014 三类缺项,分别提示所属公司、开户银行/银行卡和渠道限制;不使用或修改真实银行卡资料。
|
||||
- [x] AC11:相关服务端、客户端单元/Playwright、typecheck/build、Prisma validate、UI strict/self-test、Trellis governance、敏感扫描和 `git diff --check` 全部通过。
|
||||
- [ ] AC12(部署后):`/ready`、bundle marker、公网文案扫描通过;合成 API/页面证明 readiness 与原因码解析生效。真实资料补录和 0014 重试由授权人员完成,未取得真实证据时不得冒充 PASS。
|
||||
|
||||
## Test Plan
|
||||
|
||||
- 原因码合同:运行 parity strict 检查及 self-test,覆盖注册表唯一性、同一 details key 跨域、未注册原因、缺客户端映射、孤儿/拼写漂移、新增无原因码调用、遗留指纹变化、例外过期,以及首批主链路试图加入例外时 fail closed。
|
||||
- 首批主链路:对报销提交/编辑/票据关联、付款创建/复核/登记、请假提交、考勤修正提交运行 focused 服务端测试;逐分支断言 HTTP status、稳定 `reasonCode`、兼容 details 和准确中文映射,并验证主链路遗留例外为零。
|
||||
- 付款 readiness:用受控 fixture 覆盖正常、状态异常、活动批次占用、缺所属公司、缺开户银行/银行卡、非建行、缺或不可解密公司账户、跨公司、无合格复核人;分别验证单条和组合投影,并以正常 fixture 证明可选择、可创建和提交复核不被误伤。
|
||||
- 事务与实时性:模拟候选读取后事实变化,确认创建事务重新校验、失败时不产生 batch/item/成功 audit/notification;修改受控员工资料或公司账户后重新查询,确认 readiness 即时变化且无持久化字段或缓存依赖。
|
||||
- 客户端与可访问性:逐项处置现有 20 处“参数校验失败”测试命中;断言具体原因就近显示、禁用按钮关联 `aria-describedby` 或等价语义、toast 非唯一反馈、未知原因不泄露 details/UUID/路径,有 request ID 时显示排障编号、无编号时不留空占位;不得以宽泛正则替代精确断言。
|
||||
- 工程门禁:运行相关 server/client 单元测试与 Playwright、`npm run prisma:validate`、前端 typecheck/build、UI strict/self-test、Trellis governance、敏感信息扫描和 `git diff --check`,并确认 schema/migration/seed/flag/routing 与生产数据修改均为零。
|
||||
- 部署后验收:在另行批准部署后核对 `/ready`、bundle marker 和公网文案扫描;仅用合成 API/页面样本验证三类 0014 缺项、正常付款路径及 B2/maker-checker 回归。真实银行卡资料、0014 建批/复核/登记必须由授权人员操作;缺少真实链路证据的项目明确记为 BLOCKED 或待自然样本验证,不以合成测试冒充 PASS。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 统一首页待办、通知推导和角色行动项模型;等待真实付款链路记录后另行规划。
|
||||
- 一次性迁移全部 365 处通用文案或 518 个调用文本;非首批路径只建立可到期遗留基线。
|
||||
- 支持跨行付款、银行直连、自动付款,或改变 maker-checker、付款角色、复核规则。
|
||||
- 自动补录或读取生产员工公司、开户银行、银行卡号和公司账户。
|
||||
- 修改业务状态机,新增 schema/migration/seed/flag/routing。
|
||||
- 把完整 details、payload、UUID、账号或内部路径写入日志、任务材料或普通页面。
|
||||
|
||||
## Risks and Deferred Items
|
||||
|
||||
- 静态扫描只证明合同登记;focused 分支测试证明真实返回,二者缺一不可。
|
||||
- 单条与组合 readiness 语义不同;只做前者仍留下跨公司等点击后失败路径,因此组合预检是必交项。
|
||||
- request ID 可帮助定位,但当前 AppError details 不进服务端日志。是否增加脱敏 reasonCode 日志另行评估,本任务不记录原始 details。
|
||||
- 0014 只证明资料未维护,不证明员工没有建行卡;真实开户行由本人和授权管理员确认。
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "error-explainability-payment-readiness",
|
||||
"name": "error-explainability-payment-readiness",
|
||||
"title": "错误可解释性与付款就绪治理",
|
||||
"description": "建立稳定业务错误原因码与渐进 parity 门禁,并以付款批次实时 readiness 消除点击后才暴露的资料阻断。",
|
||||
"status": "in_progress",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P0",
|
||||
"creator": "shunli",
|
||||
"assignee": "shunli",
|
||||
"createdAt": "2026-09-05",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [
|
||||
"server/src/routes/paymentBatches.js",
|
||||
"server/src/utils/AppError.js",
|
||||
"client/src/api/errorMessages.ts",
|
||||
"client/src/lib/resolveErrorMessage.ts",
|
||||
"client/src/pages/PaymentBatchesPage.tsx",
|
||||
"scripts/qa"
|
||||
],
|
||||
"notes": "来源:真实单据 0014 创建付款批次时第三次出现‘参数校验失败’;生产只读诊断确认未创建批次,员工付款资料缺失且错误未被客户端解释。2026-09-05 最终规划已获用户批准实施;交接复核重点:门禁六类 self-test 真失败、主链路零例外且禁止新增例外、付款候选与创建共享同一 readiness resolver、20 处旧断言逐项处置且不得使用宽泛正则。0014 真实资料补录与付款不属于本 task 自动操作范围。",
|
||||
"meta": {}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户