修复铁路OCR空金额回退(07-28-travel-reimbursement-guide)
这个提交包含在:
@@ -165,6 +165,9 @@ const ocrRan = invoice.ocr_attempts > 0
|
||||
`sellerName`.
|
||||
- `electronic_train_ticket`: `invoiceNumber`, `invoiceDate`, `totalAmount`,
|
||||
`buyerName`.
|
||||
- Railway `totalAmount` uses the first individually parseable value from
|
||||
Tencent `Total`, then `Fare`. Parse each field before applying fallback:
|
||||
an empty or otherwise unparseable `Total` must not block a parseable `Fare`.
|
||||
- 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.
|
||||
@@ -225,6 +228,9 @@ const ocrRan = invoice.ocr_attempts > 0
|
||||
- OCR service/job tests assert category-specific required fields,
|
||||
number-only dedupe only for known no-code categories, and no retry on terminal
|
||||
errors.
|
||||
- Railway adapter tests must cover an empty `Total` with a parseable `Fare` and
|
||||
assert that normalized `totalAmount` uses `Fare` without emitting the source
|
||||
amount in diagnostics.
|
||||
- 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.
|
||||
@@ -251,6 +257,16 @@ const persisted = sanitizeOcrResultForPersistence("tencent", normalized);
|
||||
await prisma.ocrResult.create({ data: { result_json: persisted } });
|
||||
```
|
||||
|
||||
For railway amount fallback, do not choose the raw source before parsing:
|
||||
|
||||
```js
|
||||
// Wrong: an empty string is not nullish, so it blocks Fare.
|
||||
toNumberString(info?.Total ?? info?.Fare);
|
||||
|
||||
// Correct: only a successfully parsed Total blocks Fare.
|
||||
toNumberString(info?.Total) ?? toNumberString(info?.Fare);
|
||||
```
|
||||
|
||||
## Scenario: Live OCR Sample Stdin Probe
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
- [x] 修正 live 样票脚本:复用 `RecognizeGeneralInvoice`、按票种必填字段和安全持久化白名单,支持单 PDF 标准输入且仅输出脱敏摘要。
|
||||
- [x] 使用合成 PDF/provider 响应覆盖住宿、铁路、非 PDF、超限、缺字段和原始响应不输出,并完成本地门禁、提交和生产部署。
|
||||
- [x] 为铁路 `Total`/`Fare` 增加只含存在、类型、非空和可解析布尔值的临时诊断;证明持久化白名单丢弃诊断且 live runner 不输出真实值。
|
||||
- [ ] 完成本地门禁、提交和生产部署后,使用剩余一次授权诊断铁路金额字段;铁路未 PASS 时仍不调用住宿样票。
|
||||
- [x] 完成本地门禁、提交和生产部署后,使用剩余一次授权诊断铁路金额字段;铁路未 PASS 时仍不调用住宿样票。
|
||||
- [ ] 修正铁路空 `Total` 阻断可解析 `Fare` 回退的问题,增加回归测试并完成提交、生产部署与运行身份复核。
|
||||
- [ ] 经新增授权复验铁路票;仅在铁路 PASS 后调用住宿票,总计最多两次腾讯 OCR 调用。
|
||||
- [ ] 经单独样票批准后分别运行住宿电子普通发票和铁路电子客票 live OCR PDF 验收。
|
||||
- [ ] 只保存脱敏状态、耗时、provider 和字段命中摘要;确认不存在 mock 调用证据。
|
||||
- [ ] 按正常 API/业务生命周期清理或归档测试记录,并执行只读残留检查。
|
||||
@@ -99,6 +101,11 @@
|
||||
- 聚焦测试 19 项、完整后端 168 个文件/1075 项、Prisma validate、前端 typecheck/build、流程合同、Trellis governance、敏感内容和 diff 门禁通过;仅有既有依赖弃用提示与 22 个既有未注册文档 warning。
|
||||
- P2 聚合器的后端聚焦测试 94 项及前端门禁通过;其业务流因合成账号首次改密状态失败,live-readiness 因本地旧凭据和部署前构建标识不一致失败。进入 live OCR 步骤时已主动终止,生产最近 10 分钟新增发票和 OCR 尝试记录均为 0。
|
||||
- 诊断只存在于 provider 返回到 live runner 的进程内对象,持久化 sanitizer 丢弃整个 diagnostics;合成敏感标记未出现在序列化摘要中。
|
||||
- 诊断提交 `5f2788c` 已部署;公网构建、external deps、runtime smoke、单一 job executor、四个 timer、自动备份和新鲜度检查均通过。
|
||||
- 最后一次授权调用确认腾讯返回的铁路 `Total` 是空字符串,而 `Fare` 非空且可被当前解析器接受。现有 `info?.Total ?? info?.Fare` 在解析前选择空字符串,阻断了 `Fare` 回退;根因是适配器空字符串回退顺序,不是 PDF、凭证、网络或腾讯票种识别失败。
|
||||
- 本次仍为 `HOLD/missing_required_fields:totalAmount`,住宿样票未调用;所有既有真实 OCR 调用授权已用尽,修复后复验必须重新取得授权。
|
||||
- 已将铁路金额改为分别解析 `Total` 与 `Fare` 后回退,并增加“空 `Total` + 可解析 `Fare`”回归测试;聚焦测试 10 项、Prisma validate、流程合同、Trellis governance、敏感内容和 task validate 均通过。
|
||||
- 已在现有 OCR code-spec 固化同类字段回退规则和测试要求;本轮尚未提交、部署或调用腾讯 OCR。
|
||||
|
||||
## 风险文件与操作
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户