16 KiB
设计:报销费用口径与票据明细门禁
最新合同:选票建明细与待补分类(2026-09-09,已实施待复核)
当前状态以本节与 implement 顶部为准。下文实施前方案对比、B1/B2 数据模型与硬门禁描述保留为历史设计,不构成本次已实现或已验收声明;尤其 assets 批准依据没有现行硬门禁,旧版“应用回滚仅关闭 flag”不适用于本次可空类型数据。
最小实施与已批准的本地持久化合同
本阶段绑定接口增量:POST /invoice-pool/:id/bind 的报销分支增加互斥的 createItem: true(不能同时传 linkedItemId),明确一票一明细。旧绑定调用保持原合同。金额及开票日期从锁定的票据读取;可靠铁路票种默认火车票类型,其余类型 null。当前 OCR 没有票面项目时说明保存空字符串(数据库列仍非空),不以商户/文件名充当用途;提交前集中要求补说明,普通编辑仍可修正。不把开票日期声称为已核实发生日期。
同单重试仅在权限、参与人、单据可编辑状态、发票归属以及现有 linked_item_id 属于该单全部通过后复用现有绑定,不覆盖已编辑的类型/金额/说明;跨单占用仍拒绝。新动作先锁单再锁票(与提交的顺序保持一致),新创建、占用及整单金额重算在同一事务;旧绑定入口不默认切换。真实并发与死锁重试边界另做隔离验证,不以模拟 Prisma 测试证明锁正确。
真实隔离验证补充:绑定与旧解绑混合并发已复现 500;锁顺序合同扩至所有报销 bind/unbind。绑定按 body 的 reimbursementId 先锁单;解绑先读 linked_reimbursement_id 快照、锁单后锁票,取锁后关联与快照不一致则拒绝 409,不能去锁另一张单继续操作。旧绑定业务语义不改,仅统一取锁顺序;差旅分支不额外引入报销单锁。
批量分类接口:POST /reimbursements/:id/items/classify,请求 { itemIds, expenseTypeId },上限 100 条、ID 去重,先锁单并重验维护权限/状态,再确认所有 ID 属于该单和类型启用,事务内只更新所选明细类型与更新人。保留金额/说明/日期/批准依据/票据;部分无效整批拒绝。实查当前 assets 路径允许无批准依据保存/提交,现有测试对此有明确断言;本次不另造资产硬门禁,也不声称已有硬门禁,保留批准依据与实际提示规则。
- 已实现纯分类函数
suggestInvoiceExpenseType(invoice, expenseTypes):仅从服务端持久化的已支持票种读取依据;铁路电子客票匹配启用的travel_train,其余返回 null,不修改输入、不自动兜底其他类型。已接入新绑定意图与双端页面;真实隔离后端集成和指定样票腾讯 OCR 分层验证已完成,完整证据与未覆盖边界见 implement,不代表已可发布。 - 当前持久化 OCR 不含票面项目,VAT 本身不足以区分住宿、办公或招待;不拿合成酒店名称替代真实分类证据。类型函数将作为新动作的服务端判定输入,不能接收客户端伪造票种。
- 为满足“先生成明细、未知分类之后集中批量补、刷新保留”,候选最小模型变更是仅将
ReimbursementItem.expense_type_id和其 Prisma relation 改为可空,既有分类不回填、不改写;仅草稿/退回可保存待补类型。金额和日期没有可靠值的票仍留待处理,不放宽这两列。 - 未知类型必须在调用费用标准、预算、审批等服务前由提交门禁集中阻断;不得用可选链把它当作免票类型放行。详情与编辑 DTO、PC/移动端、材料导出和批量设类需同时支持待补态。空说明如用于草稿也必须有独立提交前校验,不能填虚构业务用途。
- 单票创建明细与发票占用同事务;批量设类只处理用户指定明细并重验权限、状态、类型启用,保留 assets 批准依据字段,不新增依据必填门禁,不改金额、日期或发票关系。
- 用户已明确批准本项字段调整的本地实施与隔离验证。新增迁移仅 DROP NOT NULL,不回填已有记录;生产迁移与部署仍单独授权。API 中 null 表示明确待补,PATCH 的 undefined 表示保留原值,不得用
??混淆两者。 - 回滚风险:一旦保存了空类型明细,旧应用对非空的依赖可能报错,不能只换旧镜像就宣称安全。发布方案须包含待补数据兼容策略;恢复 NOT NULL 前必须确认不存在空值,不得猜类型回填来完成回滚。
默认一事一单,保留后台明细费用类型,不要求员工逐条手选。选票自动生成可编辑明细;可靠类型自动填,未知类型集中提示并支持对选中的同类明细一次批量设置,不新增关联或确认步骤。既有明细入口只添票,不改写原明细。
批量操作仅作用于用户明确选中的明细,不暗中覆盖整单或其他手改项。后台重验参与人、可编辑状态、启用类型,保留 assets 批准依据字段与实际提示;提交重验剩余缺失信息。刷新或 OCR 回调不得覆盖手改结果,待处理信息不能仅保存在易丢失的前端状态。
不新增必填报销单类别或会计科目;“差旅/日常”等宽泛场景不能充当具体费用类型。餐饮用途等票面无法证明的信息不得自动断言,不填“其他”占位。
历史推进顺序曾撤回“先批准类型可空迁移”的建议;之后按已核定交互批准了上文最小可空方案的本地实施。下文两路径对比仅用于追溯,不再表示当前迁移方案未定,生产授权仍独立。
公开产品调研依据
- 分贝通官方费用报销介绍:https://www.fenbeitong.com/product/rembursement.php ,支持“一张报销单多类核销”“多笔费用一起报销”及“一事一报”。
- 汇联易官方费用管理介绍:https://www.huilianyi.com/products/helios ,支持根据发票信息自动推荐费用类型。
- 证据限于公开说明读取,不是登录实操或行业统计;不据此承诺本项目 OCR 能识别全部业务用途。
2026-09-09 后续设计:用户选票触发自动建明细
这是已确认产品取向的规划增量,替换早先手工关联默认方案;原 B1/B2 设计为已实施历史,不直接恢复其关闭门禁时的旧自动匹配逻辑。用户已批准同范围实施;生产配置、部署与新增迁移风险仍须单独确认。
实施前核查:不完整明细的实际约束(2026-09-09)
- 独立执行副本基于已发布
b87cc55,仅承接本 task 的 PRD/design/implement 增量,未混入付款、退回删除或工作流治理改动。 server/prisma/schema.prisma的ReimbursementItem要求费用类型、发生日期、金额和说明非空;新增明细 API 还要求说明非空字符串。不能仅取消前端选择明细的禁用条件就实现选票建明细。server/src/services/ocr/tencent.js当前提取票号、日期、金额与购销方,不提取票面项目;services/ocr/index.js持久化白名单也不包含票面项目。VAT 票种本身不能区分住宿、办公等业务分类;销售方或文件名不能充当可靠类型依据。旧缓存也不会因新增解析代码自动获得项目字段。routes/invoicePool.js旧自动绑定会复用唯一明细、按金额匹配、选择任意启用类型并以报销日期或当天兜底。新动作不能复用这些猜测。费用类型与说明缺失必须明确处理。- 两条可行路径:A 保持 schema,对无法可靠识别的票在选票面板内只补必要字段,再一次添加;识别完整的票不二次确认。B 支持先生成待补分类的明细,再在报销页编辑补齐,需要迁移及跨层可空兼容,符合“先生成再编辑”的体验但扩大技术范围。
- 推荐 B 的体验方向,但不据此自行批准迁移。第一阶段可只允许费用类型待补;金额或日期没有可靠初值的票继续留在待处理集合,不扩成通用空值草稿引擎。具体迁移、说明初值和提交阻断合同需在范围确认后完善,不能将本建议当作已批准设计。
- 扩展类型可空需一起检查明细新增/编辑/DTO、提交门禁、费用标准/预算/审批消费者、PC/移动端、导出与历史兼容。现有代码直接访问
item.expense_type.requires_invoice,不能只把数据库列改为可空。 - 腾讯 OCR 项目解析如需补充,只保存受限业务必需字段,不保存原始响应,不自动重跑真实历史发票;真实样票调用仍须获准材料与用途。
历史决策点(已被最新裁决替代):曾请求先确认待补类型的草稿迁移,现已撤回该推进顺序,后续按最新分类交互核定实现。
交互与数据边界
- 复用发票池、上传/OCR 队列与报销明细模型。用户入口称“我的发票”,不强制迁移全部导航命名;提前存票与报销内上传进入同一份数据。退出选票面板不删除已入池发票,也不凭上传动作抢占为本单材料。
- 面板区分整单创建明细和指定已有明细两种上下文,不用不同页面串跳。整单默认一票一明细;指定明细多选票只关联,不自动追加金额或改写用户已填内容。添加前显示本次生成条数或目标明细,点击一次即完成,不增加二次关联确认。
- 新票识别成功默认勾选,但必须尊重用户取消选择;轮询/OCR 回调不得重新勾上已取消票或代点添加。池中不属于用户、已报销、被其他单占用的票不进入可添加集合;界面禁用不能替代提交时再校验。
- 返回单据后明细与发票一起展示。用户只补识别缺失、分类不确定等必要字段;开票日期预填标明来源且可修改,不等于业务发生日期。OCR 金额缺失或无法确定可靠初值时,不写 0/今天/虚构类型使测试通过;明确保留待处理票或可表达的不完整草稿,用户就地补齐,不得伪装已成功生成完整明细。
服务端事务与兼容核查
- 优先评估复用/扩展现有绑定事务;新动作必须显式表达“由所选票创建明细”或“加入已有明细”,不能让普通绑定接口根据金额暗猜意图。实施前在本文件补接口形状、字段映射、OCR 金额口径及费用类型判定规则,规则之外不新增分类模型或 LLM 服务。
- 权威 OCR 字段从服务端读取;客户端不能传伪造票面金额绕过票据校验。每票创建明细和占用关联在同一事务完成,使用既有锁/唯一约束防双击、超时重试及跨单竞态。批量按票返回成功/失败,单票不产生半成品孤立明细;只有失败项可重试,成功项保持幂等且不再次 OCR。
- 如果现有 schema 的必填项不支持必要的待补状态,先说明候选实现/迁移影响再交审;不得隐式填占位业务数据。不为轻量流程搭建通用草稿引擎。
- 历史未关联材料保留异常修复入口,不批量重建现有明细或关联;提交/删除与添加并发必须重新锁定校验单据状态。与 09-04 删除阶段共用一致的发票释放合同,不能让已删除单据通过旧链接恢复占用。
- 既有 feature flag 的金额/缺票/权限门禁保持;新建明细来自用户明确选票动作,不通过关 gate 获得功能。OCR off 的历史兼容实现不是本次交付成功标准,生产配置修复单独授权。
必须验证
真实后端 E2E 以空单为起点:上传→OCR→默认勾选/取消→添加→自动明细→刷新→补必要项→提交;另跑提前存票后选择、多选相同金额但不同支出、已有明细多票、部分失败、关闭面板/超时重试、双击与跨单并发、金额人工调整、缺字段/错误类型、OCR 不可用及非参与人拒绝。没有额外关联步骤,正常路径不展示关联标签;错误必须定位到票或明细。
真实腾讯 OCR 单独保留获准样票、provider/调用/结果的脱敏证据,不能用 mock 或空的 OCR 配置替代。桌面/窄屏浏览器覆盖原件预览、键盘/焦点返回及不丢草稿;现有 frontend-design skill 缺失时仅沿用项目面板和组件样式。跨任务验证/发布边界仍见付款任务同日综合方案,执行端负责,不交给员工补测。
边界
Task B-1 合并交付费用主数据政策和提交门禁代码,但运行时分成两级:B1 数据模型/主数据/表单字段随部署生效,B2 强关联和金额阻断由默认关闭的双闸 feature flag 控制。B3 财务一页式另建任务,等待 B2 在真实或专用数据上验证后开始。
数据模型
ExpenseType.requires_invoice Boolean @default(true):用 migration 为既有类型回填 true,再定向把两项补助设为 false。ReimbursementItem.asset_prior_approval_basis String?:仅 assets 明细要求,保存批准人、批准时间和简短依据;非 assets 明细应保持 null 或忽略空白值。- 不为
account_code_default新增字段;财务提供映射前保持 null。重复执行 seed/init 的 update 分支不覆盖已有科目映射,避免后续部署抹掉财务回填。 - 费用类型停用只改
enabled,不删除记录、不迁移历史明细外键。
费用类型迁移
| 当前类型 | 动作 | 新提交口径 | 历史口径 |
|---|---|---|---|
business_client_meal 客户招待 |
原 code 改名 | 业务招待费 | 追溯显示新名称 |
business_training 培训会议 |
停用旧类型并新建两个 code | 会议费、培训费 | 保持培训会议 |
assets_it_hardware IT 硬件 |
停用旧类型并新建 code | 低值配件选办公用品;需登记的选择办公电子设备 | 保持 IT 硬件 |
新 code 在实施前按现有命名规则固定并写入 migration/seed 测试,不复用旧 code 承载不同语义。
写入和提交数据流
- 员工建立或编辑明细,服务端读取权威 ExpenseType。
- assets 类型必须校验
asset_prior_approval_basis;前端提示只是辅助,服务端是最终门禁。 - 办公用品明细金额达到
2,000 元时生成非阻断类型提示,员工端和财务审核视图消费同一服务端事实;该阈值不作为会计资产确认结论。 - 发票上传/绑定显式携带目标明细 ID。B2 flag 开启时禁止无法唯一匹配后的自动建明细。
- 提交事务重新读取明细、有效发票及关联关系,按明细计算票面合计。
requires_invoice=false跳过票面比较;true 类型执行缺票和超票面阻断。- flag 关闭只选择旧提交/匹配路径,关联表中的既有数据不做逆向写入。
Feature Flag
- 新增独立 key,不复用
budget_precheck_v1或project_linking_v1。 - 配置层新增 env hard gate,默认 false;服务白名单注册 key;seed/受控配置提供 DB
force_off初值。 - 后续开启先使用专用账号或部门 allowlist(如现场组织结构允许),再全局开启。
- flag 状态必须进入诊断/审计输出,但不向普通员工显示内部 key。
兼容和回滚
- schema 只做向前兼容的 nullable/default 字段新增,不在应用回滚时逆迁移。
- 旧草稿保留;B2 开启后提交时补齐,不批量伪造关联或批准依据。
- 应用异常时先关闭 B2 flag;这不会删除既有关系。B1 名称/字段回滚只通过新 migration 或后续主数据动作,不直接删列。
- OCR 仍可给出候选匹配,但 B2 开启时最终绑定必须由用户确认,不能静默创建业务明细。
延后项
- PurchaseRequest 强关联、资产自动建卡、数量/单价、B3 一页式审批和财务科目表治理不混入本轮核心门禁。