文件
hyb-oa/.trellis/tasks/09-04-reimbursement-state-gap-finance-view/design.md
T
wangsli 98eabdde7f
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s
fix: 统一报销付款详情布局与票据展示(09-04-reimbursement-state-gap-finance-view)
2026-09-11 22:28:06 +08:00

20 KiB
原始文件 Blame 文件历史

报销状态空隙与财务处理视角:设计

2026-09-11 等宽侧栏与票据表格

动作卡通过可选inlineActions布局主动作和更多,默认合同不变。两页更多复用现有Radix DropdownMenu,Portal渲染后测试按page.getByRole("menuitem")定位,而不是动作卡内找按钮。业务回调和确认对话框不变;报销调用方仅在有撤回或动作错误时传ReimbursementActions,避免React组件返回null但外层误判为有内容而生成空分隔线。

根因:共享detail-workspace-grid使用比例列,而报销壳另有max-w-7xl,付款壳无同一上限。新增局部共享detail-workspace-grid--payment类供两处使用,lg起固定320px侧栏,主列minmax(0,1fr),保持各自正文宽度和手机规则。frontend-design不可用,仅复用现有表格、边框和按钮样式,不创建新视觉体系。已关联票据采用响应式表格,窄屏按行紧凑排列;未关联票据保留原纠错控件与唯一锚点。姓名和号码允许换行,预览、移除及OCR告警按原capabilities保留。

2026-09-11 下一步与侧栏一致性方案(已批准本地实施)

本轮只读核对已发布候选,未查询生产。ReimbursementDetailPage 的 approved 分支无条件显示“等待财务组建付款批次”,其主动作只再检查 payment.batch.manage;同一页面 ProcessingProgress 已消费服务端按真实批次状态构造的阶段。修复必须连接这两处投影,不能只把错误文字换成另一句固定文案。

建议规则:

权威阶段 财务入口与说明
明确待组批 有组批资格时去组建付款批次;其他人等待财务处理
批次待复核 有读取权限时进入既有批次;说明等待付款复核,不允许制单人自审
已复核未付款 进入既有批次处理;说明消费真实制单/回单阶段,不在报销页复制下载/付款流程
已付款 查看付款结果,不再提示组批或继续付款
失败、取消、未知 按真实状态与既有资格解释;不能自动当成正常待组批或臆定可以重建

沿现有结构优先使用 ReimbursementDesktopWorkspaceShell 的 asideContext 放进度,不在 afterEvidence 再渲染一次;同时检查其审批工作台调用方。复用 BusinessDetailAside/ProcessingProgress,并处理操作卡单独 sticky 可能遮挡下方进度的问题。付款页保留现有正确结构,跨页使用同一布局约束而不是增加第二套付款状态机。

“统一”指信息层级、位置、动作与状态一致,不是让所有业务使用同一条审批链。当前处理进度与完整历史区分:前者固定在 PC 操作下方,历史内容不丢失、不伪装成当前待办。移动端沿既有壳与安全区动作,不扩复杂付款能力。

09-11 追加按钮优化:BusinessDetailActionPanel 的 grid 默认拉伸其子按钮,局部对报销付款主入口使用既有 justify-self-start 工具类,保留默认 h-11/px-4。创建文案缩为“创建批次”,查看入口保留现有明确文案;不改变通用操作卡、其他业务按钮或移动全宽触控规则。覆盖 1280/1440 桌面实际尺寸、创建入口目标/无写入、已组批隐藏与手机既有动作。frontend-design 不可用,本轮只维护已有按钮和 Tailwind 样式,不新增视觉体系。

扩展原付款页面回归发现 PaymentBatchesPage 的桌面操作卡仅设 lg:static,窄屏也可见,而同页另有 lg:hidden 的手机底栏,形成重复操作区。该页原布局验收要求仅一套可见操作,因此局部恢复 hidden lg:block lg:static,不隐藏它下面的进度。复用原“移动端固定操作区”测试验证上传前后动作切换,不删失败断言或改成允许重复;桌面入口与专用手机路由继续回归。

