From 626e87abdba1eb65e2603e9a72a7155781c75e79 Mon Sep 17 00:00:00 2001 From: andy Date: Mon, 20 Jul 2026 17:18:28 +0700 Subject: [PATCH] =?UTF-8?q?=E5=81=9C=E6=AD=A2V4=E5=85=A5=E7=AB=99=E5=8F=8C?= =?UTF-8?q?=E5=86=99=E6=97=A7=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROJECT_STATE.md | 16 +- docs/project/README.md | 8 +- .../backend-to-frontend-notes.md | 13 +- .../frontend-to-backend-api-requests.md | 8 +- .../integrations/superagent-api-contract.md | 20 +- .../dev-test-v4-legacy-task-cleanup.md | 177 +++++++++++++++++ .../M002-v4-agent-callback-field-contract.md | 4 +- ...002-v4-order-task-card-domain-model-cp2.md | 26 ++- .../security-access-control-boundary.md | 2 +- .../dto/ReservationV4AcceptedEventDraft.java | 2 +- .../SuperAgentTaskResultItemResponse.java | 8 +- .../ReservationAiTaskIntakeServiceImpl.java | 178 +++++++++++++++++- .../impl/ReservationAiV4CallbackRouter.java | 6 +- .../impl/ReservationDemoDataServiceImpl.java | 2 +- ...eservationFrontendQueryControllerTest.java | 2 + .../SuperAgentTaskResultControllerTest.java | 35 ++-- 16 files changed, 438 insertions(+), 69 deletions(-) create mode 100644 docs/project/operations/dev-test-v4-legacy-task-cleanup.md diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 83f7889..9f57ddc 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -4,16 +4,16 @@ | --- | --- | | 最近更新 | 2026-07-20 | | 当前分支 | `feature/huangting` | -| 当前阶段 | 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 订单总览页;V4 订单任务和 S10/S99 来源通知已补齐业务审计查询接口;2026-07-20 测试机 smoke 已完成主链路验证,但测试机订单详情响应疑似未部署到包含 CP15.1 的最新后端包,需要重新部署后复测订单详情 V4 总览;后续可做真实 PMS / OPERA / OHIP 同步或 SuperAgent 目录供给方案 | +| 当前阶段 | M002 V4 入站、多卡模型、持久化基线、入站写入、查询接口、卡片确认、复核解阻、目录校验、订单详情 V4 总览、DB 目录、Lookup API、前端 lookup 接入、目录管理后台 CP1 前后端、订单列表 V4 继续处理入口 / open count 收口、V4 业务审计查询和停止旧任务双写 | +| 当前重点 | M002 V4 已停止普通业务入站双写旧 `workflow_reservation_task`,V4 后新业务主线只写 V4 order task / cards / source notification;开发阶段不维护 V2/V3 旧任务兼容,测试数据可重建,生产迁移策略后续上线前单独设计。`GET /api/reservation/orders` 可返回 V4 下一步订单任务、卡片、动作类型、动作状态、V4 open 数和统一展示字段 `open_work_item_count`;旧 `open_task_count` / `next_processable_task_id` 仅作历史诊断兼容。后续可继续做测试机 V4 smoke 复测、真实 PMS / OPERA / OHIP 同步或 SuperAgent 目录供给方案。 | ## 1. 当前 Checkpoint -- 名称:`M002-V4-order-detail-overview-frontend` -- 状态:Done,前端订单详情页已改为“订单总览页”:展示 `order_overview` 当前确认快照、`next_v4_action` 下一步入口、`related_source_messages[]` 关联来源消息摘要,以及 `v4_order_tasks[].cards[]` V4 订单任务和任务卡安全摘要。点击下一步入口或 V4 时间线任务进入 `/reservation/order-tasks/{orderTaskId}`。 -- 目标:让订单详情页聚焦订单视角总览,不在该页直接确认、复核或编辑 V4 任务卡。 -- 边界:本轮不改后端、不改 V4 写操作、不展示 `ai_payload_json`、`display_payload_json`、`confirmed_payload_json`、邮件正文或附件 URL;旧 V2/V3 详情接口和独立任务详情页继续保留。 -- 联调备注:2026-07-20 测试机 V4 主链路 smoke 已完成登录 / 权限 / 酒店隔离、V4 工作台、V4 订单任务列表和详情、lookup、Basic Information 确认、业务卡确认、S10/S99 来源通知详情和 ack、V4 业务审计验证;当前测试机没有待复核 `REVIEW_REQUIRED` 卡,复核写操作未覆盖。测试机 `GET /api/reservation/orders/{orderId}` 仍返回旧响应形态,缺少 CP15.1 的 `order_overview`、`next_v4_action`、`related_source_messages[]` 和 `v4_order_tasks[].cards[]`,优先排查测试机是否部署了包含订单详情 V4 总览的最新包。 +- 名称:`M002-V4-stop-legacy-task-dual-write-and-dev-cleanup` +- 状态:Done,后端已停止 V4 普通业务入站创建旧 `workflow_reservation_task`,补充开发 / 测试旧任务数据清理说明,并通过相关回归与全量后端测试。 +- 目标:V4 普通业务只写 V4 模型,避免 V4 order task 已完成后旧兼容任务仍作为订单列表继续处理 fallback。 +- 边界:不做生产数据迁移,不推进真实 PMS / OPERA / OHIP,不推进 M011 CP4,不删除 SourceMessage、V4 order task / cards、V4 source notification、目录、用户权限、酒店配置或 SuperAgent dispatch run。 +- 联调备注:开发 / 测试环境允许清理旧任务、旧任务卡、旧任务审计和旧 OPERA 模拟数据;清理 SQL 见 `docs/project/operations/dev-test-v4-legacy-task-cleanup.md`。该策略仅限开发 / 测试阶段,不代表生产迁移方案。 ## 2. 当前优先级 @@ -36,7 +36,7 @@ - 后续每完成一个 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 / 附加费表高亮行业务 JSON;Debug 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 来源通知 ack;M002 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 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 来源通知 ack;M002 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[]`;V4 普通业务入站已停止双写旧 `workflow_reservation_task`。真实 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 diff --git a/docs/project/README.md b/docs/project/README.md index 703e2fa..1afb62b 100644 --- a/docs/project/README.md +++ b/docs/project/README.md @@ -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,CP12 已完成前端 lookup 接入,CP13 已完成目录管理后台 CP1,CP14 已完成订单列表 V4 继续处理入口,CP15 已完成 V4 业务审计查询,CP15.1 已完成订单详情 V4 总览后端补齐;后续仍需前端订单详情 V4 化、测试机联调和真实 PMS / OPERA / OHIP 同步。 | +| `requirements/M002-v4-order-task-card-domain-model-cp2.md` | 当前有效 | M002 V4 CP2 订单任务与多卡领域模型设计,并记录 CP3-CP8 表结构、入站写入、查询、确认、复核和目录校验已落地状态;CP11 已完成 DB 目录与 lookup API,CP12 已完成前端 lookup 接入,CP13 已完成目录管理后台 CP1,CP14 已完成订单列表 V4 继续处理入口,CP15 已完成 V4 业务审计查询,CP15.1 已完成订单详情 V4 总览后端补齐,当前已停止 V4 普通业务双写旧 `workflow_reservation_task`;开发阶段不维护 V2/V3 旧任务兼容,测试数据可重建,生产迁移策略后置。 | | `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` 为准。 | @@ -80,6 +80,12 @@ | `frontend-backend/frontend-to-backend-api-requests.md` | 当前有效 | 前端提醒后端需要增加或补齐的接口,已区分可用、后置和历史候选路径。 | | `frontend-backend/debug-eml-page-integration-guide.md` | 当前有效 | Debug EML 页面前端对接指南。 | +## 运维与开发测试操作 + +| 文档 | 状态 | 中文说明 | +| --- | --- | --- | +| `operations/dev-test-v4-legacy-task-cleanup.md` | 当前有效 | 开发 / 测试环境 V4 停止双写旧任务后的旧 `workflow_reservation_task` 及直接依赖数据清理说明;不适用于生产迁移。 | + ## 执行计划 | 文档 | 状态 | 中文说明 | diff --git a/docs/project/frontend-backend/backend-to-frontend-notes.md b/docs/project/frontend-backend/backend-to-frontend-notes.md index 964aa92..1b986e0 100644 --- a/docs/project/frontend-backend/backend-to-frontend-notes.md +++ b/docs/project/frontend-backend/backend-to-frontend-notes.md @@ -52,7 +52,7 @@ | `POST /api/auth/login` | 用户名密码登录 | 成功后返回 `access_token`、当前用户、可访问酒店、权限码和可见菜单;token 只放 `sessionStorage`,不要放 `localStorage`、URL、日志或错误上报。 | | `GET /api/auth/me` | 恢复当前登录态 | 前端启动后带 `Authorization: Bearer ` 调用;401 时清理 token 并进入登录页。 | | `POST /api/auth/logout` | 登出当前 session | 带 `Authorization: Bearer `;成功后前端必须清理本地 token 和当前用户上下文。 | -| `GET /api/reservation/orders` | 查询订单列表 | 必须带 `Authorization: Bearer `,需要 `RESERVATION_ORDER_READ`;默认返回全部订单状态;按后端维护的订单最近业务活动时间倒序,当前落库字段为 `workflow_reservation_order.latest_activity_at`,前端不要自行重排;`open_work_item_count` 是订单列表统一待处理展示数量,前端订单列表已用它展示待处理数,第一版等于 V4 未完成订单任务数;`open_task_count` 仍保留为旧 V2/V3 诊断计数,排除 `COMPLETED` 和 `FAILED`;已补齐 V4 继续处理入口字段,前端有 `next_v4_order_task_id` 时优先跳 V4 订单任务详情;隐藏技术订单不返回,因此 S10/S99 和旧 S000/S999 不会在订单列表形成订单。 | +| `GET /api/reservation/orders` | 查询订单列表 | 必须带 `Authorization: Bearer `,需要 `RESERVATION_ORDER_READ`;默认返回全部订单状态;按后端维护的订单最近业务活动时间倒序,当前落库字段为 `workflow_reservation_order.latest_activity_at`,前端不要自行重排;`open_work_item_count` 是订单列表统一待处理展示数量,前端订单列表已用它展示待处理数,第一版等于 V4 未完成订单任务数;V4 普通业务入站已停止双写旧 `workflow_reservation_task`,开发 / 测试阶段不维护 V2/V3 旧任务兼容,测试数据可重建;`open_task_count` 仍保留为旧任务表原始诊断计数,前端不要用于展示待处理数;已补齐 V4 继续处理入口字段,前端有 `next_v4_order_task_id` 时优先跳 V4 订单任务详情;`next_processable_task_id` 仅作为历史 V2/V3 诊断兼容字段,清理旧任务数据后新 V4 订单不应返回该字段;隐藏技术订单不返回,因此 S10/S99 和旧 S000/S999 不会在订单列表形成订单。 | | `GET /api/reservation/tasks` | 查询任务列表 / 工作台 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`;未传 `order_id` 时按来源消息接收时间倒序,传 `order_id` 时按同订单队列顺序正序;用 `can_process` 和 `readonly_reason_code` 控制入口按钮;列表不返回 AI 原始 payload、邮件正文或附件 URL;已返回来源邮件会话摘要字段,并支持 `order_status` 按任务所属订单状态筛选;旧 S000/S999 和 V3 S10/S99 以 `task_type=SOURCE_MESSAGE_ONLY` 只读任务返回,列表已透出 `result_type`、`ai_task_type`、`route_code`、`system_process_category`。V4 S10/S99 不再进入该旧任务表,应从 V4 工作台来源通知接口展示。 | | `GET /api/reservation/workbench-items` | 查询 V4 工作台统一列表 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`;返回 V4 业务订单任务和 S10/S99 来源通知混排摘要;支持 `hotel_id`、`item_type`、`keyword`、`page_num`、`page_size`;默认按 `source_received_at` 倒序,同一来源时间下按 `updated_at`、`created_at`、数字 `target_id` 倒序;列表不返回邮件正文、附件 URL、`ai_payload_json` 或来源通知原始 payload。 | | `GET /api/reservation/order-tasks` | 查询 V4 业务订单任务列表 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`;只返回 V4 业务订单任务,不包含 S10/S99 来源通知;支持 `hotel_id`、`order_id`、`order_task_status`、`card_status`、`keyword`、`page_num`、`page_size`;`order_task_status` 非 `OPEN` / `COMPLETED` 返回 400,`card_status` 非 V4 卡状态返回 400;`card_status` 只筛业务 / 可处理卡,固定来源邮件展示卡不参与筛选。 | @@ -104,7 +104,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/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` 保留为旧任务表原始诊断计数。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}`;开发 / 测试清理旧任务后,新 V4 订单通常不再回退旧 `/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[]` 和 `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 正序返回;前端不要自行重排。订单详情页只展示 V4 确认快照、下一步入口、关联来源消息和任务卡安全摘要,不在该页确认、复核或编辑任务卡,也不展示 payload、邮件正文或附件 URL。`include_source_summary` 第一版不作为前端裁剪字段的强约束,前端暂不要依赖它减少返回字段。 | | `GET /api/reservation/tasks/{taskId}` | 补齐顶层来源邮件字段,并扩展 `fields[]` 元数据。 | 顶层来源字段用于打开邮件会话;`fields[]` 中的 `result_type`、`task_type`、`task_subtype`、`default_value_source` 用于前端字段分组、调试和白名单对齐。 | @@ -190,11 +190,11 @@ POST /api/auth/logout - `GET /api/reservation/orders` 默认返回全部订单状态,包括 `TEMPORARY`、`ACTIVE`、`ENDED`、`LOGIC_DELETED`。 - `keyword` 会匹配订单业务号、临时订单号、展示名、订单状态,也会匹配来源消息安全摘要命中的 SourceMessage ID;前端可以用邮件主题、外部消息 ID 或会话 ID 辅助查订单。 - `open_work_item_count` 是订单列表统一展示数量,第一版按 V4 未完成订单任务计算,即等于 `v4_open_order_task_count`;前端展示待处理数量时只使用它,不回退旧 `open_task_count`,也不要自行把旧任务数和 V4 数相加。 -- `open_task_count` 只统计旧 V2/V3 未关闭任务,排除 `COMPLETED` 和 `FAILED`,当前保留用于兼容和问题排查。 -- `next_processable_task_id` 是后端按同订单队列实时计算出的下一条可处理任务;前端可以用它做“继续处理”入口。 +- `open_task_count` 是旧任务表原始未关闭计数,排除 `COMPLETED` 和 `FAILED`,当前保留用于历史接口兼容和问题排查;V4 普通业务入站已停止双写旧任务,开发 / 测试环境应清理旧任务数据,前端展示待处理数量不要使用它。 +- `next_processable_task_id` 是后端按旧 V2/V3 队列实时计算出的历史兼容字段;开发阶段不维护 V2/V3 旧任务兼容,清理旧任务数据后,新 V4 订单不应再通过它进入旧任务详情。 - V4 订单任务入口字段由后端实时派生:`v4_open_order_task_count` 统计当前订单下 `order_task_status!=COMPLETED` 的 V4 订单任务;`next_v4_order_task_id` 是同订单第一条仍需用户处理的 V4 订单任务;`next_v4_action_card_id` 是该订单任务下第一张待处理卡;`next_v4_action_type` 取 `CONFIRM` / `REVIEW` / `NONE`;`next_v4_action_status` 取 `PENDING_CONFIRM` / `REVIEW_REQUIRED` 或空。 - V4 派生规则:Basic Information 必须优先于业务卡;Basic 已确认后,业务卡中 `REVIEW_REQUIRED` 优先于普通 `PENDING_CONFIRM`;`COMPLETED` 的 V4 订单任务不计入 open;S10/S99 来源通知不挂订单,不进入这些订单列表字段。 -- 前端订单列表“继续处理”已按该优先级接入:有 `next_v4_order_task_id` 时跳 V4 订单任务详情;否则有旧 `next_processable_task_id` 时跳旧任务详情;`next_v4_action_type=NONE` 且旧字段为空时展示无待处理状态,查看详情仍固定进入订单详情。 +- 前端订单列表“继续处理”已按该优先级接入:有 `next_v4_order_task_id` 时跳 V4 订单任务详情;`next_v4_action_type=NONE` 且旧字段为空时展示无待处理状态,查看详情仍固定进入订单详情。开发 / 测试阶段旧任务数据可清理,清理后新 V4 订单不应再出现旧 fallback 入口。 - `display_order_key` 是前端优先展示的订单业务号或临时订单号;`group_code` 和 `confirmation_number` 只有在当前订单业务号类型匹配时返回。 - 订单 ID、任务 ID、SourceMessage ID 在这些前端接口中按字符串返回,前端不要转换成 JavaScript number。 - 当前 V3 / 过渡实现中,源邮件只读通知卡背后有隐藏技术订单用于满足后端任务外键,但订单列表不会返回该订单;任务列表中该类任务的 `display_order_key`、`temporary_order_no`、`group_code`、`confirmation_number` 可能为空,前端不要因此隐藏整条任务。V4 S10/S99 目标模型已改为独立来源通知,不再挂隐藏技术订单。 @@ -248,7 +248,7 @@ POST /api/auth/logout ### 5.6 前端联调演示数据 seed 接口 -后端提供一个受控的 dev/test 演示数据入口,方便前端在空库或本地环境快速看到页面效果。 +后端提供一个受控的 dev/test 演示数据入口,方便前端在空库或本地环境快速看到历史 V2/V3 页面效果。 ```text POST /api/system/reservation/demo-data @@ -265,6 +265,7 @@ Content-Type: application/json - dev profile 默认开启;test 默认关闭,需要后端环境显式设置 `reservation.demo-data.enabled=true` 或环境变量 `RESERVATION_TEST_DEMO_DATA_ENABLED=true`。 - 必须配置 `reservation.demo-data.access-key`;dev 优先使用 `RESERVATION_DEV_DEMO_DATA_ACCESS_KEY`,test 优先使用 `RESERVATION_TEST_DEMO_DATA_ACCESS_KEY`,旧通用变量 `RESERVATION_DEMO_DATA_ACCESS_KEY` 仅作为兼容兜底。 - 该接口只用于 dev/test 联调,不允许放进生产普通页面,也不要把访问口令写进前端仓库、浏览器环境变量或构建产物。 +- 该接口会创建旧 `workflow_reservation_task` 演示数据,是历史 V2/V3 页面演示入口;M002 V4 smoke 不应再使用该接口造数,避免重新制造旧任务残留。V4 smoke 应使用 SuperAgent V4 回调或专门 V4 fixture。 返回内容: diff --git a/docs/project/frontend-backend/frontend-to-backend-api-requests.md b/docs/project/frontend-backend/frontend-to-backend-api-requests.md index 5a7286c..3b9d5e1 100644 --- a/docs/project/frontend-backend/frontend-to-backend-api-requests.md +++ b/docs/project/frontend-backend/frontend-to-backend-api-requests.md @@ -42,7 +42,7 @@ | `GET /api/source-messages` | 已完成安全摘要列表 | 可以 | 不能替代邮件会话全文接口。 | | `GET /api/source-messages/{id}` | 已完成单条安全摘要 | 可以 | 不能替代邮件会话全文接口。 | | `GET /api/source-messages/{id}/original` | 已完成单封原文权限读取 | 谨慎接入 | 必须带 Bearer token,需要同时拥有 `SOURCE_MESSAGE_READ` 和 `SOURCE_MESSAGE_ORIGINAL_READ`;只能读单封邮件,不能返回同一 conversation 全量邮件。 | -| `GET /api/reservation/orders` | 已完成第一版 | 可以 | 默认查询全部订单状态;订单列表待处理展示使用 `open_work_item_count`;旧 `open_task_count` 仅作为 V2/V3 兼容诊断计数。 | +| `GET /api/reservation/orders` | 已完成第一版 | 可以 | 默认查询全部订单状态;订单列表待处理展示使用 `open_work_item_count`;V4 普通业务已停止双写旧任务,旧 `open_task_count` 仅作为历史诊断计数。 | | `GET /api/source-messages/{sourceMessageId}/conversation` | 已完成第一版,已补 `html_body_sanitized` 和 `html_render_mode` | 可以 | 必须带 Bearer token,需要同时拥有 `SOURCE_MESSAGE_READ` 和 `SOURCE_MESSAGE_ORIGINAL_READ`;返回完整 text/html、后端清洗后的 HTML、媒体外链和关联订单 / 任务摘要;前端不传原文读取 key,页面展示优先使用 `html_body_sanitized`。 | | `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。 | @@ -353,7 +353,7 @@ GET /api/reservation/orders/{orderId} GET /api/reservation/orders ``` -当前状态:后端已完成第一版。默认查询全部订单状态;`open_task_count` 排除旧 V2/V3 的 `COMPLETED` 和 `FAILED`,`next_processable_task_id` 按旧 V2/V3 同订单队列可处理状态实时计算。M002 V4 CP14 已补齐 V4 继续处理入口字段;后端新增 `open_work_item_count` 作为订单列表统一待处理展示数量,第一版直接等于 V4 未完成订单任务数,不叠加旧任务。前端已按 `next_v4_order_task_id` 优先进入 V4 订单任务详情,并在无 V4 入口时回退旧任务详情;订单列表待处理数量已改为只展示 `open_work_item_count`。 +当前状态:后端已完成第一版。默认查询全部订单状态;`open_task_count` 排除旧任务表中 `COMPLETED` 和 `FAILED`,保留为历史诊断计数。M002 V4 CP14 已补齐 V4 继续处理入口字段;后端新增 `open_work_item_count` 作为订单列表统一待处理展示数量,第一版直接等于 V4 未完成订单任务数,不叠加旧任务。当前开发阶段已停止 V4 普通业务双写旧 `workflow_reservation_task`,开发 / 测试环境旧任务数据可清理且可重建;`next_processable_task_id` 仅作为历史 V2/V3 诊断兼容字段,清理后新 V4 订单不应返回旧任务入口。前端已按 `next_v4_order_task_id` 优先进入 V4 订单任务详情;订单列表待处理数量已改为只展示 `open_work_item_count`。 默认排序:按后端维护的订单最近业务活动时间倒序返回,保证最近有业务活动的订单排在前面。后端当前使用 `workflow_reservation_order.latest_activity_at` 作为排序字段,并在订单创建、任务创建、草稿保存、最终确认、人工复核解阻、任务状态变更等写路径维护;前端不要再基于任务时间或更新时间自行重排。 @@ -413,7 +413,7 @@ V4 继续处理字段说明: | `next_v4_action_status` | `PENDING_CONFIRM` / `REVIEW_REQUIRED`;`NONE` 时为空。 | | `v4_open_order_task_count` | 当前订单下未完成 V4 订单任务数,`COMPLETED` 不计入。 | -前端“继续处理”入口优先级:优先使用 `next_v4_order_task_id` 跳转 V4 订单任务详情;没有 V4 待处理时再使用旧 `next_processable_task_id` 跳旧任务详情;两者都没有时展示无待处理状态。S10/S99 来源通知不创建订单,不进入订单列表字段统计。订单列表展示“待处理数量”时只使用 `open_work_item_count`,不使用旧 `open_task_count` 作为展示数量,也不在前端自行计算 `open_task_count + v4_open_order_task_count`;旧 `open_task_count` 和 `v4_open_order_task_count` 保留用于兼容与排查。 +前端“继续处理”入口优先级:优先使用 `next_v4_order_task_id` 跳转 V4 订单任务详情;没有 V4 待处理且旧 `next_processable_task_id` 为空时展示无待处理状态。S10/S99 来源通知不创建订单,不进入订单列表字段统计。订单列表展示“待处理数量”时只使用 `open_work_item_count`,不使用旧 `open_task_count` 作为展示数量,也不在前端自行计算 `open_task_count + v4_open_order_task_count`;旧 `open_task_count` 和 `v4_open_order_task_count` 保留用于兼容与排查。V4 新业务主线只写 V4 模型,开发阶段不维护 V2/V3 旧任务兼容,测试数据可重建;清理旧任务数据后,新 V4 订单不应再出现旧继续处理入口。 ## 6. 前端联调演示数据 seed 接口 @@ -423,7 +423,7 @@ V4 继续处理字段说明: POST /api/system/reservation/demo-data ``` -当前状态:后端已完成第一版。该接口只用于本地 / test 联调造数,默认关闭,不是生产业务页面接口。 +当前状态:后端已完成第一版。该接口只用于本地 / test 联调造数,默认关闭,不是生产业务页面接口。该接口会创建旧 `workflow_reservation_task` 演示数据,是历史 V2/V3 页面演示入口;M002 V4 smoke 不应再使用该接口造数,避免重新制造旧任务残留。V4 smoke 应使用 SuperAgent V4 回调或专门 V4 fixture。 启用条件: diff --git a/docs/project/integrations/superagent-api-contract.md b/docs/project/integrations/superagent-api-contract.md index 3f2b155..be908f3 100644 --- a/docs/project/integrations/superagent-api-contract.md +++ b/docs/project/integrations/superagent-api-contract.md @@ -120,12 +120,14 @@ SuperAgent 不应知道或依赖内部 SourceMessage Inbox ID,也不需要为 - 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 卡和业务事件卡。 +- 普通业务按 `source_message + order_ref` 创建 V4 订单任务,并在订单任务下创建来源消息只读卡、Basic Information 卡和业务事件卡;V4 后新业务主线不再创建旧 `workflow_reservation_task`。 - Basic Information 必须先确认;业务卡逐卡确认或复核解阻,确认后永久锁定;V4 第一版不提供前端草稿。 - `route_code=S10/S99` 使用 V4 来源通知模型,不创建隐藏技术订单,不进入订单详情时间线,不阻塞普通订单;前端只展示和 ack。 - Account / Market / Source、Room Type、Rate Code 以本系统数据库目录稳定代码为准;SuperAgent 不应输出显示文案作为业务判断依据。 -当前后端仍兼容 M002 V3 CP1-CP6: +开发阶段不维护 V2/V3 旧任务兼容,测试数据可重建;该策略仅限开发 / 测试阶段,不代表生产迁移方案。生产数据迁移策略不在当前 checkpoint 处理,后续上线前另开迁移方案。 + +当前后端仍兼容 M002 V3 CP1-CP6 历史入口代码路径,但不作为开发阶段新业务验收重点: - 已建立 40 条 P0.1 路由枚举 / 稳定配置;`route_code` 保持历史稳定,不按总数连续重编号,`R41/R42` 仍可能出现在响应和历史 transition 中。 - 已支持结构化 `S10/S99` 历史入站兼容;当前 V4 新入站使用来源通知模型。 @@ -811,6 +813,8 @@ S000/S999 不在 body 里传 `hotel_id`,后端使用平台酒店表唯一 `ACT ### 8.7 成功响应 +V4 普通业务包成功响应示例。注意:V4 新业务主线只写 V4 订单任务和任务卡,响应中的 `task_id`、`task_status`、`execution_order` 不再代表旧 `workflow_reservation_task`,因此返回 `null`;前端和后端内部处理应通过 V4 查询接口读取 `order_task_id` / `card_id`。 + ```json { "request_id": "req-003", @@ -827,19 +831,21 @@ S000/S999 不在 body 里传 `hotel_id`,后端使用平台酒店表唯一 `ACT "system_process_category": "BUSINESS_TASK", "adapter_error_code": null, "order_id": "1900000000000000300", - "task_id": "1900000000000000400", + "task_id": null, "system_task_type": "NEW_BOOKING", "task_card_type": "NEW_BOOKING", - "task_status": "PENDING_CONFIRM", - "order_status": "TEMPORARY", - "execution_order": 1 + "task_status": null, + "order_status": "ACTIVE", + "execution_order": null } ], "warnings": [] } ``` -S000/S999 成功响应示例: +历史 V2/V3 兼容入口仍可能返回旧 `task_id` 和 `execution_order`;该路径仅作历史参考,不作为开发阶段 V4 新业务验收重点。 + +旧文本 S000/S999 成功响应示例: ```json { diff --git a/docs/project/operations/dev-test-v4-legacy-task-cleanup.md b/docs/project/operations/dev-test-v4-legacy-task-cleanup.md new file mode 100644 index 0000000..7a5ef65 --- /dev/null +++ b/docs/project/operations/dev-test-v4-legacy-task-cleanup.md @@ -0,0 +1,177 @@ +# 开发 / 测试环境 V4 旧任务数据清理说明 + +## 1. 使用边界 + +本说明只适用于开发 / 测试环境,用于 M002 V4 主线停止双写旧 `workflow_reservation_task` 后,清理历史 smoke、fixture 或调试过程中遗留的旧任务数据。 + +项目当前决策: + +- V4 后新业务主线只写 V4 模型:`workflow_reservation_v4_order_task`、`workflow_reservation_v4_task_card`、`workflow_reservation_v4_source_notification`。 +- 开发阶段不维护 V2/V3 旧任务兼容,测试数据可重建。 +- 本清理策略不代表生产迁移方案;生产数据迁移必须在上线前单独设计、评审和演练。 + +## 2. 不清理的数据 + +以下数据不要通过本脚本清理: + +- `platform_source_message_inbox` +- `workflow_reservation_v4_order_task` +- `workflow_reservation_v4_task_card` +- `workflow_reservation_v4_source_notification` +- `workflow_reservation_catalog_account` +- `workflow_reservation_catalog_code` +- 用户、角色、权限、菜单和酒店配置表 +- `platform_superagent_dispatch_run` +- SourceMessage 原始链路和 SuperAgent dispatch 运行记录 + +## 3. 清理范围 + +清理目标为旧任务主模型和直接依赖旧 task 的开发测试数据: + +- `workflow_reservation_opera_operation_attempt` +- `workflow_reservation_opera_operation` +- `workflow_reservation_audit_log` 中 `task_id` 指向旧任务的审计行 +- `workflow_reservation_invoice_generation.task_id` 指向旧任务时置空,不删除生成记录 +- `workflow_reservation_task_card` +- `workflow_reservation_task` +- `workflow_reservation_order.created_from_task_id` 指向旧任务时置空,不删除订单 + +`workflow_reservation_invoice_generation` 可能带可选 `task_id`。第一版只解除旧 task 引用,不默认删除催款单生成记录;如某次测试明确需要删除旧任务关联的催款单记录,应另行确认后按测试数据范围处理。 + +## 4. 执行前检查 + +先确认当前环境是开发或测试环境,并备份数据库或保留可重建脚本。 + +```sql +SELECT COUNT(*) AS legacy_task_count +FROM workflow_reservation_task; + +SELECT hotel_id, task_status, COUNT(*) AS task_count +FROM workflow_reservation_task +GROUP BY hotel_id, task_status +ORDER BY hotel_id, task_status; +``` + +如只清理单酒店测试数据,先设置酒店 ID: + +```sql +SET @hotel_id = 'HOTEL-TEST'; +``` + +## 5. 清理 SQL + +```sql +START TRANSACTION; + +CREATE TEMPORARY TABLE tmp_dev_legacy_reservation_task_ids ( + id BIGINT PRIMARY KEY +); + +INSERT INTO tmp_dev_legacy_reservation_task_ids (id) +SELECT id +FROM workflow_reservation_task +WHERE hotel_id = @hotel_id; + +DELETE attempt +FROM workflow_reservation_opera_operation_attempt attempt +JOIN tmp_dev_legacy_reservation_task_ids legacy_task + ON legacy_task.id = attempt.task_id +WHERE attempt.hotel_id = @hotel_id; + +DELETE operation_row +FROM workflow_reservation_opera_operation operation_row +JOIN tmp_dev_legacy_reservation_task_ids legacy_task + ON legacy_task.id = operation_row.task_id +WHERE operation_row.hotel_id = @hotel_id; + +DELETE audit_row +FROM workflow_reservation_audit_log audit_row +JOIN tmp_dev_legacy_reservation_task_ids legacy_task + ON legacy_task.id = audit_row.task_id +WHERE audit_row.hotel_id = @hotel_id; + +UPDATE workflow_reservation_invoice_generation invoice_row +JOIN tmp_dev_legacy_reservation_task_ids legacy_task + ON legacy_task.id = invoice_row.task_id +SET invoice_row.task_id = NULL, + invoice_row.updated_at = UTC_TIMESTAMP(6) +WHERE invoice_row.hotel_id = @hotel_id; + +DELETE card +FROM workflow_reservation_task_card card +JOIN tmp_dev_legacy_reservation_task_ids legacy_task + ON legacy_task.id = card.task_id +WHERE card.hotel_id = @hotel_id; + +UPDATE workflow_reservation_order order_row +JOIN tmp_dev_legacy_reservation_task_ids legacy_task + ON legacy_task.id = order_row.created_from_task_id +SET order_row.created_from_task_id = NULL, + order_row.updated_at = UTC_TIMESTAMP(6) +WHERE order_row.hotel_id = @hotel_id; + +DELETE task +FROM workflow_reservation_task task +JOIN tmp_dev_legacy_reservation_task_ids legacy_task + ON legacy_task.id = task.id +WHERE task.hotel_id = @hotel_id; + +DROP TEMPORARY TABLE tmp_dev_legacy_reservation_task_ids; + +COMMIT; +``` + +如果确认要清理所有开发 / 测试酒店的旧任务,可去掉上述 SQL 中所有 `hotel_id = @hotel_id` 条件。不要在生产环境执行这种全量清理。 + +## 6. 执行后验证 + +单酒店清理时使用以下验证 SQL: + +```sql +SELECT COUNT(*) AS legacy_task_count +FROM workflow_reservation_task +WHERE hotel_id = @hotel_id; + +SELECT COUNT(*) AS legacy_task_card_count +FROM workflow_reservation_task_card +WHERE hotel_id = @hotel_id; + +SELECT COUNT(*) AS legacy_opera_operation_count +FROM workflow_reservation_opera_operation +WHERE hotel_id = @hotel_id; + +SELECT COUNT(*) AS legacy_opera_attempt_count +FROM workflow_reservation_opera_operation_attempt +WHERE hotel_id = @hotel_id; + +SELECT COUNT(*) AS legacy_invoice_task_link_count +FROM workflow_reservation_invoice_generation +WHERE hotel_id = @hotel_id + AND task_id IS NOT NULL; +``` + +如果执行的是全量开发 / 测试酒店清理,验证时同步去掉 `hotel_id = @hotel_id` 条件,确认全库旧任务及直接依赖已清空或解绑: + +```sql +SELECT COUNT(*) AS legacy_task_count +FROM workflow_reservation_task; + +SELECT COUNT(*) AS legacy_task_card_count +FROM workflow_reservation_task_card; + +SELECT COUNT(*) AS legacy_opera_operation_count +FROM workflow_reservation_opera_operation; + +SELECT COUNT(*) AS legacy_opera_attempt_count +FROM workflow_reservation_opera_operation_attempt; + +SELECT COUNT(*) AS legacy_invoice_task_link_count +FROM workflow_reservation_invoice_generation +WHERE task_id IS NOT NULL; +``` + +订单列表验证口径: + +- `GET /api/reservation/orders` 中新 V4 订单的 `open_work_item_count` 只看 V4 未完成订单任务。 +- 清理后,新 V4 订单不应再返回旧 `next_processable_task_id`。 +- `platform_source_message_inbox`、V4 order task、V4 cards、V4 source notification、目录、用户权限和酒店配置应保持不变。 diff --git a/docs/project/requirements/M002-v4-agent-callback-field-contract.md b/docs/project/requirements/M002-v4-agent-callback-field-contract.md index 634ecd5..410fbab 100644 --- a/docs/project/requirements/M002-v4-agent-callback-field-contract.md +++ b/docs/project/requirements/M002-v4-agent-callback-field-contract.md @@ -14,9 +14,9 @@ 本文把 2026-07-18 导入的业务基线、Agent 回调问题答复、草案审查答复和剩余 10 项确认回复,整理为 M002 V4 的 Agent 回调字段契约。 -本契约用于后续 M002 V4 主流程设计、后端领域建模、前端页面模型、Adapter / MCP Schema 对齐和 SuperAgent 联调。当前后端已按本文完成 V4 入站解析基线:能识别 V4 包、校验关键契约、保存 AI transition / 任务卡原始 payload,并把可映射的六类 event 先接入现有订单任务链路。 +本契约用于后续 M002 V4 主流程设计、后端领域建模、前端页面模型、Adapter / MCP Schema 对齐和 SuperAgent 联调。当前后端已按本文完成 V4 入站解析和多卡模型基线:能识别 V4 包、校验关键契约、保存 AI transition / 任务卡原始 payload,并把可映射的六类 event 写入 V4 订单任务和任务卡模型。 -V4 订单任务与多卡领域模型的 CP2 设计已经单独落到 `M002-v4-order-task-card-domain-model-cp2.md`。截至 CP14,表结构、Entity、Mapper、Repository、SuperAgent V4 入站写入、V4 查询、普通卡片确认、S10/S99 ack、V4 复核解阻、数据库目录、Account / Room Type / Rate Code Lookup API、目录管理后台 CP1 后端接口,以及订单列表 V4 继续处理入口字段已实现。真实 PMS 同步仍后置,方案见 `M002-v4-real-catalog-lookup-api-design.md`。 +V4 订单任务与多卡领域模型的 CP2 设计已经单独落到 `M002-v4-order-task-card-domain-model-cp2.md`。截至 CP14 和停止旧任务双写 checkpoint,表结构、Entity、Mapper、Repository、SuperAgent V4 入站写入、V4 查询、普通卡片确认、S10/S99 ack、V4 复核解阻、数据库目录、Account / Room Type / Rate Code Lookup API、目录管理后台 CP1 后端接口、订单列表 V4 继续处理入口字段,以及 V4 普通业务不再创建旧 `workflow_reservation_task` 已实现。真实 PMS 同步仍后置,方案见 `M002-v4-real-catalog-lookup-api-design.md`。 当前已确认开发阶段数据可以清空,因此 M002 V4 后续可以按新模型重建,不要求兼容旧任务数据、旧草稿、旧 OPERA 模拟、旧 `S000/S999`、旧 Fallback 或旧 `case_keys`。 diff --git a/docs/project/requirements/M002-v4-order-task-card-domain-model-cp2.md b/docs/project/requirements/M002-v4-order-task-card-domain-model-cp2.md index d77014c..b380aa3 100644 --- a/docs/project/requirements/M002-v4-order-task-card-domain-model-cp2.md +++ b/docs/project/requirements/M002-v4-order-task-card-domain-model-cp2.md @@ -4,19 +4,19 @@ | 项目 | 内容 | | --- | --- | -| 文档版本 | 0.7 | +| 文档版本 | 0.8 | | 日期 | 2026-07-20 | -| 状态 | CP2 设计已确认;CP3-CP8、CP11、CP13、CP14 和 V4 业务审计查询已实现 | +| 状态 | CP2 设计已确认;CP3-CP8、CP11、CP13、CP14、V4 业务审计查询和停止 V4 普通业务双写旧任务已实现 | | 适用范围 | M002 V4 入站后的订单任务、多卡、状态、查询和写操作设计 | -| 不适用范围 | V4 复核接口、真实 PMS / OPERA / OHIP、前端页面视觉稿、历史数据迁移 | +| 不适用范围 | 真实 PMS / OPERA / OHIP、前端页面视觉稿、生产历史数据迁移 | ## 1. 文档定位 -M002 V4 CP1 已完成 SuperAgent V4 回调包入站解析、基础校验、路由适配和 AI transition 最小落库。CP1 仍然把可映射的 V4 event 临时接入 M002 V3 的订单 / 任务 / 任务卡链路。 +M002 V4 CP1 已完成 SuperAgent V4 回调包入站解析、基础校验、路由适配和 AI transition 最小落库。早期 CP1 曾把可映射的 V4 event 临时接入 M002 V3 的订单 / 任务 / 任务卡链路;开发阶段最新决策已停止 V4 普通业务入站双写旧 `workflow_reservation_task`。 本文是 CP2 设计文档,用于把 2026-07-18 V4 字段契约落成后续可开发的数据模型和接口草案。 -截至 CP14 和 V4 业务审计查询补齐,后端已实现本文第 10、11、12 节中的持久化和查询基线,并已把 SuperAgent V4 入站结果写入新表:普通业务包创建 V4 订单任务、来源邮件展示卡、Basic Information 卡和业务卡;V4 S10/S99 创建来源通知。当前已开放 V4 工作台、订单任务列表 / 详情、来源通知详情查询接口、订单详情 V4 订单任务时间线、V4 卡片确认接口、S10/S99 来源通知 ack 接口、V4 `REVIEW_REQUIRED` 卡复核解阻接口、V4 订单任务 / 来源通知审计查询接口、当前酒店数据库目录校验、卡片 `fields[]` 白名单、Account / Room Type / Rate Code lookup API、目录管理后台 CP1,以及订单列表 V4 继续处理入口字段。真实 PMS 同步继续后置,设计见 `M002-v4-real-catalog-lookup-api-design.md`。 +截至 CP14、V4 业务审计查询和停止旧任务双写补齐,后端已实现本文第 10、11、12 节中的持久化和查询基线,并已把 SuperAgent V4 入站结果写入新表:普通业务包只创建 V4 订单任务、来源邮件展示卡、Basic Information 卡和业务卡,不再创建旧 `workflow_reservation_task`;V4 S10/S99 创建来源通知。当前已开放 V4 工作台、订单任务列表 / 详情、来源通知详情查询接口、订单详情 V4 订单任务时间线、V4 卡片确认接口、S10/S99 来源通知 ack 接口、V4 `REVIEW_REQUIRED` 卡复核解阻接口、V4 订单任务 / 来源通知审计查询接口、当前酒店数据库目录校验、卡片 `fields[]` 白名单、Account / Room Type / Rate Code lookup API、目录管理后台 CP1,以及订单列表 V4 继续处理入口字段。真实 PMS 同步继续后置,设计见 `M002-v4-real-catalog-lookup-api-design.md`。 后续如本文与 `M002-v4-agent-callback-field-contract.md` 的字段契约冲突,以字段契约为准;如与安全边界冲突,以 `security-access-control-boundary.md` 为准。 @@ -27,7 +27,7 @@ M002 V4 CP1 已完成 SuperAgent V4 回调包入站解析、基础校验、路 | 入站识别 | 已识别 `route_code`、`source_message`、`order_contexts[]`、`message_events[]` | CP4 已把有合法 event 的 `order_ref` 建成订单任务聚合;CP5 已开放 V4 安全查询接口 | | SourceMessage | 已按 `source_message.source_message_id` 反查 SourceMessage Inbox | CP4 已固定生成普通业务包内邮件展示卡;邮件正文完整读取仍走 SourceMessage 会话接口 | | Basic Information | 已写入 V4 Basic Information 独立卡 | CP6 已支持确认并锁定;CP7 已支持复核解阻;CP8 已支持目录校验和 `fields[]` 白名单;CP11 已改为按当前酒店数据库 Account 目录校验并派生 Market / Source | -| 业务 Event | 可映射 event 临时创建旧 `workflow_reservation_task`,并已额外创建 V4 业务卡 | 旧任务链路仍作前端过渡兼容,后续 V4 查询和写接口完成后再逐步废弃 | +| 业务 Event | V4 新业务主线已只创建 V4 order task / cards,不再双写旧 `workflow_reservation_task` | 旧 V2/V3 入站代码仍可作为历史参考保留,但开发阶段不维护旧任务兼容,测试数据可重建 | | 技术错误 | 已落 `adapter_contract_error` transition | 已符合目标方向:不创建用户可处理卡 | | 草稿 / READY / OPERA | 仍复用 V3 草稿、READY 和 OPERA 模拟骨架 | V4 新数据确认口径是不保存草稿、确认后锁定、当前不生成 OPERA | | 前端查询 | 复用旧任务列表和任务详情 | CP5 已开放 V4 工作台、订单任务列表 / 详情、来源通知详情和订单详情 V4 时间线 | @@ -661,7 +661,15 @@ GET /api/reservation/orders - Basic 已确认后,业务卡中 `REVIEW_REQUIRED` 优先于普通 `PENDING_CONFIRM`。 - `next_v4_action_type=CONFIRM` 时前端调用卡片确认接口;`REVIEW` 时调用复核解阻接口;`NONE` 表示该订单没有 V4 待处理入口。 -前端订单列表“继续处理”应优先使用 `next_v4_order_task_id` 跳转 V4 订单任务详情;没有 V4 待处理入口时,再回退旧 `next_processable_task_id`。 +前端订单列表“继续处理”应优先使用 `next_v4_order_task_id` 跳转 V4 订单任务详情;没有 V4 待处理入口时,开发阶段通常不再回退旧任务入口。`next_processable_task_id` 仅作为历史 V2/V3 诊断兼容字段保留,测试数据清理后新 V4 订单不应返回该字段。 + +开发 / 测试阶段旧任务口径: + +- V4 普通业务入站不再创建旧 `workflow_reservation_task`,也不再创建旧 `workflow_reservation_task_card`、旧任务草稿、旧 OPERA 模拟操作。 +- `open_task_count` 仍表示旧任务表原始未关闭诊断计数;开发 / 测试环境应通过专项 SQL 清理旧任务及其直接依赖数据,清理后新 V4 订单列表中该值应为 0。 +- `next_processable_task_id` 仍按旧 V2/V3 队列实时计算,但开发阶段不维护旧任务兼容;清理旧任务后,新 V4 订单不应再返回旧任务入口。 +- 开发阶段不维护 V2/V3 旧任务兼容,测试数据可重建。 +- 该策略仅限开发 / 测试阶段,不代表生产迁移方案;生产数据迁移策略不在本 checkpoint 处理,后续上线前另开迁移方案。 ### 12.7 V4 业务审计查询 @@ -749,7 +757,7 @@ S10/S99 已确认采用来源通知模型,不继续复用隐藏技术订单或 ## 14. 旧 V3 / V2 兼容和废弃边界 -开发阶段已确认可以清空数据,因此后续 V4 开发可以不迁移历史任务数据。 +开发阶段已确认可以清空数据,因此后续 V4 开发可以不迁移历史任务数据。V4 后新业务主线只写 V4 模型;开发阶段不维护 V2/V3 旧任务兼容,测试数据可重建。该策略仅限开发 / 测试阶段,不代表生产迁移方案;生产数据迁移策略不在本 checkpoint 处理,后续上线前另开迁移方案。 ### 14.1 可继续保留 @@ -776,7 +784,7 @@ S10/S99 已确认采用来源通知模型,不继续复用隐藏技术订单或 - `workflow_reservation_task` 作为 V4 多卡主模型的直接承载方式。 - 旧 V2/V3 任务详情、草稿保存和最终确认接口,待 V4 新模型落地并完成前端切换后逐步废弃。 -废弃前必须先确认前端、MCP、SuperAgent 联调方和测试 fixture 都已切到 V4。 +开发 / 测试环境旧任务清理参考 `../operations/dev-test-v4-legacy-task-cleanup.md`。废弃或删除生产历史链路前,必须先确认前端、MCP、SuperAgent 联调方和测试 fixture 都已切到 V4,并单独制定生产迁移方案。 ## 15. 安全、权限和审计 diff --git a/docs/project/security-access-control-boundary.md b/docs/project/security-access-control-boundary.md index 89d17b3..b015cd4 100644 --- a/docs/project/security-access-control-boundary.md +++ b/docs/project/security-access-control-boundary.md @@ -109,7 +109,7 @@ | `POST /api/ai-query/v1/object-detail` | `THIRD_PARTY_SUPERAGENT` | HMAC 鉴权 | 保持 HMAC;返回最小必要上下文 | 记录请求 ID、client_id 和安全错误 | | `POST /api/ai-query/v1/conversation-tasks` | `THIRD_PARTY_SUPERAGENT` | HMAC 鉴权 | 保持 HMAC;不返回邮件原文 | 记录请求 ID、client_id 和安全错误 | | `POST /api/ai-query/v1/conversation-source` | `THIRD_PARTY_SUPERAGENT` | HMAC 鉴权 | 保持 HMAC;只按契约返回需要字段 | 记录请求 ID、client_id 和安全错误 | -| `POST /api/integrations/superagent/task-results` | `THIRD_PARTY_SUPERAGENT` | HMAC + nonce + timestamp + body hash | 保持;V4 / V3 / V2 共存期均必须用外部 `source_message_id` 匹配 Inbox,技术契约错误只落 adapter error,不创建用户可处理任务;V4 普通业务包额外写入 V4 订单任务 / 多卡模型,V4 S10/S99 写入来源通知且不创建旧任务 | 记录 batch、transition、adapter error、幂等结果和安全错误 | +| `POST /api/integrations/superagent/task-results` | `THIRD_PARTY_SUPERAGENT` | HMAC + nonce + timestamp + body hash | 保持;V4 / V3 / V2 共存期均必须用外部 `source_message_id` 匹配 Inbox,技术契约错误只落 adapter error,不创建用户可处理任务;V4 普通业务包只写 V4 订单任务 / 多卡模型,不再创建旧 `workflow_reservation_task`;V4 S10/S99 写入来源通知且不创建旧任务 | 记录 batch、transition、adapter error、幂等结果和安全错误 | | `/mcp` | `THIRD_PARTY_MCP` | Bearer Token;提交工具可独立开关 | 保持;工具级能力限制,不暴露无关接口 | 记录工具调用结果和业务入站结果 | | AgentBus WebSocket | `THIRD_PARTY_AGENTBUS` | AgentBus Token + capture 开关 | 保持;只入 SourceMessage,不直接建业务任务 | 记录 SourceMessage、payload hash 和 dispatch run | diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/dto/ReservationV4AcceptedEventDraft.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/dto/ReservationV4AcceptedEventDraft.java index 3b865a4..332955e 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/dto/ReservationV4AcceptedEventDraft.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/dto/ReservationV4AcceptedEventDraft.java @@ -11,7 +11,7 @@ public record ReservationV4AcceptedEventDraft( Integer sourceEventIndex, /** 当前 event 对应的 AI transition ID。 */ Long aiTransitionId, - /** 旧过渡链路已经解析出的本地订单投影 ID,V4 第一版复用该绑定。 */ + /** 入站阶段解析出的本地订单投影 ID,V4 订单任务复用该绑定。 */ Long orderId, /** 目标订单业务类型,例如 GROUP、FIT。 */ String targetBookingType, diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/SuperAgentTaskResultItemResponse.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/SuperAgentTaskResultItemResponse.java index c4c1b6b..27587c0 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/SuperAgentTaskResultItemResponse.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/SuperAgentTaskResultItemResponse.java @@ -8,16 +8,16 @@ import com.fasterxml.jackson.annotation.JsonProperty; * @param sourceEventIndex AI 原始事件序号 * @param arrayIndex 当前批次中的处理顺序 * @param aiTransitionId AI 过渡记录 ID - * @param routeCode M002 V3 稳定路由码,S10/S99 直接使用外部路由码 + * @param routeCode M002 V3 / V4 稳定路由码,S10/S99 直接使用外部路由码 * @param systemProcessCategory 系统处理分类,用于区分业务任务、来源邮件通知和契约错误 * @param adapterErrorCode Adapter 契约错误代码,正常创建任务时为空 * @param orderId 关联订单 ID;仅落契约错误时为空 - * @param taskId 关联任务 ID;仅落契约错误时为空 + * @param taskId 旧任务 ID;V4 普通业务和契约错误场景为空 * @param systemTaskType 系统主任务类型 * @param taskCardType 任务卡类型 - * @param taskStatus 任务状态;仅落契约错误时为空 + * @param taskStatus 旧任务状态;V4 普通业务和契约错误场景为空 * @param orderStatus 订单状态;仅落契约错误时为空 - * @param executionOrder 同订单任务执行顺序;仅落契约错误时为空 + * @param executionOrder 旧同订单任务执行顺序;V4 普通业务和契约错误场景为空 */ public record SuperAgentTaskResultItemResponse( @JsonProperty("source_event_index") diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationAiTaskIntakeServiceImpl.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationAiTaskIntakeServiceImpl.java index 6f22750..7e88132 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationAiTaskIntakeServiceImpl.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationAiTaskIntakeServiceImpl.java @@ -184,7 +184,7 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta } /** - * 接收已通过 HMAC 鉴权的 SuperAgent 任务结果。先处理旧文本结果,再分流 V3 JSON 和 V2 JSON。 + * 接收已通过 HMAC 鉴权的 SuperAgent 任务结果。先处理旧文本结果,再按 V4 / V3 / V2 JSON 契约分流。 */ @Override @Transactional @@ -540,7 +540,7 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta } /** - * 接收 V4 业务根。第一版按 message_events[] 顺序派生现有任务卡,event 契约错误只落 transition。 + * 接收 V4 业务根。V4 新主线只落 AI transition 和 V4 订单任务 / 多卡模型,不再双写旧任务表。 */ private SuperAgentTaskResultResponse acceptV4BusinessRoot( JsonNode root, @@ -640,9 +640,22 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta continue; } ObjectNode item = v4CallbackRouter.normalizeEventToTaskResultItem(root, event, route, arrayIndex); - SuperAgentTaskResultItemResponse responseItem = createItem(hotelId, sourceMessageId, batchId, item, arrayIndex, now); - responseItems.add(responseItem); - acceptedEvents.add(toV4AcceptedEvent(event, route, responseItem, arrayIndex)); + V4BusinessTransitionCreation transitionCreation = createV4BusinessTransition( + hotelId, + sourceMessageId, + batchId, + item, + event, + route, + arrayIndex, + now); + responseItems.add(transitionCreation.responseItem()); + acceptedEvents.add(toV4AcceptedEvent( + event, + route, + transitionCreation.transitionId(), + transitionCreation.orderId(), + arrayIndex)); } v4TaskIntakeService.createBusinessPackageTasks(sourceMessage, batchId, root, acceptedEvents, now); @@ -1097,19 +1110,20 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta } /** - * 将已创建 transition 的 V4 event 转成新模型写入草稿,避免 V4 多卡服务反向依赖旧任务响应细节。 + * 将已创建 transition 的 V4 event 转成新模型写入草稿,避免 V4 多卡服务反向依赖旧任务模型。 */ private ReservationV4AcceptedEventDraft toV4AcceptedEvent( JsonNode event, ReservationAiRouteDefinition route, - SuperAgentTaskResultItemResponse responseItem, + Long aiTransitionId, + Long orderId, int arrayIndex) { JsonNode targetOrder = event.path("target_order"); return new ReservationV4AcceptedEventDraft( requireText(textAt(event, "order_ref"), "order_ref", LENGTH_128), arrayIndex, - longValue(responseItem.aiTransitionId()), - longValue(responseItem.orderId()), + aiTransitionId, + orderId, optionalText(textAt(targetOrder, "booking_type"), "target_order.booking_type", LENGTH_32), optionalText(textAt(targetOrder, "locator_type"), "target_order.locator_type", LENGTH_64), optionalText(textAt(targetOrder, "locator_value"), "target_order.locator_value", LENGTH_128), @@ -1120,6 +1134,140 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta ); } + /** + * 创建 V4 业务 event 的 AI transition,并解析本地订单投影;不创建旧 workflow_reservation_task。 + */ + private V4BusinessTransitionCreation createV4BusinessTransition( + String hotelId, + Long sourceMessageId, + Long batchId, + JsonNode item, + JsonNode event, + ReservationAiRouteDefinition route, + int arrayIndex, + LocalDateTime now) { + Integer sourceEventIndex = requirePositiveInt(item, "source_event_index"); + String catalogCode = requireText(textAt(item, "catalog_code"), "catalog_code", LENGTH_32); + String skillId = requireText(textAt(item, "skill_id"), "skill_id", LENGTH_128); + String resultType = requireText(textAt(item, "result_type"), "result_type", LENGTH_64); + String aiTaskType = requireText(textAt(item, "task_type"), "task_type", LENGTH_64); + String taskSubtype = optionalText(textAt(item, "task_subtype"), "task_subtype", LENGTH_128); + String routeCode = optionalText(textAt(item, "route_code"), "route_code", LENGTH_64); + String systemProcessCategory = optionalText( + textAt(item, "system_process_category"), + "system_process_category", + LENGTH_64); + if (systemProcessCategory == null) { + systemProcessCategory = route.systemProcessCategory().name(); + } + JsonNode caseKeys = item.path("case_keys"); + String currentOrHistory = optionalText(textAt(item, "current_or_history"), "current_or_history", LENGTH_32); + String groupCode = optionalText(textAt(caseKeys, "group_code"), "group_code", LENGTH_128); + String confirmationNumber = optionalText(textAt(caseKeys, "confirmation_number"), "confirmation_number", LENGTH_128); + String manualReasonCode = optionalText(textAt(item.path("manual_review"), "reason_code"), "manual_review.reason_code", LENGTH_128); + String itemPayloadJson = nodeJson(item); + String itemPayloadSha256 = sha256(itemPayloadJson); + String itemIdempotencyKey = sha256(ITEM_KEY_PREFIX + + "|" + sourceMessageId + + "|" + sourceEventIndex + + "|" + arrayIndex + + "|" + catalogCode + + "|" + skillId + + "|" + resultType + + "|" + aiTaskType + + "|" + nullToEmpty(taskSubtype) + + "|" + itemPayloadSha256); + Long transitionId = workflowRepository.insertTransition(new ReservationAiTransitionDraft( + hotelId, + batchId, + sourceMessageId, + sourceEventIndex, + arrayIndex, + arrayIndex, + catalogCode, + skillId, + resultType, + aiTaskType, + routeCode, + systemProcessCategory, + route.systemTaskType().name(), + route.taskCardType().name(), + taskSubtype, + currentOrHistory, + groupCode, + confirmationNumber, + itemPayloadSha256, + itemIdempotencyKey, + manualReasonCode, + null, + null, + null, + itemPayloadJson, + nodeJson(item.get("case_keys")), + nodeJson(item.get("extracted_fields")), + nodeJson(item.get("manual_review")), + null, + nodeJson(item.get("attachments")), + null, + null, + null, + now + )); + ReservationOrderSnapshot order = resolveV4LocalOrder(hotelId, sourceMessageId, event, arrayIndex, now); + SuperAgentTaskResultItemResponse responseItem = new SuperAgentTaskResultItemResponse( + sourceEventIndex, + arrayIndex, + transitionId.toString(), + routeCode, + systemProcessCategory, + null, + order == null ? null : order.id().toString(), + null, + route.systemTaskType().name(), + route.taskCardType().name(), + null, + order == null ? null : order.orderStatus(), + null + ); + return new V4BusinessTransitionCreation( + transitionId, + order == null ? null : order.id(), + responseItem); + } + + /** + * V4 业务包只在当前能可靠定位 Group Code / Confirmation Number 时绑定本地订单。 + */ + private ReservationOrderSnapshot resolveV4LocalOrder( + String hotelId, + Long sourceMessageId, + JsonNode event, + int arrayIndex, + LocalDateTime now) { + JsonNode targetOrder = event == null ? null : event.path("target_order"); + String locatorType = trimToNull(textAt(targetOrder, "locator_type")); + String locatorValue = optionalText(textAt(targetOrder, "locator_value"), "target_order.locator_value", LENGTH_128); + BusinessKey businessKey = selectV4BusinessKey(locatorType, locatorValue); + if (businessKey == null) { + return null; + } + return workflowRepository.findActiveOrderByBusinessKey(hotelId, businessKey.keyType().name(), businessKey.value()) + .orElseGet(() -> createActiveOrder(hotelId, sourceMessageId, businessKey, arrayIndex, now)); + } + + /** + * V4 第一版只把 Group Code 和 Confirmation Number 作为可自动落订单的业务号。 + */ + private BusinessKey selectV4BusinessKey(String locatorType, String locatorValue) { + if ("GROUP_CODE".equals(locatorType) && hasText(locatorValue)) { + return new BusinessKey(ReservationOrderKeyType.GROUP_CODE, locatorValue); + } + if ("CONFIRMATION_NUMBER".equals(locatorType) && hasText(locatorValue)) { + return new BusinessKey(ReservationOrderKeyType.CONFIRMATION_NUMBER, locatorValue); + } + return null; + } + /** * 生成 V4 业务卡 AI 原始片段,保留 event 原文和系统派生路由,不复制整封邮件正文。 */ @@ -1623,7 +1771,7 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta true, existingBatch.itemCount() == null ? 0 : existingBatch.itemCount(), List.of(), - List.of(new SuperAgentTaskResultWarning("IDEMPOTENT_REPLAY", "相同请求已经处理,本次未重复创建任务。")) + List.of(new SuperAgentTaskResultWarning("IDEMPOTENT_REPLAY", "相同请求已经处理,本次未重复创建业务对象。")) ); } @@ -3011,6 +3159,16 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta ) { } + /** + * V4 业务 event 的 transition 写入结果;V4 新主线不再携带旧 task ID。 + */ + private record V4BusinessTransitionCreation( + Long transitionId, + Long orderId, + SuperAgentTaskResultItemResponse responseItem + ) { + } + /** * 任务创建结果,包含任务 ID 和最终写入的同订单执行序号。 */ diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationAiV4CallbackRouter.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationAiV4CallbackRouter.java index 50829a3..b5e4fed 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationAiV4CallbackRouter.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationAiV4CallbackRouter.java @@ -7,7 +7,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; /** - * V4 SuperAgent 回调路由器。负责把 V4 event 映射到当前系统稳定 route 和旧任务创建链路 item。 + * V4 SuperAgent 回调路由器。负责把 V4 event 映射到当前系统稳定 route 和入站 transition item。 */ final class ReservationAiV4CallbackRouter { @@ -41,7 +41,7 @@ final class ReservationAiV4CallbackRouter { } /** - * 将 V4 event 归一为当前任务创建链路可消费的 item,并保留 V4 原始结构。 + * 将 V4 event 归一为当前入站 transition 可消费的 item,并保留 V4 原始结构。 */ ObjectNode normalizeEventToTaskResultItem( JsonNode root, @@ -70,7 +70,7 @@ final class ReservationAiV4CallbackRouter { } /** - * 将 V4 target_order 映射为旧任务链路可识别的 case_keys,同时保留 Booking Code。 + * 将 V4 target_order 映射为 transition 查询摘要可识别的 case_keys,同时保留 Booking Code。 */ ObjectNode buildCaseKeys(JsonNode targetOrder) { ObjectNode caseKeys = objectMapper.createObjectNode(); diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationDemoDataServiceImpl.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationDemoDataServiceImpl.java index d756814..8caa25c 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationDemoDataServiceImpl.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationDemoDataServiceImpl.java @@ -502,7 +502,7 @@ public class ReservationDemoDataServiceImpl implements ReservationDemoDataServic } /** - * 调用 AI 入站服务创建订单、任务和任务卡。 + * 调用历史 AI 入站服务创建 V2/V3 演示订单、任务和任务卡;V4 smoke 不使用该入口造数。 */ private SuperAgentTaskResultResponse intake(String rawBody, String demoRunId, String scenarioCode) { SuperAgentTaskResultResponse response = intakeService.accept( diff --git a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationFrontendQueryControllerTest.java b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationFrontendQueryControllerTest.java index fa519b9..a63a44b 100644 --- a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationFrontendQueryControllerTest.java +++ b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationFrontendQueryControllerTest.java @@ -877,6 +877,8 @@ class ReservationFrontendQueryControllerTest { .value(contains(0))) .andExpect(jsonPath("$.items[?(@.order_id=='" + completedOnlyOrderId + "')].next_v4_order_task_id") .value(contains(nullValue()))) + .andExpect(jsonPath("$.items[?(@.order_id=='" + completedOnlyOrderId + "')].next_processable_task_id") + .value(contains(nullValue()))) .andExpect(jsonPath("$.items[?(@.order_id=='" + completedOnlyOrderId + "')].next_v4_action_type") .value(contains("NONE"))) .andExpect(jsonPath("$.items[?(@.order_id=='" + completedOnlyOrderId + "')].next_v4_action_status") diff --git a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java index c7b47b1..0358082 100644 --- a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java +++ b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java @@ -990,7 +990,7 @@ class SuperAgentTaskResultControllerTest { } @Test - void shouldCreateBusinessTasksFromV4OrderContextsAndMessageEventsInOrder() throws Exception { + void shouldCreateV4OrderTaskAndCardsWithoutLegacyTaskDualWrite() throws Exception { SourceMessageCaptureResult source = captureSourceMessage("mail-v4-business-root-001"); String body = v4BusinessRootBody("mail-v4-business-root-001"); @@ -1003,13 +1003,17 @@ class SuperAgentTaskResultControllerTest { .andExpect(jsonPath("$.items[0].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL")) .andExpect(jsonPath("$.items[0].system_task_type").value("NEW_BOOKING")) .andExpect(jsonPath("$.items[0].task_card_type").value("NEW_BOOKING")) - .andExpect(jsonPath("$.items[0].execution_order").value(1)) + .andExpect(jsonPath("$.items[0].task_id").doesNotExist()) + .andExpect(jsonPath("$.items[0].task_status").doesNotExist()) + .andExpect(jsonPath("$.items[0].execution_order").doesNotExist()) .andExpect(jsonPath("$.items[1].source_event_index").value(2)) .andExpect(jsonPath("$.items[1].array_index").value(2)) .andExpect(jsonPath("$.items[1].route_code").value("R10_PAYMENT_EVIDENCE_NORMAL")) .andExpect(jsonPath("$.items[1].system_task_type").value("UPDATE_BOOKING")) .andExpect(jsonPath("$.items[1].task_card_type").value("PAYMENT_EVIDENCE")) - .andExpect(jsonPath("$.items[1].execution_order").value(2)) + .andExpect(jsonPath("$.items[1].task_id").doesNotExist()) + .andExpect(jsonPath("$.items[1].task_status").doesNotExist()) + .andExpect(jsonPath("$.items[1].execution_order").doesNotExist()) .andReturn(); String firstOrderId = com.jayway.jsonpath.JsonPath.read( @@ -1069,6 +1073,12 @@ class SuperAgentTaskResultControllerTest { ) """, Long.class, source.inboxId()); assertThat(v4CardStatusCount).isEqualTo(4L); + Long legacyTaskCount = jdbcTemplate.queryForObject(""" + SELECT COUNT(*) + FROM workflow_reservation_task + WHERE source_message_id = ? + """, Long.class, source.inboxId()); + assertThat(legacyTaskCount).isZero(); String sourceDisplayPayload = jdbcTemplate.queryForObject(""" SELECT display_payload_json @@ -1115,8 +1125,8 @@ class SuperAgentTaskResultControllerTest { mockMvc.perform(signedPost(body, "nonce-v4-two-order-refs-001")) .andExpect(status().isCreated()) .andExpect(jsonPath("$.accepted_count").value(2)) - .andExpect(jsonPath("$.items[0].execution_order").value(1)) - .andExpect(jsonPath("$.items[1].execution_order").value(1)); + .andExpect(jsonPath("$.items[0].execution_order").doesNotExist()) + .andExpect(jsonPath("$.items[1].execution_order").doesNotExist()); List orderRefs = jdbcTemplate.queryForList(""" SELECT order_ref @@ -1148,7 +1158,8 @@ class SuperAgentTaskResultControllerTest { mockMvc.perform(signedPost(body, "nonce-v4-fit-booking-code-001")) .andExpect(status().isCreated()) .andExpect(jsonPath("$.accepted_count").value(1)) - .andExpect(jsonPath("$.items[0].order_status").value("TEMPORARY")); + .andExpect(jsonPath("$.items[0].order_id").doesNotExist()) + .andExpect(jsonPath("$.items[0].order_status").doesNotExist()); Long unresolvedCount = jdbcTemplate.queryForObject(""" SELECT COUNT(*) @@ -1320,15 +1331,15 @@ class SuperAgentTaskResultControllerTest { .andExpect(jsonPath("$.items[0].route_code").value("R06_CANCEL_GROUP_BLOCK_NORMAL")) .andExpect(jsonPath("$.items[0].system_task_type").value("CANCEL_BOOKING")) .andExpect(jsonPath("$.items[0].task_card_type").value("CANCEL_BOOKING")) - .andExpect(jsonPath("$.items[0].execution_order").value(1)) + .andExpect(jsonPath("$.items[0].execution_order").doesNotExist()) .andExpect(jsonPath("$.items[1].route_code").value("R14_GENERAL_REQUEST_NORMAL")) .andExpect(jsonPath("$.items[1].system_task_type").value("UPDATE_BOOKING")) .andExpect(jsonPath("$.items[1].task_card_type").value("TRACE_RESERVATION_NOTES")) - .andExpect(jsonPath("$.items[1].execution_order").value(2)) + .andExpect(jsonPath("$.items[1].execution_order").doesNotExist()) .andExpect(jsonPath("$.items[2].route_code").value("R11_ROOMING_LIST_NORMAL")) .andExpect(jsonPath("$.items[2].system_task_type").value("UPDATE_BOOKING")) .andExpect(jsonPath("$.items[2].task_card_type").value("ROOMING_LIST")) - .andExpect(jsonPath("$.items[2].execution_order").value(3)) + .andExpect(jsonPath("$.items[2].execution_order").doesNotExist()) .andReturn(); String firstOrderId = com.jayway.jsonpath.JsonPath.read( @@ -1554,8 +1565,8 @@ class SuperAgentTaskResultControllerTest { .andExpect(jsonPath("$.items[0].adapter_error_code").value("PAYMENT_ATTACHMENT_ID_NOT_FOUND")) .andExpect(jsonPath("$.items[0].task_id").doesNotExist()) .andExpect(jsonPath("$.items[1].route_code").value("R06_CANCEL_GROUP_BLOCK_NORMAL")) - .andExpect(jsonPath("$.items[1].task_id").exists()) - .andExpect(jsonPath("$.items[1].execution_order").value(1)); + .andExpect(jsonPath("$.items[1].task_id").doesNotExist()) + .andExpect(jsonPath("$.items[1].execution_order").doesNotExist()); Long taskCount = jdbcTemplate.queryForObject(""" SELECT COUNT(*) @@ -1569,7 +1580,7 @@ class SuperAgentTaskResultControllerTest { AND system_process_category = 'ADAPTER_CONTRACT_ERROR' AND adapter_error_code = 'PAYMENT_ATTACHMENT_ID_NOT_FOUND' """, Long.class, source.inboxId()); - assertThat(taskCount).isEqualTo(1L); + assertThat(taskCount).isZero(); assertThat(adapterErrorCount).isEqualTo(1L); List v4CardTypes = jdbcTemplate.queryForList("""