# TH Hotel 上线注意事项

本文给产品、研发、测试、运维和后续协作 agent 使用，目标是把上线前必须确认的事项放在同一个地方。这里记录的是当前项目专属要求，不作为可整份复制到其他项目的通用模板。

## 1. 当前上线范围

当前后端已经具备以下能力：

- `GET /api/health`：后端健康检查。
- `GET /api/source-messages`：查询 SourceMessage Inbox 安全摘要。
- `GET /api/source-messages/{id}`：查询单条 SourceMessage 安全摘要。
- `GET /api/source-messages/{id}/original`：受控读取邮件原文、HTML 和媒体 URL，并记录访问审计。
- `GET /api/system/agentbus-probe`：查看 AgentBus WebSocket 连接状态和安全计数器。
- AgentBus WebSocket 入站链路：默认关闭，开启后只把业务 frame 写入 SourceMessage Inbox。
- SuperAgent 任务结果接收接口：当前代码接收一个外部 `source_message_id` 下的 AI 任务结果，反查 SourceMessage Inbox 后写入 AI 过渡层、订单、任务和任务卡；支持 V3 结构化 `S10/S99` 和业务根基础解析，同时兼容旧 `S000/S999,source_message_id` 文本入口结果并创建只读特殊任务。
- Reservation 任务详情接口：返回任务字段、队列可处理状态和 OPERA 模拟操作摘要。
- Reservation 任务草稿保存和最终确认接口：按任务卡矩阵做第一版后端校验，确认后生成 `confirmed_payload_json`。
- Reservation OPERA 模拟骨架：已确认任务固定生成两条模拟操作，支持执行、失败重试、attempt 记录和任务审计列表。
- SuperAgent 查询上下文接口 1、2：支持 HMAC 鉴权的订单上下文查询和对象详情查询。
- Debug EML 上传到 SuperAgent 调试链路：受控上传 `.eml`、转存阿里云 OSS、写入 SourceMessage Inbox、调用 SuperAgent Open API 并返回调试结果。
- 登录权限底座：支持用户名密码登录、登出、当前用户上下文、数据库 session token、可访问酒店、权限码和可见菜单。
- 系统管理后台 V1：支持用户、角色权限、菜单、酒店和管理操作审计的受控维护接口与前端页面。

当前不要把以下能力当作已上线：

- SourceMessage Replay 到 MessageEvent / Evidence。
- 真实 AI 识别服务实现、Case 完整模型、Operation、Receipt。
- 自动 ACK、`task.result` 或客户回复。
- 业务前端页面展示邮件原文。
- OHIP / OPERA 或其他业务系统真实写操作。
- 普通任务切换订单接口。
- M002 V3 字段矩阵从当前扁平结构整体迁移到 0711 P0 新结构。
- 现有业务接口强制登录和强制权限拦截。
- 业务审计 actor 全量迁移到当前登录用户。
- Debug EML 上传链路不属于生产普通业务页面能力，生产默认关闭；即使已有登录权限，也不要开放给普通用户。

## 2. 上线前必须确认

上线前至少确认以下事项：

- 当前分支、提交和部署包来源清楚，不能混入本地临时文件、真实 Secret、真实客户邮件样本或构建产物。
- `server` 后端通过完整检查：`cd server && ./mvnw verify`。
- 生产或 UAT 数据库已经备份，并确认 Flyway migration 只新增不修改历史脚本。
- 所有 Secret 都通过环境变量、部署平台 Secret 或密钥管理系统注入，不写入仓库、镜像、前端环境变量或普通配置文件。
- 生产默认不保存 AgentBus raw frame 样本。
- AgentBus 实时链路开启前，已经确认 WebSocket URL、Token、Bot Address、外部消息幂等键和断线重连语义。
- 源邮件只读通知卡上线前，必须确认 `platform_hotel` 中存在且只存在一家 `ACTIVE` 酒店，并且已有 SourceMessage Inbox 数据的 `hotel_id` 与该酒店一致。旧 `S000/S999` 和新结构化 `S10/S99` 都沿用该酒店解析约束。
- 系统管理后台上线前，必须确认至少存在一个 `ACTIVE` 超级管理员账号，且该账号拥有 `SYSTEM_ADMIN_CONSOLE_ACCESS` 和各系统管理权限。
- 单酒店阶段上线前，必须确认 `platform_hotel` 中只有一家 `ACTIVE` 酒店；新增酒店可以存在但应保持 `DISABLED`。
- 管理后台启用后，不要继续把手工改库作为常规运营方式；用户、角色、菜单和酒店变更应通过 `/api/admin/**` 并写入管理审计。
- 原文读取接口开启前，已经确认谁可以使用、在哪些场景使用、如何轮换访问 key。
- 日志采集、错误响应和监控面板都不会展示邮件正文、HTML、附件 URL、Token、Cookie、客户姓名、邮箱、电话或支付信息。

