补齐任务列表状态筛选和邮件HTML安全字段

This commit is contained in:
andy
2026-07-09 00:15:51 +08:00
parent a0cb3a05d7
commit 855396e553
13 changed files with 203 additions and 43 deletions

View File

@@ -40,9 +40,8 @@
## 5. 当前已明确后置事项 ## 5. 当前已明确后置事项
- 普通任务切换订单接口后置。 - 普通任务切换订单接口后置。
- 任务列表按订单状态 `order_status` 筛选待后端补充或确认 - 任务详情 / 任务写操作是否需要显式 `hotel_id` 已确认后置;第一版先按 ID 定位,后续多酒店隔离 / 权限方案统一补齐
- 任务详情 / 任务写操作是否需要显式 `hotel_id` 待后端确认;前端已先统一配置 `VITE_RESERVATION_HOTEL_ID` 并用于列表和订单详情查询 - 邮件会话详情已返回 `html_body_sanitized``html_render_mode`;前端展示 HTML 时优先使用清洗字段,`html_body` 只作为原始内容兼容字段
- 邮件 `html_body` 已标记为需要 sanitize前端联调阶段临时直渲只用于本地看效果生产前请后端返回已清洗 HTML 或明确清洗字段,并确认 CSP / 外链图片 / 附件 URL 策略。
- SuperAgent 查询接口 4 后置,当前先不开发。 - SuperAgent 查询接口 4 后置,当前先不开发。
- SuperAgent 查询接口 3 涉及附件解析、OCR、Excel、voucher、rooming list 等能力,当前系统暂不具备,仍后置。 - SuperAgent 查询接口 3 涉及附件解析、OCR、Excel、voucher、rooming list 等能力,当前系统暂不具备,仍后置。
- 用户身份 / 权限方案后置;当前后端审计 actor 仍是本地占位。 - 用户身份 / 权限方案后置;当前后端审计 actor 仍是本地占位。

View File

