Files
th-hotel-simple/docs/project/requirements/M002-v4-real-catalog-lookup-api-design.md
T
2026-07-21 19:11:57 +07:00

39 KiB
Raw Blame History

M002 V4 真实目录与 Lookup API 设计

文档信息

项目 内容
文档版本 0.5
日期 2026-07-21
状态 CP11 已落地第一版数据库目录与 lookup API;目录管理后台 CP1 已落地后端接口;OWNER RATE RATECODE (2) 只读整理已确认 Room Type 第一版只维护 6 个稳定 code,Rate Code 第一阶段暂不建立 Account 适用关系;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 第一版使用当前酒店数据库目录校验和字段选项提示;2026-07-21 经需求方确认,正式业务目录先收敛为 RM2、RM3、RM4、SU1、SU2、SU3 六个稳定 code,不按 Account 限定房型可用范围。
  • Rate Code 当前 CP11 实现仍是酒店级目录校验和字段选项提示;2026-07-21 经需求方确认,第一阶段暂不建立 Account 与 Rate Code 的适用关系,先把 OWNER RATE RATECODE (2) 中已确认 Account 的 Rate Code 作为酒店级 RATE_CODE 目录候选维护。
  • 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 校验和字段选项提示 RM2、RM3、RM4、SU1、SU2、SU3 已通过 V25__align_owner_rate_catalog_data.sql 和启动补种子收敛为 OWNER RATE 第一阶段目录,仍不是 PMS 全量房型;后台 CP1 可新增、启用 / 停用
Rate Code Rate Code 校验和字段选项提示 OWNER RATE RATECODE (2) 的 40 个规范化 Rate Code 已通过 V25__align_owner_rate_catalog_data.sql 和启动补种子收敛为当前酒店级目录;第一阶段暂不建立 Account 适用关系,不按房型、日期或价格过滤;后台 CP1 可新增、启用 / 停用

当前固定种子只能支撑开发和演示闭环,不能作为生产长期事实源。

2.1 OWNER RATE RATECODE (2) 目录整理结论

本节来自 2026-07-21 对 /Users/andy/Downloads/OWNER RATE.xlsx 中 RATECODE (2) sheet 的只读整理。M002-V4-owner-rate-catalog-data-alignment 已把本节 6 个 Room Type 和 40 个 Rate Code 写入固定初始化种子与 V25__align_owner_rate_catalog_data.sql;不处理 RATECODE sheet,不新增 Account 适用关系,不改变 lookup 接口参数契约。

2.1.1 Room Type 第一阶段稳定集合

需求方已确认当前系统第一阶段只维护以下 6 个 Room Type code:

code 中文说明
RM2 高级花园景观大床房
RM3 高级花园景观双床房
RM4 高级花园景观家庭房
SU1 花园景观小套房(大床)
SU2 泳池景观小套房(大床)
SU3 家庭套房

结论:

  • 第一阶段不建立 Account -> Room Type 关系表。
  • 所有 Account 默认都可使用上述 6 个 Room Type。
  • RM2/RM3、SU1/SU2 这类 Excel 组合值在后续导入或人工维护时应拆成多个候选 code;前端和 SuperAgent 最终提交仍只能提交单个稳定 room_type_code。
  • 其它 Excel 文本,例如 Deluxe TWN、GLSPCB-1800,暂不进入第一阶段 Room Type 目录,除非后续人工确认映射到上述 6 个 code 或新增正式房型。

2.1.2 Q.B.D 与 LIAN TAI Rate Code 参考清单

Rate Code 取 RATECODE (2) sheet 的 E 列原值,仅做连字符两边空格清理,例如 GRPA2 - 850UP 规范化为 GRPA2-850UP;不拆分价格、餐食、早餐地点或其它说明,不强行派生 booking_type。

第一阶段暂不建立 Account -> Rate Code 适用关系。下表只记录来源 Account 下出现过的 Rate Code,便于后续导入酒店级 RATE_CODE 目录、人工核对或未来再建适用关系。

source_account_name 当前系统 Account code 参考 Rate Code 清单
Q.B.D 当前固定种子为 QBD_TRAVEL;是否改为更短 QBD 待确认 GRPA2-850UP、GRPA2-1275、GRPA2-1400、GRPA2-1800、GRPA2-2400、GRPA1-900、GRPA1-1400、GRPA1-1300、GRPA1-1150、GRPA1-1725、GRPA1-2300、GRPA3-1200*B'FAST BUALUANG、GRPA3-2000*B'FAST BUALUANG、GRPA3-1400*B'FAST BUALUANG、GRPA3-1800*B'FAST BUALUANG、GRPA3-2400*B'FAST BUALUANG、GRPA4-1200*B'FAST LEELA、GRPA4-2000*B'FAST LEELA、GRPA4-1400*B'FAST LEELA、GRPA4-1800*B'FAST LEELA、GRPA4-2400*B'FAST LEELA
LIAN TAI LIAN_TAI WHO1-850UP、WHO1-1275、WHO1-1400、WHO1-1800、WHO1-2400、GRP1-900、GRP1-1400、GRP1-1300、GRP1-1800、GRP1-2400、WHO2-1100、WHO2-1600、WHO2-1400、WHO2-1800、WHO2-2400、WHO3-1200、WHO3-1800、WHO3-1400、WHO3-2400

