Compare commits

..

2 Commits

Author SHA1 Message Date
andy
e5218e10eb 同步V4与M011文档状态 2026-07-20 14:44:14 +07:00
andy
8f893991dd 补齐订单详情V4总览接口 2026-07-20 14:38:09 +07:00
18 changed files with 712 additions and 69 deletions

View File

@@ -4,15 +4,15 @@
| --- | --- |
| 最近更新 | 2026-07-20 |
| 当前分支 | `feature/huangting` |
| 当前阶段 | M002 V4 入站、多卡模型、持久化基线、入站写入、查询接口、卡片确认、复核解阻、目录校验、订单详情时间线、DB 目录、Lookup API、前端 lookup 接入、目录管理后台 CP1 前后端、订单列表 V4 继续处理入口 / open count 收口和 V4 业务审计查询 |
| 当前重点 | M002 V4 已完成订单列表 V4 继续处理入口字段和前端消费,`GET /api/reservation/orders` 可返回 V4 下一步订单任务、卡片、动作类型、动作状态、V4 open 数和统一展示字段 `open_work_item_count`前端“继续处理”已按 V4 优先、旧任务回退跳转,订单列表待处理数量已改用 `open_work_item_count` 展示V4 订单任务和 S10/S99 来源通知已补齐业务审计查询接口,前端可展示卡片确认、复核解阻和 ack 审计时间线;后续可做测试机联调、真实 PMS / OPERA / OHIP 同步或 SuperAgent 目录供给方案 |
| 当前阶段 | M002 V4 入站、多卡模型、持久化基线、入站写入、查询接口、卡片确认、复核解阻、目录校验、订单详情 V4 总览、DB 目录、Lookup API、前端 lookup 接入、目录管理后台 CP1 前后端、订单列表 V4 继续处理入口 / open count 收口和 V4 业务审计查询 |
| 当前重点 | M002 V4 已完成订单列表 V4 继续处理入口字段和前端消费,`GET /api/reservation/orders` 可返回 V4 下一步订单任务、卡片、动作类型、动作状态、V4 open 数和统一展示字段 `open_work_item_count``GET /api/reservation/orders/{orderId}` 已补齐订单详情 `order_overview``next_v4_action``related_source_messages[]``v4_order_tasks[].cards[]`,支撑订单总览页V4 订单任务和 S10/S99 来源通知已补齐业务审计查询接口;后续可做前端订单详情 V4 化、测试机联调、真实 PMS / OPERA / OHIP 同步或 SuperAgent 目录供给方案 |
## 1. 当前 Checkpoint
- 名称:`M002-V4-business-audit-query`
- 状态DoneV4 订单任务和来源通知已补齐业务审计查询接口`GET /api/reservation/order-tasks/{orderTaskId}/audits` 返回卡片确认 / 复核解阻审计流水,`GET /api/reservation/source-notifications/{notificationId}/audits` 返回 S10/S99 ack 审计流水。两个接口均强制 Bearer 登录、`RESERVATION_AUDIT_READ` 和对象所属酒店访问权,并对返回快照做敏感字段脱敏
- 目标:让前端可在 V4 订单任务详情和 S10/S99 来源通知详情展示审计时间线
- 边界:本轮不新增审计表、不调整 V4 写操作、不接真实 OPERA / OHIP、不处理旧 V2/V3 审计展示重构
- 名称:`M002-V4-order-detail-overview-backend`
- 状态Done订单详情接口已补齐 V4 订单总览数据`GET /api/reservation/orders/{orderId}` 返回 `order_overview``next_v4_action``related_source_messages[]``v4_order_tasks[].cards[]``order_overview` 只从已确认 V4 卡片派生,未确认 AI 建议不作为订单事实;`next_v4_action` 复用订单列表下一步处理口径,前端仍跳 V4 订单任务详情处理
- 目标:支撑前端把订单详情页从旧任务处理页改成“订单总览 + 当前确认快照 + V4 任务时间线 + 下一步入口”
- 边界:本轮不改前端、不新增表、不 V4 写操作、不接真实 OPERA / OHIP、不废弃旧 V2/V3 详情接口
## 2. 当前优先级
@@ -34,16 +34,16 @@
- `docs/import/` 下按日期导入的资料是输入材料,不等同于当前权威开发契约;当前开发应优先看 `docs/project/README.md` 标记为当前有效或权威契约的文档。
- 后续每完成一个 Feature 或 Checkpoint需要更新本文件避免项目状态继续沉淀在聊天记录里。
- M010 Rooming List Excel 生成后端 CP1 和前端 V1 已实现:前端 `/reservation/rooming-lists/new` 上传来源名单和手工字段,后端同步生成 `.xlsx` 直接下载,第一版不落库、不上传 OSS。
- M011 Booking Excel 附件预处理 CP1/CP2/CP3 已实现:后端可排除人员名单类 Excel按最近 6 个月候选窗口选择实际存在的最新 3 个业务月,抽取 Booking Update / 附加费表高亮行业务 JSONDebug EML 和 AgentBus dispatch 在各自 include 开关与总开关同时启用时,会在调用 SuperAgent 前追加 `attachment_extractions[]`。AgentBus 生产链路默认关闭,测试机验证后再评估开启
- M002 V4 CP1 当前已完成入站解析和现有任务链路过渡适配M002 V4 CP2 已完成订单任务与多卡领域模型设计M002 V4 CP3 已完成 V4 订单任务、多卡和 S10/S99 来源通知表结构与 Repository 基线M002 V4 CP4 已完成入站写入新模型M002 V4 CP5 已完成前端查询接口并补齐订单详情 `v4_order_tasks[]` 时间线M002 V4 CP6 已完成普通卡片确认和 S10/S99 来源通知 ackM002 V4 CP7 已完成 `REVIEW_REQUIRED` 卡复核解阻和复核场景订单归属确认M002 V4 CP8 已完成目录校验、V4 卡片 `fields[]` 字段白名单、确认写入白名单收口和嵌套业务字段目录校验M002 V4 CP11 已完成数据库目录、初始化种子、启动补种子、Account / Room Type / Rate Code lookup API并把 V4 入站、确认、复核目录校验切换到当前酒店数据库目录M002 V4 CP12 已完成前端 lookup 接入第一版和 V4 订单任务时间线消费M002 V4 CP13 目录管理后台 CP1 已完成前后端列表、新增、启用 / 停用闭环M002 V4 CP14 已完成订单列表 V4 继续处理入口字段和前端入口消费,`GET /api/reservation/orders` 返回 V4 下一步订单任务、卡片、动作类型、动作状态、V4 open 数和统一展示计数 `open_work_item_count`,前端按 V4 优先、旧任务回退跳转,并按 `open_work_item_count` 展示待处理数量V4 业务审计查询已补齐订单任务审计和来源通知 ack 审计两个只读接口。真实 PMS 同步和 SuperAgent 目录机器接口仍未完成。
- M011 Booking Excel 附件预处理 CP1/CP2/CP3 已实现:后端可排除人员名单类 Excel按最近 6 个月候选窗口选择实际存在的最新 3 个业务月,抽取 Booking Update / 附加费表高亮行业务 JSONDebug EML 和 AgentBus dispatch 在各自 include 开关与总开关同时启用时,会在调用 SuperAgent 前追加 `attachment_extractions[]`测试机 AgentBus 增强已开启;生产链路默认关闭,生产开启需单独确认
- M002 V4 CP1 当前已完成入站解析和现有任务链路过渡适配M002 V4 CP2 已完成订单任务与多卡领域模型设计M002 V4 CP3 已完成 V4 订单任务、多卡和 S10/S99 来源通知表结构与 Repository 基线M002 V4 CP4 已完成入站写入新模型M002 V4 CP5 已完成前端查询接口并补齐订单详情 `v4_order_tasks[]` 时间线M002 V4 CP6 已完成普通卡片确认和 S10/S99 来源通知 ackM002 V4 CP7 已完成 `REVIEW_REQUIRED` 卡复核解阻和复核场景订单归属确认M002 V4 CP8 已完成目录校验、V4 卡片 `fields[]` 字段白名单、确认写入白名单收口和嵌套业务字段目录校验M002 V4 CP11 已完成数据库目录、初始化种子、启动补种子、Account / Room Type / Rate Code lookup API并把 V4 入站、确认、复核目录校验切换到当前酒店数据库目录M002 V4 CP12 已完成前端 lookup 接入第一版和 V4 订单任务时间线消费M002 V4 CP13 目录管理后台 CP1 已完成前后端列表、新增、启用 / 停用闭环M002 V4 CP14 已完成订单列表 V4 继续处理入口字段和前端入口消费,`GET /api/reservation/orders` 返回 V4 下一步订单任务、卡片、动作类型、动作状态、V4 open 数和统一展示计数 `open_work_item_count`,前端按 V4 优先、旧任务回退跳转,并按 `open_work_item_count` 展示待处理数量V4 业务审计查询已补齐订单任务审计和来源通知 ack 审计两个只读接口;订单详情已补齐 V4 `order_overview``next_v4_action``related_source_messages[]``v4_order_tasks[].cards[]`。真实 PMS 同步和 SuperAgent 目录机器接口仍未完成。
- M002 V4 CP2 已确认V4 工作台统一列表草案为 `/api/reservation/workbench-items`,业务订单任务接口新开 `/api/reservation/order-tasks/**`S10/S99 来源通知详情草案为 `/api/reservation/source-notifications/{notificationId}`S10/S99 使用来源通知模型,不再挂隐藏技术订单;`FIT + BOOKING_CODE` 不建 ACTIVE 唯一约束匹配多条进人工复核Basic Information 必须先确认Account / Market / Source 当前通过数据库目录读取和派生;旧 V2/V3 任务详情和草稿确认接口后续可逐步废弃。
## 5. Next Steps
- 后续如继续做 M002 V4可优先进入测试机 V4 订单列表、V4 详情V4 审计时间线前后端联调,或推进真实 PMS / OPERA / OHIP 目录同步和 `workflow_reservation_catalog_sync_run` checkpoint。
- 后续如继续做 M002 V4可优先让前端改造订单详情 V4 总览页并进行测试机联调,或推进真实 PMS / OPERA / OHIP 目录同步和 `workflow_reservation_catalog_sync_run` checkpoint。
- 后续新增重要功能时,优先在 `docs/project/requirements/` 或未来 `docs/specs/` 中形成 Spec再实现代码。
- M010 后续如需预览、历史记录、OSS 下载、订单 / 任务预填或客户字段目录化,再单独开前后端 checkpoint。
- M011 后续如需运营查询或长期追踪,可进入 CP4设计 Excel 解析批次 / 行级持久化表;当前 CP3 只增强 SuperAgent 输入,不直接落订单、任务或长期解析历史。
- M011 CP4 暂不推进;当前停留在 CP3 边界,只增强 SuperAgent 输入,不直接落订单、任务或长期解析历史。后续如确实需要运营查询或长期追踪,再单独设计 Excel 解析批次 / 行级持久化表。
## 6. 文档同步提醒

View File

