# ARR 数据落库 MVP v1 数据字典

更新时间：2026-07-28  
权威 DDL：`database/008_arr_mvp_v1_rebuild.sql`  
已部署目标：`<ARR_DB_HOST>:5432/booking_test`（远程测试库）

## 1. 模型边界

数据库只保存三类内容：

1. `ingestion`：私有 OSS/本地 fixture 工件身份、Agent 处理运行、交付验收与 outbox；
2. `booking`：预订附件来源行、行级 Agent 解析版本和房型明细；
3. `finance`：日报不可变版本、每条 XML 记录的全部 outcome、current 指针和渠道计数。

月报、渠道明细和每 10 日公司用房表均从只读视图/普通程序生成，不建 `report_versions` 或报表行表。第一版不包含多酒店、账户权限、RSVN 任务卡、邮件元数据和前端追溯页面。

统一类型：主键为 `bigint identity`，业务日期为 `date`，时间为 `timestamptz`，金额为 `numeric(18,2)`，完整 JSON 为 `jsonb`。

## 2. `ingestion` schema

### `ingestion.artifacts`

只保存不可变私有对象的元数据，不保存文件字节、签名 URL 或凭据。

| 字段 | 类型 | 含义 |
|---|---|---|
| `id` | bigint | 工件 ID |
| `artifact_kind` | text | `booking_source_md`、`booking_excel`、`opera_xml`、`daily_xlsx`、`monthly_xlsx`、`channel_detail_xlsx`、`company_ten_day_xlsx`、`exception_xlsx`、`result_json`、`structured_result_json` |
| `storage_provider` | text | `oss`、`s3` 或 `local_fixture` |
| `bucket_alias` | text | 内部 bucket 别名，不含路径符 |
| `object_key` | text | 私有对象键；禁止绝对路径、`..`、查询串和 fragment |
| `object_version_id` | text? | 对象存储版本号 |
| `etag` | text? | provider ETag；内容身份仍以 SHA-256 为准 |
| `original_filename` | text | 安全原文件名 |
| `sha256` | char(64) | 内容哈希 |
| `byte_size` | bigint | 文件大小，允许 0 |
| `mime_type` | text? | MIME |
| `created_at` | timestamptz | 登记时间 |

唯一性：`artifact_kind + sha256`；同时约束 provider/bucket/object/version 身份唯一。

### `ingestion.processing_runs`

ARR 拥有的一次逻辑处理运行，不是 RSVN 任务卡。

关键字段：`run_key`、`pipeline_type`（`opera_daily`/`booking_source_import`）、源/结果工件 ID、`run_status`、请求及交付 processor/rule/schema 版本、`delivery_sha256`、`delivery_json`、`business_date`、安全失败码/消息和完整时间线。

终态规则：accepted 的 Opera 日报必须具备结果工件、营业日、交付版本、规则哈希和交付哈希；rejected/failed 必须有失败码。

### `ingestion.processing_attempts`

一次运行的可重试尝试。

关键字段：`processing_run_id`、`attempt_no`、`attempt_status`、64 位 `idempotency_key`、可选 `remote_run_id`、安全失败字段和时间线。运行内 attempt 序号唯一，idempotency key 全局唯一。

### `ingestion.processing_deliveries`

ARR 收到并验收的 DeliveryEnvelope。

关键字段：`delivery_key`、run/attempt、`envelope_sha256`、完整 `envelope_json`、`delivery_status`、`result_status`、processor/rule/schema、营业日、五类输入/输出工件 FK、`daily_version_id`、失败字段和接收/验收/提交时间。

同一个 delivery key 只能代表同一份交付；相同 attempt + envelope 哈希不得重复。

### `ingestion.outbox_events`

与业务事务一起提交的隐私最小化可靠事件。

关键字段：`event_key`、`aggregate_type`、`aggregate_id`、`event_type`、`payload`、发布状态/次数/可用时间/发布时间和最后失败码。payload 只放 opaque ID、日期、计数和安全状态，不放住客字段。

## 3. `booking` schema

### `booking.source_batches`

一份预订 Excel 或当前 MD fixture 的导入批次。

