Files
X-Financial/document/development/agent/agent plan/14_financial_document_canonical_model.md
2026-05-15 06:58:21 +00:00

19 KiB
Raw Blame History

财务单据标准模型

1. 为什么需要标准模型

OCR、MCP、用户填写、业务数据库可能都描述同一张发票但字段名和格式不同。

如果没有标准模型:

  • 规则无法复用。
  • Agent 难以解释。
  • Hermes 难以批量统计。
  • MCP 返回结果难以合并。

这里要区分三层:

  • 标准模型:定义 Agent、规则、MCP、OCR、数据库之间统一交换的数据结构。
  • 业务数据库表:定义 MVP 阶段真正落库存储、查询和统计所依赖的业务表。
  • 文件存储对象定义原始票据、预览件、OCR 中间产物、验真结果附件的存储位置与版本规则。

如果只有标准模型没有业务表和文件资产表User Agent 无法真正发起报销如果只有数据库表没有统一标准模型语义解析、规则解释、OCR 回填和 Hermes 巡检会越来越混乱。

2. 标准对象

第一版建议定义这些对象:

Invoice
Receipt
ExpenseClaim
PaymentRequest
AccountsReceivableRecord
AccountsPayableRecord
BankTransaction
Contract
Customer
Vendor
Employee
CostCenter
DocumentAsset
RiskEvent

说明:

  • 对外语义层建议统一使用 ExpenseClaim 概念,不再把“报销申请”和“报销单据”拆成两个平行主概念。
  • 现有代码中仍有 reimbursement_requestsMVP 阶段不建议再继续扩张该表,而应以 expense_claims 作为报销主表。
  • reimbursement_requests 可保留用于兼容旧页面或审批联动,但新能力默认挂到 expense_claims

3. Invoice 标准模型

{
  "invoice_id": "",
  "invoice_code": "",
  "invoice_number": "",
  "invoice_type": "",
  "seller_name": "",
  "seller_tax_no": "",
  "buyer_name": "",
  "buyer_tax_no": "",
  "issue_date": "",
  "total_amount": 0,
  "tax_amount": 0,
  "currency": "CNY",
  "verify_status": "",
  "ocr_confidence": 0,
  "source_document_id": ""
}

4. ExpenseClaim 标准模型

{
  "claim_id": "",
  "claim_no": "",
  "employee_id": "",
  "employee_name": "",
  "department_id": "",
  "department_name": "",
  "cost_center_code": "",
  "project_code": "",
  "expense_type": "",
  "reason": "",
  "location": "",
  "amount": 0,
  "currency": "CNY",
  "status": "",
  "occurred_at": "",
  "submitted_at": "",
  "approval_stage": "",
  "items": [],
  "attachments": [],
  "risk_flags": []
}

说明:

  • reasonlocationoccurred_at 是报销语义判断、规则解释、风险识别的最小必要字段。
  • 一张报销单通常包含多条费用明细,标准模型中允许聚合,数据库层必须拆到明细表。
  • attachments 指向文件资产,不直接嵌入二进制文件。

5. AccountsReceivableRecord 标准模型

{
  "ar_id": "",
  "document_no": "",
  "customer_id": "",
  "customer_name": "",
  "contract_no": "",
  "invoice_no": "",
  "amount_receivable": 0,
  "amount_received": 0,
  "amount_outstanding": 0,
  "currency": "CNY",
  "due_date": "",
  "posting_date": "",
  "status": "",
  "aging_days": 0,
  "risk_flags": []
}

6. AccountsPayableRecord 标准模型

{
  "ap_id": "",
  "document_no": "",
  "vendor_id": "",
  "vendor_name": "",
  "invoice_no": "",
  "amount_payable": 0,
  "amount_paid": 0,
  "amount_outstanding": 0,
  "currency": "CNY",
  "due_date": "",
  "posting_date": "",
  "status": "",
  "aging_days": 0,
  "risk_flags": []
}

7. BankTransaction 标准模型

{
  "transaction_id": "",
  "bank_account": "",
  "transaction_date": "",
  "amount": 0,
  "currency": "CNY",
  "counterparty_name": "",
  "summary": "",
  "matched_object_type": "",
  "matched_object_id": "",
  "match_status": ""
}

8. MVP 真实业务表设计

标准模型不等于数据库表,但 MVP 至少要有以下真实表,才能支撑 Day 5 用户报销对话、Day 6 风险巡检和后续审批/验真闭环。