@@ -46,7 +46,7 @@
| `requirements/M002-order-task-workflow-v3.md` | 当前有效 | M002 订单任务主流程 V3基于 2026-07-11 P0 冻结基线和 2026-07-12 P0.1 Parent Group 修订,记录 S10/S99、40 路由、方案 C、type-known manual review 同卡解阻和 fail-closed 边界。 |
| `requirements/M002-task-field-control-contract-v1.md` | 当前有效 | M002 任务卡字段控件契约 V1记录任务详情 `fields[]` 控件元数据、人工复核控件复用和前后端开发边界。 |
| `requirements/M002-v4-agent-callback-field-contract.md` | 当前有效 | M002 V4 Agent 回调字段契约,基于 2026-07-18 业务基线和最新答复,冻结 `source_message``order_contexts``message_events`、订单级 Basic Information、六类 Event、S10/S99 和校验口径;后端已完成 V4 入站解析、持久化、查询、确认、复核和当前酒店数据库目录校验。 |
| `requirements/M002-v4-order-task-card-domain-model-cp2.md` | 当前有效 | M002 V4 CP2 订单任务与多卡领域模型设计,并记录 CP3-CP8 表结构、入站写入、查询、确认、复核和目录校验已落地状态CP11 已完成 DB 目录与 lookup API后续仍需前端页面、目录管理后台和真实 PMS 同步。 |
| `requirements/M002-v4-order-task-card-domain-model-cp2.md` | 当前有效 | M002 V4 CP2 订单任务与多卡领域模型设计,并记录 CP3-CP8 表结构、入站写入、查询、确认、复核和目录校验已落地状态CP11 已完成 DB 目录与 lookup APICP12 已完成前端 lookup 接入CP13 已完成目录管理后台 CP1CP14 已完成订单列表 V4 继续处理入口CP15 已完成 V4 业务审计查询CP15.1 已完成订单详情 V4 总览后端补齐;后续仍需前端订单详情 V4 化、测试机联调和真实 PMS / OPERA / OHIP 同步。 |
| `requirements/M002-v4-real-catalog-lookup-api-design.md` | 当前有效 | M002 V4 真实目录与 Lookup API 设计及 CP11 / CP13 CP1 实现记录,记录 Account、Market、Source、Room Type、Rate Code 从固定种子导入数据库、前端 lookup API、目录管理后端接口、权限、缓存后置、PMS / OPERA / OHIP 同步后置和失败兜底。 |
| `requirements/M002-v4-test-machine-smoke-checklist.md` | 当前有效 | M002 V4 测试机冒烟清单覆盖登录、酒店权限、V4 工作台、订单任务详情、lookup、确认、复核解阻、S10/S99 ack、订单详情 V4 时间线和目录管理 CP1 排查点。 |
| `requirements/M002-superagent-task-result-api-contract.md` | 阶段记录 | M002 SuperAgent 任务结果入站接口契约阶段记录;对外总契约以 `integrations/superagent-api-contract.md` 为准。 |
@@ -61,7 +61,7 @@
| `requirements/M008-excel-to-pdf-conversion-v1.md` | 当前有效 | M008 Excel 转 PDF 文件转换能力方案,记录 LibreOffice headless、手动上传转换、邮件附件自动派生 PDF 和部署要求CP2 已实现手动上传后端接口。 |
| `requirements/M009-manual-invoice-generation-v1.md` | 当前有效 | M009 Manual Invoice 手工开票生成方案;后端 CP2 已支持无订单 / 无任务手工填写、填充 Excel 模板、转 PDF、OSS 输出和生成记录。 |
| `requirements/M010-rooming-list-excel-generation-v1.md` | 当前有效 | M010 Rooming List Excel 生成方案;后端 CP1 已支持前端上传来源名单和手工字段,同步生成 `.xlsx` 直接下载,不落库、不上传 OSS。 |
| `requirements/M011-booking-excel-pre-superagent-enrichment-v1.md` | 当前有效 | M011 Booking Excel 附件预处理方案CP1/CP2/CP3 已支持 Debug EML 和 AgentBus dispatch 调 SuperAgent 前排除人员名单类 Excel、抽取 Booking / 附加费类 Excel 高亮行并追加 `attachment_extractions[]`生产 AgentBus 增强默认关闭。 |
| `requirements/M011-booking-excel-pre-superagent-enrichment-v1.md` | 当前有效 | M011 Booking Excel 附件预处理方案CP1/CP2/CP3 已支持 Debug EML 和 AgentBus dispatch 调 SuperAgent 前排除人员名单类 Excel、抽取 Booking / 附加费类 Excel 高亮行并追加 `attachment_extractions[]`测试机 AgentBus 增强已开启生产默认关闭CP4 暂不推进。 |
## 集成契约
@@ -94,6 +94,6 @@
- 接口暴露、权限、酒店隔离和审计边界以 `security-access-control-boundary.md` 为总检查清单;具体 SuperAgent / MCP / AgentBus 请求响应契约仍以 `integrations/` 下对应文档为准。
- AI-NSES 的通用标准以 `../import/reusable/ai-native-software-engineering-standard.md` 为复用来源;本项目采用方式以 `ai-native-adoption.md` 为准。
- M002 V1 只作为历史参考V2 记录当前阶段实现;后续 M002 新开发以 `requirements/M002-order-task-workflow-v3.md` 为开发基线。
- 2026-07-18 导入的业务基线已形成 `requirements/M002-v4-agent-callback-field-contract.md` 字段契约M002 V4 入站解析 CP1 已落地V4 订单任务 + 多卡领域模型设计和关键业务决策见 `requirements/M002-v4-order-task-card-domain-model-cp2.md`M002 V4 CP3 已落地 V4 订单任务、任务卡、来源通知表结构和 Repository 基线CP4 已落地普通 V4 业务包和 S10/S99 来源通知入站写入新模型CP5 已落地工作台、订单任务和来源通知查询接口CP6/CP7 已落地卡片确认、S10/S99 ack 和复核解阻CP11 已落地 DB 目录与 lookup API;后续仍需前端页面模型、目录管理后台和真实 PMS 同步。
- 2026-07-18 导入的业务基线已形成 `requirements/M002-v4-agent-callback-field-contract.md` 字段契约M002 V4 入站解析 CP1 已落地V4 订单任务 + 多卡领域模型设计和关键业务决策见 `requirements/M002-v4-order-task-card-domain-model-cp2.md`M002 V4 CP3 已落地 V4 订单任务、任务卡、来源通知表结构和 Repository 基线CP4 已落地普通 V4 业务包和 S10/S99 来源通知入站写入新模型CP5 已落地工作台、订单任务和来源通知查询接口CP6/CP7 已落地卡片确认、S10/S99 ack 和复核解阻CP11 已落地 DB 目录与 lookup APICP12 已落地前端 lookup 接入CP13 已落地目录管理后台 CP1CP14 已落地订单列表 V4 继续处理入口CP15 已落地 V4 业务审计查询CP15.1 已落地订单详情 V4 总览后端补齐;后续仍需前端订单详情 V4 化、测试机联调和真实 PMS / OPERA / OHIP 同步。
- 前端展示 / 编辑字段以 2026-07-11 P0 冻结基线中的前端字段表、0712 字段控件说明和 `requirements/M002-task-field-control-contract-v1.md` 为白名单和控件契约基线;后端完整校验和 OPERA 映射仍以任务卡完整矩阵、0711 runtime 契约和后端规则为准。
- 时间点语义以 `backend-time-design.md` 为准;数据库时间点按 UTC 理解API 返回带 `Z` 的 UTC 时间,页面再按酒店或用户时区展示。

View File

@@ -48,7 +48,7 @@
| 订单任务多卡模型 V4 | `docs/project/requirements/M002-v4-order-task-card-domain-model-cp2.md` | 当前 V4 主入口后端已开放工作台统一列表、V4 订单任务列表 / 详情、卡片确认、复核解阻、S10/S99 来源通知详情和 ack前端已完成 V4 页面第一版、目录 lookup 接入、订单详情 V4 时间线消费和系统设置目录管理 CP1。 |
| Manual Invoice 手工开票生成 | `docs/project/requirements/M009-manual-invoice-generation-v1.md` | 当前有效;后端 CP2 已支持无订单 / 无任务手工填写字段、填 Excel 模板、转 PDF、OSS 输出和生成记录。 |
| Rooming List Excel 生成 | `docs/project/requirements/M010-rooming-list-excel-generation-v1.md` | 当前有效;后端 CP1 已支持前端上传来源名单并填写目标字段,同步生成 `.xlsx` 直接下载;前端 V1 已新增 `/reservation/rooming-lists/new`,按 Blob 下载处理,不落库、不上传 OSS。 |
| Booking Excel 附件预处理 | `docs/project/requirements/M011-booking-excel-pre-superagent-enrichment-v1.md` | 当前有效Debug EML 和 AgentBus dispatch 已支持调 SuperAgent 前排除人员名单类 Excel、抽取 Booking / 附加费类 Excel 高亮行并生成 `attachment_extractions[]`生产 AgentBus 增强默认关闭。 |
| Booking Excel 附件预处理 | `docs/project/requirements/M011-booking-excel-pre-superagent-enrichment-v1.md` | 当前有效Debug EML 和 AgentBus dispatch 已支持调 SuperAgent 前排除人员名单类 Excel、抽取 Booking / 附加费类 Excel 高亮行并生成 `attachment_extractions[]`测试机 AgentBus 增强已开启生产默认关闭CP4 暂不推进。 |
| 订单任务主流程 V2 | `docs/project/requirements/M002-order-task-workflow-v2.md` | 已实现阶段记录,保留用于理解当前代码中的 S000/S999、订单任务流转和 OPERA 模拟骨架。 |
| 后端 checkpoint | `docs/project/requirements/M002-backend-checkpoint-plan.md` | 阶段记录,用于理解后端拆分和验收。 |
| 前端可用接口与待补接口 | `docs/project/frontend-backend/frontend-to-backend-api-requests.md` | 前后端协作清单,已区分可用、后置和历史候选路径,不替代后端权威契约。 |
@@ -65,7 +65,7 @@
- 任务卡字段控件契约 V1 后端第一版已完成,任务详情 `fields[]` 已返回 `control_type/edit_scope/write_target/options_source/raw_readonly/control_hint`;前端后续按契约接入,不要硬编码 PMS 房型、Rate Code 或未冻结枚举。
- Manual Invoice 第一阶段按 M009 推进:后端已提供 `POST /api/reservation/invoices/manual-generations`,前端已新增 `/reservation/invoices/new` 手工开票页面,并已按 `invoice.html` 原型的三段式业务结构对齐;页面可以不依赖订单或任务,用户手工填写 / 选择字段后由后端业务接口填充 Excel 模板并生成 PDF前端不得直接调用 M008 的调试上传转换接口来完成业务开票。侧边栏入口仍以登录后端返回的 menus 为准,建议后续在菜单管理中配置 `RESERVATION_MANUAL_INVOICE` / `/reservation/invoices/new` / `RESERVATION_INVOICE_GENERATE`
- Rooming List Excel 后端 CP1 和前端 V1 已按 M010 落地:接口为 `POST /api/reservation/rooming-lists/generations`,前端页面为 `/reservation/rooming-lists/new`,上传来源名单、填写每房人数和目标列字段,后端同步返回 `.xlsx` 下载;该能力不依赖订单或任务,第一版不落库、不上传 OSS权限码为 `RESERVATION_ROOMING_LIST_GENERATE`
- Booking Excel 附件预处理已按 M011 落地 CP1/CP2/CP3Debug EML 和 AgentBus dispatch 调用 SuperAgent 前由后端解析 Excel 附件,排除人员名单类文件,只把 Booking Update / 附加费表的高亮行业务摘要追加为 `attachment_extractions[]`第一版不新增前端普通业务入口AgentBus 增强生产默认关闭。
- Booking Excel 附件预处理已按 M011 落地 CP1/CP2/CP3Debug EML 和 AgentBus dispatch 调用 SuperAgent 前由后端解析 Excel 附件,排除人员名单类文件,只把 Booking Update / 附加费表的高亮行业务摘要追加为 `attachment_extractions[]`;第一版不新增前端普通业务入口,测试机 AgentBus 增强已开启,生产默认关闭CP4 暂不推进
- Reservation V4 目录管理后台 CP1 已前后端接入:系统设置下新增 `/system/reservation-catalogs`,需要登录用户具备 `RESERVATION_CATALOG_MANAGE`;前端可维护 Account、Room Type、Rate Code 的列表、新增、启用 / 停用,并明确提示停用目录不再进入普通 V4 任务卡 lookup。
## 6. 前端开发注意事项

View File