目录落地口径:

  • 当前固定初始化和 V25 已把上表 40 个去重值作为当前酒店 workflow_reservation_catalog_code.catalog_type=RATE_CODE 的候选目录维护,code 和 display_name 暂相同。
  • 这些值是 OWNER RATE 人工整理口径,不等同 PMS / OPERA / OHIP 的最终 Rate Plan code。
  • B'FAST BUALUANG、B'FAST LEELA、850UP、数字价格等内容第一阶段只作为 Rate Code 字符串的一部分保留,不单独进入价格、早餐或餐厅规则。
  • 后续如果需求方要求按 Account 限制 Rate Code,再新建适用关系表并从本节清单回填,不影响第一阶段酒店级目录校验。

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 第一阶段仍作为当前酒店级目录下拉;暂不按 Account、Room Type、入住日期或价格过滤。后续若需求方明确 Account 适用范围,再新增关系表和联动过滤。

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 与 Rate Code 的适用关系 Account、Market、Source;也可临时维护 Room 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 未接前可由系统管理或导入文件维护酒店级 Rate Code 目录 同步有效 Rate Plan / Rate Code;Account、booking type、房型、日期和价格适用规则后续按真实需求扩展 是,New Booking 选择 Rate Code 第一阶段暂不按 Account 过滤,只选 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 逻辑删除时间和原因

建议唯一约束:

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 逻辑删除时间和原因

建议唯一约束:

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。2026-07-21 最新结论是第一阶段暂不建立 Account 与 Rate Code 的适用关系;普通 lookup 继续返回当前酒店 ACTIVE Rate Code 目录。

如果后续需求方明确“某些 Account 只能使用部分 Rate Code”,再引入本备选表表达 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 逻辑删除时间和原因

建议唯一约束:

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 作为业务稳定端口。

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(...)

integrations.ohip / integrations.pms
  CatalogSyncAdapter
    - 后续从 PMS / OPERA / OHIP 拉取目录并转换为本系统目录草稿

规则:

  • V4 入站、确认、复核只调用 ReservationV4DirectoryService。
  • Rate Code 第一阶段只校验是否属于当前酒店 ACTIVE 的 RATE_CODE 目录;如未来引入 Account 适用关系,再基于 Basic Information 的 account_code 和业务 event 的 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

GET /api/reservation/lookups/accounts

查询参数:

参数 必需 中文说明
hotel_id 否 当前酒店 ID;未传时按当前用户默认酒店
keyword 否 匹配 account_code 或 account_name
page_num / page_size 否 分页

第一版固定只返回 ACTIVE Account,不开放 active_only=false。

响应草案:

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

GET /api/reservation/lookups/room-types

查询参数:

参数 必需 中文说明
hotel_id 否 当前酒店 ID
keyword 否 匹配房型 code 或显示名
page_num / page_size 否 分页

第一版固定只返回 ACTIVE Room Type,不接 arrival_date / departure_date,日期适用范围过滤后置。

响应草案:

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

GET /api/reservation/lookups/rate-codes

查询参数:

参数 必需 中文说明
hotel_id 否 当前酒店 ID
keyword 否 匹配 Rate Code 或显示名
page_num / page_size 否 分页

当前 Rate Code lookup 保持酒店级目录查询,只返回当前酒店 ACTIVE Rate Code。2026-07-21 结论是第一阶段暂不接收 account_code、booking_type 作为必填过滤条件,也不按房型、入住日期、价格或 Account 限制候选;如未来引入 workflow_reservation_rate_code_applicability,再扩展查询参数和校验规则。

响应草案:

{
  "hotel_id": "HOTEL-TEST",
  "catalog_type": "RATE_CODE",
  "catalog_source": "FIXED_SEED_IMPORT",
  "catalog_version": "owner-rate-20260721-v1",
  "stale": false,
  "items": [
    {
      "code": "GRPA2-850UP",
      "display_name": "GRPA2-850UP",
      "status": "ACTIVE",
      "catalog_source": "FIXED_SEED_IMPORT",
      "pricing_available": false
    }
  ],
  "page": {
    "page_num": 1,
    "page_size": 20,
    "total": 1
  },
  "warnings": []
}

前端用途:

  • fields[].options_source=reservation_v4_rate_code_catalog 时调用。
  • 当前第一阶段可以按当前酒店目录查询 Rate Code,不要求已有 Account 或 booking_type。
  • 第一阶段只选 rate_code,不展示或计算真实价格。
  • V4 契约仍禁止 UPDATE_BOOKING 携带 Rate Code;lookup 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 渲染当前酒店级 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 权限。

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 字段第一阶段按当前酒店级目录查询;如未来引入 Account 适用关系,再要求先取得当前订单 Basic Information 的 Account 和当前业务 event 的 booking_type。
  6. 搜索输入做 debounce,不一次性拉全量。
  7. 显示 stale=true 或 warnings[] 时给用户非阻塞提醒。
  8. 用户提交确认或复核时只提交 code,不提交显示名、Market / Source 派生值或目录完整对象。
  9. 确认成功后以后端返回的 confirmed_payload_json / 刷新详情为准更新页面。

