# M002 Backend Data Model Design 后端数据模型设计

## 文档信息

| 项目 | 内容 |
| --- | --- |
| 文档版本 | 0.2 |
| 日期 | 2026-07-11 |
| 状态 | V2 后端数据模型与阶段实现记录；V3 新开发和当前实现状态以 `M002-order-task-workflow-v3.md` 为准 |
| 适用范围 | AI 过渡层、订单、任务、任务卡、审计和 OPERA 模拟结果 |
| 主要读者 | 后端、数据库、测试、后续协作 agent |

## 1. 文档定位

本文定义 M002 第一阶段后端数据模型草案，用于支撑 SuperAgent 任务结果入站、订单挂靠、任务卡确认、队列顺序、审计和 OPERA 模拟结果。

本文不是完整最终模型。当前后端已经按本模型落地第一阶段 Flyway migration、Entity、Mapper、Repository、Service 和测试；后续真实 OPERA、前端页面和 SuperAgent 查询上下文接口仍需继续补充。

2026-07-11 后，M002 V3 已确认采用 0711 P0 冻结基线；2026-07-12 后，Parent Group / Allotment 语义按 P0.1 修订。后续数据模型扩展必须支持结构化 `S10/S99`、`message_events[]`、40 路由、方案 C 中的 AI 原始三元组 / 系统处理分类分离、type-known manual review 同卡解阻和 `adapter_contract_error`，不能只沿用本文的 `ai_task_results[]` 阶段模型。

## 2. 设计原则

- 保留 AI 原始 JSON，不覆盖、不重写、不丢字段。
- 用户确认后的数据写入 `confirmed_payload_json`，OPERA 模拟只读取确认后的 payload。
- 高频查询、幂等、排序和状态机字段冗余为物理列。
- 任务卡字段第一版先写在代码里，但必须通过低耦合 Provider 封装，后续可迁移为数据库配置。
- `BLOCKED` 不作为任务持久状态，而是根据同订单前置任务是否完成实时计算。
- 用户不允许强制完成任务。
- 用户不能跳过失败的 OPERA 模拟操作。
- 同订单队列可处理状态实时计算时，`FAILED` 和 `COMPLETED` 都视为前置任务已结束，不阻塞后续任务。
- OPERA 当前是模拟结构，后续真实系统接入时通过 adapter 映射，不污染核心任务模型。

## 3. 枚举

### 3.1 订单状态

| 状态 | 中文说明 |
| --- | --- |
| `TEMPORARY` | 临时订单，用于 New Booking 未取得业务号、Fallback、Message Notification 或暂无法匹配订单的任务容器 |
| `ACTIVE` | 有效订单，可承载可处理业务任务 |
| `ENDED` | 已结束订单，例如已完成、已取消或业务生命周期结束；仍可查看历史 |
| `LOGIC_DELETED` | 逻辑删除订单，主要用于临时订单迁移后废弃，不再承载新任务 |

### 3.2 任务状态

| 状态 | 中文说明 |
| --- | --- |
| `PENDING_CONFIRM` | 待用户确认订单归属和任务字段 |
| `READY` | 已确认，等待执行 OPERA 模拟 |
| `EXECUTING` | 正在执行 OPERA 模拟 |
| `FAILED` | 任务级失败结束态；第一版 OPERA 单条模拟操作失败时不直接把任务改为该状态，避免按队列规则误放行 |
| `COMPLETED` | 任务已完成 |

说明：

- `Message Notification` 不参与执行队列，可在创建后直接进入 `COMPLETED` 或保持只读完成态。
- `manual_review` / `Fallback` 默认进入 `PENDING_CONFIRM`，等待人工转换或处理。
- 阻塞态通过查询同订单前置任务实时计算，不落 `BLOCKED` 状态。

### 3.3 系统主任务类型

| 类型 | 中文说明 |
| --- | --- |
| `NEW_BOOKING` | 新建订单任务 |
| `UPDATE_BOOKING` | 更新订单任务，下面包含多种任务卡 |
| `CANCEL_BOOKING` | 取消订单任务 |
| `MANUAL_REVIEW` | 人工复核 / Fallback 任务 |
| `INFORMATIONAL_MESSAGE` | 只读信息提醒任务，不参与执行队列 |