## 3. 环境变量

### 3.1 数据库

| 变量 | 是否 Secret | 上线注意事项 |
| --- | --- | --- |
| `TH_HOTEL_DB_URL` | 否 | 指向目标环境数据库。URL 包含 `&` 时要加引号。 |
| `TH_HOTEL_DB_USERNAME` | 是 | 使用最小权限账号，不使用个人账号。 |
| `TH_HOTEL_DB_PASSWORD` | 是 | 只能通过 Secret 注入，不写入仓库。 |
| `TH_HOTEL_DB_DRIVER` | 否 | MySQL 使用 `com.mysql.cj.jdbc.Driver`。 |

### 3.2 登录权限

| 变量 | 是否 Secret | 上线注意事项 |
| --- | --- | --- |
| `AUTH_DEV_BOOTSTRAP_ADMIN_USERNAME` / `AUTH_TEST_BOOTSTRAP_ADMIN_USERNAME` / `AUTH_PROD_BOOTSTRAP_ADMIN_USERNAME` | 否 | 初始超级管理员用户名；系统已有超级管理员后不会覆盖。 |
| `AUTH_DEV_BOOTSTRAP_ADMIN_PASSWORD` / `AUTH_TEST_BOOTSTRAP_ADMIN_PASSWORD` / `AUTH_PROD_BOOTSTRAP_ADMIN_PASSWORD` | 是 | 初始超级管理员密码，只能通过 Secret 注入；系统已有超级管理员后不会覆盖。 |
| `AUTH_DEV_BOOTSTRAP_ADMIN_DISPLAY_NAME` / `AUTH_TEST_BOOTSTRAP_ADMIN_DISPLAY_NAME` / `AUTH_PROD_BOOTSTRAP_ADMIN_DISPLAY_NAME` | 否 | 初始超级管理员展示名。 |
| `AUTH_DEV_BOOTSTRAP_DEFAULT_HOTEL_ID` / `AUTH_TEST_BOOTSTRAP_DEFAULT_HOTEL_ID` / `AUTH_PROD_BOOTSTRAP_DEFAULT_HOTEL_ID` | 否 | 登录权限底座初始化默认酒店；prod 必须配置真实酒店 ID。 |
| `AUTH_DEV_BOOTSTRAP_DEFAULT_HOTEL_NAME` / `AUTH_TEST_BOOTSTRAP_DEFAULT_HOTEL_NAME` / `AUTH_PROD_BOOTSTRAP_DEFAULT_HOTEL_NAME` | 否 | 默认酒店展示名称。 |
| `AUTH_DEV_BOOTSTRAP_DEFAULT_HOTEL_TIME_ZONE` / `AUTH_TEST_BOOTSTRAP_DEFAULT_HOTEL_TIME_ZONE` / `AUTH_PROD_BOOTSTRAP_DEFAULT_HOTEL_TIME_ZONE` | 否 | 默认酒店本地时区，默认 `Asia/Bangkok`。 |
| `AUTH_DEV_SESSION_TTL_MINUTES` / `AUTH_TEST_SESSION_TTL_MINUTES` / `AUTH_PROD_SESSION_TTL_MINUTES` | 否 | 数据库 session token 有效分钟数，默认 `720`。 |
| `AUTH_BOOTSTRAP_ADMIN_USERNAME` / `AUTH_BOOTSTRAP_ADMIN_PASSWORD` / `AUTH_SESSION_TTL_MINUTES` 等通用变量 | 视具体变量而定 | 兼容兜底变量；新环境优先使用环境专属变量。 |

注意：