@@ -76,7 +76,7 @@
| `POST /api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution` | V4 复核解阻并确认卡片 | 必须带 Bearer token需要 `RESERVATION_MANUAL_REVIEW_RESOLVE`,仅用于 `card_status=REVIEW_REQUIRED`;请求 JSON 带 `version`,可选 `field_overrides[]``reason`;订单任务归属未解决时 `confirmed_order_id` 必填,且必须是当前酒店下真实可见订单;目录错误字段可按 `validation_errors_json` / `fields[].validation_errors` 指向的 pointer 修正;成功后卡片 `CONFIRMED``review_status=RESOLVED`,写 `review_resolution_json/confirmed_payload_json/confirmed_at/confirmed_by` 并返回刷新后的订单任务详情。 |
| `POST /api/reservation/source-notifications/{notificationId}/ack` | 确认 V4 S10/S99 来源通知已读 / 已处理 | 必须带 Bearer token需要 `RESERVATION_TASK_CONFIRM`,请求 JSON 带 `version`;仅允许 `route_code=S10/S99`;确认后 `notification_status=ACKED`,写 `ack_by/ack_at`,成功返回刷新后的来源通知详情;重复 ack 返回当前已确认状态且不新增审计;该动作不创建订单、不参与订单阻塞。 |
| `GET /api/reservation/order-tasks/{orderTaskId}/audits` / `GET /api/reservation/source-notifications/{notificationId}/audits` | 查询 V4 业务审计展示数据 | 必须带 Bearer token需要 `RESERVATION_AUDIT_READ`;前端可在 V4 订单任务详情和 S10/S99 来源通知详情的“审计时间线”中调用。响应沿用旧审计行结构:`audit_id``actor_type``actor_id``action``reason``before_snapshot``after_snapshot``occurred_at`。快照已由后端脱敏,前端仍不要把未知 URL-like 字符串当附件或正文直渲。 |
| `GET /api/reservation/orders/{orderId}` | 查询订单详情与任务时间线 | 必须带 Bearer token需要 `RESERVATION_ORDER_READ`,后端按订单所属酒店做访问校验;`include_tasks=false` 可只取订单摘要,此时旧 `tasks[]`V4 `v4_order_tasks[]` 都为空数组;旧 `tasks[]` 按后端队列顺序返回前端不要自行按创建时间重排V4 `v4_order_tasks[]` 按同订单 V4 订单任务来源时间正序返回;隐藏技术订单详情不可作为普通订单页打开。 |
| `GET /api/reservation/orders/{orderId}` | 查询订单详情与任务时间线 | 必须带 Bearer token需要 `RESERVATION_ORDER_READ`,后端按订单所属酒店做访问校验;`include_tasks=false` 可只取轻量摘要,此时旧 `tasks[]`V4 `v4_order_tasks[]` `related_source_messages[]` 都为空数组,`order_overview` 为空快照,`next_v4_action.action_type=NONE`;旧 `tasks[]` 按后端队列顺序返回前端不要自行按创建时间重排V4 `v4_order_tasks[]` 按同订单 V4 订单任务来源时间正序返回;隐藏技术订单详情不可作为普通订单页打开。 |
| `GET /api/reservation/tasks/{taskId}` | 查询任务详情 | 必须带 Bearer token需要 `RESERVATION_TASK_READ`,后端按任务所属酒店做访问校验;以返回的可处理状态和只读原因控制按钮,不只看任务状态;`fields[]` 已包含 P0 字段元数据;旧源邮件只读通知卡字段列表和 OPERA 操作列表为空V3 结构化 S10/S99 通过 `source_message_only_result.agent_assessment``notification``manual_review` 展示;普通业务任务可通过 `adapter_contract_errors[]``unhandled_intents[]` 查看同批次未建任务的诊断信息type-known manual review 会返回顶层 `review_status``review_resolution``manual_review`。 |
| `PUT /api/reservation/tasks/{taskId}/draft` | 保存任务草稿 | 只保存草稿,不代表用户最终确认。 |
| `POST /api/reservation/tasks/{taskId}/confirm` | 最终确认任务 | 后端会做第一版字段校验,通过后进入 `READY`。 |
@@ -106,7 +106,7 @@
| --- | --- | --- |
| `GET /api/reservation/orders` | 补齐订单列表 V4 继续处理入口字段,并新增统一 open count 字段;前端展示已接入。 | `order_status` 不传时默认查询全部订单状态;`page_num` 从 1 开始;`page_size` 后端有最大值保护;`open_work_item_count` 是订单列表展示用统一待处理数量,开发阶段不考虑旧数据,第一版直接等于 `v4_open_order_task_count`;前端展示待处理数量时只读该字段,不自行计算旧任务数和 V4 数,也不使用旧 `open_task_count` 作为展示数量;旧 `open_task_count``next_processable_task_id` 继续保留用于 V2/V3 兼容与排查。V4 新增 `next_v4_order_task_id``next_v4_action_card_id``next_v4_action_type``next_v4_action_status``v4_open_order_task_count`;前端“继续处理”已按优先级实现:存在 `next_v4_order_task_id` 时跳 `/reservation/order-tasks/{next_v4_order_task_id}`,否则回退旧 `/reservation/tasks/{next_processable_task_id}`;两者都没有时展示无待处理状态。 |
| `GET /api/reservation/tasks` | 补齐来源邮件会话摘要字段,并新增 `order_status` 查询参数。 | `order_status` 按任务所属订单状态过滤,支持 `TEMPORARY``ACTIVE``ENDED``LOGIC_DELETED`列表仍然只返回安全摘要不返回正文、HTML、附件 URL 或 AI 原始 payload点击邮件入口时使用 `source_message_id` 调会话详情。 |
| `GET /api/reservation/orders/{orderId}` | 补齐旧 `tasks[]` 来源邮件会话摘要字段,并新增 V4 `v4_order_tasks[]` 订单任务时间线。 | `include_tasks=false` 可只取订单摘要,此时 `tasks[]``v4_order_tasks[]` 都为空;旧 `tasks[]` 顺序由后端按订单队列返回V4 `v4_order_tasks[]``source_received_at``source_message_id``order_context_index``created_at`、数字 ID 正序返回;前端不要自行重排。`include_source_summary` 第一版不作为前端裁剪字段的强约束,前端暂不要依赖它减少返回字段。 |
| `GET /api/reservation/orders/{orderId}` | 补齐旧 `tasks[]` 来源邮件会话摘要字段,并新增 V4 总览和 `v4_order_tasks[]` 订单任务时间线。 | `include_tasks=false` 可只取轻量摘要,此时 `tasks[]``v4_order_tasks[]` `related_source_messages[]` 都为空,`order_overview` 为空快照,`next_v4_action.action_type=NONE`;旧 `tasks[]` 顺序由后端按订单队列返回V4 `v4_order_tasks[]``source_received_at``source_message_id``order_context_index``created_at`、数字 ID 正序返回;前端不要自行重排。`include_source_summary` 第一版不作为前端裁剪字段的强约束,前端暂不要依赖它减少返回字段。 |
| `GET /api/reservation/tasks/{taskId}` | 补齐顶层来源邮件字段,并扩展 `fields[]` 元数据。 | 顶层来源字段用于打开邮件会话;`fields[]` 中的 `result_type``task_type``task_subtype``default_value_source` 用于前端字段分组、调试和白名单对齐。 |
| `GET /api/source-messages/{sourceMessageId}/conversation` | 新增邮件会话详情接口,并补齐 `html_body_sanitized` / `html_render_mode`。 | 当前唯一推荐路径是这个接口;前端渲染邮件 HTML 时优先使用 `html_body_sanitized`;不要调用历史讨论过的 `/api/source-message-conversations/{externalConversationId}`。 |
| `POST /api/system/debug/eml-superagent-runs` | 新增 Debug EML 上传到 SuperAgent 调试接口,并补齐独立 Debug 外部消息 ID、原始 Message-ID 保留、安全 HTML 字段和入口通知识别。 | 只用于调试页面;请求为 multipart/form-data必须传 `X-TH-Hotel-Debug-Upload-Key`,但该 key 不能写进前端源码、构建产物、URL、localStorage 或错误上报SuperAgent 返回旧 S000/S999 或新 S10/S99 入口通知时都不应被前端视为 JSON 解析失败。 |
@@ -157,6 +157,10 @@ POST /api/auth/logout
| `external_conversation_id` | 外部邮件会话 ID。 | 仅用于展示或调试,不作为当前会话详情接口路径参数。 |
| `conversation_message_count` | 同一外部会话下的邮件数量。 | 用于提示用户打开的是整段会话,不是单封邮件。 |
订单详情已经补齐 V4 订单页总览字段:`order_overview``next_v4_action``related_source_messages[]` 和增强后的 `v4_order_tasks[].cards[]`。订单详情页应定位为订单视角总览,不要在订单详情页直接编辑、确认或复核任务卡;点击 `next_v4_action.order_task_id` 或时间线 `order_task_id` 后进入 `/reservation/order-tasks/{orderTaskId}` 对应的 V4 订单任务详情页处理。
`order_overview` 只从已确认 V4 卡片派生Basic Information 未确认时 Account / Market / Source 为空Room Information 未确认时日期、Rate Code、房型房量为空。前端不要把未确认卡片 display payload 反推成订单事实。
订单详情新增的 V4 `v4_order_tasks[]` 每项只返回订单任务安全摘要:
| 字段 | 说明 | 前端使用方式 |
@@ -165,6 +169,7 @@ POST /api/auth/logout
| `order_ref` | SuperAgent V4 回调包内订单引用。 | 用于区分同一邮件里的多个订单上下文,不等同 PMS 永久订单号。 |
| `order_task_status` | V4 订单任务状态,当前为 `OPEN` / `COMPLETED`。 | 展示处理状态,不要用于替代卡片级 `availability`。 |
| `card_counts` | V4 卡片数量摘要。 | 展示待确认、待复核和已确认规模。 |
| `cards[]` | V4 任务卡安全摘要,只包含卡片 ID、类型、状态、复核状态、确认人、确认时间和最近活动时间。 | 用于订单页时间线和状态 chips业务字段仍去 V4 订单任务详情读取。 |
| `source_message_summary` | 来源邮件安全摘要。 | 不包含正文、HTML、附件 URL 或 AI 原始 payload需要看原文时继续调用邮件会话详情接口。 |
| `source_received_at` / `created_at` / `updated_at` / `latest_activity_at` | UTC 时间点。 | `latest_activity_at` 是 V4 订单任务及其卡片更新时间的最大值,可用于展示最近动作时间。 |

View File