docs/产品设计规则.md 目前仍允许普通/长流程历史在主列,短流程在侧栏;待确认后应明确当前进度与完整历史的区别,统一已有页面和稳定规约。frontend-design 当前不可用,本阶段只复用已存在的付款侧栏样式;若实际需要新视觉方案,另行说明缺项,不擅自换设计体系。

修复后重点不是单页截图,而是“批次创建/复核/制单之后返回报销详情”的跨页面状态一致性,覆盖不同角色、无批次与无权读批次的区别、缓存重新聚焦、取消/失败及接口缺项。不承诺只改前端:若既有 DTO 不能可靠表达动作资格,先核实并在本阶段补最小合同,不改付款写规则、角色或 schema。

发布方案补充:候选 fe025e65 的回退语义

外部只读复核通过(部分覆盖),F2 非阻断但必须进入发布审批与交付说明。本节是候选之外的运维补充,不代表已演练回退。用户后续明确批准发布,实际授权、执行与自动核验结果以 implement.md 顶部最新回执为准;部署完成不消除本节回退限制。

  • 生产当前版本、待执行迁移、磁盘余量、备份时点及完整性、独立旧镜像制品均需发布前重新读取;历史 86% / 8.93 GB 不是当前证据。
  • 本次包含 20260910010000_reimbursement_retained_delete 迁移。部署须明确 existing 模式,不执行初始化 seed;迁移在必需配置检查之前,检查失败不能宣称数据库零写入。
  • 应用问题的首选恢复方向为兼容现有数据的前向修复;如需退镜像,先核实新版本是否已产生留痕删除记录及其他新写入,禁止仅按镜像版本判断可安全回退。
  • 已发生留痕删除时,旧应用不理解 deleted_at,单据可能重新出现在列表;与此同时票据可能已经解绑或用于新单,审计已经记录删除。独立镜像制品只能还原代码,不能还原业务语义,不能声称“回滚即恢复原状”。
  • 有留痕删除记录时,回退方案须具备经审查的兼容过滤及历史只读约束,或另审受控数据库恢复方案。不清空删除标记、不删审计、不把已重用票据强制绑回旧单。
  • 数据库恢复需单独批准,明确备份时点后业务数据丢失与数据库/附件一致性风险;摘要匹配不等于恢复验证。既有隔离数据库恢复结果也不能自动推导本候选完整应用回退已通过。
  • 回退验收至少确认:正常列表/统计/付款候选不重新纳入已删除单据,历史与审计可读,票据不被重复占用或错误改绑。未验证时如实列缺口,不把“旧镜像可加载”作为验收完成。
  • 发布材料保留独立 reviewer 未覆盖的制单/前端/BIFF8 以及并发事务细节;既有自动测试是执行端证据,未通过完整发布门禁的例外须另批。部署后验证由执行端承担,任何生产测试写入不从只读授权推导。

2026-09-10 追加阶段:退回删除最小持久化方案(已批准本地实施)

用户在官方资料调研后确认实施:修改重提为主,删除为次;无需填写删除理由。易快报公开接口明确草稿/已驳回可删除,但未证明其物理存储或删除后发票释放方式;本项目留痕设计是自己的安全合同,不冒充行业统一实现。

票据释放前将当时仍关联的 PDF 文件引用、摘要保留为本单附件,通过报销原有材料权限读取;使用不可变存储对象,不读取票据重新绑定后的内容。删除审计记录票据与明细引用。历史撤回早已释放、没有关联证据的票据不猜测回填;本轮保留删除时实际仍存在的证据及原有流程记录,不宣称补全过去缺失的提交快照。

本阶段仅处理用户确认的退回删除与重新建单;下文 R1/R2 为旧交付合同,不重开其范围。执行副本基于已发布 e5685f1,独立于发票与付款副本。

