实现报销材料连续预览(07-27-yang-jun-reimbursement-readiness)
这个提交包含在:
@@ -576,96 +576,103 @@ reply.header(
|
||||
);
|
||||
```
|
||||
|
||||
## Scenario: Reimbursement Material Presentation And Item Linking
|
||||
## Scenario: Reimbursement Material Image Bundle And Category-Aware Summary
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: changing PC or mobile reimbursement-detail presentation for invoice
|
||||
originals, supporting attachments or their relationship to expense items.
|
||||
- Purpose: let finance reconcile each item with its invoice without exposing a
|
||||
separate technical “open file” button for every material.
|
||||
- Trigger: changing PC/mobile reimbursement material presentation, invoice OCR
|
||||
category labels, PDF rasterization or the continuous-preview grant.
|
||||
- Purpose: keep expense items concise while allowing an approver to enter once
|
||||
and continuously inspect every invoice and supporting attachment.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Existing detail payload: `detail.items[]`, `detail.invoices[]` with
|
||||
`invoice.linkedItemId: string | null`, and `detail.attachments[]`.
|
||||
- Existing preview actions: `openInvoiceMutation` and
|
||||
`handleOpenAttachment`; WeCom continues through `previewFile` and PC through
|
||||
its existing browser-safe path.
|
||||
- Stable UI selectors: `reimbursement-materials` and
|
||||
`mobile-reimbursement-materials`; existing invoice/attachment child test IDs
|
||||
remain available for focused regressions.
|
||||
- Detail payload keeps `invoice.linkedItemId` and adds the safe fields
|
||||
`documentCategory`, `documentSubtype` and `buyerName`; it never exposes the
|
||||
raw OCR provider response.
|
||||
- Bundle issue: `POST /api/reimbursements/:id/materials/preview-bundle`, optional
|
||||
`{ reason }`; response `{ expiresAt, pages: [{ url, materialType,
|
||||
materialIndex, pageNumber }] }`.
|
||||
- Page fetch: `GET /api/reimbursements/:id/materials/preview/:bundleId/:pageIndex?previewToken=...`.
|
||||
- Runtime dependency: production Alpine image contains `poppler-utils`;
|
||||
`reimbursementMaterialPreview` invokes `pdftoppm` with `execFile`, never a shell.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- Present invoice originals and supporting attachments under one user-facing
|
||||
`报销材料` section with separate type labels and counts. Do not merge their
|
||||
database types, API fields, authorization, audit or lifecycle rules.
|
||||
- Within each expense item, render only invoices whose
|
||||
`linkedItemId === item.id`. The linked invoice summary is itself a semantic
|
||||
preview button and shows an invoice identifier, seller and amount.
|
||||
- Keep every invoice, including unlinked or stale-linked invoices, in the
|
||||
material section. Never infer a link from amount, order or list position.
|
||||
- The material information row is the preview target; do not append a repeated
|
||||
visible `查看原件` button to each file. Draft-only add/remove/upload/delete
|
||||
actions remain separate buttons so preview cannot mutate data.
|
||||
- Do not introduce a file-browser route, carousel, bulk download or aggregate
|
||||
backend endpoint for the small-team detail page unless later real use proves
|
||||
that the inline material list is insufficient.
|
||||
- Clickable rows have a descriptive `aria-label`, visible focus, disabled state
|
||||
during open, a chevron affordance and a minimum 44px mobile touch target.
|
||||
- Expense-item UI does not render or preview linked invoices. Preserve
|
||||
`linkedItemId` for finance/audit semantics; presentation must not delete or
|
||||
rewrite the relationship.
|
||||
- The read-only detail shows one `报销材料` count and one `查看全部材料` action.
|
||||
Editable drafts may retain per-record management rows; read-only users see
|
||||
them only when continuous preview fails and the original-file fallback opens.
|
||||
- Material order is invoices by `created_at,id`, then attachments by
|
||||
`created_at,id`, then PDF page order. Never infer order from amount, file name
|
||||
or `linkedItemId`.
|
||||
- PDFs are rasterized separately; images are normalized with `sharp`. Do not
|
||||
merge PDFs, strip their signatures, use PDF.js in the WeCom page, or persist
|
||||
preview images as business attachments.
|
||||
- The in-memory bundle is bounded to 20 materials, 50 MiB source bytes, 40
|
||||
pages, 1800px page width, 40 megapixels per page, 40 MiB bundle output and
|
||||
80 MiB process cache.
|
||||
Poppler gets a fixed DPI, timeout and temporary directory; render one page
|
||||
beyond the limit so an oversized PDF fails instead of being silently cut.
|
||||
- Tokens bind purpose, reimbursement, actor, bundle, page index and expiry.
|
||||
Issue requires current object access plus any attachment download reason;
|
||||
every page access writes audit before bytes and returns `private, no-store`.
|
||||
- Category labels are `vat_invoice -> 增值税发票`,
|
||||
`full_vat_invoice -> 数电发票`, and
|
||||
`electronic_train_ticket -> 电子火车票`. Known no-code categories never show
|
||||
`无代码`; railway tickets may show `buyerName` but never claim a missing seller.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Matching `linkedItemId` -> show the invoice inside that item and in the
|
||||
material section.
|
||||
- `linkedItemId=null` or target item absent -> show only in the material
|
||||
section; do not attach it to the first or amount-matching item.
|
||||
- No invoices and no attachments -> one `暂无报销材料` empty state.
|
||||
- Preview pending -> disable repeated opens; existing preview errors and
|
||||
download-reason behavior remain authoritative.
|
||||
- Read-only approver -> preview rows visible, mutation actions absent.
|
||||
- No materials -> do not issue a bundle; show the existing empty state.
|
||||
- Unsupported/corrupt input, missing Poppler or raster timeout -> stable
|
||||
continuous-preview error and expose authorized original-file fallback.
|
||||
- Material/source/page/output/cache limit -> bounded validation error; never
|
||||
return a partial bundle or silently omit pages.
|
||||
- Missing required reason -> `400`; invalid/expired/tampered/object-mismatched
|
||||
token, absent cache bundle or process restart -> `403` with no bytes.
|
||||
- Bundle-issue audit failure deletes the bundle; page-access audit failure ->
|
||||
`5xx` with no image body.
|
||||
- Unknown or malformed OCR category -> neutral `发票` label; never guess a
|
||||
category or rerun production OCR merely to fix display text.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: a hotel item shows its linked hotel invoice inline, while an unlinked
|
||||
payment screenshot remains under `报销材料` and both originals are previewable.
|
||||
- Base: one item, one linked invoice and no attachment renders one inline
|
||||
invoice summary plus one typed material row.
|
||||
- Bad: flatten invoices away from items, guess links by equal amount, merge all
|
||||
file records into one backend type, or add a `查看原件` button beside every
|
||||
row.
|
||||
- Good: three invoice PDFs and three PNG attachments produce one ordered image
|
||||
URL array; WeCom swipes natively and PC loads the same bounded pages.
|
||||
- Base: one single-page PDF becomes one PNG preview while its original remains
|
||||
unchanged and separately auditable.
|
||||
- Bad: concatenate PDFs, expose OCR raw data, put invoice buttons back into
|
||||
every expense item, or accept the first 40 pages of a 41-page file silently.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Focused PC and WeCom Playwright fixtures contain at least one linked and one
|
||||
unlinked invoice plus one supporting attachment.
|
||||
- Assert the linked invoice appears only in the matching item; the unlinked
|
||||
invoice appears in materials but not in any item.
|
||||
- Assert material counts, absence of exact visible `查看原件` buttons, PC open
|
||||
URL behavior, WeCom `previewFile` name/size/URL, 44px touch target and mobile
|
||||
portrait/landscape overflow.
|
||||
- Preserve editable add/remove/upload/delete regression coverage, then run
|
||||
frontend typecheck/build and Trellis governance gates.
|
||||
- Service tests assert material/page order, expiry, source/page/output/cache
|
||||
limits and absence of partial bundles.
|
||||
- Route tests assert authorization, reason gate, token binding, cache miss,
|
||||
issue/access audit, audit-failure fail-closed behavior and PNG response.
|
||||
- A target-image probe must generate synthetic PDF bytes and prove the built
|
||||
Alpine container returns PNG through the production rasterizer.
|
||||
- PC/WeCom Playwright asserts no invoice entry inside expense items, correct
|
||||
category text, no `无代码/未识别销售方`, ordered `previewImage.urls`, fallback,
|
||||
editable actions and responsive layout. Run full server tests, Prisma
|
||||
validation, frontend typecheck/build and Trellis governance gates.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```tsx
|
||||
detail.invoices.map((invoice) => <Button>查看原件</Button>);
|
||||
const bundlePdf = mergePdfFiles(detail.invoices); // changes originals and adds a second document pipeline
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```tsx
|
||||
detail.invoices
|
||||
.filter((invoice) => invoice.linkedItemId === item.id)
|
||||
.map((invoice) => (
|
||||
<button aria-label={`查看关联发票:${formatInvoiceDisplayTitle(invoice)}`}>
|
||||
{formatInvoiceDisplayTitle(invoice)}
|
||||
</button>
|
||||
));
|
||||
const bundle = await fetchReimbursementMaterialPreviewBundle(reimbursementId);
|
||||
await previewImagesInWecom(bundle.pages.map((page) => page.url));
|
||||
```
|
||||
|
||||
## Scenario: Payment Batch Receipt Attention Count
|
||||
|
||||
@@ -62,8 +62,13 @@ or administration workflow has already migrated.
|
||||
- Reimbursement attachment grant:
|
||||
`POST /api/reimbursements/:id/attachments/:attachmentId/preview-url`, optional
|
||||
body `{ reason }`; response uses the same file metadata shape.
|
||||
- Reimbursement material bundle:
|
||||
`POST /api/reimbursements/:id/materials/preview-bundle`, optional body
|
||||
`{ reason }`; response contains ordered short-lived PNG URLs.
|
||||
- Native call: `previewFile({ url, name, size })`; `name` includes an extension
|
||||
and `size` is the persisted positive byte count.
|
||||
- Multi-image native call: `previewImage({ current, urls })`; `current` is one
|
||||
member of the non-empty ordered `urls` array.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
@@ -81,6 +86,14 @@ or administration workflow has already migrated.
|
||||
- PC keeps its browser/authenticated download behavior. Only a detected WeCom
|
||||
WebView dynamically loads `@wecom/jssdk` and calls `previewFile`; do not send
|
||||
the actual file through a WeCom message or native approval payload.
|
||||
- `previewImage` is the only native continuous-preview path for mixed original
|
||||
PDF/image reimbursement materials: OA first rasterizes each PDF separately
|
||||
into short-lived images. `previewFile` accepts one file only; never pass an
|
||||
invented array to it or merge originals merely to simulate navigation.
|
||||
- JS-SDK is allowed only on authenticated business pages that need a native
|
||||
capability and is dynamically imported behind WeCom detection. The hosted PC
|
||||
login/password/email-code page remains script-free as required by the
|
||||
`Hosted PC WeCom Web Login` scenario below.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
@@ -93,6 +106,9 @@ or administration workflow has already migrated.
|
||||
- Missing required download reason -> `400 { reason: "required" }`.
|
||||
- Expired, tampered, wrong-purpose or object-mismatched file grant -> `403`.
|
||||
- Access audit failure -> `5xx` with no file body.
|
||||
- Empty or inconsistent `previewImage` URL arrays -> frontend stops before the
|
||||
bridge call. Bundle expiry/cache loss -> safe fallback to individually
|
||||
authorized originals; do not broaden token scope or extend it silently.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
@@ -100,6 +116,8 @@ or administration workflow has already migrated.
|
||||
`previewFile`, sees the original in WeCom and receives no edit capability.
|
||||
- Base: a PC user opens the same invoice through the browser and downloads an
|
||||
attachment through the existing authenticated Blob path.
|
||||
- Base: a WeCom approver requests one authorized reimbursement image bundle,
|
||||
then `previewImage` opens the ordered PNG URLs with native swiping.
|
||||
- Bad: expose a long-lived generic `?token=`, trust a caller-supplied size/name,
|
||||
put a provider ticket in the response, or use async `window.open(blob:)` in
|
||||
WeCom and assume desktop popup behavior applies.
|
||||
@@ -114,6 +132,9 @@ or administration workflow has already migrated.
|
||||
- Playwright injects a synthetic `WeixinJSBridge`, asserts `previewFile` receives
|
||||
the exact URL/name/size for invoice and attachment, and asserts no
|
||||
`window.open` call in WeCom. PC assertions keep browser URL and Blob behavior.
|
||||
- Continuous-preview Playwright asserts one `previewImage` call, the exact
|
||||
ordered URL array and no JS-SDK load in a normal PC browser. Backend tests
|
||||
separately prove the page URLs remain object/actor/bundle/index bound.
|
||||
- Production deployment and real-device preview remain separate approvals.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
@@ -131,6 +152,10 @@ window.open(URL.createObjectURL(blob)); // WeCom may block after await
|
||||
authenticated grant issue -> <=120s object-bound URL
|
||||
-> ww.previewFile({ url, nameWithExtension, exactSize })
|
||||
-> audit before file bytes
|
||||
|
||||
authenticated material-bundle issue -> ordered <=120s PNG URLs
|
||||
-> ww.previewImage({ current: urls[0], urls })
|
||||
-> audit each page before image bytes
|
||||
```
|
||||
|
||||
## Scenario: WeCom Member Data Is A Read-Only Source For OA
|
||||
|
||||
@@ -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、超限和栅格化失败可安全降级;编辑态管理动作不回归。
|
||||
|
||||
在新工单中引用
屏蔽一个用户