@@ -30,7 +30,7 @@
| 接口 / 能力 | 当前后端状态 | 前端是否可直接接入 | 仍需后端处理 |
| --- | --- | --- | --- |
| `GET /api/reservation/tasks` | 已完成第一版,已补来源邮件会话字段和所属订单状态筛选 | 可以 | `order_status` 按任务所属订单状态过滤;不传时保持当前全部任务列表行为。 |
| `GET /api/reservation/orders/{orderId}` | 已完成第一版,已补旧 `tasks[]` 来源邮件会话字段和 V4 `v4_order_tasks[]` 时间线 | 可以 | 暂无;`include_tasks=false` 时旧 `tasks[]`V4 `v4_order_tasks[]` 都返回空数组。 |
| `GET /api/reservation/orders/{orderId}` | 已完成第一版,已补旧 `tasks[]` 来源邮件会话字段、V4 总览和 V4 `v4_order_tasks[]` 时间线 | 可以 | 暂无;`include_tasks=false` 时旧 `tasks[]`V4 `v4_order_tasks[]` `related_source_messages[]` 都返回空数组,`order_overview` 为空快照,`next_v4_action.action_type=NONE`。 |
| `GET /api/reservation/tasks/{taskId}` | 已完成第一版,已补任务顶层来源邮件字段和 `fields[]` 3.0 元数据 | 可以 | 当前 Controller 不接收 `hotel_id`;如后续多酒店隔离需要前端显式传酒店上下文,请后端补可选入参或确认按 taskId 全局唯一即可。 |
| `PUT /api/reservation/tasks/{taskId}/draft` | 已完成 | 可以 | 当前 Controller 不接收 `hotel_id`;如写操作需要酒店上下文幂等 / 权限校验,请后端补可选入参或请求体字段。 |
| `POST /api/reservation/tasks/{taskId}/confirm` | 已完成 | 可以 | 当前 Controller 不接收 `hotel_id`;如写操作需要酒店上下文幂等 / 权限校验,请后端补可选入参或请求体字段。 |
@@ -47,7 +47,7 @@
| `POST /api/system/reservation/demo-data` | 已完成 | 仅本地 / test 联调可用 | 默认关闭,必须后端配置访问口令;不能作为生产页面接口。 |
| `POST /api/system/debug/eml-superagent-runs` | 已完成第一版 | 仅 dev/test Debug 页面可用 | 默认关闭,必须后端配置访问口令、阿里云 OSS 和 SuperAgent Open API第一版只展示 SuperAgent 结果,不创建订单和任务;已能识别旧 S000/S999 和新结构化 S10/S99。 |
| `GET /api/source-message-conversations/{externalConversationId}` | 未发现后端实现 | 不可以 | 历史讨论过的候选路径,当前不提供;前端统一使用 `GET /api/source-messages/{sourceMessageId}/conversation`。 |
| `GET /api/reservation/message-notifications` | 未发现后端实现 | 不可以 | 第一版不做独立接口;旧 S000/S999 已通过 `SOURCE_MESSAGE_ONLY` 任务展示0711 P0 新 S10/S99 也继续复用任务列表 / 任务详情只读展示。 |
| `GET /api/reservation/message-notifications` | 未发现后端实现 | 不可以 | 历史候选路径,当前不提供。旧 S000/S999 和 V3 S10/S99 兼容数据通过 `SOURCE_MESSAGE_ONLY` 任务展示V4 S10/S99 新数据走 V4 工作台和 `/api/reservation/source-notifications/{notificationId}`,不要再请求本候选路径。 |
| `GET /api/reservation/task-card-field-whitelist` | 未发现后端实现 | 不可以 | 若任务详情 `fields[]` 已补齐 3.0 元数据,可后置。 |
| `GET /api/reservation/lookups/accounts` / `room-types` / `rate-codes` | M002 V4 CP11 已实现 | 可以 | 用于 V4 任务卡下拉 / 搜索选择Bearer token + `RESERVATION_TASK_READ` + 酒店访问权;支持 `hotel_id``keyword``page_num``page_size`,第一版只返回 ACTIVE 目录。 |
@@ -97,7 +97,7 @@ GET /api/reservation/tasks
| --- | --- | --- |
| `hotel_id` | 否 | 酒店 ID。单酒店阶段默认可为空由后端按当前用户上下文或平台酒店表唯一 `ACTIVE` 酒店解析;显式传值时后端会校验访问权限。 |
| `order_id` | 否 | 按订单过滤。 |
| `task_type` | 否 | 当前任务列表筛选只提供 `NEW_BOOKING``UPDATE_BOOKING``CANCEL_BOOKING``MANUAL_REVIEW``SOURCE_MESSAGE_ONLY`;不再提供历史 `INFORMATIONAL_MESSAGE` 筛选项。0711 P0 结构化 S10/S99 复用 `SOURCE_MESSAGE_ONLY` 只读源邮件通知卡。 |
| `task_type` | 否 | 当前任务列表筛选只提供 `NEW_BOOKING``UPDATE_BOOKING``CANCEL_BOOKING``MANUAL_REVIEW``SOURCE_MESSAGE_ONLY`;不再提供历史 `INFORMATIONAL_MESSAGE` 筛选项。旧 S000/S999 和 V3 S10/S99 兼容数据复用 `SOURCE_MESSAGE_ONLY` 只读源邮件通知卡V4 S10/S99 新数据不进旧任务列表,走 V4 工作台和来源通知详情。 |
| `task_status` | 否 | 任务状态过滤。 |
| `task_subtype` | 否 | 任务卡 subtype 过滤。 |
| `order_status` | 否 | 按任务所属订单状态过滤,支持 `TEMPORARY``ACTIVE``ENDED``LOGIC_DELETED`;不传时保持当前行为。 |
@@ -165,9 +165,9 @@ GET /api/reservation/tasks
GET /api/reservation/orders/{orderId}
```
当前状态:后端已按 P0 最小诉求实现第一版。接口返回订单摘要、旧 V2/V3 同订单任务时间线和 V4 订单任务时间线;`include_tasks=false`只返回订单摘要,`tasks[]`V4 `v4_order_tasks[]` 都为空数组。旧任务时间线已补齐每个任务的来源邮件会话摘要。
当前状态:后端已按 P0 最小诉求实现第一版,并在 M002 V4 CP15.1 补齐订单详情 V4 总览字段。接口返回订单摘要、V4 当前确认快照、V4 下一步处理入口、关联来源邮件摘要、旧 V2/V3 同订单任务时间线和 V4 订单任务时间线;`include_tasks=false` 时旧 `tasks[]`V4 `v4_order_tasks[]` `related_source_messages[]` 都返回空数组,`order_overview` 为空快照,`next_v4_action.action_type=NONE`。旧任务时间线已补齐每个任务的来源邮件会话摘要。
订单详情低保真已确认沿用“订单摘要 + 同订单任务队列 + 当前任务卡 + OPERA 模拟操作 + 审计时间线”。同订单任务队列里的每个任务都需要自己的“查看邮件会话”入口,因为不同任务可能来自不同邮件或不同邮件会话。因此 `tasks[]` 中每个任务已补齐来源邮件会话字段。V4 新模型下,同一订单的多卡订单任务通过 `v4_order_tasks[]` 单独返回,前端点击后进入 V4 订单任务详情。
订单详情页后续应定位为“订单总览 + 当前确认快照 + V4 任务时间线 + 下一步入口”,不是 V4 任务卡处理页。同订单任务队列里的每个任务都需要自己的“查看邮件会话”入口,因为不同任务可能来自不同邮件或不同邮件会话。因此 `tasks[]` 中每个任务已补齐来源邮件会话字段。V4 新模型下,同一订单的多卡订单任务通过 `v4_order_tasks[]` 单独返回,前端点击后进入 V4 订单任务详情。
建议入参:
@@ -195,6 +195,45 @@ GET /api/reservation/orders/{orderId}
"created_at": "2026-07-08T03:00:00Z",
"updated_at": "2026-07-08T03:10:00Z"
},
"order_overview": {
"account_code": "QBD_TRAVEL",
"account_name": "Q.B.D. TRAVEL GROUP CO., LTD",
"market_code": "LEISURE",
"source_code": "TRAVEL_AGENT",
"arrival_date": "2026-07-26",
"departure_date": "2026-07-29",
"rate_code": "BAR",
"room_items": [
{
"room_type_code": "RM1",
"room_count": 2
}
],
"trace_card_status": null,
"rooming_list_card_status": null,
"payment_card_status": "REVIEW_REQUIRED",
"latest_confirmed_at": "2026-07-08T04:20:00Z"
},
"next_v4_action": {
"order_task_id": "40001",
"card_id": "41003",
"action_type": "REVIEW",
"action_status": "REVIEW_REQUIRED",
"open_order_task_count": 1
},
"related_source_messages": [
{
"source_message_id": "30002",
"hotel_id": "HOTEL-TEST",
"external_message_id": "AAMk-example",
"external_conversation_id": "thread-20260708-002",
"subject": "Booking Update",
"sender_summary": "guest@example.com",
"received_at": "2026-07-08T04:00:00Z",
"source_sent_at": null,
"conversation_message_count": 2
}
],
"tasks": [
{
"task_id": "10001",
@@ -231,6 +270,32 @@ GET /api/reservation/orders/{orderId}
"review_required_count": 1,
"confirmed_count": 0
},
"cards": [
{
"card_id": "41001",
"card_type": "SOURCE_MESSAGE_DISPLAY",
"event_type": null,
"source_event_index": 0,
"card_sort_order": 10,
"card_status": "READONLY",
"review_status": null,
"confirmed_by": null,
"confirmed_at": null,
"latest_activity_at": "2026-07-08T04:00:10Z"
},
{
"card_id": "41003",
"card_type": "PAYMENT",
"event_type": "PAYMENT",
"source_event_index": 2,
"card_sort_order": 60,
"card_status": "REVIEW_REQUIRED",
"review_status": "PENDING",
"confirmed_by": null,
"confirmed_at": null,
"latest_activity_at": "2026-07-08T04:05:00Z"
}
],
"source_message_summary": {
"source_message_id": "30002",
"hotel_id": "HOTEL-TEST",
@@ -263,11 +328,20 @@ GET /api/reservation/orders/{orderId}
| `external_conversation_id` | 来源消息所属邮件会话 ID。 |
| `conversation_message_count` | 会话内邮件数量。 |
| `result_type` / `ai_task_type` / `route_code` / `system_process_category` | V3 路由展示字段,和任务列表字段语义一致。 |
| `order_overview` | V4 订单详情当前确认快照,只从已确认 V4 卡片派生;未确认 AI 建议不会进入这里。 |
| `order_overview.account_code` / `account_name` / `market_code` / `source_code` | 来自已确认 Basic Information 卡;为空表示 Basic Information 尚未确认或无可靠确认值。 |
| `order_overview.arrival_date` / `departure_date` / `rate_code` / `room_items[]` | 来自已确认 Room Information 卡;后出现的已确认卡会覆盖前面同字段。 |
| `order_overview.trace_card_status` / `rooming_list_card_status` / `payment_card_status` | 当前订单下对应业务卡最新状态,方便订单详情页展示是否还有待处理事项。 |
| `order_overview.latest_confirmed_at` | 当前订单 V4 卡片最近确认 UTC 时间。 |
| `next_v4_action` | 订单详情页下一步处理入口,口径与订单列表 V4 入口一致;前端点击后跳 `/reservation/order-tasks/{order_task_id}`。 |
| `next_v4_action.action_type` | `CONFIRM` / `REVIEW` / `NONE`。订单详情页不直接调用确认或复核接口,应进入 V4 订单任务详情页处理。 |
| `related_source_messages[]` | 当前订单 V4 订单任务关联来源邮件安全摘要去重列表用于订单页邮件区不包含正文、HTML 或附件 URL。 |
| `v4_order_tasks[]` | V4 订单任务时间线数组。旧 `tasks[]` 继续保留V4 时间线按 `source_received_at``source_message_id``order_context_index``created_at`、数字 ID 正序返回。 |
| `v4_order_tasks[].order_task_id` | V4 订单任务 ID字符串。 |
| `v4_order_tasks[].order_ref` | V4 回调包内订单引用,不等同 PMS 永久订单号。 |
| `v4_order_tasks[].order_task_status` | V4 订单任务状态,当前为 `OPEN` / `COMPLETED`。 |
| `v4_order_tasks[].card_counts` | V4 任务卡数量摘要。 |
| `v4_order_tasks[].cards[]` | V4 订单任务下任务卡安全摘要,只返回状态、复核状态、确认人和确认时间,不返回业务 payload。 |
| `v4_order_tasks[].source_message_summary` | V4 来源邮件安全摘要不包含正文、HTML、附件 URL 或 AI 原始 payload。 |
| `v4_order_tasks[].latest_activity_at` | V4 订单任务自身 `updated_at` 与其下卡片 `updated_at` 的最大 UTC 时间。 |
@@ -410,7 +484,7 @@ POST /api/system/reservation/demo-data
仍建议后端确认:
- `GET /api/reservation/tasks` 是否会在结构化 S10/S99 行中稳定返回 `task_type=SOURCE_MESSAGE_ONLY`,或允许返回 `MESSAGE_NOTIFICATION` 并只依赖 `result_type/route_code/system_process_category`;前端当前两种都兼容
- `GET /api/reservation/tasks` 中旧 S000/S999 和 V3 S10/S99 兼容行是否稳定返回 `task_type=SOURCE_MESSAGE_ONLY`。V4 S10/S99 已不应从旧任务列表返回,应通过 V4 工作台和来源通知接口展示
- `adapter_contract_error` / `unhandled_current_intent` 如果未来也作为独立列表行返回,请保持 `source_message_id` 可用,便于前端继续提供邮件会话入口。
- 同卡人工复核解阻成功后是否一定返回 `opera_operations[]`。当前文档写“两条 OPERA 模拟操作”,前端实现按实际返回刷新,不假设固定数量。
- Parent Group `manual_review.reason_code=target_object_unclear` 场景需要任务详情稳定透出 `context_used.parent_identity_candidates[]`。当前 `ReservationTaskDetailResult` 后端 DTO 仅透出 `manual_review`,前端已兼容顶层 `context_used.parent_identity_candidates[]``manual_review.context_used.parent_identity_candidates[]`,但若后端不透出 candidates页面只能显示空态提示。
@@ -713,11 +787,11 @@ Content-Type: application/json
- 如果后端已有更细的字段来源或适用场景元数据,可后续再扩展 `field_source``applicable_scenario`,不作为本轮 P0 阻塞项。
- 前端默认不需要为 `GET /api/reservation/orders``GET /api/reservation/tasks``GET /api/reservation/orders/{orderId}` 自动拼 `hotel_id`;如已接入酒店选择器,可以传当前选中酒店,后端会校验访问权限。当前 `GET /api/reservation/tasks/{taskId}` 以及任务写操作 Controller 不接收 `hotel_id`;第一版先按 ID 定位,后续多酒店隔离 / 权限方案统一补齐。
## 9. S10/S99 源邮件只读通知与历史兼容
## 9. S10/S99 源邮件只读通知与历史兼容
当前状态:后端不提供独立 Message Notification 列表 / 详情接口。旧 SuperAgent 入口返回 `S000,source_message_id``S999,source_message_id` 时,后端会创建 `SOURCE_MESSAGE_ONLY` 只读特殊任务;0711 P0 结构化 `S10/S99` 也复用同一只读任务模型。前端已通过 `GET /api/reservation/tasks?task_type=SOURCE_MESSAGE_ONLY``GET /api/reservation/tasks/{taskId}` 展示;任务列表筛选只保留 `SOURCE_MESSAGE_ONLY``S10``S99`,不再提供 `S000``S999` 历史筛选项
当前状态:后端不提供历史候选的 `/api/reservation/message-notifications` 列表 / 详情接口。旧 SuperAgent 入口返回 `S000,source_message_id``S999,source_message_id` 时,后端会创建 `SOURCE_MESSAGE_ONLY` 只读特殊任务;V3 结构化 `S10/S99` 兼容路径也沿用该旧任务模型。V4 `route_code=S10/S99` 新数据已改为独立来源通知模型,通过 V4 工作台和 `/api/reservation/source-notifications/{notificationId}` 展示,并通过 `/api/reservation/source-notifications/{notificationId}/ack` 确认已读 / 已处理
0711 P0 新入口已迁移为结构化 `S10/S99`
V3 入口兼容语义
- `S10``result_type=source_message_review_notification``route_code=S10`,表示未匹配当前支持的业务事件。
- `S99``result_type=source_message_review_notification``route_code=S99`,表示输入不足或无法形成业务素材包。
@@ -729,12 +803,13 @@ Content-Type: application/json
展示规则:
-`task_type=SOURCE_MESSAGE_ONLY``task_subtype=S000/S999`:按只读源邮件通知卡展示。
- `route_code=S10/S99`:按只读源邮件通知卡展示。
- 任务列表可见,订单列表不可见
- 任务详情只允许查看来源邮件、会话、附件和 SuperAgent 原始返回
- V3 `route_code=S10/S99` 兼容数据:按只读源邮件通知卡展示。
- V4 `route_code=S10/S99` 新数据:按来源通知详情展示,可 ack不通过旧任务详情处理
- `SOURCE_MESSAGE_ONLY` 任务列表可见订单列表不可见V4 来源通知在 V4 工作台可见,订单列表和订单详情不可见
- 旧任务详情只允许查看来源邮件、会话、附件和 SuperAgent 原始返回V4 来源通知详情只允许查看来源邮件摘要、通知信息和 ack 状态。
- 任务详情通过 `source_message_only_result` 返回 `entry_result_code``entry_result_meaning``entry_result_description``entry_result_source_message_id``result_type``route_code``agent_assessment``notification`、S99 的入口 `manual_review``raw_answer`
- 不显示编辑、确认、人工转换、执行 OPERA 或重试 OPERA 按钮。
- 不参与订单任务执行顺序阻塞。
- 不显示编辑、人工转换、执行 OPERA 或重试 OPERA 按钮V4 只显示 ack
- 不参与订单任务执行顺序阻塞,也不创建隐藏技术订单
当前不建议新增路径:
@@ -1112,9 +1187,9 @@ POST /api/reservation/tasks/{taskId}/order-binding
- 订单列表、任务列表当前统一使用 `items + page` 分页结构;邮件会话详情不分页,返回同一会话全部邮件。
- 邮件会话详情接口已优先使用 `GET /api/source-messages/{sourceMessageId}/conversation`
- 任务详情 `fields[]` 已由后端直接透出 P0 需要的 3.0 元数据;独立字段白名单接口后置。
- 独立 Message Notification 页面继续后置;旧 S000/S999 和 S10/S99 都先在任务列表和任务详情展示
- 独立 Message Notification 页面继续后置;旧 S000/S999 和 V3 S10/S99 继续在旧任务列表和任务详情兼容展示V4 S10/S99 走 V4 工作台和来源通知详情
- 邮件会话全文读取的审计策略由后端内部处理;前端不保存原文读取 key。
- `GET /api/reservation/tasks` 结构化 S10/S99 行的 `task_type` 返回值请后端最终确认:前端已兼容 `SOURCE_MESSAGE_ONLY``MESSAGE_NOTIFICATION`,但文档口径最好稳定一个
- `GET /api/reservation/tasks` 中旧 S000/S999 和 V3 S10/S99 兼容行的 `task_type` 返回值请后端最终确认V4 S10/S99 不应再进入旧任务列表
- `manual-review-resolutions` 成功响应中的 `opera_operations[]` 数量请后端最终确认;前端不写死两条,只按返回内容刷新展示。
- 系统管理菜单树增强接口已完成:`GET /api/admin/menus/tree``PUT /api/admin/menus/tree-order`
- Manual Invoice 第一阶段的客户 / 联系人目录来源、模板初始文件、VAT 配置和生成记录是否必须落库,已在 M009 中列为开发前确认项。

View File

@@ -497,7 +497,7 @@ AGENTBUS_TEST_SUPERAGENT_DISPATCH_INCLUDE_BOOKING_EXCEL_EXTRACTIONS=true
RESERVATION_BOOKING_EXCEL_EXTRACTION_ENABLED=true
```
中文说明:`agentbus.superagent-dispatch.include-booking-excel-extractions` 只控制 AgentBus worker 是否把抽取结果追加给 SuperAgent`reservation.booking-excel-extraction.enabled` 是解析服务总开关。两者必须同时开启才会产生有效高亮行结果,生产环境默认关闭
中文说明:`agentbus.superagent-dispatch.include-booking-excel-extractions` 只控制 AgentBus worker 是否把抽取结果追加给 SuperAgent`reservation.booking-excel-extraction.enabled` 是解析服务总开关。两者必须同时开启才会产生有效高亮行结果。当前测试机已开启该增强,生产环境默认关闭,生产开启需单独确认
当前表名为 `platform_superagent_dispatch_run`,详细字段、状态流转、错误分类和验收标准见
`docs/project/requirements/M007-agentbus-superagent-auto-dispatch-v1.md`

View File