核查结果

  • 当前 DELETE /reimbursements/:id 只允许 draft,调用 reimbursementForm.delete;WorkflowInstance.reimbursement_form 为 onDelete:Cascade,其 tasks/history 继续级联。因此“退回/撤回到 draft 且有流程历史”同样不能继续物理删除,更不能只把 rejected 加入白名单。
  • 模型只有 draft/reviewing/approved/paid/rejected/cancelled,无 deleted_at/deleted_by。cancelled 只表达状态,不能区分主动删除及其他取消,也不能单独保证所有列表/统计的删除语义。不借改写 description 或挪用付款字段保存删除标记。

推荐合同

  1. 新增 nullable deleted_at 与 deleted_by(删除者引用)及相应索引;既有记录保持 null,不回填业务数据,不新增状态枚举。删除时记录原状态、删除者/时间和释放的票据引用至既有审计,状态落为 cancelled,前端依据删除标记显示“已删除”。这是明确的删除语义,不将 cancelled 本身冒充删除判据。
  2. 仅 owner/submitter 且具备 expense.delete_own 可删 draft/rejected;只读全范围、管理员通配均不替代参与关系。仍在活动审批、有非取消付款占用或已付款的异常数据拒绝。重复删除先授权再幂等返回。
  3. 未进入过 workflow 的普通 draft 保持现有删除路径;曾提交的 draft 以及 rejected 使用留痕删除。已有单据、明细、附件、workflow tasks/history 保留;正常列表/搜索/统计/导出排除已删除单据,旧深链仅向原有权人员呈现只读历史与已删除提示,不新增查看他人记录的权限。
  4. 删除事务先锁单再锁票,记录关联引用后仅释放当前仍绑定本单的有效票;不改归属、不覆盖别单关联、不删除文件。旧单取消态及显式删除校验共同阻断提交/修改/绑定/审批/付款,发票能否用于新单仍执行原归属/去重/状态门禁。
  5. 发票释放后历史材料读取从本次审计保存的引用与原单授权出发,不能因为票已换绑就授权旧单访问新单;只保存必要引用,不把完整票面原始 payload 写进审计。具体查询与材料快照边界在实现前以测试锁定,不把仅保留文件声称为历史材料关联已保留。

验证与回滚

  • 本地先复现 rejected 拒绝和有历史 draft 的级联风险;随后覆盖双方参与人、非参与人、重复删除、活动流程/付款拒绝、旧深链只读、各列表旁路、删除与提交/绑定并发、释放票据重新建单。
  • 隔离 PostgreSQL 与真实认证/浏览器验证“退回→删除→历史仍在→票据重用→新单提交”;公网不自动删除真实退回单,不将上次发布授权沿用到本迁移。
  • 迁移仅增 nullable 字段;旧应用不理解删除标记,回滚后可能重新显示已删除历史。不得声称只换旧镜像行为无变化;首次产生删除记录后优先前向修复,必要回退需兼容删除过滤,不移除审计或猜测恢复已释放票据。
  • 用户已批准本地迁移/跨层实施;生产迁移、提交推送及部署仍另批。本阶段不修改原 R1/R2 权限授予,不扩经理六个 view_all。

1. 设计边界

本任务包含两个独立变更单元:

  • R1 是付款运营入口与只读进度投影调整,复用现有付款候选及创建合同。
  • R2 是报销读取授权收紧,使用已有 workflow 关系判断是否形成过业务事实。

两者不共享新数据库字段、feature flag 或写状态机。实现、测试和提交顺序允许分开回滚;部署候选可同时包含两项,但不得形成互相调用或互为前置的数据依赖。

2. R1:待组批运营队列

2.1 服务端合同

继续以 GET /payment-batches/candidates 为唯一候选来源:approved、paid_at=null、不属于任何非取消报销付款批次,并通过付款部门门禁。接口已经提供搜索、页码、页大小和 total,无需新增第二个候选接口或在客户端复制 eligibility。