8.1 设计原则

  • 报销主数据统一落在 expense_claims,不再新建第三套“报销主表”。
  • 原始票据文件二进制不进数据库,只存元数据和关联信息。
  • OCR 结果、发票结构化结果、验真结果、风险事件要分表存,避免把所有字段塞进一个 JSON。
  • 所有表都要能被 Agent 解释,也要能被 Hermes 批量扫表。
  • reimbursement_requests 进入兼容态,不作为新能力主干表继续扩展。

8.2 报销主表 expense_claims

用途:

  • 作为用户报销会话最终落单的主业务对象。
  • 承接语义层补槽后的草稿、提交、审批、打回、归档状态。

建议字段:

id                    string(36)     PK
claim_no              string(50)     UK, 报销单号
source                string(30)     来源: agent/web/import/api
title                 string(200)    报销标题
employee_id           string(64)     申请人 ID
employee_name         string(100)    申请人姓名
department_id         string(64)     部门 ID
department_name       string(100)    部门名
company_code          string(50)     公司编码
cost_center_code      string(50)     成本中心
project_code          string(50)     项目编码
expense_type          string(50)     费用大类
reason                text           事由
location              string(100)    地点
amount                numeric(12,2)  报销总金额
currency              string(10)     币种
invoice_count         int            附件票据数
attachment_count      int            附件总数
occurred_start_at     timestamptz    发生开始时间
occurred_end_at       timestamptz    发生结束时间
submitted_at          timestamptz    提交时间
status                string(30)     draft/submitted/approved/rejected/paid
status_changed_at     timestamptz    最近状态变更时间
status_changed_by     string(64)     最近状态变更人
status_change_note    text           状态变更备注
approval_stage        string(50)     当前审批节点
risk_level            string(20)     none/low/medium/high
risk_flags_json       json           风险标记快照
conversation_id       string(64)     对话会话 ID
created_by            string(64)     创建人
updated_by            string(64)     更新人
created_at            timestamptz
updated_at            timestamptz

说明:

  • 现有模型已有一部分字段,后续只做增量扩展即可。
  • occurred_start_atoccurred_end_at 比单一 occurred_at 更适合差旅、接待等跨时段报销。

8.2.1 报销状态流转建议

建议状态:

draft
submitted
approved
rejected
paid

建议流转:

语义补槽完成
  -> 创建 expense_claims 草稿
  -> status = draft

用户继续补充字段 / 上传附件
  -> 更新 expense_claims / expense_claim_items / expense_item_documents
  -> status 仍为 draft

用户明确确认提交
  -> status = submitted
  -> 写入 submitted_at / status_changed_at / status_changed_by

审批流结果回写
  -> status = approved 或 rejected

付款完成回写
  -> status = paid

边界:

  • User Agent 可以创建 draft,也可以在用户确认后提交到 submitted
  • User Agent 不应直接把状态改为 approvedrejectedpaid
  • 所有状态变化都应写审计日志,必要时保留 status_change_note

8.3 报销明细表 expense_claim_items

用途:

  • 表达一单多明细。
  • 作为 OCR 发票比对、重复报销识别、风险定位的最小粒度。

建议字段:

id                    string(36)     PK
claim_id              string(36)     FK -> expense_claims.id
line_no               int            明细序号
item_date             date           费用发生日期
item_type             string(50)     费用小类
item_reason           text           明细事由
item_location         string(100)    明细地点
merchant_name         string(200)    商户/酒店/餐厅
customer_name         string(200)    客户单位
participants_json     json           参与人员
transport_type        string(50)     交通方式
item_amount           numeric(12,2)  明细金额
tax_amount            numeric(12,2)  税额
currency              string(10)
invoice_match_status  string(30)     unmatched/partial/matched
risk_level            string(20)
risk_flags_json       json
remark                text
created_at            timestamptz
updated_at            timestamptz

说明:

  • 现有 invoice_id 单字段不足以覆盖多张附件挂同一明细的情况,后续应改为关联表。

8.4 票据资产主表 document_assets

用途:

  • 作为所有原始附件的主索引表。
  • 支持报销单、报销明细、审批、验真、风控证据等多对象挂载。

建议字段:

