# 财务单据标准模型

## 1. 为什么需要标准模型

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

如果没有标准模型：

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

这里要区分三层：

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

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

## 2. 标准对象

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

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

说明：

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

## 3. Invoice 标准模型

```json
{
  "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 标准模型

```json
{
  "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": []
}
```

说明：

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

## 5. AccountsReceivableRecord 标准模型

```json
{
  "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 标准模型

```json
{
  "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 标准模型

```json
{
  "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`

用途：

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

建议字段：

```text
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_at`、`occurred_end_at` 比单一 `occurred_at` 更适合差旅、接待等跨时段报销。

### 8.2.1 报销状态流转建议

建议状态：

```text
draft
submitted
approved
rejected
paid
```

建议流转：

```text
语义补槽完成
  -> 创建 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 不应直接把状态改为 `approved`、`rejected`、`paid`。
- 所有状态变化都应写审计日志，必要时保留 `status_change_note`。

### 8.3 报销明细表 `expense_claim_items`

用途：

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

建议字段：

```text
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`

用途：

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

建议字段：

```text
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`

用途：

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

建议字段：

```text
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、逐页图片、脱敏件等衍生产物。

建议字段：

```text
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 与人工纠错对比。

建议字段：

```text
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`

用途：

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

建议字段：

```text
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`

用途：

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

建议字段：

```text
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`

用途：

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

建议字段：

```text
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 解释“为什么拦截”的核心依据。

建议字段：

```text
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`

用途：

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

建议字段：

```text
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`

用途：

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

建议字段：

```text
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. 表关系建议

```text
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_assets` 与 `expense_item_documents` 可挂载。
- OCR 和验真结果不是一次性临时输出，而是可追溯、可回放、可审计的长期资产。
- Agent 回答“为什么被拦截”时，可以直接引用 `risk_events` 和票据证据，不再靠拼字符串解释。