- 数据库只保存 `token_hash`，不保存明文 `access_token`。
- `access_token` 只在登录成功响应中返回一次；前端只能放 `sessionStorage`，不能放 `localStorage`、URL、日志或错误上报。
- 当前第一版只做可选 Bearer token 解析，现有 Reservation / SourceMessage 业务接口仍不强制登录。
- `/api/auth/me` 和 `/api/auth/logout` 需要 `Authorization: Bearer <access_token>`。
- 初始管理员 bootstrap 只以“启用状态超级管理员”为阻断条件；如果测试库或生产库只剩禁用超级管理员，应通过环境变量恢复一个可登录超级管理员后再排查账号运营问题。
- 内置角色权限矩阵在启动时按代码同步，矩阵移除的旧权限关系会被清理；管理后台 V1 也不允许修改内置角色权限，临时权限应通过自定义角色承载。
- 普通用户默认酒店由后端写入逻辑和数据库唯一索引共同保持单默认；V10 migration 会在建约束前把历史重复默认清理为每个用户保留 id 最大的一条。
- 单酒店阶段系统酒店由 `platform_hotel` 唯一 `ACTIVE` 酒店决定；V12 migration 会通过唯一索引阻止第二家 `ACTIVE` 酒店。上线前如果已有多家 `ACTIVE` 酒店，必须先调整数据，否则迁移或运行时解析会失败。
- 管理后台 V1 写操作会记录 `platform_admin_audit_log`；重置密码只允许临时密码出现在本次响应中，不得进入日志、审计快照或前端持久化存储。

### 3.3 SourceMessage

| 变量 | 是否 Secret | 上线注意事项 |
| --- | --- | --- |
| `SOURCE_MESSAGE_DEV_ORIGINAL_READ_ACCESS_KEY` | 是 | dev 原文读取临时访问 key。未配置时可兜底使用旧通用变量。 |
| `SOURCE_MESSAGE_TEST_ORIGINAL_READ_ACCESS_KEY` | 是 | test 原文读取临时访问 key。未配置时可兜底使用旧通用变量。 |
| `SOURCE_MESSAGE_PROD_ORIGINAL_READ_ACCESS_KEY` | 是 | prod 原文读取临时访问 key。未配置时原文读取默认关闭。 |
| `SOURCE_MESSAGE_ORIGINAL_READ_ACCESS_KEY` | 是 | 旧通用原文读取 key，仅作为兼容兜底。 |

注意：

- 原文读取 key 不是用户体系，后续接入正式登录和角色权限后应替换。
- 任何能读取原文的调用都必须有调用方和访问场景，并写入审计表。

### 3.4 AgentBus

| 变量 | 是否 Secret | 上线注意事项 |
| --- | --- | --- |
| `AGENTBUS_PROBE_ENABLED` | 否 | 是否启用 WebSocket 长连接。生产首次上线建议先保持 `false`，完成连通性窗口后再打开。 |
| `AGENTBUS_WS_URL` | 否 | AgentBus WebSocket 地址。 |
| `AGENTBUS_WS_TOKEN` | 是 | WebSocket 鉴权 Token，只能通过 Secret 注入。 |
| `AGENTBUS_WS_RECONNECT_DELAY` | 否 | 断线重连间隔，默认 `5s`。 |
| `AGENTBUS_CONNECT_TIMEOUT` | 否 | 连接超时，默认 `15s`。 |
| `AGENTBUS_MAX_FRAME_BYTES` | 否 | 单个入站 frame 最大字节数，默认 `1048576`。 |
| `AGENTBUS_CAPTURE_ENABLED` | 否 | 是否把业务 frame 写入 SourceMessage Inbox。 |

注意：

- `AGENTBUS_PROBE_ENABLED=true` 只表示启用连接和入站接收，不代表可以回复客户。
- `AGENTBUS_CAPTURE_ENABLED=false` 时，业务 frame 会被忽略，不会写入 SourceMessage Inbox。
- `AGENTBUS_DEFAULT_HOTEL_ID` 是 M005 前旧变量，当前后端不再读取；系统酒店来自 `platform_hotel` 唯一 `ACTIVE` 酒店。
- 当前实现不发送 ACK、不发送 `task.result`、不自动回复客户。

### 3.5 SuperAgent HMAC