字段：`source_artifact_id`、`source_kind`（`expected_fixture`/`booking_excel`）、`source_format_version`、可选参考年月、`batch_status`、source/accepted/failed 计数、失败字段和时间线。一个工件只能形成一个批次。

### `booking.current_source_batch`

单行指针，指向公司报表当前使用的完整、已验收 Booking 批次。新工作簿只有在全部人工项确认或删除并完成原子激活后才替换该指针；历史批次保持不可变。

### `booking.extraction_drafts`

上传工作簿的持久化提取草稿。关键字段包括 `draft_id`、私有源工件、`draft_status`（`reviewing`/`activated`/`superseded`）、processor/rule/source 哈希、源行/工作表/Tour Code/提取项计数以及激活后的批次 ID。`reviewing` 草稿不会改变 `current_source_batch`。

### `booking.extraction_draft_items`

一条记录对应一个提取出的房型项目，保留工作表、原行号、item 顺序、Tour Code、酒店原文、房型原始标识、标准/人工房型、数量、源片段和 `automatic` 标志。

`review_status` 为 `confirmed`、`pending` 或 `deleted`：待人工项目保留已经识别到的数量但不进入有效 Booking 房量；人工填写房型并保存后转为 `confirmed`；删除是草稿内软删除，启用时只写入仍为 `confirmed` 的项目。

### `booking.source_rows`

一行对应原文件中的一行，重复 Group Code 也必须分行保存。

| 字段 | 类型 | 含义 |
|---|---|---|
| `id` | bigint | 来源行 ID |
| `source_batch_id` | bigint | 所属导入批次 |
| `source_worksheet` | text | 原工作表 |
| `source_row_no` | integer | 原行号 |
| `group_code_raw` | text | 原始 Group Code/Tour Code |
| `group_code_key` | text generated | `upper(btrim(group_code_raw))`，精确关联键 |
| `type_of_room_raw` | text | 原始 `TYPE OF ROOM`/`โรงแรม` 文本 |
| `no_of_rooms` | integer | 该来源行总房数，必须大于 0 |
| `source_row_sha256` | char(64) | 来源坐标和业务值的确定性哈希 |
| `created_at` | timestamptz | 落库时间 |

唯一性只针对“批次 + worksheet + 行号”和“批次 + 行哈希”，不把 Group Code 设为唯一。

### `booking.parse_versions`

一条来源行的不可变程序解析版本。

字段：`source_row_id`、`version_no`、`parse_status`（`accepted`/`needs_review`/`failed`）、结果 Schema、processor 名称/版本、规则哈希、输入行哈希、结果哈希、完整已验收 `result_json`、失败字段和验收时间。版本唯一键为 `source_row_id + version_no`。

精确 JSON 合同：`database/contracts/booking-row-parse-result.schema.json`。

### `booking.room_items`

程序提取并确认后可按 Group Code 查询的房型项目；一条来源行可拆出多项。

| 字段 | 类型 | 含义 |
|---|---|---|
| `parse_version_id` | bigint | 所属解析版本 |
| `item_no` | integer | Agent 输出顺序 |
| `room_type_raw` | text | 原始房型显示 |
| `room_type_code` | text? | 可选标准房型码 |
| `quantity` | integer | 房数，必须大于 0 |
| `unit_price` | numeric? | 解析单价；未知可为空 |
| `currency_code` | char(3)? | ISO 三位大写币种 |
| `price_token_raw` | text? | 例如 `12`，用于解释 1200 |
| `source_fragment` | text? | 对应原文片段 |
| `created_at` | timestamptz | 落库时间 |

同一解析版本内 `item_no` 唯一。

### `booking.current_row_parses`

每条来源行唯一指向一个当前 accepted parse。触发器同时验证：批次已 accepted、parse 已 accepted、所有 room item 数量合计等于来源行 `no_of_rooms`。

### Booking 视图

- `booking.v_current_room_items`：逐来源行返回当前 accepted 的全部 room item 和来源坐标。
- `booking.v_group_room_item_summary`：查询时按 `group_code_key + 房型` 汇总数量；不修改原始行。
- `booking.v_group_booking_rooms`：生成 `booking_room` 展示串、总预订房数和命中来源行数。

## 4. `finance` schema

### `finance.daily_versions`