@@ -4,9 +4,9 @@
| 项目 | 内容 |
| --- | --- |
| 文档版本 | 0.9 |
| 日期 | 2026-07-18 |
| 状态 | 当前代码契约已支持 V4 入站解析基线、V2 `ai_task_results[]` 兼容、结构化 S10/S99、V3 业务根基础解析、旧 S000/S999 兼容和单酒店 hotel_id 后端解析 |
| 文档版本 | 0.10 |
| 日期 | 2026-07-20 |
| 状态 | 当前代码契约已支持 V4 订单任务 + 多卡入站、V4 S10/S99 来源通知、V2 `ai_task_results[]` 兼容、V3 业务根兼容、旧 S000/S999 兼容、M011 Booking Excel 调 SuperAgent 前预处理增强和单酒店 hotel_id 后端解析 |
| 适用范围 | SuperAgent 调用本系统查询上下文、查询邮件会话、提交 AI 任务结果 |
| 主要读者 | SuperAgent 对接方、后端、测试、运维 |
@@ -105,7 +105,7 @@ SuperAgent 不应知道或依赖内部 SourceMessage Inbox ID也不需要为
查询接口 3、4 面向已经入库的邮件会话:缺省酒店由后端解析,`external_conversation_id` 最终仍按 `hotel_id + source_provider + source_channel + external_conversation_id` 查询;`source_message_id` 表示外部来源消息 ID可作为锚点反查该邮件所属会话。
## 3.1 M002 V3 迁移提醒
## 3.1 M002 V3 / V4 迁移提醒
2026-07-11 起,项目需求基线已确认采用 `docs/project/requirements/M002-order-task-workflow-v3.md`2026-07-12 起Parent Group / Allotment 路由采用 `docs/project/requirements/M002-v3-p0.1-parent-group-routing-update.md`
@@ -116,16 +116,39 @@ SuperAgent 不应知道或依赖内部 SourceMessage Inbox ID也不需要为
- 完整 Parent split 的父事件必须使用 `Cancel Allotment + cancel_allotment_control_block``relationship_type=linked_parent_release_after_child_split` 只用于关联和 Preflight不再作为独立任务 subtype。
- 当前新入站不接受 `Cancel Booking + linked_parent_release_after_child_split` 作为合法业务任务;该组合仅允许历史数据只读兼容。
当前后端已完成 M002 V3 CP1-CP6
2026-07-18 起M002 V4 以 `docs/project/requirements/M002-v4-agent-callback-field-contract.md``docs/project/requirements/M002-v4-order-task-card-domain-model-cp2.md` 为当前有效业务入站契约
- V4 普通业务包使用 `route_code=null``source_message``order_contexts[]``message_events[]`
- `source_message.source_message_id` 是外部来源消息 ID对应 SourceMessage Inbox 的 `external_message_id`,不是本系统内部数据库 ID。
- 普通业务按 `source_message + order_ref` 创建 V4 订单任务并在订单任务下创建来源消息只读卡、Basic Information 卡和业务事件卡。
- Basic Information 必须先确认业务卡逐卡确认或复核解阻确认后永久锁定V4 第一版不提供前端草稿。
- `route_code=S10/S99` 使用 V4 来源通知模型,不创建隐藏技术订单,不进入订单详情时间线,不阻塞普通订单;前端只展示和 ack。
- Account / Market / Source、Room Type、Rate Code 以本系统数据库目录稳定代码为准SuperAgent 不应输出显示文案作为业务判断依据。
当前后端仍兼容 M002 V3 CP1-CP6
- 已建立 40 条 P0.1 路由枚举 / 稳定配置;`route_code` 保持历史稳定,不按总数连续重编号,`R41/R42` 仍可能出现在响应和历史 transition 中。
- 已支持结构化 `S10/S99` 入站,创建只读 `SOURCE_MESSAGE_ONLY` 任务
- 已支持结构化 `S10/S99` 历史入站兼容;当前 V4 新入站使用来源通知模型
- 已支持 V3 业务根 `source_message + message_events[]` 的基础解析;可派生到现有任务模型的 event 会创建业务任务,无法派生、显式契约错误或基础 manual_review / parent split 结构不完整的 event 只落 `adapter_contract_error` transition不创建业务任务。
- 已支持 `unhandled_current_intents[]` 最小落库:只写 `UNHANDLED_CURRENT_INTENT` transition不创建业务任务也不按 adapter 契约错误返回。
- 已在 `workflow_reservation_ai_transition` 保存 `route_code``system_process_category``adapter_error_code``adapter_error_message`
- 已支持 type-known manual review 同卡解阻、当前订单归属确认、P0 fixtures 回归测试和 V3 typed `infrastructure_input_error` 响应。
尚未完成:真实 OPERA / OHIP、普通任务切换订单、字段矩阵从当前扁平结构整体迁移到 0711 P0 新结构、历史旧 Parent Cancel Booking payload 批量迁移。
尚未完成:真实 OPERA / OHIP、普通任务切换订单、历史旧 Parent Cancel Booking payload 批量迁移、真实 PMS / OPERA / OHIP 目录同步和 SuperAgent 目录机器接口
## 3.2 M011 Booking Excel 预处理输入增强
M011 已在 Debug EML 和 AgentBus 自动分发链路中接入 Booking Excel 附件预处理。该能力发生在 TH Hotel 后端调用 SuperAgent Open API 前,不属于 SuperAgent 调本系统的 `task-results` 请求体字段,但会影响 SuperAgent 实际看到的邮件 payload。
处理边界:
- 后端只处理邮件附件中的 `.xls` / `.xlsx`,识别并排除 `PASSENGER_ROSTER` 人员名单类 Excel。
-`BOOKING_UPDATE``BOOKING_SURCHARGE` 类 Excel按最近 6 个月候选窗口选择文件内实际存在的最新 3 个业务月,并抽取有背景色标记的业务行。
- 非空结果追加到 AgentBus Outlook-like payload 的 `attachment_extractions[]` 字段,供 SuperAgent 作为证据输入。
- `attachment_extractions[]` 只增强 SuperAgent 判断上下文,不直接创建订单、订单任务、任务卡、客户回复或 OPERA / OHIP 操作。
- 测试机 AgentBus 增强已开启;生产 AgentBus 增强默认关闭。只有 `reservation.booking-excel-extraction.enabled` 和 AgentBus dispatch include 开关同时开启时才会追加该字段。
字段契约、抽取规则和安全边界以 `docs/project/requirements/M011-booking-excel-pre-superagent-enrichment-v1.md` 为准。SuperAgent 生成最终 V4 结果时,仍必须按本文第 8 节的 `source_message + order_contexts[] + message_events[]` 契约回调本系统。
## 4. 接口 1查询订单上下文
@@ -564,14 +587,15 @@ V4 字段说明:
当前已支持的 V4 行为:
- 命中 SourceMessage 后保存 AI batch / transition并按 `message_events[]` 顺序处理。
- 可映射 event 先复用现有订单 / 任务 / 任务卡链路,`ai_payload_json` 会保留 `v4_source_message``v4_order_context``v4_message_event`
- `route_code=S10/S99` 创建只读 `SOURCE_MESSAGE_ONLY` 任务,任务列表可见,订单列表不可见
- V4 包级结构错误如果仍能通过 `source_message.source_message_id` 定位 SourceMessage会返回成功接收并写入 `adapter_contract_error` transition不创建订单、任务或用户可处理卡。`source_message_id` 缺失或找不到 SourceMessage 时仍返回明确错误
- `PAYMENT.attachment_ids[]` 引用不存在的附件、`UPDATE_BOOKING` 携带 `rate_code`、以及其他 V4 event 契约错误,只写 `adapter_contract_error` transition不创建用户可处理业务任务
- 命中 SourceMessage 后保存 AI batch / transition并按 `source_message + order_contexts[] + message_events[]` 处理。
- 普通业务包按 `source_message + order_ref` 创建 V4 订单任务,并创建 `SOURCE_MESSAGE_DISPLAY``BASIC_INFORMATION` 和业务事件卡
- Basic Information 必须先确认业务卡逐卡确认或复核解阻确认后永久锁定V4 第一版不提供前端草稿
- `route_code=S10/S99` 创建 V4 来源通知,工作台可见,订单列表和订单详情不可见;来源通知只能 ack不创建订单、不阻塞订单
- V4 包级结构错误如果仍能通过 `source_message.source_message_id` 定位 SourceMessage会返回成功接收并写入 `adapter_contract_error` transition不创建订单任务、任务卡或来源通知。`source_message_id` 缺失或找不到 SourceMessage 时仍返回明确错误
- `PAYMENT.attachment_ids[]` 引用不存在的附件、`UPDATE_BOOKING` 携带不允许字段、目录代码无法匹配当前酒店数据库目录,以及其他 V4 event 契约错误,只写 `adapter_contract_error` transition不创建用户可处理业务任务。
- 技术契约错误不会自动转为 S10/S99也不会创建前端可处理业务任务。
当前 V4 入站仍未完成完整多卡模型Basic Information 独立卡、V4 页面模型、真实 OPERA / OHIP、普通任务切换订单均后置
当前仍未完成:真实 OPERA / OHIP、真实 PMS / OPERA / OHIP 目录同步、普通任务切换订单、SuperAgent 目录机器接口和前端订单详情 V4 化
### 8.3 V3 S10/S99 结构化请求体
@@ -682,7 +706,7 @@ V3 字段说明:
- MCP 路径缺失 `source_message.source_message_id` 或整个 `source_message` 时,保留业务入站层 `MISSING_SOURCE_MESSAGE_ID` 错误语义。
- 40 条 P0.1 路由进入后端枚举 / 稳定配置。
- `route_code` 是稳定代码,不因路由总数从 42 调整为 40 而重编号;联调方不要按数字连续性判断合法性。
- 结构化 `S10/S99` 创建只读 `SOURCE_MESSAGE_ONLY` 任务,任务列表可见,订单列表不可见。
- V3 结构化 `S10/S99` 兼容路径创建只读 `SOURCE_MESSAGE_ONLY` 任务,任务列表可见,订单列表不可见V4 新入站不走该模型,改用来源通知
- 业务 event 能派生到稳定路由时,复用现有订单 / 任务 / 任务卡创建链路。
- 完整 Parent split 父事件必须提交为 `event_type=Cancel Allotment``extracted_fields.cancel_scope=entire_allotment_control_block``task_subtype=cancel_allotment_control_block`,并保留 `relationship_type=linked_parent_release_after_child_split` 作为关系字段。
- 当前新入站若提交 `event_type=Cancel Booking``relationship_type=linked_parent_release_after_child_split`,写入 `adapter_contract_error` transition不创建业务任务旧 V2 兼容 `ai_task_results[]` 中的同等三元组按请求级 `ADAPTER_CONTRACT_ERROR` 拒绝。
@@ -759,7 +783,7 @@ V3 字段说明:
正式联调时SuperAgent 不需要传 `hotel_id`。后端通过系统酒店和外部 `source_message_id` 查找唯一 `platform_source_message_inbox.external_message_id`,真实 provider/channel 以 Inbox 入库值为准。如果没有找到,返回 `SOURCE_MESSAGE_NOT_FOUND`;如果同一系统酒店下匹配到多条,返回 `SOURCE_MESSAGE_AMBIGUOUS`。本地旧夹具允许在缺少 `hotel_id` 时使用内部数字 SourceMessage ID但该兼容路径不作为 SuperAgent 正式契约。
`informational_message` 结构化任务仅用于历史兼容。新数据如果是纯信息类邮件或无法形成业务素材包,应优先使用 V3 结构化 `S10/S99`;旧联调或兼容场景仍可使用下面的 `S000/S999` 文本请求体。
`informational_message` 结构化任务仅用于历史兼容。新数据如果是纯信息类邮件或无法形成业务素材包,应优先使用 V4 `S10/S99` 来源通知;V3 结构化 `S10/S99`下面的 `S000/S999` 文本请求体仅作为旧联调或兼容路径
### 8.6 S000/S999 文本请求体
@@ -783,7 +807,7 @@ S999,mail-20260708-0001
| `S999` | 入口阶段无法形成业务素材包,不需要进入业务执行。 |
| `mail-20260708-0001` | 外部来源消息 ID对应 SourceMessage Inbox 的 `external_message_id`。 |
S000/S999 不在 body 里传 `hotel_id`,后端使用平台酒店表唯一 `ACTIVE` 酒店和外部消息 ID 查询 SourceMessage Inbox。命中后创建 `SOURCE_MESSAGE_ONLY` 只读特殊任务:任务列表可见,订单列表不可见,不允许编辑、确认、转换订单、执行 OPERA 或重试 OPERA也不参与同订单任务执行顺序阻塞。该文本格式仅为兼容路径新数据优先使用结构化 `S10/S99`
S000/S999 不在 body 里传 `hotel_id`,后端使用平台酒店表唯一 `ACTIVE` 酒店和外部消息 ID 查询 SourceMessage Inbox。命中后创建 `SOURCE_MESSAGE_ONLY` 只读特殊任务:任务列表可见,订单列表不可见,不允许编辑、确认、转换订单、执行 OPERA 或重试 OPERA也不参与同订单任务执行顺序阻塞。该文本格式仅为兼容路径新数据优先使用 V4 `S10/S99` 来源通知
### 8.7 成功响应

View File