| 变量 | 是否 Secret | 上线注意事项 |
| --- | --- | --- |
| `SUPERAGENT_DEV_TASK_RESULT_HMAC_SECRET` | 是 | dev SuperAgent 查询接口 1、2 和任务结果通知接口共用的 HMAC secret。 |
| `SUPERAGENT_TEST_TASK_RESULT_HMAC_SECRET` | 是 | test SuperAgent 查询接口 1、2 和任务结果通知接口共用的 HMAC secret。 |
| `SUPERAGENT_PROD_TASK_RESULT_HMAC_SECRET` | 是 | prod SuperAgent 查询接口 1、2 和任务结果通知接口共用的 HMAC secret。生产不能为空，只能通过 Secret 注入。 |
| `SUPERAGENT_TASK_RESULT_HMAC_SECRET` | 是 | 旧通用 HMAC 变量，仅作为兼容兜底；新环境优先配置环境专属变量。 |
| `SUPERAGENT_TASK_RESULT_CLOCK_SKEW_SECONDS` | 否 | 请求时间允许偏移，默认 `300` 秒。上线前确认本系统、SuperAgent 和 AgentBus 所在机器时间已通过 NTP 同步。 |
| `SUPERAGENT_TASK_RESULT_NONCE_TTL_SECONDS` | 否 | nonce 防重放窗口，默认 `600` 秒。 |
| `SUPERAGENT_TASK_RESULT_MAX_BODY_BYTES` | 否 | SuperAgent 入站请求体最大字节数，默认 `1048576`。 |
| `SUPERAGENT_TEST_ALLOW_LEGACY_INTERNAL_SOURCE_MESSAGE_ID` | 否 | 仅用于本地 / test 旧夹具兼容内部 SourceMessage ID；正式联调和生产不得开启。 |

注意：

- 查询接口和任务结果通知接口使用同一套 Header、签名串、secret、timestamp 和 nonce 规则。
- SuperAgent 侧也需要配置同一个 secret，并按原始请求体计算 SHA-256。
- 当前第一版只支持一个 HMAC secret，secret 轮换需要协调部署窗口。
- 任务结果通知接口 JSON body 里的 `source_message_id` 是外部来源消息 ID，对应 AgentBus `source.external_message_id`；SuperAgent 默认不传 `hotel_id`，后端用系统酒店 `hotel_id + provider + channel + external_message_id` 反查内部 SourceMessage Inbox。
- 当前代码的任务结果通知接口也支持 `text/plain` 的 `S000,source_message_id` 和 `S999,source_message_id`。这类请求不在 body 里带 `hotel_id`，后端同样使用平台酒店表唯一 `ACTIVE` 酒店查询 SourceMessage Inbox。
- M002 V3 已支持结构化 `S10/S99`、V3 业务根基础解析、`UNHANDLED_CURRENT_INTENT` 和 `adapter_contract_error` 最小落库：上线前必须单独验证新 JSON 入站、旧 S000/S999 兼容、隐藏技术订单、任务列表可见、订单列表不可见和只读限制。
- 0712 P0.1 后，SuperAgent runtime 必须同步使用 P0.1 Main Agent prompt / booking-desk-event skill。完整 Parent split 的父事件必须提交为 `Cancel Allotment + cancel_allotment_control_block`；当前新入站 `Cancel Booking + linked_parent_release_after_child_split` 会被后端按 `adapter_contract_error` 处理，不创建业务任务。旧历史 payload 只读兼容，不做批量迁移。
- `application/json` 和 `text/plain` 都必须使用原始请求体计算 SHA-256 并参与 HMAC 签名；SuperAgent 侧不能签名格式化后的 JSON 或二次拼接字符串。
- 旧 S000/S999 会创建 `SOURCE_MESSAGE_ONLY` 只读特殊任务和隐藏技术订单，任务列表可见，订单列表不可见，不允许编辑、确认、转换订单或执行 OPERA。V3 S10/S99 应保持同等只读和不可执行边界。
- SuperAgent 查询上下文接口中的 `source_message_id`、`source_event_index` 第一版仅兼容接收，不参与查询和校验；不要依赖它们限制查询范围。

### 3.6 Debug EML / SuperAgent Open API / 阿里云 OSS

