补齐任务列表状态筛选和邮件HTML安全字段
This commit is contained in:
@@ -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 仍是本地占位。
|
||||||
|
|||||||
@@ -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,前端类型定义需要单独处理。
|
||||||
|
|
||||||
|
|||||||
@@ -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 列表 / 详情接口
|
||||||
|
|
||||||
|
|||||||
@@ -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[]` 元数据。
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user