### 3.4 任务卡类型

第一版建议包含：

- `NEW_BOOKING`
- `UPDATE_BOOKING`
- `CANCEL_BOOKING`
- `VOUCHER_RECEIVED`
- `ROOMING_LIST`
- `AMEND_GROUP_CODE`
- `TRACE_RESERVATION_NOTES`
- `TA_RECORDER`
- `MESSAGE_NOTIFICATION`
- `FALLBACK_REVIEW`

## 4. 表设计总览

| 表名 | 中文说明 |
| --- | --- |
| `workflow_reservation_ai_batch` | AI 任务结果接收批次表 |
| `workflow_reservation_ai_transition` | AI 任务结果过渡表，一条 AI item 一行 |
| `workflow_reservation_order` | 订单表 |
| `workflow_reservation_task` | 任务表 |
| `workflow_reservation_task_card` | 任务卡确认数据表 |
| `workflow_reservation_audit_log` | 订单任务审计表 |
| `workflow_reservation_opera_operation` | OPERA 模拟逻辑操作表 |
| `workflow_reservation_opera_operation_attempt` | OPERA 模拟操作尝试记录表 |

表名前缀使用 `workflow_reservation`，表示当前属于 reservation 工作流，不放入平台中立 `platform` 模块。

## 5. AI 接收批次表

表名：`workflow_reservation_ai_batch`

| 字段 | 类型建议 | 中文说明 |
| --- | --- | --- |
| `id` | `BIGINT` | 批次 ID |
| `hotel_id` | `VARCHAR(64)` | 酒店或业务上下文 |
| `source_message_id` | `BIGINT` | 关联 SourceMessage |
| `request_payload_sha256` | `CHAR(64)` | 原始请求体 SHA-256 |
| `batch_idempotency_key` | `CHAR(64)` | 系统生成的批次幂等键 |
| `client_id` | `VARCHAR(128)` | SuperAgent 调用方 ID |
| `request_id` | `VARCHAR(128)` | 调用方请求 ID，可为空 |
| `received_at` | `DATETIME(6)` | AI 结果请求接收时间，按 UTC 理解 |
| `item_count` | `INT` | AI item 数量 |
| `extraction_warnings_json` | `LONGTEXT` | 抽取警告 JSON |
| `created_at` / `updated_at` | `DATETIME(6)` | 创建和更新时间 |

索引建议：

- 唯一索引：`hotel_id + batch_idempotency_key`
- 普通索引：`hotel_id + source_message_id`
- 普通索引：`hotel_id + received_at`

## 6. AI 过渡表

表名：`workflow_reservation_ai_transition`

V2 一条 `ai_task_results[]` item 对应一行。M002 V3 后，`message_events[]`、`S10/S99` 入口通知、`unhandled_current_intents[]` 和 adapter 契约错误也统一以 transition 方式追溯保存。