| 变量 | 是否 Secret | 上线注意事项 |
| --- | --- | --- |
| `DEBUG_EML_UPLOAD_DEV_ENABLED` | 否 | dev 是否启用 Debug EML 上传接口，未配置时可兜底 `DEBUG_EML_UPLOAD_ENABLED`。 |
| `DEBUG_EML_UPLOAD_TEST_ENABLED` | 否 | test 是否启用 Debug EML 上传接口，默认关闭。 |
| `DEBUG_EML_UPLOAD_PROD_ENABLED` | 否 | prod 默认必须保持 `false`；未接入正式用户权限前不要开放。 |
| `DEBUG_EML_UPLOAD_DEV_ACCESS_KEY` | 是 | dev Debug EML 上传访问口令，未配置时可兜底 `DEBUG_EML_UPLOAD_ACCESS_KEY`。 |
| `DEBUG_EML_UPLOAD_TEST_ACCESS_KEY` | 是 | test Debug EML 上传访问口令，未配置时可兜底 `DEBUG_EML_UPLOAD_ACCESS_KEY`。 |
| `DEBUG_EML_UPLOAD_PROD_ACCESS_KEY` | 是 | prod Debug EML 上传访问口令；生产通常不应启用该接口。 |
| `DEBUG_EML_UPLOAD_MAX_FILE_BYTES` | 否 | `.eml` 上传大小上限，默认 `10485760`。 |
| `DEBUG_EML_UPLOAD_SSE_HEARTBEAT_INTERVAL` | 否 | Debug EML 页面到后端的 SSE 心跳间隔，默认 `15s`；测试机如仍遇到空闲断流可调小到 `10s`。 |
| `DEERFLOW_DEV_BASE_URL` / `DEERFLOW_TEST_BASE_URL` / `DEERFLOW_PROD_BASE_URL` | 否 | SuperAgent / DeerFlow Open API 基础地址，未配置时可兜底 `DEERFLOW_BASE_URL`。 |
| `DEERFLOW_DEV_OPEN_API_KEY` / `DEERFLOW_TEST_OPEN_API_KEY` / `DEERFLOW_PROD_OPEN_API_KEY` | 是 | SuperAgent Open API Key，未配置时可兜底 `DEERFLOW_OPEN_API_KEY`。 |
| `SUPERAGENT_DEV_OPEN_API_ENABLED` / `SUPERAGENT_TEST_OPEN_API_ENABLED` / `SUPERAGENT_PROD_OPEN_API_ENABLED` | 否 | 是否启用真实 SuperAgent Open API 调用；prod 默认关闭。 |
| `SUPERAGENT_DEBUG_EML_EXTERNAL_SUBJECT_ID` | 否 | Debug EML 创建 SuperAgent session 的 external subject id。 |
| `SUPERAGENT_DEBUG_EML_CONNECT_TIMEOUT` | 否 | SuperAgent Open API 建连超时，默认 `15s`。 |
| `SUPERAGENT_DEBUG_EML_READ_TIMEOUT` | 否 | SuperAgent SSE 读取超时，默认 `180s`。 |
| `ALIYUN_OSS_DEV_ENDPOINT` / `ALIYUN_OSS_TEST_ENDPOINT` / `ALIYUN_OSS_PROD_ENDPOINT` | 否 | 阿里云 OSS Endpoint，未配置时可兜底 `ALIYUN_OSS_ENDPOINT`。 |
| `ALIYUN_OSS_DEV_BUCKET` / `ALIYUN_OSS_TEST_BUCKET` / `ALIYUN_OSS_PROD_BUCKET` | 否 | 阿里云 OSS Bucket，未配置时可兜底 `ALIYUN_OSS_BUCKET`。 |
| `ALIYUN_OSS_DEV_ACCESS_KEY_ID` / `ALIYUN_OSS_TEST_ACCESS_KEY_ID` / `ALIYUN_OSS_PROD_ACCESS_KEY_ID` | 是 | 阿里云 OSS AccessKey ID，未配置时可兜底 `ALIYUN_OSS_ACCESS_KEY_ID`。 |
| `ALIYUN_OSS_DEV_ACCESS_KEY_SECRET` / `ALIYUN_OSS_TEST_ACCESS_KEY_SECRET` / `ALIYUN_OSS_PROD_ACCESS_KEY_SECRET` | 是 | 阿里云 OSS AccessKey Secret，未配置时可兜底 `ALIYUN_OSS_ACCESS_KEY_SECRET`。 |
| `ALIYUN_OSS_DEV_PUBLIC_BASE_URL` / `ALIYUN_OSS_TEST_PUBLIC_BASE_URL` / `ALIYUN_OSS_PROD_PUBLIC_BASE_URL` | 否 | OSS 对外访问基础 URL，未配置时可兜底 `ALIYUN_OSS_PUBLIC_BASE_URL`。 |
| `ALIYUN_OSS_DEBUG_EML_PREFIX` | 否 | Debug EML 上传对象路径前缀，默认 `debug/eml/`。 |