前端禁止:

  • 硬编码 PMS 房型或 Rate Code 全集。
  • 硬编码 OWNER RATE Excel 中 Account 到 Rate Code 的映射;当前仅允许后端目录维护酒店级 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 OWNER RATE 目录导入口径 已完成:固定初始化种子和 V25 已把 Room Type 收敛为 RM2、RM3、RM4、SU1、SU2、SU3,并将 Q.B.D / LIAN TAI 的 40 个 Rate Code 候选作为酒店级 RATE_CODE 目录维护;暂不新增 Account + Rate Code 适用关系
M002-V4-CP15 PMS / OPERA / OHIP 目录同步 同步 Adapter、同步 run 表、失败重试、最后成功快照、同步状态管理入口
M002-V4-CP16 SuperAgent 目录供给 明确目录版本如何给 SuperAgent,必要时新增机器目录接口或导出包

CP11 已作为后端第一步落地,因为它不依赖真实 PMS,也能让前端后续不再硬编码当前固定种子。CP13 CP1 继续沿用本地目录表,不接真实 PMS,也不改变 SuperAgent 输入契约。CP14.5 已完成 OWNER RATE 目录数据收口,但只替换系统固定种子;测试 / 开发库如果存在人工新增的旧 Room Type / Rate Code,需要按本文第 16 节的限定 SQL 清理或重建。

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 过滤;如未来出现 Account 专属候选、入住日期、房型或价格过滤诉求,再单独确认是否引入适用关系或价格规则表。
  6. 生产是否允许 FIXED_SEED 作为兜底,还是只允许 dev/test 使用。
  7. SuperAgent 是否需要读取目录;如果需要,是离线给目录包,还是新增 HMAC 机器接口。

在这些问题未确认前,当前实现仍按 DB 管理目录 + 前端 lookup 查询 + 后台 CP1 手工维护闭环推进,不接真实 PMS,也不替换 SuperAgent 输入契约。

16. 测试 / 开发库 OWNER RATE 目录清理步骤

V25__align_owner_rate_catalog_data.sql 只替换 source_system=FIXED_SEED_IMPORT 的 Room Type / Rate Code 固定种子,不清理人工通过管理后台新增的目录项。测试机或开发库如果在 V25 前已经手工新增了旧目录值,且希望 lookup 严格只返回本阶段 6 个 Room Type 和 40 个 Rate Code,可以先备份后执行下面的限定更新。该步骤只停用当前酒店旧 Room Type / Rate Code,不清理 Account、Market、Source、用户、权限、SourceMessage、V4 order task 或 task card。

-- 执行前先确认目标酒店。
SET @target_hotel_id = 'HOTEL-TEST';

UPDATE workflow_reservation_catalog_code
SET status = 'DISABLED',
    updated_at = UTC_TIMESTAMP(6),
    version = version + 1
WHERE hotel_id = @target_hotel_id
  AND catalog_type = 'ROOM_TYPE'
  AND status = 'ACTIVE'
  AND code NOT IN ('RM2', 'RM3', 'RM4', 'SU1', 'SU2', 'SU3');

UPDATE workflow_reservation_catalog_code
SET status = 'DISABLED',
    updated_at = UTC_TIMESTAMP(6),
    version = version + 1
WHERE hotel_id = @target_hotel_id
  AND catalog_type = 'RATE_CODE'
  AND status = 'ACTIVE'
  AND code NOT IN (
      'GRPA2-850UP', 'GRPA2-1275', 'GRPA2-1400', 'GRPA2-1800', 'GRPA2-2400',
      'GRPA1-900', 'GRPA1-1400', 'GRPA1-1300', 'GRPA1-1150', 'GRPA1-1725', 'GRPA1-2300',
      'GRPA3-1200*B''FAST BUALUANG', 'GRPA3-2000*B''FAST BUALUANG',
      'GRPA3-1400*B''FAST BUALUANG', 'GRPA3-1800*B''FAST BUALUANG',
      'GRPA3-2400*B''FAST BUALUANG',
      'GRPA4-1200*B''FAST LEELA', 'GRPA4-2000*B''FAST LEELA',
      'GRPA4-1400*B''FAST LEELA', 'GRPA4-1800*B''FAST LEELA',
      'GRPA4-2400*B''FAST LEELA',
      'WHO1-850UP', 'WHO1-1275', 'WHO1-1400', 'WHO1-1800', 'WHO1-2400',
      'GRP1-900', 'GRP1-1400', 'GRP1-1300', 'GRP1-1800', 'GRP1-2400',
      'WHO2-1100', 'WHO2-1600', 'WHO2-1400', 'WHO2-1800', 'WHO2-2400',
      'WHO3-1200', 'WHO3-1800', 'WHO3-1400', 'WHO3-2400'
  );