@@ -37,7 +37,7 @@
| 接口 | 用途 | 前端注意 | | 接口 | 用途 | 前端注意 |
| --- | --- | --- | | --- | --- | --- |
| `GET /api/reservation/orders` | 查询订单列表 | 默认返回全部订单状态;`open_task_count` 排除 `COMPLETED``FAILED`;用 `next_processable_task_id` 引导用户继续处理。 | | `GET /api/reservation/orders` | 查询订单列表 | 默认返回全部订单状态;`open_task_count` 排除 `COMPLETED``FAILED`;用 `next_processable_task_id` 引导用户继续处理。 |
| `GET /api/reservation/tasks` | 查询任务列表 / 工作台 | 用 `can_process``readonly_reason_code` 控制入口按钮;列表不返回 AI 原始 payload、邮件正文或附件 URL已返回来源邮件会话摘要字段。 | | `GET /api/reservation/tasks` | 查询任务列表 / 工作台 | 用 `can_process``readonly_reason_code` 控制入口按钮;列表不返回 AI 原始 payload、邮件正文或附件 URL已返回来源邮件会话摘要字段,并支持 `order_status` 按任务所属订单状态筛选。 |
| `GET /api/reservation/orders/{orderId}` | 查询订单详情与任务时间线 | `include_tasks=false` 可只取订单摘要;时间线按后端队列顺序返回,前端不要自行按创建时间重排;`tasks[]` 已返回来源邮件会话摘要字段。 | | `GET /api/reservation/orders/{orderId}` | 查询订单详情与任务时间线 | `include_tasks=false` 可只取订单摘要;时间线按后端队列顺序返回,前端不要自行按创建时间重排;`tasks[]` 已返回来源邮件会话摘要字段。 |
| `GET /api/reservation/tasks/{taskId}` | 查询任务详情 | 以返回的可处理状态和只读原因控制按钮,不只看任务状态;`fields[]` 已包含 P0 字段元数据。 | | `GET /api/reservation/tasks/{taskId}` | 查询任务详情 | 以返回的可处理状态和只读原因控制按钮,不只看任务状态;`fields[]` 已包含 P0 字段元数据。 |
| `PUT /api/reservation/tasks/{taskId}/draft` | 保存任务草稿 | 只保存草稿,不代表用户最终确认。 | | `PUT /api/reservation/tasks/{taskId}/draft` | 保存任务草稿 | 只保存草稿,不代表用户最终确认。 |
@@ -49,7 +49,7 @@
| `GET /api/source-messages` | 查询来源消息安全摘要 | 列表不返回邮件正文、HTML、附件 URL 或原始 payload。 | | `GET /api/source-messages` | 查询来源消息安全摘要 | 列表不返回邮件正文、HTML、附件 URL 或原始 payload。 |
| `GET /api/source-messages/{id}` | 查询来源消息安全详情 | 只用于安全摘要详情。 | | `GET /api/source-messages/{id}` | 查询来源消息安全详情 | 只用于安全摘要详情。 |
| `GET /api/source-messages/{id}/original` | 读取来源消息原文 | 需要受控访问头,返回 HTML 时前端展示前必须 sanitize。 | | `GET /api/source-messages/{id}/original` | 读取来源消息原文 | 需要受控访问头,返回 HTML 时前端展示前必须 sanitize。 |
| `GET /api/source-messages/{sourceMessageId}/conversation` | 读取邮件会话详情 | 返回同一外部会话全部邮件的完整 text/html、附件外链、内联图片和关联订单 / 任务摘要;前端不传原文读取 key。2026-07-08 联调阶段前端可临时直渲 `html_body` 看效果;生产方案必须对 `html_body` 做 sanitize建议后端返回已清洗 HTML 或明确清洗字段。 | | `GET /api/source-messages/{sourceMessageId}/conversation` | 读取邮件会话详情 | 返回同一外部会话全部邮件的完整 text/html、`html_body_sanitized`附件外链、内联图片和关联订单 / 任务摘要;前端不传原文读取 key,展示 HTML 时优先使用 `html_body_sanitized`。 |
### 5.1 本轮新增 / 修改接口说明 ### 5.1 本轮新增 / 修改接口说明
@@ -58,10 +58,10 @@
| 接口 | 本轮变化 | 前端接入注意 | | 接口 | 本轮变化 | 前端接入注意 |
| --- | --- | --- | | --- | --- | --- |
| `GET /api/reservation/orders` | 新增订单列表接口。 | `order_status` 不传时默认查询全部订单状态;`page_num` 从 1 开始;`page_size` 后端有最大值保护;`open_task_count` 排除 `COMPLETED``FAILED``next_processable_task_id` 为空表示当前没有可继续处理的任务。 | | `GET /api/reservation/orders` | 新增订单列表接口。 | `order_status` 不传时默认查询全部订单状态;`page_num` 从 1 开始;`page_size` 后端有最大值保护;`open_task_count` 排除 `COMPLETED``FAILED``next_processable_task_id` 为空表示当前没有可继续处理的任务。 |
| `GET /api/reservation/tasks` | 补齐来源邮件会话摘要字段。 | 列表仍然只返回安全摘要不返回正文、HTML、附件 URL 或 AI 原始 payload点击邮件入口时使用 `source_message_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[]` 每条任务的来源邮件会话摘要字段。 | `include_tasks=false` 可只取订单摘要;时间线顺序由后端按订单队列返回,前端不要自行按创建时间重排。`include_source_summary` 第一版不作为前端裁剪字段的强约束,前端暂不要依赖它减少返回字段。 | | `GET /api/reservation/orders/{orderId}` | 补齐 `tasks[]` 每条任务的来源邮件会话摘要字段。 | `include_tasks=false` 可只取订单摘要;时间线顺序由后端按订单队列返回,前端不要自行按创建时间重排。`include_source_summary` 第一版不作为前端裁剪字段的强约束,前端暂不要依赖它减少返回字段。 |
| `GET /api/reservation/tasks/{taskId}` | 补齐顶层来源邮件字段,并扩展 `fields[]` 元数据。 | 顶层来源字段用于打开邮件会话;`fields[]` 中的 `result_type``task_type``task_subtype``default_value_source` 用于前端字段分组、调试和白名单对齐。 | | `GET /api/reservation/tasks/{taskId}` | 补齐顶层来源邮件字段,并扩展 `fields[]` 元数据。 | 顶层来源字段用于打开邮件会话;`fields[]` 中的 `result_type``task_type``task_subtype``default_value_source` 用于前端字段分组、调试和白名单对齐。 |
| `GET /api/source-messages/{sourceMessageId}/conversation` | 新增邮件会话详情接口。 | 当前唯一推荐路径是这个接口;不要调用历史讨论过的 `/api/source-message-conversations/{externalConversationId}`。 | | `GET /api/source-messages/{sourceMessageId}/conversation` | 新增邮件会话详情接口,并补齐 `html_body_sanitized` / `html_render_mode`。 | 当前唯一推荐路径是这个接口;前端渲染邮件 HTML 时优先使用 `html_body_sanitized`;不要调用历史讨论过的 `/api/source-message-conversations/{externalConversationId}`。 |
### 5.2 来源邮件会话字段说明 ### 5.2 来源邮件会话字段说明
@@ -83,8 +83,8 @@
- 后端会根据 `sourceMessageId` 定位 `external_conversation_id`,并返回同一会话下全部邮件;如果来源消息没有外部会话 ID会降级返回当前单封邮件。 - 后端会根据 `sourceMessageId` 定位 `external_conversation_id`,并返回同一会话下全部邮件;如果来源消息没有外部会话 ID会降级返回当前单封邮件。
- `messages[]` 按后端接收时间正序返回,前端不要重新按创建时间或任务时间排序。 - `messages[]` 按后端接收时间正序返回,前端不要重新按创建时间或任务时间排序。
- 返回内容包含完整 `text_body``html_body``inline_images[]``attachments[]``related_orders[]``related_tasks[]` - 返回内容包含完整 `text_body``html_body``inline_images[]``attachments[]``related_orders[]``related_tasks[]`
- `html_body` 已标记为需要 sanitize`html_sanitize_required=true` 时,生产渲染必须使用清洗后的 HTML。当前为了联调观察效果前端临时直接渲染原始 `html_body`,上线前需要后端 / 前端共同确认清洗字段、CSP、外链图片和附件 URL 策略 - `html_body` 是原始 HTML 兼容字段`html_body_sanitized` 是后端第一版清洗结果,已移除脚本标签、事件属性和危险协议链接。前端生产展示必须优先使用 `html_body_sanitized`,并可用 `html_render_mode=SANITIZED_HTML` 判断渲染模式
- `inline_images[]``attachments[]` `externalUrl` 可能包含有时效或访问凭证的外链,前端不得写入普通日志、错误上报、localStorage 或 URL query。 - 第一版仅处理 HTML 内容安全;`inline_images[]``attachments[]``externalUrl` 来自本系统 OSS 服务,暂不做额外拦截,但前端不得写入普通日志、错误上报、localStorage 或 URL query。
- 会话详情接口由后端内部写原文读取审计,前端不传 `X-TH-Hotel-Source-Original-Read-Key` - 会话详情接口由后端内部写原文读取审计,前端不传 `X-TH-Hotel-Source-Original-Read-Key`
- 会话详情外层字段主要是 snake_case但媒体对象沿用原文读取接口字段当前是 `mediaType``fileName``contentType``sizeBytes``externalUrl``externalMediaId` 这种 camelCase前端类型定义需要单独处理。 - 会话详情外层字段主要是 snake_case但媒体对象沿用原文读取接口字段当前是 `mediaType``fileName``contentType``sizeBytes``externalUrl``externalMediaId` 这种 camelCase前端类型定义需要单独处理。

View File

