Files
th-hotel-simple/docs/project/requirements/M002-backend-data-model-design.md
2026-07-12 01:27:42 +08:00

414 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 冻结基线。后续数据模型扩展必须支持结构化 `S10/S99``message_events[]`、42 路由、方案 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 的具体类名和包路径,后续实现时按项目规范确认。