补齐订单详情V4任务时间线

This commit is contained in:
andy
2026-07-19 13:15:14 +07:00
parent 10c28f0acb
commit 13364e7edf
9 changed files with 496 additions and 25 deletions

View File

@@ -1,17 +1,21 @@
package cn.nianxx.thhotel.workflows.reservation.common.result;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
/**
* 前端订单详情响应,包含订单摘要、任务时间线和非阻塞警告。
* 前端订单详情响应,包含订单摘要、旧任务时间线、V4 订单任务时间线和非阻塞警告。
*
* @param order 订单摘要
* @param tasks 同订单任务时间线
* @param tasks 同订单任务时间线
* @param v4OrderTasks 同订单 V4 订单任务时间线
* @param warnings 当前无法提供的扩展信息或非阻塞提醒
*/
public record ReservationOrderDetailResult(
ReservationOrderSummaryResult order,
List<ReservationOrderTaskTimelineItemResult> tasks,
@JsonProperty("v4_order_tasks")
List<ReservationOrderV4TaskTimelineItemResult> v4OrderTasks,
List<String> warnings
) {
}

View File

@@ -0,0 +1,39 @@
package cn.nianxx.thhotel.workflows.reservation.common.result;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.OffsetDateTime;
/**
* 订单详情页 V4 订单任务时间线单项。用于旧订单详情页补充展示 V4 多卡任务摘要。
*
* @param orderTaskId V4 订单任务 ID
* @param orderRef V4 回调包内订单引用
* @param orderTaskStatus V4 订单任务状态
* @param cardCounts V4 卡片数量摘要
* @param sourceMessageSummary 来源邮件安全摘要
* @param sourceReceivedAt 来源邮件接收 UTC 时间
* @param createdAt V4 订单任务创建 UTC 时间
* @param updatedAt V4 订单任务更新 UTC 时间
* @param latestActivityAt V4 订单任务及其卡片的最新活动 UTC 时间
*/
public record ReservationOrderV4TaskTimelineItemResult(
@JsonProperty("order_task_id")
String orderTaskId,
@JsonProperty("order_ref")
String orderRef,
@JsonProperty("order_task_status")
String orderTaskStatus,
@JsonProperty("card_counts")
ReservationV4CardCountsResult cardCounts,
@JsonProperty("source_message_summary")
ReservationV4SourceMessageSummaryResult sourceMessageSummary,
@JsonProperty("source_received_at")
OffsetDateTime sourceReceivedAt,
@JsonProperty("created_at")
OffsetDateTime createdAt,
@JsonProperty("updated_at")
OffsetDateTime updatedAt,
@JsonProperty("latest_activity_at")
OffsetDateTime latestActivityAt
) {
}

View File

