实现前端演示数据受控生成接口
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.request;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Reservation 前端演示数据生成请求。该请求只用于 dev/test 受控 seed,不允许作为生产业务入口。
|
||||
*
|
||||
* @param hotelId 酒店上下文 ID,为空时使用演示默认酒店
|
||||
* @param runLabel 本次演示数据的人类可读标签,会参与生成可搜索的 demo_run_id
|
||||
*/
|
||||
public record ReservationDemoDataSeedRequest(
|
||||
@JsonProperty("hotel_id")
|
||||
String hotelId,
|
||||
@JsonProperty("run_label")
|
||||
String runLabel
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Reservation 演示数据中的订单摘要。用于前端快速跳转订单详情。
|
||||
*
|
||||
* @param scenarioCode 演示场景代码
|
||||
* @param orderId 订单 ID
|
||||
* @param orderStatus 订单状态
|
||||
* @param displayOrderKey 前端展示订单号或临时订单号
|
||||
*/
|
||||
public record ReservationDemoDataOrderResult(
|
||||
@JsonProperty("scenario_code")
|
||||
String scenarioCode,
|
||||
@JsonProperty("order_id")
|
||||
String orderId,
|
||||
@JsonProperty("order_status")
|
||||
String orderStatus,
|
||||
@JsonProperty("display_order_key")
|
||||
String displayOrderKey
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Reservation 前端演示数据生成结果。返回本次 seed 的关键 ID 和可直接访问的后端查询入口。
|
||||
*
|
||||
* @param demoRunId 本次演示数据唯一运行 ID,可作为 keyword 过滤
|
||||
* @param hotelId 酒店上下文 ID
|
||||
* @param sourceMessages 本次生成的来源消息列表
|
||||
* @param orders 本次生成的订单列表
|
||||
* @param tasks 本次生成的任务列表
|
||||
* @param entrypoints 前端可直接调用的后端查询 URL
|
||||
* @param notes 使用注意事项
|
||||
*/
|
||||
public record ReservationDemoDataSeedResult(
|
||||
@JsonProperty("demo_run_id")
|
||||
String demoRunId,
|
||||
@JsonProperty("hotel_id")
|
||||
String hotelId,
|
||||
@JsonProperty("source_messages")
|
||||
List<ReservationDemoDataSourceMessageResult> sourceMessages,
|
||||
List<ReservationDemoDataOrderResult> orders,
|
||||
List<ReservationDemoDataTaskResult> tasks,
|
||||
Map<String, String> entrypoints,
|
||||
List<String> notes
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Reservation 演示数据中的来源消息摘要。ID 均以字符串返回,避免前端 Long 精度问题。
|
||||
*
|
||||
* @param scenarioCode 演示场景代码
|
||||
* @param sourceMessageId SourceMessage Inbox 内部 ID
|
||||
* @param externalMessageId 外部邮件消息 ID
|
||||
* @param externalConversationId 外部邮件会话 ID
|
||||
* @param subject 邮件主题
|
||||
*/
|
||||
public record ReservationDemoDataSourceMessageResult(
|
||||
@JsonProperty("scenario_code")
|
||||
String scenarioCode,
|
||||
@JsonProperty("source_message_id")
|
||||
String sourceMessageId,
|
||||
@JsonProperty("external_message_id")
|
||||
String externalMessageId,
|
||||
@JsonProperty("external_conversation_id")
|
||||
String externalConversationId,
|
||||
String subject
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Reservation 演示数据中的任务摘要。用于前端快速跳转任务详情和验证状态展示。
|
||||
*
|
||||
* @param scenarioCode 演示场景代码
|
||||
* @param taskId 任务 ID
|
||||
* @param orderId 任务所属订单 ID
|
||||
* @param taskType 系统主任务类型
|
||||
* @param taskSubtype 任务 subtype
|
||||
* @param taskStatus 任务状态
|
||||
*/
|
||||
public record ReservationDemoDataTaskResult(
|
||||
@JsonProperty("scenario_code")
|
||||
String scenarioCode,
|
||||
@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
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.control;
|
||||
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationDemoDataSeedRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationDemoDataSeedResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationDemoDataService;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Reservation 演示数据 Controller。该入口只在 dev/test 显式开启时注册,用于前端联调造数。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/system/reservation/demo-data")
|
||||
@ConditionalOnProperty(prefix = "reservation.demo-data", name = "enabled", havingValue = "true")
|
||||
public class ReservationDemoDataController {
|
||||
|
||||
private final ReservationDemoDataService demoDataService;
|
||||
|
||||
/**
|
||||
* 注入演示数据服务,Controller 不直接访问持久化层。
|
||||
*/
|
||||
public ReservationDemoDataController(ReservationDemoDataService demoDataService) {
|
||||
this.demoDataService = demoDataService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成一批前端演示数据;调用方必须携带受控访问口令。
|
||||
*/
|
||||
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<ReservationDemoDataSeedResult> seed(
|
||||
@RequestHeader(name = "X-TH-Hotel-Demo-Data-Key", required = false) String accessKey,
|
||||
@RequestBody(required = false) ReservationDemoDataSeedRequest request) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(demoDataService.seed(request, accessKey));
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
*/
|
||||
@RestControllerAdvice(assignableTypes = {
|
||||
ReservationTaskController.class,
|
||||
ReservationFrontendQueryController.class
|
||||
ReservationFrontendQueryController.class,
|
||||
ReservationDemoDataController.class
|
||||
})
|
||||
public class ReservationTaskControllerAdvice {
|
||||
|
||||
|
||||
@@ -397,8 +397,10 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
|
||||
@Override
|
||||
public ReservationPageSnapshot<ReservationAiQueryOrderSnapshot> queryFrontendOrders(
|
||||
ReservationOrderListQueryRequest request,
|
||||
List<Long> keywordSourceMessageIds,
|
||||
int pageNum,
|
||||
int pageSize) {
|
||||
List<Long> sourceMessageIds = keywordSourceMessageIds == null ? List.of() : keywordSourceMessageIds;
|
||||
Page<ReservationOrderEntity> page = orderMapper.selectPage(Page.of(pageNum, pageSize),
|
||||
Wrappers.<ReservationOrderEntity>lambdaQuery()
|
||||
.eq(ReservationOrderEntity::getHotelId, request.hotelId())
|
||||
@@ -426,7 +428,11 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
|
||||
.or()
|
||||
.like(ReservationOrderEntity::getOrderStatus, trim(request.keyword()))
|
||||
.or()
|
||||
.like(ReservationOrderEntity::getDisplayName, trim(request.keyword())))
|
||||
.like(ReservationOrderEntity::getDisplayName, trim(request.keyword()))
|
||||
.or(!sourceMessageIds.isEmpty())
|
||||
.in(!sourceMessageIds.isEmpty(),
|
||||
ReservationOrderEntity::getSourceMessageId,
|
||||
sourceMessageIds))
|
||||
.orderByDesc(ReservationOrderEntity::getUpdatedAt)
|
||||
.orderByDesc(ReservationOrderEntity::getId));
|
||||
return new ReservationPageSnapshot<>(
|
||||
|
||||
@@ -118,10 +118,11 @@ public interface ReservationAiWorkflowRepository {
|
||||
List<Long> sourceMessageIds);
|
||||
|
||||
/**
|
||||
* 分页查询前端订单列表,默认包含全部订单状态。
|
||||
* 分页查询前端订单列表,默认包含全部订单状态;keyword 可匹配订单字段或来源消息安全摘要命中的 ID。
|
||||
*/
|
||||
ReservationPageSnapshot<ReservationAiQueryOrderSnapshot> queryFrontendOrders(
|
||||
ReservationOrderListQueryRequest request,
|
||||
List<Long> keywordSourceMessageIds,
|
||||
int pageNum,
|
||||
int pageSize);
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service;
|
||||
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationDemoDataSeedRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationDemoDataSeedResult;
|
||||
|
||||
/**
|
||||
* Reservation 前端演示数据服务。只在 dev/test 受控启用,用已有业务服务生成真实落库数据。
|
||||
*/
|
||||
public interface ReservationDemoDataService {
|
||||
|
||||
/**
|
||||
* 生成一批前端演示数据,并返回订单、任务和来源消息关键 ID。
|
||||
*/
|
||||
ReservationDemoDataSeedResult seed(ReservationDemoDataSeedRequest request, String accessKey);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service.impl;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Reservation 演示数据 seed 配置。默认关闭,访问口令只能通过环境变量或本地配置注入。
|
||||
*/
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "reservation.demo-data")
|
||||
public class ReservationDemoDataProperties {
|
||||
|
||||
/** 是否启用演示数据 seed 接口。 */
|
||||
private boolean enabled = false;
|
||||
/** 演示数据 seed 访问口令。 */
|
||||
private String accessKey = "";
|
||||
/** 演示数据默认酒店上下文。 */
|
||||
private String defaultHotelId = "HOTEL-TEST";
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getAccessKey() {
|
||||
return accessKey;
|
||||
}
|
||||
|
||||
public void setAccessKey(String accessKey) {
|
||||
this.accessKey = accessKey;
|
||||
}
|
||||
|
||||
public String getDefaultHotelId() {
|
||||
return defaultHotelId;
|
||||
}
|
||||
|
||||
public void setDefaultHotelId(String defaultHotelId) {
|
||||
this.defaultHotelId = defaultHotelId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,758 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.platform.message.common.enums.SourceMessageMediaType;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageCommand;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageMedia;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResult;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageCaptureService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationDemoDataSeedRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationOperaSimulationRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationTaskPayloadMutationRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationDemoDataOrderResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationDemoDataSeedResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationDemoDataSourceMessageResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationDemoDataTaskResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOperaOperationResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskDetailResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskPayloadMutationResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.SuperAgentTaskResultItemResponse;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.SuperAgentTaskResultResponse;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationAiTaskIntakeService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationDemoDataService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationTaskWorkflowService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Reservation 演示数据服务实现。通过真实 SourceMessage、SuperAgent 入站、任务确认和 OPERA 模拟服务造数。
|
||||
*/
|
||||
@Service
|
||||
public class ReservationDemoDataServiceImpl implements ReservationDemoDataService {
|
||||
|
||||
private static final String DEMO_CLIENT_ID = "reservation-demo-data-seed";
|
||||
private static final String SOURCE_PROVIDER = "AGENTBUS";
|
||||
private static final String SOURCE_CHANNEL = "EMAIL";
|
||||
private static final String SOURCE_SCHEMA_VERSION = "agentbus-outlook-v1";
|
||||
private static final String SCENARIO_QUEUE = "QUEUE_BLOCKED";
|
||||
private static final String SCENARIO_QUEUE_REPLY = "QUEUE_CONVERSATION_REPLY";
|
||||
private static final String SCENARIO_QUEUE_FIRST = "QUEUE_FIRST";
|
||||
private static final String SCENARIO_COMPLETED = "OPERA_COMPLETED";
|
||||
private static final String SCENARIO_FAILED = "OPERA_FAILED";
|
||||
private static final String SCENARIO_FALLBACK = "FALLBACK_REVIEW";
|
||||
private static final String SCENARIO_INFORMATIONAL = "MESSAGE_NOTIFICATION";
|
||||
private static final DateTimeFormatter RUN_ID_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
||||
|
||||
private final ReservationDemoDataProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final SourceMessageCaptureService captureService;
|
||||
private final ReservationAiTaskIntakeService intakeService;
|
||||
private final ReservationTaskWorkflowService taskWorkflowService;
|
||||
|
||||
/**
|
||||
* 注入演示配置、JSON 工具和已有业务服务,避免 seed 功能直接写表。
|
||||
*/
|
||||
public ReservationDemoDataServiceImpl(
|
||||
ReservationDemoDataProperties properties,
|
||||
ObjectMapper objectMapper,
|
||||
SourceMessageCaptureService captureService,
|
||||
ReservationAiTaskIntakeService intakeService,
|
||||
ReservationTaskWorkflowService taskWorkflowService) {
|
||||
this.properties = properties;
|
||||
this.objectMapper = objectMapper;
|
||||
this.captureService = captureService;
|
||||
this.intakeService = intakeService;
|
||||
this.taskWorkflowService = taskWorkflowService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成前端 P0 页面可直接查看的一批演示数据。该方法默认事务回滚失败的半成品。
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public ReservationDemoDataSeedResult seed(ReservationDemoDataSeedRequest request, String accessKey) {
|
||||
ensureAuthorized(accessKey);
|
||||
String hotelId = normalizeHotelId(request == null ? null : request.hotelId());
|
||||
String demoRunId = buildDemoRunId(request == null ? null : request.runLabel());
|
||||
SeedAccumulator accumulator = new SeedAccumulator();
|
||||
|
||||
seedQueueBlockedScenario(hotelId, demoRunId, accumulator);
|
||||
seedCompletedOperaScenario(hotelId, demoRunId, accumulator);
|
||||
seedFailedOperaScenario(hotelId, demoRunId, accumulator);
|
||||
seedFallbackScenario(hotelId, demoRunId, accumulator);
|
||||
seedInformationalScenario(hotelId, demoRunId, accumulator);
|
||||
|
||||
return new ReservationDemoDataSeedResult(
|
||||
demoRunId,
|
||||
hotelId,
|
||||
List.copyOf(accumulator.sourceMessages()),
|
||||
List.copyOf(accumulator.orders()),
|
||||
List.copyOf(accumulator.tasks()),
|
||||
entrypoints(hotelId, demoRunId, accumulator),
|
||||
notes());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成同订单两条任务,第一条未完成,第二条用于前端验证只读阻塞状态。
|
||||
*/
|
||||
private void seedQueueBlockedScenario(String hotelId, String demoRunId, SeedAccumulator accumulator) {
|
||||
String conversationId = code("thread-demo-queue", demoRunId);
|
||||
String externalMessageId = code("demo-queue-main", demoRunId);
|
||||
CapturedSourceMessage mainSource = captureSourceMessage(
|
||||
hotelId,
|
||||
externalMessageId,
|
||||
conversationId,
|
||||
"Demo " + demoRunId + " queue booking request",
|
||||
"Please create group booking and then update the stay dates. Demo run " + demoRunId + ".",
|
||||
"<html><body><p>Please create group booking and then update the stay dates.</p>"
|
||||
+ "<img src=\"cid:demo-inline\" /></body></html>",
|
||||
Instant.now().minusSeconds(900),
|
||||
List.of(
|
||||
new CaptureSourceMessageMedia(
|
||||
SourceMessageMediaType.ATTACHMENT.code(),
|
||||
"demo-rooming-list-" + demoRunId + ".xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
2048L,
|
||||
"https://example.test/demo/" + demoRunId + "/rooming-list.xlsx",
|
||||
code("att-rooming", demoRunId)),
|
||||
new CaptureSourceMessageMedia(
|
||||
SourceMessageMediaType.INLINE_IMAGE.code(),
|
||||
"demo-inline-" + demoRunId + ".png",
|
||||
"image/png",
|
||||
1024L,
|
||||
"https://example.test/demo/" + demoRunId + "/inline.png",
|
||||
code("img-inline", demoRunId))));
|
||||
CapturedSourceMessage replySource = captureSourceMessage(
|
||||
hotelId,
|
||||
code("demo-queue-reply", demoRunId),
|
||||
conversationId,
|
||||
"Re: Demo " + demoRunId + " queue booking request",
|
||||
"Adding a note to the same conversation for demo run " + demoRunId + ".",
|
||||
"<html><body><p>Adding a note to the same conversation.</p></body></html>",
|
||||
Instant.now().minusSeconds(600),
|
||||
List.of());
|
||||
|
||||
String groupCode = code("GRP-DEMO-QUEUE", demoRunId).toUpperCase(Locale.ROOT);
|
||||
SuperAgentTaskResultResponse response = intake(
|
||||
twoTaskBody(hotelId, mainSource.externalMessageId(), demoRunId, groupCode),
|
||||
demoRunId,
|
||||
SCENARIO_QUEUE);
|
||||
SuperAgentTaskResultItemResponse first = response.items().get(0);
|
||||
SuperAgentTaskResultItemResponse blocked = response.items().get(1);
|
||||
|
||||
accumulator.sourceMessages().add(sourceMessageResult(SCENARIO_QUEUE, mainSource));
|
||||
accumulator.sourceMessages().add(sourceMessageResult(SCENARIO_QUEUE_REPLY, replySource));
|
||||
accumulator.orders().add(orderResult(SCENARIO_QUEUE, first, groupCode));
|
||||
accumulator.tasks().add(taskResult(SCENARIO_QUEUE_FIRST, first, "new_group_block"));
|
||||
accumulator.tasks().add(taskResult(SCENARIO_QUEUE, blocked, "update_stay_dates"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成已确认并两条 OPERA 模拟操作全部成功的任务,用于前端查看 completed 状态。
|
||||
*/
|
||||
private void seedCompletedOperaScenario(String hotelId, String demoRunId, SeedAccumulator accumulator) {
|
||||
CapturedSourceMessage source = captureSimpleSourceMessage(
|
||||
hotelId,
|
||||
SCENARIO_COMPLETED.toLowerCase(Locale.ROOT),
|
||||
demoRunId,
|
||||
"Demo " + demoRunId + " completed booking request");
|
||||
String confirmationNumber = code("CNF-DEMO-DONE", demoRunId).toUpperCase(Locale.ROOT);
|
||||
SuperAgentTaskResultResponse response = intake(
|
||||
newBookingBody(hotelId, source.externalMessageId(), demoRunId, confirmationNumber),
|
||||
demoRunId,
|
||||
SCENARIO_COMPLETED);
|
||||
SuperAgentTaskResultItemResponse item = response.items().get(0);
|
||||
ReservationTaskPayloadMutationResult confirmed = confirmTask(item.taskId());
|
||||
executeOperation(item.taskId(), confirmed.operaOperations().get(0).operationId(), true);
|
||||
executeOperation(item.taskId(), confirmed.operaOperations().get(1).operationId(), true);
|
||||
ReservationTaskDetailResult detail = taskWorkflowService.getTaskDetail(Long.valueOf(item.taskId()));
|
||||
|
||||
accumulator.sourceMessages().add(sourceMessageResult(SCENARIO_COMPLETED, source));
|
||||
accumulator.orders().add(orderResult(SCENARIO_COMPLETED, item, confirmationNumber));
|
||||
accumulator.tasks().add(taskResult(SCENARIO_COMPLETED, item, "new_fit_reservation", detail.taskStatus()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成已确认但第一条 OPERA 模拟失败的任务,用于前端查看失败和重试入口。
|
||||
*/
|
||||
private void seedFailedOperaScenario(String hotelId, String demoRunId, SeedAccumulator accumulator) {
|
||||
CapturedSourceMessage source = captureSimpleSourceMessage(
|
||||
hotelId,
|
||||
SCENARIO_FAILED.toLowerCase(Locale.ROOT),
|
||||
demoRunId,
|
||||
"Demo " + demoRunId + " failed operation booking request");
|
||||
String confirmationNumber = code("CNF-DEMO-FAIL", demoRunId).toUpperCase(Locale.ROOT);
|
||||
SuperAgentTaskResultResponse response = intake(
|
||||
newBookingBody(hotelId, source.externalMessageId(), demoRunId, confirmationNumber),
|
||||
demoRunId,
|
||||
SCENARIO_FAILED);
|
||||
SuperAgentTaskResultItemResponse item = response.items().get(0);
|
||||
ReservationTaskPayloadMutationResult confirmed = confirmTask(item.taskId());
|
||||
executeOperation(item.taskId(), confirmed.operaOperations().get(0).operationId(), false);
|
||||
ReservationTaskDetailResult detail = taskWorkflowService.getTaskDetail(Long.valueOf(item.taskId()));
|
||||
|
||||
accumulator.sourceMessages().add(sourceMessageResult(SCENARIO_FAILED, source));
|
||||
accumulator.orders().add(orderResult(SCENARIO_FAILED, item, confirmationNumber));
|
||||
accumulator.tasks().add(taskResult(SCENARIO_FAILED, item, "new_fit_reservation", detail.taskStatus()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Fallback 人工复核任务,用于前端查看异常任务转换前状态。
|
||||
*/
|
||||
private void seedFallbackScenario(String hotelId, String demoRunId, SeedAccumulator accumulator) {
|
||||
CapturedSourceMessage source = captureSimpleSourceMessage(
|
||||
hotelId,
|
||||
SCENARIO_FALLBACK.toLowerCase(Locale.ROOT),
|
||||
demoRunId,
|
||||
"Demo " + demoRunId + " fallback review request");
|
||||
SuperAgentTaskResultResponse response = intake(
|
||||
fallbackBody(hotelId, source.externalMessageId(), demoRunId),
|
||||
demoRunId,
|
||||
SCENARIO_FALLBACK);
|
||||
SuperAgentTaskResultItemResponse item = response.items().get(0);
|
||||
|
||||
accumulator.sourceMessages().add(sourceMessageResult(SCENARIO_FALLBACK, source));
|
||||
accumulator.orders().add(orderResult(SCENARIO_FALLBACK, item, temporaryOrderCode(source)));
|
||||
accumulator.tasks().add(taskResult(SCENARIO_FALLBACK, item, "manual_review"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Message Notification 任务,用于前端验证非执行队列消息提醒。
|
||||
*/
|
||||
private void seedInformationalScenario(String hotelId, String demoRunId, SeedAccumulator accumulator) {
|
||||
CapturedSourceMessage source = captureSimpleSourceMessage(
|
||||
hotelId,
|
||||
SCENARIO_INFORMATIONAL.toLowerCase(Locale.ROOT),
|
||||
demoRunId,
|
||||
"Demo " + demoRunId + " informational reply");
|
||||
SuperAgentTaskResultResponse response = intake(
|
||||
informationalBody(hotelId, source.externalMessageId(), demoRunId),
|
||||
demoRunId,
|
||||
SCENARIO_INFORMATIONAL);
|
||||
SuperAgentTaskResultItemResponse item = response.items().get(0);
|
||||
|
||||
accumulator.sourceMessages().add(sourceMessageResult(SCENARIO_INFORMATIONAL, source));
|
||||
accumulator.orders().add(orderResult(SCENARIO_INFORMATIONAL, item, temporaryOrderCode(source)));
|
||||
accumulator.tasks().add(taskResult(SCENARIO_INFORMATIONAL, item, "thank_you"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 seed 开关和访问口令,口令比较使用常量时间比较,避免明显时序差异。
|
||||
*/
|
||||
private void ensureAuthorized(String accessKey) {
|
||||
if (!properties.isEnabled()) {
|
||||
throw error(HttpStatus.SERVICE_UNAVAILABLE, "DEMO_DATA_DISABLED", "演示数据 seed 接口未启用。");
|
||||
}
|
||||
String configuredKey = trimToNull(properties.getAccessKey());
|
||||
if (configuredKey == null) {
|
||||
throw error(HttpStatus.SERVICE_UNAVAILABLE, "DEMO_DATA_ACCESS_KEY_NOT_CONFIGURED", "演示数据访问口令未配置。");
|
||||
}
|
||||
if (!accessKeyMatches(configuredKey, accessKey)) {
|
||||
throw error(HttpStatus.FORBIDDEN, "DEMO_DATA_ACCESS_DENIED", "演示数据访问被拒绝。");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成同订单两任务的 SuperAgent 入站请求体。
|
||||
*/
|
||||
private String twoTaskBody(String hotelId, String externalMessageId, String demoRunId, String groupCode) {
|
||||
return taskResultBody(hotelId, externalMessageId, List.of(
|
||||
taskItem(
|
||||
1,
|
||||
"S01",
|
||||
"S01_new_booking_skill",
|
||||
"normal_task",
|
||||
"New Booking",
|
||||
"new_group_block",
|
||||
"AI extracted demo new group booking " + demoRunId + ".",
|
||||
"Please create the group booking for " + demoRunId + ".",
|
||||
Map.of("group_code", groupCode),
|
||||
Map.of(
|
||||
"booking_object_type", "Group Block",
|
||||
"arrival_date", "2026-08-01",
|
||||
"departure_date", "2026-08-04",
|
||||
"room_quantity", 2,
|
||||
"room_type", "Deluxe Twin",
|
||||
"pms_room_type_code", "RM2",
|
||||
"rate_code_result", Map.of(
|
||||
"rate_code", "GRPA1-900",
|
||||
"settlement_price", 900),
|
||||
"fix_charge_required", false),
|
||||
Map.of("demo_run_id", demoRunId)),
|
||||
taskItem(
|
||||
2,
|
||||
"S02",
|
||||
"S02_update_booking_amendment_skill",
|
||||
"normal_task",
|
||||
"Update Booking",
|
||||
"update_stay_dates",
|
||||
"AI extracted demo update booking " + demoRunId + ".",
|
||||
"Please update the group booking for " + demoRunId + ".",
|
||||
Map.of("group_code", groupCode),
|
||||
Map.of(
|
||||
"update_subtypes", List.of("update_stay_dates"),
|
||||
"before_after", Map.of(
|
||||
"arrival_date", Map.of("before", "2026-08-01", "after", "2026-08-02"),
|
||||
"departure_date", Map.of("before", "2026-08-04", "after", "2026-08-05"))),
|
||||
Map.of("demo_run_id", demoRunId))));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 New Booking 单任务 SuperAgent 入站请求体。
|
||||
*/
|
||||
private String newBookingBody(
|
||||
String hotelId,
|
||||
String externalMessageId,
|
||||
String demoRunId,
|
||||
String confirmationNumber) {
|
||||
return taskResultBody(hotelId, externalMessageId, List.of(taskItem(
|
||||
1,
|
||||
"S01",
|
||||
"S01_new_booking_skill",
|
||||
"normal_task",
|
||||
"New Booking",
|
||||
"new_fit_reservation",
|
||||
"AI extracted demo FIT booking " + demoRunId + ".",
|
||||
"Please create the FIT booking for " + demoRunId + ".",
|
||||
Map.of("confirmation_number", confirmationNumber),
|
||||
Map.of(
|
||||
"booking_object_type", "FIT Reservation",
|
||||
"arrival_date", "2026-08-10",
|
||||
"departure_date", "2026-08-12",
|
||||
"room_quantity", 1,
|
||||
"room_type", "Deluxe King",
|
||||
"pms_room_type_code", "RM2",
|
||||
"rate_code_result", Map.of(
|
||||
"rate_code", "GRPA1-900",
|
||||
"settlement_price", 900),
|
||||
"fix_charge_required", false),
|
||||
Map.of("demo_run_id", demoRunId))));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Fallback 人工复核 SuperAgent 入站请求体。
|
||||
*/
|
||||
private String fallbackBody(String hotelId, String externalMessageId, String demoRunId) {
|
||||
Map<String, Object> item = taskItem(
|
||||
1,
|
||||
"S99",
|
||||
"S99_fallback_manual_review_skill",
|
||||
"manual_review",
|
||||
"Fallback",
|
||||
"manual_review",
|
||||
"AI could not classify demo message " + demoRunId + ".",
|
||||
"Please review ambiguous booking message for " + demoRunId + ".",
|
||||
Map.of("confirmation_number", code("CNF-DEMO-FB", demoRunId).toUpperCase(Locale.ROOT)),
|
||||
Map.of(
|
||||
"booking_object_type", "FIT Reservation",
|
||||
"arrival_date", "2026-09-01",
|
||||
"departure_date", "2026-09-03"),
|
||||
Map.of("demo_run_id", demoRunId));
|
||||
item.put("manual_review", Map.of(
|
||||
"reason_code", "ambiguous_task_type",
|
||||
"reason_text", "Demo fallback needs human classification.",
|
||||
"candidate_task_types", List.of("New Booking", "Update Booking", "Cancel Booking")));
|
||||
return taskResultBody(hotelId, externalMessageId, List.of(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Message Notification SuperAgent 入站请求体。
|
||||
*/
|
||||
private String informationalBody(String hotelId, String externalMessageId, String demoRunId) {
|
||||
Map<String, Object> item = taskItem(
|
||||
1,
|
||||
"S98",
|
||||
"S98_message_notification_skill",
|
||||
"informational_message",
|
||||
"Message Notification",
|
||||
"thank_you",
|
||||
"AI extracted demo informational message " + demoRunId + ".",
|
||||
"Guest replied thanks for " + demoRunId + ".",
|
||||
Map.of(),
|
||||
Map.of(),
|
||||
Map.of("demo_run_id", demoRunId));
|
||||
item.put("informational_message", Map.of(
|
||||
"category", "guest_reply",
|
||||
"summary", "Guest acknowledged the reservation update.",
|
||||
"severity", "low"));
|
||||
return taskResultBody(hotelId, externalMessageId, List.of(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装 SuperAgent 顶层请求体。
|
||||
*/
|
||||
private String taskResultBody(String hotelId, String externalMessageId, List<Map<String, Object>> items) {
|
||||
Map<String, Object> root = new LinkedHashMap<>();
|
||||
root.put("hotel_id", hotelId);
|
||||
root.put("source_message_id", externalMessageId);
|
||||
root.put("ai_task_results", items);
|
||||
root.put("extraction_warnings", List.of());
|
||||
return toJson(root);
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装单条 ai_task_results item,保留 SuperAgent 已稳定字段。
|
||||
*/
|
||||
private Map<String, Object> taskItem(
|
||||
int sourceEventIndex,
|
||||
String catalogCode,
|
||||
String skillId,
|
||||
String resultType,
|
||||
String taskType,
|
||||
String taskSubtype,
|
||||
String visibleReason,
|
||||
String relevantMessageExcerpt,
|
||||
Map<String, Object> caseKeys,
|
||||
Map<String, Object> extractedFields,
|
||||
Map<String, Object> contextUsed) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("source_event_index", sourceEventIndex);
|
||||
item.put("catalog_code", catalogCode);
|
||||
item.put("skill_id", skillId);
|
||||
item.put("result_type", resultType);
|
||||
item.put("task_type", taskType);
|
||||
item.put("task_subtype", taskSubtype);
|
||||
item.put("current_or_history", "current");
|
||||
item.put("visible_reason", visibleReason);
|
||||
item.put("relevant_message_excerpt", relevantMessageExcerpt);
|
||||
item.put("attachments", List.of());
|
||||
item.put("file_references", List.of());
|
||||
item.put("context_used", contextUsed);
|
||||
item.put("case_keys", caseKeys);
|
||||
item.put("extracted_fields", extractedFields);
|
||||
item.put("additional_operations", List.of());
|
||||
item.put("idempotency_key", null);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 捕获一封简单演示邮件。
|
||||
*/
|
||||
private CapturedSourceMessage captureSimpleSourceMessage(
|
||||
String hotelId,
|
||||
String scenarioCode,
|
||||
String demoRunId,
|
||||
String subject) {
|
||||
return captureSourceMessage(
|
||||
hotelId,
|
||||
code("demo-" + scenarioCode, demoRunId),
|
||||
code("thread-demo-" + scenarioCode, demoRunId),
|
||||
subject,
|
||||
"Demo mail for " + demoRunId + " scenario " + scenarioCode + ".",
|
||||
"<html><body><p>Demo mail for " + demoRunId + " scenario " + scenarioCode + ".</p></body></html>",
|
||||
Instant.now().minusSeconds(300),
|
||||
List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 SourceMessage 捕获服务落库演示邮件。
|
||||
*/
|
||||
private CapturedSourceMessage captureSourceMessage(
|
||||
String hotelId,
|
||||
String externalMessageId,
|
||||
String externalConversationId,
|
||||
String subject,
|
||||
String textBody,
|
||||
String htmlBody,
|
||||
Instant sourceSentAt,
|
||||
List<CaptureSourceMessageMedia> mediaItems) {
|
||||
SourceMessageCaptureResult result = captureService.capture(new CaptureSourceMessageCommand(
|
||||
hotelId,
|
||||
SOURCE_PROVIDER,
|
||||
SOURCE_CHANNEL,
|
||||
externalMessageId,
|
||||
externalConversationId,
|
||||
code("frame", externalMessageId),
|
||||
code("session-demo-data", hotelId),
|
||||
sourceSentAt,
|
||||
"demo.guest@example.test",
|
||||
subject,
|
||||
textBody,
|
||||
htmlBody,
|
||||
toJson(Map.of(
|
||||
"demo_run_id", externalMessageId,
|
||||
"source", Map.of("external_message_id", externalMessageId))),
|
||||
SOURCE_SCHEMA_VERSION,
|
||||
mediaItems));
|
||||
if (!"RECEIVED".equals(result.captureStatus())) {
|
||||
throw error(HttpStatus.CONFLICT, "DEMO_SOURCE_MESSAGE_CAPTURE_FAILED", "演示来源消息入库失败。");
|
||||
}
|
||||
return new CapturedSourceMessage(result.inboxId(), externalMessageId, externalConversationId, subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 AI 入站服务创建订单、任务和任务卡。
|
||||
*/
|
||||
private SuperAgentTaskResultResponse intake(String rawBody, String demoRunId, String scenarioCode) {
|
||||
SuperAgentTaskResultResponse response = intakeService.accept(
|
||||
rawBody,
|
||||
DEMO_CLIENT_ID,
|
||||
"demo-data-" + scenarioCode.toLowerCase(Locale.ROOT) + "-" + demoRunId);
|
||||
if (response.items().isEmpty()) {
|
||||
throw error(HttpStatus.CONFLICT, "DEMO_INTAKE_EMPTY", "演示 AI 入站未创建任务。");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认任务并生成固定两条 OPERA 模拟操作。
|
||||
*/
|
||||
private ReservationTaskPayloadMutationResult confirmTask(String taskId) {
|
||||
return taskWorkflowService.confirmTask(
|
||||
Long.valueOf(taskId),
|
||||
new ReservationTaskPayloadMutationRequest(Map.of()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行一条 OPERA 模拟操作,成功或失败由调用方指定。
|
||||
*/
|
||||
private ReservationOperaOperationResult executeOperation(String taskId, String operationId, boolean success) {
|
||||
return taskWorkflowService.executeOperaOperation(
|
||||
Long.valueOf(taskId),
|
||||
Long.valueOf(operationId),
|
||||
new ReservationOperaSimulationRequest(success, success ? null : "Demo OPERA failure for retry."));
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换来源消息为 seed 响应对象。
|
||||
*/
|
||||
private ReservationDemoDataSourceMessageResult sourceMessageResult(
|
||||
String scenarioCode,
|
||||
CapturedSourceMessage source) {
|
||||
return new ReservationDemoDataSourceMessageResult(
|
||||
scenarioCode,
|
||||
source.sourceMessageId().toString(),
|
||||
source.externalMessageId(),
|
||||
source.externalConversationId(),
|
||||
source.subject());
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换订单为 seed 响应对象。
|
||||
*/
|
||||
private ReservationDemoDataOrderResult orderResult(
|
||||
String scenarioCode,
|
||||
SuperAgentTaskResultItemResponse item,
|
||||
String displayOrderKey) {
|
||||
return new ReservationDemoDataOrderResult(
|
||||
scenarioCode,
|
||||
item.orderId(),
|
||||
item.orderStatus(),
|
||||
displayOrderKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换任务为 seed 响应对象,使用 AI 入站后的初始状态。
|
||||
*/
|
||||
private ReservationDemoDataTaskResult taskResult(
|
||||
String scenarioCode,
|
||||
SuperAgentTaskResultItemResponse item,
|
||||
String taskSubtype) {
|
||||
return taskResult(scenarioCode, item, taskSubtype, item.taskStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换任务为 seed 响应对象,允许覆盖任务后续状态。
|
||||
*/
|
||||
private ReservationDemoDataTaskResult taskResult(
|
||||
String scenarioCode,
|
||||
SuperAgentTaskResultItemResponse item,
|
||||
String taskSubtype,
|
||||
String taskStatus) {
|
||||
return new ReservationDemoDataTaskResult(
|
||||
scenarioCode,
|
||||
item.taskId(),
|
||||
item.orderId(),
|
||||
item.systemTaskType(),
|
||||
taskSubtype,
|
||||
taskStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成前端可直接调用的查询入口。
|
||||
*/
|
||||
private Map<String, String> entrypoints(String hotelId, String demoRunId, SeedAccumulator accumulator) {
|
||||
String encodedHotelId = urlEncode(hotelId);
|
||||
String encodedRunId = urlEncode(demoRunId);
|
||||
String queueOrderId = accumulator.orders().stream()
|
||||
.filter(order -> SCENARIO_QUEUE.equals(order.scenarioCode()))
|
||||
.map(ReservationDemoDataOrderResult::orderId)
|
||||
.findFirst()
|
||||
.orElse("");
|
||||
String queueSourceMessageId = accumulator.sourceMessages().stream()
|
||||
.filter(source -> SCENARIO_QUEUE.equals(source.scenarioCode()))
|
||||
.map(ReservationDemoDataSourceMessageResult::sourceMessageId)
|
||||
.findFirst()
|
||||
.orElse("");
|
||||
String failedTaskId = accumulator.tasks().stream()
|
||||
.filter(task -> SCENARIO_FAILED.equals(task.scenarioCode()))
|
||||
.map(ReservationDemoDataTaskResult::taskId)
|
||||
.findFirst()
|
||||
.orElse("");
|
||||
Map<String, String> entrypoints = new LinkedHashMap<>();
|
||||
entrypoints.put("task_list_url", "/api/reservation/tasks?hotel_id=" + encodedHotelId
|
||||
+ "&keyword=" + encodedRunId + "&page_num=1&page_size=20");
|
||||
entrypoints.put("order_list_url", "/api/reservation/orders?hotel_id=" + encodedHotelId
|
||||
+ "&keyword=" + encodedRunId + "&page_num=1&page_size=20");
|
||||
entrypoints.put("queue_order_detail_url", "/api/reservation/orders/" + queueOrderId
|
||||
+ "?hotel_id=" + encodedHotelId + "&include_tasks=true&include_source_summary=true");
|
||||
entrypoints.put("failed_task_detail_url", "/api/reservation/tasks/" + failedTaskId);
|
||||
entrypoints.put("source_conversation_url", "/api/source-messages/" + queueSourceMessageId + "/conversation");
|
||||
return entrypoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回演示数据使用注意事项。
|
||||
*/
|
||||
private List<String> notes() {
|
||||
return List.of(
|
||||
"该接口默认关闭,仅用于 dev/test 前端联调造数。",
|
||||
"演示数据通过真实业务服务写入,可用于任务列表、订单列表、订单详情、任务详情和邮件会话详情。",
|
||||
"OPERA 仍为模拟骨架,失败场景只用于验证前端重试入口。");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成安全短 run id,用于业务号、邮件主题和 keyword 查询。
|
||||
*/
|
||||
private String buildDemoRunId(String rawRunLabel) {
|
||||
String label = sanitizeCode(trimToNull(rawRunLabel) == null ? "demo" : rawRunLabel);
|
||||
String time = LocalDateTime.now(ZoneOffset.UTC).format(RUN_ID_TIME_FORMATTER);
|
||||
String suffix = UUID.randomUUID().toString().substring(0, 8);
|
||||
return label + "-" + time + "-" + suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化酒店上下文 ID。
|
||||
*/
|
||||
private String normalizeHotelId(String rawHotelId) {
|
||||
String hotelId = trimToNull(rawHotelId);
|
||||
return hotelId == null ? properties.getDefaultHotelId() : hotelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接稳定代码,保持长度适合外部消息 ID 和业务号字段。
|
||||
*/
|
||||
private String code(String prefix, String suffix) {
|
||||
return prefix + "-" + sanitizeCode(suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅保留适合业务号和消息 ID 的字符,并限制长度。
|
||||
*/
|
||||
private String sanitizeCode(String value) {
|
||||
String sanitized = value == null
|
||||
? "demo"
|
||||
: value.trim().replaceAll("[^A-Za-z0-9_-]", "-").replaceAll("-+", "-");
|
||||
if (sanitized.isBlank()) {
|
||||
return "demo";
|
||||
}
|
||||
return sanitized.length() > 48 ? sanitized.substring(0, 48) : sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 访问口令常量时间比较。
|
||||
*/
|
||||
private boolean accessKeyMatches(String configuredKey, String providedKey) {
|
||||
if (providedKey == null) {
|
||||
return false;
|
||||
}
|
||||
return MessageDigest.isEqual(
|
||||
sha256Bytes(configuredKey),
|
||||
sha256Bytes(providedKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算访问口令摘要,用固定长度字节数组做常量时间比较。
|
||||
*/
|
||||
private byte[] sha256Bytes(String value) {
|
||||
try {
|
||||
return MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8));
|
||||
} catch (NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException("当前 Java 运行时不支持 SHA-256", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成与订单创建逻辑一致的临时订单展示号。
|
||||
*/
|
||||
private String temporaryOrderCode(CapturedSourceMessage source) {
|
||||
return "TMP-SM-" + source.sourceMessageId() + "-1";
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 序列化工具,避免手工拼接 SuperAgent 请求体。
|
||||
*/
|
||||
private String toJson(Object value) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(value);
|
||||
} catch (JsonProcessingException exception) {
|
||||
throw new IllegalStateException("演示数据 JSON 构建失败。", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 参数编码。
|
||||
*/
|
||||
private String urlEncode(String value) {
|
||||
return URLEncoder.encode(value, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理空白字符串。
|
||||
*/
|
||||
private String trimToNull(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建受控业务异常。
|
||||
*/
|
||||
private ReservationTaskWorkflowException error(HttpStatus status, String errorCode, String message) {
|
||||
return new ReservationTaskWorkflowException(status, errorCode, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* seed 过程中的来源消息引用。
|
||||
*/
|
||||
private record CapturedSourceMessage(
|
||||
Long sourceMessageId,
|
||||
String externalMessageId,
|
||||
String externalConversationId,
|
||||
String subject
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* seed 过程中的响应累加器,保持返回顺序稳定。
|
||||
*/
|
||||
private record SeedAccumulator(
|
||||
List<ReservationDemoDataSourceMessageResult> sourceMessages,
|
||||
List<ReservationDemoDataOrderResult> orders,
|
||||
List<ReservationDemoDataTaskResult> tasks
|
||||
) {
|
||||
|
||||
private SeedAccumulator() {
|
||||
this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,8 +106,11 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
ReservationOrderListQueryRequest normalizedRequest = normalizeOrderListRequest(request);
|
||||
int pageNum = normalizePageNum(normalizedRequest.pageNum());
|
||||
int pageSize = normalizePageSize(normalizedRequest.pageSize());
|
||||
List<Long> keywordSourceMessageIds = findSourceMessageIdsByKeyword(
|
||||
normalizedRequest.hotelId(),
|
||||
normalizedRequest.keyword());
|
||||
ReservationPageSnapshot<ReservationAiQueryOrderSnapshot> page =
|
||||
workflowRepository.queryFrontendOrders(normalizedRequest, pageNum, pageSize);
|
||||
workflowRepository.queryFrontendOrders(normalizedRequest, keywordSourceMessageIds, pageNum, pageSize);
|
||||
List<Long> orderIds = page.items().stream().map(ReservationAiQueryOrderSnapshot::id).toList();
|
||||
List<ReservationAiQueryTaskSnapshot> orderTasks = workflowRepository.findAiQueryTasksByOrderIds(
|
||||
normalizedRequest.hotelId(),
|
||||
|
||||
Reference in New Issue
Block a user