| 字段 | 类型建议 | 中文说明 |
| --- | --- | --- |
| `id` | `BIGINT` | AI 过渡记录 ID |
| `hotel_id` | `VARCHAR(64)` | 酒店或业务上下文 |
| `batch_id` | `BIGINT` | 接收批次 ID |
| `source_message_id` | `BIGINT` | 来源消息 ID |
| `source_event_index` | `INT` | AI current 事件序号 |
| `array_index` | `INT` | AI 返回列表中的顺序，建议从 1 开始 |
| `execution_order` | `INT` | 映射到订单任务队列的初始顺序 |
| `catalog_code` | `VARCHAR(32)` | Skill 目录代码 |
| `skill_id` | `VARCHAR(128)` | Skill 标识 |
| `result_type` | `VARCHAR(64)` | AI 结果类型，例如 `normal_task`、`manual_review`、`source_message_review_notification`、`adapter_contract_error` |
| `ai_task_type` | `VARCHAR(64)` | AI 原始任务类型 |
| `route_code` | `VARCHAR(64)` | M002 V3 派生路由码；S10/S99 使用外部 route_code，业务事件使用系统稳定路由码 |
| `system_process_category` | `VARCHAR(64)` | 系统处理分类：`BUSINESS_TASK`、`SOURCE_MESSAGE_NOTIFICATION`、`UNHANDLED_CURRENT_INTENT`、`ADAPTER_CONTRACT_ERROR` |
| `system_task_type` | `VARCHAR(64)` | 系统主任务类型 |
| `task_card_type` | `VARCHAR(64)` | 任务卡类型 |
| `task_subtype` | `VARCHAR(128)` | 业务动作 subtype |
| `current_or_history` | `VARCHAR(32)` | 当前或历史标识 |
| `group_code` | `VARCHAR(128)` | Group Code 候选 |
| `confirmation_number` | `VARCHAR(128)` | Confirmation Number 候选 |
| `item_payload_sha256` | `CHAR(64)` | 单个 item JSON 哈希 |
| `item_idempotency_key` | `CHAR(64)` | 系统生成 item 幂等键 |
| `manual_reason_code` | `VARCHAR(128)` | 人工复核原因码 |
| `parent_source_event_index` | `INT` | 父任务事件序号 |
| `linked_task_group_id` | `VARCHAR(128)` | 联动任务组 ID |
| `blocked_until_parent_completed` | `TINYINT(1)` | 是否等待父任务完成 |
| `ai_payload_json` | `LONGTEXT` | AI 原始 item JSON |
| `case_keys_json` | `LONGTEXT` | 订单候选键 JSON |
| `extracted_fields_json` | `LONGTEXT` | AI 业务字段 JSON |
| `manual_review_json` | `LONGTEXT` | 人工复核 JSON |
| `informational_message_json` | `LONGTEXT` | 信息提醒 JSON |
| `attachments_json` | `LONGTEXT` | 附件 JSON |
| `context_used_json` | `LONGTEXT` | 上下文 JSON |
| `adapter_error_code` | `VARCHAR(128)` | Adapter 契约错误代码，仅在当前 event 不建业务任务时保存 |
| `adapter_error_message` | `VARCHAR(512)` | Adapter 契约错误说明，仅保存安全摘要 |
| `created_at` / `updated_at` | `DATETIME(6)` | 创建和更新时间 |

索引建议：

- 唯一索引：`hotel_id + item_idempotency_key`
- 普通索引：`hotel_id + source_message_id + source_event_index`
- 普通索引：`hotel_id + result_type + ai_task_type`
- 普通索引：`hotel_id + route_code`
- 普通索引：`hotel_id + system_process_category`
- 普通索引：`hotel_id + group_code`
- 普通索引：`hotel_id + confirmation_number`

## 7. 订单表

表名：`workflow_reservation_order`

| 字段 | 类型建议 | 中文说明 |
| --- | --- | --- |
| `id` | `BIGINT` | 系统内部订单 ID |
| `hotel_id` | `VARCHAR(64)` | 酒店或业务上下文 |
| `order_key_type` | `VARCHAR(32)` | 业务号类型：`GROUP_CODE`、`CONFIRMATION_NUMBER`、`TEMPORARY` |
| `order_business_key` | `VARCHAR(128)` | 真实业务号 |
| `active_business_key` | `VARCHAR(128)` | ACTIVE 订单唯一约束辅助键，ACTIVE 业务订单时等于真实业务号，其他状态为空 |
| `temporary_order_code` | `VARCHAR(128)` | 临时订单展示编号 |
| `order_status` | `VARCHAR(32)` | 订单状态 |
| `business_key_source` | `VARCHAR(64)` | 业务号来源，例如 `AI_CANDIDATE`、`USER_CONFIRMED`、`OPERA_SIMULATION_RESULT` |
| `business_key_backfilled_at` | `DATETIME(6)` | New Booking 成功后回填真实业务号的 UTC 时间 |
| `display_name` | `VARCHAR(256)` | 前端展示名称 |
| `source_message_id` | `BIGINT` | 首次创建该订单的来源消息 |
| `created_from_task_id` | `BIGINT` | 首次创建该订单的任务 ID，可为空 |
| `ended_at` | `DATETIME(6)` | 订单进入 ENDED 的时间 |
| `logic_deleted_at` | `DATETIME(6)` | 逻辑删除时间 |
| `logic_deleted_reason` | `VARCHAR(512)` | 逻辑删除原因 |
| `version` | `BIGINT` | 乐观锁版本 |
| `created_at` / `updated_at` | `DATETIME(6)` | 创建和更新时间 |