候选查询仍要求 payment.batch.manage。付款批次列表继续查询已经创建的 PaymentBatch;“待组批”与“付款批次”是同一付款工作台的两个数据视图,不把候选伪装成批次记录。

2.2 页面状态与路由

付款页引入显式 URL 视图,例如:

  • /payment-batches?view=pending:待组批。
  • /payment-batches?view=batches:付款批次。
  • /payment-batches?id=<batch-id>:既有批次详情,优先于 view。

报销详情深链使用 view=pending&q=<form-number>。页面从 URL 初始化搜索并保持返回来源;不得把内部 UUID 放入用户文案。组批管理员无 detail 参数时默认 pending,复核只读角色固定 batches。

现有“新建批次”面板中的候选搜索/选择提升为 pending 视图的主体,创建所需的最小字段和主动作沿用当前 KISS 合同。实现应只有一个候选 map 和一份选择状态;不能保留旧弹层或折叠区中的第二份候选 DOM。

候选行使用稳定 entity ID 作为 key,展示服务端权威字段。查询超过 300ms 显示加载反馈;请求失败就地显示可重试错误;空态只写“当前没有待组批报销”。选择后才显示批量创建主动作,避免未选择时铺设高风险动作。

2.3 进度语义

共享 PAYMENT_PROCESSING_STAGES 已有 payment_batch_created / 批次创建,但报销详情当前故意丢弃该阶段并在无批次时把 payment_batch_review 提前标 current。本任务调整报销组装层:

  • 将 created 阶段对报销用户重命名/投影为“组建付款批次”,不改变付款批次详情使用的“批次创建”。
  • 无 batch:created=current,result=待组批,detail=待财务组建付款批次;review=pending;payment=pending。
  • 有 batch:三个付款阶段全部来自共享 buildPaymentProcessingSemantics,created 的时间和创建人按现有 actorSource 解析,review/payment 语义不变。

报销 DTO 阶段 key 增加 payment_batch_created 属向后兼容的数组扩展;旧客户端按通用 stage 渲染或忽略,不需要 schema/migration。结构测试锁定共享语义仍只有一套,禁止在客户端根据 batch status 再写矩阵。

2.4 查询刷新

创建批次成功后同时失效:候选列表、候选总数、付款批次列表和当前批次详情所需查询。候选资格由服务端自然消失;客户端不得乐观伪造 approved -> batched 状态。失败时保留选择并显示现有自然中文错误。

3. R2:曾进入流程的草稿可见性

3.1 权威判据

定义可复用的 Prisma 条件:

own-or-submitted-draft =
  owner_id = actor
  OR submitter_id = actor
  OR workflow_instances.some(entity_type = reimbursement)

关系本身已限定为报销实体;若 Prisma relation 不需要额外 entity type 条件,以 relation some: {} 为准,并由结构测试证明关联字段正确。判据只在记录 status=draft 时附加。非 draft 沿用原范围条件。

撤回 workflow 的 current_status=cancelled、退回 workflow 的 current_status=terminated,两者都满足“曾进入流程”;不得使用 pending/latest/outcome 过滤。

3.2 列表、搜索、导出和统计

提取或共享一个“当前人员可读取的报销事实”条件构建器,组合顺序为:

  1. 既有角色范围:全局角色 {};经理为本人/提交人/直属下属;普通 own 为本人/提交人。
  2. 业务事实条件:非 draft,或本人/提交人,或存在 workflow。
  3. 页面自己的状态、时间、部门、搜索等条件。

列表、全局搜索、报销 CSV/XLSX 导出、报销统计/看板使用同一事实条件。原始 SQL 统计使用 EXISTS 子查询表达相同合同,不能因为实现技术不同而遗漏。审批瓶颈查询本身 inner join workflow 且只查 reviewing,天然满足,但测试应证明无需额外分支。

付款候选固定只查 approved,不复用这个授权条件;项目关联、AP 等非全范围草稿入口若本就不展示 draft,保持不变。实现前逐个确认所有报销 record-level 导出入口,防止只修一条 CSV 路径。

