补齐前端P0查询接口
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package cn.nianxx.thhotel.platform.message.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 邮件会话中的单封邮件详情。该结果会返回完整 text/html 和媒体外链,只能用于受控会话详情场景。
|
||||
*
|
||||
* @param id 内部 SourceMessage Inbox ID
|
||||
* @param externalMessageId 外部单封邮件 ID
|
||||
* @param externalConversationId 外部邮件会话 ID
|
||||
* @param senderSummary 发送人安全摘要
|
||||
* @param subject 邮件主题摘要
|
||||
* @param receivedAt 本系统接收时间,UTC
|
||||
* @param sourceSentAt 邮件来源发送时间,UTC
|
||||
* @param textBody 完整纯文本正文
|
||||
* @param htmlBody 完整 HTML 正文,前端展示前必须 sanitize
|
||||
* @param htmlSanitizeRequired 是否要求前端 sanitize HTML
|
||||
* @param inlineImages 内联图片外链
|
||||
* @param attachments 附件外链
|
||||
* @param relatedOrders 关联订单摘要
|
||||
* @param relatedTasks 关联任务摘要
|
||||
*/
|
||||
public record SourceMessageConversationMessageResult(
|
||||
String id,
|
||||
@JsonProperty("external_message_id")
|
||||
String externalMessageId,
|
||||
@JsonProperty("external_conversation_id")
|
||||
String externalConversationId,
|
||||
@JsonProperty("sender_summary")
|
||||
String senderSummary,
|
||||
String subject,
|
||||
@JsonProperty("received_at")
|
||||
OffsetDateTime receivedAt,
|
||||
@JsonProperty("source_sent_at")
|
||||
OffsetDateTime sourceSentAt,
|
||||
@JsonProperty("text_body")
|
||||
String textBody,
|
||||
@JsonProperty("html_body")
|
||||
String htmlBody,
|
||||
@JsonProperty("html_sanitize_required")
|
||||
Boolean htmlSanitizeRequired,
|
||||
@JsonProperty("inline_images")
|
||||
List<SourceMessageOriginalMediaResponse> inlineImages,
|
||||
List<SourceMessageOriginalMediaResponse> attachments,
|
||||
@JsonProperty("related_orders")
|
||||
List<SourceMessageRelatedOrderResult> relatedOrders,
|
||||
@JsonProperty("related_tasks")
|
||||
List<SourceMessageRelatedTaskResult> relatedTasks
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.nianxx.thhotel.platform.message.common.result;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* SourceMessage 邮件会话详情结果。包含会话摘要和同会话所有邮件原文。
|
||||
*
|
||||
* @param conversation 会话摘要
|
||||
* @param messages 同一会话内全部邮件,按接收时间正序返回
|
||||
*/
|
||||
public record SourceMessageConversationResult(
|
||||
SourceMessageConversationSummaryResult conversation,
|
||||
List<SourceMessageConversationMessageResult> messages
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cn.nianxx.thhotel.platform.message.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/**
|
||||
* 邮件会话摘要。用于前端在会话详情顶部展示来源链路信息。
|
||||
*
|
||||
* @param hotelId 酒店上下文 ID
|
||||
* @param channel 来源渠道
|
||||
* @param externalConversationId 外部邮件会话 ID
|
||||
* @param subject 当前定位消息的主题摘要
|
||||
* @param messageCount 会话内邮件数量
|
||||
* @param firstReceivedAt 会话第一封入库时间,UTC
|
||||
* @param lastReceivedAt 会话最后一封入库时间,UTC
|
||||
*/
|
||||
public record SourceMessageConversationSummaryResult(
|
||||
@JsonProperty("hotel_id")
|
||||
String hotelId,
|
||||
String channel,
|
||||
@JsonProperty("external_conversation_id")
|
||||
String externalConversationId,
|
||||
String subject,
|
||||
@JsonProperty("message_count")
|
||||
Long messageCount,
|
||||
@JsonProperty("first_received_at")
|
||||
OffsetDateTime firstReceivedAt,
|
||||
@JsonProperty("last_received_at")
|
||||
OffsetDateTime lastReceivedAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.nianxx.thhotel.platform.message.common.result;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* SourceMessage 关联业务上下文。由业务模块实现提供,平台会话服务只做聚合展示。
|
||||
*
|
||||
* @param relatedOrders 关联订单摘要列表
|
||||
* @param relatedTasks 关联任务摘要列表
|
||||
*/
|
||||
public record SourceMessageRelatedContextResult(
|
||||
List<SourceMessageRelatedOrderResult> relatedOrders,
|
||||
List<SourceMessageRelatedTaskResult> relatedTasks
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cn.nianxx.thhotel.platform.message.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* SourceMessage 会话中关联订单摘要。平台层只定义通用展示字段,具体来源由业务模块提供。
|
||||
*
|
||||
* @param orderId 订单 ID
|
||||
* @param displayOrderKey 前端展示用订单号或临时订单号
|
||||
* @param orderStatus 订单状态
|
||||
*/
|
||||
public record SourceMessageRelatedOrderResult(
|
||||
@JsonProperty("order_id")
|
||||
String orderId,
|
||||
@JsonProperty("display_order_key")
|
||||
String displayOrderKey,
|
||||
@JsonProperty("order_status")
|
||||
String orderStatus
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.nianxx.thhotel.platform.message.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* SourceMessage 会话中关联任务摘要。用于前端从邮件原文跳转到任务详情。
|
||||
*
|
||||
* @param taskId 任务 ID
|
||||
* @param orderId 任务当前挂靠订单 ID
|
||||
* @param taskType 系统主任务类型
|
||||
* @param taskSubtype 任务 subtype
|
||||
* @param taskStatus 任务状态
|
||||
* @param cardName 任务卡名称
|
||||
*/
|
||||
public record SourceMessageRelatedTaskResult(
|
||||
@JsonProperty("task_id")
|
||||
String taskId,
|
||||
@JsonProperty("order_id")
|
||||
String orderId,
|
||||
@JsonProperty("task_type")
|
||||
String taskType,
|
||||
@JsonProperty("task_subtype")
|
||||
String taskSubtype,
|
||||
@JsonProperty("task_status")
|
||||
String taskStatus,
|
||||
@JsonProperty("card_name")
|
||||
String cardName
|
||||
) {
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package cn.nianxx.thhotel.platform.message.control;
|
||||
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessagePageResult;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageConversationResult;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageOriginalResponse;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.SourceMessageOriginalAccessRequest;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.SourceMessageQueryRequest;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageConversationService;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageOriginalService;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageQueryService;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageSummaryResponse;
|
||||
@@ -25,15 +27,18 @@ public class SourceMessageController {
|
||||
|
||||
private final SourceMessageQueryService queryService;
|
||||
private final SourceMessageOriginalService originalService;
|
||||
private final SourceMessageConversationService conversationService;
|
||||
|
||||
/**
|
||||
* 注入 SourceMessage 查询与原文读取服务,Controller 不直接访问 Mapper 或 Repository。
|
||||
*/
|
||||
public SourceMessageController(
|
||||
SourceMessageQueryService queryService,
|
||||
SourceMessageOriginalService originalService) {
|
||||
SourceMessageOriginalService originalService,
|
||||
SourceMessageConversationService conversationService) {
|
||||
this.queryService = queryService;
|
||||
this.originalService = originalService;
|
||||
this.conversationService = conversationService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,6 +90,15 @@ public class SourceMessageController {
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "source message not found"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取指定 SourceMessage 所在邮件会话完整详情。该接口由后端内部记录原文读取审计,前端不传原文 key。
|
||||
*/
|
||||
@GetMapping("/{id}/conversation")
|
||||
public SourceMessageConversationResult conversation(@PathVariable Long id) {
|
||||
return conversationService.getConversation(id)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "source message not found"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验请求头文本是否有效,避免空白调用方或场景进入审计记录。
|
||||
*/
|
||||
|
||||
@@ -27,7 +27,9 @@ import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Repository;
|
||||
@@ -90,6 +92,24 @@ public class MybatisSourceMessageInboxRepository implements SourceMessageInboxRe
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询同一外部会话下的全部 SourceMessage 安全快照,按接收时间正序返回。
|
||||
*/
|
||||
@Override
|
||||
public List<SourceMessageInboxSnapshot> findByExternalConversationId(String hotelId, String externalConversationId) {
|
||||
if (!hasText(hotelId) || !hasText(externalConversationId)) {
|
||||
return List.of();
|
||||
}
|
||||
return inboxMapper.selectList(Wrappers.<SourceMessageInboxEntity>lambdaQuery()
|
||||
.eq(SourceMessageInboxEntity::getHotelId, trim(hotelId))
|
||||
.eq(SourceMessageInboxEntity::getExternalConversationId, trim(externalConversationId))
|
||||
.orderByAsc(SourceMessageInboxEntity::getReceivedAt)
|
||||
.orderByAsc(SourceMessageInboxEntity::getId))
|
||||
.stream()
|
||||
.map(this::toSnapshot)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 SourceMessage 幂等键读取已有记录,用于重复投递判断。
|
||||
*/
|
||||
@@ -160,6 +180,30 @@ public class MybatisSourceMessageInboxRepository implements SourceMessageInboxRe
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量统计会话消息数。该方法只访问 Inbox 索引表,不读取正文或媒体 URL。
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Long> countByExternalConversationIds(String hotelId, List<String> externalConversationIds) {
|
||||
if (!hasText(hotelId) || externalConversationIds == null || externalConversationIds.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
List<String> safeConversationIds = externalConversationIds.stream()
|
||||
.filter(this::hasText)
|
||||
.map(this::trim)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (safeConversationIds.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
Map<String, Long> counts = new LinkedHashMap<>();
|
||||
inboxMapper.selectList(Wrappers.<SourceMessageInboxEntity>lambdaQuery()
|
||||
.eq(SourceMessageInboxEntity::getHotelId, trim(hotelId))
|
||||
.in(SourceMessageInboxEntity::getExternalConversationId, safeConversationIds))
|
||||
.forEach(entity -> counts.merge(entity.getExternalConversationId(), 1L, Long::sum));
|
||||
return counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入 Inbox 主记录及原始 payload;RECEIVED 状态额外保存正文和媒体引用。
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,7 @@ import cn.nianxx.thhotel.platform.message.common.result.SourceMessagePageResult;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.SourceMessageQueryRequest;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@@ -25,6 +26,11 @@ public interface SourceMessageInboxRepository {
|
||||
*/
|
||||
List<SourceMessageInboxSnapshot> findByIds(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 按外部邮件会话 ID 查询同一会话全部 Inbox 安全快照。
|
||||
*/
|
||||
List<SourceMessageInboxSnapshot> findByExternalConversationId(String hotelId, String externalConversationId);
|
||||
|
||||
/**
|
||||
* 按酒店、来源、渠道、外部邮件 ID 查询幂等记录。
|
||||
*/
|
||||
@@ -47,6 +53,11 @@ public interface SourceMessageInboxRepository {
|
||||
*/
|
||||
List<Long> findIdsBySafeKeyword(String hotelId, String keyword, int limit);
|
||||
|
||||
/**
|
||||
* 批量统计外部邮件会话下的 SourceMessage 数量,用于任务和订单前端摘要。
|
||||
*/
|
||||
Map<String, Long> countByExternalConversationIds(String hotelId, List<String> externalConversationIds);
|
||||
|
||||
/**
|
||||
* 插入 Inbox 及其 payload/body/media 子记录,返回内部 SourceMessage ID。
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package cn.nianxx.thhotel.platform.message.service;
|
||||
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageConversationResult;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* SourceMessage 邮件会话详情服务。该服务会返回完整正文、HTML 和媒体外链,并写入内部读取审计。
|
||||
*/
|
||||
public interface SourceMessageConversationService {
|
||||
|
||||
/**
|
||||
* 读取指定 SourceMessage 所在邮件会话的完整详情。
|
||||
*
|
||||
* @param sourceMessageId 当前定位的 SourceMessage Inbox ID
|
||||
* @return 找到时返回同一会话全部邮件详情;不存在时为空
|
||||
*/
|
||||
Optional<SourceMessageConversationResult> getConversation(Long sourceMessageId);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageSummaryRespons
|
||||
import cn.nianxx.thhotel.platform.message.common.request.SourceMessageQueryRequest;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessagePageResult;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@@ -43,4 +44,13 @@ public interface SourceMessageQueryService {
|
||||
* @return 匹配到的安全摘要列表,不返回正文、HTML、附件 URL 或 payload
|
||||
*/
|
||||
List<SourceMessageSummaryResponse> getSummariesByIds(List<Long> inboxIds);
|
||||
|
||||
/**
|
||||
* 批量统计外部邮件会话下的 SourceMessage 数量,供业务摘要字段展示。
|
||||
*
|
||||
* @param hotelId 酒店上下文 ID
|
||||
* @param externalConversationIds 外部邮件会话 ID 列表
|
||||
* @return 会话 ID 到消息数量的映射
|
||||
*/
|
||||
Map<String, Long> countByExternalConversationIds(String hotelId, List<String> externalConversationIds);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package cn.nianxx.thhotel.platform.message.service;
|
||||
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageRelatedContextResult;
|
||||
|
||||
/**
|
||||
* SourceMessage 关联业务上下文 Provider。业务模块实现该接口,平台会话服务只做聚合展示。
|
||||
*/
|
||||
public interface SourceMessageRelatedContextProvider {
|
||||
|
||||
/**
|
||||
* 查询单封 SourceMessage 关联的业务摘要。
|
||||
*
|
||||
* @param hotelId 酒店上下文 ID
|
||||
* @param sourceMessageId SourceMessage Inbox ID
|
||||
* @return 关联订单和任务摘要;没有关联时返回空列表结果
|
||||
*/
|
||||
SourceMessageRelatedContextResult findRelatedContext(String hotelId, Long sourceMessageId);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package cn.nianxx.thhotel.platform.message.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.platform.common.time.UtcTimeFormatter;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageInboxSnapshot;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageOriginalAccessAuditDraft;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageOriginalContent;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageOriginalMediaItem;
|
||||
import cn.nianxx.thhotel.platform.message.common.enums.SourceMessageOriginalAccessResult;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageConversationMessageResult;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageConversationResult;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageConversationSummaryResult;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageOriginalMediaResponse;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageRelatedContextResult;
|
||||
import cn.nianxx.thhotel.platform.message.repository.SourceMessageInboxRepository;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageConversationService;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageRelatedContextProvider;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* SourceMessage 邮件会话详情服务实现。该服务负责受控读取原文并写入后端内部审计。
|
||||
*/
|
||||
@Service
|
||||
public class SourceMessageConversationServiceImpl implements SourceMessageConversationService {
|
||||
|
||||
private static final String INTERNAL_ACTOR_ID = "system: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_ATTACHMENT = "ATTACHMENT";
|
||||
|
||||
private final SourceMessageInboxRepository inboxRepository;
|
||||
private final List<SourceMessageRelatedContextProvider> relatedContextProviders;
|
||||
|
||||
/**
|
||||
* 注入 SourceMessage 持久化端口和业务关联上下文 Provider 列表。
|
||||
*/
|
||||
public SourceMessageConversationServiceImpl(
|
||||
SourceMessageInboxRepository inboxRepository,
|
||||
List<SourceMessageRelatedContextProvider> relatedContextProviders) {
|
||||
this.inboxRepository = inboxRepository;
|
||||
this.relatedContextProviders = relatedContextProviders == null ? List.of() : relatedContextProviders;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取指定 SourceMessage 所在会话的完整原文链路,并为每封邮件写入内部读取审计。
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public Optional<SourceMessageConversationResult> getConversation(Long sourceMessageId) {
|
||||
Optional<SourceMessageInboxSnapshot> sourceOptional = inboxRepository.findById(sourceMessageId);
|
||||
if (sourceOptional.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
SourceMessageInboxSnapshot source = sourceOptional.get();
|
||||
List<SourceMessageInboxSnapshot> conversationMessages = findConversationMessages(source);
|
||||
SourceMessageConversationSummaryResult conversation = toConversationSummary(source, conversationMessages);
|
||||
List<SourceMessageConversationMessageResult> messages = conversationMessages.stream()
|
||||
.map(this::toConversationMessage)
|
||||
.toList();
|
||||
return Optional.of(new SourceMessageConversationResult(conversation, messages));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询同一外部会话邮件;外部会话 ID 缺失时降级为单封邮件详情。
|
||||
*/
|
||||
private List<SourceMessageInboxSnapshot> findConversationMessages(SourceMessageInboxSnapshot source) {
|
||||
if (source.externalConversationId() == null || source.externalConversationId().isBlank()) {
|
||||
return List.of(source);
|
||||
}
|
||||
List<SourceMessageInboxSnapshot> messages = inboxRepository.findByExternalConversationId(
|
||||
source.hotelId(),
|
||||
source.externalConversationId());
|
||||
return messages.isEmpty() ? List.of(source) : messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成会话顶部摘要,时间统一转为 UTC OffsetDateTime。
|
||||
*/
|
||||
private SourceMessageConversationSummaryResult toConversationSummary(
|
||||
SourceMessageInboxSnapshot source,
|
||||
List<SourceMessageInboxSnapshot> messages) {
|
||||
SourceMessageInboxSnapshot first = messages.get(0);
|
||||
SourceMessageInboxSnapshot last = messages.get(messages.size() - 1);
|
||||
return new SourceMessageConversationSummaryResult(
|
||||
source.hotelId(),
|
||||
source.channel(),
|
||||
source.externalConversationId(),
|
||||
source.subject(),
|
||||
(long) messages.size(),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(first.receivedAt()),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(last.receivedAt()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Inbox 快照转换为会话邮件详情,并附带原文、媒体外链和业务关联摘要。
|
||||
*/
|
||||
private SourceMessageConversationMessageResult toConversationMessage(SourceMessageInboxSnapshot message) {
|
||||
SourceMessageOriginalContent originalContent = readOriginalAndAudit(message);
|
||||
SourceMessageRelatedContextResult relatedContext = findRelatedContext(message);
|
||||
return new SourceMessageConversationMessageResult(
|
||||
message.id().toString(),
|
||||
message.externalMessageId(),
|
||||
message.externalConversationId(),
|
||||
message.senderSummary(),
|
||||
message.subject(),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(message.receivedAt()),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(message.sourceSentAt()),
|
||||
originalContent.textBody(),
|
||||
originalContent.htmlBody(),
|
||||
true,
|
||||
filterMedia(originalContent.mediaItems(), MEDIA_TYPE_INLINE_IMAGE),
|
||||
filterMedia(originalContent.mediaItems(), MEDIA_TYPE_ATTACHMENT),
|
||||
relatedContext.relatedOrders(),
|
||||
relatedContext.relatedTasks());
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取原文并写入内部审计。会话接口不依赖前端传原文读取 key。
|
||||
*/
|
||||
private SourceMessageOriginalContent readOriginalAndAudit(SourceMessageInboxSnapshot message) {
|
||||
SourceMessageOriginalContent content = inboxRepository.findOriginalContent(message.id())
|
||||
.orElse(new SourceMessageOriginalContent(message.id(), null, null, List.of()));
|
||||
LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
|
||||
inboxRepository.insertOriginalAccessAudit(new SourceMessageOriginalAccessAuditDraft(
|
||||
message.id(),
|
||||
INTERNAL_ACTOR_ID,
|
||||
ACCESS_SCENE,
|
||||
SourceMessageOriginalAccessResult.GRANTED.code(),
|
||||
now));
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 聚合所有业务模块提供的关联上下文。当前 reservation 模块会提供订单和任务摘要。
|
||||
*/
|
||||
private SourceMessageRelatedContextResult findRelatedContext(SourceMessageInboxSnapshot message) {
|
||||
List<SourceMessageRelatedContextResult> contexts = relatedContextProviders.stream()
|
||||
.map(provider -> provider.findRelatedContext(message.hotelId(), message.id()))
|
||||
.toList();
|
||||
return new SourceMessageRelatedContextResult(
|
||||
contexts.stream().flatMap(context -> context.relatedOrders().stream()).toList(),
|
||||
contexts.stream().flatMap(context -> context.relatedTasks().stream()).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按媒体类型筛选并转换原文媒体引用。
|
||||
*/
|
||||
private List<SourceMessageOriginalMediaResponse> filterMedia(
|
||||
List<SourceMessageOriginalMediaItem> mediaItems,
|
||||
String mediaType) {
|
||||
if (mediaItems == null || mediaItems.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return mediaItems.stream()
|
||||
.filter(item -> mediaType.equals(item.mediaType()))
|
||||
.map(this::toMediaResponse)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换媒体内部 DTO 为接口响应对象。
|
||||
*/
|
||||
private SourceMessageOriginalMediaResponse toMediaResponse(SourceMessageOriginalMediaItem item) {
|
||||
return new SourceMessageOriginalMediaResponse(
|
||||
item.mediaType(),
|
||||
item.fileName(),
|
||||
item.contentType(),
|
||||
item.sizeBytes(),
|
||||
item.externalUrl(),
|
||||
item.externalMediaId());
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import cn.nianxx.thhotel.platform.message.common.result.SourceMessagePageResult;
|
||||
import cn.nianxx.thhotel.platform.message.repository.SourceMessageInboxRepository;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageQueryService;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -72,6 +73,14 @@ public class SourceMessageQueryServiceImpl implements SourceMessageQueryService
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量统计邮件会话消息数,只访问 SourceMessage Inbox 索引表。
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Long> countByExternalConversationIds(String hotelId, List<String> externalConversationIds) {
|
||||
return inboxRepository.countByExternalConversationIds(hotelId, externalConversationIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化页码,缺失或非法页码统一回到第一页。
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.request;
|
||||
|
||||
/**
|
||||
* 前端订单列表查询条件。第一版默认查询全部订单状态,按酒店上下文做强过滤。
|
||||
*
|
||||
* @param hotelId 酒店上下文 ID
|
||||
* @param orderStatus 订单状态,可为空;为空时查询全部状态
|
||||
* @param groupCode Group Code 精确或模糊查询值
|
||||
* @param confirmationNumber Confirmation No. 精确或模糊查询值
|
||||
* @param keyword 订单展示名、业务号或临时订单号关键词
|
||||
* @param pageNum 页码,从 1 开始
|
||||
* @param pageSize 每页数量
|
||||
*/
|
||||
public record ReservationOrderListQueryRequest(
|
||||
String hotelId,
|
||||
String orderStatus,
|
||||
String groupCode,
|
||||
String confirmationNumber,
|
||||
String keyword,
|
||||
Integer pageNum,
|
||||
Integer pageSize
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/**
|
||||
* 前端订单列表单行结果。用于工作台订单视角快速判断订单状态和可继续处理的任务。
|
||||
*
|
||||
* @param orderId 订单 ID,按字符串返回避免前端长整型精度问题
|
||||
* @param hotelId 酒店上下文 ID
|
||||
* @param orderStatus 订单状态
|
||||
* @param displayOrderKey 前端优先展示的订单业务号或临时订单号
|
||||
* @param temporaryOrderNo 临时订单号
|
||||
* @param groupCode Group Code,仅订单业务号类型为 GROUP_CODE 时返回
|
||||
* @param confirmationNumber Confirmation No.,仅订单业务号类型为 CONFIRMATION_NUMBER 时返回
|
||||
* @param displayName 订单展示名
|
||||
* @param openTaskCount 未关闭任务数,排除 COMPLETED 和 FAILED
|
||||
* @param nextProcessableTaskId 下一条当前可处理任务 ID
|
||||
* @param createdAt 订单创建时间,UTC
|
||||
* @param updatedAt 订单更新时间,UTC
|
||||
*/
|
||||
public record ReservationOrderListItemResult(
|
||||
@JsonProperty("order_id")
|
||||
String orderId,
|
||||
@JsonProperty("hotel_id")
|
||||
String hotelId,
|
||||
@JsonProperty("order_status")
|
||||
String orderStatus,
|
||||
@JsonProperty("display_order_key")
|
||||
String displayOrderKey,
|
||||
@JsonProperty("temporary_order_no")
|
||||
String temporaryOrderNo,
|
||||
@JsonProperty("group_code")
|
||||
String groupCode,
|
||||
@JsonProperty("confirmation_number")
|
||||
String confirmationNumber,
|
||||
@JsonProperty("display_name")
|
||||
String displayName,
|
||||
@JsonProperty("open_task_count")
|
||||
Integer openTaskCount,
|
||||
@JsonProperty("next_processable_task_id")
|
||||
String nextProcessableTaskId,
|
||||
@JsonProperty("created_at")
|
||||
OffsetDateTime createdAt,
|
||||
@JsonProperty("updated_at")
|
||||
OffsetDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 前端订单列表分页结果。items 为当前页订单,page 为统一分页摘要。
|
||||
*
|
||||
* @param items 当前页订单列表
|
||||
* @param page 分页信息
|
||||
*/
|
||||
public record ReservationOrderListResult(
|
||||
List<ReservationOrderListItemResult> items,
|
||||
ReservationPaginationResult page
|
||||
) {
|
||||
}
|
||||
@@ -15,6 +15,12 @@ import java.time.OffsetDateTime;
|
||||
* @param queueParticipation 是否参与执行队列
|
||||
* @param canProcess 当前是否可处理
|
||||
* @param readonlyReasonCode 只读原因代码
|
||||
* @param sourceMessageId 来源 SourceMessage Inbox ID
|
||||
* @param sourceSubject 来源消息主题摘要
|
||||
* @param sourceSenderSummary 来源发送人安全摘要
|
||||
* @param sourceReceivedAt 本系统接收来源消息时间,UTC
|
||||
* @param externalConversationId 外部邮件会话 ID
|
||||
* @param conversationMessageCount 同一外部邮件会话下的消息数量
|
||||
* @param createdAt 任务创建时间
|
||||
*/
|
||||
public record ReservationOrderTaskTimelineItemResult(
|
||||
@@ -36,6 +42,18 @@ public record ReservationOrderTaskTimelineItemResult(
|
||||
Boolean canProcess,
|
||||
@JsonProperty("readonly_reason_code")
|
||||
String readonlyReasonCode,
|
||||
@JsonProperty("source_message_id")
|
||||
String sourceMessageId,
|
||||
@JsonProperty("source_subject")
|
||||
String sourceSubject,
|
||||
@JsonProperty("source_sender_summary")
|
||||
String sourceSenderSummary,
|
||||
@JsonProperty("source_received_at")
|
||||
OffsetDateTime sourceReceivedAt,
|
||||
@JsonProperty("external_conversation_id")
|
||||
String externalConversationId,
|
||||
@JsonProperty("conversation_message_count")
|
||||
Long conversationMessageCount,
|
||||
@JsonProperty("created_at")
|
||||
OffsetDateTime createdAt
|
||||
) {
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 任务详情结果。提供队列可处理状态、任务卡 payload 和按矩阵生成的字段列表。
|
||||
*
|
||||
* @param taskId 任务 ID
|
||||
* @param orderId 订单 ID
|
||||
* @param sourceMessageId 来源 SourceMessage Inbox ID
|
||||
* @param sourceSubject 来源消息主题摘要
|
||||
* @param sourceSenderSummary 来源发送人安全摘要
|
||||
* @param sourceReceivedAt 本系统接收来源消息时间,UTC
|
||||
* @param externalConversationId 外部邮件会话 ID
|
||||
* @param conversationMessageCount 同一外部邮件会话下的消息数量
|
||||
* @param systemTaskType 系统主任务类型
|
||||
* @param taskCardType 任务卡类型
|
||||
* @param taskStatus 任务状态
|
||||
* @param fieldContractVersion 字段矩阵契约版本
|
||||
* @param draftPayload 草稿 payload
|
||||
* @param confirmedPayload 最终确认 payload
|
||||
* @param availability 当前可处理状态
|
||||
* @param fields 按字段矩阵生成的字段列表
|
||||
* @param operaOperations OPERA 模拟操作列表
|
||||
*/
|
||||
public record ReservationTaskDetailResult(
|
||||
@JsonProperty("task_id")
|
||||
@@ -13,6 +32,16 @@ public record ReservationTaskDetailResult(
|
||||
String orderId,
|
||||
@JsonProperty("source_message_id")
|
||||
String sourceMessageId,
|
||||
@JsonProperty("source_subject")
|
||||
String sourceSubject,
|
||||
@JsonProperty("source_sender_summary")
|
||||
String sourceSenderSummary,
|
||||
@JsonProperty("source_received_at")
|
||||
OffsetDateTime sourceReceivedAt,
|
||||
@JsonProperty("external_conversation_id")
|
||||
String externalConversationId,
|
||||
@JsonProperty("conversation_message_count")
|
||||
Long conversationMessageCount,
|
||||
@JsonProperty("system_task_type")
|
||||
String systemTaskType,
|
||||
@JsonProperty("task_card_type")
|
||||
|
||||
@@ -4,12 +4,45 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* 任务详情字段结果。字段规则来自矩阵,value 为当前可回显值。
|
||||
*
|
||||
* @param rowNumber 矩阵行号
|
||||
* @param cardName 任务卡展示名称
|
||||
* @param resultType AI 结果类型白名单值
|
||||
* @param taskType 任务类型白名单值
|
||||
* @param taskSubtype 任务 subtype 白名单值
|
||||
* @param displayArea 前端展示区域
|
||||
* @param fieldPath 字段路径
|
||||
* @param displayName 字段展示名称
|
||||
* @param visible 是否可见
|
||||
* @param editable 是否可编辑
|
||||
* @param inputEditable 是否文本输入可编辑
|
||||
* @param selectEditable 是否下拉选择可编辑
|
||||
* @param datePicker 是否日期选择
|
||||
* @param numberInput 是否数字输入
|
||||
* @param fileDisplay 是否文件展示
|
||||
* @param tableEditable 是否表格编辑
|
||||
* @param enumOptions 枚举选项
|
||||
* @param requiredRule 必填规则
|
||||
* @param defaultValueSource 默认值来源
|
||||
* @param displayCondition 展示条件
|
||||
* @param validationRule 校验规则
|
||||
* @param writePath 确认写入路径
|
||||
* @param operaWriteParticipation 是否参与 OPERA 参数组装
|
||||
* @param operaParameterMapping OPERA 参数映射说明
|
||||
* @param notes 备注说明
|
||||
* @param value 当前回显值
|
||||
*/
|
||||
public record ReservationTaskFieldResult(
|
||||
@JsonProperty("row_number")
|
||||
Integer rowNumber,
|
||||
@JsonProperty("card_name")
|
||||
String cardName,
|
||||
@JsonProperty("result_type")
|
||||
String resultType,
|
||||
@JsonProperty("task_type")
|
||||
String taskType,
|
||||
@JsonProperty("task_subtype")
|
||||
String taskSubtype,
|
||||
@JsonProperty("display_area")
|
||||
String displayArea,
|
||||
@JsonProperty("field_path")
|
||||
@@ -34,6 +67,8 @@ public record ReservationTaskFieldResult(
|
||||
String enumOptions,
|
||||
@JsonProperty("required_rule")
|
||||
String requiredRule,
|
||||
@JsonProperty("default_value_source")
|
||||
String defaultValueSource,
|
||||
@JsonProperty("display_condition")
|
||||
String displayCondition,
|
||||
@JsonProperty("validation_rule")
|
||||
|
||||
@@ -21,6 +21,10 @@ import java.time.OffsetDateTime;
|
||||
* @param readonlyReasonCode 只读原因代码
|
||||
* @param sourceMessageId 来源 SourceMessage Inbox ID
|
||||
* @param sourceSubject 来源消息主题摘要
|
||||
* @param sourceSenderSummary 来源发送人安全摘要
|
||||
* @param sourceReceivedAt 本系统接收来源消息时间,UTC
|
||||
* @param externalConversationId 外部邮件会话 ID
|
||||
* @param conversationMessageCount 同一外部邮件会话下的消息数量
|
||||
* @param createdAt 任务创建时间
|
||||
* @param updatedAt 任务更新时间
|
||||
*/
|
||||
@@ -55,6 +59,14 @@ public record ReservationTaskWorkbenchItemResult(
|
||||
String sourceMessageId,
|
||||
@JsonProperty("source_subject")
|
||||
String sourceSubject,
|
||||
@JsonProperty("source_sender_summary")
|
||||
String sourceSenderSummary,
|
||||
@JsonProperty("source_received_at")
|
||||
OffsetDateTime sourceReceivedAt,
|
||||
@JsonProperty("external_conversation_id")
|
||||
String externalConversationId,
|
||||
@JsonProperty("conversation_message_count")
|
||||
Long conversationMessageCount,
|
||||
@JsonProperty("created_at")
|
||||
OffsetDateTime createdAt,
|
||||
@JsonProperty("updated_at")
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.control;
|
||||
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationOrderListQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationTaskWorkbenchQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderDetailResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskWorkbenchListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationFrontendQueryService;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -53,6 +55,28 @@ public class ReservationFrontendQueryController {
|
||||
pageSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单列表,默认返回全部订单状态,并实时聚合未关闭任务数量。
|
||||
*/
|
||||
@GetMapping(value = "/orders", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ReservationOrderListResult listOrders(
|
||||
@RequestParam(name = "hotel_id", required = false) String hotelId,
|
||||
@RequestParam(name = "order_status", required = false) String orderStatus,
|
||||
@RequestParam(name = "group_code", required = false) String groupCode,
|
||||
@RequestParam(name = "confirmation_number", required = false) String confirmationNumber,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(name = "page_num", required = false) Integer pageNum,
|
||||
@RequestParam(name = "page_size", required = false) Integer pageSize) {
|
||||
return frontendQueryService.queryOrders(new ReservationOrderListQueryRequest(
|
||||
hotelId,
|
||||
orderStatus,
|
||||
groupCode,
|
||||
confirmationNumber,
|
||||
keyword,
|
||||
pageNum,
|
||||
pageSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单详情和任务时间线,供订单详情页展示当前订单处理脉络。
|
||||
*/
|
||||
|
||||
@@ -18,6 +18,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskCardDra
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskCardSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationOrderListQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationTaskWorkbenchQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderKeyType;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderStatus;
|
||||
@@ -371,6 +372,70 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按来源消息 ID 批量查询订单快照,供 SourceMessage 会话关联摘要使用。
|
||||
*/
|
||||
@Override
|
||||
public List<ReservationAiQueryOrderSnapshot> findAiQueryOrdersBySourceMessageIds(
|
||||
String hotelId,
|
||||
List<Long> sourceMessageIds) {
|
||||
if (sourceMessageIds == null || sourceMessageIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return orderMapper.selectList(Wrappers.<ReservationOrderEntity>lambdaQuery()
|
||||
.eq(ReservationOrderEntity::getHotelId, hotelId)
|
||||
.in(ReservationOrderEntity::getSourceMessageId, sourceMessageIds)
|
||||
.orderByDesc(ReservationOrderEntity::getUpdatedAt))
|
||||
.stream()
|
||||
.map(this::toAiQueryOrderSnapshot)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端订单列表分页查询。状态参数为空时不加状态过滤,默认返回全部订单状态。
|
||||
*/
|
||||
@Override
|
||||
public ReservationPageSnapshot<ReservationAiQueryOrderSnapshot> queryFrontendOrders(
|
||||
ReservationOrderListQueryRequest request,
|
||||
int pageNum,
|
||||
int pageSize) {
|
||||
Page<ReservationOrderEntity> page = orderMapper.selectPage(Page.of(pageNum, pageSize),
|
||||
Wrappers.<ReservationOrderEntity>lambdaQuery()
|
||||
.eq(ReservationOrderEntity::getHotelId, request.hotelId())
|
||||
.eq(hasText(request.orderStatus()),
|
||||
ReservationOrderEntity::getOrderStatus,
|
||||
trim(request.orderStatus()))
|
||||
.and(hasText(request.groupCode()), wrapper -> wrapper
|
||||
.like(ReservationOrderEntity::getOrderBusinessKey, trim(request.groupCode()))
|
||||
.or()
|
||||
.like(ReservationOrderEntity::getActiveBusinessKey, trim(request.groupCode()))
|
||||
.or()
|
||||
.like(ReservationOrderEntity::getDisplayName, trim(request.groupCode())))
|
||||
.and(hasText(request.confirmationNumber()), wrapper -> wrapper
|
||||
.like(ReservationOrderEntity::getOrderBusinessKey, trim(request.confirmationNumber()))
|
||||
.or()
|
||||
.like(ReservationOrderEntity::getActiveBusinessKey, trim(request.confirmationNumber()))
|
||||
.or()
|
||||
.like(ReservationOrderEntity::getDisplayName, trim(request.confirmationNumber())))
|
||||
.and(hasText(request.keyword()), wrapper -> wrapper
|
||||
.like(ReservationOrderEntity::getOrderBusinessKey, trim(request.keyword()))
|
||||
.or()
|
||||
.like(ReservationOrderEntity::getActiveBusinessKey, trim(request.keyword()))
|
||||
.or()
|
||||
.like(ReservationOrderEntity::getTemporaryOrderCode, trim(request.keyword()))
|
||||
.or()
|
||||
.like(ReservationOrderEntity::getOrderStatus, trim(request.keyword()))
|
||||
.or()
|
||||
.like(ReservationOrderEntity::getDisplayName, trim(request.keyword())))
|
||||
.orderByDesc(ReservationOrderEntity::getUpdatedAt)
|
||||
.orderByDesc(ReservationOrderEntity::getId));
|
||||
return new ReservationPageSnapshot<>(
|
||||
page.getRecords().stream().map(this::toAiQueryOrderSnapshot).toList(),
|
||||
page.getTotal(),
|
||||
pageNum,
|
||||
pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端任务列表分页查询。只读取任务摘要字段,完整任务卡 payload 仍走任务详情接口。
|
||||
*/
|
||||
@@ -428,6 +493,24 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
|
||||
return toAiQueryTaskSnapshots(hotelId, tasks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按来源消息 ID 批量查询任务快照,供 SourceMessage 会话关联摘要使用。
|
||||
*/
|
||||
@Override
|
||||
public List<ReservationAiQueryTaskSnapshot> findAiQueryTasksBySourceMessageIds(
|
||||
String hotelId,
|
||||
List<Long> sourceMessageIds) {
|
||||
if (sourceMessageIds == null || sourceMessageIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<ReservationTaskEntity> tasks = taskMapper.selectList(Wrappers.<ReservationTaskEntity>lambdaQuery()
|
||||
.eq(ReservationTaskEntity::getHotelId, hotelId)
|
||||
.in(ReservationTaskEntity::getSourceMessageId, sourceMessageIds)
|
||||
.orderByAsc(ReservationTaskEntity::getOrderId)
|
||||
.orderByAsc(ReservationTaskEntity::getExecutionOrder));
|
||||
return toAiQueryTaskSnapshots(hotelId, tasks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 AI transition 中冗余的 Group Code 或 Confirmation Number 查询任务。
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskCardSna
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationTaskWorkbenchQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationOrderListQueryRequest;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -109,6 +110,21 @@ public interface ReservationAiWorkflowRepository {
|
||||
*/
|
||||
List<ReservationAiQueryOrderSnapshot> findAiQueryOrdersByIds(String hotelId, List<Long> orderIds);
|
||||
|
||||
/**
|
||||
* 按来源消息 ID 批量查询订单快照,用于 SourceMessage 会话关联业务摘要。
|
||||
*/
|
||||
List<ReservationAiQueryOrderSnapshot> findAiQueryOrdersBySourceMessageIds(
|
||||
String hotelId,
|
||||
List<Long> sourceMessageIds);
|
||||
|
||||
/**
|
||||
* 分页查询前端订单列表,默认包含全部订单状态。
|
||||
*/
|
||||
ReservationPageSnapshot<ReservationAiQueryOrderSnapshot> queryFrontendOrders(
|
||||
ReservationOrderListQueryRequest request,
|
||||
int pageNum,
|
||||
int pageSize);
|
||||
|
||||
/**
|
||||
* 分页查询前端任务列表 / 工作台摘要。
|
||||
*/
|
||||
@@ -123,6 +139,13 @@ public interface ReservationAiWorkflowRepository {
|
||||
*/
|
||||
List<ReservationAiQueryTaskSnapshot> findAiQueryTasksByOrderIds(String hotelId, List<Long> orderIds);
|
||||
|
||||
/**
|
||||
* 按来源消息 ID 批量查询任务快照,用于邮件会话关联任务摘要。
|
||||
*/
|
||||
List<ReservationAiQueryTaskSnapshot> findAiQueryTasksBySourceMessageIds(
|
||||
String hotelId,
|
||||
List<Long> sourceMessageIds);
|
||||
|
||||
/**
|
||||
* 按 AI transition 冗余业务 key 查询任务,用于查到待处理任务但尚无正式订单的场景。
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service;
|
||||
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationOrderListQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationTaskWorkbenchQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderDetailResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskWorkbenchListResult;
|
||||
|
||||
/**
|
||||
@@ -14,6 +16,11 @@ public interface ReservationFrontendQueryService {
|
||||
*/
|
||||
ReservationTaskWorkbenchListResult queryTaskWorkbench(ReservationTaskWorkbenchQueryRequest request);
|
||||
|
||||
/**
|
||||
* 查询订单列表,并实时聚合未关闭任务数量和下一条可处理任务。
|
||||
*/
|
||||
ReservationOrderListResult queryOrders(ReservationOrderListQueryRequest request);
|
||||
|
||||
/**
|
||||
* 查询订单详情和同订单任务时间线,供前端订单详情页展示。
|
||||
*/
|
||||
|
||||
@@ -9,8 +9,11 @@ import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationPageSnapsho
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderKeyType;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationTaskStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationOrderListQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationTaskWorkbenchQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderDetailResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderListItemResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderSummaryResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderTaskTimelineItemResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationPaginationResult;
|
||||
@@ -19,6 +22,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskWork
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskWorkbenchListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationAiWorkflowRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationFrontendQueryService;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -78,19 +82,50 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
Map<Long, ReservationTaskAvailabilityResult> availabilityByTaskId = findAvailabilityByTaskId(
|
||||
normalizedRequest.hotelId(),
|
||||
page.items());
|
||||
Map<Long, String> sourceSubjectsById = findSourceSubjectsById(page.items());
|
||||
Map<Long, SourceMessageDisplayContext> sourceContextsById = findSourceContextsById(
|
||||
normalizedRequest.hotelId(),
|
||||
page.items());
|
||||
List<ReservationTaskWorkbenchItemResult> items = page.items().stream()
|
||||
.map(task -> toWorkbenchItem(
|
||||
task,
|
||||
ordersById.get(task.orderId()),
|
||||
availabilityOrReadOnly(task, availabilityByTaskId),
|
||||
sourceSubjectsById.get(task.sourceMessageId())))
|
||||
sourceContextsById.get(task.sourceMessageId())))
|
||||
.toList();
|
||||
return new ReservationTaskWorkbenchListResult(
|
||||
items,
|
||||
new ReservationPaginationResult(page.pageNum(), page.pageSize(), page.total()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单列表,并按当前任务队列实时计算未关闭任务数量和下一条可处理任务。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public ReservationOrderListResult queryOrders(ReservationOrderListQueryRequest request) {
|
||||
ReservationOrderListQueryRequest normalizedRequest = normalizeOrderListRequest(request);
|
||||
int pageNum = normalizePageNum(normalizedRequest.pageNum());
|
||||
int pageSize = normalizePageSize(normalizedRequest.pageSize());
|
||||
ReservationPageSnapshot<ReservationAiQueryOrderSnapshot> page =
|
||||
workflowRepository.queryFrontendOrders(normalizedRequest, pageNum, pageSize);
|
||||
List<Long> orderIds = page.items().stream().map(ReservationAiQueryOrderSnapshot::id).toList();
|
||||
List<ReservationAiQueryTaskSnapshot> orderTasks = workflowRepository.findAiQueryTasksByOrderIds(
|
||||
normalizedRequest.hotelId(),
|
||||
orderIds);
|
||||
Map<Long, List<ReservationAiQueryTaskSnapshot>> tasksByOrderId = groupTasksByOrderId(orderTasks);
|
||||
Map<Long, ReservationTaskAvailabilityResult> availabilityByTaskId =
|
||||
calculateAvailabilityByTaskId(orderTasks, orderTasks);
|
||||
List<ReservationOrderListItemResult> items = page.items().stream()
|
||||
.map(order -> toOrderListItem(
|
||||
order,
|
||||
tasksByOrderId.getOrDefault(order.id(), List.of()),
|
||||
availabilityByTaskId))
|
||||
.toList();
|
||||
return new ReservationOrderListResult(
|
||||
items,
|
||||
new ReservationPaginationResult(page.pageNum(), page.pageSize(), page.total()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单详情。includeTasks=false 时只返回订单摘要,便于后续前端轻量使用。
|
||||
*/
|
||||
@@ -114,8 +149,14 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
Map<Long, ReservationTaskAvailabilityResult> availabilityByTaskId = calculateAvailabilityByTaskId(
|
||||
taskSnapshots,
|
||||
taskSnapshots);
|
||||
Map<Long, SourceMessageDisplayContext> sourceContextsById = findSourceContextsById(
|
||||
normalizedHotelId,
|
||||
taskSnapshots);
|
||||
List<ReservationOrderTaskTimelineItemResult> tasks = taskSnapshots.stream()
|
||||
.map(task -> toTimelineItem(task, availabilityOrReadOnly(task, availabilityByTaskId)))
|
||||
.map(task -> toTimelineItem(
|
||||
task,
|
||||
availabilityOrReadOnly(task, availabilityByTaskId),
|
||||
sourceContextsById.get(task.sourceMessageId())))
|
||||
.toList();
|
||||
return new ReservationOrderDetailResult(toOrderSummary(order), tasks, List.of());
|
||||
}
|
||||
@@ -148,6 +189,30 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
request.pageSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化订单列表查询条件。订单状态为空时保留为空,表示查询全部状态。
|
||||
*/
|
||||
private ReservationOrderListQueryRequest normalizeOrderListRequest(ReservationOrderListQueryRequest request) {
|
||||
if (request == null) {
|
||||
return new ReservationOrderListQueryRequest(
|
||||
DEFAULT_HOTEL_ID,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
DEFAULT_PAGE_NUM,
|
||||
DEFAULT_PAGE_SIZE);
|
||||
}
|
||||
return new ReservationOrderListQueryRequest(
|
||||
normalizeHotelId(request.hotelId()),
|
||||
trimToNull(request.orderStatus()),
|
||||
trimToNull(request.groupCode()),
|
||||
trimToNull(request.confirmationNumber()),
|
||||
trimToNull(request.keyword()),
|
||||
request.pageNum(),
|
||||
request.pageSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量读取订单快照并按订单 ID 建立索引。
|
||||
*/
|
||||
@@ -192,7 +257,9 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
/**
|
||||
* 批量读取来源消息主题摘要,避免任务列表按每条任务调用 SourceMessage 详情查询。
|
||||
*/
|
||||
private Map<Long, String> findSourceSubjectsById(List<ReservationAiQueryTaskSnapshot> tasks) {
|
||||
private Map<Long, SourceMessageDisplayContext> findSourceContextsById(
|
||||
String hotelId,
|
||||
List<ReservationAiQueryTaskSnapshot> tasks) {
|
||||
if (tasks == null || tasks.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
@@ -201,9 +268,22 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
Map<Long, String> result = new LinkedHashMap<>();
|
||||
for (SourceMessageSummaryResponse summary : sourceMessageQueryService.getSummariesByIds(sourceMessageIds)) {
|
||||
result.put(Long.valueOf(summary.id()), summary.subject());
|
||||
List<SourceMessageSummaryResponse> summaries = sourceMessageQueryService.getSummariesByIds(sourceMessageIds);
|
||||
Map<String, Long> conversationCounts = sourceMessageQueryService.countByExternalConversationIds(
|
||||
hotelId,
|
||||
summaries.stream()
|
||||
.map(SourceMessageSummaryResponse::externalConversationId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList());
|
||||
Map<Long, SourceMessageDisplayContext> result = new LinkedHashMap<>();
|
||||
for (SourceMessageSummaryResponse summary : summaries) {
|
||||
result.put(Long.valueOf(summary.id()), new SourceMessageDisplayContext(
|
||||
summary.subject(),
|
||||
summary.senderSummary(),
|
||||
summary.receivedAt(),
|
||||
summary.externalConversationId(),
|
||||
conversationCounts.get(summary.externalConversationId())));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -225,7 +305,7 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
ReservationAiQueryTaskSnapshot task,
|
||||
ReservationAiQueryOrderSnapshot order,
|
||||
ReservationTaskAvailabilityResult availability,
|
||||
String sourceSubject) {
|
||||
SourceMessageDisplayContext sourceContext) {
|
||||
return new ReservationTaskWorkbenchItemResult(
|
||||
task.id().toString(),
|
||||
task.orderId().toString(),
|
||||
@@ -241,7 +321,11 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
canProcess(availability),
|
||||
readonlyReasonCode(task, availability),
|
||||
task.sourceMessageId().toString(),
|
||||
sourceSubject,
|
||||
sourceContext == null ? null : sourceContext.subject(),
|
||||
sourceContext == null ? null : sourceContext.senderSummary(),
|
||||
sourceContext == null ? null : sourceContext.receivedAt(),
|
||||
sourceContext == null ? null : sourceContext.externalConversationId(),
|
||||
sourceContext == null ? null : sourceContext.conversationMessageCount(),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(task.createdAt()),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(task.updatedAt()));
|
||||
}
|
||||
@@ -251,7 +335,8 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
*/
|
||||
private ReservationOrderTaskTimelineItemResult toTimelineItem(
|
||||
ReservationAiQueryTaskSnapshot task,
|
||||
ReservationTaskAvailabilityResult availability) {
|
||||
ReservationTaskAvailabilityResult availability,
|
||||
SourceMessageDisplayContext sourceContext) {
|
||||
return new ReservationOrderTaskTimelineItemResult(
|
||||
task.id().toString(),
|
||||
task.systemTaskType(),
|
||||
@@ -262,9 +347,37 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
task.queueParticipation(),
|
||||
canProcess(availability),
|
||||
readonlyReasonCode(task, availability),
|
||||
task.sourceMessageId().toString(),
|
||||
sourceContext == null ? null : sourceContext.subject(),
|
||||
sourceContext == null ? null : sourceContext.senderSummary(),
|
||||
sourceContext == null ? null : sourceContext.receivedAt(),
|
||||
sourceContext == null ? null : sourceContext.externalConversationId(),
|
||||
sourceContext == null ? null : sourceContext.conversationMessageCount(),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(task.createdAt()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换订单快照为前端订单列表单行结果。
|
||||
*/
|
||||
private ReservationOrderListItemResult toOrderListItem(
|
||||
ReservationAiQueryOrderSnapshot order,
|
||||
List<ReservationAiQueryTaskSnapshot> tasks,
|
||||
Map<Long, ReservationTaskAvailabilityResult> availabilityByTaskId) {
|
||||
return new ReservationOrderListItemResult(
|
||||
order.id().toString(),
|
||||
order.hotelId(),
|
||||
order.orderStatus(),
|
||||
displayOrderKey(order),
|
||||
order.temporaryOrderCode(),
|
||||
groupCode(order),
|
||||
confirmationNumber(order),
|
||||
order.displayName(),
|
||||
openTaskCount(tasks),
|
||||
nextProcessableTaskId(tasks, availabilityByTaskId),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(order.createdAt()),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(order.updatedAt()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换订单快照为前端订单摘要。
|
||||
*/
|
||||
@@ -283,6 +396,41 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(order.updatedAt()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按订单 ID 对任务分组,并保持数据库返回顺序。
|
||||
*/
|
||||
private Map<Long, List<ReservationAiQueryTaskSnapshot>> groupTasksByOrderId(
|
||||
List<ReservationAiQueryTaskSnapshot> tasks) {
|
||||
Map<Long, List<ReservationAiQueryTaskSnapshot>> tasksByOrderId = new LinkedHashMap<>();
|
||||
for (ReservationAiQueryTaskSnapshot task : tasks) {
|
||||
tasksByOrderId.computeIfAbsent(task.orderId(), ignored -> new java.util.ArrayList<>()).add(task);
|
||||
}
|
||||
return tasksByOrderId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计未关闭任务数量,排除 COMPLETED 和 FAILED。
|
||||
*/
|
||||
private Integer openTaskCount(List<ReservationAiQueryTaskSnapshot> tasks) {
|
||||
return Math.toIntExact(tasks.stream()
|
||||
.filter(task -> !ReservationTaskStatus.COMPLETED.name().equals(task.taskStatus()))
|
||||
.filter(task -> !ReservationTaskStatus.FAILED.name().equals(task.taskStatus()))
|
||||
.count());
|
||||
}
|
||||
|
||||
/**
|
||||
* 找到当前第一条可处理任务。FAILED 已结束,不会阻塞后续任务。
|
||||
*/
|
||||
private String nextProcessableTaskId(
|
||||
List<ReservationAiQueryTaskSnapshot> tasks,
|
||||
Map<Long, ReservationTaskAvailabilityResult> availabilityByTaskId) {
|
||||
return tasks.stream()
|
||||
.filter(task -> canProcess(availabilityOrReadOnly(task, availabilityByTaskId)))
|
||||
.map(task -> task.id().toString())
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 AI 查询任务快照为可处理状态解析器使用的通用任务快照。
|
||||
*/
|
||||
@@ -417,4 +565,15 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 来源邮件会话展示上下文。只包含 Inbox 安全摘要,不包含正文、HTML 或附件 URL。
|
||||
*/
|
||||
private record SourceMessageDisplayContext(
|
||||
String subject,
|
||||
String senderSummary,
|
||||
OffsetDateTime receivedAt,
|
||||
String externalConversationId,
|
||||
Long conversationMessageCount) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageRelatedContextResult;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageRelatedOrderResult;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageRelatedTaskResult;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageRelatedContextProvider;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiQueryOrderSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiQueryTaskSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderKeyType;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationAiWorkflowRepository;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Reservation 模块提供的 SourceMessage 关联上下文实现。用于邮件会话详情展示关联订单和任务摘要。
|
||||
*/
|
||||
@Service
|
||||
public class ReservationSourceMessageRelatedContextProvider implements SourceMessageRelatedContextProvider {
|
||||
|
||||
private final ReservationAiWorkflowRepository workflowRepository;
|
||||
|
||||
/**
|
||||
* 注入 Reservation 工作流持久化边界,避免平台层直接依赖 reservation 内部表。
|
||||
*/
|
||||
public ReservationSourceMessageRelatedContextProvider(ReservationAiWorkflowRepository workflowRepository) {
|
||||
this.workflowRepository = workflowRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定 SourceMessage 在 reservation 工作流中的订单和任务关联摘要。
|
||||
*/
|
||||
@Override
|
||||
public SourceMessageRelatedContextResult findRelatedContext(String hotelId, Long sourceMessageId) {
|
||||
if (sourceMessageId == null) {
|
||||
return new SourceMessageRelatedContextResult(List.of(), List.of());
|
||||
}
|
||||
List<ReservationAiQueryTaskSnapshot> tasks = workflowRepository.findAiQueryTasksBySourceMessageIds(
|
||||
hotelId,
|
||||
List.of(sourceMessageId));
|
||||
Map<Long, ReservationAiQueryOrderSnapshot> ordersById = findRelatedOrders(hotelId, sourceMessageId, tasks);
|
||||
return new SourceMessageRelatedContextResult(
|
||||
ordersById.values().stream().map(this::toRelatedOrder).toList(),
|
||||
tasks.stream().map(this::toRelatedTask).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并来源消息直接创建的订单和任务当前挂靠订单,避免只展示其中一侧。
|
||||
*/
|
||||
private Map<Long, ReservationAiQueryOrderSnapshot> findRelatedOrders(
|
||||
String hotelId,
|
||||
Long sourceMessageId,
|
||||
List<ReservationAiQueryTaskSnapshot> tasks) {
|
||||
Map<Long, ReservationAiQueryOrderSnapshot> ordersById = new LinkedHashMap<>();
|
||||
workflowRepository.findAiQueryOrdersBySourceMessageIds(hotelId, List.of(sourceMessageId))
|
||||
.forEach(order -> ordersById.put(order.id(), order));
|
||||
List<Long> taskOrderIds = tasks.stream()
|
||||
.map(ReservationAiQueryTaskSnapshot::orderId)
|
||||
.distinct()
|
||||
.toList();
|
||||
workflowRepository.findAiQueryOrdersByIds(hotelId, taskOrderIds)
|
||||
.forEach(order -> ordersById.put(order.id(), order));
|
||||
return ordersById;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换订单快照为 SourceMessage 会话可展示的关联订单摘要。
|
||||
*/
|
||||
private SourceMessageRelatedOrderResult toRelatedOrder(ReservationAiQueryOrderSnapshot order) {
|
||||
return new SourceMessageRelatedOrderResult(
|
||||
order.id().toString(),
|
||||
displayOrderKey(order),
|
||||
order.orderStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换任务快照为 SourceMessage 会话可展示的关联任务摘要。
|
||||
*/
|
||||
private SourceMessageRelatedTaskResult toRelatedTask(ReservationAiQueryTaskSnapshot task) {
|
||||
return new SourceMessageRelatedTaskResult(
|
||||
task.id().toString(),
|
||||
task.orderId().toString(),
|
||||
task.systemTaskType(),
|
||||
task.taskSubtype(),
|
||||
task.taskStatus(),
|
||||
task.taskCardType());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成订单展示键,优先真实业务号,其次临时订单号。
|
||||
*/
|
||||
private String displayOrderKey(ReservationAiQueryOrderSnapshot order) {
|
||||
String activeBusinessKey = trimToNull(order.activeBusinessKey());
|
||||
if (activeBusinessKey != null) {
|
||||
return activeBusinessKey;
|
||||
}
|
||||
String orderBusinessKey = trimToNull(order.orderBusinessKey());
|
||||
if (orderBusinessKey != null) {
|
||||
return orderBusinessKey;
|
||||
}
|
||||
if (ReservationOrderKeyType.TEMPORARY.name().equals(order.orderKeyType())) {
|
||||
return order.temporaryOrderCode();
|
||||
}
|
||||
return order.displayName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 去除空白字符串,空字符串按 null 处理。
|
||||
*/
|
||||
private String trimToNull(String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageSummaryResponse;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageQueryService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationAttemptDraft;
|
||||
@@ -38,6 +40,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
@@ -74,6 +77,7 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
|
||||
private final ReservationAiWorkflowRepository workflowRepository;
|
||||
private final ReservationTaskCardFieldDefinitionProvider fieldDefinitionProvider;
|
||||
private final ReservationTaskAvailabilityResolver availabilityResolver;
|
||||
private final SourceMessageQueryService sourceMessageQueryService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
@@ -83,10 +87,12 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
|
||||
ReservationAiWorkflowRepository workflowRepository,
|
||||
ReservationTaskCardFieldDefinitionProvider fieldDefinitionProvider,
|
||||
ReservationTaskAvailabilityResolver availabilityResolver,
|
||||
SourceMessageQueryService sourceMessageQueryService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.workflowRepository = workflowRepository;
|
||||
this.fieldDefinitionProvider = fieldDefinitionProvider;
|
||||
this.availabilityResolver = availabilityResolver;
|
||||
this.sourceMessageQueryService = sourceMessageQueryService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@@ -103,10 +109,16 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
|
||||
ReservationTaskAvailabilityResult availability = availabilityResolver.calculateAvailability(task);
|
||||
List<ReservationTaskFieldResult> fields = buildFieldResults(task, taskCard);
|
||||
List<ReservationOperaOperationResult> operaOperations = findOperaOperationResults(task);
|
||||
SourceMessageDetailContext sourceContext = findSourceMessageDetailContext(task);
|
||||
return new ReservationTaskDetailResult(
|
||||
task.id().toString(),
|
||||
task.orderId().toString(),
|
||||
task.sourceMessageId().toString(),
|
||||
sourceContext.subject(),
|
||||
sourceContext.senderSummary(),
|
||||
sourceContext.receivedAt(),
|
||||
sourceContext.externalConversationId(),
|
||||
sourceContext.conversationMessageCount(),
|
||||
task.systemTaskType(),
|
||||
task.taskCardType(),
|
||||
task.taskStatus(),
|
||||
@@ -1230,6 +1242,30 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询任务详情顶部来源消息会话摘要。只读取安全摘要,不读取正文或附件 URL。
|
||||
*/
|
||||
private SourceMessageDetailContext findSourceMessageDetailContext(ReservationTaskSnapshot task) {
|
||||
SourceMessageSummaryResponse summary = sourceMessageQueryService.getSummary(task.sourceMessageId())
|
||||
.orElse(null);
|
||||
if (summary == null) {
|
||||
return new SourceMessageDetailContext(null, null, null, null, null);
|
||||
}
|
||||
Long conversationMessageCount = null;
|
||||
if (trimToNull(summary.externalConversationId()) != null) {
|
||||
conversationMessageCount = sourceMessageQueryService.countByExternalConversationIds(
|
||||
task.hotelId(),
|
||||
List.of(summary.externalConversationId()))
|
||||
.get(summary.externalConversationId());
|
||||
}
|
||||
return new SourceMessageDetailContext(
|
||||
summary.subject(),
|
||||
summary.senderSummary(),
|
||||
summary.receivedAt(),
|
||||
summary.externalConversationId(),
|
||||
conversationMessageCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字段定义转换为详情字段结果,并从 AI payload 中读取当前值。
|
||||
*/
|
||||
@@ -1237,6 +1273,9 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
|
||||
return new ReservationTaskFieldResult(
|
||||
definition.rowNumber(),
|
||||
definition.cardName(),
|
||||
definition.resultType(),
|
||||
definition.taskType(),
|
||||
definition.taskSubtype(),
|
||||
definition.displayArea(),
|
||||
definition.fieldPath(),
|
||||
definition.displayName(),
|
||||
@@ -1250,6 +1289,7 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
|
||||
definition.tableEditable(),
|
||||
definition.enumOptions(),
|
||||
definition.requiredRule(),
|
||||
definition.defaultValueSource(),
|
||||
definition.displayCondition(),
|
||||
definition.validationRule(),
|
||||
definition.writePath(),
|
||||
@@ -1259,6 +1299,17 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
|
||||
valueAt(aiPayload, definition.fieldPath()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务详情来源消息展示上下文。只包含 SourceMessage Inbox 安全摘要字段。
|
||||
*/
|
||||
private record SourceMessageDetailContext(
|
||||
String subject,
|
||||
String senderSummary,
|
||||
OffsetDateTime receivedAt,
|
||||
String externalConversationId,
|
||||
Long conversationMessageCount) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JSON 中按简单点路径读取值,支持矩阵中的数组路径后缀。
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user