注意：

- Debug EML 上传接口会接收原始邮件、上传 OSS 并调用 SuperAgent，风险和成本高于普通查询接口。
- Debug EML 上传 key、SuperAgent Open API Key、阿里云 OSS AccessKey 都不得进入前端源码、`VITE_*`、镜像、普通日志或文档真实值。
- Debug EML 写入 SourceMessage Inbox 时 `provider=DEBUG_EML_UPLOAD`，不能伪装为 AgentBus 来源。
- Debug EML 写入 SourceMessage Inbox 时 `external_message_id` 是后端生成的 `debug-eml-run-{debugRunId}-{sha256前缀}`；原始邮件 `Message-ID` 只保存在 payload 的 `source.original_message_id`。
- Debug EML 返回 `html_body_sanitized`、`html_sanitize_required`、`html_render_mode`，前端展示 HTML 时应优先使用清洗字段。
- Debug EML 当前会识别 SuperAgent Open API 返回的旧 `S000/S999,source_message_id`，并在 `superagent_parsed_json` 中返回结构化入口结果；这不是 JSON 解析失败。结构化 `S10/S99` 可通过任务结果通知接口入站，Debug EML 页面若要直接展示完整 V3 入口结构，前端展示仍需继续补齐。
- Debug EML 第一版只展示 SuperAgent 结果，不创建订单、不创建任务、不调用任务结果通知接口。
- AgentBus 实时收到邮件后自动推 SuperAgent 当前未实现，不能把 Debug EML 链路等同于生产实时自动处理链路。

## 4. 数据库上线注意事项

当前 SourceMessage 相关 migration：

- `server/src/main/resources/db/migration/V1__create_source_message_inbox.sql`
- `server/src/main/resources/db/migration/V2__create_source_message_original_access_audit.sql`

当前 M002 订单任务相关 migration：

- `server/src/main/resources/db/migration/V3__create_reservation_ai_task_workflow.sql`
- `server/src/main/resources/db/migration/V4__harden_reservation_task_queue_and_manual_conversion.sql`
- `server/src/main/resources/db/migration/V5__add_reservation_task_draft_and_confirmation.sql`
- `server/src/main/resources/db/migration/V6__create_reservation_opera_simulation_tables.sql`
- `server/src/main/resources/db/migration/V11__add_reservation_order_visibility.sql`
- `server/src/main/resources/db/migration/V16__add_m002_v3_ai_route_fields.sql`

当前 M004 Debug EML 相关 migration：

- `server/src/main/resources/db/migration/V7__create_debug_eml_superagent_run.sql`

当前 SourceMessage duplicate 诊断相关 migration：

- `server/src/main/resources/db/migration/V8__create_source_message_payload_duplicate.sql`

当前 M003 登录权限相关 migration：

- `server/src/main/resources/db/migration/V9__create_identity_access_hotel_menu.sql`
- `server/src/main/resources/db/migration/V10__enforce_single_default_user_hotel.sql`
- `server/src/main/resources/db/migration/V12__enforce_single_active_platform_hotel.sql`

当前 M006 系统管理相关 migration：

- `server/src/main/resources/db/migration/V15__create_platform_admin_audit_log.sql`

上线前确认：