3.3 详情与材料防旁路

把当前 ensureOwnCanViewAllOrWorkflowReviewer 的无条件 canViewAll 快速放行改为异步事实判断:

  • 本人/提交人:按原权限放行。
  • 非本人且持有 view_all:记录非 draft 直接放行;draft 必须查询至少一个 workflow instance。
  • 历史/当前 workflow reviewer:继续按既有 task 关系放行;从未提交草稿不可能存在 reviewer task。
  • 其他人员:拒绝。

详情、附件文件、附件 preview URL、完整材料 preview 和 POST /reimbursements/:id/precheck 都调用同一事实授权 helper。材料与 precheck 的现有轻量查询没有同时选择 status/workflow,可由 helper 使用 reimbursement ID 做最小 findFirst/count,避免为每个调用方复制状态和 workflow 查询。拒绝统一返回既有 PERM_001,不区分不存在、未提交或越权原因。

写接口的 owner/submitter、状态和 permission 门禁不因本任务放宽。precheck 虽是 POST,但只读返回预算、费用标准、审批规则与风险事实,必须使用同一读取边界;它不得因重用 helper 让 view_all 用户获得修改他人草稿的能力。当前或历史 workflow reviewer 分支继续按 task 关系独立放行,focused 测试必须证明新草稿过滤不会阻断审批人读取自己的待办报销。

3.4 性能与兼容

优先使用现有 relation filter 与 workflow_instances(entity_type, reimbursement_id) 查询路径,不新增 submitted_at 或 migration。分页先过滤后计数,保证 items 和 total 一致。对原始 SQL 使用 EXISTS,避免 join 放大金额或记录数。

生产当前仅 1 张从未提交草稿、0 张有 workflow 的草稿,发布不需要数据修复。历史 returned/withdrawn/rejected 数据不改写;列表状态中文映射、审批历史和再次提交保持兼容。

4. 权限与状态矩阵

访问者/记录 自己的未提交 draft 范围内他人的未提交 draft 范围内他人的退回/撤回 draft 范围内他人的非 draft
owner/submitter 可见、可按原规则编辑 不适用 可见、可按原规则编辑 可见
全局 view_all 角色 可见 不可见 只读可见 只读可见
manager 可见 不可见 仅直属团队只读可见 仅直属团队只读可见
workflow reviewer(无 view_all) 按本人规则 不存在 reviewer task,拒绝 有历史任务时只读可见 有任务时只读可见
无关员工 按本人规则 不可见 不可见 不可见

5. UI/UX 约束

本任务使用现有 React/Tailwind 工作台设计,不引入新色板、字体或动画体系。ui-ux-pro-max 的适用结论落实为:异步候选查询必须有加载/错误反馈;列表使用稳定 key;导航和搜索状态写入 URL;危险批量动作仅在选择后出现;移动触控目标维持至少 44px;1280px 不产生横向滚动。

“待组批”是可执行运营队列,不是解释卡:首屏优先数量、搜索、候选和组批动作,不重复说明系统内部状态。付款批次仍是历史与后续处理对象,两者用清晰标签区分。

6. 发布、回滚与真实验收

  • 无 migration、schema、seed、flag、生产配置或数据修正。
  • R1 与 R2 使用独立提交和 focused gate。R1 回滚只恢复付款视图/进度投影;R2 回滚只恢复读取条件。任何一项回滚都不撤销已由财务人工创建的批次。
  • 部署前跑 UI strict、权限/workflow focused 回归和标准 provenance 门禁。
  • 部署后只读确认目标单据在待组批可搜到、未被自动修改;由财务人工点击创建后,才能形成真实付款复核对象。
  • R2 部署后使用受控测试身份或既有非敏感 fixture 核验未提交他人草稿不可见、有 workflow 草稿可见;不在生产临时创建真实员工草稿来凑证据。