@@ -598,7 +598,16 @@ GET /api/reservation/orders/{orderId}
权限RESERVATION_ORDER_READ
```
用于订单详情页展示 V4 订单任务时间线,同时保留旧 `tasks[]`新增字段为 `v4_order_tasks[]``include_tasks=false``tasks[]``v4_order_tasks[]` 都返回空数组
用于订单详情页展示 V4 订单总览和 V4 订单任务时间线,同时保留旧 `tasks[]`M002 V4 CP15.1 已补齐 `order_overview``next_v4_action``related_source_messages[]` `v4_order_tasks[].cards[]``include_tasks=false``tasks[]``v4_order_tasks[]` `related_source_messages[]` 都返回空数组,`order_overview` 为空快照,`next_v4_action.action_type=NONE`
`order_overview` 只从已确认 V4 卡片派生:
- Basic Information 已确认后,返回 `account_code``account_name``market_code``source_code`
- Room Information 已确认后,返回 `arrival_date``departure_date``rate_code``room_items[]`
- Trace / Rooming List / Payment 返回对应最新卡片状态,方便订单详情展示待处理事项;
- 未确认 AI 建议不得进入 `order_overview`,避免把未处理内容展示成订单事实。
`next_v4_action` 使用和订单列表一致的下一步处理口径Basic Information 优先;业务卡中 `REVIEW_REQUIRED` 优先于 `PENDING_CONFIRM`;无待处理卡时 `action_type=NONE`。订单详情页应使用该字段跳转 V4 订单任务详情页,不在订单详情页直接确认或复核。
`v4_order_tasks[]` 每项返回:
@@ -606,6 +615,7 @@ GET /api/reservation/orders/{orderId}
- `order_ref`
- `order_task_status`
- `card_counts`
- `cards[]`
- `source_message_summary`
- `source_received_at`
- `created_at`
@@ -617,6 +627,7 @@ GET /api/reservation/orders/{orderId}
- 排序沿用 V4 Repository 的同订单顺序:`source_received_at``source_message_id``order_context_index``created_at`、数字 ID 正序。
- `source_message_summary` 只返回安全摘要不返回邮件正文、HTML、附件 URL 或 AI 原始 payload原文仍走 SourceMessage 会话接口。
- `latest_activity_at` 为 V4 订单任务自身 `updated_at` 与其下卡片 `updated_at` 的最大值。
- `cards[]` 只返回卡片安全摘要,不返回业务字段 payload订单详情页如需处理字段必须跳转 V4 订单任务详情接口。
- 接口权限仍使用 `RESERVATION_ORDER_READ`并按订单实际所属酒店校验访问权V4 任务读取时继续以该订单酒店过滤,避免跨酒店脏数据泄露。
### 12.6 订单列表 V4 继续处理入口
@@ -802,6 +813,7 @@ AI 原始 payload、邮件正文、附件 URL 和技术 trace 不应直接进入
| M002-V4-CP13 | 目录管理后台 V1 | Account / Market / Source 管理,临时 Room Type / Rate Code 管理,目录维护权限和管理审计 |
| M002-V4-CP14 | 订单列表 V4 继续处理入口 | 已完成:`GET /api/reservation/orders` 返回 V4 下一步订单任务、卡片、动作类型、动作状态和 open 数,前端可优先跳 V4 订单任务详情 |
| M002-V4-CP15 | V4 业务审计查询 | 已完成:`GET /api/reservation/order-tasks/{orderTaskId}/audits``GET /api/reservation/source-notifications/{notificationId}/audits` 返回卡片确认、复核解阻和来源通知 ack 的脱敏审计流水 |
| M002-V4-CP15.1 | 订单详情 V4 化后端补齐 | 已完成:`GET /api/reservation/orders/{orderId}` 返回 `order_overview``next_v4_action``related_source_messages[]``v4_order_tasks[].cards[]`,支撑订单总览页 |
| M002-V4-CP16 | PMS / OPERA / OHIP 目录同步 | 同步 Adapter、同步 run、最后成功快照、失败重试和同步状态管理入口 |
## 17. 已确认设计决策

View File

@@ -2,7 +2,7 @@
| 项目 | 内容 |
| --- | --- |
| 文档状态 | 当前有效CP1 / CP2 / CP3 已实现,生产链路默认关闭 |
| 文档状态 | 当前有效CP1 / CP2 / CP3 已实现,测试机 AgentBus 增强已开启生产链路默认关闭CP4 暂不推进 |
| 适用范围 | AgentBus 入站邮件和 Debug EML 上传邮件中的 Excel 附件,在调用 SuperAgent Open API 前做结构化预处理 |
| 当前目标 | 排除旅行名单类 Excel抽取 Booking / 附加费类 Excel 中带背景色的业务行,生成安全 JSON 并附加到发给 SuperAgent 的 payload |
| 依赖能力 | SourceMessage Inbox、Debug EML、AgentBus 自动分发、Apache POI、OSS 附件读取能力、SuperAgent Open API |
@@ -19,7 +19,7 @@
## 2. 目标
- 在 AgentBus 自动分发和 Debug EML 上传两条链路中复用同一套 Excel 附件预处理逻辑;当前两条链路均已接入,生产链路仍由配置默认关闭。
- 在 AgentBus 自动分发和 Debug EML 上传两条链路中复用同一套 Excel 附件预处理逻辑;当前两条链路均已接入,测试机 AgentBus 增强已开启,生产链路仍由配置默认关闭。
- 识别并排除旅行名单类 Excel避免把人员名单误当成 Booking 更新或附加费数据。
- 对 Booking Update / Booking Surcharge 类 Excel按最近月份筛选 sheet只抽取有业务背景色的行。
- 把抽取结果追加到发给 SuperAgent 的 AgentBus Outlook-like payload 中,字段建议为 `attachment_extractions[]`
@@ -352,7 +352,7 @@ server/src/main/java/cn/nianxx/thhotel/workflows/reservation/excelimport
| `agentbus.superagent-dispatch.booking-excel-download-max-size` | `10MB` | AgentBus 自动分发读取单个 Excel 附件的最大大小,避免 worker 下载异常大文件 |
| `debug.eml-upload.include-booking-excel-extractions` | `false` | Debug EML 是否返回并传递抽取结果 |
中文说明:建议先在 Debug EML 打开,验证样例 Excel SuperAgent 结果稳定后,再在测试机打开 AgentBus 自动分发增强;生产仍保持默认关闭
中文说明Debug EML 和测试机 AgentBus 自动分发增强已可用于验证样例 Excel SuperAgent 结果稳定性;生产仍保持默认关闭,生产开启需要单独确认
## 11. 错误处理和安全边界
@@ -403,12 +403,12 @@ server/src/main/java/cn/nianxx/thhotel/workflows/reservation/excelimport
- AgentBus dispatch worker 在调用 SuperAgent 前读取 SourceMessage 已保存附件引用,并通过后端对象存储端口下载 Excel 内容。
- 复用 `ReservationBookingExcelAttachmentExtractionService` 执行同一预处理,非空结果追加到发给 SuperAgent 的 AgentBus Outlook-like payload。
- 生产链路默认配置关闭,测试机可通过 `agentbus.superagent-dispatch.include-booking-excel-extractions=true` 验证后再评估开启
- 测试机 AgentBus 增强已开启;生产链路默认配置关闭,生产开启需单独确认
- 附件读取或解析失败时追加安全跳过结果 / warning 并继续调用 SuperAgent日志和 payload 不写完整附件 URL、签名参数、API Key、Cookie 或 Secret。
### CP4可选持久化和运营查询
### CP4可选持久化和运营查询(暂不推进)
如果后续需要追踪 Excel 解析历史,再单独设计持久化表,例如:
当前 CP4 不进入近期开发计划。如果后续需要追踪 Excel 解析历史,再单独设计持久化表,例如:
```text
workflow_reservation_booking_excel_import_batch

View File

@@ -45,7 +45,7 @@
| --- | --- | --- | --- | --- |
| `GET /api/reservation/tasks` | `FRONTEND_USER` | 已强制 Bearer 登录 + `RESERVATION_TASK_READ`;支持可选 `hotel_id` 并校验酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 酒店访问权 | 只读查询默认不写业务审计 |
| `GET /api/reservation/orders` | `FRONTEND_USER` | 已强制 Bearer 登录 + `RESERVATION_ORDER_READ`;支持可选 `hotel_id` 并校验酒店访问权;已返回 V4 继续处理入口和统一 open count 安全字段 | 保持登录 + `RESERVATION_ORDER_READ` + 酒店访问权V4 入口字段只返回 order task / card ID、动作类型、状态和数量摘要`open_work_item_count` 仅返回当前订单待处理数量摘要 | 只读查询默认不写业务审计;不得返回邮件正文、附件 URL、AI 原始 payload 或来源通知原始 payload |
| `GET /api/reservation/orders/{orderId}` | `FRONTEND_USER` | 已强制 Bearer 登录 + `RESERVATION_ORDER_READ`;按订单实际所属酒店校验访问权;已返回旧 `tasks[]`V4 `v4_order_tasks[]` 安全摘要时间线 | 保持登录 + `RESERVATION_ORDER_READ` + 订单所属酒店访问权V4 时间线读取按订单酒店过滤 | 只读查询默认不写业务审计;不得在 `v4_order_tasks[]` 返回邮件正文、附件 URL、AI 原始 payload 或来源通知原始 payload |
| `GET /api/reservation/orders/{orderId}` | `FRONTEND_USER` | 已强制 Bearer 登录 + `RESERVATION_ORDER_READ`;按订单实际所属酒店校验访问权;已返回旧 `tasks[]`V4 `order_overview``next_v4_action``related_source_messages[]``v4_order_tasks[]` 安全摘要时间线 | 保持登录 + `RESERVATION_ORDER_READ` + 订单所属酒店访问权V4 时间线读取按订单酒店过滤`order_overview` 只能从已确认 V4 卡片派生,`next_v4_action` 只返回下一步处理 ID / 动作 / 状态 / 数量摘要,`related_source_messages[]` 只返回来源邮件安全摘要 | 只读查询默认不写业务审计;不得在 `order_overview``v4_order_tasks[].cards[]` 返回未确认 AI 建议、邮件正文、附件 URL、AI 原始 payload、display payload、confirmed payload 或来源通知原始 payload |
| `GET /api/reservation/tasks/{taskId}` | `FRONTEND_USER` | 已强制 Bearer 登录 + `RESERVATION_TASK_READ`;按任务实际所属酒店校验访问权 | 保持登录 + `RESERVATION_TASK_READ` + 任务所属酒店访问权 | 只读查询默认不写业务审计 |
| `GET /api/reservation/workbench-items` | `FRONTEND_USER` | 已实现 M002 V4 CP5强制 Bearer 登录 + `RESERVATION_TASK_READ` + 酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 酒店访问权;统一返回 V4 业务订单任务和 S10/S99 来源通知摘要 | 只读查询默认不写业务审计;不得返回邮件正文、附件 URL、AI 原始 payload 或来源通知原始 payload同来源时间下使用 `updated_at` / `created_at` / 数字 ID 稳定排序 |
| `GET /api/reservation/order-tasks` | `FRONTEND_USER` | 已实现 M002 V4 CP5强制 Bearer 登录 + `RESERVATION_TASK_READ` + 酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 酒店访问权;只返回 V4 业务订单任务,不返回 S10/S99 来源通知 | 只读查询默认不写业务审计;不得返回 AI 原始 payload`card_status` 只匹配业务 / 可处理卡,固定来源邮件展示卡不参与筛选 |
@@ -121,7 +121,7 @@
| SuperAgent Open API Client | `INTERNAL_ONLY` | 只能后端 Adapter 使用Secret 不出后端 | 通过 debug run 或 dispatch run 追踪 |
| OSS Adapter | `INTERNAL_ONLY` | 前端只能拿后端返回的安全 URL不能拿 OSS Secret | 上传和读取入口记录安全摘要 |
| AgentBus dispatch worker | `INTERNAL_ONLY` | 只由后端调度或受控管理入口触发 | `platform_superagent_dispatch_run` |
| Booking Excel 附件预处理 | `INTERNAL_ONLY` | M011 CP1 / CP2 / CP3 已接入 Debug EML 与 AgentBus dispatch只允许后端在调用 SuperAgent 前通过 Service / Port 使用,不单独暴露给前端或第三方 | 记录安全 warning、附件名、hash 前缀、sheet 名、行号和高亮业务行摘要;不得记录完整 Excel、完整附件 URL、签名参数、API Key、Cookie、Secret 或名单类客户敏感原文 |
| Booking Excel 附件预处理 | `INTERNAL_ONLY` | M011 CP1 / CP2 / CP3 已接入 Debug EML 与 AgentBus dispatch,测试机 AgentBus 增强已开启;只允许后端在调用 SuperAgent 前通过 Service / Port 使用,不单独暴露给前端或第三方 | 记录安全 warning、附件名、hash 前缀、sheet 名、行号和高亮业务行摘要;不得记录完整 Excel、完整附件 URL、签名参数、API Key、Cookie、Secret 或名单类客户敏感原文 |
| Flyway / bootstrap 初始化 | `INTERNAL_ONLY` | 不提供运行时外部接口 | 通过部署记录和数据库 history 追踪 |
| 未来 OPERA / OHIP Adapter | `INTERNAL_ONLY` | 浏览器不得直接调用;只能业务服务触发 | 必须记录操作、attempt 和外部结果摘要 |

View File

