修正邮件接收时间和发件人展示

This commit is contained in:
andy
2026-07-10 17:36:56 +08:00
parent 4745e20b9a
commit e76cb80f28
25 changed files with 226 additions and 59 deletions

View File

@@ -308,6 +308,8 @@ GET <AGENTBUS_WS_URL>?ready=1
- `source.channel` - `source.channel`
- `source.external_message_id` - `source.external_message_id`
- `source.external_conversation_id` - `source.external_conversation_id`
- payload 或 source 中的 `received_at`
- `source.sent_at`
- frame id - frame id
- session id - session id
- sender - sender
@@ -322,6 +324,9 @@ GET <AGENTBUS_WS_URL>?ready=1
| `source.channel` | `channel` | | `source.channel` | `channel` |
| `source.external_message_id` | `externalMessageId`,作为幂等键组成部分 | | `source.external_message_id` | `externalMessageId`,作为幂等键组成部分 |
| `source.external_conversation_id` | `externalConversationId` | | `source.external_conversation_id` | `externalConversationId` |
| payload 或 source 中的 `received_at` | `receivedAt`,作为消息来源接收时间 |
| `source.sent_at` | `sourceSentAt`,作为消息来源发送时间 |
| sender | `senderSummary`,是否打码由项目业务和权限策略决定 |
| frame id | `providerFrameId` | | frame id | `providerFrameId` |
| session id | `providerSessionId` | | session id | `providerSessionId` |
| payload 规范 JSON | `payloadJson``payloadSha256` | | payload 规范 JSON | `payloadJson``payloadSha256` |

View File

@@ -103,8 +103,8 @@ POST /api/auth/logout
| --- | --- | --- | | --- | --- | --- |
| `source_message_id` | 本系统内部 SourceMessage Inbox ID。 | 打开邮件会话详情时作为路径参数传入 `/api/source-messages/{sourceMessageId}/conversation`。 | | `source_message_id` | 本系统内部 SourceMessage Inbox ID。 | 打开邮件会话详情时作为路径参数传入 `/api/source-messages/{sourceMessageId}/conversation`。 |
| `source_subject` | 来源邮件主题安全摘要。 | 用于列表或任务详情标题旁展示,不代表完整邮件主题一定无敏感信息。 | | `source_subject` | 来源邮件主题安全摘要。 | 用于列表或任务详情标题旁展示,不代表完整邮件主题一定无敏感信息。 |
| `source_sender_summary` | 来源发件人安全摘要。 | 用于辅助用户判断邮件来源。 | | `source_sender_summary` | 来源发件人展示值,当前不打码。 | 用于辅助用户判断邮件来源。 |
| `source_received_at` | 本系统接收来源消息时间UTC。 | 前端展示时按用户或酒店时区格式化。 | | `source_received_at` | 邮件来源接收时间UTC优先取 AgentBus payload `received_at`,缺失时使用本系统接收时间。 | 前端展示时按用户或酒店时区格式化。 |
| `external_conversation_id` | 外部邮件会话 ID。 | 仅用于展示或调试,不作为当前会话详情接口路径参数。 | | `external_conversation_id` | 外部邮件会话 ID。 | 仅用于展示或调试,不作为当前会话详情接口路径参数。 |
| `conversation_message_count` | 同一外部会话下的邮件数量。 | 用于提示用户打开的是整段会话,不是单封邮件。 | | `conversation_message_count` | 同一外部会话下的邮件数量。 | 用于提示用户打开的是整段会话,不是单封邮件。 |
@@ -113,7 +113,7 @@ POST /api/auth/logout
- `GET /api/source-messages/{sourceMessageId}/conversation` 只接收路径参数 `sourceMessageId`;第一版不接收 `hotelId``includeBody``includeRelated` - `GET /api/source-messages/{sourceMessageId}/conversation` 只接收路径参数 `sourceMessageId`;第一版不接收 `hotelId``includeBody``includeRelated`
- Reservation 列表、任务列表和订单详情默认不需要前端传 `hotel_id`;如果前端已经接入酒店选择器,可以把当前选中酒店作为可选 `hotel_id` 传给后端。任务详情、任务写操作和邮件会话详情当前仍按对象 ID 定位,不接收该参数。 - Reservation 列表、任务列表和订单详情默认不需要前端传 `hotel_id`;如果前端已经接入酒店选择器,可以把当前选中酒店作为可选 `hotel_id` 传给后端。任务详情、任务写操作和邮件会话详情当前仍按对象 ID 定位,不接收该参数。
- 后端会根据 `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` 是原始 HTML 兼容字段;`html_body_sanitized` 是后端第一版清洗结果,已移除脚本标签、事件属性和危险协议链接。前端生产展示必须优先使用 `html_body_sanitized`,并可用 `html_render_mode=SANITIZED_HTML` 判断渲染模式。 - `html_body` 是原始 HTML 兼容字段;`html_body_sanitized` 是后端第一版清洗结果,已移除脚本标签、事件属性和危险协议链接。前端生产展示必须优先使用 `html_body_sanitized`,并可用 `html_render_mode=SANITIZED_HTML` 判断渲染模式。
- 第一版仅处理 HTML 内容安全;`inline_images[]``attachments[]``externalUrl` 来自本系统 OSS 服务暂不做额外拦截但前端仍不得写入普通日志、错误上报、localStorage 或 URL query。 - 第一版仅处理 HTML 内容安全;`inline_images[]``attachments[]``externalUrl` 来自本系统 OSS 服务暂不做额外拦截但前端仍不得写入普通日志、错误上报、localStorage 或 URL query。

View File