@@ -9,9 +9,12 @@ import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiQueryOrde
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiQueryTaskSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationPageSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4TaskCardSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderKeyType;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderVisibility;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationTaskStatus;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CardStatus;
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;
@@ -19,12 +22,17 @@ import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderLis
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.ReservationOrderV4TaskTimelineItemResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationPaginationResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskAvailabilityResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskWorkbenchItemResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskWorkbenchListResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4CardCountsResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4SourceMessageSummaryResult;
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationAiWorkflowRepository;
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4WorkflowRepository;
import cn.nianxx.thhotel.workflows.reservation.service.ReservationFrontendQueryService;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.LinkedHashMap;
import java.util.List;
@@ -49,6 +57,7 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
private static final String REASON_READ_ONLY = "READ_ONLY";
private final ReservationAiWorkflowRepository workflowRepository;
private final ReservationV4WorkflowRepository v4WorkflowRepository;
private final SourceMessageQueryService sourceMessageQueryService;
private final ReservationTaskAvailabilityResolver availabilityResolver;
private final HotelContextService hotelContextService;
@@ -58,10 +67,12 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
*/
public ReservationFrontendQueryServiceImpl(
ReservationAiWorkflowRepository workflowRepository,
ReservationV4WorkflowRepository v4WorkflowRepository,
SourceMessageQueryService sourceMessageQueryService,
ReservationTaskAvailabilityResolver availabilityResolver,
HotelContextService hotelContextService) {
this.workflowRepository = workflowRepository;
this.v4WorkflowRepository = v4WorkflowRepository;
this.sourceMessageQueryService = sourceMessageQueryService;
this.availabilityResolver = availabilityResolver;
this.hotelContextService = hotelContextService;
@@ -172,7 +183,10 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
availabilityOrReadOnly(task, availabilityByTaskId),
sourceContextsById.get(task.sourceMessageId())))
.toList();
return new ReservationOrderDetailResult(toOrderSummary(order), tasks, List.of());
List<ReservationOrderV4TaskTimelineItemResult> v4OrderTasks = Boolean.FALSE.equals(includeTasks)
? List.of()
: findV4OrderTaskTimeline(orderHotelId, order.id());
return new ReservationOrderDetailResult(toOrderSummary(order), tasks, v4OrderTasks, List.of());
}
/**
@@ -304,6 +318,104 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
return result;
}
/**
* 查询订单详情页 V4 订单任务时间线,复用当前订单酒店作为对象级隔离边界。
*/
private List<ReservationOrderV4TaskTimelineItemResult> findV4OrderTaskTimeline(String hotelId, Long orderId) {
List<ReservationV4OrderTaskSnapshot> orderTasks = v4WorkflowRepository.findOrderTasksByOrderIds(
hotelId,
List.of(orderId));
if (orderTasks.isEmpty()) {
return List.of();
}
Map<Long, List<ReservationV4TaskCardSnapshot>> cardsByOrderTaskId = findV4TaskCardsByOrderTaskId(
hotelId,
orderTasks);
Map<Long, ReservationV4SourceMessageSummaryResult> sourceSummariesById = findV4SourceSummariesById(
hotelId,
orderTasks.stream().map(ReservationV4OrderTaskSnapshot::sourceMessageId).toList());
return orderTasks.stream()
.map(orderTask -> toV4TimelineItem(
orderTask,
cardsByOrderTaskId.getOrDefault(orderTask.id(), List.of()),
sourceSummariesById.get(orderTask.sourceMessageId())))
.toList();
}
/**
* 批量读取 V4 订单任务卡片并按订单任务 ID 分组,用于订单详情页统计卡片状态。
*/
private Map<Long, List<ReservationV4TaskCardSnapshot>> findV4TaskCardsByOrderTaskId(
String hotelId,
List<ReservationV4OrderTaskSnapshot> orderTasks) {
List<Long> orderTaskIds = orderTasks.stream()
.map(ReservationV4OrderTaskSnapshot::id)
.filter(Objects::nonNull)
.distinct()
.toList();
List<ReservationV4TaskCardSnapshot> cards = v4WorkflowRepository.findTaskCardsByOrderTaskIds(
hotelId,
orderTaskIds);
Map<Long, List<ReservationV4TaskCardSnapshot>> result = new LinkedHashMap<>();
for (ReservationV4TaskCardSnapshot card : cards) {
result.computeIfAbsent(card.v4OrderTaskId(), ignored -> new java.util.ArrayList<>()).add(card);
}
return result;
}
/**
* 批量读取 V4 来源邮件安全摘要;若数据缺失或酒店不匹配,只返回带 ID 的空摘要避免泄露。
*/
private Map<Long, ReservationV4SourceMessageSummaryResult> findV4SourceSummariesById(
String hotelId,
List<Long> sourceMessageIds) {
if (sourceMessageIds == null || sourceMessageIds.isEmpty()) {
return Map.of();
}
List<Long> uniqueIds = sourceMessageIds.stream()
.filter(Objects::nonNull)
.distinct()
.toList();
List<SourceMessageSummaryResponse> summaries = sourceMessageQueryService.getSummariesByIds(uniqueIds);
List<SourceMessageSummaryResponse> safeSummaries = summaries.stream()
.filter(summary -> Objects.equals(hotelId, summary.hotelId()))
.toList();
Map<String, Long> conversationCounts = sourceMessageQueryService.countByExternalConversationIds(
hotelId,
safeSummaries.stream()
.map(SourceMessageSummaryResponse::externalConversationId)
.filter(Objects::nonNull)
.distinct()
.toList());
Map<Long, ReservationV4SourceMessageSummaryResult> result = new LinkedHashMap<>();
for (SourceMessageSummaryResponse summary : safeSummaries) {
Long sourceMessageId = Long.valueOf(summary.id());
result.put(sourceMessageId, new ReservationV4SourceMessageSummaryResult(
summary.id(),
summary.hotelId(),
summary.externalMessageId(),
summary.externalConversationId(),
summary.subject(),
summary.senderSummary(),
summary.receivedAt(),
summary.sourceSentAt(),
conversationCounts.getOrDefault(summary.externalConversationId(), 1L)));
}
for (Long sourceMessageId : uniqueIds) {
result.putIfAbsent(sourceMessageId, new ReservationV4SourceMessageSummaryResult(
sourceMessageId.toString(),
hotelId,
null,
null,
null,
null,
null,
null,
null));
}
return result;
}
/**
* 按来源消息安全摘要关键词查找 SourceMessage ID避免任务列表直接读取来源消息正文。
*/
@@ -380,6 +492,63 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
UtcTimeFormatter.toUtcOffsetDateTime(task.createdAt()));
}
/**
* 转换 V4 订单任务快照为订单详情页时间线安全摘要。
*/
private ReservationOrderV4TaskTimelineItemResult toV4TimelineItem(
ReservationV4OrderTaskSnapshot orderTask,
List<ReservationV4TaskCardSnapshot> cards,
ReservationV4SourceMessageSummaryResult sourceSummary) {
return new ReservationOrderV4TaskTimelineItemResult(
orderTask.id().toString(),
orderTask.orderRef(),
orderTask.orderTaskStatus(),
v4CardCounts(cards),
sourceSummary,
UtcTimeFormatter.toUtcOffsetDateTime(orderTask.sourceReceivedAt()),
UtcTimeFormatter.toUtcOffsetDateTime(orderTask.createdAt()),
UtcTimeFormatter.toUtcOffsetDateTime(orderTask.updatedAt()),
UtcTimeFormatter.toUtcOffsetDateTime(latestV4ActivityAt(orderTask, cards)));
}
/**
* 统计 V4 订单任务下各状态卡片数量,供订单详情时间线轻量展示。
*/
private ReservationV4CardCountsResult v4CardCounts(List<ReservationV4TaskCardSnapshot> cards) {
List<ReservationV4TaskCardSnapshot> safeCards = cards == null ? List.of() : cards;
return new ReservationV4CardCountsResult(
safeCards.size(),
countV4Cards(safeCards, ReservationV4CardStatus.READONLY.name()),
countV4Cards(safeCards, ReservationV4CardStatus.PENDING_CONFIRM.name()),
countV4Cards(safeCards, ReservationV4CardStatus.REVIEW_REQUIRED.name()),
countV4Cards(safeCards, ReservationV4CardStatus.CONFIRMED.name()));
}
/**
* 统计指定 V4 卡片状态数量。
*/
private int countV4Cards(List<ReservationV4TaskCardSnapshot> cards, String cardStatus) {
return Math.toIntExact(cards.stream()
.filter(card -> cardStatus.equals(card.cardStatus()))
.count());
}
/**
* 派生 V4 订单任务最新活动时间,卡片确认或复核后可驱动订单详情时间线更新。
*/
private LocalDateTime latestV4ActivityAt(
ReservationV4OrderTaskSnapshot orderTask,
List<ReservationV4TaskCardSnapshot> cards) {
LocalDateTime latest = orderTask.updatedAt() == null ? orderTask.createdAt() : orderTask.updatedAt();
for (ReservationV4TaskCardSnapshot card : cards == null ? List.<ReservationV4TaskCardSnapshot>of() : cards) {
LocalDateTime cardActivityAt = card.updatedAt() == null ? card.createdAt() : card.updatedAt();
if (cardActivityAt != null && (latest == null || cardActivityAt.isAfter(latest))) {
latest = cardActivityAt;
}
}
return latest;
}
/**
* 转换订单快照为前端订单列表单行结果。
*/