- 目标数据库为空库或 Flyway history 与当前代码一致。
- 如果某个环境已经在缺少 V10 的临时提交上执行过 V11 / V12，不能直接用默认 Flyway 策略补跑 V10；应先重建测试库，或按运维窗口明确 out-of-order / repair 策略。
- MySQL 版本满足项目要求，默认使用 MySQL 8.0+。
- migration 在 UAT 或测试库已经跑过。
- 表和字段中文注释能正常创建。
- 数据库业务时间点按 UTC 写入，接口层返回带 `Z` 的 ISO 8601 UTC 时间，例如 `2026-07-08T03:00:00Z`。
- MySQL JDBC URL 建议明确 `serverTimezone=UTC`；部署容器和 JVM 也应使用 UTC，或至少确认应用代码所有入库时间均通过 UTC 时钟生成。
- AgentBus 邮件来源时间、SuperAgent HMAC timestamp、本系统 `created_at` / `updated_at` 等时间点统一按 UTC 理解；SourceMessage `received_at` 优先保存 AgentBus payload `received_at`，缺失时回退本系统接收时间，前端展示时再按用户或酒店时区格式化。
- 入住日期、离店日期、酒店营业日属于酒店本地业务日期，不应因为 UTC 换算而自动前后偏移。
- 详细时间设计、页面展示和按酒店本地日期筛选规则见 `docs/project/backend-time-design.md`。
- 执行 V4 前，如果目标库已有 M002 试运行数据，必须先检查 ACTIVE 订单业务号重复和同订单任务队列序号重复。
- 执行 V5 / V6 前，如果目标库已有 M002 试运行数据，必须确认任务草稿、确认 payload 和 OPERA 模拟操作表允许从空数据开始补齐；不要手工伪造已确认 payload 或 attempt 历史。
- 执行 V16 前，确认 `workflow_reservation_ai_transition.result_type` 和 `workflow_reservation_task.result_type` 扩容到 `VARCHAR(64)` 不会被历史手工约束阻断；V16 会新增 `route_code`、`system_process_category`、`adapter_error_code`、`adapter_error_message` 和对应查询索引。
- 执行 V11 前，如果目标库已有手工造数或历史隐藏订单方案，必须确认是否需要回填 `order_visibility`；默认值 `VISIBLE` 会让历史订单继续出现在订单列表。

V4 前置检查 SQL：

```sql
SELECT hotel_id, order_key_type, order_business_key, COUNT(*) AS duplicate_count
FROM workflow_reservation_order
WHERE order_status = 'ACTIVE'
  AND order_key_type IN ('GROUP_CODE', 'CONFIRMATION_NUMBER')
  AND order_business_key IS NOT NULL
GROUP BY hotel_id, order_key_type, order_business_key
HAVING COUNT(*) > 1;

SELECT hotel_id, order_id, queue_participation, execution_order, COUNT(*) AS duplicate_count
FROM workflow_reservation_task
GROUP BY hotel_id, order_id, queue_participation, execution_order
HAVING COUNT(*) > 1;
```

处理要求：

- 如果任一 SQL 返回记录，不要继续执行 V4。
- ACTIVE 订单业务号重复时，先由业务确认保留哪一条 ACTIVE，其他订单应转为 `LOGIC_DELETED`、`ENDED` 或完成任务迁移后再上线。
- 同订单任务队列序号重复时，先按来源顺序和审计证据重新分配 `execution_order`，确认前置任务关系正确后再上线。
- 不要为了让唯一索引创建成功而随意删除订单、任务或 AI 原始记录。
- OPERA 模拟操作失败只代表单条模拟操作失败，当前任务不会因此自动进入 `FAILED`；上线验证时不能把失败操作当作真实 OPERA 失败处理。

禁止事项：

- 禁止直接修改已发布 migration。
- 禁止手工改生产表结构后再让代码“凑合跑”。
- 禁止把真实邮件正文、附件 URL 或客户数据做成测试种子数据提交。

## 5. 安全与日志

上线前必须确认普通日志、错误响应和状态接口不会输出：

- Authorization、Cookie、CSRF Token。
- Provider API Key、AgentBus Token、数据库密码。
- 邮件正文、HTML、附件 URL、原始 payload。
- 客户姓名、电话、证件号；发件人展示字段因业务识别需要允许返回完整邮箱或名称，但不得写入普通日志、错误响应或调试状态接口。
- 支付信息。

SourceMessage 普通列表和普通详情只能返回安全摘要：

- 可以返回：SourceMessage ID、酒店 ID、provider、channel、外部邮件 ID、外部邮件链 ID、状态、时间、发件人展示值、主题摘要、安全短摘要。
- 不应返回：完整正文、HTML、附件 URL、原始 payload。

原文读取接口只允许在明确授权场景下使用：

```text
GET /api/source-messages/{id}/original
Header: X-TH-Hotel-Source-Original-Read-Key
Header: X-TH-Hotel-Actor
Header: X-TH-Hotel-Access-Scene
```

前端展示 `htmlBody` 前必须 sanitize。后端返回 `htmlSanitizeRequired=true` 是提醒前端不要直接信任 HTML。

## 6. AgentBus 上线注意事项

AgentBus 是消息入口，不是 AI Provider。生产实时链路必须保持以下边界：

- 只写 SourceMessage Inbox。
- 不自动调用 SuperAgent。
- 不创建 MessageEvent、Evidence、Case、Task、Operation、Receipt。
- 不调用 OHIP、ERP、支付系统等业务写接口。
- 不自动发送 ACK、`task.result` 或客户回复。
- 不在普通日志里输出 raw frame、邮件正文、HTML 或附件 URL。