@@ -136,8 +136,8 @@ GET /api/reservation/tasks
| 字段 | 说明 | | 字段 | 说明 |
| --- | --- | | --- | --- |
| `source_sender_summary` | 来源消息发件人摘要,用于任务列表快速判断来源。 | | `source_sender_summary` | 来源消息发件人展示值,当前不打码,用于任务列表快速判断来源。 |
| `source_received_at` | 来源消息接收时间,用于任务列表排序和展示。 | | `source_received_at` | 邮件来源接收时间,优先取 AgentBus payload `received_at`,用于任务列表排序和展示。 |
| `external_conversation_id` | 来源消息所属邮件会话 ID用于打开完整邮件会话详情。 | | `external_conversation_id` | 来源消息所属邮件会话 ID用于打开完整邮件会话详情。 |
| `conversation_message_count` | 会话内邮件数量,用于提示用户该入口是整段会话,不是单封邮件。 | | `conversation_message_count` | 会话内邮件数量,用于提示用户该入口是整段会话,不是单封邮件。 |
@@ -209,8 +209,8 @@ GET /api/reservation/orders/{orderId}
| --- | --- | | --- | --- |
| `source_message_id` | 任务对应的来源 SourceMessage Inbox ID。 | | `source_message_id` | 任务对应的来源 SourceMessage Inbox ID。 |
| `source_subject` | 来源消息主题摘要。 | | `source_subject` | 来源消息主题摘要。 |
| `source_sender_summary` | 来源消息发件人摘要。 | | `source_sender_summary` | 来源消息发件人展示值,当前不打码。 |
| `source_received_at` | 来源消息接收时间。 | | `source_received_at` | 邮件来源接收时间,优先取 AgentBus payload `received_at`。 |
| `external_conversation_id` | 来源消息所属邮件会话 ID。 | | `external_conversation_id` | 来源消息所属邮件会话 ID。 |
| `conversation_message_count` | 会话内邮件数量。 | | `conversation_message_count` | 会话内邮件数量。 |
@@ -322,7 +322,7 @@ GET /api/source-message-conversations/{externalConversationId}
中文说明: 中文说明:
- “全部邮件”指同一个 `externalConversationId` 下的历史邮件、当前邮件和后续回复,不是只展示任务对应的单封来源邮件。 - “全部邮件”指同一个 `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_sanitized``html_render_mode`;前端页面展示邮件 HTML 时应优先使用 `html_body_sanitized``html_body` 只作为原始内容兼容字段,不建议生产直渲。 - 2026-07-08 后端已新增 `html_body_sanitized``html_render_mode`;前端页面展示邮件 HTML 时应优先使用 `html_body_sanitized``html_body` 只作为原始内容兼容字段,不建议生产直渲。
@@ -450,8 +450,8 @@ GET /api/reservation/tasks/{taskId}
| 位置 | 字段 | 说明 | | 位置 | 字段 | 说明 |
| --- | --- | --- | | --- | --- | --- |
| 顶层 | `source_subject` | 任务来源消息主题摘要。 | | 顶层 | `source_subject` | 任务来源消息主题摘要。 |
| 顶层 | `source_sender_summary` | 任务来源消息发件人摘要。 | | 顶层 | `source_sender_summary` | 任务来源消息发件人展示值,当前不打码。 |
| 顶层 | `source_received_at` | 任务来源消息接收时间。 | | 顶层 | `source_received_at` | 任务来源邮件接收时间,优先取 AgentBus payload `received_at`。 |
| 顶层 | `external_conversation_id` | 任务来源消息所属邮件会话 ID。 | | 顶层 | `external_conversation_id` | 任务来源消息所属邮件会话 ID。 |
| 顶层 | `conversation_message_count` | 会话内邮件数量。 | | 顶层 | `conversation_message_count` | 会话内邮件数量。 |
| `fields[]` | `result_type` | 3.0 字段表中的结果类型,用于前端调试和字段分组校验。 | | `fields[]` | `result_type` | 3.0 字段表中的结果类型,用于前端调试和字段分组校验。 |

View File

@@ -462,7 +462,7 @@ pnpm test -- SomeSpecName.spec.ts
- 邮件会话详情页展示同一个 `external_conversation_id` 下的全部邮件,包括历史邮件、当前邮件和后续回复。 - 邮件会话详情页展示同一个 `external_conversation_id` 下的全部邮件,包括历史邮件、当前邮件和后续回复。
- 页面入口可以来自订单详情任务队列、当前任务卡证据区或任务详情页,但落地页统一为“邮件会话详情”,不是单封邮件详情。 - 页面入口可以来自订单详情任务队列、当前任务卡证据区或任务详情页,但落地页统一为“邮件会话详情”,不是单封邮件详情。
- 每封邮件应展示完整正文或清洗后的 HTML、附件列表、内联图片信息、发送 / 接收时间、发件人摘要、主题和关联订单 / 任务。 - 每封邮件应展示完整正文或清洗后的 HTML、附件列表、内联图片信息、发送 / 接收时间、发件人展示值、主题和关联订单 / 任务。
- 前端不在本页展示假数据;后端接口未接入时显示“接口待接入 / 暂无邮件会话数据”。 - 前端不在本页展示假数据;后端接口未接入时显示“接口待接入 / 暂无邮件会话数据”。
- 邮件全文属于用户在邮箱中本来可见的信息,前端页面不做业务层面的截断隐藏;但仍必须通过本项目后端接口读取,不在前端保存外部系统 Secret 或访问 key。 - 邮件全文属于用户在邮箱中本来可见的信息,前端页面不做业务层面的截断隐藏;但仍必须通过本项目后端接口读取,不在前端保存外部系统 Secret 或访问 key。

View File

