补齐订单详情V4总览接口
This commit is contained in:
@@ -7,12 +7,21 @@ import java.util.List;
|
||||
* 前端订单详情响应,包含订单摘要、旧任务时间线、V4 订单任务时间线和非阻塞警告。
|
||||
*
|
||||
* @param order 订单摘要
|
||||
* @param orderOverview V4 当前订单快照摘要
|
||||
* @param nextV4Action V4 下一步处理入口
|
||||
* @param relatedSourceMessages 订单关联来源邮件摘要
|
||||
* @param tasks 同订单旧任务时间线
|
||||
* @param v4OrderTasks 同订单 V4 订单任务时间线
|
||||
* @param warnings 当前无法提供的扩展信息或非阻塞提醒
|
||||
*/
|
||||
public record ReservationOrderDetailResult(
|
||||
ReservationOrderSummaryResult order,
|
||||
@JsonProperty("order_overview")
|
||||
ReservationOrderV4OverviewResult orderOverview,
|
||||
@JsonProperty("next_v4_action")
|
||||
ReservationOrderV4NextActionResult nextV4Action,
|
||||
@JsonProperty("related_source_messages")
|
||||
List<ReservationV4SourceMessageSummaryResult> relatedSourceMessages,
|
||||
List<ReservationOrderTaskTimelineItemResult> tasks,
|
||||
@JsonProperty("v4_order_tasks")
|
||||
List<ReservationOrderV4TaskTimelineItemResult> v4OrderTasks,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* 订单详情页 V4 下一步处理入口摘要。前端用它跳转订单任务详情,不在订单详情页直接处理卡片。
|
||||
*
|
||||
* @param orderTaskId 下一步 V4 订单任务 ID
|
||||
* @param cardId 下一步待处理任务卡 ID
|
||||
* @param actionType 下一步动作类型,CONFIRM / REVIEW / NONE
|
||||
* @param actionStatus 下一步卡片状态,PENDING_CONFIRM / REVIEW_REQUIRED
|
||||
* @param openOrderTaskCount 当前订单下未完成 V4 订单任务数量
|
||||
*/
|
||||
public record ReservationOrderV4NextActionResult(
|
||||
@JsonProperty("order_task_id")
|
||||
String orderTaskId,
|
||||
@JsonProperty("card_id")
|
||||
String cardId,
|
||||
@JsonProperty("action_type")
|
||||
String actionType,
|
||||
@JsonProperty("action_status")
|
||||
String actionStatus,
|
||||
@JsonProperty("open_order_task_count")
|
||||
Integer openOrderTaskCount
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 订单详情页 V4 当前快照摘要。只从已确认 V4 卡片派生,不把未确认 AI 建议当作订单事实。
|
||||
*
|
||||
* @param accountCode Account 目录 code
|
||||
* @param accountName Account 展示名称
|
||||
* @param marketCode Market 目录 code
|
||||
* @param sourceCode Source 目录 code
|
||||
* @param arrivalDate 入住酒店本地日期
|
||||
* @param departureDate 离店酒店本地日期
|
||||
* @param rateCode Rate Code
|
||||
* @param roomItems 已确认房型房量摘要
|
||||
* @param traceCardStatus Trace 卡片当前状态
|
||||
* @param roomingListCardStatus Rooming List 卡片当前状态
|
||||
* @param paymentCardStatus Payment 卡片当前状态
|
||||
* @param latestConfirmedAt 最近一次 V4 卡片确认 UTC 时间
|
||||
*/
|
||||
public record ReservationOrderV4OverviewResult(
|
||||
@JsonProperty("account_code")
|
||||
String accountCode,
|
||||
@JsonProperty("account_name")
|
||||
String accountName,
|
||||
@JsonProperty("market_code")
|
||||
String marketCode,
|
||||
@JsonProperty("source_code")
|
||||
String sourceCode,
|
||||
@JsonProperty("arrival_date")
|
||||
String arrivalDate,
|
||||
@JsonProperty("departure_date")
|
||||
String departureDate,
|
||||
@JsonProperty("rate_code")
|
||||
String rateCode,
|
||||
@JsonProperty("room_items")
|
||||
List<ReservationOrderV4RoomSummaryResult> roomItems,
|
||||
@JsonProperty("trace_card_status")
|
||||
String traceCardStatus,
|
||||
@JsonProperty("rooming_list_card_status")
|
||||
String roomingListCardStatus,
|
||||
@JsonProperty("payment_card_status")
|
||||
String paymentCardStatus,
|
||||
@JsonProperty("latest_confirmed_at")
|
||||
OffsetDateTime latestConfirmedAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* 订单详情页 V4 房型房量摘要。
|
||||
*
|
||||
* @param roomTypeCode 房型目录 code
|
||||
* @param roomCount 房量
|
||||
*/
|
||||
public record ReservationOrderV4RoomSummaryResult(
|
||||
@JsonProperty("room_type_code")
|
||||
String roomTypeCode,
|
||||
@JsonProperty("room_count")
|
||||
Integer roomCount
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/**
|
||||
* 订单详情页 V4 时间线中的任务卡摘要。只返回状态和确认信息,不返回卡片业务字段。
|
||||
*
|
||||
* @param cardId V4 任务卡 ID
|
||||
* @param cardType 卡片类型
|
||||
* @param eventType 业务事件类型
|
||||
* @param sourceEventIndex 来源事件序号
|
||||
* @param cardSortOrder 卡片排序号
|
||||
* @param cardStatus 卡片状态
|
||||
* @param reviewStatus 复核状态
|
||||
* @param confirmedBy 确认人
|
||||
* @param confirmedAt 确认 UTC 时间
|
||||
* @param latestActivityAt 卡片最新活动 UTC 时间
|
||||
*/
|
||||
public record ReservationOrderV4TaskCardTimelineItemResult(
|
||||
@JsonProperty("card_id")
|
||||
String cardId,
|
||||
@JsonProperty("card_type")
|
||||
String cardType,
|
||||
@JsonProperty("event_type")
|
||||
String eventType,
|
||||
@JsonProperty("source_event_index")
|
||||
Integer sourceEventIndex,
|
||||
@JsonProperty("card_sort_order")
|
||||
Integer cardSortOrder,
|
||||
@JsonProperty("card_status")
|
||||
String cardStatus,
|
||||
@JsonProperty("review_status")
|
||||
String reviewStatus,
|
||||
@JsonProperty("confirmed_by")
|
||||
String confirmedBy,
|
||||
@JsonProperty("confirmed_at")
|
||||
OffsetDateTime confirmedAt,
|
||||
@JsonProperty("latest_activity_at")
|
||||
OffsetDateTime latestActivityAt
|
||||
) {
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 订单详情页 V4 订单任务时间线单项。用于旧订单详情页补充展示 V4 多卡任务摘要。
|
||||
@@ -10,6 +11,7 @@ import java.time.OffsetDateTime;
|
||||
* @param orderRef V4 回调包内订单引用
|
||||
* @param orderTaskStatus V4 订单任务状态
|
||||
* @param cardCounts V4 卡片数量摘要
|
||||
* @param cards V4 任务卡时间线摘要
|
||||
* @param sourceMessageSummary 来源邮件安全摘要
|
||||
* @param sourceReceivedAt 来源邮件接收 UTC 时间
|
||||
* @param createdAt V4 订单任务创建 UTC 时间
|
||||
@@ -25,6 +27,7 @@ public record ReservationOrderV4TaskTimelineItemResult(
|
||||
String orderTaskStatus,
|
||||
@JsonProperty("card_counts")
|
||||
ReservationV4CardCountsResult cardCounts,
|
||||
List<ReservationOrderV4TaskCardTimelineItemResult> cards,
|
||||
@JsonProperty("source_message_summary")
|
||||
ReservationV4SourceMessageSummaryResult sourceMessageSummary,
|
||||
@JsonProperty("source_received_at")
|
||||
|
||||
@@ -25,6 +25,10 @@ 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.ReservationOrderV4NextActionResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderV4OverviewResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderV4RoomSummaryResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderV4TaskCardTimelineItemResult;
|
||||
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;
|
||||
@@ -35,6 +39,10 @@ import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4Source
|
||||
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 com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.ArrayList;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -64,6 +72,7 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
private final SourceMessageQueryService sourceMessageQueryService;
|
||||
private final ReservationTaskAvailabilityResolver availabilityResolver;
|
||||
private final HotelContextService hotelContextService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 注入持久化边界、SourceMessage 安全摘要服务和可处理状态解析器。
|
||||
@@ -73,12 +82,14 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
ReservationV4WorkflowRepository v4WorkflowRepository,
|
||||
SourceMessageQueryService sourceMessageQueryService,
|
||||
ReservationTaskAvailabilityResolver availabilityResolver,
|
||||
HotelContextService hotelContextService) {
|
||||
HotelContextService hotelContextService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.workflowRepository = workflowRepository;
|
||||
this.v4WorkflowRepository = v4WorkflowRepository;
|
||||
this.sourceMessageQueryService = sourceMessageQueryService;
|
||||
this.availabilityResolver = availabilityResolver;
|
||||
this.hotelContextService = hotelContextService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,10 +201,19 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
availabilityOrReadOnly(task, availabilityByTaskId),
|
||||
sourceContextsById.get(task.sourceMessageId())))
|
||||
.toList();
|
||||
List<ReservationOrderV4TaskTimelineItemResult> v4OrderTasks = Boolean.FALSE.equals(includeTasks)
|
||||
? List.of()
|
||||
: findV4OrderTaskTimeline(orderHotelId, order.id());
|
||||
return new ReservationOrderDetailResult(toOrderSummary(order), tasks, v4OrderTasks, List.of());
|
||||
V4OrderDetailContext v4Context = Boolean.FALSE.equals(includeTasks)
|
||||
? V4OrderDetailContext.empty()
|
||||
: findV4OrderDetailContext(orderHotelId, order.id());
|
||||
List<ReservationOrderV4TaskTimelineItemResult> v4OrderTasks = toV4TimelineItems(v4Context);
|
||||
V4OrderListNextAction v4NextAction = toV4OrderListNextAction(v4Context.orderTasks(), v4Context.cardsByOrderTaskId());
|
||||
return new ReservationOrderDetailResult(
|
||||
toOrderSummary(order),
|
||||
toV4OrderOverview(v4Context.orderTasks(), v4Context.cardsByOrderTaskId()),
|
||||
toV4NextActionResult(v4NextAction),
|
||||
relatedV4SourceMessages(v4Context),
|
||||
tasks,
|
||||
v4OrderTasks,
|
||||
List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -328,12 +348,12 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
/**
|
||||
* 查询订单详情页 V4 订单任务时间线,复用当前订单酒店作为对象级隔离边界。
|
||||
*/
|
||||
private List<ReservationOrderV4TaskTimelineItemResult> findV4OrderTaskTimeline(String hotelId, Long orderId) {
|
||||
private V4OrderDetailContext findV4OrderDetailContext(String hotelId, Long orderId) {
|
||||
List<ReservationV4OrderTaskSnapshot> orderTasks = v4WorkflowRepository.findOrderTasksByOrderIds(
|
||||
hotelId,
|
||||
List.of(orderId));
|
||||
if (orderTasks.isEmpty()) {
|
||||
return List.of();
|
||||
return V4OrderDetailContext.empty();
|
||||
}
|
||||
Map<Long, List<ReservationV4TaskCardSnapshot>> cardsByOrderTaskId = findV4TaskCardsByOrderTaskId(
|
||||
hotelId,
|
||||
@@ -341,11 +361,18 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
Map<Long, ReservationV4SourceMessageSummaryResult> sourceSummariesById = findV4SourceSummariesById(
|
||||
hotelId,
|
||||
orderTasks.stream().map(ReservationV4OrderTaskSnapshot::sourceMessageId).toList());
|
||||
return orderTasks.stream()
|
||||
return new V4OrderDetailContext(orderTasks, cardsByOrderTaskId, sourceSummariesById);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换订单详情页 V4 时间线列表。
|
||||
*/
|
||||
private List<ReservationOrderV4TaskTimelineItemResult> toV4TimelineItems(V4OrderDetailContext context) {
|
||||
return context.orderTasks().stream()
|
||||
.map(orderTask -> toV4TimelineItem(
|
||||
orderTask,
|
||||
cardsByOrderTaskId.getOrDefault(orderTask.id(), List.of()),
|
||||
sourceSummariesById.get(orderTask.sourceMessageId())))
|
||||
context.cardsByOrderTaskId().getOrDefault(orderTask.id(), List.of()),
|
||||
context.sourceSummariesById().get(orderTask.sourceMessageId())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -511,6 +538,7 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
orderTask.orderRef(),
|
||||
orderTask.orderTaskStatus(),
|
||||
v4CardCounts(cards),
|
||||
toV4CardTimelineItems(cards),
|
||||
sourceSummary,
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(orderTask.sourceReceivedAt()),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(orderTask.createdAt()),
|
||||
@@ -518,6 +546,209 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(latestV4ActivityAt(orderTask, cards)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 V4 任务卡为订单详情时间线摘要,不返回业务字段 payload。
|
||||
*/
|
||||
private List<ReservationOrderV4TaskCardTimelineItemResult> toV4CardTimelineItems(
|
||||
List<ReservationV4TaskCardSnapshot> cards) {
|
||||
return (cards == null ? List.<ReservationV4TaskCardSnapshot>of() : cards).stream()
|
||||
.map(card -> new ReservationOrderV4TaskCardTimelineItemResult(
|
||||
card.id().toString(),
|
||||
card.cardType(),
|
||||
card.eventType(),
|
||||
card.sourceEventIndex(),
|
||||
card.cardSortOrder(),
|
||||
card.cardStatus(),
|
||||
card.reviewStatus(),
|
||||
card.confirmedBy(),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(card.confirmedAt()),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(card.updatedAt() == null
|
||||
? card.createdAt()
|
||||
: card.updatedAt())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从已确认 V4 卡片中派生订单详情当前快照;未确认 AI 建议不进入订单事实展示。
|
||||
*/
|
||||
private ReservationOrderV4OverviewResult toV4OrderOverview(
|
||||
List<ReservationV4OrderTaskSnapshot> orderTasks,
|
||||
Map<Long, List<ReservationV4TaskCardSnapshot>> cardsByOrderTaskId) {
|
||||
V4OrderOverviewDraft draft = new V4OrderOverviewDraft();
|
||||
for (ReservationV4OrderTaskSnapshot orderTask : orderTasks == null
|
||||
? List.<ReservationV4OrderTaskSnapshot>of()
|
||||
: orderTasks) {
|
||||
for (ReservationV4TaskCardSnapshot card : cardsByOrderTaskId.getOrDefault(orderTask.id(), List.of())) {
|
||||
applyV4CardStatus(draft, card);
|
||||
applyConfirmedV4CardSnapshot(draft, card);
|
||||
}
|
||||
}
|
||||
return new ReservationOrderV4OverviewResult(
|
||||
draft.accountCode,
|
||||
draft.accountName,
|
||||
draft.marketCode,
|
||||
draft.sourceCode,
|
||||
draft.arrivalDate,
|
||||
draft.departureDate,
|
||||
draft.rateCode,
|
||||
draft.roomItems,
|
||||
draft.traceCardStatus,
|
||||
draft.roomingListCardStatus,
|
||||
draft.paymentCardStatus,
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(draft.latestConfirmedAt));
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取任务卡状态到订单详情总览,帮助前端展示订单下 Trace / Rooming List / Payment 是否仍待处理。
|
||||
*/
|
||||
private void applyV4CardStatus(V4OrderOverviewDraft draft, ReservationV4TaskCardSnapshot card) {
|
||||
if (ReservationV4CardType.TRACE_RESERVATION_NOTES.name().equals(card.cardType())) {
|
||||
draft.traceCardStatus = card.cardStatus();
|
||||
} else if (ReservationV4CardType.ROOMING_LIST.name().equals(card.cardType())) {
|
||||
draft.roomingListCardStatus = card.cardStatus();
|
||||
} else if (ReservationV4CardType.PAYMENT.name().equals(card.cardType())) {
|
||||
draft.paymentCardStatus = card.cardStatus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从单张已确认 V4 卡片抽取订单总览字段,后出现的确认卡覆盖先前同字段。
|
||||
*/
|
||||
private void applyConfirmedV4CardSnapshot(V4OrderOverviewDraft draft, ReservationV4TaskCardSnapshot card) {
|
||||
if (!ReservationV4CardStatus.CONFIRMED.name().equals(card.cardStatus())
|
||||
|| trimToNull(card.confirmedPayloadJson()) == null) {
|
||||
return;
|
||||
}
|
||||
JsonNode payload = readJsonOrEmpty(card.confirmedPayloadJson());
|
||||
if (ReservationV4CardType.BASIC_INFORMATION.name().equals(card.cardType())) {
|
||||
JsonNode basicInformation = objectOrSelf(payload, "basic_information");
|
||||
draft.accountCode = coalesceText(basicInformation, "account_code", draft.accountCode);
|
||||
draft.accountName = coalesceText(basicInformation, "account_name", draft.accountName);
|
||||
draft.marketCode = coalesceText(basicInformation, "market_code", draft.marketCode);
|
||||
draft.sourceCode = coalesceText(basicInformation, "source_code", draft.sourceCode);
|
||||
} else if (ReservationV4CardType.ROOM_INFORMATION.name().equals(card.cardType())) {
|
||||
JsonNode businessFields = businessFields(payload);
|
||||
draft.arrivalDate = coalesceText(businessFields, "arrival_date", draft.arrivalDate);
|
||||
draft.departureDate = coalesceText(businessFields, "departure_date", draft.departureDate);
|
||||
draft.rateCode = coalesceText(businessFields, "rate_code", draft.rateCode);
|
||||
if (businessFields.path("room_items").isArray()) {
|
||||
draft.roomItems = roomSummaries(businessFields.path("room_items"));
|
||||
}
|
||||
}
|
||||
if (card.confirmedAt() != null
|
||||
&& (draft.latestConfirmedAt == null || card.confirmedAt().isAfter(draft.latestConfirmedAt))) {
|
||||
draft.latestConfirmedAt = card.confirmedAt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换订单详情页 V4 下一步入口结果。
|
||||
*/
|
||||
private ReservationOrderV4NextActionResult toV4NextActionResult(V4OrderListNextAction action) {
|
||||
V4OrderListNextAction safeAction = action == null ? V4OrderListNextAction.none() : action;
|
||||
return new ReservationOrderV4NextActionResult(
|
||||
safeAction.orderTaskId(),
|
||||
safeAction.cardId(),
|
||||
safeAction.actionType(),
|
||||
safeAction.actionStatus(),
|
||||
safeAction.openOrderTaskCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成订单关联来源邮件列表,按 V4 订单任务时间线去重。
|
||||
*/
|
||||
private List<ReservationV4SourceMessageSummaryResult> relatedV4SourceMessages(V4OrderDetailContext context) {
|
||||
Map<String, ReservationV4SourceMessageSummaryResult> related = new LinkedHashMap<>();
|
||||
for (ReservationV4OrderTaskSnapshot orderTask : context.orderTasks()) {
|
||||
ReservationV4SourceMessageSummaryResult summary = context.sourceSummariesById()
|
||||
.get(orderTask.sourceMessageId());
|
||||
if (summary != null && summary.sourceMessageId() != null) {
|
||||
related.putIfAbsent(summary.sourceMessageId(), summary);
|
||||
}
|
||||
}
|
||||
return List.copyOf(related.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取业务字段主体;Update Booking 的 after 结构优先作为订单当前快照来源。
|
||||
*/
|
||||
private JsonNode businessFields(JsonNode payload) {
|
||||
JsonNode businessFields = objectOrSelf(payload, "business_fields");
|
||||
JsonNode after = businessFields.path("after");
|
||||
return after.isObject() ? after : businessFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取对象字段;字段不存在或非对象时返回原节点,兼容测试和早期确认 payload。
|
||||
*/
|
||||
private JsonNode objectOrSelf(JsonNode node, String fieldName) {
|
||||
if (node == null || node.isMissingNode() || node.isNull()) {
|
||||
return objectMapper.createObjectNode();
|
||||
}
|
||||
JsonNode child = node.path(fieldName);
|
||||
return child.isObject() ? child : node;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 字符串解析兜底,避免脏历史数据让订单详情整体失败。
|
||||
*/
|
||||
private JsonNode readJsonOrEmpty(String json) {
|
||||
if (trimToNull(json) == null) {
|
||||
return objectMapper.createObjectNode();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readTree(json);
|
||||
} catch (JsonProcessingException ex) {
|
||||
return objectMapper.createObjectNode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取文本字段,空值保留既有快照值。
|
||||
*/
|
||||
private String coalesceText(JsonNode node, String fieldName, String fallback) {
|
||||
String value = trimToNull(node.path(fieldName).asText(null));
|
||||
return value == null ? fallback : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换已确认 room_items[] 为订单页安全摘要。
|
||||
*/
|
||||
private List<ReservationOrderV4RoomSummaryResult> roomSummaries(JsonNode roomItemsNode) {
|
||||
List<ReservationOrderV4RoomSummaryResult> roomItems = new ArrayList<>();
|
||||
for (JsonNode roomItem : roomItemsNode) {
|
||||
String roomTypeCode = coalesceText(roomItem, "room_type_code", null);
|
||||
if (roomTypeCode == null) {
|
||||
roomTypeCode = coalesceText(roomItem, "pms_room_type_code", null);
|
||||
}
|
||||
Integer roomCount = integerAt(roomItem, "room_count");
|
||||
if (roomCount == null) {
|
||||
roomCount = integerAt(roomItem, "room_quantity");
|
||||
}
|
||||
roomItems.add(new ReservationOrderV4RoomSummaryResult(roomTypeCode, roomCount));
|
||||
}
|
||||
return List.copyOf(roomItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* 宽松读取整数字段,兼容数字和字符串数字。
|
||||
*/
|
||||
private Integer integerAt(JsonNode node, String fieldName) {
|
||||
JsonNode value = node.path(fieldName);
|
||||
if (value.isInt() || value.isLong()) {
|
||||
return value.asInt();
|
||||
}
|
||||
String text = trimToNull(value.asText(null));
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Integer.valueOf(text);
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计 V4 订单任务下各状态卡片数量,供订单详情时间线轻量展示。
|
||||
*/
|
||||
@@ -974,6 +1205,37 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
Long conversationMessageCount) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单详情页 V4 聚合上下文,避免同一个接口为时间线、总览和下一步入口重复查库。
|
||||
*/
|
||||
private record V4OrderDetailContext(
|
||||
List<ReservationV4OrderTaskSnapshot> orderTasks,
|
||||
Map<Long, List<ReservationV4TaskCardSnapshot>> cardsByOrderTaskId,
|
||||
Map<Long, ReservationV4SourceMessageSummaryResult> sourceSummariesById) {
|
||||
|
||||
private static V4OrderDetailContext empty() {
|
||||
return new V4OrderDetailContext(List.of(), Map.of(), Map.of());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V4 订单总览派生草稿。仅在 Service 内部按时间线顺序覆盖字段,不向外暴露。
|
||||
*/
|
||||
private static final class V4OrderOverviewDraft {
|
||||
private String accountCode;
|
||||
private String accountName;
|
||||
private String marketCode;
|
||||
private String sourceCode;
|
||||
private String arrivalDate;
|
||||
private String departureDate;
|
||||
private String rateCode;
|
||||
private List<ReservationOrderV4RoomSummaryResult> roomItems = List.of();
|
||||
private String traceCardStatus;
|
||||
private String roomingListCardStatus;
|
||||
private String paymentCardStatus;
|
||||
private LocalDateTime latestConfirmedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单列表中一个本地订单对应的 V4 下一步处理摘要。
|
||||
*/
|
||||
|
||||
@@ -512,6 +512,111 @@ class ReservationFrontendQueryControllerTest {
|
||||
.andExpect(jsonPath("$.v4_order_tasks[1].source_received_at").value("2026-07-08T09:00:00Z"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnOrderDetailWithV4OverviewNextActionAndCardTimeline() throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage(
|
||||
"mail-frontend-order-detail-v4-overview-001",
|
||||
"Frontend V4 Overview",
|
||||
Instant.parse("2026-07-08T11:00:00Z"));
|
||||
Long orderId = 930000000000005001L;
|
||||
insertActiveGroupOrder(orderId, source.inboxId(), "GRP-FRONTEND-V4-OVERVIEW-001");
|
||||
ReservationV4OrderTaskSnapshot orderTask = insertV4OrderTask(
|
||||
930000000000005101L,
|
||||
source,
|
||||
orderId,
|
||||
"order-overview",
|
||||
"GRP-FRONTEND-V4-OVERVIEW-001",
|
||||
Instant.parse("2026-07-08T11:00:00Z"),
|
||||
HOTEL_ID);
|
||||
insertV4TaskCard(orderTask, ReservationV4CardType.SOURCE_MESSAGE_DISPLAY.name(), null, 0, 10,
|
||||
ReservationV4CardStatus.READONLY.name(), null, "{}");
|
||||
ReservationV4TaskCardSnapshot basicCard = insertV4TaskCard(
|
||||
orderTask, ReservationV4CardType.BASIC_INFORMATION.name(), null, 0, 20,
|
||||
ReservationV4CardStatus.CONFIRMED.name(), null, """
|
||||
{"basic_information":{"account_code":"QBD_TRAVEL"}}
|
||||
""");
|
||||
confirmV4TaskCard(basicCard.id(), """
|
||||
{
|
||||
"basic_information": {
|
||||
"account_code": "QBD_TRAVEL",
|
||||
"account_name": "Q.B.D. TRAVEL GROUP CO., LTD",
|
||||
"market_code": "LEISURE",
|
||||
"source_code": "TRAVEL_AGENT"
|
||||
}
|
||||
}
|
||||
""", "frontend-query-admin", Instant.parse("2026-07-08T11:10:00Z"));
|
||||
ReservationV4TaskCardSnapshot roomCard = insertV4TaskCard(
|
||||
orderTask, ReservationV4CardType.ROOM_INFORMATION.name(), "NEW_BOOKING", 1, 30,
|
||||
ReservationV4CardStatus.CONFIRMED.name(), null, "{}");
|
||||
confirmV4TaskCard(roomCard.id(), """
|
||||
{
|
||||
"business_fields": {
|
||||
"arrival_date": "2026-07-26",
|
||||
"departure_date": "2026-07-29",
|
||||
"rate_code": "BAR",
|
||||
"room_items": [
|
||||
{"room_type_code": "RM1", "room_count": 2},
|
||||
{"room_type_code": "RM2", "room_count": 1}
|
||||
]
|
||||
}
|
||||
}
|
||||
""", "frontend-query-admin", Instant.parse("2026-07-08T11:20:00Z"));
|
||||
insertV4TaskCard(orderTask, ReservationV4CardType.ROOM_INFORMATION.name(), "UPDATE_BOOKING", 2, 40,
|
||||
ReservationV4CardStatus.PENDING_CONFIRM.name(), null, """
|
||||
{
|
||||
"business_fields": {
|
||||
"arrival_date": "2099-01-01",
|
||||
"departure_date": "2099-01-02",
|
||||
"rate_code": "SHOULD_NOT_BE_ORDER_FACT",
|
||||
"room_items": [
|
||||
{"room_type_code": "SHOULD_NOT_APPEAR", "room_count": 99}
|
||||
]
|
||||
}
|
||||
}
|
||||
""");
|
||||
ReservationV4TaskCardSnapshot paymentCard = insertV4TaskCard(
|
||||
orderTask, ReservationV4CardType.PAYMENT.name(), "PAYMENT", 3, 60,
|
||||
ReservationV4CardStatus.REVIEW_REQUIRED.name(), "PENDING", "{}");
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders/{orderId}", orderId)
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("include_tasks", "true"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.order_overview.account_code").value("QBD_TRAVEL"))
|
||||
.andExpect(jsonPath("$.order_overview.account_name").value("Q.B.D. TRAVEL GROUP CO., LTD"))
|
||||
.andExpect(jsonPath("$.order_overview.market_code").value("LEISURE"))
|
||||
.andExpect(jsonPath("$.order_overview.source_code").value("TRAVEL_AGENT"))
|
||||
.andExpect(jsonPath("$.order_overview.arrival_date").value("2026-07-26"))
|
||||
.andExpect(jsonPath("$.order_overview.departure_date").value("2026-07-29"))
|
||||
.andExpect(jsonPath("$.order_overview.rate_code").value("BAR"))
|
||||
.andExpect(jsonPath("$.order_overview.room_items.length()").value(2))
|
||||
.andExpect(jsonPath("$.order_overview.room_items[0].room_type_code").value("RM1"))
|
||||
.andExpect(jsonPath("$.order_overview.room_items[0].room_count").value(2))
|
||||
.andExpect(jsonPath("$.order_overview.room_items[?(@.room_type_code=='SHOULD_NOT_APPEAR')]")
|
||||
.isEmpty())
|
||||
.andExpect(jsonPath("$.order_overview.payment_card_status").value("REVIEW_REQUIRED"))
|
||||
.andExpect(jsonPath("$.order_overview.latest_confirmed_at").value("2026-07-08T11:20:00Z"))
|
||||
.andExpect(jsonPath("$.next_v4_action.order_task_id").value(orderTask.id().toString()))
|
||||
.andExpect(jsonPath("$.next_v4_action.card_id").value(paymentCard.id().toString()))
|
||||
.andExpect(jsonPath("$.next_v4_action.action_type").value("REVIEW"))
|
||||
.andExpect(jsonPath("$.next_v4_action.action_status").value("REVIEW_REQUIRED"))
|
||||
.andExpect(jsonPath("$.next_v4_action.open_order_task_count").value(1))
|
||||
.andExpect(jsonPath("$.related_source_messages.length()").value(1))
|
||||
.andExpect(jsonPath("$.related_source_messages[0].source_message_id").value(source.inboxId().toString()))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].cards.length()").value(5))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].cards[1].card_id").value(basicCard.id().toString()))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].cards[1].card_type").value("BASIC_INFORMATION"))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].cards[1].card_status").value("CONFIRMED"))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].cards[1].confirmed_by").value("frontend-query-admin"))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].cards[1].confirmed_at").value("2026-07-08T11:10:00Z"))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].cards[1].ai_payload_json").doesNotExist())
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].cards[1].display_payload_json").doesNotExist())
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].cards[1].confirmed_payload_json").doesNotExist())
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].cards[4].card_id").value(paymentCard.id().toString()))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].cards[4].card_status").value("REVIEW_REQUIRED"))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].cards[4].review_status").value("PENDING"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHideV4OrderTaskTimelineWhenIncludeTasksFalseAndIgnoreCrossHotelRows() throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage(
|
||||
@@ -853,6 +958,20 @@ class ReservationFrontendQueryControllerTest {
|
||||
""", Timestamp.valueOf(LocalDateTime.ofInstant(updatedAt, ZoneOffset.UTC)), orderTaskId, cardSortOrder);
|
||||
}
|
||||
|
||||
private void confirmV4TaskCard(Long cardId, String confirmedPayloadJson, String confirmedBy, Instant confirmedAt) {
|
||||
jdbcTemplate.update("""
|
||||
UPDATE workflow_reservation_v4_task_card
|
||||
SET confirmed_payload_json = ?,
|
||||
confirmed_by = ?,
|
||||
confirmed_at = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
""", confirmedPayloadJson, confirmedBy,
|
||||
Timestamp.valueOf(LocalDateTime.ofInstant(confirmedAt, ZoneOffset.UTC)),
|
||||
Timestamp.valueOf(LocalDateTime.ofInstant(confirmedAt, ZoneOffset.UTC)),
|
||||
cardId);
|
||||
}
|
||||
|
||||
private ReservationV4OrderTaskSnapshot insertV4OrderTask(
|
||||
Long aiBatchId,
|
||||
SourceMessageCaptureResult source,
|
||||
|
||||
Reference in New Issue
Block a user