索引建议：

- 普通索引：`hotel_id + order_status + updated_at`
- 普通索引：`hotel_id + order_key_type + order_business_key`
- 唯一索引：`hotel_id + order_key_type + active_business_key`，用于保证同一 `hotel_id + GROUP_CODE` / `hotel_id + CONFIRMATION_NUMBER` 只能有一个 ACTIVE 订单
- 唯一索引：`hotel_id + temporary_order_code`

说明：

- MySQL 第一版不依赖部分唯一索引；通过 `active_business_key` 在 ACTIVE 业务订单时写入真实业务号、非 ACTIVE 或临时订单为空，配合唯一索引保证同一业务号只能有一个 ACTIVE 订单。
- `LOGIC_DELETED` 订单不得再挂新任务。

## 8. 任务表

表名：`workflow_reservation_task`

| 字段 | 类型建议 | 中文说明 |
| --- | --- | --- |
| `id` | `BIGINT` | 系统任务 ID |
| `hotel_id` | `VARCHAR(64)` | 酒店或业务上下文 |
| `order_id` | `BIGINT` | 当前挂靠订单 ID |
| `source_message_id` | `BIGINT` | 来源消息 ID |
| `ai_transition_id` | `BIGINT` | AI 过渡记录 ID |
| `result_type` | `VARCHAR(32)` | AI 结果类型 |
| `ai_task_type` | `VARCHAR(64)` | AI 原始任务类型 |
| `system_task_type` | `VARCHAR(64)` | 系统主任务类型 |
| `task_card_type` | `VARCHAR(64)` | 任务卡类型 |
| `task_subtype` | `VARCHAR(128)` | 业务动作 subtype |
| `task_status` | `VARCHAR(32)` | 任务状态 |
| `queue_participation` | `TINYINT(1)` | 是否参与订单执行队列 |
| `execution_order` | `INT` | 同订单执行顺序 |
| `parent_task_id` | `BIGINT` | 父任务 ID，可为空 |
| `parent_source_event_index` | `INT` | 父任务 source event index |
| `linked_task_group_id` | `VARCHAR(128)` | 联动任务组 ID |
| `blocked_until_parent_completed` | `TINYINT(1)` | 是否等待父任务完成 |
| `last_failure_reason` | `VARCHAR(512)` | 最近失败原因摘要 |
| `confirmed_at` | `DATETIME(6)` | 用户确认时间 |
| `completed_at` | `DATETIME(6)` | 完成时间 |
| `version` | `BIGINT` | 乐观锁版本 |
| `created_at` / `updated_at` | `DATETIME(6)` | 创建和更新时间 |

索引建议：

- 普通索引：`hotel_id + order_id + queue_participation + execution_order`
- 唯一索引：`hotel_id + order_id + queue_participation + execution_order`，用于防止并发创建任务时出现相同队列序号
- 普通索引：`hotel_id + task_status + updated_at`
- 普通索引：`hotel_id + source_message_id`
- 普通索引：`hotel_id + ai_transition_id`

队列规则：

- `queue_participation=false` 的任务不阻塞队列，适用于 `Message Notification`。
- 是否可编辑、可确认、可执行由服务层实时计算。
- 当前任务前面存在未完成且参与队列的任务时，本任务只能查看。

## 9. 任务卡确认数据表

表名：`workflow_reservation_task_card`

| 字段 | 类型建议 | 中文说明 |
| --- | --- | --- |
| `id` | `BIGINT` | 任务卡 ID |
| `hotel_id` | `VARCHAR(64)` | 酒店或业务上下文 |
| `task_id` | `BIGINT` | 所属任务 |
| `task_card_type` | `VARCHAR(64)` | 任务卡类型 |
| `field_contract_version` | `VARCHAR(64)` | 字段契约版本，当前 P0 字段矩阵写 `20260711-p0` |
| `ai_payload_json` | `LONGTEXT` | 任务卡使用的 AI 原始 JSON 快照 |
| `draft_payload_json` | `LONGTEXT` | 用户编辑草稿，可为空 |
| `confirmed_payload_json` | `LONGTEXT` | 用户确认后的最终 payload |
| `confirmed_by` | `VARCHAR(128)` | 确认人 |
| `confirmed_at` | `DATETIME(6)` | 确认时间 |
| `created_at` / `updated_at` | `DATETIME(6)` | 创建和更新时间 |