@@ -7,12 +7,21 @@ import java.util.List;
* 前端订单详情响应包含订单摘要、旧任务时间线、V4 订单任务时间线和非阻塞警告。
*
* @param order 订单摘要
* @param orderOverview V4 当前订单快照摘要
* @param nextV4Action V4 下一步处理入口
* @param relatedSourceMessages 订单关联来源邮件摘要
* @param tasks 同订单旧任务时间线
* @param v4OrderTasks 同订单 V4 订单任务时间线
* @param warnings 当前无法提供的扩展信息或非阻塞提醒
*/
public record ReservationOrderDetailResult(
ReservationOrderSummaryResult order,
@JsonProperty("order_overview")
ReservationOrderV4OverviewResult orderOverview,
@JsonProperty("next_v4_action")
ReservationOrderV4NextActionResult nextV4Action,
@JsonProperty("related_source_messages")
List<ReservationV4SourceMessageSummaryResult> relatedSourceMessages,
List<ReservationOrderTaskTimelineItemResult> tasks,
@JsonProperty("v4_order_tasks")
List<ReservationOrderV4TaskTimelineItemResult> v4OrderTasks,

View File

@@ -0,0 +1,26 @@
package cn.nianxx.thhotel.workflows.reservation.common.result;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* 订单详情页 V4 下一步处理入口摘要。前端用它跳转订单任务详情,不在订单详情页直接处理卡片。
*
* @param orderTaskId 下一步 V4 订单任务 ID
* @param cardId 下一步待处理任务卡 ID
* @param actionType 下一步动作类型CONFIRM / REVIEW / NONE
* @param actionStatus 下一步卡片状态PENDING_CONFIRM / REVIEW_REQUIRED
* @param openOrderTaskCount 当前订单下未完成 V4 订单任务数量
*/
public record ReservationOrderV4NextActionResult(
@JsonProperty("order_task_id")
String orderTaskId,
@JsonProperty("card_id")
String cardId,
@JsonProperty("action_type")
String actionType,
@JsonProperty("action_status")
String actionStatus,
@JsonProperty("open_order_task_count")
Integer openOrderTaskCount
) {
}

View File

@@ -0,0 +1,49 @@
package cn.nianxx.thhotel.workflows.reservation.common.result;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.OffsetDateTime;
import java.util.List;
/**
* 订单详情页 V4 当前快照摘要。只从已确认 V4 卡片派生,不把未确认 AI 建议当作订单事实。
*
* @param accountCode Account 目录 code
* @param accountName Account 展示名称
* @param marketCode Market 目录 code
* @param sourceCode Source 目录 code
* @param arrivalDate 入住酒店本地日期
* @param departureDate 离店酒店本地日期
* @param rateCode Rate Code
* @param roomItems 已确认房型房量摘要
* @param traceCardStatus Trace 卡片当前状态
* @param roomingListCardStatus Rooming List 卡片当前状态
* @param paymentCardStatus Payment 卡片当前状态
* @param latestConfirmedAt 最近一次 V4 卡片确认 UTC 时间
*/
public record ReservationOrderV4OverviewResult(
@JsonProperty("account_code")
String accountCode,
@JsonProperty("account_name")
String accountName,
@JsonProperty("market_code")
String marketCode,
@JsonProperty("source_code")
String sourceCode,
@JsonProperty("arrival_date")
String arrivalDate,
@JsonProperty("departure_date")
String departureDate,
@JsonProperty("rate_code")
String rateCode,
@JsonProperty("room_items")
List<ReservationOrderV4RoomSummaryResult> roomItems,
@JsonProperty("trace_card_status")
String traceCardStatus,
@JsonProperty("rooming_list_card_status")
String roomingListCardStatus,
@JsonProperty("payment_card_status")
String paymentCardStatus,
@JsonProperty("latest_confirmed_at")
OffsetDateTime latestConfirmedAt
) {
}

View File

@@ -0,0 +1,17 @@
package cn.nianxx.thhotel.workflows.reservation.common.result;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* 订单详情页 V4 房型房量摘要。
*
* @param roomTypeCode 房型目录 code
* @param roomCount 房量
*/
public record ReservationOrderV4RoomSummaryResult(
@JsonProperty("room_type_code")
String roomTypeCode,
@JsonProperty("room_count")
Integer roomCount
) {
}

View File

@@ -0,0 +1,42 @@
package cn.nianxx.thhotel.workflows.reservation.common.result;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.OffsetDateTime;
/**
* 订单详情页 V4 时间线中的任务卡摘要。只返回状态和确认信息,不返回卡片业务字段。
*
* @param cardId V4 任务卡 ID
* @param cardType 卡片类型
* @param eventType 业务事件类型
* @param sourceEventIndex 来源事件序号
* @param cardSortOrder 卡片排序号
* @param cardStatus 卡片状态
* @param reviewStatus 复核状态
* @param confirmedBy 确认人
* @param confirmedAt 确认 UTC 时间
* @param latestActivityAt 卡片最新活动 UTC 时间
*/
public record ReservationOrderV4TaskCardTimelineItemResult(
@JsonProperty("card_id")
String cardId,
@JsonProperty("card_type")
String cardType,
@JsonProperty("event_type")
String eventType,
@JsonProperty("source_event_index")
Integer sourceEventIndex,
@JsonProperty("card_sort_order")
Integer cardSortOrder,
@JsonProperty("card_status")
String cardStatus,
@JsonProperty("review_status")
String reviewStatus,
@JsonProperty("confirmed_by")
String confirmedBy,
@JsonProperty("confirmed_at")
OffsetDateTime confirmedAt,
@JsonProperty("latest_activity_at")
OffsetDateTime latestActivityAt
) {
}

View File

@@ -2,6 +2,7 @@ package cn.nianxx.thhotel.workflows.reservation.common.result;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.OffsetDateTime;
import java.util.List;
/**
* 订单详情页 V4 订单任务时间线单项。用于旧订单详情页补充展示 V4 多卡任务摘要。
@@ -10,6 +11,7 @@ import java.time.OffsetDateTime;
* @param orderRef V4 回调包内订单引用
* @param orderTaskStatus V4 订单任务状态
* @param cardCounts V4 卡片数量摘要
* @param cards V4 任务卡时间线摘要
* @param sourceMessageSummary 来源邮件安全摘要
* @param sourceReceivedAt 来源邮件接收 UTC 时间
* @param createdAt V4 订单任务创建 UTC 时间
@@ -25,6 +27,7 @@ public record ReservationOrderV4TaskTimelineItemResult(
String orderTaskStatus,
@JsonProperty("card_counts")
ReservationV4CardCountsResult cardCounts,
List<ReservationOrderV4TaskCardTimelineItemResult> cards,
@JsonProperty("source_message_summary")
ReservationV4SourceMessageSummaryResult sourceMessageSummary,
@JsonProperty("source_received_at")

View File

@@ -25,6 +25,10 @@ import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderLis
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderListResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderSummaryResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderTaskTimelineItemResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderV4NextActionResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderV4OverviewResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderV4RoomSummaryResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderV4TaskCardTimelineItemResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderV4TaskTimelineItemResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationPaginationResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskAvailabilityResult;
@@ -35,6 +39,10 @@ import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4Source
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationAiWorkflowRepository;
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4WorkflowRepository;
import cn.nianxx.thhotel.workflows.reservation.service.ReservationFrontendQueryService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.LinkedHashMap;
@@ -64,6 +72,7 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
private final SourceMessageQueryService sourceMessageQueryService;
private final ReservationTaskAvailabilityResolver availabilityResolver;
private final HotelContextService hotelContextService;
private final ObjectMapper objectMapper;
/**
* 注入持久化边界、SourceMessage 安全摘要服务和可处理状态解析器。
@@ -73,12 +82,14 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
ReservationV4WorkflowRepository v4WorkflowRepository,
SourceMessageQueryService sourceMessageQueryService,
ReservationTaskAvailabilityResolver availabilityResolver,
HotelContextService hotelContextService) {
HotelContextService hotelContextService,
ObjectMapper objectMapper) {
this.workflowRepository = workflowRepository;
this.v4WorkflowRepository = v4WorkflowRepository;
this.sourceMessageQueryService = sourceMessageQueryService;
this.availabilityResolver = availabilityResolver;
this.hotelContextService = hotelContextService;
this.objectMapper = objectMapper;
}
/**
@@ -190,10 +201,19 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
availabilityOrReadOnly(task, availabilityByTaskId),
sourceContextsById.get(task.sourceMessageId())))
.toList();
List<ReservationOrderV4TaskTimelineItemResult> v4OrderTasks = Boolean.FALSE.equals(includeTasks)
? List.of()
: findV4OrderTaskTimeline(orderHotelId, order.id());
return new ReservationOrderDetailResult(toOrderSummary(order), tasks, v4OrderTasks, List.of());
V4OrderDetailContext v4Context = Boolean.FALSE.equals(includeTasks)
? V4OrderDetailContext.empty()
: findV4OrderDetailContext(orderHotelId, order.id());
List<ReservationOrderV4TaskTimelineItemResult> v4OrderTasks = toV4TimelineItems(v4Context);
V4OrderListNextAction v4NextAction = toV4OrderListNextAction(v4Context.orderTasks(), v4Context.cardsByOrderTaskId());
return new ReservationOrderDetailResult(
toOrderSummary(order),
toV4OrderOverview(v4Context.orderTasks(), v4Context.cardsByOrderTaskId()),
toV4NextActionResult(v4NextAction),
relatedV4SourceMessages(v4Context),
tasks,
v4OrderTasks,
List.of());
}
/**
@@ -328,12 +348,12 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
/**
* 查询订单详情页 V4 订单任务时间线,复用当前订单酒店作为对象级隔离边界。
*/
private List<ReservationOrderV4TaskTimelineItemResult> findV4OrderTaskTimeline(String hotelId, Long orderId) {
private V4OrderDetailContext findV4OrderDetailContext(String hotelId, Long orderId) {
List<ReservationV4OrderTaskSnapshot> orderTasks = v4WorkflowRepository.findOrderTasksByOrderIds(
hotelId,
List.of(orderId));
if (orderTasks.isEmpty()) {
return List.of();
return V4OrderDetailContext.empty();
}
Map<Long, List<ReservationV4TaskCardSnapshot>> cardsByOrderTaskId = findV4TaskCardsByOrderTaskId(
hotelId,
@@ -341,11 +361,18 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
Map<Long, ReservationV4SourceMessageSummaryResult> sourceSummariesById = findV4SourceSummariesById(
hotelId,
orderTasks.stream().map(ReservationV4OrderTaskSnapshot::sourceMessageId).toList());
return orderTasks.stream()
return new V4OrderDetailContext(orderTasks, cardsByOrderTaskId, sourceSummariesById);
}
/**
* 转换订单详情页 V4 时间线列表。
*/
private List<ReservationOrderV4TaskTimelineItemResult> toV4TimelineItems(V4OrderDetailContext context) {
return context.orderTasks().stream()
.map(orderTask -> toV4TimelineItem(
orderTask,
cardsByOrderTaskId.getOrDefault(orderTask.id(), List.of()),
sourceSummariesById.get(orderTask.sourceMessageId())))
context.cardsByOrderTaskId().getOrDefault(orderTask.id(), List.of()),
context.sourceSummariesById().get(orderTask.sourceMessageId())))
.toList();
}
@@ -511,6 +538,7 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
orderTask.orderRef(),
orderTask.orderTaskStatus(),
v4CardCounts(cards),
toV4CardTimelineItems(cards),
sourceSummary,
UtcTimeFormatter.toUtcOffsetDateTime(orderTask.sourceReceivedAt()),
UtcTimeFormatter.toUtcOffsetDateTime(orderTask.createdAt()),
@@ -518,6 +546,209 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
UtcTimeFormatter.toUtcOffsetDateTime(latestV4ActivityAt(orderTask, cards)));
}
/**
* 转换 V4 任务卡为订单详情时间线摘要,不返回业务字段 payload。
*/
private List<ReservationOrderV4TaskCardTimelineItemResult> toV4CardTimelineItems(
List<ReservationV4TaskCardSnapshot> cards) {
return (cards == null ? List.<ReservationV4TaskCardSnapshot>of() : cards).stream()
.map(card -> new ReservationOrderV4TaskCardTimelineItemResult(
card.id().toString(),
card.cardType(),
card.eventType(),
card.sourceEventIndex(),
card.cardSortOrder(),
card.cardStatus(),
card.reviewStatus(),
card.confirmedBy(),
UtcTimeFormatter.toUtcOffsetDateTime(card.confirmedAt()),
UtcTimeFormatter.toUtcOffsetDateTime(card.updatedAt() == null
? card.createdAt()
: card.updatedAt())))
.toList();
}
/**
* 从已确认 V4 卡片中派生订单详情当前快照;未确认 AI 建议不进入订单事实展示。
*/
private ReservationOrderV4OverviewResult toV4OrderOverview(
List<ReservationV4OrderTaskSnapshot> orderTasks,
Map<Long, List<ReservationV4TaskCardSnapshot>> cardsByOrderTaskId) {
V4OrderOverviewDraft draft = new V4OrderOverviewDraft();
for (ReservationV4OrderTaskSnapshot orderTask : orderTasks == null
? List.<ReservationV4OrderTaskSnapshot>of()
: orderTasks) {
for (ReservationV4TaskCardSnapshot card : cardsByOrderTaskId.getOrDefault(orderTask.id(), List.of())) {
applyV4CardStatus(draft, card);
applyConfirmedV4CardSnapshot(draft, card);
}
}
return new ReservationOrderV4OverviewResult(
draft.accountCode,
draft.accountName,
draft.marketCode,
draft.sourceCode,
draft.arrivalDate,
draft.departureDate,
draft.rateCode,
draft.roomItems,
draft.traceCardStatus,
draft.roomingListCardStatus,
draft.paymentCardStatus,
UtcTimeFormatter.toUtcOffsetDateTime(draft.latestConfirmedAt));
}
/**
* 提取任务卡状态到订单详情总览,帮助前端展示订单下 Trace / Rooming List / Payment 是否仍待处理。
*/
private void applyV4CardStatus(V4OrderOverviewDraft draft, ReservationV4TaskCardSnapshot card) {
if (ReservationV4CardType.TRACE_RESERVATION_NOTES.name().equals(card.cardType())) {
draft.traceCardStatus = card.cardStatus();
} else if (ReservationV4CardType.ROOMING_LIST.name().equals(card.cardType())) {
draft.roomingListCardStatus = card.cardStatus();
} else if (ReservationV4CardType.PAYMENT.name().equals(card.cardType())) {
draft.paymentCardStatus = card.cardStatus();
}
}
/**
* 从单张已确认 V4 卡片抽取订单总览字段,后出现的确认卡覆盖先前同字段。
*/
private void applyConfirmedV4CardSnapshot(V4OrderOverviewDraft draft, ReservationV4TaskCardSnapshot card) {
if (!ReservationV4CardStatus.CONFIRMED.name().equals(card.cardStatus())
|| trimToNull(card.confirmedPayloadJson()) == null) {
return;
}
JsonNode payload = readJsonOrEmpty(card.confirmedPayloadJson());
if (ReservationV4CardType.BASIC_INFORMATION.name().equals(card.cardType())) {
JsonNode basicInformation = objectOrSelf(payload, "basic_information");
draft.accountCode = coalesceText(basicInformation, "account_code", draft.accountCode);
draft.accountName = coalesceText(basicInformation, "account_name", draft.accountName);
draft.marketCode = coalesceText(basicInformation, "market_code", draft.marketCode);
draft.sourceCode = coalesceText(basicInformation, "source_code", draft.sourceCode);
} else if (ReservationV4CardType.ROOM_INFORMATION.name().equals(card.cardType())) {
JsonNode businessFields = businessFields(payload);
draft.arrivalDate = coalesceText(businessFields, "arrival_date", draft.arrivalDate);
draft.departureDate = coalesceText(businessFields, "departure_date", draft.departureDate);
draft.rateCode = coalesceText(businessFields, "rate_code", draft.rateCode);
if (businessFields.path("room_items").isArray()) {
draft.roomItems = roomSummaries(businessFields.path("room_items"));
}
}
if (card.confirmedAt() != null
&& (draft.latestConfirmedAt == null || card.confirmedAt().isAfter(draft.latestConfirmedAt))) {
draft.latestConfirmedAt = card.confirmedAt();
}
}
/**
* 转换订单详情页 V4 下一步入口结果。
*/
private ReservationOrderV4NextActionResult toV4NextActionResult(V4OrderListNextAction action) {
V4OrderListNextAction safeAction = action == null ? V4OrderListNextAction.none() : action;
return new ReservationOrderV4NextActionResult(
safeAction.orderTaskId(),
safeAction.cardId(),
safeAction.actionType(),
safeAction.actionStatus(),
safeAction.openOrderTaskCount());
}
/**
* 生成订单关联来源邮件列表,按 V4 订单任务时间线去重。
*/
private List<ReservationV4SourceMessageSummaryResult> relatedV4SourceMessages(V4OrderDetailContext context) {
Map<String, ReservationV4SourceMessageSummaryResult> related = new LinkedHashMap<>();
for (ReservationV4OrderTaskSnapshot orderTask : context.orderTasks()) {
ReservationV4SourceMessageSummaryResult summary = context.sourceSummariesById()
.get(orderTask.sourceMessageId());
if (summary != null && summary.sourceMessageId() != null) {
related.putIfAbsent(summary.sourceMessageId(), summary);
}
}
return List.copyOf(related.values());
}
/**
* 读取业务字段主体Update Booking 的 after 结构优先作为订单当前快照来源。
*/
private JsonNode businessFields(JsonNode payload) {
JsonNode businessFields = objectOrSelf(payload, "business_fields");
JsonNode after = businessFields.path("after");
return after.isObject() ? after : businessFields;
}
/**
* 读取对象字段;字段不存在或非对象时返回原节点,兼容测试和早期确认 payload。
*/
private JsonNode objectOrSelf(JsonNode node, String fieldName) {
if (node == null || node.isMissingNode() || node.isNull()) {
return objectMapper.createObjectNode();
}
JsonNode child = node.path(fieldName);
return child.isObject() ? child : node;
}
/**
* JSON 字符串解析兜底,避免脏历史数据让订单详情整体失败。
*/
private JsonNode readJsonOrEmpty(String json) {
if (trimToNull(json) == null) {
return objectMapper.createObjectNode();
}
try {
return objectMapper.readTree(json);
} catch (JsonProcessingException ex) {
return objectMapper.createObjectNode();
}
}
/**
* 读取文本字段,空值保留既有快照值。
*/
private String coalesceText(JsonNode node, String fieldName, String fallback) {
String value = trimToNull(node.path(fieldName).asText(null));
return value == null ? fallback : value;
}
/**
* 转换已确认 room_items[] 为订单页安全摘要。
*/
private List<ReservationOrderV4RoomSummaryResult> roomSummaries(JsonNode roomItemsNode) {
List<ReservationOrderV4RoomSummaryResult> roomItems = new ArrayList<>();
for (JsonNode roomItem : roomItemsNode) {
String roomTypeCode = coalesceText(roomItem, "room_type_code", null);
if (roomTypeCode == null) {
roomTypeCode = coalesceText(roomItem, "pms_room_type_code", null);
}
Integer roomCount = integerAt(roomItem, "room_count");
if (roomCount == null) {
roomCount = integerAt(roomItem, "room_quantity");
}
roomItems.add(new ReservationOrderV4RoomSummaryResult(roomTypeCode, roomCount));
}
return List.copyOf(roomItems);
}
/**
* 宽松读取整数字段,兼容数字和字符串数字。
*/
private Integer integerAt(JsonNode node, String fieldName) {
JsonNode value = node.path(fieldName);
if (value.isInt() || value.isLong()) {
return value.asInt();
}
String text = trimToNull(value.asText(null));
if (text == null) {
return null;
}
try {
return Integer.valueOf(text);
} catch (NumberFormatException ex) {
return null;
}
}
/**
* 统计 V4 订单任务下各状态卡片数量,供订单详情时间线轻量展示。
*/
@@ -974,6 +1205,37 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
Long conversationMessageCount) {
}
/**
* 订单详情页 V4 聚合上下文,避免同一个接口为时间线、总览和下一步入口重复查库。
*/
private record V4OrderDetailContext(
List<ReservationV4OrderTaskSnapshot> orderTasks,
Map<Long, List<ReservationV4TaskCardSnapshot>> cardsByOrderTaskId,
Map<Long, ReservationV4SourceMessageSummaryResult> sourceSummariesById) {
private static V4OrderDetailContext empty() {
return new V4OrderDetailContext(List.of(), Map.of(), Map.of());
}
}
/**
* V4 订单总览派生草稿。仅在 Service 内部按时间线顺序覆盖字段,不向外暴露。
*/
private static final class V4OrderOverviewDraft {
private String accountCode;
private String accountName;
private String marketCode;
private String sourceCode;
private String arrivalDate;
private String departureDate;
private String rateCode;
private List<ReservationOrderV4RoomSummaryResult> roomItems = List.of();
private String traceCardStatus;
private String roomingListCardStatus;
private String paymentCardStatus;
private LocalDateTime latestConfirmedAt;
}
/**
* 订单列表中一个本地订单对应的 V4 下一步处理摘要。
*/