@@ -211,7 +211,7 @@
- 表和字段中文注释能正常创建。 - 表和字段中文注释能正常创建。
- 数据库业务时间点按 UTC 写入,接口层返回带 `Z` 的 ISO 8601 UTC 时间,例如 `2026-07-08T03:00:00Z` - 数据库业务时间点按 UTC 写入,接口层返回带 `Z` 的 ISO 8601 UTC 时间,例如 `2026-07-08T03:00:00Z`
- MySQL JDBC URL 建议明确 `serverTimezone=UTC`;部署容器和 JVM 也应使用 UTC或至少确认应用代码所有入库时间均通过 UTC 时钟生成。 - MySQL JDBC URL 建议明确 `serverTimezone=UTC`;部署容器和 JVM 也应使用 UTC或至少确认应用代码所有入库时间均通过 UTC 时钟生成。
- AgentBus 邮件来源时间、SuperAgent HMAC timestamp、本系统 `created_at` / `updated_at` / `received_at` 等时间点统一按 UTC 理解;前端展示时再按用户或酒店时区格式化。 - AgentBus 邮件来源时间、SuperAgent HMAC timestamp、本系统 `created_at` / `updated_at` 等时间点统一按 UTC 理解;SourceMessage `received_at` 优先保存 AgentBus payload `received_at`,缺失时回退本系统接收时间,前端展示时再按用户或酒店时区格式化。
- 入住日期、离店日期、酒店营业日属于酒店本地业务日期,不应因为 UTC 换算而自动前后偏移。 - 入住日期、离店日期、酒店营业日属于酒店本地业务日期,不应因为 UTC 换算而自动前后偏移。
- 执行 V4 前,如果目标库已有 M002 试运行数据,必须先检查 ACTIVE 订单业务号重复和同订单任务队列序号重复。 - 执行 V4 前,如果目标库已有 M002 试运行数据,必须先检查 ACTIVE 订单业务号重复和同订单任务队列序号重复。
- 执行 V5 / V6 前,如果目标库已有 M002 试运行数据,必须确认任务草稿、确认 payload 和 OPERA 模拟操作表允许从空数据开始补齐;不要手工伪造已确认 payload 或 attempt 历史。 - 执行 V5 / V6 前,如果目标库已有 M002 试运行数据,必须确认任务草稿、确认 payload 和 OPERA 模拟操作表允许从空数据开始补齐;不要手工伪造已确认 payload 或 attempt 历史。
@@ -255,12 +255,12 @@ HAVING COUNT(*) > 1;
- Authorization、Cookie、CSRF Token。 - Authorization、Cookie、CSRF Token。
- Provider API Key、AgentBus Token、数据库密码。 - Provider API Key、AgentBus Token、数据库密码。
- 邮件正文、HTML、附件 URL、原始 payload。 - 邮件正文、HTML、附件 URL、原始 payload。
- 客户姓名、完整邮箱、电话、证件号。 - 客户姓名、电话、证件号;发件人展示字段因业务识别需要允许返回完整邮箱或名称,但不得写入普通日志、错误响应或调试状态接口
- 支付信息。 - 支付信息。
SourceMessage 普通列表和普通详情只能返回安全摘要: SourceMessage 普通列表和普通详情只能返回安全摘要:
- 可以返回SourceMessage ID、酒店 ID、provider、channel、外部邮件 ID、外部邮件链 ID、状态、时间、发送人摘要、主题摘要、安全短摘要。 - 可以返回SourceMessage ID、酒店 ID、provider、channel、外部邮件 ID、外部邮件链 ID、状态、时间、发件人展示值、主题摘要、安全短摘要。
- 不应返回完整正文、HTML、附件 URL、原始 payload。 - 不应返回完整正文、HTML、附件 URL、原始 payload。
原文读取接口只允许在明确授权场景下使用: 原文读取接口只允许在明确授权场景下使用:

View File

@@ -348,12 +348,14 @@ body.html
body.text body.text
inline_images[] inline_images[]
attachments[] attachments[]
received_at
source.channel source.channel
source.channel_account source.channel_account
source.external_message_id source.external_message_id
source.external_conversation_id source.external_conversation_id
source.sender source.sender
source.subject source.subject
source.sent_at
source.web_link source.web_link
reply_policy.mode reply_policy.mode
reply_policy.final_only reply_policy.final_only
@@ -366,6 +368,9 @@ reply_policy.final_only
| `source.channel` | `channel`,例如 `EMAIL` | | `source.channel` | `channel`,例如 `EMAIL` |
| `source.external_message_id` | `externalMessageId`,作为幂等键组成部分 | | `source.external_message_id` | `externalMessageId`,作为幂等键组成部分 |
| `source.external_conversation_id` | `externalConversationId` | | `source.external_conversation_id` | `externalConversationId` |
| `payload.received_at``source.received_at` | `receivedAt`,作为邮件来源接收时间 |
| `source.sent_at` | `sourceSentAt`,作为邮件来源发送时间 |
| `source.sender` | `senderSummary`,当前作为发件人展示值不打码 |
| frame `id` | `agentbusFrameId` | | frame `id` | `agentbusFrameId` |
| frame `session_id` | `agentbusSessionId` | | frame `session_id` | `agentbusSessionId` |
| `payload` 规范 JSON | `payloadJson``payloadSha256` | | `payload` 规范 JSON | `payloadJson``payloadSha256` |

View File

@@ -117,7 +117,7 @@ AgentBus 邮件 JSON
- 幂等去重:同一酒店、同一 provider、同一 channel、同一外部邮件 ID 不重复建记录。 - 幂等去重:同一酒店、同一 provider、同一 channel、同一外部邮件 ID 不重复建记录。
- 历史查询:查询本项目已经接收并落库过的邮件。 - 历史查询:查询本项目已经接收并落库过的邮件。
- 邮件链查询:按 `externalConversationId` 查询同一邮件链下的记录。 - 邮件链查询:按 `externalConversationId` 查询同一邮件链下的记录。
- 安全摘要:列表只返回主题、发送人摘要、时间、状态、短摘要等安全信息。 - 安全摘要:列表只返回主题摘要、发件人展示值、时间、状态、短摘要等受控展示信息。
- 原文读取:提供统一受控接口读取 text、HTML、正文图片 URL、附件 URL。 - 原文读取:提供统一受控接口读取 text、HTML、正文图片 URL、附件 URL。
- 媒体引用保存:保存 AgentBus 已处理好的长期有效媒体 URL不下载、不转存、不刷新。 - 媒体引用保存:保存 AgentBus 已处理好的长期有效媒体 URL不下载、不转存、不刷新。
- 扩展来源:后续可以接入 Microsoft Graph、IMAP、Webhook、手工导入等来源。 - 扩展来源:后续可以接入 Microsoft Graph、IMAP、Webhook、手工导入等来源。
@@ -152,9 +152,9 @@ source_message_media
| `providerSessionId` | AgentBus session ID用于排查连接会话不作为邮件链 ID | | `providerSessionId` | AgentBus session ID用于排查连接会话不作为邮件链 ID |
| `payloadSha256` | 规范 JSON 的哈希值,用于排查 payload 是否变化 | | `payloadSha256` | 规范 JSON 的哈希值,用于排查 payload 是否变化 |
| `captureStatus` | 捕获状态,例如 `RECEIVED``FAILED` | | `captureStatus` | 捕获状态,例如 `RECEIVED``FAILED` |
| `receivedAt` | 本项目接收时间 | | `receivedAt` | 邮件来源接收时间,优先取 AgentBus payload `received_at`;缺失时使用本项目接收时间 |
| `sourceSentAt` | 邮件来源发送时间,如果 payload 提供则保存 | | `sourceSentAt` | 邮件来源发送时间,如果 payload 提供则保存 |
| `senderSummary` | 发送人安全摘要,列表可展示,不保存或不返回完整敏感信息 | | `senderSummary` | 发件人展示值,业务人员需要识别真实发件人,当前只清理首尾空白不打码 |
| `subject` | 邮件主题 | | `subject` | 邮件主题 |
| `safeSnippet` | 邮件安全摘要不能包含完整正文、HTML、附件 URL 或敏感凭证 | | `safeSnippet` | 邮件安全摘要不能包含完整正文、HTML、附件 URL 或敏感凭证 |

