12 KiB
12 KiB
非参与人报销详情只读投影修复
Goal
把报销详情按对象参与资格收敛为两种明确模式:owner/submitter 在允许编辑的状态维护自己的报销;财务、审批人、管理员和其他因职责或 expense.view_all 获得访问权的非参与人只查看、核验材料和执行职责内的独立业务动作,不得复用报销人的维护入口或直接变更他人报销数据。
Background
ReimbursementDetailPage.tsx:725和MobileReimbursementDetailPage.tsx:206只按draft/rejected判断可编辑状态。PC 的新增/编辑/删除明细、项目关联、附件上传/删除和主要提交动作,以及移动端对应动作,没有全部叠加 owner/submitter 资格。- PC 与移动端的
canUnbindInvoices都把expense.view_all当成他人报销的解绑资格;invoicePool.js:1082的服务端解绑合同也允许该权限直接解除他人报销与发票的关联,因此不是单纯的前端误显。 ensureApplicantOrAdmin允许管理员修改、删除、提交他人草稿,新增/修改/删除他人明细和附件;link-project与发票 bind 也把全量查看权限当成写入资格。全量查看、系统管理或当前审批职责不等于报销人自助维护资格。- 发票卡片在所有视角共用
resolveInvoiceNextStepText,向非参与人显示“草稿或驳回状态可移除”;linkedItemId为空时又显示技术性兜底“自动生成或匹配明细”。这些文案不是只读核验事实。 - 现有事实源要求页面按当前任务角色收敛,审批人和财务只看到职责内动作;报销材料仍允许授权查看原件和合并预览。Task A 已修复审批中撤回入口,但没有覆盖更广的草稿/驳回维护动作和服务端对象资格。
Defect Severity
- S1 真实写入能力:reimbursement 发票 unbind/bind 把
expense.view_all当成对象写资格;ensureApplicantOrAdmin允许 admin 修改、删除、提交他人报销头、明细和附件。link-project的授权设计同样错误(仅认证、复用读守卫且没有状态门),但原实现还调用了未定义的parseBody,因此 feature flag 打开后会在写入前 500,历史上该写入洞不可实际利用;本轮仍须同时修复可用性和授权边界。测试必须断言 403/409 及零副作用,不能只检查按钮不可见。 - S2 仅显示或前后端不一致:PC 多处明细/附件动作和移动端对应动作只按 editable status 渲染,普通财务可能看到按钮但被部分服务端路由拒绝。测试必须断言 PC/移动 DOM 中不存在无资格动作,并保留直接 API 负向回归。
- S3 已有正确边界:PC/移动报销头编辑器已经同时检查 editable status 和参与人资格;实施只统一资格事实并证明基线,不重复改写已经正确的组件合同。
Scope
- 统一 PC、移动端的报销参与人和可维护资格计算,修正所有报销人自助维护动作的投影。
- 收紧报销与发票路由中把 admin 或
expense.view_all当成他人报销维护资格的服务端分支。 - 为报销 precheck 补对象级读取授权,避免普通员工横向读取他人预算、费用标准和风险结果。
- 为非参与人提供明确的只读查看语义和事实型票据文案,同时保留授权材料查看、打印、审批和财务专属流程。
- 使用合成身份和数据覆盖 owner/submitter、财务、管理员及普通非参与查看者,不读取或修改真实报销。
Requirements
R1 服务端对象资格必须 fail closed
- 报销 owner 或 submitter 统称参与人。只有参与人可通过报销人自助接口修改报销头、删除草稿、上传/删除附件、新增/修改/删除明细、提交报销、关联项目,以及为该报销绑定或解绑发票;状态和对应权限码仍需同时满足。
POST /reimbursements/:id/link-project必须补expense.edit_own权限门,只允许参与人在draft/rejected状态写入;非参与人返回 403,已通过、已支付等非编辑状态返回 409。不得继续使用ensureOwnOrCanViewAll读守卫授权写入。- admin、财务、审批人及任何仅持
expense.view_all的非参与人调用上述接口时必须稳定返回403 PERM_001,并在读取目标对象后、任何业务写入、文件持久化、审计或通知副作用前失败。 expense.view_all继续允许授权查看报销详情、发票/附件原件和材料合并预览;不得把读权限降级为无权查看。POST /reimbursements/:id/precheck虽不写业务数据,但会返回预算、费用标准、审批规则和风险结果,必须先读取目标报销并通过 owner/submitter 或expense.view_all对象级读守卫;普通非参与人返回 403,且不得调用 precheck 服务。审批页既有standardViolations快照继续保留。- 显式职责动作保持独立:workflow 审批/退回、财务付款及付款批次接口不改;差旅订单 bind/unbind 分支不改。若未来需要财务纠正票据归属,必须在 Task B 设计专用、可审计的业务动作,不能继续借用报销人解绑接口。
- 不改变状态机、权限码集合、角色定义、数据库 schema 或迁移。
R2 PC 与移动端统一维护资格
- 页面使用“参与人 AND 状态允许编辑”计算统一的
canMaintainReimbursement,不得再把draft/rejected或expense.view_all单独当成维护资格。 - 非参与人不得看到或触发:新增/编辑/删除明细、修改报销日期/事由/差旅摘要、关联项目下拉框、添加/移除发票、上传/删除附件、提交报销、删除草稿、撤回、催办等报销人自助动作。
- owner/submitter 在 draft/rejected 状态继续看到并可使用既有维护动作;owner/submitter 在 reviewing 状态继续使用 Task A 已修复的撤回和催办合同,其他状态行为不变。
- 只读查看者可继续查看报销概要、明细事实、处理进度、材料原件/合并预览、风险与材料对应情况,以及权限允许的打印入口;页面不通过禁用按钮暗示不可用动作,而是完全不渲染无资格动作。
- PC 和移动端必须使用同一资格语义,不能只修截图对应的 PC 页面。
R3 非参与人使用事实型查看文案
- 同一详情路由可以复用,但非参与人应显示明确的“只读查看”上下文,不伪装成报销人维护页,也不新建一套重复详情页面。
- 非参与人的“下一步”按当前业务状态表达观察事实:草稿为“等待报销人完善”,驳回为“等待报销人修改后重提”,审批中显示当前处理事实,已通过为“等待财务处理”,已支付为“查看付款结果”。不得提示其新增明细、添加发票或提交审批。
- 发票存在真实
linkedItemId时显示关联明细:<费用类型 · 明细说明>;为空时显示关联明细:未关联,关联对象无法解析时显示关联明细:无法识别。不得再显示“自动生成或匹配明细”。 - bound 发票在当前不可维护视角(包括非参与人,以及 owner/submitter 查看 approved/paid 等非编辑状态)使用事实状态“已关联”,且不显示“草稿或驳回状态可移除”等操作提示。只有参与人的可维护视角可保留必要的移除说明。
- 材料对应、票面金额、日期和风险提示继续作为财务/审批核验信息展示;本轮不重构为 Task B 的一页式审批工作台。
R4 回归与可部署基线
- 前端回归至少覆盖 PC 和移动端的非参与管理员/财务查看他人 draft、rejected、reviewing 报销,断言全部维护动作不存在、只读文案正确、原件和合并预览入口仍可用。
- owner/submitter 合成场景继续覆盖 draft/rejected 维护动作和 reviewing 撤回,防止把页面整体改成只读。
- 服务端对每类维护接口至少覆盖一个非参与高权限身份的
403和零副作用,并保留参与人成功基线;发票 bind/unbind 同时保留差旅分支现有合同。 - 权限与审批规则改动实施后必须完成独立只读复核;未经复核和新工作提交,不更新部署候选、不推送、不部署。
Acceptance Criteria
- 非参与管理员或财务查看他人 draft/rejected 报销时,PC 和移动端不存在明细、项目、票据、附件、提交、删除、撤回和催办等报销人维护动作。
- 同一非参与人仍能查看报销概要、明细事实、处理进度、材料原件和合并预览;reviewing 审批/财务职责入口仍在原工作流页面正常使用。
- 非参与人看到“只读查看”上下文;未关联票据显示“关联明细:未关联”,bound 状态显示“已关联”,页面不存在“自动生成或匹配明细”和“草稿或驳回状态可移除”。
- 非参与人直接调用报销头修改/删除、附件增删、明细增改删、提交、项目关联、发票 bind/unbind 均返回
403 PERM_001,且对应数据、文件、审计、workflow 和通知零副作用。 - 普通非参与人调用 precheck 返回
403 PERM_001且 precheck 服务零调用;owner/submitter 与expense.view_all查看者保持成功读取基线。 - owner/submitter 在 approved/paid 等非编辑状态调用
link-project返回 409 且项目归属不变;draft/rejected 且具备expense.edit_own时保持成功基线。 - owner/submitter 在既有允许状态仍可维护、提交、撤回和催办自己的报销;现有状态校验、材料查看、打印、财务付款、workflow 审批和差旅票据分支不变。
- 无 schema、迁移、权限码或角色定义变更;前后端聚焦测试、前端 typecheck/build、相关 Playwright、Trellis 治理、敏感检查和
git diff --check通过。 - 只读权限复核结论为 PASS 后才形成新工作提交;部署子任务随后重新冻结包含 Task A 与本任务的候选 SHA,原
174cb7a不再作为最终部署基线。
Out of Scope
- Task B 的财务一页式审批、费用口径、
requires_invoice、票据—明细强制关联、金额差异门禁和多票拆分。 - 为财务或管理员新增“代员工修改”“纠正票据归属”或其他管理修复入口。
- 修改 workflow 审批/退回、付款批次、财务付款、材料查看授权、打印权限、发票 OCR 或材料合并算法。
- 处理
BX-20260828-0017的真实单据、票面差额、退回、付款或附件重传。 - 修复 r102 差旅夹具测试债;该项仍按独立任务处理。
- 推送、部署、公网验证、生产配置、生产数据写入和任务归档。
Risks and Constraints
- 服务端移除 admin/
expense.view_all写入豁免属于真实权限收紧,必须用直接接口测试证明读取能力仍在、写入统一拒绝。 - 不能只隐藏前端动作;否则旧客户端或直接 API 仍可变更他人报销。
- 不能把所有非参与人操作一刀切:workflow 审批、财务付款和授权材料查看是职责动作,必须与报销人维护动作区分。
- 移除 admin 通用写入豁免后,员工离职或长期缺席造成的卡住草稿暂时没有代维护入口;若真实发生,应在 Task B 的专用可审计纠错动作或独立运营修复任务中重新规划,本任务不以保留通用后门兜底。
- 同一页面角色化比复制新页面更低风险,但 PC/移动端必须共享资格事实和文案合同,避免再次漂移。
Test Plan
- 服务端语法与基础:
cd server && node --check src/routes/reimbursements.js && node --check src/routes/invoicePool.js && npm run prisma:validate。 - 服务端聚焦:新增或扩展报销参与人资格矩阵测试,覆盖 form/items/attachments/submit/project/invoice bind-unbind 的非参与高权限 403、零副作用和参与人成功基线。
- 前端:
cd client && npm run typecheck && npm run build。 - Playwright:扩展
r102-reimbursement-main-flow.spec.ts或新增聚焦 spec,使用阻断 Service Worker 的合成 PC/移动身份覆盖 participant 与 nonparticipant 的 draft/rejected/reviewing 投影及只读票据文案。 - 全局回归:运行相关报销、材料预览和系统动作确认用例;完整 r102 的既有差旅夹具失败必须如实单列,不得把它当成本任务新失败或默认绿色基线。
- 治理:
node scripts/qa/check-process-contract.mjs --baseline --json、node scripts/qa/check-trellis-governance.mjs、node scripts/qa/check-sensitive-content.mjs、git diff --check。