@@ -9,7 +9,7 @@
| 优先级 | 接口 | 页面 / 场景 | 状态 | | 优先级 | 接口 | 页面 / 场景 | 状态 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| P0 | 订单列表接口 `GET /api/reservation/orders` | 订单列表页、首页工作台 | 已完成第一版 | | P0 | 订单列表接口 `GET /api/reservation/orders` | 订单列表页、首页工作台 | 已完成第一版 |
| P0 | 任务列表接口 `GET /api/reservation/tasks` | 任务列表菜单、订单详情任务入口 | 已完成第一版,已补来源邮件会话字段`order_status` 筛选仍待后端补充或确认 | | P0 | 任务列表接口 `GET /api/reservation/tasks` | 任务列表菜单、订单详情任务入口 | 已完成第一版,已补来源邮件会话字段`order_status` 筛选 |
| P0 | 订单详情接口 `GET /api/reservation/orders/{orderId}` | 订单详情页 | 已完成第一版,已补 `tasks[]` 每个任务的来源邮件会话字段 | | P0 | 订单详情接口 `GET /api/reservation/orders/{orderId}` | 订单详情页 | 已完成第一版,已补 `tasks[]` 每个任务的来源邮件会话字段 |
| P0 | 任务详情读取接口 `GET /api/reservation/tasks/{taskId}` | 任务详情页动态渲染 | 已完成第一版,已补来源邮件字段和 3.0 字段元数据 | | P0 | 任务详情读取接口 `GET /api/reservation/tasks/{taskId}` | 任务详情页动态渲染 | 已完成第一版,已补来源邮件字段和 3.0 字段元数据 |
| P0 | 任务详情操作接口 | 任务详情保存、确认、OPERA、审计 | 已完成;前端可直接接入 | | P0 | 任务详情操作接口 | 任务详情保存、确认、OPERA、审计 | 已完成;前端可直接接入 |
@@ -25,7 +25,7 @@
| 接口 / 能力 | 当前后端状态 | 前端是否可直接接入 | 仍需后端处理 | | 接口 / 能力 | 当前后端状态 | 前端是否可直接接入 | 仍需后端处理 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `GET /api/reservation/tasks` | 已完成第一版,已补来源邮件会话字段 | 可以 | 前端暂不传 `order_status`,如任务列表需要按订单状态筛选,请后端补 `order_status` 入参并明确匹配逻辑。 | | `GET /api/reservation/tasks` | 已完成第一版,已补来源邮件会话字段和所属订单状态筛选 | 可以 | `order_status` 按任务所属订单状态过滤;不传时保持当前全部任务列表行为。 |
| `GET /api/reservation/orders/{orderId}` | 已完成第一版,已补 `tasks[]` 来源邮件会话字段 | 可以 | 暂无。 | | `GET /api/reservation/orders/{orderId}` | 已完成第一版,已补 `tasks[]` 来源邮件会话字段 | 可以 | 暂无。 |
| `GET /api/reservation/tasks/{taskId}` | 已完成第一版,已补任务顶层来源邮件字段和 `fields[]` 3.0 元数据 | 可以 | 当前 Controller 不接收 `hotel_id`;如后续多酒店隔离需要前端显式传酒店上下文,请后端补可选入参或确认按 taskId 全局唯一即可。 | | `GET /api/reservation/tasks/{taskId}` | 已完成第一版,已补任务顶层来源邮件字段和 `fields[]` 3.0 元数据 | 可以 | 当前 Controller 不接收 `hotel_id`;如后续多酒店隔离需要前端显式传酒店上下文,请后端补可选入参或确认按 taskId 全局唯一即可。 |
| `PUT /api/reservation/tasks/{taskId}/draft` | 已完成 | 可以 | 当前 Controller 不接收 `hotel_id`;如写操作需要酒店上下文幂等 / 权限校验,请后端补可选入参或请求体字段。 | | `PUT /api/reservation/tasks/{taskId}/draft` | 已完成 | 可以 | 当前 Controller 不接收 `hotel_id`;如写操作需要酒店上下文幂等 / 权限校验,请后端补可选入参或请求体字段。 |
@@ -38,7 +38,7 @@
| `GET /api/source-messages/{id}` | 已完成单条安全摘要 | 可以 | 不能替代邮件会话全文接口。 | | `GET /api/source-messages/{id}` | 已完成单条安全摘要 | 可以 | 不能替代邮件会话全文接口。 |
| `GET /api/source-messages/{id}/original` | 已完成单封原文受控读取 | 谨慎接入 | 只能读单封邮件,不能返回同一 conversation 全量邮件。 | | `GET /api/source-messages/{id}/original` | 已完成单封原文受控读取 | 谨慎接入 | 只能读单封邮件,不能返回同一 conversation 全量邮件。 |
| `GET /api/reservation/orders` | 已完成第一版 | 可以 | 默认查询全部订单状态;`open_task_count` 排除 `COMPLETED``FAILED`。 | | `GET /api/reservation/orders` | 已完成第一版 | 可以 | 默认查询全部订单状态;`open_task_count` 排除 `COMPLETED``FAILED`。 |
| `GET /api/source-messages/{sourceMessageId}/conversation` | 已完成第一版 | 可以 | 返回完整 text/html、媒体外链和关联订单 / 任务摘要;前端不传原文读取 key。当前前端为看效果临时直渲 `html_body`;请后端标记 / 保障 `html_body` 需要 sanitize生产前返回已清洗 HTML 或明确清洗字段、CSP、附件 URL 策略。 | | `GET /api/source-messages/{sourceMessageId}/conversation` | 已完成第一版,已补 `html_body_sanitized``html_render_mode` | 可以 | 返回完整 text/html、后端清洗后的 HTML、媒体外链和关联订单 / 任务摘要;前端不传原文读取 key,页面展示优先使用 `html_body_sanitized`。 |
| `POST /api/system/reservation/demo-data` | 已完成 | 仅本地 / test 联调可用 | 默认关闭,必须后端配置访问口令;不能作为生产页面接口。 | | `POST /api/system/reservation/demo-data` | 已完成 | 仅本地 / test 联调可用 | 默认关闭,必须后端配置访问口令;不能作为生产页面接口。 |
| `GET /api/source-message-conversations/{externalConversationId}` | 未发现后端实现 | 不可以 | 历史讨论过的候选路径,当前不提供;前端统一使用 `GET /api/source-messages/{sourceMessageId}/conversation`。 | | `GET /api/source-message-conversations/{externalConversationId}` | 未发现后端实现 | 不可以 | 历史讨论过的候选路径,当前不提供;前端统一使用 `GET /api/source-messages/{sourceMessageId}/conversation`。 |
| `GET /api/reservation/message-notifications` | 未发现后端实现 | 不可以 | 如需独立信息提醒页再新增;第一版可先用任务接口过滤。 | | `GET /api/reservation/message-notifications` | 未发现后端实现 | 不可以 | 如需独立信息提醒页再新增;第一版可先用任务接口过滤。 |
@@ -87,7 +87,7 @@ GET /api/reservation/tasks
| `task_type` | 否 | `NEW_BOOKING``UPDATE_BOOKING``CANCEL_BOOKING``MANUAL_REVIEW``INFORMATIONAL_MESSAGE`。 | | `task_type` | 否 | `NEW_BOOKING``UPDATE_BOOKING``CANCEL_BOOKING``MANUAL_REVIEW``INFORMATIONAL_MESSAGE`。 |
| `task_status` | 否 | 任务状态过滤。 | | `task_status` | 否 | 任务状态过滤。 |
| `task_subtype` | 否 | 任务卡 subtype 过滤。 | | `task_subtype` | 否 | 任务卡 subtype 过滤。 |
| `order_status` | 待补 | 前端任务列表已有订单状态筛选控件,但当前后端 Controller 未接收该参数,前端先禁用该筛选。建议后端确认是否支持按任务所属订单状态过滤。 | | `order_status` | | 按任务所属订单状态过滤,支持 `TEMPORARY``ACTIVE``ENDED``LOGIC_DELETED`;不传时保持当前行为。 |
| `queue_participation` | 否 | 是否参与订单执行队列。 | | `queue_participation` | 否 | 是否参与订单执行队列。 |
| `keyword` | 否 | Group Code、Confirmation No、临时订单号、来源消息安全摘要关键词。 | | `keyword` | 否 | Group Code、Confirmation No、临时订单号、来源消息安全摘要关键词。 |
| `page_num` | 否 | 页码,建议从 1 开始。 | | `page_num` | 否 | 页码,建议从 1 开始。 |
@@ -324,7 +324,8 @@ GET /api/source-message-conversations/{externalConversationId}
- 邮件会话详情页需要展示完整正文或清洗后的 HTML、附件、内联图片、发件人摘要、发送 / 接收时间、主题和关联订单 / 任务。 - 邮件会话详情页需要展示完整正文或清洗后的 HTML、附件、内联图片、发件人摘要、发送 / 接收时间、主题和关联订单 / 任务。
- 前端不在页面上做业务截断或隐藏但仍只调用本项目后端接口不直接访问邮箱、AgentBus、数据库或外部附件 URL Secret。 - 前端不在页面上做业务截断或隐藏但仍只调用本项目后端接口不直接访问邮箱、AgentBus、数据库或外部附件 URL Secret。
- 如果后端仍需要审计原文读取,应由后端在该业务接口内部处理;前端不保存 `X-TH-Hotel-Source-Original-Read-Key` 一类受控访问 key。 - 如果后端仍需要审计原文读取,应由后端在该业务接口内部处理;前端不保存 `X-TH-Hotel-Source-Original-Read-Key` 一类受控访问 key。
- 2026-07-08 联调临时策略:前端为快速看效果,会优先直接渲染后端返回的 `html_body`;这不是最终安全方案。请后端明确标记 `html_body` 需要 sanitize并确认是否输出已清洗 HTML、是否增加 `html_body_sanitized` / `html_render_mode` 等字段,以及生产环境 CSP 和外链图片策略 - 2026-07-08 后端已新增 `html_body_sanitized``html_render_mode`;前端页面展示邮件 HTML 时应优先使用 `html_body_sanitized``html_body` 只作为原始内容兼容字段,不建议生产直渲
- 第一版仅处理 HTML 内容清洗;附件和内联图片 URL 来自本系统 OSS 服务,暂不做额外拦截或代理转换。
建议入参: 建议入参:
@@ -332,7 +333,7 @@ GET /api/source-message-conversations/{externalConversationId}
| --- | --- | --- | | --- | --- | --- |
| `sourceMessageId` | 是 | 入口来源消息 ID。后端据此定位 `externalConversationId`。 | | `sourceMessageId` | 是 | 入口来源消息 ID。后端据此定位 `externalConversationId`。 |
当前第一版不额外接收 `hotelId``includeBody``includeRelated`。后端默认按 SourceMessage 自身酒店上下文查询同会话邮件,返回完整 text/html 和关联订单 / 任务摘要,并在内部写原文读取审计。 当前第一版不额外接收 `hotelId``includeBody``includeRelated`。后端默认按 SourceMessage 自身酒店上下文查询同会话邮件,返回完整 text/html、清洗后的 HTML 和关联订单 / 任务摘要,并在内部写原文读取审计。
建议返参: 建议返参:
@@ -358,7 +359,9 @@ GET /api/source-message-conversations/{externalConversationId}
"source_sent_at": "2026-07-06T01:08:00Z", "source_sent_at": "2026-07-06T01:08:00Z",
"text_body": "完整邮件正文", "text_body": "完整邮件正文",
"html_body": "<p>完整邮件 HTML</p>", "html_body": "<p>完整邮件 HTML</p>",
"html_body_sanitized": "<p>完整邮件 HTML</p>",
"html_sanitize_required": true, "html_sanitize_required": true,
"html_render_mode": "SANITIZED_HTML",
"inline_images": [], "inline_images": [],
"attachments": [ "attachments": [
{ {
@@ -495,7 +498,7 @@ GET /api/reservation/tasks/{taskId}
- 如果前端只做“按后端字段直接渲染”,现有 `fields[]` 可以支撑第一版表单展示;本轮已经扩展 `ReservationTaskFieldResult`,避免前端维护第二套字段矩阵。 - 如果前端只做“按后端字段直接渲染”,现有 `fields[]` 可以支撑第一版表单展示;本轮已经扩展 `ReservationTaskFieldResult`,避免前端维护第二套字段矩阵。
- `result_type``task_type``task_subtype``default_value_source` 当前从后端字段矩阵定义透出。 - `result_type``task_type``task_subtype``default_value_source` 当前从后端字段矩阵定义透出。
- 如果后端已有更细的字段来源或适用场景元数据,可后续再扩展 `field_source``applicable_scenario`,不作为本轮 P0 阻塞项。 - 如果后端已有更细的字段来源或适用场景元数据,可后续再扩展 `field_source``applicable_scenario`,不作为本轮 P0 阻塞项。
- 前端已统一配置 `VITE_RESERVATION_HOTEL_ID`,并会在 `GET /api/reservation/orders``GET /api/reservation/tasks``GET /api/reservation/orders/{orderId}` 自动传 `hotel_id`。当前 `GET /api/reservation/tasks/{taskId}` 以及任务写操作 Controller 不接收 `hotel_id`,请后端确认 taskId / operationId 是否全局唯一;如果不是,需要补可选 `hotel_id` 以避免多酒店数据串读或误写 - 前端已统一配置 `VITE_RESERVATION_HOTEL_ID`,并会在 `GET /api/reservation/orders``GET /api/reservation/tasks``GET /api/reservation/orders/{orderId}` 自动传 `hotel_id`。当前 `GET /api/reservation/tasks/{taskId}` 以及任务写操作 Controller 不接收 `hotel_id`;第一版先按 ID 定位,后续多酒店隔离 / 权限方案统一补齐
## 9. Message Notification 列表 / 详情接口 ## 9. Message Notification 列表 / 详情接口

View File

@@ -311,14 +311,14 @@ Controller、Service、Service 实现类的方法必须有中文注释。Entity
当前实现记录: 当前实现记录:
- 已实现 `GET /api/reservation/tasks` 第一版,支持酒店、订单、任务类型、任务状态、任务 subtype、队列参与、关键词和分页筛选。 - 已实现 `GET /api/reservation/tasks` 第一版,支持酒店、订单、任务类型、任务状态、任务 subtype、任务所属订单状态、队列参与、关键词和分页筛选。
- 已实现 `GET /api/reservation/orders/{orderId}` 第一版,支持返回订单摘要和任务时间线,`include_tasks=false` 时只返回订单摘要。 - 已实现 `GET /api/reservation/orders/{orderId}` 第一版,支持返回订单摘要和任务时间线,`include_tasks=false` 时只返回订单摘要。
- 两个接口均复用同订单队列可处理状态计算,`FAILED``COMPLETED` 视为结束,不阻塞后续任务。 - 两个接口均复用同订单队列可处理状态计算,`FAILED``COMPLETED` 视为结束,不阻塞后续任务。
- 已补齐 `GET /api/reservation/tasks` 来源邮件会话摘要字段:`source_sender_summary``source_received_at``external_conversation_id``conversation_message_count` - 已补齐 `GET /api/reservation/tasks` 来源邮件会话摘要字段:`source_sender_summary``source_received_at``external_conversation_id``conversation_message_count`
- 已补齐 `GET /api/reservation/orders/{orderId}``tasks[]` 来源邮件会话摘要字段。 - 已补齐 `GET /api/reservation/orders/{orderId}``tasks[]` 来源邮件会话摘要字段。
- 已补齐 `GET /api/reservation/tasks/{taskId}` 顶层来源邮件会话字段,并在 `fields[]` 透出 `result_type``task_type``task_subtype``default_value_source` - 已补齐 `GET /api/reservation/tasks/{taskId}` 顶层来源邮件会话字段,并在 `fields[]` 透出 `result_type``task_type``task_subtype``default_value_source`
- 已实现订单列表接口 `GET /api/reservation/orders`默认查询全部订单状态支持酒店、订单状态、Group Code、Confirmation No.、关键词和分页筛选;`keyword` 可匹配订单字段,也可匹配来源消息安全摘要命中的 SourceMessage ID`open_task_count` 排除 `COMPLETED``FAILED` - 已实现订单列表接口 `GET /api/reservation/orders`默认查询全部订单状态支持酒店、订单状态、Group Code、Confirmation No.、关键词和分页筛选;`keyword` 可匹配订单字段,也可匹配来源消息安全摘要命中的 SourceMessage ID`open_task_count` 排除 `COMPLETED``FAILED`
- 已实现邮件会话详情接口 `GET /api/source-messages/{sourceMessageId}/conversation`,根据 SourceMessage 定位外部会话,返回完整 text/html、附件外链、内联图片、来源摘要和关联订单 / 任务摘要;原文读取审计由后端内部写入。 - 已实现邮件会话详情接口 `GET /api/source-messages/{sourceMessageId}/conversation`,根据 SourceMessage 定位外部会话,返回完整 text/html、`html_body_sanitized``html_render_mode`附件外链、内联图片、来源摘要和关联订单 / 任务摘要;原文读取审计由后端内部写入,前端展示 HTML 时优先使用清洗字段
- 已实现 dev/test 受控演示数据 seed 接口 `POST /api/system/reservation/demo-data`,默认关闭,需配置 `reservation.demo-data.enabled=true` 和访问口令;生成真实落库的任务列表、订单列表、订单详情、任务详情和邮件会话详情演示数据。 - 已实现 dev/test 受控演示数据 seed 接口 `POST /api/system/reservation/demo-data`,默认关闭,需配置 `reservation.demo-data.enabled=true` 和访问口令;生成真实落库的任务列表、订单列表、订单详情、任务详情和邮件会话详情演示数据。
- Message Notification 独立列表 / 详情、任务卡前端字段白名单独立接口继续后置;第一版分别复用任务列表 / 任务详情和 `fields[]` 元数据。 - Message Notification 独立列表 / 详情、任务卡前端字段白名单独立接口继续后置;第一版分别复用任务列表 / 任务详情和 `fields[]` 元数据。

View File

@@ -16,7 +16,9 @@ import java.util.List;
* @param sourceSentAt 邮件来源发送时间UTC * @param sourceSentAt 邮件来源发送时间UTC
* @param textBody 完整纯文本正文 * @param textBody 完整纯文本正文
* @param htmlBody 完整 HTML 正文,前端展示前必须 sanitize * @param htmlBody 完整 HTML 正文,前端展示前必须 sanitize
* @param htmlBodySanitized 后端第一版清洗后的 HTML前端生产展示应优先使用
* @param htmlSanitizeRequired 是否要求前端 sanitize HTML * @param htmlSanitizeRequired 是否要求前端 sanitize HTML
* @param htmlRenderMode HTML 渲染建议模式,例如 SANITIZED_HTML 或 TEXT_ONLY
* @param inlineImages 内联图片外链 * @param inlineImages 内联图片外链
* @param attachments 附件外链 * @param attachments 附件外链
* @param relatedOrders 关联订单摘要 * @param relatedOrders 关联订单摘要
@@ -39,8 +41,12 @@ public record SourceMessageConversationMessageResult(
String textBody, String textBody,
@JsonProperty("html_body") @JsonProperty("html_body")
String htmlBody, String htmlBody,
@JsonProperty("html_body_sanitized")
String htmlBodySanitized,
@JsonProperty("html_sanitize_required") @JsonProperty("html_sanitize_required")
Boolean htmlSanitizeRequired, Boolean htmlSanitizeRequired,
@JsonProperty("html_render_mode")
String htmlRenderMode,
@JsonProperty("inline_images") @JsonProperty("inline_images")
List<SourceMessageOriginalMediaResponse> inlineImages, List<SourceMessageOriginalMediaResponse> inlineImages,
List<SourceMessageOriginalMediaResponse> attachments, List<SourceMessageOriginalMediaResponse> attachments,

View File

@@ -18,6 +18,7 @@ import java.time.LocalDateTime;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.regex.Pattern;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -31,6 +32,20 @@ public class SourceMessageConversationServiceImpl implements SourceMessageConver
private static final String ACCESS_SCENE = "source-message-conversation"; private static final String ACCESS_SCENE = "source-message-conversation";
private static final String MEDIA_TYPE_INLINE_IMAGE = "INLINE_IMAGE"; private static final String MEDIA_TYPE_INLINE_IMAGE = "INLINE_IMAGE";
private static final String MEDIA_TYPE_ATTACHMENT = "ATTACHMENT"; private static final String MEDIA_TYPE_ATTACHMENT = "ATTACHMENT";
private static final String HTML_RENDER_MODE_SANITIZED = "SANITIZED_HTML";
private static final String HTML_RENDER_MODE_TEXT_ONLY = "TEXT_ONLY";
private static final Pattern DANGEROUS_TAG_PATTERN = Pattern.compile(
"(?is)<\\s*(script|style|iframe|object|embed|meta|link|base|form)[^>]*>.*?<\\s*/\\s*\\1\\s*>"
+ "|<\\s*(script|style|iframe|object|embed|meta|link|base|form)[^>]*/?\\s*>");
private static final Pattern EVENT_ATTRIBUTE_PATTERN = Pattern.compile(
"(?i)\\s+on[a-z0-9_-]+\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s>]+)");
private static final Pattern STYLE_ATTRIBUTE_PATTERN = Pattern.compile(
"(?i)\\s+style\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s>]+)");
private static final Pattern DANGEROUS_URL_ATTRIBUTE_PATTERN = Pattern.compile(
"(?i)\\s+(href|src|xlink:href|formaction)\\s*=\\s*"
+ "(\"\\s*(?:javascript|data|vbscript):[^\"]*\""
+ "|'\\s*(?:javascript|data|vbscript):[^']*'"
+ "|\\s*(?:javascript|data|vbscript):[^\\s>]+)");
private final SourceMessageInboxRepository inboxRepository; private final SourceMessageInboxRepository inboxRepository;
private final List<SourceMessageRelatedContextProvider> relatedContextProviders; private final List<SourceMessageRelatedContextProvider> relatedContextProviders;
@@ -101,6 +116,7 @@ public class SourceMessageConversationServiceImpl implements SourceMessageConver
private SourceMessageConversationMessageResult toConversationMessage(SourceMessageInboxSnapshot message) { private SourceMessageConversationMessageResult toConversationMessage(SourceMessageInboxSnapshot message) {
SourceMessageOriginalContent originalContent = readOriginalAndAudit(message); SourceMessageOriginalContent originalContent = readOriginalAndAudit(message);
SourceMessageRelatedContextResult relatedContext = findRelatedContext(message); SourceMessageRelatedContextResult relatedContext = findRelatedContext(message);
String htmlBody = originalContent.htmlBody();
return new SourceMessageConversationMessageResult( return new SourceMessageConversationMessageResult(
message.id().toString(), message.id().toString(),
message.externalMessageId(), message.externalMessageId(),
@@ -110,8 +126,10 @@ public class SourceMessageConversationServiceImpl implements SourceMessageConver
UtcTimeFormatter.toUtcOffsetDateTime(message.receivedAt()), UtcTimeFormatter.toUtcOffsetDateTime(message.receivedAt()),
UtcTimeFormatter.toUtcOffsetDateTime(message.sourceSentAt()), UtcTimeFormatter.toUtcOffsetDateTime(message.sourceSentAt()),
originalContent.textBody(), originalContent.textBody(),
originalContent.htmlBody(), htmlBody,
sanitizeHtml(htmlBody),
true, true,
htmlRenderMode(htmlBody),
filterMedia(originalContent.mediaItems(), MEDIA_TYPE_INLINE_IMAGE), filterMedia(originalContent.mediaItems(), MEDIA_TYPE_INLINE_IMAGE),
filterMedia(originalContent.mediaItems(), MEDIA_TYPE_ATTACHMENT), filterMedia(originalContent.mediaItems(), MEDIA_TYPE_ATTACHMENT),
relatedContext.relatedOrders(), relatedContext.relatedOrders(),
@@ -173,4 +191,27 @@ public class SourceMessageConversationServiceImpl implements SourceMessageConver
item.externalUrl(), item.externalUrl(),
item.externalMediaId()); item.externalMediaId());
} }
/**
* 第一版 HTML 清洗:保留邮件正文基本结构,移除脚本标签、事件属性和危险协议链接。
*/
private String sanitizeHtml(String htmlBody) {
if (htmlBody == null) {
return null;
}
String sanitized = DANGEROUS_TAG_PATTERN.matcher(htmlBody).replaceAll("");
sanitized = EVENT_ATTRIBUTE_PATTERN.matcher(sanitized).replaceAll("");
sanitized = STYLE_ATTRIBUTE_PATTERN.matcher(sanitized).replaceAll("");
return DANGEROUS_URL_ATTRIBUTE_PATTERN.matcher(sanitized).replaceAll("");
}
/**
* 返回前端渲染建议;有 HTML 时优先使用后端清洗后的安全 HTML。
*/
private String htmlRenderMode(String htmlBody) {
if (htmlBody == null || htmlBody.isBlank()) {
return HTML_RENDER_MODE_TEXT_ONLY;
}
return HTML_RENDER_MODE_SANITIZED;
}
} }

View File

@@ -8,6 +8,7 @@ package cn.nianxx.thhotel.workflows.reservation.common.request;
* @param taskType 系统主任务类型过滤 * @param taskType 系统主任务类型过滤
* @param taskStatus 任务状态过滤 * @param taskStatus 任务状态过滤
* @param taskSubtype 任务 subtype 过滤 * @param taskSubtype 任务 subtype 过滤
* @param orderStatus 任务所属订单状态过滤
* @param queueParticipation 是否参与订单执行队列 * @param queueParticipation 是否参与订单执行队列
* @param keyword 业务号、临时订单号、任务字段或来源消息安全摘要关键词 * @param keyword 业务号、临时订单号、任务字段或来源消息安全摘要关键词
* @param pageNum 页码,从 1 开始 * @param pageNum 页码,从 1 开始
@@ -19,6 +20,7 @@ public record ReservationTaskWorkbenchQueryRequest(
String taskType, String taskType,
String taskStatus, String taskStatus,
String taskSubtype, String taskSubtype,
String orderStatus,
Boolean queueParticipation, Boolean queueParticipation,
String keyword, String keyword,
Integer pageNum, Integer pageNum,

View File

@@ -39,6 +39,7 @@ public class ReservationFrontendQueryController {
@RequestParam(name = "task_type", required = false) String taskType, @RequestParam(name = "task_type", required = false) String taskType,
@RequestParam(name = "task_status", required = false) String taskStatus, @RequestParam(name = "task_status", required = false) String taskStatus,
@RequestParam(name = "task_subtype", required = false) String taskSubtype, @RequestParam(name = "task_subtype", required = false) String taskSubtype,
@RequestParam(name = "order_status", required = false) String orderStatus,
@RequestParam(name = "queue_participation", required = false) Boolean queueParticipation, @RequestParam(name = "queue_participation", required = false) Boolean queueParticipation,
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(name = "page_num", required = false) Integer pageNum, @RequestParam(name = "page_num", required = false) Integer pageNum,
@@ -49,6 +50,7 @@ public class ReservationFrontendQueryController {
taskType, taskType,
taskStatus, taskStatus,
taskSubtype, taskSubtype,
orderStatus,
queueParticipation, queueParticipation,
keyword, keyword,
pageNum, pageNum,

View File

@@ -2,11 +2,78 @@ package cn.nianxx.thhotel.workflows.reservation.mapper;
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationTaskEntity; import cn.nianxx.thhotel.workflows.reservation.domain.ReservationTaskEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.util.List;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/** /**
* Reservation 任务 Mapper只负责本表持久化访问。 * Reservation 任务 Mapper只负责本表持久化访问。
*/ */
@Mapper @Mapper
public interface ReservationTaskMapper extends BaseMapper<ReservationTaskEntity> { public interface ReservationTaskMapper extends BaseMapper<ReservationTaskEntity> {
/**
* 分页查询前端任务列表摘要,支持按任务所属订单状态过滤。
*/
@Select("""
<script>
SELECT t.*
FROM workflow_reservation_task t
LEFT JOIN workflow_reservation_order o
ON o.hotel_id = t.hotel_id
AND o.id = t.order_id
WHERE t.hotel_id = #{hotelId}
<if test="orderId != null">
AND t.order_id = #{orderId}
</if>
<if test="orderStatus != null and orderStatus != ''">
AND o.order_status = #{orderStatus}
</if>
<if test="taskType != null and taskType != ''">
AND t.system_task_type = #{taskType}
</if>
<if test="taskStatus != null and taskStatus != ''">
AND t.task_status = #{taskStatus}
</if>
<if test="taskSubtype != null and taskSubtype != ''">
AND t.task_subtype = #{taskSubtype}
</if>
<if test="queueParticipation != null">
AND t.queue_participation = #{queueParticipation}
</if>
<if test="keyword != null and keyword != ''">
AND (
t.system_task_type LIKE CONCAT('%', #{keyword}, '%')
OR t.task_subtype LIKE CONCAT('%', #{keyword}, '%')
<if test="keywordOrderIds != null and keywordOrderIds.size() > 0">
OR t.order_id IN
<foreach collection="keywordOrderIds" item="orderIdItem" open="(" separator="," close=")">
#{orderIdItem}
</foreach>
</if>
<if test="sourceMessageIds != null and sourceMessageIds.size() > 0">
OR t.source_message_id IN
<foreach collection="sourceMessageIds" item="sourceMessageIdItem" open="(" separator="," close=")">
#{sourceMessageIdItem}
</foreach>
</if>
)
</if>
ORDER BY t.order_id ASC, t.execution_order ASC
</script>
""")
Page<ReservationTaskEntity> selectFrontendTaskPage(
Page<ReservationTaskEntity> page,
@Param("hotelId") String hotelId,
@Param("orderId") Long orderId,
@Param("orderStatus") String orderStatus,
@Param("taskType") String taskType,
@Param("taskStatus") String taskStatus,
@Param("taskSubtype") String taskSubtype,
@Param("queueParticipation") Boolean queueParticipation,
@Param("keyword") String keyword,
@Param("keywordOrderIds") List<Long> keywordOrderIds,
@Param("sourceMessageIds") List<Long> sourceMessageIds);
} }

View File

@@ -453,29 +453,18 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
int pageSize) { int pageSize) {
List<Long> keywordOrderIds = findOrderIdsByKeyword(request.hotelId(), request.keyword()); List<Long> keywordOrderIds = findOrderIdsByKeyword(request.hotelId(), request.keyword());
List<Long> sourceMessageIds = keywordSourceMessageIds == null ? List.of() : keywordSourceMessageIds; List<Long> sourceMessageIds = keywordSourceMessageIds == null ? List.of() : keywordSourceMessageIds;
Page<ReservationTaskEntity> page = taskMapper.selectPage(Page.of(pageNum, pageSize), Page<ReservationTaskEntity> page = taskMapper.selectFrontendTaskPage(
Wrappers.<ReservationTaskEntity>lambdaQuery() Page.of(pageNum, pageSize),
.eq(ReservationTaskEntity::getHotelId, request.hotelId()) request.hotelId(),
.eq(request.orderId() != null, ReservationTaskEntity::getOrderId, request.orderId()) request.orderId(),
.eq(hasText(request.taskType()), ReservationTaskEntity::getSystemTaskType, trim(request.taskType())) trim(request.orderStatus()),
.eq(hasText(request.taskStatus()), ReservationTaskEntity::getTaskStatus, trim(request.taskStatus())) trim(request.taskType()),
.eq(hasText(request.taskSubtype()), ReservationTaskEntity::getTaskSubtype, trim(request.taskSubtype())) trim(request.taskStatus()),
.eq(request.queueParticipation() != null, trim(request.taskSubtype()),
ReservationTaskEntity::getQueueParticipation, request.queueParticipation(),
request.queueParticipation()) trim(request.keyword()),
.and(hasText(request.keyword()), wrapper -> { keywordOrderIds,
wrapper.like(ReservationTaskEntity::getSystemTaskType, trim(request.keyword())) sourceMessageIds);
.or()
.like(ReservationTaskEntity::getTaskSubtype, trim(request.keyword()));
if (!keywordOrderIds.isEmpty()) {
wrapper.or().in(ReservationTaskEntity::getOrderId, keywordOrderIds);
}
if (!sourceMessageIds.isEmpty()) {
wrapper.or().in(ReservationTaskEntity::getSourceMessageId, sourceMessageIds);
}
})
.orderByAsc(ReservationTaskEntity::getOrderId)
.orderByAsc(ReservationTaskEntity::getExecutionOrder));
return new ReservationPageSnapshot<>( return new ReservationPageSnapshot<>(
toAiQueryTaskSnapshots(request.hotelId(), page.getRecords()), toAiQueryTaskSnapshots(request.hotelId(), page.getRecords()),
page.getTotal(), page.getTotal(),

View File

@@ -177,6 +177,7 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
null, null,
null, null,
null, null,
null,
DEFAULT_PAGE_NUM, DEFAULT_PAGE_NUM,
DEFAULT_PAGE_SIZE); DEFAULT_PAGE_SIZE);
} }
@@ -186,6 +187,7 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
trimToNull(request.taskType()), trimToNull(request.taskType()),
trimToNull(request.taskStatus()), trimToNull(request.taskStatus()),
trimToNull(request.taskSubtype()), trimToNull(request.taskSubtype()),
trimToNull(request.orderStatus()),
request.queueParticipation(), request.queueParticipation(),
trimToNull(request.keyword()), trimToNull(request.keyword()),
request.pageNum(), request.pageNum(),

View File

@@ -132,7 +132,8 @@ class SourceMessageControllerTest {
"mail-conversation-p0-001", "mail-conversation-p0-001",
"conversation-p0-001", "conversation-p0-001",
"First full text body with booking context.", "First full text body with booking context.",
"<html><body><p>First HTML body</p><img src=\"cid:image-001\" /></body></html>", "<html><body><p>First HTML body</p><img src=\"cid:image-001\" onerror=\"alert(1)\" />"
+ "<a href=\"javascript:alert(2)\">unsafe</a><script>alert(3)</script></body></html>",
List.of( List.of(
media("INLINE_IMAGE", "image-001.png", "image/png", 100L, media("INLINE_IMAGE", "image-001.png", "image/png", 100L,
"https://media.example.test/image-001.png?token=secret", "image-001"), "https://media.example.test/image-001.png?token=secret", "image-001"),
@@ -161,6 +162,20 @@ class SourceMessageControllerTest {
.value(contains(containsString("First full text body")))) .value(contains(containsString("First full text body"))))
.andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_body") .andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_body")
.value(contains(containsString("First HTML body")))) .value(contains(containsString("First HTML body"))))
.andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_body")
.value(contains(containsString("<script>alert(3)</script>"))))
.andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_body_sanitized")
.value(contains(containsString("First HTML body"))))
.andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_body_sanitized")
.value(contains(not(containsString("<script")))))
.andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_body_sanitized")
.value(contains(not(containsString("onerror")))))
.andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_body_sanitized")
.value(contains(not(containsString("javascript:")))))
.andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_sanitize_required")
.value(contains(true)))
.andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].html_render_mode")
.value(contains("SANITIZED_HTML")))
.andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].inline_images[0].externalUrl") .andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].inline_images[0].externalUrl")
.value(contains("https://media.example.test/image-001.png?token=secret"))) .value(contains("https://media.example.test/image-001.png?token=secret")))
.andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].attachments[0].externalUrl") .andExpect(jsonPath("$.messages[?(@.external_message_id=='mail-conversation-p0-001')].attachments[0].externalUrl")

