# 技术设计 ## 边界 本任务不开发新的 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 次专用免费包,不接独立火车票接口。生产固定中国大陆地域;控制台确认免费包已到账、后付费关闭,并配置费用或余额告警。免费包耗尽时服务失败关闭,应用记录额度类可解释终态,不自动付费续用。 ### 生产部署根收敛 - 生产只读核验发现,活动 Compose 的工作目录被错误设置成标准部署根下的 `deploy/trialrun` 子目录,导致仓库再次同步到自身子目录,形成重复的 `deploy/trialrun/deploy/trialrun`。活动 server、环境文件和四个 systemd 运维 timer 均已引用该嵌套拓扑,因此它不是可直接删除的闲置副本。 - 本次发布把部署根收敛回 deploy 脚本约定的标准单层根。迁移窗口开始时先暂停相关 timer,将当前活动根原子改名为时间戳回退目录,再创建干净标准根;不在原目录上用 `rsync --delete` 就地清理,以免同时破坏活动配置与回退证据。 - 从回退目录把生产环境文件复制到标准根的预期相对位置,保持所有者和 `0600` 权限,不读取、打印或经本地中转 Secret。数据库和附件继续使用部署根之外的既有宿主目录,不搬迁、不初始化、不重置。 - 使用同一 Compose project 和已批准提交部署 server;数据库容器与数据卷保持原位,不创建 worker。部署后重新安装自动备份 daily/watchdog/drill-reminder 与 WARCH3 timer,使所有 WorkingDirectory、ExecStart 和环境文件引用只指向标准根。 - 验收包括活动 Compose 工作目录不再重复、环境文件唯一受控、server `/ready` 和构建标识正确、provider 为 `tencent`、仅一个 job executor、四个 timer 启用且各自 dry/status 检查通过。旧根作为短期回退保留,本任务不删除;后续删除需在新备份成功且另行批准后执行。 ## 腾讯 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. 正常清理或归档测试业务记录,再允许杨俊提交真实票据。 ### 授权样票验证工具边界 - 既有 `check-ocr-provider-samples.mjs` 必须与生产 adapter 使用同一 `RecognizeGeneralInvoice` 入口和按票种必填字段规则,不得继续引用已删除的 VAT-only 导出或把铁路电子客票套入 VAT 五字段门禁。 - 真实 PDF 从本地受控、Git 忽略的样票目录按匿名别名选择,经 SSH 标准输入直接流入生产 server 容器内存;不得把原件、临时副本或 manifest 写入远端磁盘、OA 附件存储、数据库、仓库或报告。 - 每个进程只接收一份 PDF,验证文件签名和 7MiB 上限后最多调用一次 `RecognizeGeneralInvoice`。住宿与铁路样票分两次执行,任一失败立即停止,不自动遍历目录中的其他文件。 - 输出只允许匿名别名、provider、票种、安全传输模式、耗时、字段命中布尔值、缺字段名和稳定错误码;不得输出文件名、原始响应、识别值、人员/企业名称、票号、金额或票面内容。 - live runner 的代码、合成测试和生产部署先通过,再单独取得两次真实 API 调用批准;工具可用不等于真实样票已验证。 - 当铁路票已被腾讯识别但金额缺失时,适配器可生成仅供 live runner 使用的临时字段形态诊断:只包含 `Total`/`Fare` 是否存在、是否为字符串或数字、是否非空及能否被当前金额解析器接受的布尔值。诊断不得携带字段值,必须在持久化白名单处被丢弃,普通 OCR job、缓存和数据库均不得保存。 - 字段形态诊断先用合成 provider 响应证明不输出金额或原始 payload,再部署;真实调用仍受单次授权和“铁路未 PASS 即停止住宿”约束。 ## 文档口径 - `docs/员工快速上手.md` 增加一页式差旅报销步骤:火车票和住宿费拆明细,官方 PDF 绑定对应明细,两类费用均附支付截图,金额差异必须说明,提交后进入主管和财务流程。 - `docs/报销发票附件规则.md` 固化支付证明必交、敏感字段遮挡、退款/代付/优惠差异和无截图例外口径。 - OCR 结果仅辅助识别和查重;财务仍核对明细、PDF 原件和个人支付证明。 ## 差旅补助与费用类型 - `travel_bus` 已由差旅行程 `transport_type=bus` 自动映射,业务语义是城际“大巴”;将其显示名收口为“大巴车票”,不得复用为市内交通补助。 - `travel_meal` 保留代码并将显示名收口为“伙食补助费”;新增 `travel_local_transport_allowance`,显示名为“市内交通补助费”。生产当前两类既有费用均无历史报销明细,种子脚本按代码幂等更新名称和新增类型。 - 伙食补助费为 100 元/人/天,市内交通补助费为 80 元/人/天,均按自然日包干且不要求发票;直接承担、重复报销和个人延长天数按 PRD 规则处理。 - 当前 `ReimbursementItem` 没有天数或数量字段,`ExpenseStandard.per_unit=day` 也不会在费用控制中乘以天数。因此本轮不创建 100/80 元 block 或 warn 标准,员工分别新增补助明细,以“天数 × 单价”人工算出总额并在说明写明计算式,由主管和财务复核。 - 自动带出差旅起止日、计算符合条件的补助天数以及按天校验属于后续产品能力,不阻塞本次真实报销。 ## 失败与回退 - adapter 聚焦测试、配置 smoke、腾讯调用或任一真实样票验证失败:停止开放真实票据,不继续制造 pending/retry 数据。 - 若新配置导致运行异常,恢复变更前环境文件的受控副本并重建同一 server;回退后不得使用 mock 处理真实票据。 - 若标准根部署、timer 重装或健康检查失败,停止继续发布,将 Compose 工作目录和 systemd unit 恢复到时间戳回退根并重建原 server;数据库和附件目录不参与回退切换。 - 样票失败记录使用可解释状态和脱敏摘要,不直接修改数据库字段绕过状态机。 ## 历史凭证结论 2026 年 5 月的证据能证明真实腾讯 OCR 曾可用,但不能证明当时密钥仍有效或满足当前最小权限。当前生产机、历史容器、同机旧环境文件、开发机环境文件及 shell 历史均无可恢复凭证;数据库/附件备份也不包含环境密钥。因此本任务不设计“从旧部署复制密钥”的路径。