一次 ARR 验收结果形成一个不可变日报版本。

字段：营业日/版本号、`processing_run_id`、源 XML/日报/result/structured-result/异常工件 FK、`version_status`、processor/rule/schema、`result_sha256`、五类 outcome 计数、失败字段和创建/验收/激活/替换时间。

五类计数必须严格满足：

```text
source_rows = retained_rows
            + excluded_rate_code_rows
            + duplicate_rows
            + validation_failed_rows
            + price_unmatched_rows
```

验收成功版本状态为 validated/active/superseded；失败交付可保存为 rejected，但绝不能成为 current。

### `finance.daily_records`

保存 structured-result 中每一条 XML reservation，包括未进入正式日报的记录。

来源与处理字段：

| 字段 | 含义 |
|---|---|
| `daily_version_id` | 所属不可变版本 |
| `source_sequence` | Agent 连续源序号 |
| `source_location` | 如 `reservation[1]` |
| `source_worksheet/source_row_no` | Excel 型输入坐标；直接 XML 必须同时为空 |
| `outcome` | `retained`、`excluded_rate_code`、`duplicate`、`validation_failed`、`price_unmatched` |
| `decision_codes` | 处理决定码数组 |
| `duplicate_of_record_id` | duplicate 指向同版本原记录 |

日报业务字段：

| 字段 | 类型 |
|---|---|
| `block_code` | text |
| `adults` / `children` | integer |
| `company_name` / `company_key` | text |
| `confirmation_no` | text |
| `disp_room_no` | text |
| `effective_rate_amount` | numeric(18,2) |
| `full_name` | text |
| `res_comment` | text |
| `group_code_key` | generated：`upper(btrim(res_comment))`；空值为 null |
| `trace_text` | text |
| `no_of_rooms` | integer |
| `products` | text |
| `rate_code` | text |
| `normalized_rate_code` | generated：规范化 rate code |
| `room_category_label` | text |
| `arrival` / `departure` | date |
| `nights` | integer |
| `real_price` / `total_price` / `kb_amount` | numeric(18,2) |
| `channel_key` / `pricing_method` | text |

预订关联字段：`booking_source_match_status`（matched/unmatched/missing_group_code/not_checked）与 `booking_source_match_count`。重复 Group Code 命中多条来源行仍是 matched，并记录实际命中数。

retained 记录强制约束：日期/晚数一致、金额非负、去重键完整、预订查询已检查，并且：

```text
total_price = real_price × no_of_rooms × nights
```

### `finance.daily_channel_metrics`

每个日报版本的渠道名称、确定性顺序和 retained 行数。主键为 `daily_version_id + channel_key`；同版本 `channel_order` 唯一。

### `finance.current_daily_versions`

每个营业日唯一指向当前 active 版本。触发器阻止指向未激活或日期不一致的版本。同日修正先完整落新版本，最后原子切换该指针。

## 5. 业务只读视图

- `finance.v_active_daily_facts`：仅 current daily version + `outcome='retained'`；所有普通业务查询的权威源。
- `finance.v_monthly_report_rows`：月报 20 列投影；`RATE_AMOUNT`、`ROOM_CATEGORY` 为字段别名，`Booking Room` 来自预订 Group Code 查询，`TOTAL PRICE` 直接使用日报值。
- `finance.v_channel_details`：ARRIVAL、DEPARTURE、NIGHTS、BLOCK_CODE、RES_COMMENT、Booking Room、Total Booking Price 等；Total Booking Price 等于该日报行 `total_price`，不按 Group Code 二次汇总。
- `finance.v_company_report_source`：公司 10 日报表程序使用的 Finance 日期/价格事实和预订房型补充。
- `finance.v_daily_processing_audit`：当前与历史版本的全部 outcome、决定码、duplicate lineage、源文件哈希、processor/rule/schema 和验收时间；用于以后追溯页面。

## 6. 精确关联规则

```text
upper(trim(finance.daily_records.RES_COMMENT))
  = booking.source_rows.group_code_key
```

禁止使用 `BLOCK_CODE` 代替，禁止模糊匹配。预订数据只提供 Booking Room；Finance 自己提供到离日期、晚数、实际房数、价格和 `total_price`。