View File

@@ -45,6 +45,7 @@ public class AgentBusSourceMessageAdapter {
text(source, "external_conversation_id"), text(source, "external_conversation_id"),
text(frame, "id"), text(frame, "id"),
text(frame, "session_id"), text(frame, "session_id"),
firstInstant(source, "received_at", payload, "received_at"),
parseInstant(text(source, "sent_at")), parseInstant(text(source, "sent_at")),
text(source, "sender"), text(source, "sender"),
text(source, "subject"), text(source, "subject"),
@@ -110,6 +111,17 @@ public class AgentBusSourceMessageAdapter {
} }
} }
/**
* 从两个候选字段中读取第一个可解析的时间,格式非法时继续尝试后续字段。
*/
private Instant firstInstant(JsonNode firstNode, String firstField, JsonNode secondNode, String secondField) {
Instant firstValue = parseInstant(text(firstNode, firstField));
if (firstValue != null) {
return firstValue;
}
return parseInstant(text(secondNode, secondField));
}
/** /**
* 将渠道等稳定代码转为大写,缺失时使用服务端默认值。 * 将渠道等稳定代码转为大写,缺失时使用服务端默认值。
*/ */

View File

@@ -20,6 +20,7 @@ public record SourceMessageInboxDraft(
boolean duplicatePayloadChanged, boolean duplicatePayloadChanged,
String safeErrorSummary, String safeErrorSummary,
LocalDateTime receivedAt, LocalDateTime receivedAt,
LocalDateTime capturedAt,
LocalDateTime sourceSentAt, LocalDateTime sourceSentAt,
String senderSummary, String senderSummary,
String subject, String subject,

View File

@@ -14,6 +14,7 @@ public record CaptureSourceMessageCommand(
String externalConversationId, String externalConversationId,
String providerFrameId, String providerFrameId,
String providerSessionId, String providerSessionId,
Instant sourceReceivedAt,
Instant sourceSentAt, Instant sourceSentAt,
String senderIdentifier, String senderIdentifier,
String subject, String subject,
@@ -23,4 +24,38 @@ public record CaptureSourceMessageCommand(
String schemaVersion, String schemaVersion,
List<CaptureSourceMessageMedia> mediaItems List<CaptureSourceMessageMedia> mediaItems
) { ) {
public CaptureSourceMessageCommand(
String hotelId,
String provider,
String channel,
String externalMessageId,
String externalConversationId,
String providerFrameId,
String providerSessionId,
Instant sourceSentAt,
String senderIdentifier,
String subject,
String textBody,
String htmlBody,
String payloadJson,
String schemaVersion,
List<CaptureSourceMessageMedia> mediaItems) {
this(
hotelId,
provider,
channel,
externalMessageId,
externalConversationId,
providerFrameId,
providerSessionId,
null,
sourceSentAt,
senderIdentifier,
subject,
textBody,
htmlBody,
payloadJson,
schemaVersion,
mediaItems);
}
} }

View File

@@ -10,9 +10,9 @@ import java.util.List;
* @param id 内部 SourceMessage Inbox ID * @param id 内部 SourceMessage Inbox ID
* @param externalMessageId 外部单封邮件 ID * @param externalMessageId 外部单封邮件 ID
* @param externalConversationId 外部邮件会话 ID * @param externalConversationId 外部邮件会话 ID
* @param senderSummary 发送人安全摘要 * @param senderSummary 发件人展示值,当前不打码
* @param subject 邮件主题摘要 * @param subject 邮件主题摘要
* @param receivedAt 本系统接收时间UTC * @param receivedAt 邮件来源接收时间UTC;缺失时回退本系统接收时间
* @param sourceSentAt 邮件来源发送时间UTC * @param sourceSentAt 邮件来源发送时间UTC
* @param textBody 完整纯文本正文 * @param textBody 完整纯文本正文
* @param htmlBody 完整 HTML 正文,前端展示前必须 sanitize * @param htmlBody 完整 HTML 正文,前端展示前必须 sanitize

View File

@@ -36,11 +36,11 @@ public class SourceMessageInboxEntity {
private Boolean duplicatePayloadChanged; private Boolean duplicatePayloadChanged;
/** 安全错误摘要,不包含正文、附件 URL、Token 或个人敏感信息。 */ /** 安全错误摘要,不包含正文、附件 URL、Token 或个人敏感信息。 */
private String safeErrorSummary; private String safeErrorSummary;
/** 本项目接收消息的 UTC 时间。 */ /** 邮件来源接收 UTC 时间,缺失时使用本项目接收消息的 UTC 时间。 */
private LocalDateTime receivedAt; private LocalDateTime receivedAt;
/** 来源系统提供的邮件发送 UTC 时间,缺失时为空。 */ /** 来源系统提供的邮件发送 UTC 时间,缺失时为空。 */
private LocalDateTime sourceSentAt; private LocalDateTime sourceSentAt;
/** 发送人安全摘要,普通查询可展示。 */ /** 发件人展示值,业务人员需要识别真实发件人,当前只清理首尾空白不打码。 */
private String senderSummary; private String senderSummary;
/** 脱敏后的邮件主题,普通查询可展示。 */ /** 脱敏后的邮件主题,普通查询可展示。 */
private String subject; private String subject;

View File

@@ -244,7 +244,7 @@ public class MybatisSourceMessageInboxRepository implements SourceMessageInboxRe
insertPayload(draft, inbox.getId()); insertPayload(draft, inbox.getId());
if (SourceMessageCaptureStatus.RECEIVED.code().equals(draft.captureStatus())) { if (SourceMessageCaptureStatus.RECEIVED.code().equals(draft.captureStatus())) {
insertBody(draft, inbox.getId()); insertBody(draft, inbox.getId());
insertMedia(draft.mediaItems(), inbox.getId(), draft.receivedAt()); insertMedia(draft.mediaItems(), inbox.getId(), draft.capturedAt());
} }
return inbox.getId(); return inbox.getId();
} }
@@ -340,8 +340,8 @@ public class MybatisSourceMessageInboxRepository implements SourceMessageInboxRe
inbox.setSenderSummary(draft.senderSummary()); inbox.setSenderSummary(draft.senderSummary());
inbox.setSubject(draft.subject()); inbox.setSubject(draft.subject());
inbox.setSafeSnippet(draft.safeSnippet()); inbox.setSafeSnippet(draft.safeSnippet());
inbox.setCreatedAt(draft.receivedAt()); inbox.setCreatedAt(draft.capturedAt());
inbox.setUpdatedAt(draft.receivedAt()); inbox.setUpdatedAt(draft.capturedAt());
return inbox; return inbox;
} }
@@ -354,7 +354,7 @@ public class MybatisSourceMessageInboxRepository implements SourceMessageInboxRe
payload.setPayloadJson(draft.payloadJson()); payload.setPayloadJson(draft.payloadJson());
payload.setPayloadSha256(draft.payloadSha256()); payload.setPayloadSha256(draft.payloadSha256());
payload.setSchemaVersion(draft.schemaVersion()); payload.setSchemaVersion(draft.schemaVersion());
payload.setCreatedAt(draft.receivedAt()); payload.setCreatedAt(draft.capturedAt());
payloadMapper.insert(payload); payloadMapper.insert(payload);
} }
@@ -368,7 +368,7 @@ public class MybatisSourceMessageInboxRepository implements SourceMessageInboxRe
body.setTextBody(draft.textBody()); body.setTextBody(draft.textBody());
body.setHtmlBody(draft.htmlBody()); body.setHtmlBody(draft.htmlBody());
body.setBodySha256(sha256(draft.textBody() + "\n" + draft.htmlBody())); body.setBodySha256(sha256(draft.textBody() + "\n" + draft.htmlBody()));
body.setCreatedAt(draft.receivedAt()); body.setCreatedAt(draft.capturedAt());
bodyMapper.insert(body); bodyMapper.insert(body);
} }

