实现报销材料连续预览(07-27-yang-jun-reimbursement-readiness)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-08-01 00:17:54 +08:00
父节点 b6485a278e
当前提交 8a9279efce
共修改 26 个文件,包含 1211 行新增和 178 行删除
@@ -205,9 +205,10 @@
## 12. 报销明细与材料的 KISS 对齐
- 交互采用“展示统一、语义保留”:用户界面把发票原件和补充附件统一归入“报销材料”,但 API、数据库类型、授权、审计和生命周期保持不变;付款后材料不在本轮提前接入。
- 财务核对以费用明细为主。两端页面按 `invoice.linkedItemId === item.id` 将发票摘要渲染在对应明细内,摘要至少包含发票标识、销售方和金额;摘要整行是明确的键盘/触控预览入口,并复用现有 `openInvoiceMutation`。
- 未关联或关联目标已不存在的发票只出现在“报销材料”区,不猜测关联关系。材料区显示“发票 N · 补充附件 M”,并继续列出全部材料,方便主管快速通览以及处理未关联材料。
- 材料列表以带类型标签的文件信息行作为预览入口,不再给每份材料追加“查看原件”按钮。草稿编辑态的添加发票、移除发票、上传附件和删除附件仍作为独立显式动作,避免把预览与数据变更混在同一点击目标。
- 不新增材料浏览页、底部抽屉、上一份/下一份状态、批量下载、缩略图瀑布流或新的后端聚合接口。PC 与移动端保持相同信息结构,但沿用各自已有桌面表格和移动卡片组件。
- 可点击信息行必须有可见焦点、自然中文 `aria-label`,移动端触控目标不小于 44px;加载中禁用重复打开。企微仍走 `previewFile`,PC 仍走当前安全预览路径。
- 交互采用“一个入口、一次通览、语义保留”:费用明细只展示费用事实,不再渲染关联发票;`linkedItemId`、票据/附件类型、对象权限、审计和生命周期均不改变。付款回单仍属于付款阶段材料,不提前混入本次提交材料。
- 报销材料只读态首屏显示“发票 N · 补充附件 M”和一个明确的“查看全部材料”按钮,不为每份文件重复暴露技术操作。草稿/驳回编辑态仍保留发票添加/移除和附件上传/删除所需列表,数据变更动作与只读通览分离。
- 官方 `previewFile` 只有单文件参数;混合 PDF/图片不能直接连续切换。服务端新增报销级图片预览 grant:授权后读取当前绑定的发票 PDF 与补充附件,PDF 通过受控 Poppler `pdftoppm` 子进程逐页栅格化,JPG/PNG/WebP 通过现有 `sharp` 规范化为浏览器/企微兼容图片,生成短时图片预览包。企微调用一次 `previewImage({ current, urls })` 原生左右滑动,PC/普通浏览器在同一材料浏览页按顺序连续显示。
- 预览顺序固定为发票创建顺序、补充附件创建顺序、PDF 内页顺序;不根据 `linkedItemId`、金额或文件名猜测排序。图片预览包不进入业务表、不形成长期附件,只在单进程有界内存缓存中保存至 grant 过期,token 绑定 purpose、reimbursement、actor、随机 bundle key、图片索引和过期时间。
- grant 签发前复用报销对象级读权限及附件治理/理由门禁;生成时限制材料数量、源文件总字节、PDF 总页数、单页像素和输出字节,Poppler 使用无 shell 的固定参数、超时和临时目录。每个图片 GET 验签、核对缓存 bundle 与 actor 后先写审计再发送。缓存缺失、进程重启、材料不支持/损坏、超限或栅格化失败时返回稳定错误,客户端展开现有逐份原件预览作为降级,不静默漏页。
- 后端从已脱敏持久化 OCR `result_json` 只映射 `documentCategory` / `documentSubtype` 到报销详情,不下发原始腾讯响应。前端共享格式化函数按 `vat_invoice`、`full_vat_invoice`、`electronic_train_ticket` 选择票种名称和适用字段;未知票种使用中性“发票 + 号码/文件名”,不显示“无代码”或对不适用字段宣称“未识别”。
- “查看全部材料”和降级行均使用语义化 button、可见焦点、加载反馈和至少 44px 触控目标;保留可预测返回。PC 与移动端共享业务文案、票种格式化逻辑和材料浏览页;不引入 PDF.js,企微优先原生 `previewImage`,PC/普通浏览器在短时 URL 过期前主动加载有界图片包。
@@ -158,7 +158,18 @@
- [x] 发布脚本的来源、环境合同、init、migration、`/ready`、external/runtime smoke 和公网资源扫描通过;公网构建标识为 `0e44aa72f3f5`,活动资源包含 PC/移动统一材料区标识。
- [x] 复用现有 unit 参数和运行时间刷新 automatic backup 三项与 WARCH3;4 个 service 构建身份一致,4 个 timer 均 enabled/active。
- [x] 手工触发 automatic backup daily 和源端 watchdog,二者均为 `success/0`;未执行真实审批、财务审核、付款批次或付款写入。
- [ ] 请用户在企微刷新后验证:费用明细内可直接点关联发票,页面只有一个“报销材料”区,发票与补充附件整行可点且不再重复显示“查看原件”按钮。
- [x] 用户验收后修正方向:不需要在费用明细内查看关联发票;现有逐份材料行仍然繁琐,需要一次进入连续查看全部材料。该反馈覆盖 6.9 的内联发票展示合同,已部署事实保留但不再作为目标体验。
## 6.11 票种摘要与整单材料预览
- [x] 只读核查生产真实报销:绑定票据 3 张,均为 `bound`、腾讯 OCR 已实际执行且无存储错误;号码/日期/金额完整。票种为 2 张电子火车票和 1 张数电发票,传统代码均不适用,火车票销售方字段不适用;未输出号码、金额、主体、文件名、UUID 或原始 OCR 结果。
- [x] 核查企微 SDK 合同:`previewFile` 仅接受单个 `url/name/size`;只有图片接口 `previewImage` 接受 `urls[]`。当前真实材料为 3 份 PDF 发票和 3 份 PNG 补充附件,总源文件小于 1 MiB,适合逐份 PDF 栅格化后组成有序图片包验证连续预览。
- [x] 用户批准采用服务端 PDF 栅格化 + 企微 `previewImage`,其余按统一材料入口、票种摘要和原件降级方案实施;不采用合并 PDF 或 PDF.js。
- [x] 删除 PC/移动费用明细中的关联发票 UI 和对应旧断言,保留后端 `linkedItemId`。
- [x] 报销详情 API 增加安全票种分类映射;共享票据格式化按普通增值税、数电发票和电子火车票显示适用字段,不重跑或改写当前真实 OCR 数据。
- [x] 在运行镜像加入固定 Poppler 工具,结合既有 `sharp` 实现有界、短时、对象授权的报销级图片预览 grant;企微一次 `previewImage` 原生滑动,PC/普通浏览器连续浏览,失败展开逐份原件预览降级。
- [x] 更新后端路由/授权/审计/超限/缓存失效测试与 PC/企微 Playwright;运行 Prisma validate、server 聚焦测试与全量基线、client typecheck/build、Trellis/governance/process/docs/sensitive/diff 门禁。
- [x] 稳定合同验收后再反向更新 `reimbursement-invoice-ocr.md` 与 `wecom-boundary.md`,覆盖当前“明细内发票、无聚合接口”的旧规范;提交、推送、生产部署和真实材料查看分别遵守既有批准门禁。
## 7. 完成条件
@@ -225,3 +236,10 @@
- 本次发布第一次本地命令因包含临时目录递归清理,被安全护栏在进程启动前拒绝,未生成快照、未连接或改变生产。第二次移除清理动作后,从干净 clone 发布成功;发布后按提交标识和受限临时目录模式删除本轮唯一快照,未保留构建日志。
- 发布后运维对齐:只读检查先发现 automatic backup 三项与 WARCH3 均仍为旧构建身份;复用现有 unit 参数和运行时间刷新后,4 个 service 匹配 `0e44aa7`、4 个 timer enabled/active。新 automatic backup daily 与源端 watchdog 均为 `success/0`。
- 本轮未同步生产 `.env`,未修改权限、审批状态机、企微认证或业务数据,未执行真实审批、财务审核、付款批次、银行回单或付款动作;真人企微 KISS 页面验收仍待用户刷新确认。
- 整单材料实现:PC/移动费用明细不再渲染关联发票;只读态以一个“查看全部材料”入口生成有序图片包,企微调用一次 `previewImage`,PC 使用共享材料页连续显示;编辑态和失败降级仍保留逐份原件管理/查看能力。
- 票种显示回归:合成数电发票与电子火车票分别显示正确票种;数电发票保留销售方,火车票使用购买方且不再出现“无代码 / 未识别销售方”;未重跑或改写生产 OCR 数据。
- 后端验证:聚焦 5 个测试文件、23 项通过;服务端全量 171 个文件、1109 项通过,Prisma validate 与 touched-file `node --check` 通过。新增断言覆盖顺序、页/源/输出/缓存上限、token 篡改、缓存缺失、下载理由、访问审计失败和安全票种映射。
- 前端验证:生产 build(含 typecheck)通过;R111 PC/企微 2/2 通过,覆盖费用明细无发票入口、票种文案、统一计数、材料页与一次有序 `previewImage.urls` 调用。
- 目标镜像验证:`server_build` 成功安装 `poppler-utils`;Playwright 生成的无业务合成单页 PDF 经容器内生产 `reimbursementMaterialPreview` 转换为 1 页 PNG,探针结果为 `raster-probe-pass`。未合并、改写或使用真实票据。
- 治理复核:task validate、Trellis governance、process contract、敏感扫描和 `git diff --check` 通过;文档审计 0 error、1 条既有汇总 warning(22 个历史未登记文件)。首次敏感扫描命令误指向不存在的旧路径并立即失败,改用当前 `scripts/qa/check-sensitive-content.mjs` 后 30 个变更文件、0 finding;该失败未执行生产或业务写入。
- 本步骤只完成本地实现与验证;未推送、未部署、未修改生产配置或真实业务数据,不能据此通知真人线上测试。
@@ -43,7 +43,9 @@
- 真实审批继续暴露材料查看阻断:PC 能打开普通附件,但 PC 与移动报销详情均没有发票原件入口,企微 WebView 中异步 `window.open(blob:)` 只下载附件而不能预览。企业微信自建应用内必须优先使用官方 `ww.previewFile` / `wx.previewFile` 原生能力查看 PDF;普通 PC 浏览器保留浏览器预览,两端保持“查看原件”的业务能力一致。
- 生产验收发现移动报销列表固定请求 `scope=own`,导致已有 `expense.view_all` 的管理员无法在企微工作台找到杨俊的报销单并代为验证原生预览。移动端必须与 PC 的范围能力对齐:仅对 `expense.view_all` 用户显示“我的 / 全部”切换,默认仍为“我的”,选择“全部”时复用现有 `scope=all` API;普通员工不显示范围切换,不修改后端权限模型。
- 企业微信原生预览使用准确的文件字节数、带扩展名的文件名和短时签名 URL。原生层不会携带 OA Bearer token 或可靠复用 WebView Cookie,因此附件可以在当前任务内增加仅用于 `previewFile` 的短时 bearer URL 例外;该 URL 必须在签发前执行当前对象级授权和下载理由检查,绑定文件与访问人、短时过期、不得包含长期凭据,并在实际取件时写审计。
- 生产可见性验收通过后发现报销详情仍把每张发票、每个补充附件都暴露为独立“查看原件”按钮,且费用明细与其 `linkedItemId` 发票分区展示,不利于财务逐条核对。PC 与移动端应统一采用“业务关联优先、材料入口收敛”的展示:每条明细直接显示并可预览其关联发票;全部发票和补充附件仍在单一“报销材料”区按类型汇总,文件信息行本身作为预览入口,不新增独立材料浏览路由、轮播器或后端模型。
- 生产可见性验收进一步确认:费用明细内再次展示并预览关联发票仍然重复,审批人希望一次进入后连续查看本单全部材料。PC 与移动端应统一采用“明细只讲费用、材料一次通览”:费用明细不再渲染发票摘要或原件入口;发票和补充附件仍保留各自后台类型、关联、权限和审计,但只在单一“报销材料”区汇总,并提供一个“查看全部材料”动作。
- 企业微信官方 `previewFile({ url, name, size })` 一次只接收一个文件,不具备 `previewImage({ urls })` 的多文件数组和左右切换合同。为满足企微内一次进入后滑动查看全部 PDF/图片,本任务将当前报销材料按稳定顺序栅格化/规范化为短时图片预览包,再调用一次 `previewImage({ current, urls })`;普通 PC 使用同一预览包连续浏览。栅格化失败时必须保留逐份原件预览的降级路径,不能让单个异常文件阻断全部材料核对。
- 生产只读核查确认当前真实报销的 3 张票均已由腾讯 OCR 成功处理:号码、日期和金额齐全,无 OCR 错误;2 张为电子火车票,1 张为数电发票。三者按票种均无传统发票代码,电子火车票也没有销售方字段。页面显示“无代码 / 未识别销售方”属于票种展示错误,不得通过重新调用 OCR、伪造代码或把其他主体字段冒充销售方来补值。报销详情 API 应暴露已持久化 OCR 结果中的安全票种分类,前端按票种显示“电子火车票”或“数电发票”、号码、日期、金额及适用主体字段。
## Scope
@@ -79,8 +81,10 @@
- [x] 移动报销列表仅对具备 `expense.view_all` 的用户显示“我的 / 全部”切换,默认“我的”;切换“全部”后能通过现有列表进入他人报销详情,普通员工始终只能查看本人列表。
- [x] JS-SDK 签名接口只接受本站允许 origin 的无 fragment 页面 URL,不向客户端、日志或审计暴露 JSAPI ticket、企微 Secret 或 access token;签名失败和非企微环境安全降级。
- [x] 发票/附件预览 URL 的过期、篡改、对象不匹配、缺少下载理由和未授权访问均被拒绝;实际文件响应保留审计,用户已确认能在企微查看当前真实报销材料。
- [x] PC 与移动端每条费用明细展示通过 `linkedItemId` 关联的发票摘要,点击该摘要可沿用现有安全预览能力打开原件;未关联发票不错误归入任何明细。
- [x] PC 与移动端将原“发票 / 附件材料”收敛为单一“报销材料”区,显示发票与补充附件计数;材料信息行可点击预览,不再为每份材料重复渲染“查看原件”按钮,同时保留草稿编辑态的添加、移除、上传和删除能力。
- [x] PC 与移动端费用明细不再展示或预览关联发票;`linkedItemId` 数据关系继续保留,仅不在费用明细 UI 重复呈现。
- [x] PC 与移动端单一“报销材料”区显示发票与补充附件计数,并提供一个“查看全部材料”动作;企微调用一次原生 `previewImage` 打开有序图片数组,PC 使用同一图片预览包连续浏览。
- [x] 图片预览包只包含当前有权查看的报销所绑定发票与补充附件,按“发票 → 补充附件、各自稳定创建顺序、PDF 内页序”生成;短时授权、无持久业务副本、访问审计、数量/页数/源文件字节/输出像素和输出字节上限及失败降级均有验证。
- [x] 票据摘要按 OCR `documentCategory` 展示正确票种与适用字段;数电发票和电子火车票不再出现误导性的“无代码”,电子火车票不再出现“未识别销售方”。当前真实票据不重跑 OCR、不改原始识别数据。
## Out of Scope
@@ -104,4 +108,4 @@
- 用合成 `/ready` 响应验证前台恢复时的构建不一致提示、同版本静默和检查失败静默;不依赖生产真机数据。
- 用合成企微 ticket、签名 URL 和 PDF/图片元数据验证:JS-SDK URL 签名、origin allowlist、原生 `previewFile` 参数、PC fallback、审批人对象级只读授权、附件理由门禁、短时 token 过期/篡改拒绝和实际查看审计;不得使用真实发票或附件。
- 用合成管理员与普通员工权限验证移动报销范围:管理员默认请求 `scope=own`,切换后请求 `scope=all` 并显示他人报销;普通员工不显示“全部”且始终请求 `scope=own`。
- 用合成多明细、已关联发票、未关联发票和补充附件验证 PC/移动一致性:关联发票出现在正确明细内;统一材料区计数正确且所有材料仍可预览;页面不再出现逐文件“查看原件”按钮;编辑态管理动作不回归。
- 用合成普通增值税发票、数电发票、电子火车票、PDF 与 JPG/PNG/WebP 补充附件验证 PC/移动一致性:费用明细无发票预览;票种标题不虚构缺失字段;统一材料区计数正确;一次图片预览包包含全部可读材料并保持顺序;异常/加密 PDF、超限和栅格化失败可安全降级;编辑态管理动作不回归。