修复 V4 查询接口安全与筛选问题
This commit is contained in:
@@ -54,9 +54,9 @@
|
||||
| `POST /api/auth/logout` | 登出当前 session | 带 `Authorization: Bearer <access_token>`;成功后前端必须清理本地 token 和当前用户上下文。 |
|
||||
| `GET /api/reservation/orders` | 查询订单列表 | 必须带 `Authorization: Bearer <access_token>`,需要 `RESERVATION_ORDER_READ`;默认返回全部订单状态;按后端维护的订单最近业务活动时间倒序,当前落库字段为 `workflow_reservation_order.latest_activity_at`,前端不要自行重排;`open_task_count` 排除 `COMPLETED` 和 `FAILED`;隐藏技术订单不返回,因此 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` 倒序;列表不返回邮件正文、附件 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`;前端用 `availability`、`card_counts` 和 `next_action_card_id` 控制入口。 |
|
||||
| `GET /api/reservation/order-tasks/{orderTaskId}` | 查询 V4 订单任务详情 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`,后端按订单任务实际酒店校验访问权;返回 `order_task`、`source_message_summary`、`source_message_card`、`basic_information_card`、`business_cards[]`、`card_counts`、`adapter_contract_errors[]` 和 `availability`;邮件正文和附件仍走 SourceMessage 会话接口。 |
|
||||
| `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` 只筛业务 / 可处理卡,固定来源邮件展示卡不参与筛选。 |
|
||||
| `GET /api/reservation/order-tasks/{orderTaskId}` | 查询 V4 订单任务详情 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`,后端按订单任务实际酒店校验访问权;返回 `order_task`、`source_message_summary`、`source_message_card`、`basic_information_card`、`business_cards[]`、`card_counts`、`adapter_contract_errors[]` 和 `availability`;来源摘要按酒店过滤,邮件正文和附件仍走 SourceMessage 会话接口。 |
|
||||
| `GET /api/reservation/source-notifications/{notificationId}` | 查询 V4 S10/S99 来源通知详情 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`,后端按来源通知实际酒店校验访问权;只返回通知摘要、来源邮件通知卡、会话摘要和 `availability`;不返回订单任务、业务卡、邮件正文、附件 URL 或原始 AI payload。 |
|
||||
| `GET /api/reservation/orders/{orderId}` | 查询订单详情与任务时间线 | 必须带 Bearer token,需要 `RESERVATION_ORDER_READ`,后端按订单所属酒店做访问校验;`include_tasks=false` 可只取订单摘要;时间线按后端队列顺序返回,前端不要自行按创建时间重排;`tasks[]` 已返回来源邮件会话摘要字段和 V3 路由字段;隐藏技术订单详情不可作为普通订单页打开。 |
|
||||
| `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`。 |
|
||||
@@ -92,9 +92,9 @@
|
||||
| `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 解析失败。 |
|
||||
| `GET /api/reservation/workbench-items` / `/api/reservation/order-tasks/**` / `/api/reservation/source-notifications/{notificationId}` | 新增 M002 V4 CP5 查询接口。 | 这是 V4 新模型前端主入口;查询阶段只读,`confirmable=false`、`ackable=false` 表示写接口还没开放,不是前端按钮 bug。前端不要继续从旧 `/api/reservation/tasks/**` 推断 V4 多卡详情。 |
|
||||
| `GET /api/reservation/workbench-items` / `/api/reservation/order-tasks/**` / `/api/reservation/source-notifications/{notificationId}` | 新增 M002 V4 CP5 查询接口。 | 这是 V4 新模型前端主入口;查询阶段只读,`confirmable=false`、`ackable=false` 表示写接口还没开放,不是前端按钮 bug。工作台条目已返回 `created_at` / `updated_at` 作为排序兜底和调试字段;前端不要继续从旧 `/api/reservation/tasks/**` 推断 V4 多卡详情。 |
|
||||
|
||||
V4 CP5 分页注意:`page_num` 从 1 开始,后端第一版安全上限为 100;`page_size` 最大 100。超出上限时后端按上限处理并在 `page.page_num` / `page.page_size` 中返回实际使用值。
|
||||
V4 CP5 分页注意:`page_num` 从 1 开始,后端第一版安全上限为 100;`page_size` 最大 100。超出上限时后端按上限处理并在 `page.page_num` / `page.page_size` 中返回实际使用值。`order_task_status` 和 `card_status` 是稳定枚举查询参数,前端不要传中文文案或自造状态码。
|
||||
|
||||
酒店上下文注意:Reservation 列表、订单详情、任务列表和 Debug EML 上传的 `hotel_id` 第一版都是可选参数。对已收口的 Reservation / SourceMessage 只读接口,前端必须先登录并带 Bearer token;不传 `hotel_id` 时后端按当前登录用户默认酒店或对象所属酒店校验,传了当前选中酒店时后端会校验该酒店是否可访问。Debug EML 仍按调试入口规则受控,不属于本轮登录权限收口范围。
|
||||
|
||||
@@ -464,7 +464,7 @@ RESERVATION_ROOMING_LIST_GENERATE
|
||||
- M002 V4 CP5 已完成查询接口:普通 V4 业务包可通过 `/api/reservation/workbench-items`、`/api/reservation/order-tasks`、`/api/reservation/order-tasks/{orderTaskId}` 查看;V4 S10/S99 来源通知可通过 `/api/reservation/source-notifications/{notificationId}` 查看。卡片确认、复核和 S10/S99 ack 写接口仍未开放。
|
||||
- V4 任务卡的 `display_payload_json` 只保留后端白名单展示字段;`ai_payload_json` 才包含完整 SuperAgent 原始 event。后续 V4 查询接口不得把 `ai_payload_json`、附件 URL 或 raw evidence 直接给普通页面渲染。
|
||||
- V4 可映射 event 现阶段仍保留现有任务详情结构作为过渡兼容;任务详情中若出现 `field_contract_version=20260718-v4` 或 AI payload 内的 `v4_source_message`、`v4_order_context`、`v4_message_event`,前端第一版只读展示即可,不要据此假定完整 V4 多卡页面已经完成。
|
||||
- V4 `PAYMENT.attachment_ids[]` 不匹配、`UPDATE_BOOKING` 携带 `rate_code` 等问题会出现在任务详情同批次的 `adapter_contract_errors[]` 只读诊断块中,不展示保存、确认、执行或重试按钮。
|
||||
- V4 `PAYMENT.attachment_ids[]` 不匹配、`UPDATE_BOOKING` 携带 `rate_code` 等问题会出现在任务详情同批次的 `adapter_contract_errors[]` 只读诊断块中,不展示保存、确认、执行或重试按钮。该字段只返回白名单诊断字段,不返回完整 AI payload、邮件正文、附件 URL 或 raw evidence。
|
||||
- V4 包级契约错误只会保存在 AI transition 中,不会出现在普通任务列表;V4 event 级契约错误如果同批次存在其它业务任务,前端仍按任务详情里的 `adapter_contract_errors[]` 只读展示诊断信息。
|
||||
- M002 V4 CP2 订单任务与多卡领域模型设计已落到 `docs/project/requirements/M002-v4-order-task-card-domain-model-cp2.md`:后续前端 V4 页面应围绕 `order_task + source_message_card + basic_information_card + business_cards[]` 设计;V4 工作台统一列表、业务订单任务列表 / 详情和 S10/S99 来源通知详情已实现。
|
||||
- M002 V4 CP3 已新增 V4 订单任务、任务卡、S10/S99 来源通知三张表和 Repository 基线;M002 V4 CP4 已把正式 V4 回调写入这些表;M002 V4 CP5 已开放查询。当前仍不代表 V4 卡片确认、复核或 S10/S99 ack API 已经可用。
|
||||
|
||||
@@ -464,11 +464,12 @@ GET /api/reservation/workbench-items
|
||||
- `order_task_status`:仅业务订单任务返回 `OPEN` / `COMPLETED`。
|
||||
- `display_status`:可返回 `OPEN` / `BLOCKED` / `COMPLETED` / `ACK_REQUIRED` / `ACKED`。
|
||||
- `readonly_reason_code`
|
||||
- `created_at` / `updated_at`:工作台条目创建和更新时间,主要用于同一来源时间下的稳定排序和前端调试。
|
||||
|
||||
排序规则:
|
||||
|
||||
- 默认按 `source_received_at` 倒序。
|
||||
- 同一来源时间下按 AI batch 接收时间、记录创建时间倒序。
|
||||
- 同一来源时间下按记录 `updated_at`、`created_at`、数字 ID 倒序。
|
||||
|
||||
### 12.2 业务订单任务列表
|
||||
|
||||
@@ -488,6 +489,12 @@ GET /api/reservation/order-tasks
|
||||
| `keyword` | 可选,匹配邮件主题、发件人、Group Code、Booking Code、Confirmation Number |
|
||||
| `page_num` / `page_size` | 分页 |
|
||||
|
||||
筛选说明:
|
||||
|
||||
- `order_task_status` 只接受 `OPEN` / `COMPLETED`,非法值返回 `V4_ORDER_TASK_STATUS_INVALID`。
|
||||
- `card_status` 只接受 `READONLY` / `PENDING_CONFIRM` / `REVIEW_REQUIRED` / `CONFIRMED`,非法值返回 `V4_CARD_STATUS_INVALID`。
|
||||
- `card_status` 按业务 / 可处理卡筛选,固定的 `SOURCE_MESSAGE_DISPLAY` 来源邮件只读卡不参与匹配,避免 `READONLY` 把所有普通业务任务都筛出来。
|
||||
|
||||
返回摘要应包含:
|
||||
|
||||
- `order_task_id`
|
||||
@@ -530,6 +537,10 @@ GET /api/reservation/order-tasks/{orderTaskId}
|
||||
|
||||
前端应以返回的 `cards[]` 和 `availability` 为准渲染,不自行拼完整字段矩阵。
|
||||
|
||||
`adapter_contract_errors[]` 返回同一 AI 批次中未生成业务卡的 V4 event 级契约错误诊断块,只包含白名单诊断字段,例如 `event_type`、`source_event_index`、`contract_errors`、`reason_code`、`missing_fields` 等;不返回完整 AI payload、邮件正文、附件 URL 或 raw evidence。
|
||||
|
||||
来源邮件摘要必须按当前查询酒店过滤。如果 V4 订单任务因脏引用指向了其它酒店的 SourceMessage,详情接口只能返回该 `source_message_id` 的空摘要占位,不得透出对方酒店的主题、外部消息 ID、发件人或会话信息。
|
||||
|
||||
### 12.4 S10/S99 来源通知详情
|
||||
|
||||
```text
|
||||
|
||||
@@ -47,9 +47,9 @@
|
||||
| `GET /api/reservation/orders` | `FRONTEND_USER` | 已强制 Bearer 登录 + `RESERVATION_ORDER_READ`;支持可选 `hotel_id` 并校验酒店访问权 | 保持登录 + `RESERVATION_ORDER_READ` + 酒店访问权 | 只读查询默认不写业务审计 |
|
||||
| `GET /api/reservation/orders/{orderId}` | `FRONTEND_USER` | 已强制 Bearer 登录 + `RESERVATION_ORDER_READ`;按订单实际所属酒店校验访问权 | 保持登录 + `RESERVATION_ORDER_READ` + 订单所属酒店访问权 | 只读查询默认不写业务审计 |
|
||||
| `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 |
|
||||
| `GET /api/reservation/order-tasks` | `FRONTEND_USER` | 已实现 M002 V4 CP5;强制 Bearer 登录 + `RESERVATION_TASK_READ` + 酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 酒店访问权;只返回 V4 业务订单任务,不返回 S10/S99 来源通知 | 只读查询默认不写业务审计;不得返回 AI 原始 payload |
|
||||
| `GET /api/reservation/order-tasks/{orderTaskId}` | `FRONTEND_USER` | 已实现 M002 V4 CP5;强制 Bearer 登录 + `RESERVATION_TASK_READ` + 订单任务所属酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 订单任务所属酒店访问权 | 只读查询默认不写业务审计;邮件正文和附件读取仍走 SourceMessage 原文权限;不得返回 `ai_payload_json` |
|
||||
| `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` 只匹配业务 / 可处理卡,固定来源邮件展示卡不参与筛选 |
|
||||
| `GET /api/reservation/order-tasks/{orderTaskId}` | `FRONTEND_USER` | 已实现 M002 V4 CP5;强制 Bearer 登录 + `RESERVATION_TASK_READ` + 订单任务所属酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 订单任务所属酒店访问权 | 只读查询默认不写业务审计;邮件正文和附件读取仍走 SourceMessage 原文权限;不得返回 `ai_payload_json`;同批次 `adapter_contract_errors[]` 只返回白名单诊断字段 |
|
||||
| `GET /api/reservation/source-notifications/{notificationId}` | `FRONTEND_USER` | 已实现 M002 V4 CP5;强制 Bearer 登录 + `RESERVATION_TASK_READ` + 来源通知所属酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 来源通知所属酒店访问权 | 只读查询默认不写业务审计;邮件正文和附件读取仍走 SourceMessage 原文权限;不得返回来源通知原始 payload |
|
||||
| `PUT /api/reservation/tasks/{taskId}/draft` | `FRONTEND_USER` | 第一版未全量强制登录;actor 仍待迁移 | 登录 + `RESERVATION_TASK_EDIT` + 酒店访问权 | 写草稿审计可按业务需要记录 |
|
||||
| `POST /api/reservation/tasks/{taskId}/confirm` | `FRONTEND_USER` | 第一版未全量强制登录;actor 仍待迁移 | 登录 + `RESERVATION_TASK_CONFIRM` + 酒店访问权 | 必须写业务审计 |
|
||||
|
||||
@@ -19,6 +19,8 @@ import java.time.OffsetDateTime;
|
||||
* @param orderTaskStatus 订单任务状态,仅 ORDER_TASK 返回
|
||||
* @param displayStatus 前端展示状态
|
||||
* @param readonlyReasonCode 只读原因稳定码
|
||||
* @param createdAt 条目创建 UTC 时间,用于前端调试和稳定排序兜底
|
||||
* @param updatedAt 条目最近更新 UTC 时间,用于同来源时间下的稳定排序
|
||||
*/
|
||||
public record ReservationV4WorkbenchItemResult(
|
||||
@JsonProperty("item_type")
|
||||
@@ -46,6 +48,10 @@ public record ReservationV4WorkbenchItemResult(
|
||||
@JsonProperty("display_status")
|
||||
String displayStatus,
|
||||
@JsonProperty("readonly_reason_code")
|
||||
String readonlyReasonCode
|
||||
String readonlyReasonCode,
|
||||
@JsonProperty("created_at")
|
||||
OffsetDateTime createdAt,
|
||||
@JsonProperty("updated_at")
|
||||
OffsetDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ public interface ReservationV4OrderTaskMapper extends BaseMapper<ReservationV4Or
|
||||
WHERE c.hotel_id = ot.hotel_id
|
||||
AND c.v4_order_task_id = ot.id
|
||||
AND c.card_status = #{cardStatus}
|
||||
AND c.card_type != 'SOURCE_MESSAGE_DISPLAY'
|
||||
AND c.logic_deleted_at IS NULL
|
||||
)
|
||||
</if>
|
||||
|
||||
@@ -5,10 +5,12 @@ import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageSummaryResponse;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageQueryService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationPageSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4SourceNotificationSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4TaskCardSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationAiSystemProcessCategory;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CardStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CardType;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4NotificationStatus;
|
||||
@@ -18,6 +20,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4Workben
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4OrderTaskQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4SourceNotificationQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4WorkbenchQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationAiTransitionDisplayResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderSummaryResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationPaginationResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4ActionAvailabilityResult;
|
||||
@@ -32,6 +35,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4Source
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4TaskCardResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4WorkbenchItemResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4WorkbenchListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationAiWorkflowRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4SourceNotificationRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4WorkflowRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationV4QueryService;
|
||||
@@ -66,6 +70,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
private static final String DISPLAY_STATUS_OPEN = "OPEN";
|
||||
|
||||
private final ReservationV4WorkflowRepository workflowRepository;
|
||||
private final ReservationAiWorkflowRepository aiWorkflowRepository;
|
||||
private final ReservationV4SourceNotificationRepository sourceNotificationRepository;
|
||||
private final SourceMessageQueryService sourceMessageQueryService;
|
||||
private final HotelContextService hotelContextService;
|
||||
@@ -76,11 +81,13 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
*/
|
||||
public ReservationV4QueryServiceImpl(
|
||||
ReservationV4WorkflowRepository workflowRepository,
|
||||
ReservationAiWorkflowRepository aiWorkflowRepository,
|
||||
ReservationV4SourceNotificationRepository sourceNotificationRepository,
|
||||
SourceMessageQueryService sourceMessageQueryService,
|
||||
HotelContextService hotelContextService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.workflowRepository = workflowRepository;
|
||||
this.aiWorkflowRepository = aiWorkflowRepository;
|
||||
this.sourceNotificationRepository = sourceNotificationRepository;
|
||||
this.sourceMessageQueryService = sourceMessageQueryService;
|
||||
this.hotelContextService = hotelContextService;
|
||||
@@ -215,7 +222,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
basicInformationCard,
|
||||
businessCards,
|
||||
cardCounts(cards),
|
||||
List.of(),
|
||||
adapterContractErrors(orderTask),
|
||||
orderAvailability);
|
||||
}
|
||||
|
||||
@@ -281,7 +288,9 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
null,
|
||||
orderTask.orderTaskStatus(),
|
||||
item.displayStatus(),
|
||||
item.availability().readonlyReasonCode());
|
||||
item.availability().readonlyReasonCode(),
|
||||
orderTask.createdAt(),
|
||||
orderTask.updatedAt());
|
||||
}
|
||||
|
||||
private ReservationV4WorkbenchItemResult toWorkbenchSourceNotificationItem(
|
||||
@@ -301,7 +310,9 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
notification.notificationStatus(),
|
||||
null,
|
||||
notification.notificationStatus(),
|
||||
availability.readonlyReasonCode());
|
||||
availability.readonlyReasonCode(),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(notification.createdAt()),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(notification.updatedAt()));
|
||||
}
|
||||
|
||||
private ReservationV4TaskCardResult toCardResult(
|
||||
@@ -573,7 +584,10 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
}
|
||||
List<Long> uniqueIds = sourceMessageIds.stream().filter(Objects::nonNull).distinct().toList();
|
||||
List<SourceMessageSummaryResponse> summaries = sourceMessageQueryService.getSummariesByIds(uniqueIds);
|
||||
List<String> conversationIds = summaries.stream()
|
||||
List<SourceMessageSummaryResponse> safeSummaries = summaries.stream()
|
||||
.filter(summary -> Objects.equals(hotelId, summary.hotelId()))
|
||||
.toList();
|
||||
List<String> conversationIds = safeSummaries.stream()
|
||||
.map(SourceMessageSummaryResponse::externalConversationId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
@@ -582,7 +596,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
hotelId,
|
||||
conversationIds);
|
||||
Map<Long, ReservationV4SourceMessageSummaryResult> result = new LinkedHashMap<>();
|
||||
for (SourceMessageSummaryResponse summary : summaries) {
|
||||
for (SourceMessageSummaryResponse summary : safeSummaries) {
|
||||
Long sourceMessageId = Long.valueOf(summary.id());
|
||||
result.put(sourceMessageId, new ReservationV4SourceMessageSummaryResult(
|
||||
summary.id(),
|
||||
@@ -610,6 +624,59 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<ReservationAiTransitionDisplayResult> adapterContractErrors(ReservationV4OrderTaskSnapshot orderTask) {
|
||||
if (orderTask.aiBatchId() == null) {
|
||||
return List.of();
|
||||
}
|
||||
return aiWorkflowRepository.findTransitionsByBatchId(orderTask.hotelId(), orderTask.aiBatchId()).stream()
|
||||
.filter(transition -> ReservationAiSystemProcessCategory.ADAPTER_CONTRACT_ERROR.name()
|
||||
.equals(transition.systemProcessCategory()))
|
||||
.map(this::toTransitionDisplayResult)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private ReservationAiTransitionDisplayResult toTransitionDisplayResult(ReservationAiTransitionSnapshot transition) {
|
||||
return new ReservationAiTransitionDisplayResult(
|
||||
transition.id().toString(),
|
||||
transition.sourceEventIndex(),
|
||||
transition.arrayIndex(),
|
||||
transition.resultType(),
|
||||
transition.aiTaskType(),
|
||||
transition.taskSubtype(),
|
||||
transition.routeCode(),
|
||||
transition.systemProcessCategory(),
|
||||
transition.adapterErrorCode(),
|
||||
transition.adapterErrorMessage(),
|
||||
diagnosticPayloadFragment(transition.aiPayloadJson()));
|
||||
}
|
||||
|
||||
private Object diagnosticPayloadFragment(String aiPayloadJson) {
|
||||
if (!hasText(aiPayloadJson)) {
|
||||
return null;
|
||||
}
|
||||
JsonNode payload = parseJson(aiPayloadJson);
|
||||
Map<String, Object> fragment = new LinkedHashMap<>();
|
||||
copyDiagnosticPayloadField(payload, fragment, "event_type");
|
||||
copyDiagnosticPayloadField(payload, fragment, "source_event_index");
|
||||
copyDiagnosticPayloadField(payload, fragment, "current_or_history");
|
||||
copyDiagnosticPayloadField(payload, fragment, "contract_errors");
|
||||
copyDiagnosticPayloadField(payload, fragment, "intent_type");
|
||||
copyDiagnosticPayloadField(payload, fragment, "reason_code");
|
||||
copyDiagnosticPayloadField(payload, fragment, "missing_fields");
|
||||
copyDiagnosticPayloadField(payload, fragment, "parent_source_event_index");
|
||||
copyDiagnosticPayloadField(payload, fragment, "linked_task_group_id");
|
||||
copyDiagnosticPayloadField(payload, fragment, "blocked_until_parent_completed");
|
||||
return fragment;
|
||||
}
|
||||
|
||||
private void copyDiagnosticPayloadField(JsonNode payload, Map<String, Object> fragment, String fieldName) {
|
||||
JsonNode value = payload == null ? null : payload.get(fieldName);
|
||||
if (value == null || value.isNull() || value.isMissingNode()) {
|
||||
return;
|
||||
}
|
||||
fragment.put(fieldName, objectMapper.convertValue(value, Object.class));
|
||||
}
|
||||
|
||||
private List<Long> findSourceMessageIdsByKeyword(String hotelId, String keyword) {
|
||||
if (!hasText(keyword)) {
|
||||
return List.of();
|
||||
@@ -725,7 +792,21 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
private Comparator<ReservationV4WorkbenchItemResult> workbenchItemComparator() {
|
||||
return Comparator.comparing(ReservationV4WorkbenchItemResult::sourceReceivedAt,
|
||||
Comparator.nullsLast(Comparator.reverseOrder()))
|
||||
.thenComparing(ReservationV4WorkbenchItemResult::targetId, Comparator.reverseOrder());
|
||||
.thenComparing(ReservationV4WorkbenchItemResult::updatedAt,
|
||||
Comparator.nullsLast(Comparator.reverseOrder()))
|
||||
.thenComparing(ReservationV4WorkbenchItemResult::createdAt,
|
||||
Comparator.nullsLast(Comparator.reverseOrder()))
|
||||
.thenComparing(this::targetIdAsLong, Comparator.nullsLast(Comparator.reverseOrder()))
|
||||
.thenComparing(ReservationV4WorkbenchItemResult::targetId,
|
||||
Comparator.nullsLast(Comparator.reverseOrder()));
|
||||
}
|
||||
|
||||
private Long targetIdAsLong(ReservationV4WorkbenchItemResult item) {
|
||||
try {
|
||||
return item.targetId() == null ? null : Long.valueOf(item.targetId());
|
||||
} catch (NumberFormatException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ReservationV4WorkbenchQueryRequest normalizeWorkbenchRequest(ReservationV4WorkbenchQueryRequest request) {
|
||||
@@ -748,13 +829,45 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
return new ReservationV4OrderTaskQueryRequest(
|
||||
normalizeHotelId(request.hotelId()),
|
||||
request.orderId(),
|
||||
trimToNull(request.orderTaskStatus()),
|
||||
trimToNull(request.cardStatus()),
|
||||
normalizeOrderTaskStatus(request.orderTaskStatus()),
|
||||
normalizeCardStatus(request.cardStatus()),
|
||||
trimToNull(request.keyword()),
|
||||
request.pageNum(),
|
||||
request.pageSize());
|
||||
}
|
||||
|
||||
private String normalizeOrderTaskStatus(String orderTaskStatus) {
|
||||
String normalized = trimToNull(orderTaskStatus);
|
||||
if (normalized == null) {
|
||||
return null;
|
||||
}
|
||||
for (ReservationV4OrderTaskStatus status : ReservationV4OrderTaskStatus.values()) {
|
||||
if (status.name().equals(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
throw new ReservationTaskWorkflowException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"V4_ORDER_TASK_STATUS_INVALID",
|
||||
"V4 订单任务状态不支持。");
|
||||
}
|
||||
|
||||
private String normalizeCardStatus(String cardStatus) {
|
||||
String normalized = trimToNull(cardStatus);
|
||||
if (normalized == null) {
|
||||
return null;
|
||||
}
|
||||
for (ReservationV4CardStatus status : ReservationV4CardStatus.values()) {
|
||||
if (status.name().equals(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
throw new ReservationTaskWorkflowException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"V4_CARD_STATUS_INVALID",
|
||||
"V4 任务卡状态不支持。");
|
||||
}
|
||||
|
||||
private String normalizeHotelId(String hotelId) {
|
||||
try {
|
||||
return hotelContextService.resolveCurrentHotelId(hotelId);
|
||||
|
||||
@@ -19,16 +19,22 @@ import cn.nianxx.thhotel.platform.identity.service.impl.AuthPasswordService;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageCommand;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResult;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageCaptureService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiBatchDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4SourceNotificationDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4SourceNotificationSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4TaskCardDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.AiResultType;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationAiSystemProcessCategory;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationSystemTaskType;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CardStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CardType;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4NotificationStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4OrderTaskStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4TargetResolutionStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationAiWorkflowRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4SourceNotificationRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4WorkflowRepository;
|
||||
import java.time.Instant;
|
||||
@@ -73,6 +79,9 @@ class ReservationV4QueryControllerTest {
|
||||
@Autowired
|
||||
private ReservationV4WorkflowRepository workflowRepository;
|
||||
|
||||
@Autowired
|
||||
private ReservationAiWorkflowRepository aiWorkflowRepository;
|
||||
|
||||
@Autowired
|
||||
private ReservationV4SourceNotificationRepository sourceNotificationRepository;
|
||||
@Autowired
|
||||
@@ -136,6 +145,7 @@ class ReservationV4QueryControllerTest {
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/workbench-items")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("keyword", "mail-v4-query-workbench")
|
||||
.param("page_num", "1")
|
||||
.param("page_size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
@@ -159,6 +169,64 @@ class ReservationV4QueryControllerTest {
|
||||
.andExpect(content().string(not(containsString("Sensitive raw notification body"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnAdapterContractErrorsOnV4OrderTaskDetail() throws Exception {
|
||||
Instant receivedAt = Instant.parse("2026-07-18T03:10:00Z");
|
||||
SourceMessageCaptureResult source = captureSourceMessage(
|
||||
"mail-v4-query-adapter-error-001",
|
||||
"V4 Query Adapter Error",
|
||||
receivedAt,
|
||||
HOTEL_ID);
|
||||
Long batchId = insertAiBatch(source.inboxId(), LocalDateTime.ofInstant(receivedAt, ZoneOffset.UTC));
|
||||
insertAdapterContractErrorTransition(
|
||||
source.inboxId(),
|
||||
batchId,
|
||||
LocalDateTime.ofInstant(receivedAt.plusSeconds(1), ZoneOffset.UTC));
|
||||
ReservationV4OrderTaskSnapshot orderTask = seedOrderTask(
|
||||
source,
|
||||
batchId,
|
||||
receivedAt,
|
||||
"order-adapter-error",
|
||||
"GRP-V4-ADAPTER-001");
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}", orderTask.id())
|
||||
.param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.adapter_contract_errors.length()").value(1))
|
||||
.andExpect(jsonPath("$.adapter_contract_errors[0].system_process_category")
|
||||
.value("ADAPTER_CONTRACT_ERROR"))
|
||||
.andExpect(jsonPath("$.adapter_contract_errors[0].adapter_error_code")
|
||||
.value("V4_PAYMENT_ATTACHMENT_NOT_FOUND"))
|
||||
.andExpect(jsonPath("$.adapter_contract_errors[0].payload_fragment.contract_errors[0]")
|
||||
.value("attachment_ids 未匹配来源附件"))
|
||||
.andExpect(content().string(not(containsString("raw_sensitive_payload"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotExposeCrossHotelSourceMessageSummaryWhenV4ReferenceIsPolluted() throws Exception {
|
||||
Instant receivedAt = Instant.parse("2026-07-18T03:20:00Z");
|
||||
SourceMessageCaptureResult otherSource = captureSourceMessage(
|
||||
"mail-v4-query-cross-source-001",
|
||||
"Other Hotel Secret Subject",
|
||||
receivedAt,
|
||||
OTHER_HOTEL_ID);
|
||||
ReservationV4OrderTaskSnapshot orderTask = seedOrderTask(
|
||||
otherSource,
|
||||
990000000000000901L,
|
||||
receivedAt,
|
||||
"order-cross-source",
|
||||
"GRP-V4-CROSS-SOURCE-001");
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}", orderTask.id())
|
||||
.param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.source_message_summary.source_message_id")
|
||||
.value(otherSource.inboxId().toString()))
|
||||
.andExpect(jsonPath("$.source_message_summary.subject").doesNotExist())
|
||||
.andExpect(jsonPath("$.source_message_summary.external_message_id").doesNotExist())
|
||||
.andExpect(content().string(not(containsString("Other Hotel Secret Subject"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnV4OrderTaskDetailWithSafeCards() throws Exception {
|
||||
ReservationV4OrderTaskSnapshot orderTask = seedOrderTask("mail-v4-query-detail-001",
|
||||
@@ -203,6 +271,45 @@ class ReservationV4QueryControllerTest {
|
||||
.andExpect(jsonPath("$.page.total").value(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotMatchReadonlySourceMessageDisplayCardWhenFilteringBusinessCards() throws Exception {
|
||||
seedOrderTask("mail-v4-query-readonly-filter-001",
|
||||
Instant.parse("2026-07-18T03:40:00Z"));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("card_status", "READONLY")
|
||||
.param("keyword", "mail-v4-query-readonly-filter-001")
|
||||
.param("page_num", "1")
|
||||
.param("page_size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items.length()").value(0))
|
||||
.andExpect(jsonPath("$.page.total").value(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSortSameReceivedAtWorkbenchItemsByUpdatedAtAndId() throws Exception {
|
||||
Instant sameReceivedAt = Instant.parse("2026-07-18T03:50:00Z");
|
||||
ReservationV4SourceNotificationSnapshot notification = seedSourceNotification(
|
||||
"mail-v4-query-same-time-notification-001",
|
||||
"S10",
|
||||
sameReceivedAt);
|
||||
ReservationV4OrderTaskSnapshot orderTask = seedOrderTask(
|
||||
"mail-v4-query-same-time-order-001",
|
||||
sameReceivedAt);
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/workbench-items")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("keyword", "mail-v4-query-same-time")
|
||||
.param("page_num", "1")
|
||||
.param("page_size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items[0].item_type").value("SOURCE_NOTIFICATION"))
|
||||
.andExpect(jsonPath("$.items[0].target_id").value(notification.id().toString()))
|
||||
.andExpect(jsonPath("$.items[1].item_type").value("ORDER_TASK"))
|
||||
.andExpect(jsonPath("$.items[1].target_id").value(orderTask.id().toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnV4SourceNotificationDetailWithoutRawPayload() throws Exception {
|
||||
ReservationV4SourceNotificationSnapshot notification = seedSourceNotification(
|
||||
@@ -296,6 +403,21 @@ class ReservationV4QueryControllerTest {
|
||||
.andExpect(jsonPath("$.error_code").value("V4_WORKBENCH_ITEM_TYPE_INVALID"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectUnsupportedOrderTaskStatusAndCardStatus() throws Exception {
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("order_task_status", "UNKNOWN"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("V4_ORDER_TASK_STATUS_INVALID"));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("card_status", "UNKNOWN"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("V4_CARD_STATUS_INVALID"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCapHugeWorkbenchPageSafely() throws Exception {
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/workbench-items")
|
||||
@@ -308,18 +430,32 @@ class ReservationV4QueryControllerTest {
|
||||
}
|
||||
|
||||
private ReservationV4OrderTaskSnapshot seedOrderTask(String externalMessageId, Instant receivedAt) {
|
||||
SourceMessageCaptureResult source = captureSourceMessage(externalMessageId, "V4 Query Business", receivedAt);
|
||||
SourceMessageCaptureResult source = captureSourceMessage(externalMessageId, "V4 Query Business", receivedAt, HOTEL_ID);
|
||||
return seedOrderTask(
|
||||
source,
|
||||
990000000000000001L,
|
||||
receivedAt,
|
||||
"order-1",
|
||||
"GRP-V4-QUERY-001");
|
||||
}
|
||||
|
||||
private ReservationV4OrderTaskSnapshot seedOrderTask(
|
||||
SourceMessageCaptureResult source,
|
||||
Long aiBatchId,
|
||||
Instant receivedAt,
|
||||
String orderRef,
|
||||
String targetLocatorValue) {
|
||||
LocalDateTime now = LocalDateTime.ofInstant(receivedAt.plusSeconds(10), ZoneOffset.UTC);
|
||||
ReservationV4OrderTaskSnapshot orderTask = workflowRepository.findOrCreateOrderTask(new ReservationV4OrderTaskDraft(
|
||||
HOTEL_ID,
|
||||
source.inboxId(),
|
||||
990000000000000001L,
|
||||
"order-1",
|
||||
aiBatchId,
|
||||
orderRef,
|
||||
1,
|
||||
null,
|
||||
"GROUP",
|
||||
"GROUP_CODE",
|
||||
"GRP-V4-QUERY-001",
|
||||
targetLocatorValue,
|
||||
ReservationV4TargetResolutionStatus.RESOLVED.name(),
|
||||
ReservationV4OrderTaskStatus.OPEN.name(),
|
||||
LocalDateTime.ofInstant(receivedAt, ZoneOffset.UTC),
|
||||
@@ -346,7 +482,8 @@ class ReservationV4QueryControllerTest {
|
||||
SourceMessageCaptureResult source = captureSourceMessage(
|
||||
externalMessageId,
|
||||
"V4 Query Notification",
|
||||
receivedAt);
|
||||
receivedAt,
|
||||
HOTEL_ID);
|
||||
return sourceNotificationRepository.findOrCreateSourceNotification(new ReservationV4SourceNotificationDraft(
|
||||
HOTEL_ID,
|
||||
source.inboxId(),
|
||||
@@ -392,9 +529,10 @@ class ReservationV4QueryControllerTest {
|
||||
private SourceMessageCaptureResult captureSourceMessage(
|
||||
String externalMessageId,
|
||||
String subject,
|
||||
Instant receivedAt) {
|
||||
Instant receivedAt,
|
||||
String hotelId) {
|
||||
return captureService.capture(new CaptureSourceMessageCommand(
|
||||
HOTEL_ID,
|
||||
hotelId,
|
||||
"AGENTBUS",
|
||||
"EMAIL",
|
||||
externalMessageId,
|
||||
@@ -412,4 +550,57 @@ class ReservationV4QueryControllerTest {
|
||||
List.of()
|
||||
));
|
||||
}
|
||||
|
||||
private Long insertAiBatch(Long sourceMessageId, LocalDateTime receivedAt) {
|
||||
return aiWorkflowRepository.insertBatch(new ReservationAiBatchDraft(
|
||||
HOTEL_ID,
|
||||
sourceMessageId,
|
||||
"sha256-v4-query-adapter-error",
|
||||
"batch-v4-query-adapter-error",
|
||||
"superagent-test",
|
||||
"request-v4-query-adapter-error",
|
||||
receivedAt,
|
||||
2,
|
||||
null));
|
||||
}
|
||||
|
||||
private void insertAdapterContractErrorTransition(Long sourceMessageId, Long batchId, LocalDateTime now) {
|
||||
aiWorkflowRepository.insertTransition(new ReservationAiTransitionDraft(
|
||||
HOTEL_ID,
|
||||
batchId,
|
||||
sourceMessageId,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
"P0",
|
||||
"reservation-v4",
|
||||
AiResultType.ADAPTER_CONTRACT_ERROR.code(),
|
||||
"adapter_contract_error",
|
||||
null,
|
||||
ReservationAiSystemProcessCategory.ADAPTER_CONTRACT_ERROR.name(),
|
||||
ReservationSystemTaskType.ADAPTER_CONTRACT_ERROR.name(),
|
||||
"ADAPTER_CONTRACT_ERROR",
|
||||
null,
|
||||
"current",
|
||||
null,
|
||||
null,
|
||||
"sha256-v4-query-adapter-error-item",
|
||||
"item-v4-query-adapter-error",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
"""
|
||||
{"event_type":"PAYMENT","source_event_index":2,"contract_errors":["attachment_ids 未匹配来源附件"],"raw_sensitive_payload":"must not leak"}
|
||||
""",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"V4_PAYMENT_ATTACHMENT_NOT_FOUND",
|
||||
"PAYMENT.attachment_ids 未匹配来源附件。",
|
||||
now));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user