View File

@@ -512,6 +512,111 @@ class ReservationFrontendQueryControllerTest {
.andExpect(jsonPath("$.v4_order_tasks[1].source_received_at").value("2026-07-08T09:00:00Z"));
}
@Test
void shouldReturnOrderDetailWithV4OverviewNextActionAndCardTimeline() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage(
"mail-frontend-order-detail-v4-overview-001",
"Frontend V4 Overview",
Instant.parse("2026-07-08T11:00:00Z"));
Long orderId = 930000000000005001L;
insertActiveGroupOrder(orderId, source.inboxId(), "GRP-FRONTEND-V4-OVERVIEW-001");
ReservationV4OrderTaskSnapshot orderTask = insertV4OrderTask(
930000000000005101L,
source,
orderId,
"order-overview",
"GRP-FRONTEND-V4-OVERVIEW-001",
Instant.parse("2026-07-08T11:00:00Z"),
HOTEL_ID);
insertV4TaskCard(orderTask, ReservationV4CardType.SOURCE_MESSAGE_DISPLAY.name(), null, 0, 10,
ReservationV4CardStatus.READONLY.name(), null, "{}");
ReservationV4TaskCardSnapshot basicCard = insertV4TaskCard(
orderTask, ReservationV4CardType.BASIC_INFORMATION.name(), null, 0, 20,
ReservationV4CardStatus.CONFIRMED.name(), null, """
{"basic_information":{"account_code":"QBD_TRAVEL"}}
""");
confirmV4TaskCard(basicCard.id(), """
{
"basic_information": {
"account_code": "QBD_TRAVEL",
"account_name": "Q.B.D. TRAVEL GROUP CO., LTD",
"market_code": "LEISURE",
"source_code": "TRAVEL_AGENT"
}
}
""", "frontend-query-admin", Instant.parse("2026-07-08T11:10:00Z"));
ReservationV4TaskCardSnapshot roomCard = insertV4TaskCard(
orderTask, ReservationV4CardType.ROOM_INFORMATION.name(), "NEW_BOOKING", 1, 30,
ReservationV4CardStatus.CONFIRMED.name(), null, "{}");
confirmV4TaskCard(roomCard.id(), """
{
"business_fields": {
"arrival_date": "2026-07-26",
"departure_date": "2026-07-29",
"rate_code": "BAR",
"room_items": [
{"room_type_code": "RM1", "room_count": 2},
{"room_type_code": "RM2", "room_count": 1}
]
}
}
""", "frontend-query-admin", Instant.parse("2026-07-08T11:20:00Z"));
insertV4TaskCard(orderTask, ReservationV4CardType.ROOM_INFORMATION.name(), "UPDATE_BOOKING", 2, 40,
ReservationV4CardStatus.PENDING_CONFIRM.name(), null, """
{
"business_fields": {
"arrival_date": "2099-01-01",
"departure_date": "2099-01-02",
"rate_code": "SHOULD_NOT_BE_ORDER_FACT",
"room_items": [
{"room_type_code": "SHOULD_NOT_APPEAR", "room_count": 99}
]
}
}
""");
ReservationV4TaskCardSnapshot paymentCard = insertV4TaskCard(
orderTask, ReservationV4CardType.PAYMENT.name(), "PAYMENT", 3, 60,
ReservationV4CardStatus.REVIEW_REQUIRED.name(), "PENDING", "{}");
performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders/{orderId}", orderId)
.param("hotel_id", HOTEL_ID)
.param("include_tasks", "true"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.order_overview.account_code").value("QBD_TRAVEL"))
.andExpect(jsonPath("$.order_overview.account_name").value("Q.B.D. TRAVEL GROUP CO., LTD"))
.andExpect(jsonPath("$.order_overview.market_code").value("LEISURE"))
.andExpect(jsonPath("$.order_overview.source_code").value("TRAVEL_AGENT"))
.andExpect(jsonPath("$.order_overview.arrival_date").value("2026-07-26"))
.andExpect(jsonPath("$.order_overview.departure_date").value("2026-07-29"))
.andExpect(jsonPath("$.order_overview.rate_code").value("BAR"))
.andExpect(jsonPath("$.order_overview.room_items.length()").value(2))
.andExpect(jsonPath("$.order_overview.room_items[0].room_type_code").value("RM1"))
.andExpect(jsonPath("$.order_overview.room_items[0].room_count").value(2))
.andExpect(jsonPath("$.order_overview.room_items[?(@.room_type_code=='SHOULD_NOT_APPEAR')]")
.isEmpty())
.andExpect(jsonPath("$.order_overview.payment_card_status").value("REVIEW_REQUIRED"))
.andExpect(jsonPath("$.order_overview.latest_confirmed_at").value("2026-07-08T11:20:00Z"))
.andExpect(jsonPath("$.next_v4_action.order_task_id").value(orderTask.id().toString()))
.andExpect(jsonPath("$.next_v4_action.card_id").value(paymentCard.id().toString()))
.andExpect(jsonPath("$.next_v4_action.action_type").value("REVIEW"))
.andExpect(jsonPath("$.next_v4_action.action_status").value("REVIEW_REQUIRED"))
.andExpect(jsonPath("$.next_v4_action.open_order_task_count").value(1))
.andExpect(jsonPath("$.related_source_messages.length()").value(1))
.andExpect(jsonPath("$.related_source_messages[0].source_message_id").value(source.inboxId().toString()))
.andExpect(jsonPath("$.v4_order_tasks[0].cards.length()").value(5))
.andExpect(jsonPath("$.v4_order_tasks[0].cards[1].card_id").value(basicCard.id().toString()))
.andExpect(jsonPath("$.v4_order_tasks[0].cards[1].card_type").value("BASIC_INFORMATION"))
.andExpect(jsonPath("$.v4_order_tasks[0].cards[1].card_status").value("CONFIRMED"))
.andExpect(jsonPath("$.v4_order_tasks[0].cards[1].confirmed_by").value("frontend-query-admin"))
.andExpect(jsonPath("$.v4_order_tasks[0].cards[1].confirmed_at").value("2026-07-08T11:10:00Z"))
.andExpect(jsonPath("$.v4_order_tasks[0].cards[1].ai_payload_json").doesNotExist())
.andExpect(jsonPath("$.v4_order_tasks[0].cards[1].display_payload_json").doesNotExist())
.andExpect(jsonPath("$.v4_order_tasks[0].cards[1].confirmed_payload_json").doesNotExist())
.andExpect(jsonPath("$.v4_order_tasks[0].cards[4].card_id").value(paymentCard.id().toString()))
.andExpect(jsonPath("$.v4_order_tasks[0].cards[4].card_status").value("REVIEW_REQUIRED"))
.andExpect(jsonPath("$.v4_order_tasks[0].cards[4].review_status").value("PENDING"));
}
@Test
void shouldHideV4OrderTaskTimelineWhenIncludeTasksFalseAndIgnoreCrossHotelRows() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage(
@@ -853,6 +958,20 @@ class ReservationFrontendQueryControllerTest {
""", Timestamp.valueOf(LocalDateTime.ofInstant(updatedAt, ZoneOffset.UTC)), orderTaskId, cardSortOrder);
}
private void confirmV4TaskCard(Long cardId, String confirmedPayloadJson, String confirmedBy, Instant confirmedAt) {
jdbcTemplate.update("""
UPDATE workflow_reservation_v4_task_card
SET confirmed_payload_json = ?,
confirmed_by = ?,
confirmed_at = ?,
updated_at = ?
WHERE id = ?
""", confirmedPayloadJson, confirmedBy,
Timestamp.valueOf(LocalDateTime.ofInstant(confirmedAt, ZoneOffset.UTC)),
Timestamp.valueOf(LocalDateTime.ofInstant(confirmedAt, ZoneOffset.UTC)),
cardId);
}
private ReservationV4OrderTaskSnapshot insertV4OrderTask(
Long aiBatchId,
SourceMessageCaptureResult source,