建议上线顺序：

1. 保持 `AGENTBUS_PROBE_ENABLED=false`，先部署服务并确认健康检查。
2. 确认数据库 migration 和 SourceMessage 查询接口正常。
3. 配置 AgentBus URL 和 Token，但仍保持连接关闭。
4. 在约定观察窗口打开 `AGENTBUS_PROBE_ENABLED=true`。
5. 观察 `/api/system/agentbus-probe`，确认连接状态、`sessionReady`、计数器和最近错误代码。
6. 用合成测试邮件验证 SourceMessage Inbox 是否写入。
7. 确认日志和监控没有泄露 raw frame、正文或附件 URL。

如果出现异常：

- 先关闭 `AGENTBUS_PROBE_ENABLED`，停止接收入站 frame。
- 如果只是想暂停入库但保留连接，可关闭 `AGENTBUS_CAPTURE_ENABLED`。
- 保留状态接口、应用日志和数据库记录用于排查，但不要导出真实邮件正文或附件 URL。

## 7. 上线后冒烟验证

后端服务启动后，按顺序验证：

```text
GET /api/health
```

期望：

- HTTP 200。
- `status = UP`。

```text
GET /api/system/agentbus-probe
```

期望：

- HTTP 200。
- 返回 `enabled`、`connected`、`sessionReady`、计数器和最近错误代码。
- 响应中不包含 Token、Authorization、raw frame、payload 或邮件正文。

```text
GET /api/source-messages?pageNum=1&pageSize=20
```

期望：

- HTTP 200。
- 只返回安全摘要。
- 不包含正文、HTML、附件 URL 或原始 payload。

```text
GET /api/source-messages/{id}
```

期望：

- HTTP 200 或 404。
- 如果存在记录，只返回安全摘要。

```text
GET /api/source-messages/{id}/original
```

期望：

- 未携带正确访问 key 时返回 403。
- 携带正确访问 key、调用方和访问场景时返回原文内容，并写入 `platform_source_message_original_access_audit`。

## 8. 监控建议

至少监控：

- 应用进程是否存活。
- `GET /api/health` 是否正常。
- AgentBus `connected` 和 `sessionReady` 状态。
- AgentBus `failedFrameCount`、`rejectedFrameCount` 是否持续增长。
- SourceMessage Inbox 每小时入库数量是否异常突增或归零。
- `FAILED` SourceMessage 数量和安全错误摘要。
- 原文读取审计数量是否异常。
- 数据库连接池、慢 SQL、磁盘空间和 migration 状态。

告警信息不得包含 Secret、正文、HTML、附件 URL 或客户个人信息。

## 9. 回滚与降级

优先降级开关：

```text
AGENTBUS_PROBE_ENABLED=false
AGENTBUS_CAPTURE_ENABLED=false
SOURCE_MESSAGE_PROD_ORIGINAL_READ_ACCESS_KEY=
SOURCE_MESSAGE_ORIGINAL_READ_ACCESS_KEY=
```

说明：

- 关闭 `AGENTBUS_PROBE_ENABLED` 可以停止 WebSocket 入站连接。
- 关闭 `AGENTBUS_CAPTURE_ENABLED` 可以保留连接但暂停写入 Inbox。
- 清空当前 profile 对应的 `SOURCE_MESSAGE_*_ORIGINAL_READ_ACCESS_KEY`，且不配置旧通用变量，可以关闭原文读取接口。

数据库回滚注意：

- 已执行的 migration 不应直接删除或手工回滚。
- 如果新版本已写入 SourceMessage 数据，回滚应用前要确认旧版本是否能兼容新表存在。
- 需要修复表结构时，应新增 migration，而不是修改已发布 migration。

## 10. 上线责任确认

上线前需要有人明确确认：

- 部署版本：确认本次上线的分支、提交和构建产物。
- 数据库：确认 migration、备份和连接信息。
- Secret：确认所有密钥由部署平台注入。
- AgentBus：确认是否开启 WebSocket，是否允许捕获入库。
- 安全：确认日志、错误响应、状态接口和监控面板没有敏感数据。
- 业务：确认当前上线范围不包含 replay、AI、Case、Task、客户回复或 OHIP 写操作。

只要上述任何一项没人确认，就不要打开生产实时入口。
