From 9e41f107fade13551eadfc1071131f16ab2199b7 Mon Sep 17 00:00:00 2001 From: shunli Date: Fri, 11 Sep 2026 20:21:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=94=B6=E7=AA=84=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E5=B9=B6=E6=A0=A1=E6=AD=A3=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E5=BC=8F=E5=9B=9E=E5=BD=92=EF=BC=8809-04-reimbursement-state-g?= =?UTF-8?q?ap-finance-view=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spec/hyb-oa/frontend-react-vite-design.md | 3 + .../design.md | 4 + .../implement.md | 130 ++++++++++++++++++ .../prd.md | 2 + .../task.json | 4 +- client/src/pages/PaymentBatchesPage.tsx | 2 +- client/src/pages/ReimbursementDetailPage.tsx | 6 +- .../e2e/system-action-confirmation.spec.ts | 58 +++++++- scripts/qa/test-payment-experience.cjs | 7 +- 9 files changed, 202 insertions(+), 14 deletions(-) diff --git a/.trellis/spec/hyb-oa/frontend-react-vite-design.md b/.trellis/spec/hyb-oa/frontend-react-vite-design.md index 11b17786..e4a67330 100644 --- a/.trellis/spec/hyb-oa/frontend-react-vite-design.md +++ b/.trellis/spec/hyb-oa/frontend-react-vite-design.md @@ -20,6 +20,8 @@ and verification contracts. Do not copy broad aesthetic guidance from - PC uses `asideContext` for current progress after `actionPanel`, with `lg:static` on the action card. Payment detail and reimbursement approval share this order; comments remain readable without horizontal overflow. Mobile retains its own safe-area action contract. - Test absent batch, review, export, receipt, paid, failed/cancelled, missing progress and no batch-read permission. Test create/review/export with real isolated roles, then return from batch to reimbursement. Include visibility return and refresh; a payment-only test does not prove the source detail is correct. - Wrong: `approved => create batch`, or `!targetHref => no batch`. Correct: stage-derived next step and one server-authorized destination, including a conservative unknown state. +- Reimbursement desktop payment links use content width (`justify-self-start`) and the existing 44px button height, not grid stretching across the aside. The creation label is `创建批次`; it navigates to the existing pending view and never creates a batch itself. Keep mobile touch sizing and the generic action-panel layout unchanged. Verify actual desktop bounds and that navigation sends no batch-creation POST. +- When `PaymentBatchesPage` retains its `lg:hidden` mobile action bar, its desktop action card must keep `hidden lg:block` as well as `lg:static`; changing stickiness must not remove responsive visibility. The adjacent progress stays visible. Cover the narrow PC route in addition to the dedicated mobile route, including receipt-upload action switching. ## Scenario: Actionable Validation And Selectable Payment Channels @@ -612,6 +614,7 @@ reference, not a copy of Feishu or Ant Design branding. - Mobile reimbursement approval is offline -> no evidence is loaded from IndexedDB and no approval draft is allowed; an explained disabled state remains visible. - Mobile edit/approval has a bottom action -> fixed bar is visible at the viewport bottom and final evidence remains reachable above it. No bottom action -> no fixed-action page padding. - Current or historical assigned reviewers may read the authorized current evidence; unrelated employees remain forbidden. Material endpoints continue their own permission, governance, audit and short-lived WeCom grant checks. +- Supplementary attachment browser tests locate the intended file within `approval-attachments` by its accessible `查看补充附件:` control, then activate that row's `删除附件`. Do not depend on the retired `.detail-evidence-card` reimbursement layout or restore old product markup for a test. Preserve object/consequence copy, zero DELETE requests on cancellation or fewer than two reason characters, and exactly one target-specific DELETE carrying a valid reason; a mock browser pass does not prove persisted or production deletion. ### 5. Good/Base/Bad Cases diff --git a/.trellis/tasks/09-04-reimbursement-state-gap-finance-view/design.md b/.trellis/tasks/09-04-reimbursement-state-gap-finance-view/design.md index 438ff94d..92e6ef51 100644 --- a/.trellis/tasks/09-04-reimbursement-state-gap-finance-view/design.md +++ b/.trellis/tasks/09-04-reimbursement-state-gap-finance-view/design.md @@ -18,6 +18,10 @@ “统一”指信息层级、位置、动作与状态一致,不是让所有业务使用同一条审批链。当前处理进度与完整历史区分:前者固定在 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。 diff --git a/.trellis/tasks/09-04-reimbursement-state-gap-finance-view/implement.md b/.trellis/tasks/09-04-reimbursement-state-gap-finance-view/implement.md index 08f78e05..59a0834f 100644 --- a/.trellis/tasks/09-04-reimbursement-state-gap-finance-view/implement.md +++ b/.trellis/tasks/09-04-reimbursement-state-gap-finance-view/implement.md @@ -1,5 +1,135 @@ # 报销状态空隙与财务处理视角:实施计划 +## 2026-09-11 迟到 401 复核接续(最新,门户取消修正本地通过) + +主会话只读核对 dcde0d44 的 33 源文件、3 补丁重放及历史证据后,双端合成浏览器补查甲→乙→甲:甲旧请求迟到 401 后按乙会话重试,结果进入甲缓存,甲再次登录等待新响应时显示乙待办。该包 NO_GO;独立复核仍 PENDING_INLINE,原 6/62/真实隔离通过记录不改写。回执在 postpush-identity/readonly-review.md。 + +沿原批准门户隔离目标由 09-11 补请求取消,不改全局认证、权限或服务端,不新建任务。双端红测复现后,修正专项 10/10、联合三文件同轮 66/66(包含该 10 项)、新构建/PWA 与真实隔离业务链通过;临时库/合成附件已清理,135 单测按未变输入复用。新证据及修订树另存 postpush-cancellation,不覆盖 dcde0d44/NO_GO 或旧通过记录。 + +下一步新修订输入独立只读复核,明确授权后提交推送、冻结新 SHA,再刷新执行时预检并另批部署及执行端公网自动验收。主会话自检不抵扣独立 review;迟到 401 为合成 auth/API,未证明全局进行中刷新或其他查询生命周期。未提交、推送、部署或连接生产,不让员工代测。 + +## 2026-09-11 门户身份隔离本地闭合(最新,未提交/部署) + +用户已确认,由 09-11 原 task 在既有 integration 副本修 PC/手机门户身份缓存;09-04 继续协调联合候选,不新建小任务。运行时相对上一冻结树仅改两页的员工身份 queryKey/无身份禁用门,不改认证、角色、服务端、seed 或付款流程。旧 postpush-recheck 冻结包和只读 P1 回执保留,新修订包在联合目录 postpush-identity,树与摘要以其 manifest.json 为准。 + +未修构建双端 slow 红测均复现;修后专项 6/6、联合三文件同轮 62/62(含这 6 项)、新构建/PWA 及真实隔离业务链均通过。真实链包含同文档 UI 退出登录、员工→CEO、身份确认及真实门户响应暂缓,零旧待办;临时库与合成附件已清理。首次专项事件夹具和真实链 /me 夹具失败原样保留,具体归因见 09-11 implement。135/135 单测仅按输入未变复用,不报重跑;主会话检查不抵扣独立 review。 + +下一步:新修订包独立只读复核 → 明确授权提交推送并冻结新 SHA → 执行时刷新生产版本/迁移、磁盘、备份及独立回滚制品 → 候选专属部署批准和执行端公网合成验收。当前不发布 fedcaed1 或旧修订树,不用本地通过抹去完整门禁、全站缓存、实时 OCR、公网身份与应用级恢复缺口。根 checkout 和既有恢复/付款准备文件不混入;未连接生产、未提交推送/部署,员工不代测。 + +## 2026-09-11 只读复核新增阻断:跨账号门户缓存(历史:当时待修) + +本轮按“执行下一步”核对修订树 68f44e1531719c4d83308cc80494181f0d390803、33 个文件及原证据,未改冻结 manifest/补丁。主会话 inline 检查不是独立 review;源码检查与本地合成浏览器诊断发现 P1:同一浏览器甲退出、乙登录后,在乙的门户请求返回前仍展示甲的业务待办。账号控件已是乙,正确空响应返回后甲的待办才消失。生产未连接,不能据此称已发生线上泄露或服务端越权。 + +原因是门户 queryKey 不含员工身份、全局 QueryClient 跨登录保留缓存;现有退出只清认证与 PWA Cache Storage,不清 React Query。相关认证/cache 基础实现与生产基线源码一致,但未运行旧版本作历史复现,不能断言本补丁首次引入或此前必无风险。前次 56/56 的独立上下文测试没有覆盖同一文档切账号,不作废其原结果,也不能继续据此放行此候选。 + +- [x] 冻结输入摘要核对、主会话源码检查、PC 同文档双合成员工诊断及账号控件确认;未操作真实业务,未派独立 reviewer。 +- [x] 证据和具体修复方案回到 09-11 原待办 task,不新建账号切换/测试小任务;本节是冻结树之外的任务回执。 +- [ ] 确认后按 09-11 设计隔离 PC/手机门户查询身份,补同浏览器切账号、慢响应/失败/旧请求晚返回回归;不改角色、真实账号或付款状态机。 +- [ ] 修复并验证后重新冻结候选交审;当前不进入提交推送或发布,不重新运行未受影响的整套门禁来掩盖新缺口。 + +诊断脚本、首次 fixture 前置失败及两次复现记录、覆盖边界见联合目录 postpush-recheck/readonly-review.md。下一步是确认这项身份隔离修复;独立 review、提交推送、执行时预检、部署及公网验收仍单列,不让员工代测。 + +## 2026-09-11 遗留附件用例接续(已批准本地核实与验证) + +用户采纳“核实遗留附件用例、补验证和复核材料”的下一步,复用本 task,不创建测试子任务,不提交、推送、部署或连接生产。失败快照已显示补充附件文件名和“删除附件”按钮;当前共享材料组件使用 approval-attachments 分组,而旧用例仍依赖 .detail-evidence-card 和“删除”。先修正测试定位并实际执行,不能由快照单独推断删除业务已通过。 + +- [x] 核对失败快照、当前共享附件行与请求 mock,定位旧测试选择器和按钮名失效;不修改产品 DOM 或删除权限来迎合测试。 +- [x] 使用既有材料分组和文件名定位正确附件,保留对象/后果/理由、取消和理由不足零请求断言;验证有效理由只向目标附件发送一次 DELETE。 +- [x] 定向复跑后完整重跑原三个页面测试文件;旧失败日志和 postpush 冻结包不覆盖,不以分批去重冒充同轮全部通过。 +- [x] 同步本任务与 09-11 的最新验证状态,准备 postpush-recheck 修订包;未变的运行时/单测/真实隔离输入核对后复用,不宣称重新执行。最终树、补丁及证据摘要以该目录 manifest.json 为准。 + +独立只读复核仍待完成;本次只处理已知测试缺口,不扩展生产附件搜索、历史数据清理或付款状态机。 + +实际结果:附件专项 1/1,原 visual-portal / reimbursement-content-consistency / system-action-confirmation 三文件同轮 56/56,均退出 0、零重试。取消、空理由及单字理由零 DELETE,有效理由只请求目标附件一次;mock 请求验证不等于持久化删除或生产验证。根因是共享材料布局变更后旧测试定位未同步,本轮不修改产品删除行为;对应可执行定位/确认合同补入既有前端规约。 + +运行时及 QA 输入与上一冻结树保持一致,135/135 单测、typecheck/build/PWA、真实隔离认证/HTTP/双端业务链继续复用,不是本轮重跑。原失败和中断证据及 postpush 的 27 份证据/两份补丁摘要核对不变;本轮补丁仅新增测试/规约/回执差异。下节“55 通过/1 未过”是上一冻结时点的历史记录,已由本节结果闭合,但完整项目门禁、独立复核、真实腾讯 OCR 和公网受控业务验收不因此通过。 + +下一步:修订树独立只读复核 → 明确授权后提交推送并冻结新候选 → 刷新发布前证据 → 另批部署与执行端公网合成验收。仍未提交、推送、连接生产或部署;不让员工承担基础测试。 + +## 2026-09-11 提交后复核与首批准入接续(本轮已批准,尚未发布) + +当前批准的是本地规划、修复及验证;复用本任务与 09-11 待办任务,不创建新 task,不自动提交、推送、部署或执行生产合成写入。用户同轮追加操作按钮过宽、文字冗长,纳入本任务:创建入口改“创建批次”,桌面付款主入口按内容宽度,其他角色、状态和手机触控边界不变。 + +- [x] 完成按钮尺寸与短文案回归,核对点击只是进入既有付款页,不自动创建批次。 +- [x] 修复扩展回归发现的付款 PC 路由窄屏双操作区;原隐藏断言失败证据保留,手机底栏及其动作切换不移除。 +- [x] 联合 09-11 的手机非财务待办查询修正,完成新构建、定向浏览器及真实隔离角色链验证;旧 fedcaed1 不再是无已知缺陷的可部署候选。 +- [x] 核对已推送旧候选及原证据,另存提交后封面和待提交修正包,不覆盖历史冻结输入。 +- [x] 整理首批角色就绪、执行时预检、发布授权范围、公网自动验收与停止条件;未执行项保持未验证。 +- [ ] 新修正另经提交推送授权冻结新 SHA,完成独立复核后再申请该候选的发布批准;员工不承担基础测试。 + +### 首批角色与资料:已知和待核分开 + +| 对象 | 已有记录可确认 | 仍需完成,不由旧证据推断 | +| --- | --- | --- | +| 普通员工、财务报销人 | 两人的直接经理及所属主体已修正回读,不再回填 | 正常身份认证、本人提交审批解析、本人收款资料及解密可用性;资料由授权维护人直接录系统 | +| CEO 审批/复核人 | 保留审批路由,单独 CEO 不授 AP 全量读取;既有付款复核业务反馈 | 新手机待办、公网角色深链与非本人制单复核;不摘角色、不替本人处理真实任务 | +| 财务付款人 | 公司主体/付款账户存在性及已有创建/下载业务反馈 | 自审拒绝、制单/回单权限、已组批返回原单及待办消失;不把他人的成功付款资料推及所有人 | +| 管理员 | 日常管理权限保留 | 个人报销不是默认首批需求;如本人需要,再单独核经理/主体/收款资料,不给全部 smoke 回填 | + +### 发布与执行端公网验收清单(待批准执行) + +1. 绑定新候选 commit/tree/补丁及验证证据;独立只读复核与主会话自检分列。旧 fe025e65/fedcaed1 审查或例外不能自动覆盖本次修正。 +2. 发布前重新只读核对生产 build/image、迁移状态/checksum、磁盘及峰值余量、最新同源 DB/附件备份完整性和时点、当前独立镜像制品。早期 86%/8.93GB 不再引用为当前容量;40.04GiB 也只是前次观测。本轮没有刷新生产。 +3. existing 模式保持不运行初始化 seed;迁移先于必需配置检查,不能宣称失败时零写库。本轮运行时修正不新增迁移、角色、seed 或银行渠道。 +4. 恢复风险如实披露:数据库已恢复成功;配对检查发现一条无当前业务关联的历史附件缺失,用户已选择保留归档并标记、暂不追查;原 NO_GO/应用未验证不改成 PASS。镜像只能恢复代码,不能撤销数据库写入;恢复旧备份可能丢失之后业务。fe025e65 已认识留痕删除,不误写成本次一定要退到不认识删除标记的旧版本;如跨越更早版本仍需单独评估。 +5. 同一份发布方案明确部署、专用测试身份、有限合成单据/发票/审批/批次/回单写入和清理范围,获批后连续验收,避免逐动作打断。只处理隔离标识的合成对象,审计留存,不重放真实员工单据;外部通知/银行连接关闭。真实腾讯 OCR 调用须使用获准票据及受控凭据,只回传结果,不把 mock 当实测。 +6. 顺序为候选版本/ready/bundle → 专用普通员工及财务/CEO 认证 → 双端上传/OCR/选票建明细/分类编辑 → 提交/退回重提/留痕删除重建 → 审批 → 财务组批/本人自审拒绝/CEO 复核 → 中文 BIFF8 制单 → 返回原报销操作与进度一致 → 合成回单登记 → 对应待办消失。生产若没有获准的合成测试渠道,停止该段并保留未验证,不以登录页或 admin 登录抵扣。 +7. 任何版本不一致、权限越界、状态错误、待办漏失、备份/迁移异常均停止对应阶段,保存证据;不自动回填/付款/清备份或凭不确定响应重试写入。回退须根据本次实际写入与已批准方案判断。 +8. 执行端验收通过、剩余例外逐项批准后才开放首批正常业务。真实审批/网银付款仍由职责人办理,不称合成 paid 是银行付款成功。 + +### 并行债务排期(不叠入本次冻结代码) + +- 09-06 门禁及已有 09-07 认证任务:先按历史诊断归因恢复可信登录前置与完整验收;不把旧失败数当确定产品缺陷,不默认运行可能部署/访问公网的 all 或 project-wide 入口。 +- 09-05 错误解释:复用现有模块清单,09-18 首批验证、09-25 复审余量、09-30 前逐模块延期说明、10-02 查漏。673 条仍 10-05 到期,不统一续期。 +- 08-23 恢复:完整附件/应用级隔离验证另排期,保留数据库 PASS 与历史缺失原结论;不再扩查旧发票或加复杂 runner。 +- F1 付款读取纵深防御、其他详情页扩围和任务状态收尾仍归已有任务,随相关模块实施,不阻塞本轮本地验收也不冒充已经完成。 + +扩展检查发现同一付款 spec 两条手机断言相互矛盾:旧“固定操作区”要求桌面卡隐藏,旧“先制单”又要求该卡可见并用于测量进度。按已批合同,后者改为检查实际手机底栏可见/fixed/至少 44px,桌面仍测进度在操作下方;不删除可见性或付款动作断言。完整三文件运行还发现一条本轮未修改的“高敏附件删除”用例找不到旧 .detail-evidence-card 目标;保留失败,不推断生产附件不可删除,也不顺手改附件实现或删断言。作为既有门禁债的具体验证项记录,未通过的完整扩展结果不能冒充全绿。 + +### 本轮执行结果与下一步 + +- 代码:仅 3 个前端页面运行时变化,本任务按钮/窄屏与 09-11 手机待办同原任务交付,不改通用组件、服务端、角色、schema、seed 或付款状态机。两项定位失败后修复,付款既有断言冲突另行纠正;本轮不提交、推送、连接生产或发送消息。 +- 验证:unit-complete.log 135/135、build-integrated.log typecheck/build/PWA、真实隔离 real-db-browser-final.log 均退出 0;真实密码登录/JWT/HTTP、PC/手机、上传选票/分类/退回重提/审批/组批/独立复核/中文 BIFF8/返回原单/合成回单登记以及两类手机待办冷进入 1→0 通过,临时容器和合成文件已清理。OCR 是真实 worker/mock provider,没有腾讯调用或银行连接。 +- 页面:新角色组合 6 项和按钮 4 项先定向通过;扩展三文件原始 54/56,响应式冲突断言修正后 3/3 复跑,去重 55 项通过、历史附件选择器用例 1 项仍失败。不得写成全量 56/56;该用例本体及共享附件组件与 HEAD 相同,但未做旧候选运行时独立复现,不下“生产无问题”结论。 +- 环境与治理:首轮系统 Python 缺 xlwt 引发导出测试 409,换用既有固定 BIFF8 环境,不改产品兜底;前后端测试均需显式传同一 PAYMENT_ORDER_PYTHON_BIN。143/130 中断及原失败日志保留,以完成回执为准。过程、Trellis、UI strict、原因码 strict、语法及 diff 检查通过;文档 registry 0 errors/1 warning(既有 22 项),不顺手清理。 +- 交审:联合 postpush/review.md、manifest.json、supplement.diff 和 combined-changes.diff 区分原已推送候选与本地修正;原 32 文件/13 份证据及两笔补丁树核对通过。恢复/付款任务的既有未提交补记和根 checkout 改动不纳入修正补丁。 +- 等待事项:独立只读复核、历史附件用例归因(由既有门禁任务接续)、明确提交推送修正授权、冻结新 SHA、执行时预检及本候选部署/公网合成验收授权。旧附件缺失按已确认处置保留;不重复回填、不删回滚制品、不让员工代测,不把阶段完成等同员工已可公网使用。 + +## 2026-09-11 当前两项窄授权(已完成,未部署) + +用户在明确“保存并校验当前生产镜像,同时仅为财务和 yj 补齐唯一所属主体;不部署、不恢复、不修改银行资料”后回复“是”。本轮允许以下两项,不继承或扩大为管理员/smoke 回填、应用发布、docker load、备份恢复、业务单据操作或提交推送。 + +- [x] 刷新目标身份与当前 build/image/容量,使用已审镜像保存流程,只新建本次独立制品目录;配置摘要、全部层、归档摘要与大小通过,另一次 SSH 只读回读一致。 +- [x] 复用 09-08 付款任务执行财务与 yj 的 legal_entity_id 空值→唯一启用主体:只读预检冻结身份,检查员工 UPDATE 触发器/规则,事务锁定、条件恰好两行更新、提交后新连接只读回读;又经单独 SSH READ ONLY 复核一致。 +- [x] 补脱敏回执与实际剩余项;独立复核/ARM 管理员渠道/配对恢复/部署仍待处理。镜像回退不撤销数据库写入;本轮不写真实单据。 + +执行回执:10:46(Asia/Shanghai)当前 fe025e65 镜像独立保存并回读成功,归档 1,119,357,440 字节(约 1.04 GiB),SHA-256 为 5115ca42ac1c59e9726c8d92447e82080c2816bf9460196f430d04a66b1cf886;配置摘要与运行镜像一致、14/14 层通过,文件 0600/目录 0700,独立于 Docker 存储但仍同机,余量约 40.04 GiB。容器及启动时间未变,不做 load/恢复,不删旧制品。 + +10:50 先只读核对两人身份/角色/经理/原值、唯一主体及已审 UPDATE 触发器/规则;10:51 Serializable 事务仅条件更新 legal_entity_id 恰好两行,提交后新数据库连接回读一致,10:52 再经独立 SSH READ ONLY 回读。管理员、CEO、smoke、经理、角色、银行资料与单据未纳入写目标;未部署、未提交推送此次文档补记。 + +证据位于本副本 artifacts/qa-flow/20260911-authorized-preparation/:image-result.json、image-save-receipt.json、image-independent-readback.json、entity-precheck.json、entity-apply.json、entity-readback.json。修正程序 Node 语法检查及 13 项合成正反例通过,涵盖原值/角色/经理/唯一主体漂移、部分更新回滚、触发器变化与不确定提交不重试;合成测试不冒充真实数据库事务测试,实际写入/回读回执另列。 + +今日备份时点早于本次两字段修正,没有触发新备份;恢复旧备份会回退本次修正及其后业务数据,镜像回退不会撤销它们。当前已补齐镜像制品和两人主体缺项,不代表银行解密、真实提交/付款、DB+附件恢复或独立候选复核通过。下一步仍是独立复核及获得隔离目标/管理员渠道后的配对恢复;部署和公网合成验收另批。 + +## 2026-09-11 已授权提交推送与限定只读预检回执(最新,未部署) + +用户回复“执行你推荐的这些下一步的工作”,本轮承接两笔提交推送及限定只读预检;不扩为保存镜像、恢复、生产资料回填、消息发送、部署或合成业务写入。inline 主会话复核继续单列,独立 reviewer 未执行,不把用户同意下一步写成独立复核已通过或发布例外获批。 + +- 提交:f9aee4c93272204d18408217358d95334cedcd5e(本 task,19 文件);fedcaed1a13cc3b32ee47e1d72c2b5ffa5512c57(09-11 待办,15 文件)。共享两份 QA 脚本按补丁拆分,临时 index 核对第一笔无第二笔投影逻辑、两笔最终树为 476364c76cf476e6af14f0f263599f4d5eced566,与原冻结树完全一致;没有新分支、amend、强推或移动根 checkout 的 main。 +- 推送:origin/main 回读 fedcaed1,待推送 0。32 个最终变更文件及旧验证日志摘要均与未提交冻结包匹配,复用 135/135 单测、39/39 页面、真实隔离数据库/HTTP/双端联合验证,不假称本轮重跑全部测试。提交/推送回执及两笔补丁在本副本 artifacts/qa-flow/20260911-combined/,旧 manifest/changes.diff 未改。 +- 生产预检:09:47–09:52(Asia/Shanghai),运行仍为 fe025e65,容器/启动时间不变;68 条迁移全部完成,pending/未知/异常/checksum 不匹配均 0。本轮候选无新增 migration/schema/seed/角色修改。数据库、附件、回滚目录所在文件系统均约 41.09 GiB 可用、30.3% 使用率,不需要本轮清理。 +- 本地备份:今日 02:30:49 完成,核验时约 7.27 小时;manifest、DB 包、附件包大小/摘要一致,build/image 与当前生产匹配,PASS_INTEGRITY_ONLY。使用既有管理员 SSH 通道只读验真,未改权限;未恢复备份、未导出到开发机。 +- 恢复制品:既有登记目录中 3 份非当前版本回执,没有匹配当前 fe025e65/image 的独立镜像制品;这是登记位置内的结论,不断言其他所有位置均不存在。旧制品保留,本轮没有 docker save/load、清理或重启。 +- 异机:源端正式 last-success 对应今日批次,最后时效检查为 PASS_AUTOMATIC_BACKUP_FRESH、findings=0;ARM dead-man 今日 06:30 服务退出 0、timer active。ARM 普通账号读归档目录被拒,现有 sudo 非交互通道要求密码,未绕权限;今日 ARM 文件摘要/磁盘仍未独立核验,不能用源回执和 timer 成功代替。源摘要 last-run 使用 12 位 VITE_BUILD_SHA、last-success 使用完整 SHA;本轮探针误按全长比较已按正式脚本合同修正,失败记录保留,不认作备份故障。 +- 员工:排除 smoke,仅核对已确认四个登录标识的最小必要字段。CEO 角色保留且无 AP 全量权限;财务/管理员保留原 AP 读取。财务和 yj 的直接经理均为已确认 CEO,且经理启用。四人的门户及付款部门 gate 均开启;腾讯 OCR 配置与 jobs 开启,未调用接口。 +- 就绪缺项:CEO 所属主体、本人建行资料存在性、对应付款账户密文存在性均满足已查前置;财务/yj/管理员仍无所属主体,不能据此说他们自己的报销已具备付款条件。管理员还无直接经理、未关联有效建行机构;若本人也报销需另核修正范围。全公司当前唯一启用主体、1 个有效建行付款账户。这里只检查字段/权限/规则角色存在性,没有实际登录、提交解析或银行解密,不能说已证实全部人能走通,也不宣称已发生业务阻断。 + +本轮只读证据在本副本 artifacts/qa-flow/20260911-readonly/:production-precheck.json、backup-admin-check.json、employee-readiness.json、gates-transfer-final.json、arm-readonly.json、arm-admin-readonly.json。未读取真实单据金额/票据/银行明文/密码或个人访问历史,未写业务库。 + +下一步优先独立只读复核已推送候选,并单独批准保存校验当前 fe025e65 独立镜像;财务/yj 的主体回填是一笔独立生产数据授权,管理员是否纳入须按实际本人报销需要裁决,不顺手回填。配对 DB/附件恢复仍需明确隔离目标与管理员执行渠道,实际恢复/部署/公网合成写入另批。不让员工代测、不把历史 DB_RESTORE_PASS 泛化为本候选配对恢复。 + +以下为历史本地实施回执;本节覆盖其中未提交/未推送状态。本次执行回执是候选之外的文档补记,未自动再提交或归档。 + ## 2026-09-11 本地联合验收回执(最新,未提交/未部署) - 代码:付款下一步统一消费服务端进度;只有明确未组批才显示创建入口,已存在批次使用授权深链,无权/未知不猜。报销详情与报销审批工作台统一右栏操作→当前进度→适用的完整历史,手机保留单列和底栏动作;局部重入/聚焦刷新不覆盖编辑态。 diff --git a/.trellis/tasks/09-04-reimbursement-state-gap-finance-view/prd.md b/.trellis/tasks/09-04-reimbursement-state-gap-finance-view/prd.md index c6b28946..5ddd8a5e 100644 --- a/.trellis/tasks/09-04-reimbursement-state-gap-finance-view/prd.md +++ b/.trellis/tasks/09-04-reimbursement-state-gap-finance-view/prd.md @@ -9,6 +9,8 @@ - 操作、下一步和进度必须消费同一份权威付款阶段,不再各自仅由报销状态猜测。只有明确处于待组批且当前用户有组批权限时才展示组批入口;已存在有效批次时进入该批次,不重复组建。 - 复用既有服务端 processingProgress、付款阶段语义和权限控制;批次入口不可因缺少 targetHref 而推断“未组批”。数据缺失、请求失败或不完整时不展示猜测性的创建入口。 - PC 统一为主列业务内容/材料,右栏上方操作及下一步、下方当前处理进度;进度不在主列重复。长历史按既有授权完整保留,可折叠或进入历史区域,不能因侧栏变窄截掉意见/记录。手机保持单列及底栏动作,同类页面状态/下一步/进度顺序一致,不强做双栏。 +- 09-11 用户追加:报销详情“去组建付款批次”主按钮改为“创建批次”,桌面创建/查看批次入口按内容宽度左对齐,不铺满侧栏。仍是进入付款页而不是自动创建;状态、权限、目标与查询参数不变,触控高度维持既有 44px,手机不照搬桌面窄按钮。 +- 联合回归发现原付款详情 PC 路由在窄屏同时展示桌面操作卡和手机底栏;属于本轮三类详情布局一致性范围。窄屏只保留手机底栏的同一动作模型,处理进度仍可见,不改批次状态或付款动作权限。 - 首批闭合报销详情、报销审批工作台与付款批次详情的状态/位置一致性;其他业务详情先列实际差异再按归属分批调整,不以“统一”重写全部页面、复制审批引擎或改变各业务状态机。 - 按已批准体验复用现有侧栏/进度样式,不引入新视觉体系。全系统位置原则需在方案获批后同步既有产品规则及共享组件规约,避免现有规则仍允许两种位置。 diff --git a/.trellis/tasks/09-04-reimbursement-state-gap-finance-view/task.json b/.trellis/tasks/09-04-reimbursement-state-gap-finance-view/task.json index dd9b39af..e25b92f6 100644 --- a/.trellis/tasks/09-04-reimbursement-state-gap-finance-view/task.json +++ b/.trellis/tasks/09-04-reimbursement-state-gap-finance-view/task.json @@ -15,12 +15,12 @@ "branch": null, "base_branch": "main", "worktree_path": null, - "commit": "5fe9f78", + "commit": "f9aee4c93272204d18408217358d95334cedcd5e", "pr_url": null, "subtasks": [], "children": [], "parent": null, "relatedFiles": [], - "notes": "历史业务提交:R2=8e0af82,R1=5fe9f78;删除73fcd5a6随fe025e65于09-10发布。09-11付款下一步/侧栏修复与09-11待办投影按补丁联合,本地135单测、39页面和真实隔离流程通过。基线fe025e65不含本次未提交改动;独立复核、提交推送、生产就绪/恢复及发布验收待执行。详情见implement最新回执,不归档。", + "notes": "09-11复核dcde0d44发现旧请求迟到401污染旧身份缓存,双端甲→乙→甲已合成复现,原包NO_GO保留。沿原批准门户隔离范围接续取消:两页queryFn消费signal,经门户API传给HTTP,不改全局认证、角色或服务端。双端红测后最新专项10/10、联合66/66零重试、新构建/PWA及真实隔离业务链PASS,临时库/合成附件已清理;135单测按未变输入复用。新包postpush-cancellation,独立review仍PENDING_INLINE;需新输入交审、明确提交推送授权、新SHA执行时预检及另批部署/公网自动验收。本轮未提交推送、连接生产或部署,不声称全站认证缓存或真实过期会话已验证。", "meta": {} } diff --git a/client/src/pages/PaymentBatchesPage.tsx b/client/src/pages/PaymentBatchesPage.tsx index 442753e5..453f3d40 100644 --- a/client/src/pages/PaymentBatchesPage.tsx +++ b/client/src/pages/PaymentBatchesPage.tsx @@ -1073,7 +1073,7 @@ export function PaymentBatchesPage() { } primaryAction={paymentActionModel.primary ? renderPaymentActionButton(paymentActionModel.primary) : undefined} moreActions={paymentActionModel.more.length ? ( diff --git a/client/src/pages/ReimbursementDetailPage.tsx b/client/src/pages/ReimbursementDetailPage.tsx index 6146e9e8..07f5f323 100644 --- a/client/src/pages/ReimbursementDetailPage.tsx +++ b/client/src/pages/ReimbursementDetailPage.tsx @@ -1006,14 +1006,14 @@ export function ReimbursementDetailPage() { } if (paymentHandoff?.targetHref) { - return ; + return ; } if (paymentHandoff?.canCreateBatch && canManagePaymentBatch) { return ( - ); diff --git a/client/tests/e2e/system-action-confirmation.spec.ts b/client/tests/e2e/system-action-confirmation.spec.ts index 2635fce0..144deb98 100644 --- a/client/tests/e2e/system-action-confirmation.spec.ts +++ b/client/tests/e2e/system-action-confirmation.spec.ts @@ -676,9 +676,12 @@ test.describe("报销付款交接一致性", () => { await page.goto(`${mobile ? "/mobile" : ""}/reimbursements/${reimbursementId}`); const actions = page.getByTestId(mobile ? "mobile-reimbursement-payment-actions" : "reimbursement-action-panel"); await expect(actions).toContainText("已生成银行制单,等待付款完成并上传回单"); - await expect(page.getByText("去组建付款批次", { exact: true })).toHaveCount(0); + await expect(page.getByRole("link", { name: "创建批次", exact: true })).toHaveCount(0); const link = page.getByRole("link", { name: "查看所在付款批次", exact: true }); await expect(link).toHaveCount(1); + const linkBounds = await link.boundingBox(); + expect(linkBounds!.height).toBeGreaterThanOrEqual(44); + if (!mobile) expect(linkBounds!.width).toBeLessThan(180); await link.click(); await expect(page.getByTestId("payment-batch-processing-panel")).toBeVisible(); await page.goBack(); @@ -695,6 +698,34 @@ test.describe("报销付款交接一致性", () => { }); } + for (const width of [1280, 1440]) { + test(`桌面${width}创建批次按钮短文案按内容宽度且只导航不组批`, async ({ page }, testInfo) => { + await page.setViewportSize({ width, height: 960 }); + await mockSystemActionApis(page, { reimbursementStatus: "approved", reimbursementProgress: () => progress("none") }); + let batchWrites = 0; + page.on("request", (request) => { + if (new URL(request.url()).pathname === "/api/payment-batches" && request.method() === "POST") batchWrites += 1; + }); + await login(page, user); + await page.goto(`/reimbursements/${reimbursementId}`); + const button = page.getByRole("link", { name: "创建批次", exact: true }); + await expect(button).toBeVisible(); + const actions = page.getByTestId("reimbursement-action-panel"); + const buttonBounds = await button.boundingBox(); + const actionBounds = await actions.boundingBox(); + expect(buttonBounds!.width).toBeGreaterThanOrEqual(44); + expect(buttonBounds!.width).toBeLessThan(160); + expect(buttonBounds!.width).toBeLessThan(actionBounds!.width * 0.7); + expect(buttonBounds!.height).toBeGreaterThanOrEqual(44); + expect(Math.abs(buttonBounds!.x - actionBounds!.x)).toBeLessThan(2); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2)).toBe(true); + await actions.locator("..").screenshot({ path: testInfo.outputPath("compact-payment-action.png") }); + await button.click(); + await expect(page).toHaveURL(/\/payment-batches\?view=pending&q=/); + expect(batchWrites).toBe(0); + }); + } + for (const [status, expected] of [ ["none", "待财务组建付款批次"], ["draft", "待非制单人复核付款"], ["reviewed", "已生成银行制单"], ["failed", "付款登记失败"], @@ -709,6 +740,7 @@ test.describe("报销付款交接一致性", () => { await page.goto(`/reimbursements/${reimbursementId}`); await expect(page.getByTestId("reimbursement-action-panel")).toContainText(expected); await expect(page.getByRole("link", { name: /付款批次/ })).toHaveCount(0); + await expect(page.getByTestId("reimbursement-open-pending-payment-batches")).toHaveCount(0); }); } @@ -941,11 +973,17 @@ test.describe("R7E 全站高风险操作二次确认", () => { const progress = page.getByTestId("payment-batch-processing-panel"); await expect(progress).toBeVisible(); expect(await progress.evaluate((element) => Boolean(element.previousElementSibling?.querySelector('[data-testid="payment-batch-action-panel"]')))).toBe(true); - const actionBounds = await sideActions.boundingBox(); + const actionBounds = await actions.boundingBox(); const progressBounds = await progress.boundingBox(); expect(actionBounds).not.toBeNull(); expect(progressBounds).not.toBeNull(); - expect(progressBounds!.y).toBeGreaterThanOrEqual(actionBounds!.y + actionBounds!.height); + if (mobile) { + await expect(sideActions).toBeHidden(); + expect(await actions.evaluate((element) => getComputedStyle(element).position)).toBe("fixed"); + expect(actionBounds!.height).toBeGreaterThanOrEqual(44); + } else { + expect(progressBounds!.y).toBeGreaterThanOrEqual(actionBounds!.y + actionBounds!.height); + } expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); await page.screenshot({ path: testInfo.outputPath("before-payment-order.png"), fullPage: true }); await actions.getByTestId("payment-batch-action-downloadOrder").click(); @@ -1142,25 +1180,33 @@ test.describe("R7E 全站高风险操作二次确认", () => { await login(page, user); await page.goto(`/reimbursements/${reimbursementId}`); - const attachment = page.locator(".detail-evidence-card").filter({ hasText: "合成报销敏感附件.pdf" }); + const attachment = page.getByTestId("approval-attachments").locator(":scope > div").filter({ + has: page.getByRole("button", { name: "查看补充附件:合成报销敏感附件.pdf", exact: true }), + }); await expect(attachment).toBeVisible(); - await attachment.getByRole("button", { name: "删除" }).click(); + await attachment.getByRole("button", { name: "删除附件", exact: true }).click(); const dialog = page.getByTestId("attachment-delete-confirm-dialog"); await expect(dialog).toContainText("合成报销敏感附件.pdf"); await expect(dialog).toContainText("无法在 OA 内恢复"); expect(calls.deleteReimbursementAttachment).toBe(0); + await page.getByTestId("attachment-delete-confirm-submit").click(); + await expect(dialog).toContainText("请填写至少 2 个字的删除理由"); + expect(calls.deleteReimbursementAttachment).toBe(0); + await dialog.getByRole("textbox", { name: "删除理由" }).fill("一"); await page.getByTestId("attachment-delete-confirm-submit").click(); await expect(dialog).toContainText("请填写至少 2 个字的删除理由"); expect(calls.deleteReimbursementAttachment).toBe(0); await dialog.getByRole("button", { name: "取消" }).click(); + await expect(dialog).not.toBeVisible(); expect(calls.deleteReimbursementAttachment).toBe(0); - await attachment.getByRole("button", { name: "删除" }).click(); + await attachment.getByRole("button", { name: "删除附件", exact: true }).click(); await page.getByRole("textbox", { name: "删除理由" }).fill("附件重复上传"); await page.getByTestId("attachment-delete-confirm-submit").click(); await expect.poll(() => calls.deleteReimbursementAttachment).toBe(1); expect(calls.attachmentDeleteReasons).toEqual(["附件重复上传"]); + await expect(dialog).not.toBeVisible(); }); test("付款回单作废要求对象、后果和理由,取消与理由不足均为零请求", async ({ page }) => { diff --git a/scripts/qa/test-payment-experience.cjs b/scripts/qa/test-payment-experience.cjs index 3d6be482..89dd967d 100644 --- a/scripts/qa/test-payment-experience.cjs +++ b/scripts/qa/test-payment-experience.cjs @@ -96,9 +96,12 @@ module.exports = async function verifyPaymentExperience({ await page.goto(`${uiOrigin}${mobile ? "/mobile" : ""}/reimbursements/${form.id}`); const sourceActions = page.getByTestId(mobile ? "mobile-reimbursement-payment-actions" : "reimbursement-action-panel"); await expect(sourceActions).toContainText("已生成银行制单"); - await expect(page.getByText("去组建付款批次", { exact: true })).toHaveCount(0); + await expect(page.getByRole("link", { name: "创建批次", exact: true })).toHaveCount(0); const sourceLink = page.getByRole("link", { name: "查看所在付款批次", exact: true }); await expect(sourceLink).toHaveCount(1); + const linkBounds = await sourceLink.boundingBox(); + assert.ok(linkBounds.height >= 44); + if (!mobile) assert.ok(linkBounds.width < 180); await sourceLink.click(); const actions = page.getByTestId(mobile ? "payment-batch-mobile-actions" : "payment-batch-action-panel"); await expect(actions.getByRole("button", { name: "上传银行回单" })).toBeVisible(); @@ -117,7 +120,7 @@ module.exports = async function verifyPaymentExperience({ await expect(page.getByTestId("payment-batch-processing-panel")).toBeVisible(); await page.goBack(); await expect(sourceActions).toContainText("已生成银行制单"); - await expect(page.getByText("去组建付款批次", { exact: true })).toHaveCount(0); + await expect(page.getByRole("link", { name: "创建批次", exact: true })).toHaveCount(0); await expect(sourceLink).toHaveCount(1); const sourceProgress = page.getByTestId(mobile ? "mobile-reimbursement-processing-progress" : "reimbursement-workflow-progress"); await expect(sourceProgress).toHaveCount(1);