View File

@@ -213,7 +213,7 @@ public class SourceMessageCaptureServiceImpl implements SourceMessageCaptureServ
} }
/** /**
* 构建入库草稿,统一生成安全主题、发送人摘要和正文摘要。 * 构建入库草稿,统一生成安全主题、发件人展示值和正文摘要。
*/ */
private SourceMessageInboxDraft buildDraft( private SourceMessageInboxDraft buildDraft(
CaptureSourceMessageCommand command, CaptureSourceMessageCommand command,
@@ -224,7 +224,9 @@ public class SourceMessageCaptureServiceImpl implements SourceMessageCaptureServ
String payloadSha256, String payloadSha256,
SourceMessageCaptureStatus captureStatus, SourceMessageCaptureStatus captureStatus,
String safeErrorSummary) { String safeErrorSummary) {
LocalDateTime now = nowUtc(); LocalDateTime capturedAt = nowUtc();
LocalDateTime sourceReceivedAt = toLocalDateTime(command.sourceReceivedAt());
LocalDateTime receivedAt = sourceReceivedAt == null ? capturedAt : sourceReceivedAt;
String textBody = command.textBody() == null ? "" : command.textBody(); String textBody = command.textBody() == null ? "" : command.textBody();
String htmlBody = command.htmlBody() == null ? "" : command.htmlBody(); String htmlBody = command.htmlBody() == null ? "" : command.htmlBody();
// 邮件主题也可能携带联系方式或带 token 的 URL普通查询只保存可展示摘要。 // 邮件主题也可能携带联系方式或带 token 的 URL普通查询只保存可展示摘要。
@@ -244,7 +246,8 @@ public class SourceMessageCaptureServiceImpl implements SourceMessageCaptureServ
captureStatus.code(), captureStatus.code(),
false, false,
safeErrorSummary, safeErrorSummary,
now, receivedAt,
capturedAt,
toLocalDateTime(command.sourceSentAt()), toLocalDateTime(command.sourceSentAt()),
safetySanitizer.senderSummary(command.senderIdentifier()), safetySanitizer.senderSummary(command.senderIdentifier()),
safeSubject, safeSubject,

View File

@@ -1,11 +1,10 @@
package cn.nianxx.thhotel.platform.message.service.impl; package cn.nianxx.thhotel.platform.message.service.impl;
import java.util.Locale;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
* 来源消息安全摘要工具。列表和普通详情只能使用脱敏后的摘要,不能透出原文或媒体 URL * 来源消息安全摘要工具。正文和主题仍做安全摘要;发件人展示值只清理首尾空白
*/ */
@Component @Component
public class SourceMessageSafetySanitizer { public class SourceMessageSafetySanitizer {
@@ -18,21 +17,10 @@ public class SourceMessageSafetySanitizer {
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
/** /**
* 生成发送人安全摘要,避免普通列表直接暴露完整邮箱或客户标识 * 生成发送人展示值。业务人员需要识别邮件真实发件人,因此这里只清理首尾空白,不再打码
*/ */
String senderSummary(String senderIdentifier) { String senderSummary(String senderIdentifier) {
String value = trimToEmpty(senderIdentifier); return trimToEmpty(senderIdentifier);
if (value.isEmpty()) {
return "";
}
int atIndex = value.indexOf('@');
if (atIndex > 0 && atIndex < value.length() - 1) {
return value.charAt(0) + "***" + value.substring(atIndex).toLowerCase(Locale.ROOT);
}
if (value.length() <= 2) {
return "*";
}
return value.charAt(0) + "***" + value.charAt(value.length() - 1);
} }
/** /**

View File

@@ -27,9 +27,9 @@ public record ReservationMessageConversationMessagesResult(
* *
* @param externalSourceMessageId 外部来源消息 ID对应 AgentBus source.external_message_id * @param externalSourceMessageId 外部来源消息 ID对应 AgentBus source.external_message_id
* @param externalConversationId 外部邮件会话 ID * @param externalConversationId 外部邮件会话 ID
* @param senderSummary 发送人安全摘要 * @param senderSummary 发件人展示值,当前不打码
* @param subject 邮件主题安全摘要 * @param subject 邮件主题安全摘要
* @param receivedAt 本系统接收该邮件的 UTC 时间 * @param receivedAt 邮件来源接收时间UTC缺失时回退本系统接收时间
* @param sourceSentAt 来源系统发送时间 * @param sourceSentAt 来源系统发送时间
* @param textBody 纯文本正文 * @param textBody 纯文本正文
* @param htmlBodySanitized 后端清洗后的 HTML 正文 * @param htmlBodySanitized 后端清洗后的 HTML 正文

View File

@@ -29,7 +29,7 @@ public record ReservationMessageConversationTasksResult(
* @param orderId 任务当前挂靠订单 ID * @param orderId 任务当前挂靠订单 ID
* @param externalSourceMessageId 外部来源消息 ID对应 AgentBus source.external_message_id * @param externalSourceMessageId 外部来源消息 ID对应 AgentBus source.external_message_id
* @param externalConversationId 外部邮件会话 ID * @param externalConversationId 外部邮件会话 ID
* @param sourceReceivedAt 本系统接收该邮件的 UTC 时间 * @param sourceReceivedAt 邮件来源接收时间UTC缺失时回退本系统接收时间
* @param sourceEventIndex AI transition 来源事件序号 * @param sourceEventIndex AI transition 来源事件序号
* @param catalogCode Skill 目录代码 * @param catalogCode Skill 目录代码
* @param skillId Skill 标识 * @param skillId Skill 标识

View File

@@ -17,8 +17,8 @@ import java.time.OffsetDateTime;
* @param readonlyReasonCode 只读原因代码 * @param readonlyReasonCode 只读原因代码
* @param sourceMessageId 来源 SourceMessage Inbox ID * @param sourceMessageId 来源 SourceMessage Inbox ID
* @param sourceSubject 来源消息主题摘要 * @param sourceSubject 来源消息主题摘要
* @param sourceSenderSummary 来源发送人安全摘要 * @param sourceSenderSummary 来源发件人展示值,当前不打码
* @param sourceReceivedAt 本系统接收来源消息时间UTC * @param sourceReceivedAt 邮件来源接收时间UTC缺失时回退本系统接收时间
* @param externalConversationId 外部邮件会话 ID * @param externalConversationId 外部邮件会话 ID
* @param conversationMessageCount 同一外部邮件会话下的消息数量 * @param conversationMessageCount 同一外部邮件会话下的消息数量
* @param createdAt 任务创建时间 * @param createdAt 任务创建时间

View File

@@ -11,8 +11,8 @@ import java.util.List;
* @param orderId 订单 ID * @param orderId 订单 ID
* @param sourceMessageId 来源 SourceMessage Inbox ID * @param sourceMessageId 来源 SourceMessage Inbox ID
* @param sourceSubject 来源消息主题摘要 * @param sourceSubject 来源消息主题摘要
* @param sourceSenderSummary 来源发送人安全摘要 * @param sourceSenderSummary 来源发件人展示值,当前不打码
* @param sourceReceivedAt 本系统接收来源消息时间UTC * @param sourceReceivedAt 邮件来源接收时间UTC缺失时回退本系统接收时间
* @param externalConversationId 外部邮件会话 ID * @param externalConversationId 外部邮件会话 ID
* @param conversationMessageCount 同一外部邮件会话下的消息数量 * @param conversationMessageCount 同一外部邮件会话下的消息数量
* @param systemTaskType 系统主任务类型 * @param systemTaskType 系统主任务类型

View File

@@ -21,8 +21,8 @@ import java.time.OffsetDateTime;
* @param readonlyReasonCode 只读原因代码 * @param readonlyReasonCode 只读原因代码
* @param sourceMessageId 来源 SourceMessage Inbox ID * @param sourceMessageId 来源 SourceMessage Inbox ID
* @param sourceSubject 来源消息主题摘要 * @param sourceSubject 来源消息主题摘要
* @param sourceSenderSummary 来源发送人安全摘要 * @param sourceSenderSummary 来源发件人展示值,当前不打码
* @param sourceReceivedAt 本系统接收来源消息时间UTC * @param sourceReceivedAt 邮件来源接收时间UTC缺失时回退本系统接收时间
* @param externalConversationId 外部邮件会话 ID * @param externalConversationId 外部邮件会话 ID
* @param conversationMessageCount 同一外部邮件会话下的消息数量 * @param conversationMessageCount 同一外部邮件会话下的消息数量
* @param createdAt 任务创建时间 * @param createdAt 任务创建时间

View File

@@ -0,0 +1,46 @@
-- M001 SourceMessage 字段语义修正received_at 表示邮件实际接收时间sender_summary 不再打码。
-- 说明MySQL 8.0+ 执行以下注释指令H2 MySQL Mode 会跳过,避免测试库不支持完整 MySQL COMMENT 语法。
/*!80000 ALTER TABLE platform_source_message_inbox
MODIFY COLUMN received_at DATETIME(6) NOT NULL COMMENT '邮件来源接收时间,优先取 AgentBus payload received_at缺失时使用本项目接收 UTC 时间' */;
/*!80000 ALTER TABLE platform_source_message_inbox
MODIFY COLUMN sender_summary VARCHAR(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT '发件人展示值,业务人员需要识别真实发件人,当前只清理首尾空白不打码' */;
-- 历史数据修正:已保存原始 payload 的旧记录,尽量回填邮件来源接收时间和完整发件人展示值。
-- 说明:只处理合法 JSON 且字段存在的记录;无法解析的旧记录保留原值,避免迁移阻断上线。
/*!80000 UPDATE platform_source_message_inbox inbox
JOIN (
SELECT
payload.inbox_id,
NULLIF(TRIM(
CASE
WHEN JSON_VALID(payload.payload_json) THEN JSON_UNQUOTE(COALESCE(
JSON_EXTRACT(payload.payload_json, '$.source.sender'),
JSON_EXTRACT(payload.payload_json, '$.sender')))
ELSE NULL
END
), '') AS source_sender,
NULLIF(TRIM(
CASE
WHEN JSON_VALID(payload.payload_json) THEN JSON_UNQUOTE(COALESCE(
JSON_EXTRACT(payload.payload_json, '$.source.received_at'),
JSON_EXTRACT(payload.payload_json, '$.received_at')))
ELSE NULL
END
), '') AS source_received_at
FROM platform_source_message_payload payload
) payload_source ON payload_source.inbox_id = inbox.id
SET
inbox.sender_summary = COALESCE(payload_source.source_sender, inbox.sender_summary),
inbox.received_at = CASE
WHEN payload_source.source_received_at REGEXP '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,6})?Z$'
THEN CASE
WHEN payload_source.source_received_at REGEXP '\\.[0-9]{1,6}Z$'
THEN STR_TO_DATE(REPLACE(REPLACE(payload_source.source_received_at, 'T', ' '), 'Z', ''), '%Y-%m-%d %H:%i:%s.%f')
ELSE STR_TO_DATE(REPLACE(REPLACE(payload_source.source_received_at, 'T', ' '), 'Z', ''), '%Y-%m-%d %H:%i:%s')
END
ELSE inbox.received_at
END
WHERE payload_source.source_sender IS NOT NULL
OR payload_source.source_received_at REGEXP '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,6})?Z$' */;

View File

@@ -44,6 +44,7 @@ class AgentBusSourceMessageAdapterTest {
"external_url": "https://media.example.test/booking.pdf" "external_url": "https://media.example.test/booking.pdf"
} }
], ],
"received_at": "2026-07-09T05:56:56Z",
"source": { "source": {
"channel": "email", "channel": "email",
"external_message_id": "outlook-message-001", "external_message_id": "outlook-message-001",
@@ -65,6 +66,7 @@ class AgentBusSourceMessageAdapterTest {
assertThat(command.externalConversationId()).isEqualTo("outlook-conversation-001"); assertThat(command.externalConversationId()).isEqualTo("outlook-conversation-001");
assertThat(command.providerFrameId()).isEqualTo("frame-agentbus-001"); assertThat(command.providerFrameId()).isEqualTo("frame-agentbus-001");
assertThat(command.providerSessionId()).isEqualTo("session-agentbus-001"); assertThat(command.providerSessionId()).isEqualTo("session-agentbus-001");
assertThat(command.sourceReceivedAt()).isEqualTo(Instant.parse("2026-07-09T05:56:56Z"));
assertThat(command.sourceSentAt()).isEqualTo(Instant.parse("2026-07-06T08:00:00Z")); assertThat(command.sourceSentAt()).isEqualTo(Instant.parse("2026-07-06T08:00:00Z"));
assertThat(command.senderIdentifier()).isEqualTo("guest@example.test"); assertThat(command.senderIdentifier()).isEqualTo("guest@example.test");
assertThat(command.subject()).isEqualTo("Booking change"); assertThat(command.subject()).isEqualTo("Booking change");
@@ -78,4 +80,40 @@ class AgentBusSourceMessageAdapterTest {
assertThat(command.mediaItems().get(1).mediaType()).isEqualTo("ATTACHMENT"); assertThat(command.mediaItems().get(1).mediaType()).isEqualTo("ATTACHMENT");
assertThat(command.mediaItems().get(1).fileName()).isEqualTo("booking.pdf"); assertThat(command.mediaItems().get(1).fileName()).isEqualTo("booking.pdf");
} }
@Test
void shouldPreferValidSourceReceivedAtAndFallbackToPayloadReceivedAt() throws Exception {
JsonNode sourceReceivedFrame = objectMapper.readTree("""
{
"id": "frame-agentbus-002",
"payload": {
"received_at": "2026-07-09T05:56:56Z",
"source": {
"channel": "email",
"external_message_id": "outlook-message-002",
"received_at": "2026-07-09T05:50:00Z"
}
}
}
""");
JsonNode invalidSourceReceivedFrame = objectMapper.readTree("""
{
"id": "frame-agentbus-003",
"payload": {
"received_at": "2026-07-09T05:56:56Z",
"source": {
"channel": "email",
"external_message_id": "outlook-message-003",
"received_at": "bad-time"
}
}
}
""");
CaptureSourceMessageCommand sourceCommand = adapter.toCaptureCommand("HOTEL-TEST", sourceReceivedFrame);
CaptureSourceMessageCommand fallbackCommand = adapter.toCaptureCommand("HOTEL-TEST", invalidSourceReceivedFrame);
assertThat(sourceCommand.sourceReceivedAt()).isEqualTo(Instant.parse("2026-07-09T05:50:00Z"));
assertThat(fallbackCommand.sourceReceivedAt()).isEqualTo(Instant.parse("2026-07-09T05:56:56Z"));
}
} }

View File

@@ -106,7 +106,7 @@ class SourceMessageCaptureServiceImplTest {
assertThat(summary.externalMessageId()).isEqualTo("mail-m001-001"); assertThat(summary.externalMessageId()).isEqualTo("mail-m001-001");
assertThat(summary.externalConversationId()).isEqualTo("conversation-m001"); assertThat(summary.externalConversationId()).isEqualTo("conversation-m001");
assertThat(summary.captureStatus()).isEqualTo("RECEIVED"); assertThat(summary.captureStatus()).isEqualTo("RECEIVED");
assertThat(summary.senderSummary()).isEqualTo("g***@example.test"); assertThat(summary.senderSummary()).isEqualTo("guest@example.test");
assertThat(summary.subject()).isEqualTo("Booking change request"); assertThat(summary.subject()).isEqualTo("Booking change request");
assertThat(summary.safeSnippet()).contains("Please change the arrival date"); assertThat(summary.safeSnippet()).contains("Please change the arrival date");
assertThat(summary.safeSnippet()).doesNotContain("13800138000"); assertThat(summary.safeSnippet()).doesNotContain("13800138000");
@@ -187,6 +187,40 @@ class SourceMessageCaptureServiceImplTest {
assertThat(inbox.getSafeErrorSummary()).contains("重复投递 payload"); assertThat(inbox.getSafeErrorSummary()).contains("重复投递 payload");
} }
@Test
void shouldPersistSourceReceivedAtAndExposeFullSenderIdentifier() {
CaptureSourceMessageCommand command = new CaptureSourceMessageCommand(
"HOTEL-TEST",
"AGENTBUS",
"EMAIL",
"mail-source-received-time-001",
"conversation-source-received-time",
"frame-source-received-time-001",
"session-source-received-time",
Instant.parse("2026-07-09T05:56:56Z"),
Instant.parse("2026-07-09T05:55:00Z"),
"guest@example.test",
"Source received time",
"Please check received time.",
"<html>Please check received time.</html>",
"{\"received_at\":\"2026-07-09T05:56:56Z\",\"source\":{\"external_message_id\":\"mail-source-received-time-001\"}}",
"agentbus-outlook-v1",
List.of()
);
SourceMessageCaptureResult result = captureService.capture(command);
SourceMessageSummaryResponse summary = queryService.getSummary(result.inboxId()).orElseThrow();
assertThat(summary.receivedAt().toInstant()).isEqualTo(Instant.parse("2026-07-09T05:56:56Z"));
assertThat(summary.sourceSentAt().toInstant()).isEqualTo(Instant.parse("2026-07-09T05:55:00Z"));
assertThat(summary.senderSummary()).isEqualTo("guest@example.test");
SourceMessageInboxEntity inbox = inboxMapper.selectById(result.inboxId());
assertThat(inbox.getReceivedAt()).isEqualTo(LocalDateTime.parse("2026-07-09T05:56:56"));
assertThat(inbox.getCreatedAt()).isNotEqualTo(inbox.getReceivedAt());
assertThat(inbox.getUpdatedAt()).isEqualTo(inbox.getCreatedAt());
}
@Test @Test
void shouldPersistFailedInboxWhenExternalMessageIdIsMissing() { void shouldPersistFailedInboxWhenExternalMessageIdIsMissing() {
CaptureSourceMessageCommand command = new CaptureSourceMessageCommand( CaptureSourceMessageCommand command = new CaptureSourceMessageCommand(
@@ -319,7 +353,7 @@ class SourceMessageCaptureServiceImplTest {
false, false,
LocalDateTime.parse("2026-07-06T09:50:00"), LocalDateTime.parse("2026-07-06T09:50:00"),
LocalDateTime.parse("2026-07-06T09:50:00"), LocalDateTime.parse("2026-07-06T09:50:00"),
"g***@example.test", "guest@example.test",
"Race delivery", "Race delivery",
"Race body" "Race body"
); );
@@ -373,7 +407,7 @@ class SourceMessageCaptureServiceImplTest {
true, true,
LocalDateTime.parse("2026-07-06T10:00:00"), LocalDateTime.parse("2026-07-06T10:00:00"),
LocalDateTime.parse("2026-07-06T10:00:00"), LocalDateTime.parse("2026-07-06T10:00:00"),
"g***@example.test", "guest@example.test",
"Duplicate diagnostic fail", "Duplicate diagnostic fail",
"Original body" "Original body"
); );

View File

@@ -91,7 +91,7 @@ class ReservationFrontendQueryControllerTest {
.andExpect(jsonPath("$.items[0].readonly_reason_code").value("PROCESSABLE")) .andExpect(jsonPath("$.items[0].readonly_reason_code").value("PROCESSABLE"))
.andExpect(jsonPath("$.items[0].source_message_id").value(source.inboxId().toString())) .andExpect(jsonPath("$.items[0].source_message_id").value(source.inboxId().toString()))
.andExpect(jsonPath("$.items[0].source_subject").value("Frontend Query List Smoke")) .andExpect(jsonPath("$.items[0].source_subject").value("Frontend Query List Smoke"))
.andExpect(jsonPath("$.items[0].source_sender_summary").value("g***@example.test")) .andExpect(jsonPath("$.items[0].source_sender_summary").value("guest@example.test"))
.andExpect(jsonPath("$.items[0].source_received_at").value(matchesPattern(UTC_INSTANT_PATTERN))) .andExpect(jsonPath("$.items[0].source_received_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
.andExpect(jsonPath("$.items[0].external_conversation_id") .andExpect(jsonPath("$.items[0].external_conversation_id")
.value("thread-mail-frontend-task-list-001")) .value("thread-mail-frontend-task-list-001"))
@@ -193,7 +193,7 @@ class ReservationFrontendQueryControllerTest {
.andExpect(jsonPath("$.tasks[0].task_status").value("COMPLETED")) .andExpect(jsonPath("$.tasks[0].task_status").value("COMPLETED"))
.andExpect(jsonPath("$.tasks[0].source_message_id").value(source.inboxId().toString())) .andExpect(jsonPath("$.tasks[0].source_message_id").value(source.inboxId().toString()))
.andExpect(jsonPath("$.tasks[0].source_subject").value("Frontend Query Detail Smoke")) .andExpect(jsonPath("$.tasks[0].source_subject").value("Frontend Query Detail Smoke"))
.andExpect(jsonPath("$.tasks[0].source_sender_summary").value("g***@example.test")) .andExpect(jsonPath("$.tasks[0].source_sender_summary").value("guest@example.test"))
.andExpect(jsonPath("$.tasks[0].source_received_at").value(matchesPattern(UTC_INSTANT_PATTERN))) .andExpect(jsonPath("$.tasks[0].source_received_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
.andExpect(jsonPath("$.tasks[0].external_conversation_id") .andExpect(jsonPath("$.tasks[0].external_conversation_id")
.value("thread-mail-frontend-order-detail-001")) .value("thread-mail-frontend-order-detail-001"))

View File

@@ -702,7 +702,7 @@ class SuperAgentTaskResultControllerTest {
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.task_card_type").value("NEW_BOOKING")) .andExpect(jsonPath("$.task_card_type").value("NEW_BOOKING"))
.andExpect(jsonPath("$.source_subject").value("M002 SuperAgent intake")) .andExpect(jsonPath("$.source_subject").value("M002 SuperAgent intake"))
.andExpect(jsonPath("$.source_sender_summary").value("g***@example.test")) .andExpect(jsonPath("$.source_sender_summary").value("guest@example.test"))
.andExpect(jsonPath("$.source_received_at").value(matchesPattern(UTC_INSTANT_PATTERN))) .andExpect(jsonPath("$.source_received_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
.andExpect(jsonPath("$.external_conversation_id").value("thread-mail-detail-matrix-001")) .andExpect(jsonPath("$.external_conversation_id").value("thread-mail-detail-matrix-001"))
.andExpect(jsonPath("$.conversation_message_count").value(1)) .andExpect(jsonPath("$.conversation_message_count").value(1))