实现腾讯通用票据OCR安全适配(07-28-travel-reimbursement-guide)

这个提交包含在:
2026-07-28 22:02:11 +08:00
父节点 b4bd484260
当前提交 ac17092fa0
共修改 19 个文件,包含 1277 行新增和 127 行删除
@@ -121,6 +121,136 @@ const ocrRan = invoice.ocr_attempts > 0
|| invoice.ocr_result_id != null;
```
## Scenario: Tencent General Invoice OCR Safe Persistence
### 1. Scope / Trigger
- Trigger: production uses Tencent OCR for employee-uploaded PDF invoices,
including VAT/electronic ordinary invoices and railway electronic tickets.
- Use the single `RecognizeGeneralInvoice` entry point. Do not split railway
tickets onto the legacy train-ticket API or fall back to synthetic OCR.
- This contract applies to upload limits, provider requests, normalization,
required-field validation, duplicate detection, cache reuse, retry behavior
and database persistence.
### 2. Signatures
- Runtime env: `OCR_PROVIDER=tencent`, `TENCENT_SECRET_ID`,
`TENCENT_SECRET_KEY`, `TENCENT_OCR_REGION` and `OCR_TIMEOUT_MS`.
- Upload: `POST /api/invoice-pool/upload` with one
`application/pdf` multipart `file`, at most `7 * 1024 * 1024` bytes.
- Provider call:
`runTencentGeneralInvoiceOcr(config, { imageBase64, sourceMimeType })` calls
`client.RecognizeGeneralInvoice(request)`.
- Persistence boundary:
`sanitizeOcrResultForPersistence(ocrProvider, normalized)` must run before
every Tencent `OcrResult.result_json` create, cache return or cache repair.
- Duplicate boundary:
`shouldDeduplicateByInvoiceNumberOnly(normalized)` is true only for a known
no-code category with a non-empty invoice number.
### 3. Contracts
- Each PDF contains one invoice. Tencent receives page 1 only with
`EnablePdf=true`, `PdfPageNumber=1`, `EnableMultiplePage=false`,
`EnableCutImage=false`, `EnableItemPolygon=false`, `EnableQRCode=false`,
`EnableSeal=false` and `EnableOther=false`.
- Supported categories are `vat_invoice`, `full_vat_invoice` and
`electronic_train_ticket` (`ElectronicTrainTicketFull`). Other subtypes and
multiple detected invoices fail closed.
- Required normalized fields:
- `vat_invoice`: `invoiceCode`, `invoiceNumber`, `invoiceDate`,
`totalAmount`, `sellerName`.
- `full_vat_invoice`: `invoiceNumber`, `invoiceDate`, `totalAmount`,
`sellerName`.
- `electronic_train_ticket`: `invoiceNumber`, `invoiceDate`, `totalAmount`,
`buyerName`.
- Never invent an invoice code from an invoice number or vice versa. Known
no-code full-VAT and railway tickets use invoice-number-only duplicate
detection; other categories retain code-plus-number semantics.
- Tencent persistence is allow-list only: document subtype/category/count,
normalized invoice identifiers/date/amounts/buyer/seller, approved quality
error and `pdf_native` transport mode. Never persist the raw provider
response, passenger name, identity number, QR/seal data or full ticket fields.
- Existing Tencent cache rows are not trusted as already safe. Every cache hit,
including the unique-key race path, is re-sanitized and written back before
use.
- OCR remains assistive. Finance must compare the reimbursement item, source
PDF and required payment evidence; recognition success is not automatic
approval.
### 4. Validation & Error Matrix
- Non-PDF source -> terminal `unsupported_source_mime_type`; do not retry.
- PDF larger than 7 MiB -> upload rejects with
`PDF 发票不能超过 7 MiB`; do not enqueue OCR.
- No invoice / more than one invoice / unsupported subtype / provider item not
`OK` -> `invoice_not_detected` /
`multiple_invoices_not_supported` / `unsupported_invoice_subtype` /
`invoice_recognition_failed`; mark OCR failed without inventing fields.
- Missing per-category fields ->
`missing_required_fields:<comma-separated-fields>`; mark OCR failed.
- `AuthFailure*`, `UnauthorizedOperation*`, `InvalidParameter*`,
`InvalidParameterValue*`, `FailedOperation.UnOpenError`,
`LimitExceeded.TooLargeFileError`,
`ResourceUnavailable.ResourcePackageRunOut`,
`ResourceUnavailable.InArrears` and
`ResourcesSoldOut.ChargeStatusException` -> terminal failure; do not consume
retry attempts blindly or switch to a billable/ mock provider.
- A transient provider/network error not in the terminal allowlist -> retain the
bounded existing retry policy; never expose credentials or raw responses in
the user-facing error.
### 5. Good/Base/Bad Cases
- Good: one railway electronic ticket PDF under 7 MiB yields
`electronic_train_ticket`, a blank code, a valid number/date/total/buyer and
participates in number-only duplicate detection; persisted JSON contains no
passenger identity fields or raw response.
- Base: one supported VAT/electronic ordinary invoice yields its required
category fields and follows the normal code-plus-number or known no-code
duplicate rule.
- Bad: send a multi-invoice PDF, parse only the first detected invoice, copy
number into code, persist `raw`, or retry after the free resource package is
exhausted.
### 6. Tests Required
- Adapter unit tests assert the exact `RecognizeGeneralInvoice` request flags,
PDF-only transport and normalization for VAT, full VAT and
`ElectronicTrainTicketFull`.
- Adapter error tests assert terminal classification for credentials,
authorization, invalid input, oversized input, service-not-open,
arrears/charge state and free-package exhaustion.
- OCR service/job tests assert category-specific required fields,
number-only dedupe only for known no-code categories, and no retry on terminal
errors.
- Cache tests seed a legacy Tencent result containing `raw` and sensitive
fields, then assert both normal and race cache paths return and persist only
the allow-listed shape.
- Upload route tests assert exactly 7 MiB remains accepted and any larger PDF
is rejected before OCR enqueue.
- Full backend regression, Prisma validation, sensitive-content check and
Trellis governance check remain required before commit.
### 7. Wrong vs Correct
#### Wrong
```js
const result = await client.VatInvoiceOCR({ ImageBase64: imageBase64 });
await prisma.ocrResult.create({ data: { result_json: { ...result, raw: result } } });
```
#### Correct
```js
const result = await client.RecognizeGeneralInvoice(buildGeneralInvoiceRequest(imageBase64));
const normalized = normalizeGeneralInvoiceResult(result);
const persisted = sanitizeOcrResultForPersistence("tencent", normalized);
await prisma.ocrResult.create({ data: { result_json: persisted } });
```
## Scenario: Payment Batch Receipt Attention Count
### 1. Scope / Trigger
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,53 @@
# 技术设计
## 边界
本任务不开发新的 OCR provider,也不开放图片发票或改变审批流。在现有腾讯 provider 内把统一入口从 `VatInvoiceOCR` 改为 `RecognizeGeneralInvoice`,增加票种感知的安全归一化和上传大小门禁,再完成生产配置恢复、真实链路验证及两份既有业务文档更新。
生产配置写入、容器重建、腾讯 API 调用和真实样票上传均属于单独受控步骤;每一步只在前一步通过后继续。
## 配置与运行拓扑
- 生产环境文件继续作为 OCR 配置事实源。
- 凭证采用 OA OCR 专用的最小权限子用户密钥;不使用主账号永久密钥,不从聊天、仓库、日志、命令参数或历史残留复制。
- 用户在现场终端静默输入凭证。自动化只检查 provider、必要字段是否非空、文件权限和配置 smoke,不回显值或长度。
- 当前仅 server 执行 jobs,因此只重建 server,不启动 worker;重建后再次确认只有一个 OCR job executor。
- provider 从 `mock` 直接切换为 `tencent` 只能发生在服务开通、权限、计费状态和密钥均确认后。任一前置失败时保持禁止真实票据上传,不用 mock 代替真实能力。
- 计费只使用 `RecognizeGeneralInvoice` 对应的通用票据识别(高级版)每月 1,000 次专用免费包,不接独立火车票接口。生产固定中国大陆地域;控制台确认免费包已到账、后付费关闭,并配置费用或余额告警。免费包耗尽时服务失败关闭,应用记录额度类可解释终态,不自动付费续用。
## 腾讯 API 合同
- API:`RecognizeGeneralInvoice`,版本 `2018-11-19`,由当前腾讯 Node SDK 调用。
- PDF 请求:Base64、第一页、关闭多页、切图、坐标、二维码和印章返回。
- 一份 PDF 只允许解析出一张受支持票据;零张、多张、未知或不支持子类型进入明确失败状态。
- 支持子类型按两族归一化:住宿 VAT/电子普通发票;电子发票(铁路电子客票)。
- 质量门禁按 `documentSubtype` 选择必填字段;无代码电子票据不再伪造发票代码。
- `OcrResult.result_json` 仅保存规范化业务字段和安全元数据,不保存 provider 原始响应或乘车人身份字段。
- PDF 原文件上限为 7MiB,使 Base64 后保持在腾讯 10M 上限内;大小错误在上传阶段阻断。
## 验证数据流
1. 用户在腾讯云控制台确认通用票据识别(高级版)每月免费包已到账、中国大陆地域、后付费关闭和费用/余额告警。
2. 用户创建或轮换 OA OCR 专用子用户密钥,并确认 OCR 服务可用。
3. 用户在现场终端把 provider、密钥和地域写入生产受控环境文件。
4. 重建 server,使新配置进入实际运行进程。
5. 运行外部依赖 smoke、运行时 smoke、`/ready`、构建标识和 job 拓扑检查。
6. 经样票单独授权后,通过现有 PDF 发票入口分别上传住宿电子普通发票和铁路电子客票样票。
7. 只记录 provider、状态、尝试/完成证据和脱敏字段命中摘要;不记录票面、完整字段或原始 payload。
8. 正常清理或归档测试业务记录,再允许杨俊提交真实票据。
## 文档口径
- `docs/员工快速上手.md` 增加一页式差旅报销步骤:火车票和住宿费拆明细,官方 PDF 绑定对应明细,两类费用均附支付截图,金额差异必须说明,提交后进入主管和财务流程。
- `docs/报销发票附件规则.md` 固化支付证明必交、敏感字段遮挡、退款/代付/优惠差异和无截图例外口径。
- OCR 结果仅辅助识别和查重;财务仍核对明细、PDF 原件和个人支付证明。
## 失败与回退
- adapter 聚焦测试、配置 smoke、腾讯调用或任一真实样票验证失败:停止开放真实票据,不继续制造 pending/retry 数据。
- 若新配置导致运行异常,恢复变更前环境文件的受控副本并重建同一 server;回退后不得使用 mock 处理真实票据。
- 样票失败记录使用可解释状态和脱敏摘要,不直接修改数据库字段绕过状态机。
## 历史凭证结论
2026 年 5 月的证据能证明真实腾讯 OCR 曾可用,但不能证明当时密钥仍有效或满足当前最小权限。当前生产机、历史容器、同机旧环境文件、开发机环境文件及 shell 历史均无可恢复凭证;数据库/附件备份也不包含环境密钥。因此本任务不设计“从旧部署复制密钥”的路径。
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,64 @@
# 实施计划
## 1. 外部前置门禁
- [x] 用户确认采用 OA OCR 专用最小权限子用户密钥。
- [ ] 用户在腾讯云侧创建或轮换该专用密钥。
- [ ] 在实际账号确认通用票据识别(高级版)每月 1,000 次专用免费包已到账,生产使用中国大陆地域。
- [ ] 在 OCR 控制台设置页确认后付费关闭,并配置费用或余额告警;额度耗尽时接受 OCR 失败关闭。
- [ ] 确认腾讯 OCR 服务和 `ocr:RecognizeGeneralInvoice` 最小权限可用,不接独立火车票接口。
- [ ] 确认住宿电子普通发票和铁路电子客票各一份 PDF 样票的测试授权及仓库外受控位置;不得向 Codex 或聊天提供票据。
## 2. 腾讯 adapter 修正
- [x] 将现有腾讯调用改为 `RecognizeGeneralInvoice`,固定单页 PDF 和最小返回选项。
- [x] 分别归一化住宿 VAT/电子普通发票和 `ElectronicTrainTicketFull`。
- [x] 按安全票种标识执行必填字段检查;删除代码/号码互相伪回填。
- [x] 不持久化腾讯原始响应、乘车人姓名、证件号或完整票面字段;命中旧版腾讯缓存时按同一白名单压缩。
- [x] 发票上传增加 7MiB OCR 输入上限和明确错误。
- [x] 增加两类票种、无代码、多票/不支持票据、敏感字段、大小门禁和 provider 错误聚焦测试。
## 3. 生产配置
- [ ] 对当前生产环境文件制作受控、限权、短期回退副本,不输出内容。
- [ ] 由用户在现场终端静默写入 `OCR_PROVIDER=tencent` 及专用凭证;地域沿用核验后的配置。
- [ ] 脱敏检查 provider、凭证非空、环境文件权限和配置合同。
- [ ] 仅重建实际执行 jobs 的 server,不启动独立 worker。
回退点:外部依赖 smoke 前若任一配置门禁失败,恢复变更前环境文件并重建 server;保持真实票据禁止上传。
## 4. 生产与真实能力验证
- [ ] 检查公网 `/ready` 和预期构建标识。
- [ ] 在 server 内运行现有 external deps 和运行时 smoke,不输出 Secret 或原始响应。
- [ ] 确认实际 provider 为 `tencent`,且只有一个 OCR job executor。
- [ ] 经单独样票批准后分别运行住宿电子普通发票和铁路电子客票 live OCR PDF 验收。
- [ ] 只保存脱敏状态、耗时、provider 和字段命中摘要;确认不存在 mock 调用证据。
- [ ] 按正常 API/业务生命周期清理或归档测试记录,并执行只读残留检查。
回退点:真实调用失败时停止样票和真实票据上传,保留可解释失败摘要;不得改库伪造成功或切回 mock 处理真实票据。
## 5. 文档更新
- [x] 更新 `docs/员工快速上手.md` 的差旅报销一页指南。
- [x] 更新 `docs/报销发票附件规则.md` 的支付截图、实报实销差异、退款/代付和人工复核规则。
- [x] 不新建平行员工指南,不写入真实票据、支付信息、账号或环境路径。
## 6. 验证与审查
- [x] 静态核对页面/API 上传格式、明细绑定和员工/财务字段边界。
- [x] 运行腾讯 adapter、OCR service/job 和发票上传聚焦测试。
- [x] `cd server && npm run prisma:validate`
- [x] `node scripts/qa/check-process-contract.mjs --baseline --json`
- [x] `node scripts/qa/audit-docs.mjs --check-registry`(0 个错误;22 个既有未注册文档 warning,本任务未新增)
- [x] `node scripts/qa/check-trellis-governance.mjs`
- [x] `git diff --check`
- [ ] 对生产配置、样票证据和文档口径进行只读复核。
- [ ] 部署与公网检查均通过后,才通知杨俊可以提交真实差旅报销。
## 风险文件与操作
- 生产环境文件:包含 Secret,只允许用户现场静默输入;不得由补丁、聊天或命令参数传入。
- server 重建:短暂影响 OA;仅操作当前实际 server,保留回退配置。
- 真实样票:包含发票和个人/企业信息;只留在仓库外受控位置。
- 文档:员工指南必须短且与生产费用类型、PDF-only 和附件入口一致,避免把财务付款回单误写成员工材料。
@@ -0,0 +1,80 @@
# 差旅报销指南与腾讯 OCR 正式启用
## Goal
先把生产发票识别从 mock 切换到真实腾讯 OCR 并取得授权样票证据,再让杨俊及后续员工一次看懂差旅报销需要提交什么、分别上传到哪里,以及财务如何按“明细—发票—个人支付证明”复核。
## Background
- 当前报销单明细包含费用类型、发生日期、金额和说明;发票绑定到具体明细,补充附件属于整张报销单。
- 发票入口只接受 PDF;补充附件接受 PDF、JPG、PNG、WebP,可用于支付截图、订单和情况说明。
- 员工不填写财务付款凭证号、付款日期或付款渠道;这些字段属于审批通过后的财务处理。
- 生产已有 37 个 active 费用类型,其中“火车票”“住宿费”“机票”“打车费”“差旅餐补”各唯一存在;当前 active 费用标准为 0,不会自动执行金额上限。
- 用户已决定:火车票和住宿费均要求员工提交个人支付截图。
- 生产只读核验发现当前运行配置为 `OCR_PROVIDER=mock`,`TENCENT_SECRET_ID` 和 `TENCENT_SECRET_KEY` 均未配置;server 自身运行 jobs,当前没有独立 worker。
- 2026-05-05 的试运行证据证明生产腾讯 OCR 当时曾通过真实 PDF 验证;但当前生产环境、同机历史环境文件、已停止应用容器、开发机环境文件和 shell 历史均没有可恢复的旧腾讯密钥。现有自动备份只覆盖数据库和附件,不覆盖环境密钥,因此不能从备份恢复该凭证。
- 官方 API 和当前实现复核发现:现有腾讯 adapter 调用 `VatInvoiceOCR`,适合住宿增值税/电子普通发票,但不是统一覆盖电子发票(铁路电子客票)的推荐接口。官方 `RecognizeGeneralInvoice` 同时覆盖住宿常见 VAT 子类型和 `ElectronicTrainTicketFull`,当前锁定 SDK 已支持该方法。
- 当前实现会对所有票据强制要求发票代码和销售方,并在代码/号码缺失时互相回填;铁路电子客票及部分数电票会被误判失败或污染查重语义。现有 `raw` 结果还会持久化腾讯完整响应,铁路票返回可能包含乘车人证件信息。
- 腾讯要求文件 Base64 编码后不超过 10M,现有 OA 允许 10MiB 原始 PDF,存在上传成功后 OCR 必然超限的区间。
- 用户决定不接受 `off` 人工模式,要求配置并正式启用真实 OCR;在凭证配置和授权样票验证通过前,杨俊不得上传真实发票。
- 用户已确认采用推荐的凭证方案:在腾讯云创建或轮换 OA OCR 专用最小权限子用户密钥,并由用户在现场终端静默写入生产。
## Scope
- 核对报销明细、PDF 发票、补充附件、支付截图和财务付款记录的实际页面/API边界。
- 更新 `docs/员工快速上手.md`,增加一页可直接照做的差旅报销指南。
- 更新 `docs/报销发票附件规则.md`,固化火车票/住宿费支付证明、实报实销和 OCR 关闭期间人工核对规则。
- 配置并正式启用生产腾讯 OCR,完成配置门禁、受控发布、公网检查和授权 PDF 样票验证。
- 在现有腾讯 provider 内改用通用票据识别高级版,补齐住宿电子普通发票和铁路电子客票的安全归一化、质量门禁、大小限制和测试。
## Requirements
- 火车票与住宿费必须拆为两条报销明细;每条分别填写费用类型、发生日期、实际合规金额和事由说明。
- 正式电子票据/发票使用官方 PDF,通过对应明细的“添加发票”入口上传并绑定;截图不得冒充正式发票。
- 火车票和住宿费的个人支付截图通过“附件材料/补充附件”上传,并用文件名或说明明确对应哪条明细。
- 支付截图仅保留商户、金额、时间和支付成功状态;银行卡号、支付账号、订单号等无关敏感字段应遮挡。
- 报销金额以个人实际支付的合规金额为准,原则上不得超过有效发票金额;优惠券、多人合并开票、部分自费或其他差异必须在明细中说明。
- 已退款、公司直接支付或他人承担的金额不得重复报销;确实无法提供支付截图时写情况说明,由财务决定退回或例外接受,不得伪造凭证。
- 腾讯 OCR 识别字段用于匹配、查重和财务复核,但不替代支付真实性、费用合理性或发票法律状态的人工判断;财务仍核对明细金额、PDF 原件和支付证明。
- 腾讯 provider 使用 `RecognizeGeneralInvoice`,每份 PDF 只识别第一页且只接受一张受支持票据;不请求切图、字段坐标、二维码或印章数据。
- 住宿 VAT/电子普通发票与铁路电子客票按各自官方返回结构归一化;铁路电子客票不强制要求不存在的发票代码或销售方,不得用发票号码伪造代码。
- 持久化结果只保留业务需要的规范化字段、安全票种标识和传输模式,不保存腾讯原始响应、乘车人姓名、证件号或完整票面字段。
- 正式 PDF 发票增加不高于 7MiB 的 OCR 输入门禁,避免 Base64 后超过腾讯 10M 限制;超限在上传阶段给出明确提示,不进入重试队列。
- 腾讯云凭证使用最小权限子用户或专用身份,不使用主账号永久密钥;凭证只通过用户现场终端写入生产受控环境文件,不进入聊天、命令参数、日志、仓库或 Trellis。
- 不复用来源、权限或有效性无法确认的历史密钥;优先在腾讯云侧为 OA OCR 创建或轮换专用子用户密钥,并撤销确认不再使用的旧密钥。
- 正式启用前需确认 OCR 服务已开通、调用费用/免费额度和告警边界;无凭证、权限不足、欠费或 API 未开通均不得把 provider 切成 `tencent`。
- 只使用 `RecognizeGeneralInvoice` 的通用票据识别(高级版)每月 1,000 次专用免费额度,不接仅老客户续费的独立火车票接口;生产使用中国大陆地域,必须在实际账号确认免费包到账、关闭后付费并配置费用或余额告警。额度耗尽时允许 OCR 明确失败,不得自动产生费用。
- 当前无独立 worker 且 server 自身运行 jobs,配置变更只重建实际加载配置的 server;不得额外启动第二个 OCR job executor。
- 真实能力必须使用已授权 PDF 样票验证;缺少授权样票只能证明配置齐全,不能宣称真实 OCR 已通过。
- 指南明确审批链:员工提交、直属主管审批、财务复核;财务付款和回单不由员工预填。
- 文档不放真实票据、支付截图、账号、订单号、银行信息或完整业务数据。
## Acceptance Criteria
- [ ] 员工指南能在一页内回答“交什么、传到哪里、金额怎么填、哪些材料不能混传、提交后谁处理”。
- [ ] 指南内容与当前页面、服务端格式限制及生产费用类型一致。
- [ ] 业务事实源明确火车票和住宿费支付截图为必交补充材料,并包含遮挡、差异、退款和无截图例外规则。
- [ ] 腾讯 adapter 使用 `RecognizeGeneralInvoice`,聚焦测试覆盖住宿电子普通发票、铁路电子客票、无代码票据、多票/不支持票据、大小门禁和敏感原始字段不持久化。
- [ ] 腾讯云控制台确认通用票据识别(高级版)每月 1,000 次免费包已到账、中国大陆地域、后付费关闭和费用/余额告警可用;额度耗尽不产生后付费。
- [ ] 生产实际 provider 为 `tencent`,只授予 `ocr:RecognizeGeneralInvoice`,两个凭证字段已配置但不泄露,external deps smoke、`/ready` 和运行 job 拓扑通过。
- [ ] 住宿电子普通发票和铁路电子客票各一份已授权 PDF 经真实腾讯 OCR 进入可解释终态,并证明没有调用 mock;样票和原始识别 payload 不进入仓库或聊天。
- [ ] 明确 OCR 只辅助字段识别/查重,财务仍执行明细—PDF 原件—支付证明复核。
- [ ] 在真实 OCR 验收完成前不得仅因指南已完成就通知杨俊可以提交。
- [ ] 全程不把真实票据、支付截图、完整账号、银行资料、Secret 或原始 payload 写入仓库、任务或聊天。
## Out of Scope
- 新增第二个 OCR provider、开放图片发票、自动验真或增加人工发票字段。
- 本轮不做通用发票平台或覆盖全部票种;仅支持现有住宿 VAT/电子普通发票和电子发票(铁路电子客票)主路径。
- 修改报销页面、上传格式、审批规则、费用标准或费用类型。
- 代替杨俊上传票据、创建报销、提交审批,或代替主管/财务处理。
- 制定覆盖所有费用类别的完整财务制度;本轮只收口首笔差旅的火车票和住宿费。
## Test Plan
- 静态核对前后端上传格式、明细绑定、员工/财务字段边界。
- 运行腾讯 adapter 和 OCR job 聚焦单测,覆盖两类票种、缺字段、错误分类、大小限制和敏感字段最小化。
- 生产只读核对费用类型、OCR provider 和必要运行状态,仅输出类别与计数。
- 运行文档注册、敏感内容、流程合同、Trellis governance 和 `git diff --check`。
- 启用真实 OCR 后验证 `/ready`、构建标识、运行 provider=`tencent`、单一 job executor 和配置 smoke。
- 经单独样票批准后运行现有 live OCR 验收,输出只保留状态、耗时和脱敏字段命中摘要,并按正常业务/API 生命周期清理或归档测试记录。
@@ -0,0 +1,88 @@
# 腾讯 OCR API 调研
调研日期:2026-07-28。仅查阅腾讯云官方文档、当前锁定 SDK 和本地代码;未创建密钥、未调用付费 API、未上传票据。
## 官方资料
- 通用票据识别(高级版):https://cloud.tencent.com/document/api/866/90802
- 增值税发票识别:https://cloud.tencent.com/document/api/866/36210
- OCR 控制台访问管理:https://cloud.tencent.com/document/product/866/102912
- 免费额度:https://cloud.tencent.com/document/product/866/35945
- 计费概述:https://cloud.tencent.com/document/product/866/17619
- 混贴票据计费说明:https://cloud.tencent.com/document/product/866/48522
- 错误码计费说明:https://cloud.tencent.com/document/product/866/45470
- 新手常见问题:https://cloud.tencent.com/document/product/866/43818
- 欠费说明:https://cloud.tencent.com/document/product/866/30576
## 结论
### 1. 当前接口不适合作为两类差旅票据的统一入口
当前代码调用 `VatInvoiceOCR`。官方说明明确覆盖增值税专票、普票、电子专票、电子普票和数电普通/专用发票,适合住宿电子普通发票;但官方用于统一识别火车票、电子发票(铁路电子客票)和增值税发票的是 `RecognizeGeneralInvoice`。
`RecognizeGeneralInvoice` 明确返回 `ElectronicTrainTicketFull`,同时覆盖 `VatElectronicCommonInvoice`、`VatElectronicInvoiceFull` 等住宿发票常见子类型。因此本轮应在现有腾讯 provider 内改用该接口,而不是直接给当前 `VatInvoiceOCR` 配密钥。
### 2. 请求合同
- API 版本仍为 `2018-11-19`,请求域名为 `ocr.tencentcloudapi.com`。
- 当前锁定的 Node SDK 已包含 `RecognizeGeneralInvoice`;无需为了该方法先升级依赖。
- PDF 通过 Base64 传输,不需要公开文件 URL。
- Base64 编码后不得超过 10M;为避免 OA 的 10MiB 原文件上限在编码后超限,正式发票应增加不高于 7MiB 的 OCR 输入门禁。
- 本系统一份 PDF 对应一张发票,只识别第一页:`EnablePdf=true`、`PdfPageNumber=1`、`EnableMultiplePage=false`。
- 不请求切图、字段坐标、二维码或印章返回,避免无用数据和额外敏感内容。
- 默认接口频率为 5 次/秒;当前 job 串行处理,远低于该上限。
### 3. 返回结构和业务字段
- 住宿增值税/电子普通发票:从对应 VAT 子类型归一化发票代码(存在时)、号码、日期、不含税金额、税额、价税合计、销售方和购买方。
- 电子发票(铁路电子客票):从 `ElectronicTrainTicketFull` 归一化发票号码、开票日期、价税合计、税额和购买方;该票种可以没有发票代码和销售方,不能继续套用现有 VAT 五字段必填规则。
- 归一化结果需要保留安全的 `documentSubtype`,用它选择必填字段和脱敏处理;不得把乘车人姓名、证件号、完整票面字段或腾讯原始响应写入 `OcrResult.result_json`。
- 电子票据没有发票代码时,不再把发票号码复制到代码字段。文件 SHA-256 查重继续生效;号码级查重需要按已知无代码子类型做显式规则,不能依赖伪造代码绕过现有复合键。
### 4. CAM 最小权限
腾讯官方确认 OCR 支持接口级自定义授权,接口级授权的 `resource` 必须为 `*`。运行身份只需:
```json
{
"version": "2.0",
"statement": [
{
"effect": "allow",
"action": ["ocr:RecognizeGeneralInvoice"],
"resource": ["*"]
}
]
}
```
运行子用户不需要 `QcloudOCRFullAccess`,也不需要开通/关闭服务或控制台管理权限。OCR 服务开通、计费设置和告警由管理员在控制台完成。
### 5. 免费接口选择、计费和错误
- 官方免费额度表明确列出:`通用票据识别(高级版)` 使用同名专用资源包,免费额度为每月 1,000 次,当月有效、每月发放。它不是与其他票据接口共用的模糊额度。
- `增值税发票识别` 也有每月 1,000 次专用免费额度,但只能解决住宿常见 VAT/电子普通发票,不能统一覆盖电子发票(铁路电子客票)。
- 独立的 `火车票识别` 位于官方“仅老客户续费相关接口”区域,且增加第二套 API、权限、归一化和错误处理,不适合作为当前新接入方案。
- 因此最省钱且最简单的方案是只接 `RecognizeGeneralInvoice`:一个接口同时覆盖住宿和铁路电子客票,并消耗通用票据识别(高级版)的每月 1,000 次专用免费额度。按当前 8 人规模通常足够,但免费政策不是永久合同,上线前仍必须在实际账号控制台确认免费包已到账。
- 高级版免费额度耗尽后,如已购买预付费包则继续扣包;如开通后付费则可能继续计费。官方当前后付费起价为 0.15 元/次。
- 为确保零费用,生产应使用中国大陆地域,在 OCR 控制台设置页确认后付费关闭;免费额度耗尽后允许 OCR 不可用并进入可解释失败,不为保持识别可用而自动付费。境外地域按国际站后付费计费,不能抵扣国内账号资源包,因此不在本轮方案内。
- 官方 FAQ 说明,2021 年 12 月 14 日前开通 OCR 的客户在资源包耗尽后可能自动开通后付费;无论账号何时开通,都以实际控制台开关状态为准,不能只凭开通日期推断不会收费。
- 通用票据识别高级版按实际识别出的发票张数计费;增值税发票进入大票计费项,铁路电子客票/全电发票进入小票计费项。
- 每份 PDF 必须只含一张票据,使一次正常识别对应一次额度扣减;混贴或多票文件可能按实际识别张数扣减,应用继续拒绝多票结果。
- 免费/付费资源包耗尽且未开后付费时会不可用;应用需把它分类为额度/计费门禁失败,而不是票据质量失败。腾讯云侧同时设置费用或余额告警作为兜底。
- 高级版接口官方错误码已核对:`ResourceUnavailable.ResourcePackageRunOut` 表示资源包耗尽,`ResourceUnavailable.InArrears` 表示欠费,`ResourcesSoldOut.ChargeStatusException` 表示计费状态异常,`FailedOperation.UnOpenError` 表示服务未开通,`LimitExceeded.TooLargeFileError` 表示文件过大。这些状态对同一输入重试没有收益,应用应一次终止并给出配置/额度类提示。
- 大部分失败不计费,但官方明确有少量失败错误也可能计费。应用必须保留有限重试和可解释终态,不能无限重试。
- 需要区别处理未开通、欠费/资源包耗尽、文件过大、图片解码/模糊和一般 OCR 失败;不得把配置或额度错误伪装成票据质量问题。
## 当前实现差距
1. `server/src/services/ocr/tencent.js` 调用 `VatInvoiceOCR`,没有铁路电子客票结构解析。
2. `server/src/services/ocr/index.js` 对所有 `vat` 强制要求代码、号码、日期、金额、销售方;铁路电子客票和部分数电票会被误判失败。
3. 现有归一化会在代码/号码缺失时互相回填,掩盖无代码电子票据并污染复合查重语义。
4. 现有归一化把腾讯完整响应放进 `raw`,随后持久化到 `OcrResult.result_json`;改用高级版后可能保存乘车人证件信息。
5. OA 发票上传使用通用 10MiB 上限,可能产生“上传成功、腾讯因 Base64 超限失败”的死路。
6. 旧版腾讯 `OcrResult.result_json` 可能仍带 `raw`;新 adapter 不再生成还不够,命中历史缓存时也必须先按安全白名单压缩,避免旧格式绕过新隐私边界。
## 推荐
只使用 `RecognizeGeneralInvoice`,不拆成 `VatInvoiceOCR + TrainTicketOCR` 两个接口。先完成腾讯 adapter、票种归一化、按票种质量门禁、敏感响应最小化、7MiB PDF 门禁和聚焦测试;再由用户在实际腾讯云账号确认高级版每月 1,000 次免费包已到账、中国大陆地域、后付费关闭和费用告警,最后创建最小权限密钥并部署。住宿发票和铁路电子客票各用一份已授权 PDF 做真实验收;在此之前不允许真实票据进入当前 `mock` 环境。
@@ -0,0 +1,32 @@
{
"id": "travel-reimbursement-guide",
"name": "travel-reimbursement-guide",
"title": "差旅报销指南与腾讯OCR正式启用",
"description": "配置并验证生产腾讯OCR,随后固化火车票、住宿费发票和支付截图的提交位置、实报实销及财务复核规则。",
"status": "in_progress",
"dev_type": null,
"scope": "生产腾讯OCR启用、授权PDF样票验证及差旅报销员工指南与支付凭证规则",
"package": null,
"priority": "P2",
"creator": "shunli",
"assignee": "shunli",
"createdAt": "2026-07-28",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [
"docs/员工快速上手.md",
"docs/报销发票附件规则.md",
"server/src/routes/reimbursements.js",
"server/src/routes/invoicePool.js",
"client/src/pages/ReimbursementDetailPage.tsx"
],
"notes": "",
"meta": {}
}