实现腾讯通用票据OCR安全适配(07-28-travel-reimbursement-guide)
这个提交包含在:
@@ -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
|
||||
|
||||
在新工单中引用
屏蔽一个用户