Files
th-hotel-simple/docs/project/requirements/M005-hotel-context-unification-plan.md
2026-07-10 15:31:51 +08:00

392 lines
21 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.

# M005 Hotel Context 统一收口改造计划
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 文档版本 | 0.2 |
| 日期 | 2026-07-10 |
| 状态 | 已落地第一版:单酒店阶段严格要求 `platform_hotel` 必须且只能有一家 `ACTIVE` 酒店 |
| 适用范围 | `hotel_id` 来源、用户酒店上下文、SuperAgent / MCP、AgentBus、Reservation 查询、SourceMessage 查询、Debug / Demo 入口 |
| 主要读者 | 产品、后端、前端、测试、后续协作 agent |
## 1. 文档定位
本文记录当前系统 `hotel_id` 使用方式的统一收口方案。目标不是删除业务表里的
`hotel_id`,而是把运行时 `hotel_id` 的来源从“外部系统传入、前端环境变量、后端硬编码和配置默认值混用”
调整为“后端从平台酒店表和当前用户上下文解析”。
当前已明确:
- SuperAgent 不会传 `hotel_id`
- AgentBus 不会传 `hotel_id`
- 本系统上线前暂时只服务一家酒店。
- 后端已经有平台酒店表 `platform_hotel` 和用户酒店授权表 `platform_user_hotel`
- 登录态第一版采用数据库 session token不使用 JWTtoken 字符串本身不携带酒店 ID。
- 单酒店阶段采用严格规则:`platform_hotel` 没有 `ACTIVE` 酒店时报错,多家 `ACTIVE` 酒店也报错;数据库通过唯一约束阻止新增第二家 `ACTIVE` 酒店。
因此,后续业务入口不应再要求 SuperAgent、AgentBus 或前端环境变量作为酒店上下文事实来源。
系统内部仍然必须保留 `hotel_id`,用于数据隔离、幂等键、查询过滤、审计和未来多酒店扩展。
## 2. 核心结论
这次想法与现有系统没有根本冲突,可以实现,但需要把现有几个来源不一致的入口统一改造。
关键判断如下:
- `hotel_id` 字段继续保留在 SourceMessage、Reservation、Task、OPERA、审计等业务表中。
- 外部系统不传 `hotel_id` 是合理的,酒店归属应由本系统后端根据平台酒店表解析。
- token 不是 JWT不会把 `hotel_id` 放在 token 字符串里;后端可通过 session token 解析出
`AuthenticatedUserContext.defaultHotelId``accessibleHotelIds`
- 前端展示酒店已经可以从 `/api/auth/me` 返回的 `hotels[]``default_hotel_id` 获取Reservation 查询默认不再依赖 `VITE_RESERVATION_HOTEL_ID` 自动拼查询参数。
- 任务详情、任务写操作和 OPERA 操作当前多处已从任务自身读取 `task.hotelId()`,这类逻辑方向正确,
后续重点是补权限校验和查询隔离,不需要前端再传 `hotel_id`
## 3. 非目标范围
本次 M005 不做以下事情:
- 不删除任何业务表中的 `hotel_id` 字段。
- 不把 token 改成 JWT。
- 不实现完整多酒店路由策略例如按邮箱、AgentBus channel、SuperAgent org 或 OHIP 配置自动映射酒店。
- 不新增酒店管理后台 CRUD。
- 不改变 SuperAgent 和 AgentBus 的系统定位AgentBus 仍是消息入口适配器SuperAgent 仍是 AI / Agent 能力提供方。
- 不让前端直接调用数据库、SuperAgent、AgentBus 或任何持有 Secret 的外部系统。
## 4. 当前现状梳理
| 入口或模块 | 当前 `hotel_id` 来源 | 问题 | 目标方向 |
| --- | --- | --- | --- |
| AgentBus 入站捕获 | 改造前使用 `agentbus.capture.default-hotel-id`,代码默认 `HOTEL-TEST` | AgentBus 不会传酒店,配置默认值和平台酒店表割裂 | 已改为从平台酒店表解析系统酒店 |
| SuperAgent 任务结果 REST | 改造前 JSON 正式请求要求 `hotel_id`S000 / S999 文本请求使用 AgentBus 默认酒店 | SuperAgent 不会传酒店,正式契约与真实能力冲突 | 已支持缺省 `hotel_id` 使用系统酒店,再用 SourceMessage 自身酒店写业务表 |
| SuperAgent MCP 查询工具 | 改造前 tools schema 要求 `hotel_id` | SuperAgent MCP 配置页面和调用方不应承担酒店上下文 | 已将 `hotel_id` 改为可选,服务端解析系统酒店 |
| Reservation AI 查询服务 | 改造前 `ReservationAiQueryServiceImpl` 校验 `hotel_id` 必填 | 机器查询入口无法在无酒店参数时工作 | 已统一通过酒店上下文服务解析 |
| 前端 Reservation 列表 / 详情 | 改造前 `VITE_RESERVATION_HOTEL_ID` 自动拼 `hotel_id` | 前端环境变量成为业务事实来源,容易和平台酒店表不一致 | 已改为默认不传;如有当前选中酒店才传,后端校验 |
| 任务详情 / 任务写操作 / OPERA | 先按 taskId 找任务,再使用 `task.hotelId()` | 方向正确,但后续需要当前用户可访问酒店校验 | 保持从业务对象自身取酒店,并补权限边界 |
| SourceMessage 列表 | 改造前 `hotelId` 查询参数可选;为空时仓储不加酒店过滤 | 单酒店阶段可能还能工作,未来会有跨酒店暴露风险 | 已默认解析当前或系统酒店,不允许无边界列表 |
| SourceMessage 会话详情 | 先按内部 SourceMessage ID 找源消息,再用 `source.hotelId()` 查同会话 | 方向正确 | 保持源消息自身酒店上下文 |
| Debug EML | 改造前 multipart 必填 `hotel_id` | 测试人员需要手填,和平台酒店表割裂 | 已改为可选,缺省使用系统酒店,显式传值必须校验 |
| Demo seed | 改造前请求可传 `hotel_id`,否则使用 demo 配置默认值,默认 `HOTEL-TEST` | 演示数据入口也有独立默认来源 | 已改为系统酒店,返回入口 URL 不再强依赖手写酒店参数 |
| 登录 / 当前用户 | `/api/auth/me` 已返回 `default_hotel_id``hotels[]` | 后端业务接口尚未统一消费当前用户酒店上下文 | 作为用户请求的酒店上下文来源 |
## 5. 目标酒店上下文模型
建议新增一个平台级服务,统一承担酒店上下文解析。命名可在实现时二选一:
```text
cn.nianxx.thhotel.platform.hotel.service.HotelContextService
cn.nianxx.thhotel.platform.hotel.service.impl.HotelContextServiceImpl
```
或:
```text
cn.nianxx.thhotel.platform.hotel.service.HotelContextResolver
cn.nianxx.thhotel.platform.hotel.service.impl.HotelContextResolverImpl
```
职责建议:
| 方法 | 使用场景 | 规则 |
| --- | --- | --- |
| `resolveSystemHotelId()` | AgentBus、SuperAgent、MCP、Debug、Demo 等机器入口 | 从 `platform_hotel` 解析当前系统酒店;单酒店阶段要求只有一个启用酒店,或存在明确默认酒店 |
| `resolveCurrentHotelId(String requestedHotelId)` | 前端用户查询入口 | 有 token 时优先校验请求酒店是否在 `accessibleHotelIds`;请求为空时使用 `defaultHotelId` |
| `requireAccessibleHotel(String hotelId)` | 后续写操作、详情操作、审计查询 | 当前用户必须可访问该酒店;无 token 的兼容策略需单独声明 |
单酒店阶段建议使用严格规则:
1. 如果平台酒店表没有启用酒店,启动或首次调用时返回明确错误,例如 `SYSTEM_HOTEL_NOT_CONFIGURED`
2. 如果平台酒店表存在多家启用酒店,但没有明确默认酒店,返回明确错误,例如 `SYSTEM_HOTEL_AMBIGUOUS`
3. 当前阶段不要继续用 `HOTEL-TEST` 作为运行时兜底值;`HOTEL-TEST` 只能保留在测试夹具、示例文档或 bootstrap 默认配置中。
## 6. 各入口目标规则
### 6.1 AgentBus 入站捕获
当前代码位置:
- `server/src/main/java/cn/nianxx/thhotel/integrations/messaging/agentbus/adapter/AgentBusFrameProcessor.java`
- `server/src/main/java/cn/nianxx/thhotel/integrations/messaging/agentbus/adapter/AgentBusProperties.java`
- `server/src/main/java/cn/nianxx/thhotel/platform/message/service/impl/SourceMessageCaptureServiceImpl.java`
目标规则:
- `AgentBusFrameProcessor` 不再从 `AgentBusProperties.capture.defaultHotelId` 取酒店。
- 捕获前调用 `HotelContextService.resolveSystemHotelId()`
- `SourceMessageCaptureServiceImpl` 仍要求 `CaptureSourceMessageCommand.hotelId` 非空,因为落库必须有稳定酒店上下文。
- `agentbus.capture.default-hotel-id` 后续可以废弃,或仅作为临时兼容配置,优先级低于平台酒店表。
### 6.2 SuperAgent 任务结果 REST
当前代码位置:
- `server/src/main/java/cn/nianxx/thhotel/integrations/ai/superagent/control/SuperAgentTaskResultController.java`
- `server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationAiTaskIntakeServiceImpl.java`
目标规则:
- JSON 正式请求不再要求 SuperAgent 传 `hotel_id`
- 当请求体没有 `hotel_id` 时,后端使用 `resolveSystemHotelId()` 查 SourceMessage。
- 命中 SourceMessage 后,后续 Reservation 批次、订单、任务、审计都继续使用 `sourceMessage.hotelId()`,不直接信任外部请求体。
- 如果外部请求体仍传了 `hotel_id`,单酒店阶段建议校验它必须等于系统酒店;不一致时返回明确错误,避免静默写错酒店。
- 旧的“无 `hotel_id` 时按内部 SourceMessage ID 查询”的本地兼容路径应继续限制为 dev / test 或明确标注为非正式契约。
### 6.3 SuperAgent MCP
当前代码位置:
- `server/src/main/java/cn/nianxx/thhotel/integrations/mcp/superagent/service/impl/SuperAgentMcpServiceImpl.java`
- `docs/project/integrations/superagent-mcp/tools.md`
- `docs/project/integrations/superagent-mcp/test-cases.md`
目标规则:
- MCP tools schema 中的 `hotel_id``required` 移除。
- 查询类工具在参数缺少 `hotel_id` 时,由 MCP 服务端补入系统酒店。
- 写入类工具 `submit_ai_task_results` 也不再要求 `hotel_id`,但仍必须要求 `source_message_id` 和结果列表。
- 文档中应明确SuperAgent 不需要知道酒店 ID酒店上下文由 TH Hotel 后端托管。
- 现有 `HOTEL_ID_REQUIRED` 的 MCP 测试用例要改成“不传 `hotel_id` 仍成功或进入业务查询缺失 key 错误”。
### 6.4 Reservation AI 查询服务
当前代码位置:
- `server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationAiQueryServiceImpl.java`
- `server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/request/ReservationAiCaseContextQueryRequest.java`
- `server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/request/ReservationAiObjectDetailQueryRequest.java`
- `server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/request/ReservationMessageConversationQueryRequest.java`
目标规则:
- `hotel_id` 在请求 DTO 中保留,但语义改为可选。
- 进入查询服务时统一调用酒店上下文服务解析有效酒店 ID。
- 查询仓储层继续显式按 `hotel_id` 过滤,保证业务数据隔离。
- 对 SuperAgent / MCP 机器入口,缺省酒店来自系统酒店。
- 对用户入口,缺省酒店来自当前用户默认酒店,显式酒店必须通过可访问酒店校验。
### 6.5 前端 Reservation 查询
当前代码位置:
- `client/src/services/reservationService.ts`
- `client/src/config/reservationConfig.ts`
- `client/src/stores/authStore.ts`
- `client/src/layouts/ReservationAppShell.vue`
目标规则:
- 前端酒店展示以 `authStore.hotels``authStore.defaultHotelId``authStore.selectedHotelId` 为准。
- Reservation 列表、任务列表、订单详情默认不再依赖 `VITE_RESERVATION_HOTEL_ID`
- 单酒店阶段可以默认不传 `hotel_id`,由后端按当前用户上下文解析。
- 如果保留酒店选择器,前端传当前选中的 `selectedHotelId`,后端必须校验该酒店在当前用户可访问列表中。
- `VITE_RESERVATION_HOTEL_ID` 后续只适合保留为 fixture / 本地兼容配置,不作为真实业务上下文。
### 6.6 前端 Reservation 查询 Controller
当前代码位置:
- `server/src/main/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationFrontendQueryController.java`
- `server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationFrontendQueryServiceImpl.java`
目标规则:
- Controller 可以继续接收可选 `hotel_id`,用于未来用户切换酒店。
- Service 不再用 `DEFAULT_HOTEL_ID = "HOTEL-TEST"` 兜底。
- Service 调用 `resolveCurrentHotelId(request.hotelId())` 得到有效酒店 ID。
- 没有登录态的兼容行为需要明确:建议 dev / test 可回退系统酒店,生产逐步要求 token。
### 6.7 SourceMessage 查询
当前代码位置:
- `server/src/main/java/cn/nianxx/thhotel/platform/message/control/SourceMessageController.java`
- `server/src/main/java/cn/nianxx/thhotel/platform/message/repository/MybatisSourceMessageInboxRepository.java`
- `server/src/main/java/cn/nianxx/thhotel/platform/message/service/impl/SourceMessageConversationServiceImpl.java`
目标规则:
- SourceMessage 列表接口不应在 `hotelId` 为空时返回跨酒店结果。
- 列表接口缺省酒店时调用 `resolveCurrentHotelId(null)``resolveSystemHotelId()`,具体取决于是否要求登录。
- 会话详情和原文读取已经能从 SourceMessage 自身获取 `source.hotelId()`,方向正确;后续只补当前用户是否可访问该酒店的校验。
### 6.8 Debug EML 与 Demo Seed
当前代码位置:
- `server/src/main/java/cn/nianxx/thhotel/platform/debug/control/DebugEmlSuperAgentController.java`
- `server/src/main/java/cn/nianxx/thhotel/platform/debug/service/impl/DebugEmlSuperAgentRunServiceImpl.java`
- `server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationDemoDataServiceImpl.java`
- `server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationDemoDataProperties.java`
目标规则:
- Debug EML 的 `hotel_id` 从必填改为可选。
- dev / test 缺省时使用系统酒店。
- 如果调试人员显式传 `hotel_id`,后端校验它必须存在于平台酒店表;生产不建议开放该覆盖能力。
- Demo seed 缺省酒店改为系统酒店,不再有独立 `defaultHotelId = "HOTEL-TEST"` 运行时兜底。
## 7. 配置调整建议
| 配置 | 当前用途 | 调整建议 |
| --- | --- | --- |
| `AUTH_*_BOOTSTRAP_DEFAULT_HOTEL_ID` | 初始化平台酒店和管理员默认酒店 | 保留,用于 bootstrap不作为每次请求的运行时兜底 |
| `AGENTBUS_*_DEFAULT_HOTEL_ID` | 旧 AgentBus fallback | 第一版运行时已不再使用,后续清理配置文件和环境模板 |
| `RESERVATION_*_DEMO_DATA_DEFAULT_HOTEL_ID` | 旧 Demo seed 默认酒店 | 第一版运行时已不再使用,后续清理配置文件和环境模板 |
| `VITE_RESERVATION_HOTEL_ID` | 前端 Reservation 默认酒店 | 从真实联调和生产配置中移除,保留 fixture / 本地兼容说明 |
配置文件调整原则:
- `application-test.yml``application-prod.yml` 不应继续写死运行时端口、数据库、酒店 ID 等会由环境覆盖的值。
- 平台酒店初始化可以继续通过 bootstrap env 控制,但业务请求解析应查平台酒店表。
- 如果系统酒店无法解析,应快速失败并给出明确错误,而不是默默回到 `HOTEL-TEST`
## 8. 分阶段落地计划
### Phase 1酒店上下文服务底座
修改范围:
- 新增 `platform.hotel.service.HotelContextService`
- 新增 `platform.hotel.service.impl.HotelContextServiceImpl`
- 扩展 `PlatformHotelRepository`,支持解析启用酒店和默认酒店。
- 为系统酒店解析、当前用户默认酒店、无酒店、多酒店歧义写单元测试。
验收标准:
- 单启用酒店时可解析系统酒店。
- 无启用酒店时报 `SYSTEM_HOTEL_NOT_CONFIGURED`
- 多启用酒店且无默认规则时报 `SYSTEM_HOTEL_AMBIGUOUS`
- 当前用户有默认酒店时,用户请求解析使用默认酒店。
- 显式请求酒店不在当前用户可访问列表时拒绝。
### Phase 2机器入口收口
修改范围:
- AgentBus 捕获改用 `resolveSystemHotelId()`
- SuperAgent REST 入站缺省 `hotel_id` 时使用系统酒店。
- MCP tools schema 移除 `hotel_id` required并由服务端补酒店。
- Reservation AI 查询服务把 `hotel_id` 必填校验改成上下文解析。
验收标准:
- AgentBus 入站 frame 不带 `hotel_id` 也能入库 SourceMessage。
- SuperAgent JSON 任务结果不带 `hotel_id` 也能通过外部 `source_message_id` 找到 SourceMessage。
- MCP 查询工具不传 `hotel_id` 也能执行。
- 业务写入最终使用 SourceMessage 自身 `hotelId`
### Phase 3用户入口和前端收口
修改范围:
- Reservation 前端查询服务不再默认拼 `VITE_RESERVATION_HOTEL_ID`
- Reservation 后端查询服务移除 `DEFAULT_HOTEL_ID = "HOTEL-TEST"`
- SourceMessage 列表接口缺省酒店时按当前用户或系统酒店过滤。
- 任务详情、任务写操作、OPERA 操作增加当前用户可访问酒店校验。
验收标准:
- 登录后前端酒店名称来自 `/api/auth/me``hotels[]`
- Reservation 列表、任务列表、订单详情在不传 `hotel_id` 时也能按默认酒店查询。
- 用户显式切换酒店时,后端校验酒店访问权限。
- SourceMessage 列表不会因为缺少 `hotelId` 返回跨酒店数据。
### Phase 4配置、文档和兼容清理
修改范围:
- 更新 `README.md``VITE_RESERVATION_HOTEL_ID` 的说明。
- 更新 SuperAgent API / MCP 文档,明确外部系统不需要传 `hotel_id`
- 更新 go-live notes移除“正式 JSON 请求必须带 `hotel_id`”的上线要求。
- 清理 `application-*.yml` 中和运行时默认酒店相关的硬编码。
- 保留测试夹具里的 `HOTEL-TEST`,但标注为测试数据。
验收标准:
- 文档与真实契约一致。
- prod / test 启动依赖平台酒店表,不依赖散落默认酒店配置。
- 搜索运行时代码时,不再存在作为兜底逻辑的 `DEFAULT_HOTEL_ID = "HOTEL-TEST"`
## 9. 数据和迁移注意事项
上线前需要确认:
- `platform_hotel` 中存在当前酒店的启用记录。
- 单酒店阶段 `platform_hotel` 只能有一家 `ACTIVE` 酒店;迁移脚本 `V12__enforce_single_active_platform_hotel.sql` 会通过唯一索引阻止第二家 `ACTIVE` 酒店。
- 现有 SourceMessage、Reservation、Task、OPERA、审计数据的 `hotel_id``platform_hotel.hotel_id` 一致。
- 如果现有数据使用 `HOTEL-TEST`,而正式平台酒店 ID 不是 `HOTEL-TEST`,需要先设计数据迁移脚本。
- 管理员用户在 `platform_user_hotel` 中有当前酒店默认授权,或超级管理员可访问全部启用酒店。
- SuperAgent / MCP 文档和配置页面中的服务地址、鉴权 token、工具 schema 已同步更新。
建议先在测试环境执行检查:
```sql
SELECT hotel_id, hotel_name, hotel_status, time_zone
FROM platform_hotel
ORDER BY sort_order, id;
SELECT hotel_id, COUNT(*) AS source_message_count
FROM platform_source_message_inbox
GROUP BY hotel_id;
SELECT hotel_id, COUNT(*) AS reservation_task_count
FROM workflow_reservation_task
GROUP BY hotel_id;
```
中文说明:第一条确认平台酒店表;第二条和第三条确认已有消息与任务数据的酒店 ID 是否和平台酒店表一致。
## 10. 测试建议
后端建议覆盖:
- `HotelContextServiceImplTest`
- `AgentBusFrameProcessorTest`
- `SuperAgentTaskResultControllerTest`
- `ReservationAiTaskIntakeServiceImplTest`
- `SuperAgentMcpServiceImplTest`
- `ReservationAiQueryServiceImplTest`
- `ReservationFrontendQueryServiceImplTest`
- `SourceMessageController` 或相关查询服务测试
前端建议覆盖:
- `reservationService.spec.ts`:不再默认拼 `VITE_RESERVATION_HOTEL_ID`,或改为使用当前选中酒店。
- `authStore.spec.ts`:继续验证默认酒店和可访问酒店选择逻辑。
- `reservationAppShell.spec.ts`:酒店展示来自登录上下文。
集成验收建议:
- AgentBus frame 入站不带酒店字段SourceMessage 落库有正确 `hotel_id`
- SuperAgent 任务结果 JSON 不带 `hotel_id`,仍能写入正确酒店下的订单和任务。
- MCP `tools/call` 不带 `hotel_id`,仍可查询订单上下文。
- 前端登录后不配置 `VITE_RESERVATION_HOTEL_ID`,订单列表和任务列表仍可查询。
- 非授权用户访问其他酒店数据时被拒绝。
## 11. 当前决策和后置事项
已确认并落地:
1. 单酒店阶段“系统酒店”采用严格规则:`platform_hotel` 必须且只能有一家 `ACTIVE` 酒店。
2. 如果机器入口或前端兼容旧契约显式传入 `hotel_id`,后端严格校验它必须等于系统酒店或当前用户可访问酒店。
3. SuperAgent、AgentBus、MCP tools 和 Debug EML 默认都不需要传 `hotel_id`
4. `HOTEL-TEST` 只能保留在测试夹具、示例文档或 bootstrap 默认配置中,不作为业务运行时兜底酒店。
后置事项:
1. 生产环境是否强制 Reservation / SourceMessage 用户接口必须带登录 token等权限拦截策略整体收口时再确认。
2. 如果正式酒店 ID 不是 `HOTEL-TEST`,需要先迁移测试机或存量数据里的 `hotel_id`
3. `AGENTBUS_*_DEFAULT_HOTEL_ID``RESERVATION_*_DEMO_DATA_DEFAULT_HOTEL_ID` 等旧配置项后续可以从配置模板中清理;本次第一版先保证运行时代码不再依赖它们。
## 12. 最终验收口径
M005 完成后,应满足以下口径:
- SuperAgent 和 AgentBus 不传 `hotel_id`,系统仍能正常入库、查询和写入 Reservation 任务。
- 前端酒店展示来自平台酒店表和当前登录用户上下文。
- 前端 Reservation 查询不依赖 `VITE_RESERVATION_HOTEL_ID` 作为真实业务上下文。
- 后端所有查询和写入最终都带有明确 `hotel_id` 过滤或落库值。
- `hotel_id` 的运行时来源统一为平台酒店表或当前用户上下文。
- 运行时代码中不再出现 `HOTEL-TEST` 作为业务兜底酒店。
- 文档、测试用例、SuperAgent MCP tools schema 与真实能力一致。