View File

@@ -122,6 +122,40 @@ class ReservationFrontendQueryControllerTest {
verify(workflowRepository, never()).findQueueTasksBefore(anyString(), anyLong(), any()); verify(workflowRepository, never()).findQueueTasksBefore(anyString(), anyLong(), any());
} }
@Test
void shouldFilterTaskWorkbenchListByOrderStatus() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage(
"mail-frontend-task-order-status-001",
"Frontend Query Task Order Status Smoke");
Long activeOrderId = 930000000000001101L;
Long endedOrderId = 930000000000001102L;
Long activeTaskId = 930000000000001301L;
Long endedTaskId = 930000000000001302L;
insertGroupOrder(activeOrderId, source.inboxId(), "GRP-FRONTEND-TASK-ACTIVE-001", "ACTIVE");
insertGroupOrder(endedOrderId, source.inboxId(), "GRP-FRONTEND-TASK-ENDED-001", "ENDED");
insertTransition(930000000000001201L, source.inboxId(), 1, "GRP-FRONTEND-TASK-ACTIVE-001",
"New Booking", "NEW_BOOKING", "NEW_BOOKING");
insertTransition(930000000000001202L, source.inboxId(), 2, "GRP-FRONTEND-TASK-ENDED-001",
"Cancel Booking", "CANCEL_BOOKING", "CANCEL_BOOKING");
insertTask(activeTaskId, activeOrderId, source.inboxId(), 930000000000001201L, "New Booking",
"NEW_BOOKING", "NEW_BOOKING", "PENDING_CONFIRM", 1);
insertTask(endedTaskId, endedOrderId, source.inboxId(), 930000000000001202L, "Cancel Booking",
"CANCEL_BOOKING", "CANCEL_BOOKING", "PENDING_CONFIRM", 1);
mockMvc.perform(get("/api/reservation/tasks")
.param("hotel_id", HOTEL_ID)
.param("order_status", "ENDED")
.param("keyword", "GRP-FRONTEND-TASK-")
.param("page_num", "1")
.param("page_size", "20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items.length()").value(1))
.andExpect(jsonPath("$.items[0].task_id").value(endedTaskId.toString()))
.andExpect(jsonPath("$.items[0].order_id").value(endedOrderId.toString()))
.andExpect(jsonPath("$.items[0].display_order_key").value("GRP-FRONTEND-TASK-ENDED-001"))
.andExpect(jsonPath("$.page.total").value(1));
}
@Test @Test
void shouldReturnOrderDetailWithTaskTimeline() throws Exception { void shouldReturnOrderDetailWithTaskTimeline() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage( SourceMessageCaptureResult source = captureSourceMessage(