索引建议：

- 唯一索引：`hotel_id + task_id`
- 普通索引：`hotel_id + task_card_type`

说明：

- 第一版任务卡字段配置写在代码中，但通过 `TaskCardFieldDefinitionProvider` 之类的接口提供，避免 Controller / Service 直接依赖硬编码数组。
- 后续如果改为数据库配置，只替换 Provider 实现，不改任务核心流程。
- 字段契约从 `code-v1` 迁移到 `20260711-p0` 时，不能盲目改写已经存在用户草稿或确认结果的历史任务卡。当前 V18 仅更新 `draft_payload_json IS NULL` 且 `confirmed_payload_json IS NULL` 的旧任务卡；已存在 payload 的历史数据继续保留原 `field_contract_version`，待重新保存、最终确认或后续专项 backfill 时再迁移。

## 10. 审计表

表名：`workflow_reservation_audit_log`

| 字段 | 类型建议 | 中文说明 |
| --- | --- | --- |
| `id` | `BIGINT` | 审计 ID |
| `hotel_id` | `VARCHAR(64)` | 酒店或业务上下文 |
| `order_id` | `BIGINT` | 订单 ID，可为空 |
| `task_id` | `BIGINT` | 任务 ID，可为空 |
| `operation_id` | `BIGINT` | OPERA 模拟操作 ID，可为空 |
| `actor_type` | `VARCHAR(32)` | 操作人类型，例如 `SYSTEM`、`USER`、`SUPERAGENT` |
| `actor_id` | `VARCHAR(128)` | 操作人标识 |
| `action` | `VARCHAR(128)` | 操作类型 |
| `reason` | `VARCHAR(512)` | 操作原因 |
| `before_snapshot_json` | `LONGTEXT` | 变更前摘要 JSON |
| `after_snapshot_json` | `LONGTEXT` | 变更后摘要 JSON |
| `occurred_at` | `DATETIME(6)` | 发生时间 |
| `created_at` | `DATETIME(6)` | 记录创建时间 |

索引建议：

- 普通索引：`hotel_id + order_id + occurred_at`
- 普通索引：`hotel_id + task_id + occurred_at`
- 普通索引：`hotel_id + action + occurred_at`

审计不得保存 Secret、Token、完整邮件正文、真实附件 URL 或不必要的个人敏感信息。

## 11. OPERA 模拟逻辑操作表

表名：`workflow_reservation_opera_operation`

一条任务可能生成多条 OPERA 模拟逻辑操作。

| 字段 | 类型建议 | 中文说明 |
| --- | --- | --- |
| `id` | `BIGINT` | OPERA 模拟操作 ID |
| `hotel_id` | `VARCHAR(64)` | 酒店或业务上下文 |
| `order_id` | `BIGINT` | 订单 ID |
| `task_id` | `BIGINT` | 任务 ID |
| `operation_sequence` | `INT` | 同任务下操作顺序，从 1 开始 |
| `operation_code` | `VARCHAR(64)` | 模拟操作代码，第一版固定 `SIMULATE_PRECHECK` 和 `SIMULATE_WRITE` |
| `operation_name` | `VARCHAR(128)` | 模拟操作展示名称 |
| `operation_status` | `VARCHAR(32)` | 操作状态：`PENDING`、`SUCCEEDED`、`FAILED` |
| `request_payload_json` | `LONGTEXT` | 生成该操作时使用的模拟请求摘要，不保存完整邮件原文 |
| `attempt_count` | `INT` | 已执行 attempt 次数 |
| `last_attempt_id` | `BIGINT` | 最近一次 attempt ID |
| `last_error_message` | `VARCHAR(512)` | 最近一次失败原因摘要 |
| `created_at` / `updated_at` | `DATETIME(6)` | 创建和更新时间 |

索引建议：

- 唯一索引：`hotel_id + task_id + operation_sequence`
- 普通索引：`hotel_id + task_id + operation_status`
- 普通索引：`hotel_id + order_id + operation_sequence`

