Files
th-hotel-simple/docs/project/requirements/M002-v4-real-catalog-lookup-api-design.md

580 lines
33 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 V4 真实目录与 Lookup API 设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 文档版本 | 0.4 |
| 日期 | 2026-07-20 |
| 状态 | CP11 已落地第一版数据库目录与 lookup API目录管理后台 CP1 已落地后端接口Account + booking type 过滤 Rate Code 和 Room Information 早餐派生已确认为下一阶段待实现契约;真实 PMS 同步继续后置 |
| 适用范围 | M002 V4 Account、Market、Source、Room Type、Rate Code 目录来源、Room Information 早餐派生、数据模型、前端 lookup、缓存、酒店隔离、权限和失败兜底 |
| 不适用范围 | 真实 OPERA / OHIP 写操作、真实价格计算、房型 / 日期 / 价格级 Rate Code 适用规则、前端页面实现、SuperAgent Prompt 修改、目录同步任务、SuperAgent 机器目录接口 |
## 1. 文档定位
M002 V4 CP8 已实现第一版固定种子目录校验M002 V4 CP11 已把该目录迁移为数据库目录和前端 lookup API
- Basic Information 的 `account_code` 必须存在于当前酒店数据库 Account 目录。
- Market / Source 由 Account 派生,不由 SuperAgent 输出。
- Room Type 第一版使用当前酒店数据库目录校验和字段选项提示。
- Rate Code 当前 CP11 实现仍是酒店级目录校验和字段选项提示;下一阶段已确认需要按订单级 Account + `booking_type``GROUP` / `FIT`)过滤候选和校验适用性。
- Room Information 卡中 Breakfast 第一版由后端派生Group 固定含早Fit 按 Rate Code 中 `RB` / `RO` 判断,无法派生时要求用户在卡片中必填确认。
- 目录错误会让对应 V4 卡片进入 `REVIEW_REQUIRED`,用户通过复核解阻选择合法 code。
本文记录真实目录和 lookup API 的设计与 CP11 第一版实现。CP11 新增 `workflow_reservation_catalog_account``workflow_reservation_catalog_code` 两张表,并通过 Flyway 初始化 `HOTEL-TEST``HOTEL-DEV` 以及迁移执行时已存在的 `ACTIVE` 平台酒店的固定种子目录;同时新增 `ReservationV4CatalogBootstrapRunner`,在平台默认酒店由启动流程创建后,如果该酒店目录为空,会补一份 `FIXED_SEED_IMPORT` 初始化目录。不再在运行时代码中把固定种子作为全局目录事实。目录管理后台 CP1 已新增 Account、Room Type、Rate Code 后端查询、新增、启用 / 停用接口;`workflow_reservation_catalog_sync_run`、真实 PMS / OPERA / OHIP 同步、Market / Source 独立管理页面和 SuperAgent 机器目录供给接口继续后置。
后续若本文与 `M002-v4-agent-callback-field-contract.md` 的 Agent 输入字段冲突,以 Agent 字段契约为准;若与 `security-access-control-boundary.md` 的接口权限冲突,以安全边界为准。
## 2. 当前固定种子目录
CP11 之前固定种子实现位于后端 `ReservationV4DirectoryService``FixedReservationV4DirectoryServiceImpl`。CP11 起实现改为 `ReservationV4DatabaseDirectoryServiceImpl`,目录读取只走数据库目录表;固定种子通过 `V24__create_reservation_catalog_tables.sql` 初始化导入 dev/test 基准酒店和迁移时已有 `ACTIVE` 酒店,并由 `ReservationV4CatalogBootstrapRunner` 补齐迁移后才创建的默认 `ACTIVE` 酒店,来源标记为 `FIXED_SEED_IMPORT`
| 目录 | 当前用途 | 当前固定值 | 当前限制 |
| --- | --- | --- | --- |
| Account | Basic Information 可选目录SuperAgent 和用户提交都使用稳定 code | `QBD_TRAVEL``LIAN_TAI``HANATOUR_TD` | 已进入数据库初始化目录,仍不是 PMS 全量 Account后台 CP1 可新增、启用 / 停用 |
| Market | 由 Account 派生的订单级 Market | 当前 Account 均派生 `LEISURE` | 已进入通用代码目录,前端仍不直接编辑 |
| Source | 由 Account 派生的订单级 Source | 当前 Account 均派生 `TRAVEL_AGENT` | 已进入通用代码目录,前端仍不直接编辑 |
| Room Type | 房型 code 校验和字段选项提示 | `TWN``KING``DBL``SGL``TRP``RM1``RM2``RM3` | 已进入数据库初始化目录,仍不是 PMS 全量房型;后台 CP1 可新增、启用 / 停用 |
| Rate Code | Rate Code 校验和字段选项提示 | `BAR``RACK``PACKAGE``GROUP``FIT` | 已进入数据库初始化目录;当前实现仍是酒店级目录,下一阶段改为 Account + booking type 适用范围过滤;不按房型、日期或价格过滤;后台 CP1 可新增、启用 / 停用 |
当前固定种子只能支撑开发和演示闭环,不能作为生产长期事实源。
## 3. 设计目标
真实目录能力要解决以下问题:
1. 前端不再硬编码 Account、Room Type、Rate Code 选项。
2. 后端确认和复核仍然是最终校验者,前端 lookup 只用于选择体验。
3. 每个目录都按 `hotel_id` 隔离,不能跨酒店泄露目录。
4. 目录有来源、版本、更新时间和启停状态,便于排查 SuperAgent 输出 code 与系统目录不一致的问题。
5. PMS / OPERA / OHIP 不稳定或暂未接入时,系统仍可使用最后一次成功目录快照或系统管理目录兜底。
6. 固定种子目录可以作为 dev/test 或导入初始化兜底,但生产不应默认靠代码固定值。
7. Rate Code 不再作为全酒店通用下拉;业务卡 Rate Code 候选必须先受当前订单 Basic Information 的 Account 和 event `booking_type` 限定。
## 4. 目录来源分层
目录来源按阶段分三层,后续可以逐步切换,不要求一步接真实 PMS。
| 阶段 | 来源 | 中文说明 | 适用目录 |
| --- | --- | --- | --- |
| Phase 0 | `FIXED_SEED_IMPORT` | CP11 已将固定种子通过 Flyway 和启动补种子流程导入数据库,不再作为运行时代码全局 Map | 当前 Account、Market、Source、Room Type、Rate Code |
| Phase 1 | `SYSTEM_MANAGED` / `IMPORT_FILE` | 本系统数据库目录,由初始化脚本、管理后台或受控导入文件维护;`IMPORT_FILE` 用于 OWNER RATE 等人工确认过的目录 / 适用关系导入 | Account、Market、Source也可临时维护 Room Type、Rate Code 和 Account + booking type 的 Rate Code 适用关系 |
| Phase 2 | `PMS_SYNC` | 后端从 PMS / OPERA / OHIP 同步目录到本系统本地表,业务查询只读本地快照 | Room Type、Rate Code 优先Account、Market、Source 视 PMS 能力再接 |
原则:
- 业务确认接口只依赖本系统目录服务,不直接调用外部 PMS。
- 真实 PMS / OHIP Adapter 只负责同步目录快照,不把厂商 DTO 直接暴露给业务服务或前端。
- 同一目录可有 `source_system` 标记,但业务接口只使用稳定 `code`
## 5. 目录归属建议
| 目录 | 第一版真实来源建议 | 未来 PMS / OPERA / OHIP 方向 | 前端是否可编辑 | 说明 |
| --- | --- | --- | --- | --- |
| Account | 本系统管理目录优先 | 可选同步 PMS profile / company / travel agent 主数据,但本系统仍维护映射 code | 是Basic Information 选择 Account | Account 影响 Market / Source 派生,第一版建议先由系统管理维护,避免 PMS profile 字段未确认导致业务不可用 |
| Market | 本系统管理目录 | 可选同步 PMS market code 配置 | 否,随 Account 派生展示 | 前端不直接改 Market修改 Account 后后端派生 Market |
| Source | 本系统管理目录 | 可选同步 PMS source code 配置 | 否,随 Account 派生展示 | 前端不直接改 Source修改 Account 后后端派生 Source |
| Room Type | PMS / OPERA / OHIP 同步目录优先 | 同步酒店有效房型、展示名、人数、启停状态 | 是,业务卡选择房型 | 若 PMS 未接入,可临时由系统管理维护或固定种子初始化 |
| Rate Code | PMS / OPERA / OHIP 同步目录优先;在 PMS 未接前可由系统管理或导入文件维护 Account 适用关系 | 同步有效 Rate Plan / Rate CodeAccount、booking type、房型、日期和价格适用规则逐步扩展 | 是New Booking 选择 Rate Code | 下一阶段先按 Account + GROUP/FIT 过滤候选,只选 code不做价格计算 |
Department 目录在 V4 字段契约中也会被 Trace 使用。当前任务详情页第一版先固定 `FO``HSK``FO+HSK` 三个 Department code用于 Trace 卡下拉和 SuperAgent 输出约束;这不是正式数据库目录。后续可沿用本文模型扩展 `DEPARTMENT`、lookup API、系统管理维护和目录校验不放入本 checkpoint。
## 6. 数据模型草案
### 6.1 推荐表:`workflow_reservation_catalog_account`
Account 单独建表,原因是它不仅有显示名称,还要派生 Market / Source。
| 字段 | 中文说明 |
| --- | --- |
| `id` | 内部主键 ID |
| `hotel_id` | 酒店 ID目录按酒店隔离 |
| `account_code` | Account 稳定 code大小写敏感 |
| `account_name` | Account 显示名称 |
| `market_code` | 该 Account 派生的 Market code |
| `source_code` | 该 Account 派生的 Source code |
| `status` | `ACTIVE` / `DISABLED` |
| `source_system` | `SYSTEM_MANAGED` / `PMS_SYNC` / `FIXED_SEED_IMPORT` |
| `external_account_id` | 外部 PMS profile 或 account ID可为空 |
| `catalog_version` | 目录版本,用于前端缓存和排查 |
| `last_synced_at` | 外部同步成功 UTC 时间;系统管理数据可为空 |
| `metadata_json` | 扩展元数据,不替代可查询字段 |
| `version` | 乐观锁版本 |
| `created_at` / `updated_at` | UTC 创建和更新时间 |
| `logic_deleted_at` / `logic_deleted_reason` | 逻辑删除时间和原因 |
建议唯一约束:
```text
uk_reservation_catalog_account_code(hotel_id, account_code)
```
### 6.2 推荐表:`workflow_reservation_catalog_code`
Market、Source、Room Type、Rate Code 可先使用统一 code 表。
| 字段 | 中文说明 |
| --- | --- |
| `id` | 内部主键 ID |
| `hotel_id` | 酒店 ID目录按酒店隔离 |
| `catalog_type` | `MARKET` / `SOURCE` / `ROOM_TYPE` / `RATE_CODE` |
| `code` | 稳定目录 code大小写敏感 |
| `display_name` | 前端显示名称 |
| `status` | `ACTIVE` / `DISABLED` |
| `source_system` | `SYSTEM_MANAGED` / `PMS_SYNC` / `FIXED_SEED_IMPORT` |
| `external_id` | 外部 PMS / OPERA / OHIP ID可为空 |
| `sort_order` | 前端默认排序 |
| `effective_from` / `effective_to` | 生效日期范围,可为空;酒店本地业务日期语义 |
| `catalog_version` | 目录版本,用于前端缓存和排查 |
| `last_synced_at` | 外部同步成功 UTC 时间 |
| `metadata_json` | 扩展元数据例如房型人数、Rate Code 适用说明 |
| `version` | 乐观锁版本 |
| `created_at` / `updated_at` | UTC 创建和更新时间 |
| `logic_deleted_at` / `logic_deleted_reason` | 逻辑删除时间和原因 |
建议唯一约束:
```text
uk_reservation_catalog_code(hotel_id, catalog_type, code)
```
### 6.3 推荐表:`workflow_reservation_rate_code_applicability`
Rate Code 本身仍保存在 `workflow_reservation_catalog_code` 中,`catalog_type=RATE_CODE` 表示该 code 是当前酒店已知的 Rate Code是否对某个 Account、GROUP/FIT 可用,由适用关系表表达。这样可以避免在 `metadata_json` 中写不可查询的业务规则,也避免前端硬编码 OWNER RATE Excel。
本 checkpoint 只确认 Account + booking type 粒度,不纳入房型、日期和价格规则。后续如果需要按 Room Type 或入住日期进一步过滤,应在此表或独立价格规则表上扩展,不改变前端只提交稳定 `rate_code` 的基本原则。
| 字段 | 中文说明 |
| --- | --- |
| `id` | 内部主键 ID |
| `hotel_id` | 酒店 ID必须和 Account、Rate Code 同酒店 |
| `account_code` | Reservation Account 稳定 code引用当前酒店 Account 目录 |
| `booking_type` | `GROUP` / `FIT`;用于区分团队价和散客价候选 |
| `rate_code` | Rate Code 稳定 code引用当前酒店 `RATE_CODE` 目录 |
| `status` | `ACTIVE` / `DISABLED`;普通 lookup 只返回 `ACTIVE` |
| `source_system` | `SYSTEM_MANAGED` / `IMPORT_FILE` / `PMS_SYNC` / `FIXED_SEED_IMPORT` |
| `catalog_version` | 适用关系版本,用于排查和前端非阻塞提示 |
| `sort_order` | 同一 Account + booking type 下的默认展示排序 |
| `metadata_json` | 扩展说明,例如导入文件行号、业务备注;不得作为唯一可查询条件 |
| `version` | 乐观锁版本 |
| `created_at` / `updated_at` | UTC 创建和更新时间 |
| `logic_deleted_at` / `logic_deleted_reason` | 逻辑删除时间和原因 |
建议唯一约束:
```text
uk_reservation_rate_code_applicability(hotel_id, account_code, booking_type, rate_code)
```
建议外键 / 逻辑校验:
- `account_code` 必须存在于同酒店 `workflow_reservation_catalog_account` 且为 `ACTIVE`,否则不进入普通 lookup。
- `rate_code` 必须存在于同酒店 `workflow_reservation_catalog_code``catalog_type=RATE_CODE``status=ACTIVE`,否则不进入普通 lookup。
- 停用 Account 或 Rate Code 后,普通 Rate Code lookup 不再返回对应适用关系;已确认历史卡片不回滚。
### 6.4 Rate Code 早餐派生
Room Information 卡需要展示 `breakfast_included` 布尔字段,但该字段不由 SuperAgent 输出。
第一版派生规则:
- Group 固定含早,`breakfast_included=true`,前端显示勾选且只读。
- Fit 根据最终 Rate Code 派生Rate Code 中包含 `RB``breakfast_included=true`,包含 `RO``breakfast_included=false`
- Fit Rate Code 同时无法命中 `RB` / `RO` 时,后端应把 `breakfast_included` 标为未解决,前端显示必填勾选框,由用户确认是否含早。
- 该规则只用于 Room Information 卡展示和确认,不代表真实价格计算,也不替代 PMS Rate Plan 规则。
- 后续如果目录表显式维护早餐属性,可优先使用目录元数据;但普通前端仍消费布尔 `breakfast_included`,不要自行按 Rate Code 字符串猜测。
### 6.5 推荐表:`workflow_reservation_catalog_sync_run`
如果接 PMS / OPERA / OHIP 同步,建议记录每次同步运行。
CP11 本轮没有创建该表。原因是当前不接真实 PMS / OPERA / OHIP 同步暂时没有同步运行事实可记录CP13 目录管理后台 CP1 也只维护系统管理目录项,不产生外部同步运行事实。后续做 PMS / OPERA / OHIP 同步 worker 时,再新增该表和对应 Repository。
| 字段 | 中文说明 |
| --- | --- |
| `id` | 同步运行 ID |
| `hotel_id` | 酒店 ID |
| `catalog_type` | 同步目录类型 |
| `source_system` | 外部来源系统 |
| `sync_status` | `SUCCESS` / `FAILED` / `PARTIAL_SUCCESS` |
| `started_at` / `finished_at` | UTC 开始和结束时间 |
| `catalog_version` | 本次生成的目录版本 |
| `items_seen_count` | 外部返回数量 |
| `items_upserted_count` | 本系统写入或更新数量 |
| `items_disabled_count` | 本系统停用数量 |
| `safe_error_summary` | 安全错误摘要,不保存 Secret 或完整外部响应 |
| `created_at` / `updated_at` | UTC 创建和更新时间 |
同步 run 是技术追踪,不直接给普通业务前端展示完整细节。管理后台后续如展示同步记录,应单独登记权限和脱敏规则。
## 7. 后端服务边界
CP11 已替换当前 `FixedReservationV4DirectoryServiceImpl`,保留 `ReservationV4DirectoryService` 作为业务稳定端口。
```text
workflows.reservation.service
ReservationV4DirectoryService
- findAccount(hotelId, accountCode)
- isKnownRoomTypeCode(hotelId, roomTypeCode)
- isKnownRateCode(hotelId, rateCode)
- isRateCodeApplicable(hotelId, accountCode, bookingType, rateCode)
workflows.reservation.repository
ReservationV4CatalogRepository
- 只封装本地目录表查询,不直接调用 PMS
workflows.reservation.service
ReservationV4CatalogLookupService
- listAccounts(...)
- listRoomTypes(...)
- listRateCodes(...)
- listRateCodesByAccountAndBookingType(...)
integrations.ohip / integrations.pms
CatalogSyncAdapter
- 后续从 PMS / OPERA / OHIP 拉取目录并转换为本系统目录草稿
```
规则:
- V4 入站、确认、复核只调用 `ReservationV4DirectoryService`
- Basic Information 的 `account_code` 先确认或在同次复核中修正后,业务卡 Rate Code 才能按该 Account + `booking_type` 做适用性校验。
- 前端 lookup Controller 调用 `ReservationV4CatalogLookupService`,该服务同样只读本地目录表。
- PMS / OHIP 同步 Adapter 只能写本地目录表或同步 run不直接参与用户确认事务。
- 如果目录服务不可用,确认接口 fail closed不接受自由文本。
## 8. Lookup API 草案
Lookup API 属于前端业务查询接口,不给 SuperAgent 或 AgentBus 调用。
统一要求:
- 分类:`FRONTEND_USER`
- 鉴权Bearer session token。
- 权限:第一版建议复用 `RESERVATION_TASK_READ`;目录维护后台另行使用管理权限。
- 酒店隔离:`hotel_id` 可选;不传时按当前用户默认酒店解析,传入时必须校验用户可访问。
- 返回:只返回目录 code、显示名、状态、来源和安全元数据不返回外部 PMS 原始响应。
- 分页:`page_num` 从 1 开始,`page_size` 后端限制最大值。
- 搜索:`keyword` 匹配稳定 code 时,后端先按 `Locale.ROOT` 大写归一化后查询,因此前端或管理后台传小写 code 也能命中;匹配显示名时仍按数据库比较规则,不额外做大小写归一化。
- `keyword` 无匹配时,`items=[]``page.total=0`;如果当前酒店未过滤的 ACTIVE 目录仍存在,`catalog_source` / `catalog_version` 继续返回真实目录元数据,不把筛选无结果误报为 `DATABASE_EMPTY`
### 8.1 Account Lookup
```text
GET /api/reservation/lookups/accounts
```
查询参数:
| 参数 | 必需 | 中文说明 |
| --- | --- | --- |
| `hotel_id` | 否 | 当前酒店 ID未传时按当前用户默认酒店 |
| `keyword` | 否 | 匹配 `account_code``account_name` |
| `page_num` / `page_size` | 否 | 分页 |
第一版固定只返回 `ACTIVE` Account不开放 `active_only=false`
响应草案:
```json
{
"hotel_id": "HOTEL-TEST",
"catalog_type": "ACCOUNT",
"catalog_source": "FIXED_SEED_IMPORT",
"catalog_version": "seed-20260719-v1",
"stale": false,
"items": [
{
"code": "QBD_TRAVEL",
"display_name": "Q.B.D. TRAVEL GROUP CO., LTD",
"status": "ACTIVE",
"catalog_source": "FIXED_SEED_IMPORT",
"market_code": "LEISURE",
"market_name": "LEISURE",
"source_code": "TRAVEL_AGENT",
"source_name": "TRAVEL_AGENT"
}
],
"page": {
"page_num": 1,
"page_size": 20,
"total": 1
},
"warnings": []
}
```
前端用途:
- `fields[].options_source=reservation_v4_account_catalog` 时调用。
- 用户选择 Account 后,前端可以立即展示响应中的 Market / Source最终以后端确认接口派生结果为准。
- 不允许用户自由输入 Account code。
### 8.2 Room Type Lookup
```text
GET /api/reservation/lookups/room-types
```
查询参数:
| 参数 | 必需 | 中文说明 |
| --- | --- | --- |
| `hotel_id` | 否 | 当前酒店 ID |
| `keyword` | 否 | 匹配房型 code 或显示名 |
| `page_num` / `page_size` | 否 | 分页 |
第一版固定只返回 `ACTIVE` Room Type不接 `arrival_date` / `departure_date`,日期适用范围过滤后置。
响应草案:
```json
{
"hotel_id": "HOTEL-TEST",
"catalog_type": "ROOM_TYPE",
"catalog_source": "FIXED_SEED_IMPORT",
"catalog_version": "seed-20260719-v1",
"stale": false,
"items": [
{
"code": "RM2",
"display_name": "RM2",
"status": "ACTIVE",
"catalog_source": "FIXED_SEED_IMPORT",
"adult_capacity": 2
}
],
"page": {
"page_num": 1,
"page_size": 20,
"total": 1
},
"warnings": []
}
```
前端用途:
- `fields[].options_source=reservation_v4_room_type_catalog` 时调用。
- V4 业务卡里的 `room_items[].room_type_code``business_fields.after.room_items[].room_type_code`、加床目标房型等都应从该接口选。
- 前端不要把旧固定种子当 PMS 全量房型。
### 8.3 Rate Code Lookup
```text
GET /api/reservation/lookups/rate-codes
```
查询参数:
| 参数 | 必需 | 中文说明 |
| --- | --- | --- |
| `hotel_id` | 否 | 当前酒店 ID |
| `account_code` | 是 | 已选择或已确认的 Reservation Account code必须属于当前酒店 ACTIVE Account 目录 |
| `booking_type` | 是 | `GROUP` / `FIT`;取自当前业务 event 的 `target_order.booking_type` |
| `keyword` | 否 | 匹配 Rate Code 或显示名 |
| `page_num` / `page_size` | 否 | 分页 |
下一阶段 Rate Code lookup 必须按 `account_code + booking_type` 返回 `ACTIVE` 且适用的 Rate Code。`account_code` 未传、无效或不属于当前酒店时返回 400`booking_type``GROUP` / `FIT` 时返回 400Account 合法但当前无适用 Rate Code 时返回空 `items[]`。前端在 Account 未选时不应拉全酒店 Rate Code。`arrival_date` / `departure_date`、房型和价格过滤后置。
响应草案:
```json
{
"hotel_id": "HOTEL-TEST",
"catalog_type": "RATE_CODE",
"account_code": "QBD_TRAVEL",
"booking_type": "GROUP",
"catalog_source": "IMPORT_FILE",
"catalog_version": "owner-rate-20260720-v1",
"stale": false,
"items": [
{
"code": "GRPA1",
"display_name": "GRPA1",
"status": "ACTIVE",
"catalog_source": "IMPORT_FILE",
"pricing_available": false
}
],
"page": {
"page_num": 1,
"page_size": 20,
"total": 1
},
"warnings": []
}
```
前端用途:
- `fields[].options_source=reservation_v4_rate_code_catalog` 时调用。
- 只能在当前订单 Basic Information 已有有效 Account且当前业务 event 有 `booking_type` 时调用Account 切换后必须清空或重新校验已选 Rate Code。
- 下一阶段只选 `rate_code`,不展示或计算真实价格。
- V4 契约仍禁止 `UPDATE_BOOKING` 携带 Rate Codelookup API 不改变该规则。
### 8.4 Market / Source Lookup
Market / Source 第一版不作为用户可编辑字段,不建议给普通业务表单单独开放选择。
如果后续系统管理后台需要维护 Market / Source可使用管理接口或通用目录接口但不应让 V4 Basic Information 页面绕过 Account 派生规则。
## 9. `fields[]` 与 lookup 的关系
后端任务详情字段仍是前端渲染白名单。Lookup API 只解决选项来源,不决定字段是否展示或可编辑。
| `options_source` | 对应 lookup | 前端行为 |
| --- | --- | --- |
| `reservation_v4_account_catalog` | `GET /api/reservation/lookups/accounts` | 渲染 Account 下拉 / 搜索选择,展示派生 Market / Source |
| `reservation_v4_room_type_catalog` | `GET /api/reservation/lookups/room-types` | 渲染房型搜索选择 |
| `reservation_v4_rate_code_catalog` | `GET /api/reservation/lookups/rate-codes?account_code=...&booking_type=...` | 渲染当前 Account + GROUP/FIT 适用的 Rate Code 搜索选择 |
| `static_enum` | 使用 `fields[].enum_options` | 不调用 lookup |
| `system_case_lookup` | 后续订单 / 任务对象 lookup | 不属于本目录 checkpoint |
前端提交时仍按卡片确认或复核接口提交字段值。后端确认前再次校验目录,不能因为前端选项来自 lookup 就跳过后端校验。
## 10. 缓存设计
缓存是优化,不是事实源。本系统本地目录表才是业务查询事实源。
后续建议。CP11 第一版暂不加内存缓存,直接读取本系统本地目录表;原因是当前种子目录规模很小,先保证目录事实源、酒店隔离和确认校验一致。
- 目录 Service 增加内存缓存,缓存 key 包含 `hotel_id``catalog_type``keyword``active_only`、分页和过滤参数。
- 精确校验类查询,例如 `findAccount(hotelId, accountCode)``isKnownRoomTypeCode(hotelId, code)`,使用单独短 TTL 缓存。
- TTL 建议 5 分钟;真实同步后可根据 `catalog_version` 主动失效。
- 管理后台修改目录或同步 run 成功后,清理对应酒店和目录类型缓存。
- 多节点部署时,第一版可以依赖短 TTL后续如目录变更频繁再接 Redis 或事件广播失效。
响应应返回:
| 字段 | 中文说明 |
| --- | --- |
| `catalog_version` | 当前目录版本,前端可用于调试和避免重复请求 |
| `catalog_source` | 当前目录主要来源,例如 `SYSTEM_MANAGED` / `PMS_SYNC` |
| `stale` | 当前是否为过期但可用的最后成功快照 |
| `warnings[]` | 非阻塞警告,例如 PMS 同步失败但仍返回本地快照 |
## 11. 失败兜底
| 场景 | Lookup API 行为 | 确认 / 复核行为 |
| --- | --- | --- |
| 目录有本地 Active 快照 | 返回 200 和可选项 | 按目录校验,通过后确认 |
| PMS 同步失败,但有上次成功快照 | 返回 200`stale=true`,带 warning | 仍可按本地快照确认,并在审计或 payload 中保留目录版本 |
| PMS 同步失败且无本地快照 | 返回 200 空列表和 warning或按后续实现返回明确错误 | 必填目录字段 fail closed返回 `V4_CATALOG_UNAVAILABLE``V4_FIELD_VALIDATION_FAILED` |
| SuperAgent 输出未知非空 code | 任务卡进入 `REVIEW_REQUIRED``fields[].validation_errors` 指向该字段 | 用户必须选择已知 code不能自由输入原值 |
| 目录 code 后续停用 | 已确认卡保持历史确认快照不回滚 | 新确认不能选择停用 code除非后续设计允许历史兼容选择 |
| 前端提交未返回字段或自由 code | 后端忽略未开放字段;目录字段校验失败 | 不写入 `confirmed_payload_json` |
生产建议:
- `FIXED_SEED` 可用于 dev/test 和初始化导入。
- 生产如果真实目录为空,不应静默接受固定种子;应让卡片进入复核或目录不可用错误,避免写入错误 PMS code。
## 12. 权限、酒店隔离和审计
### 12.1 Lookup 查询权限
第一版 lookup 查询建议:
| 接口 | 分类 | 权限 | 酒店隔离 | 审计 |
| --- | --- | --- | --- | --- |
| `GET /api/reservation/lookups/accounts` | `FRONTEND_USER` | `RESERVATION_TASK_READ` | 按用户可访问酒店校验 | 只读不写业务审计 |
| `GET /api/reservation/lookups/room-types` | `FRONTEND_USER` | `RESERVATION_TASK_READ` | 按用户可访问酒店校验 | 只读不写业务审计 |
| `GET /api/reservation/lookups/rate-codes` | `FRONTEND_USER` | `RESERVATION_TASK_READ` | 按用户可访问酒店校验;`account_code``booking_type` 只能用于当前酒店目录过滤 | 只读不写业务审计 |
复用 `RESERVATION_TASK_READ` 的原因:
- Lookup 是任务详情表单选项的辅助查询能力。
- 目录值本身不包含邮件正文、附件、AI payload 或 PMS Secret。
- 可以避免第一版为了表单下拉再新增一个普通用户权限码,降低前端角色配置复杂度。
### 12.2 目录维护权限
目录管理后台 CP1 已实现后端接口。该能力属于 `FRONTEND_ADMIN`,使用管理后台错误响应和管理审计,不复用普通 lookup 权限。
```text
RESERVATION_CATALOG_MANAGE
```
已实现接口:
| 接口 | 中文说明 | 权限 | 酒店隔离 | 审计 |
| --- | --- | --- | --- | --- |
| `GET /api/admin/reservation/catalogs/accounts` | Account 管理列表 | `RESERVATION_CATALOG_MANAGE` | `hotel_id` 必须在当前用户可访问酒店内;不传时使用默认酒店 | 只读不写审计 |
| `POST /api/admin/reservation/catalogs/accounts` | 新增 Account | `RESERVATION_CATALOG_MANAGE` | 按请求 `hotel_id` 校验 | 写平台管理审计 |
| `PUT /api/admin/reservation/catalogs/accounts/{accountId}/status` | 启用 / 停用 Account | `RESERVATION_CATALOG_MANAGE` | 按记录所属酒店校验 | 状态实际变化时写平台管理审计;相同状态幂等返回,不新增审计 |
| `GET /api/admin/reservation/catalogs/room-types` | Room Type 管理列表 | `RESERVATION_CATALOG_MANAGE` | 同上 | 只读不写审计 |
| `POST /api/admin/reservation/catalogs/room-types` | 新增 Room Type | `RESERVATION_CATALOG_MANAGE` | 按请求 `hotel_id` 校验 | 写平台管理审计 |
| `PUT /api/admin/reservation/catalogs/room-types/{catalogId}/status` | 启用 / 停用 Room Type | `RESERVATION_CATALOG_MANAGE` | 按记录所属酒店校验 | 状态实际变化时写平台管理审计;相同状态幂等返回,不新增审计 |
| `GET /api/admin/reservation/catalogs/rate-codes` | Rate Code 管理列表 | `RESERVATION_CATALOG_MANAGE` | 同上 | 只读不写审计 |
| `POST /api/admin/reservation/catalogs/rate-codes` | 新增 Rate Code | `RESERVATION_CATALOG_MANAGE` | 按请求 `hotel_id` 校验 | 写平台管理审计 |
| `PUT /api/admin/reservation/catalogs/rate-codes/{catalogId}/status` | 启用 / 停用 Rate Code | `RESERVATION_CATALOG_MANAGE` | 按记录所属酒店校验 | 状态实际变化时写平台管理审计;相同状态幂等返回,不新增审计 |
CP1 限制:
- 新增目录默认 `ACTIVE``source_system=SYSTEM_MANAGED`
- 管理列表可按 `status=ACTIVE/DISABLED` 过滤;普通 lookup 仍只返回 `ACTIVE`
- 管理列表 `keyword` 搜索稳定 code 时大小写不敏感,显示名仍按数据库比较规则。
- Account 新增时 `market_code``source_code` 必须是当前酒店 `ACTIVE` 的 Market / Source code。
- Market / Source 独立管理页面暂不做,仍沿用当前初始化目录。
- 停用目录不会回滚已确认历史卡片,但新确认 / 复核会按当前 ACTIVE 目录校验。
- 后续如做前端管理页面,应在系统设置下使用该权限码控制入口。
### 12.3 第三方接口边界
SuperAgent 当前不调用本 lookup API。SuperAgent 目录供给后续有两种方式:
1. 线下或配置文件方式把目录版本给 SuperAgent。
2. 单独设计机器接口,例如 `GET /api/integrations/superagent/catalogs/...`,使用 HMAC 鉴权和最小字段。
不得让 SuperAgent 使用前端 Bearer token 或前端 lookup API。
## 13. 前端使用方式
前端建议:
1. 先读取 V4 订单任务详情。
2. 遍历每张卡 `fields[]`
3. 只有字段 `editable=true``control_type=select/lookup` 时才加载 lookup。
4. 根据 `options_source` 选择 lookup 接口。
5. Rate Code 字段必须先取得当前订单 Basic Information 的 Account 和当前业务 event 的 `booking_type`;缺任一条件时禁用或显示空态,不调用全酒店 Rate Code 全量查询。
6. 搜索输入做 debounce不一次性拉全量。
7. 显示 `stale=true``warnings[]` 时给用户非阻塞提醒。
8. 用户提交确认或复核时只提交 code不提交显示名、Market / Source 派生值或目录完整对象。
9. 确认成功后以后端返回的 `confirmed_payload_json` / 刷新详情为准更新页面。
前端禁止:
- 硬编码 PMS 房型或 Rate Code 全集。
- 硬编码 OWNER RATE Excel 中 Account 到 Rate Code 的映射;该映射必须由后端目录 / 适用关系接口提供。
- 把目录显示名当业务 code 提交。
- 绕过 `fields[]` 自行补业务字段。
- 使用 lookup API 给 SuperAgent、AgentBus 或 Debug 链路拼接输入。
- 在浏览器保存目录中的外部 PMS ID、同步错误详情或任何 Secret。
## 14. 后续开发 checkpoint
| Checkpoint | 目标 | 主要交付 |
| --- | --- | --- |
| M002-V4-CP11 | DB 管理目录与 Lookup API V1 | 新增 Account / Code 目录表、Repository、DirectoryService DB 实现、Account / Room Type / Rate Code lookup 查询接口、权限和测试 |
| M002-V4-CP12 | 前端 Lookup 接入 | V4 卡片字段渲染按 `options_source` 调用 lookup替换固定种子硬编码选项处理 stale / warning / 空目录 |
| M002-V4-CP13 | 目录管理后台 V1 | CP1 已完成 Account / Room Type / Rate Code 前后端列表、新增、启用 / 停用闭环、`RESERVATION_CATALOG_MANAGE` 权限和管理审计Market / Source 独立管理后置 |
| M002-V4-CP14 | 订单列表 V4 继续处理入口 | 已完成:订单列表返回 V4 下一步订单任务、卡片、动作类型、动作状态和 open 数,前端可优先跳 V4 订单任务详情 |
| M002-V4-CP14.5 | Account 范围 Rate Code Lookup | 待实现:新增 Account + booking type 适用关系模型 / 导入种子Rate Code lookup 接收 `account_code``booking_type`,确认和复核校验 Rate Code 适用性,前端联动 Account 后展示候选 |
| M002-V4-CP15 | PMS / OPERA / OHIP 目录同步 | 同步 Adapter、同步 run 表、失败重试、最后成功快照、同步状态管理入口 |
| M002-V4-CP16 | SuperAgent 目录供给 | 明确目录版本如何给 SuperAgent必要时新增机器目录接口或导出包 |
CP11 已作为后端第一步落地,因为它不依赖真实 PMS也能让前端后续不再硬编码当前固定种子。CP13 CP1 继续沿用本地目录表,不接真实 PMS也不改变 SuperAgent 输入契约。
## 15. 仍需确认的问题
1. Account 的第一版真实维护入口已放在系统设置 `/system/reservation-catalogs`;前端第一版固定使用 `market_code=LEISURE``source_code=TRAVEL_AGENT`Market / Source 独立管理仍需后续确认。
2. Account code 是否继续使用本系统定义的稳定 code例如 `QBD_TRAVEL`,还是必须对齐 PMS profile code。
3. Market / Source 是否只允许随 Account 派生,还是未来允许用户在 Basic Information 中单独改选。
4. Room Type 第一版后端已支持系统管理维护;后续是否仍要接 PMS / OHIP 同步替换为主来源待确认。
5. Rate Code 已确认下一阶段按 `account_code + booking_type` 过滤;入住日期、房型和价格是否也要进入过滤仍待后续确认。
6. 生产是否允许 `FIXED_SEED` 作为兜底,还是只允许 dev/test 使用。
7. SuperAgent 是否需要读取目录;如果需要,是离线给目录包,还是新增 HMAC 机器接口。
在这些问题未确认前,当前实现仍按 DB 管理目录 + 前端 lookup 查询 + 后台 CP1 手工维护闭环推进,不接真实 PMS也不替换 SuperAgent 输入契约。