id                    string(36)     PK
biz_domain            string(30)     expense/ap/ar/common
biz_object_type       string(50)     expense_claim/expense_item/approval_record
biz_object_id         string(36)     业务对象 ID
document_type         string(50)     invoice/receipt/itinerary/contract/other
document_subtype      string(50)     vat_special/taxi/train/hotel/meal 等
source                string(30)     upload/agent/import/system
original_filename     string(255)
mime_type             string(100)
file_ext              string(20)
page_count            int
file_size_bytes       bigint
sha256                string(64)     去重与追溯
storage_provider      string(30)     local/minio/s3/oss/cos
storage_bucket        string(100)    本地模式可为空
storage_key           string(500)    指向当前有效版本原件
current_version_no    int
classification_status string(30)     pending/success/failed
ocr_status            string(30)     pending/running/success/failed
virus_scan_status     string(30)     pending/clean/infected
retention_policy      string(30)     finance_default/legal_hold/manual
uploaded_by           string(64)
uploaded_at           timestamptz
created_at            timestamptz
updated_at            timestamptz

8.5 票据版本表 document_asset_versions

用途:

  • 保留原始文件和后续重新上传版本。
  • 允许“修正”但不允许覆盖原始证据。

建议字段:

id                    string(36)     PK
document_id           string(36)     FK -> document_assets.id
version_no            int            1,2,3...
is_current            bool
change_reason         string(100)    replace/rotate/desensitize/reupload
original_filename     string(255)
mime_type             string(100)
file_size_bytes       bigint
sha256                string(64)
storage_provider      string(30)
storage_bucket        string(100)
storage_key           string(500)
uploaded_by           string(64)
uploaded_at           timestamptz
created_at            timestamptz

8.6 衍生文件表 document_derivatives

用途:

  • 存储缩略图、预览 PDF、逐页图片、脱敏件等衍生产物。

建议字段:

id                    string(36)     PK
document_version_id   string(36)     FK -> document_asset_versions.id
derivative_type       string(50)     thumb/preview/page_image/desensitized
page_no               int            可空
mime_type             string(100)
file_size_bytes       bigint
storage_provider      string(30)
storage_bucket        string(100)
storage_key           string(500)
created_by            string(64)
created_at            timestamptz

8.7 OCR 结果表 document_ocr_results

用途:

  • 保留每次 OCR 原始结果、模型版本、置信度和错误信息。
  • 支持后续重跑 OCR 与人工纠错对比。

建议字段:

id                    string(36)     PK
document_id           string(36)     FK -> document_assets.id
document_version_id   string(36)     FK -> document_asset_versions.id
ocr_engine            string(50)     paddle/aliyun/tencent/openai 等
ocr_model             string(100)
run_no                int            第几次 OCR
status                string(30)     success/failed/partial
language              string(20)
raw_text              text
raw_result_json       json
structured_result_json json
confidence            numeric(5,4)
error_message         text
started_at            timestamptz
finished_at           timestamptz
created_at            timestamptz

8.8 发票结构化表 invoice_structured_records

用途:

  • 将发票核心字段标准化后独立存储,便于查重、验真、规则计算。

建议字段:

id                    string(36)     PK
document_id           string(36)     FK -> document_assets.id
ocr_result_id         string(36)     FK -> document_ocr_results.id
invoice_code          string(50)
invoice_number        string(50)
invoice_type          string(50)
seller_name           string(200)
seller_tax_no         string(50)
buyer_name            string(200)
buyer_tax_no          string(50)
issue_date            date
total_amount          numeric(12,2)
tax_amount            numeric(12,2)
currency              string(10)
check_code            string(100)
is_red_invoice        bool
is_electronic         bool
ocr_confidence        numeric(5,4)
normalized_status     string(30)     normalized/manual_corrected
duplicate_fingerprint string(100)    发票号+代码+金额+日期
created_at            timestamptz
updated_at            timestamptz

8.9 发票验真记录表 invoice_verification_records

用途:

  • 保留每次调用税局/第三方验真服务的结果,支持追溯。

建议字段:

id                    string(36)     PK
invoice_record_id     string(36)     FK -> invoice_structured_records.id
verification_channel  string(50)     tax_mcp/third_party/manual
request_payload_json  json
response_payload_json json
verify_status         string(30)     verified/unverified/voided/error
voided                bool
red_reversed          bool
verified_amount       numeric(12,2)
verified_issue_date   date
error_code            string(50)
error_message         text
verified_by           string(64)
verified_at           timestamptz
created_at            timestamptz

8.10 明细与票据关联表 expense_item_documents

用途:

  • 解决一条明细可关联多张票据、一张票据也可能支撑多条拆分明细的场景。

建议字段:

id                    string(36)     PK
claim_id              string(36)     FK -> expense_claims.id
claim_item_id         string(36)     FK -> expense_claim_items.id
document_id           string(36)     FK -> document_assets.id
relation_type         string(30)     evidence/invoice/boarding_pass/receipt
allocated_amount      numeric(12,2)  分摊到该明细的金额
match_status          string(30)     unmatched/partial/matched
match_confidence      numeric(5,4)
created_at            timestamptz
updated_at            timestamptz