## 12. OPERA 模拟尝试记录表

表名：`workflow_reservation_opera_operation_attempt`

每次执行或重试都新增一条 attempt，不能覆盖历史。

| 字段 | 类型建议 | 中文说明 |
| --- | --- | --- |
| `id` | `BIGINT` | 尝试记录 ID |
| `hotel_id` | `VARCHAR(64)` | 酒店或业务上下文 |
| `order_id` | `BIGINT` | 订单 ID |
| `task_id` | `BIGINT` | 所属任务 |
| `operation_id` | `BIGINT` | 所属逻辑操作 |
| `attempt_number` | `INT` | 第几次尝试，从 1 开始 |
| `attempt_status` | `VARCHAR(32)` | 尝试状态：`SUCCEEDED`、`FAILED` |
| `request_payload_json` | `LONGTEXT` | 本次模拟请求 payload |
| `response_payload_json` | `LONGTEXT` | 本次模拟响应 payload |
| `error_message` | `VARCHAR(512)` | 失败原因摘要 |
| `started_at` | `DATETIME(6)` | 开始时间 |
| `finished_at` | `DATETIME(6)` | 结束时间 |
| `created_at` | `DATETIME(6)` | 记录创建时间 |

索引建议：

- 唯一索引：`hotel_id + operation_id + attempt_number`
- 普通索引：`hotel_id + task_id + created_at`
- 普通索引：`hotel_id + operation_id + attempt_number`

规则：

- 用户不能跳过失败的 OPERA 模拟操作。
- 失败后操作进入 `FAILED`，任务保持未完成；因为队列规则里任务 `FAILED` 视为已结束，第一版不能把 OPERA 操作失败直接落成任务 `FAILED`，避免后续任务被错误放行。
- 不允许用户强制把失败任务改成 `COMPLETED`。
- 后续真实 OPERA 接入时，应通过 adapter 把真实响应映射到 `response_payload_json`，业务号候选字段需在拿到真实结构后再补稳定字段或 JSON 结构。

## 13. 订单业务号回填

只有 `NEW_BOOKING` 且 `confirmed_payload_json` 没有可用业务号时，才需要从 OPERA 成功结果回填订单业务号。

当前第一版后端不做业务号回填，也没有落地 `business_key_candidates_json` 字段。后续拿到真实 OPERA 返回结构后，再通过低耦合 adapter 解析候选业务号并补充稳定字段或 JSON 结构。候选结构可以参考：

```json
{
  "key_type": "CONFIRMATION_NUMBER",
  "business_key": "CNF123456",
  "source": "OPERA_SIMULATION_RESULT",
  "raw_path": "reserved.for.future.real.opera.path"
}
```

说明：

- 当前没有真实 OPERA 返回样例，`raw_path` 只能预留。
- FIT Reservation 回填 `CONFIRMATION_NUMBER`。
- Group / Block / Allotment 回填 `GROUP_CODE` 或后续确认的对应业务号类型。

## 14. 第一版不落库的内容

以下内容第一版不建议单独建表：

- 任务卡字段矩阵配置：先写在代码 Provider 中。
- 任务阻塞状态：实时计算，不落 `BLOCKED`。
- OPERA 真实接口字段映射：后续真实系统接入后在 adapter 层补充。
- 全量 Excel 字段路径：后端引用 `docs/import/20260706/任务卡展示编辑矩阵.xlsx` 作为完整规则来源，不手抄成数据库。
- 前端展示 / 编辑白名单：V2 阶段前端引用 `docs/import/20260708/任务卡前端展示字段表 3.0.xlsx`；V3 起以 2026-07-11 P0 冻结基线中的前端 Excel 和路由说明为准。前端白名单不作为后端校验和 OPERA 映射的替代来源。

## 15. 待确认问题

- `ENDED` 是否仅表示取消完成，还是也包含正常完成后的历史订单。
- 临时订单逻辑删除前是否需要保留空订单一段时间。
- OPERA 真实响应字段出现后，是否需要新增稳定字段而不是只放 JSON。
- 任务卡字段代码 Provider 的具体类名和包路径，后续实现时按项目规范确认。