8.11 风险事件表 risk_events

用途:

  • 记录风险命中,而不是只在主表里塞一个 risk_flags_json
  • 作为 Agent 解释“为什么拦截”的核心依据。

建议字段:

id                    string(36)     PK
biz_domain            string(30)     expense/ap/ar
biz_object_type       string(50)     expense_claim/expense_item/invoice
biz_object_id         string(36)
risk_code             string(50)     duplicate_invoice/amount_mismatch 等
risk_name             string(100)
risk_level            string(20)     low/medium/high
hit_source            string(30)     rule/agent/hermes/manual
evidence_json         json
status                string(30)     open/confirmed/resolved/ignored
detected_at           timestamptz
detected_by           string(64)
resolved_at           timestamptz
resolved_by           string(64)
resolution_note       text
created_at            timestamptz
updated_at            timestamptz

8.12 风险处置表 risk_actions

用途:

  • 记录每次人工确认、驳回、忽略、要求补件等处置动作。

建议字段:

id                    string(36)     PK
risk_event_id         string(36)     FK -> risk_events.id
action_type           string(30)     confirm/reject/ignore/request_more
action_note           text
operator_id           string(64)
operator_name         string(100)
created_at            timestamptz

8.13 文件访问审计表 document_access_logs

用途:

  • 记录谁看过、下载过、导出过原始票据。
  • 支撑财务审计和数据安全追溯。

建议字段:

id                    string(36)     PK
document_id           string(36)     FK -> document_assets.id
document_version_id   string(36)     FK -> document_asset_versions.id
action                string(30)     preview/download/export/delete
operator_id           string(64)
operator_name         string(100)
operator_role         string(50)
client_ip             string(64)
user_agent            string(255)
trace_id              string(64)
created_at            timestamptz

9. 表关系建议

expense_claims
  └─ expense_claim_items
       └─ expense_item_documents
            └─ document_assets
                 └─ document_asset_versions
                      └─ document_derivatives
                 └─ document_ocr_results
                      └─ invoice_structured_records
                           └─ invoice_verification_records

risk_events -> 可指向 expense_claims / expense_claim_items / invoice_structured_records
risk_actions -> risk_events
document_access_logs -> document_assets / document_asset_versions

原则:

  • 主业务对象和文件资产解耦。
  • OCR、验真、风险都挂在文件资产或业务对象之上不把责任塞到一个巨表。
  • 文件版本和业务关系分离,避免替换附件时把历史证据冲掉。

10. 与现有表的衔接策略

当前代码中已经存在:

  • expense_claims
  • expense_claim_items
  • reimbursement_requests

建议策略:

  • expense_claims 继续作为未来报销主表。
  • expense_claim_items 增量扩字段并替换当前单一 invoice_id 直连方式。
  • reimbursement_requests 暂不删除,但冻结扩表。
  • 如旧流程仍引用 reimbursement_requests,可在过渡期建立:
    • request_no -> claim_no 对照字段
    • 或由 approval_records 同时支持两类来源对象

不建议做法:

  • 再新建第四张“报销申请主表”。
  • 把原始发票图片以 blob 方式存进 expense_claims
  • 把 OCR、验真、风控结果全塞进一个 JSON 大字段。

11. 实施顺序建议

Phase 1

  • 扩展 expense_claims
  • 扩展 expense_claim_items
  • 新增 document_assets
  • 新增 document_asset_versions
  • 新增 expense_item_documents

Phase 2

  • 新增 document_ocr_results
  • 新增 invoice_structured_records
  • 新增 invoice_verification_records
  • 新增 document_derivatives

Phase 3

  • 新增 risk_events
  • 新增 risk_actions
  • 新增 document_access_logs

Phase 4

  • 逐步弱化 reimbursement_requests
  • 将 Agent 草稿、审批、OCR、验真、风控全收敛到 expense_claims 体系

12. 对 Agent 的直接收益

  • 用户说“我要报销”时Agent 能先创建 expense_claims 草稿,再持续补槽。
  • 用户上传票据后,系统有明确的 document_assetsexpense_item_documents 可挂载。
  • OCR 和验真结果不是一次性临时输出,而是可追溯、可回放、可审计的长期资产。
  • Agent 回答“为什么被拦截”时,可以直接引用 risk_events 和票据证据,不再靠拼字符串解释。