实现酒店上下文单酒店收口
This commit is contained in:
@@ -4,7 +4,7 @@ import cn.nianxx.thhotel.integrations.ai.superagent.common.request.SuperAgentTas
|
||||
import cn.nianxx.thhotel.integrations.ai.superagent.service.SuperAgentTaskResultSecurityService;
|
||||
import cn.nianxx.thhotel.integrations.ai.superagent.service.impl.SuperAgentTaskResultException;
|
||||
import cn.nianxx.thhotel.integrations.ai.superagent.service.impl.SuperAgentTaskResultProperties;
|
||||
import cn.nianxx.thhotel.integrations.messaging.agentbus.adapter.AgentBusProperties;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.SuperAgentTaskResultResponse;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationAiTaskIntakeService;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -29,7 +29,7 @@ public class SuperAgentTaskResultController {
|
||||
|
||||
private final SuperAgentTaskResultSecurityService securityService;
|
||||
private final SuperAgentTaskResultProperties properties;
|
||||
private final AgentBusProperties agentBusProperties;
|
||||
private final HotelContextService hotelContextService;
|
||||
private final ReservationAiTaskIntakeService intakeService;
|
||||
|
||||
/**
|
||||
@@ -38,11 +38,11 @@ public class SuperAgentTaskResultController {
|
||||
public SuperAgentTaskResultController(
|
||||
SuperAgentTaskResultSecurityService securityService,
|
||||
SuperAgentTaskResultProperties properties,
|
||||
AgentBusProperties agentBusProperties,
|
||||
HotelContextService hotelContextService,
|
||||
ReservationAiTaskIntakeService intakeService) {
|
||||
this.securityService = securityService;
|
||||
this.properties = properties;
|
||||
this.agentBusProperties = agentBusProperties;
|
||||
this.hotelContextService = hotelContextService;
|
||||
this.intakeService = intakeService;
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ public class SuperAgentTaskResultController {
|
||||
requestBody,
|
||||
clientId,
|
||||
requestId,
|
||||
agentBusProperties.getCapture().getDefaultHotelId());
|
||||
hotelContextService.resolveSystemHotelId());
|
||||
HttpStatus status = response.idempotentReplay() ? HttpStatus.OK : HttpStatus.CREATED;
|
||||
return ResponseEntity.status(status).body(response);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package cn.nianxx.thhotel.integrations.ai.superagent.control;
|
||||
|
||||
import cn.nianxx.thhotel.integrations.ai.superagent.common.result.SuperAgentTaskResultErrorResponse;
|
||||
import cn.nianxx.thhotel.integrations.ai.superagent.service.impl.SuperAgentTaskResultException;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.impl.ReservationAiTaskIntakeException;
|
||||
import java.util.List;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -34,6 +35,16 @@ public class SuperAgentTaskResultControllerAdvice {
|
||||
.body(error(exception.getErrorCode(), exception.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统酒店缺失、多 ACTIVE 酒店或酒店访问受限等上下文异常。
|
||||
*/
|
||||
@ExceptionHandler(HotelContextException.class)
|
||||
public ResponseEntity<SuperAgentTaskResultErrorResponse> handleHotelContextException(
|
||||
HotelContextException exception) {
|
||||
return ResponseEntity.status(exception.getStatus())
|
||||
.body(error(exception.getErrorCode(), exception.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建统一错误响应,第一版不回显 request_id,避免异常路径暴露未经校验的外部输入。
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,8 @@ import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcp
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcpToolDefinition;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcpToolsListResult;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.service.SuperAgentMcpService;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationAiCaseContextQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationAiObjectDetailQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationMessageConversationQueryRequest;
|
||||
@@ -48,6 +50,7 @@ public class SuperAgentMcpServiceImpl implements SuperAgentMcpService {
|
||||
private final ReservationAiTaskIntakeService intakeService;
|
||||
private final SuperAgentMcpProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final HotelContextService hotelContextService;
|
||||
|
||||
/**
|
||||
* 注入已有业务服务和 JSON 工具,MCP 层不直接访问 Mapper 或数据库。
|
||||
@@ -56,11 +59,13 @@ public class SuperAgentMcpServiceImpl implements SuperAgentMcpService {
|
||||
ReservationAiQueryService aiQueryService,
|
||||
ReservationAiTaskIntakeService intakeService,
|
||||
SuperAgentMcpProperties properties,
|
||||
ObjectMapper objectMapper) {
|
||||
ObjectMapper objectMapper,
|
||||
HotelContextService hotelContextService) {
|
||||
this.aiQueryService = aiQueryService;
|
||||
this.intakeService = intakeService;
|
||||
this.properties = properties;
|
||||
this.objectMapper = objectMapper;
|
||||
this.hotelContextService = hotelContextService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,6 +159,13 @@ public class SuperAgentMcpServiceImpl implements SuperAgentMcpService {
|
||||
exception.getErrorCode(),
|
||||
exception.getMessage(),
|
||||
Map.of("http_status", exception.getStatus().value())));
|
||||
} catch (HotelContextException exception) {
|
||||
return SuperAgentMcpToolCallResult.error(
|
||||
"TH Hotel 酒店上下文解析失败:" + exception.getMessage(),
|
||||
errorStructuredContent(
|
||||
exception.getErrorCode(),
|
||||
exception.getMessage(),
|
||||
Map.of("http_status", exception.getStatus().value())));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return SuperAgentMcpToolCallResult.error(
|
||||
"MCP 工具参数不合法。",
|
||||
@@ -238,7 +250,11 @@ public class SuperAgentMcpServiceImpl implements SuperAgentMcpService {
|
||||
Map.of("tool", TOOL_SUBMIT_TASK_RESULTS)));
|
||||
}
|
||||
String rawBody = objectMapper.writeValueAsString(arguments);
|
||||
SuperAgentTaskResultResponse response = intakeService.accept(rawBody, MCP_CLIENT_ID, null);
|
||||
SuperAgentTaskResultResponse response = intakeService.accept(
|
||||
rawBody,
|
||||
MCP_CLIENT_ID,
|
||||
null,
|
||||
hotelContextService.resolveSystemHotelId());
|
||||
return SuperAgentMcpToolCallResult.success(TOOL_SUBMIT_TASK_RESULTS + " 调用成功。", response);
|
||||
}
|
||||
|
||||
@@ -311,37 +327,37 @@ public class SuperAgentMcpServiceImpl implements SuperAgentMcpService {
|
||||
|
||||
private Map<String, Object> caseContextSchema() {
|
||||
Map<String, Object> propertiesMap = new LinkedHashMap<>();
|
||||
propertiesMap.put("hotel_id", stringField("酒店上下文 ID"));
|
||||
propertiesMap.put("hotel_id", stringField("可选酒店上下文 ID;缺省由 TH Hotel 后端解析系统酒店"));
|
||||
propertiesMap.put("group_code", nullableStringField("Group / Allotment 查询 key"));
|
||||
propertiesMap.put("confirmation_number", nullableStringField("FIT confirmation number 查询 key"));
|
||||
propertiesMap.put("reservation_no", nullableStringField("OPERA reservation no"));
|
||||
propertiesMap.put("object_type_hint", nullableStringField("调用方推测的对象类型"));
|
||||
propertiesMap.put("target_key_source", nullableStringField("key 来源,例如 body_current"));
|
||||
propertiesMap.put("body_thread_used_only_as_evidence", Map.of("type", "boolean", "description", "历史线程 key 是否仅作为证据"));
|
||||
return objectSchema(propertiesMap, List.of("hotel_id"));
|
||||
return objectSchema(propertiesMap, List.of());
|
||||
}
|
||||
|
||||
private Map<String, Object> objectDetailSchema() {
|
||||
Map<String, Object> propertiesMap = new LinkedHashMap<>();
|
||||
propertiesMap.put("hotel_id", stringField("酒店上下文 ID"));
|
||||
propertiesMap.put("hotel_id", stringField("可选酒店上下文 ID;缺省由 TH Hotel 后端解析系统酒店"));
|
||||
propertiesMap.put("object_id", stringField("查询对象 ID,第一版支持 ORDER:{order_id}"));
|
||||
propertiesMap.put("object_type", nullableStringField("调用方对象类型提示"));
|
||||
return objectSchema(propertiesMap, List.of("hotel_id", "object_id"));
|
||||
return objectSchema(propertiesMap, List.of("object_id"));
|
||||
}
|
||||
|
||||
private Map<String, Object> conversationQuerySchema() {
|
||||
Map<String, Object> propertiesMap = new LinkedHashMap<>();
|
||||
propertiesMap.put("hotel_id", stringField("酒店上下文 ID"));
|
||||
propertiesMap.put("hotel_id", stringField("可选酒店上下文 ID;缺省由 TH Hotel 后端解析系统酒店"));
|
||||
propertiesMap.put("source_provider", nullableStringField("来源提供方,默认 AGENTBUS"));
|
||||
propertiesMap.put("source_channel", nullableStringField("来源渠道,默认 EMAIL"));
|
||||
propertiesMap.put("external_conversation_id", nullableStringField("外部邮件会话 ID"));
|
||||
propertiesMap.put("source_message_id", nullableStringField("外部来源消息 ID,可作为锚点反查会话"));
|
||||
return objectSchema(propertiesMap, List.of("hotel_id"));
|
||||
return objectSchema(propertiesMap, List.of());
|
||||
}
|
||||
|
||||
private Map<String, Object> submitTaskResultsSchema() {
|
||||
Map<String, Object> propertiesMap = new LinkedHashMap<>();
|
||||
propertiesMap.put("hotel_id", stringField("酒店上下文 ID"));
|
||||
propertiesMap.put("hotel_id", stringField("可选酒店上下文 ID;缺省由 TH Hotel 后端解析系统酒店"));
|
||||
propertiesMap.put("source_provider", nullableStringField("来源提供方,默认 AGENTBUS"));
|
||||
propertiesMap.put("source_channel", nullableStringField("来源渠道,默认 EMAIL"));
|
||||
propertiesMap.put("source_message_id", stringField("外部来源消息 ID,对应 AgentBus source.external_message_id"));
|
||||
@@ -353,7 +369,7 @@ public class SuperAgentMcpServiceImpl implements SuperAgentMcpService {
|
||||
"type", "array",
|
||||
"description", "AI 抽取警告",
|
||||
"items", Map.of("type", "object")));
|
||||
return objectSchema(propertiesMap, List.of("hotel_id", "source_message_id", "ai_task_results"));
|
||||
return objectSchema(propertiesMap, List.of("source_message_id", "ai_task_results"));
|
||||
}
|
||||
|
||||
private Map<String, Object> objectSchema(Map<String, Object> propertiesMap, List<String> required) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package cn.nianxx.thhotel.integrations.messaging.agentbus.adapter;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageCommand;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResult;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageCaptureService;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -26,6 +27,7 @@ public class AgentBusFrameProcessor {
|
||||
private final SourceMessageCaptureService captureService;
|
||||
private final AgentBusConnectionStatus status;
|
||||
private final AgentBusProperties properties;
|
||||
private final HotelContextService hotelContextService;
|
||||
|
||||
/**
|
||||
* 注入 AgentBus frame 处理依赖。外部协议转换与平台捕获服务通过稳定命令隔离。
|
||||
@@ -35,12 +37,14 @@ public class AgentBusFrameProcessor {
|
||||
AgentBusSourceMessageAdapter sourceMessageAdapter,
|
||||
SourceMessageCaptureService captureService,
|
||||
AgentBusConnectionStatus status,
|
||||
AgentBusProperties properties) {
|
||||
AgentBusProperties properties,
|
||||
HotelContextService hotelContextService) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.sourceMessageAdapter = sourceMessageAdapter;
|
||||
this.captureService = captureService;
|
||||
this.status = status;
|
||||
this.properties = properties;
|
||||
this.hotelContextService = hotelContextService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,8 +73,9 @@ public class AgentBusFrameProcessor {
|
||||
return AgentBusFrameProcessResult.ignored();
|
||||
}
|
||||
try {
|
||||
String hotelId = hotelContextService.resolveSystemHotelId();
|
||||
CaptureSourceMessageCommand command = sourceMessageAdapter.toCaptureCommand(
|
||||
properties.getCapture().getDefaultHotelId(),
|
||||
hotelId,
|
||||
frame);
|
||||
SourceMessageCaptureResult result = captureService.capture(command);
|
||||
status.markFrameCaptured();
|
||||
|
||||
@@ -123,8 +123,6 @@ public class AgentBusProperties {
|
||||
|
||||
/** 是否把业务 frame 写入 SourceMessage Inbox。 */
|
||||
private boolean enabled = true;
|
||||
/** AgentBus 未提供酒店上下文时使用的默认酒店 ID。 */
|
||||
private String defaultHotelId = "HOTEL-TEST";
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
@@ -133,13 +131,5 @@ public class AgentBusProperties {
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getDefaultHotelId() {
|
||||
return defaultHotelId;
|
||||
}
|
||||
|
||||
public void setDefaultHotelId(String defaultHotelId) {
|
||||
this.defaultHotelId = defaultHotelId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public class DebugEmlSuperAgentController {
|
||||
public ResponseEntity<DebugEmlSuperAgentRunResult> upload(
|
||||
@RequestHeader(name = "X-TH-Hotel-Debug-Upload-Key", required = false) String accessKey,
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("hotel_id") String hotelId,
|
||||
@RequestParam(name = "hotel_id", required = false) String hotelId,
|
||||
@RequestParam(name = "run_label", required = false) String runLabel) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(runService.uploadAndRun(accessKey, file, hotelId, runLabel));
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ import cn.nianxx.thhotel.platform.debug.common.result.DebugEmlSuperAgentRunResul
|
||||
import cn.nianxx.thhotel.platform.debug.common.result.DebugEmlUploadedMediaResult;
|
||||
import cn.nianxx.thhotel.platform.debug.repository.DebugEmlSuperAgentRunRepository;
|
||||
import cn.nianxx.thhotel.platform.debug.service.DebugEmlSuperAgentRunService;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.ParsedEmlMediaItem;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.ParsedEmlMessage;
|
||||
import cn.nianxx.thhotel.platform.message.common.enums.SourceMessageMediaType;
|
||||
@@ -76,6 +78,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
private final SuperAgentOpenApiClient superAgentOpenApiClient;
|
||||
private final DebugEmlSuperAgentRunRepository runRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final HotelContextService hotelContextService;
|
||||
|
||||
/**
|
||||
* 注入 Debug EML 所需的内部服务和外部端口。
|
||||
@@ -89,7 +92,8 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
SourceMessageHtmlSanitizerService htmlSanitizerService,
|
||||
SuperAgentOpenApiClient superAgentOpenApiClient,
|
||||
DebugEmlSuperAgentRunRepository runRepository,
|
||||
ObjectMapper objectMapper) {
|
||||
ObjectMapper objectMapper,
|
||||
HotelContextService hotelContextService) {
|
||||
this.properties = properties;
|
||||
this.ossProperties = ossProperties;
|
||||
this.parseService = parseService;
|
||||
@@ -99,6 +103,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
this.superAgentOpenApiClient = superAgentOpenApiClient;
|
||||
this.runRepository = runRepository;
|
||||
this.objectMapper = objectMapper;
|
||||
this.hotelContextService = hotelContextService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,7 +116,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
String hotelId,
|
||||
String runLabel) {
|
||||
validateAccessKey(accessKey);
|
||||
String normalizedHotelId = requireText(hotelId, "hotel_id");
|
||||
String normalizedHotelId = normalizeHotelId(hotelId);
|
||||
validateFile(file);
|
||||
byte[] emlBytes = readFileBytes(file);
|
||||
String safeFileName = safeFileName(file.getOriginalFilename(), "debug-email.eml");
|
||||
@@ -161,6 +166,21 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Debug 上传酒店上下文。第一版可不传 hotel_id,由单酒店系统上下文兜底。
|
||||
*/
|
||||
private String normalizeHotelId(String hotelId) {
|
||||
try {
|
||||
return hotelContextService.resolveCurrentHotelId(hotelId);
|
||||
} catch (HotelContextException exception) {
|
||||
throw new DebugEmlSuperAgentException(
|
||||
exception.getStatus(),
|
||||
exception.getErrorCode(),
|
||||
exception.getMessage(),
|
||||
exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 SuperAgent 失败安全摘要,只记录错误类型,不保存响应 body、API Key、正文或附件 URL。
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.nianxx.thhotel.platform.hotel.service;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
/**
|
||||
* 酒店上下文解析受控异常。用于把 hotel_id 来源不明确、系统酒店未配置和用户无权限转换为安全错误。
|
||||
*/
|
||||
public class HotelContextException extends RuntimeException {
|
||||
|
||||
private final HttpStatus status;
|
||||
private final String errorCode;
|
||||
|
||||
public HotelContextException(HttpStatus status, String errorCode, String message) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public HttpStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package cn.nianxx.thhotel.platform.hotel.service;
|
||||
|
||||
/**
|
||||
* 酒店上下文解析服务。统一收口机器入口和用户入口的运行时 hotel_id 来源。
|
||||
*/
|
||||
public interface HotelContextService {
|
||||
|
||||
/**
|
||||
* 解析系统酒店 ID。单酒店阶段要求平台酒店表中必须且只能有一家 ACTIVE 酒店。
|
||||
*/
|
||||
String resolveSystemHotelId();
|
||||
|
||||
/**
|
||||
* 解析当前用户请求酒店 ID。已登录时校验用户授权;未登录兼容入口回退系统酒店。
|
||||
*/
|
||||
String resolveCurrentHotelId(String requestedHotelId);
|
||||
|
||||
/**
|
||||
* 校验当前用户是否可访问指定酒店。未登录时按系统酒店兼容校验。
|
||||
*/
|
||||
String requireAccessibleHotel(String hotelId);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package cn.nianxx.thhotel.platform.hotel.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.platform.hotel.domain.PlatformHotelEntity;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
|
||||
import cn.nianxx.thhotel.platform.hotel.repository.PlatformHotelRepository;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.platform.security.common.dto.AuthenticatedUserContext;
|
||||
import cn.nianxx.thhotel.platform.security.service.CurrentUserContextService;
|
||||
import java.util.List;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 酒店上下文解析服务实现。单酒店阶段以平台酒店表唯一 ACTIVE 酒店作为系统酒店事实来源。
|
||||
*/
|
||||
@Service
|
||||
public class HotelContextServiceImpl implements HotelContextService {
|
||||
|
||||
private final PlatformHotelRepository hotelRepository;
|
||||
private final CurrentUserContextService currentUserContextService;
|
||||
|
||||
/**
|
||||
* 注入平台酒店仓储和当前用户上下文服务,避免业务模块各自读取配置兜底酒店。
|
||||
*/
|
||||
public HotelContextServiceImpl(
|
||||
PlatformHotelRepository hotelRepository,
|
||||
CurrentUserContextService currentUserContextService) {
|
||||
this.hotelRepository = hotelRepository;
|
||||
this.currentUserContextService = currentUserContextService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析系统酒店。单酒店阶段严格要求平台酒店表只有一家 ACTIVE 酒店。
|
||||
*/
|
||||
@Override
|
||||
public String resolveSystemHotelId() {
|
||||
List<PlatformHotelEntity> activeHotels = hotelRepository.listActiveHotels();
|
||||
if (activeHotels == null || activeHotels.isEmpty()) {
|
||||
throw new HotelContextException(
|
||||
HttpStatus.CONFLICT,
|
||||
"SYSTEM_HOTEL_NOT_CONFIGURED",
|
||||
"系统酒店未配置,请先在平台酒店表配置一家 ACTIVE 酒店。");
|
||||
}
|
||||
if (activeHotels.size() > 1) {
|
||||
throw new HotelContextException(
|
||||
HttpStatus.CONFLICT,
|
||||
"SYSTEM_HOTEL_AMBIGUOUS",
|
||||
"单酒店阶段只允许平台酒店表存在一家 ACTIVE 酒店。");
|
||||
}
|
||||
String hotelId = trimToNull(activeHotels.get(0).getHotelId());
|
||||
if (hotelId == null) {
|
||||
throw new HotelContextException(
|
||||
HttpStatus.CONFLICT,
|
||||
"SYSTEM_HOTEL_NOT_CONFIGURED",
|
||||
"系统酒店 ID 为空,请检查平台酒店表。");
|
||||
}
|
||||
return hotelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析当前请求酒店。登录用户按授权酒店校验;未登录兼容入口按系统酒店解析并严格比对显式请求值。
|
||||
*/
|
||||
@Override
|
||||
public String resolveCurrentHotelId(String requestedHotelId) {
|
||||
String normalizedRequestedHotelId = trimToNull(requestedHotelId);
|
||||
return currentUserContextService.currentUser()
|
||||
.map(context -> resolveUserHotelId(context, normalizedRequestedHotelId))
|
||||
.orElseGet(() -> resolveAnonymousHotelId(normalizedRequestedHotelId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 要求当前请求可访问指定酒店。空入参走当前酒店解析,非空入参走授权校验。
|
||||
*/
|
||||
@Override
|
||||
public String requireAccessibleHotel(String hotelId) {
|
||||
return resolveCurrentHotelId(hotelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 已登录用户酒店解析。显式请求酒店必须在可访问列表内,缺省时使用默认酒店。
|
||||
*/
|
||||
private String resolveUserHotelId(AuthenticatedUserContext context, String requestedHotelId) {
|
||||
List<String> accessibleHotelIds = context.accessibleHotelIds() == null
|
||||
? List.of()
|
||||
: context.accessibleHotelIds();
|
||||
if (requestedHotelId != null) {
|
||||
if (accessibleHotelIds.contains(requestedHotelId)) {
|
||||
return requestedHotelId;
|
||||
}
|
||||
throw accessDenied();
|
||||
}
|
||||
String defaultHotelId = trimToNull(context.defaultHotelId());
|
||||
if (defaultHotelId != null && accessibleHotelIds.contains(defaultHotelId)) {
|
||||
return defaultHotelId;
|
||||
}
|
||||
return accessibleHotelIds.stream()
|
||||
.filter(this::hasText)
|
||||
.findFirst()
|
||||
.orElseThrow(this::accessDenied);
|
||||
}
|
||||
|
||||
/**
|
||||
* 未登录兼容入口酒店解析。请求未传酒店时使用系统酒店,显式酒店必须等于系统酒店。
|
||||
*/
|
||||
private String resolveAnonymousHotelId(String requestedHotelId) {
|
||||
String systemHotelId = resolveSystemHotelId();
|
||||
if (requestedHotelId == null || systemHotelId.equals(requestedHotelId)) {
|
||||
return systemHotelId;
|
||||
}
|
||||
throw accessDenied();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造酒店访问拒绝异常,不回显用户或酒店授权细节。
|
||||
*/
|
||||
private HotelContextException accessDenied() {
|
||||
return new HotelContextException(
|
||||
HttpStatus.FORBIDDEN,
|
||||
"HOTEL_ACCESS_DENIED",
|
||||
"当前用户无权访问该酒店。");
|
||||
}
|
||||
|
||||
private boolean hasText(String value) {
|
||||
return trimToNull(value) != null;
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.nianxx.thhotel.platform.message.control;
|
||||
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
|
||||
import java.util.Map;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* SourceMessage 查询接口异常处理。只返回安全错误码和摘要信息,不暴露内部堆栈。
|
||||
*/
|
||||
@RestControllerAdvice(assignableTypes = SourceMessageController.class)
|
||||
public class SourceMessageControllerAdvice {
|
||||
|
||||
/**
|
||||
* 处理酒店上下文解析失败,例如单酒店未配置、多 ACTIVE 酒店或当前用户无权限访问。
|
||||
*/
|
||||
@ExceptionHandler(HotelContextException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleHotelContextException(HotelContextException exception) {
|
||||
return ResponseEntity.status(exception.getStatus())
|
||||
.body(Map.of(
|
||||
"error_code", exception.getErrorCode(),
|
||||
"message", exception.getMessage()));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package cn.nianxx.thhotel.platform.message.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.platform.common.time.UtcTimeFormatter;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageInboxSnapshot;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageSummaryResponse;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.SourceMessageQueryRequest;
|
||||
@@ -24,12 +25,16 @@ public class SourceMessageQueryServiceImpl implements SourceMessageQueryService
|
||||
private static final int MAX_SAFE_KEYWORD_MATCHES = 500;
|
||||
|
||||
private final SourceMessageInboxRepository inboxRepository;
|
||||
private final HotelContextService hotelContextService;
|
||||
|
||||
/**
|
||||
* 注入 SourceMessage 持久化边界,查询服务不直接依赖 Mapper。
|
||||
* 注入 SourceMessage 持久化边界和酒店上下文服务,查询服务不直接依赖 Mapper。
|
||||
*/
|
||||
public SourceMessageQueryServiceImpl(SourceMessageInboxRepository inboxRepository) {
|
||||
public SourceMessageQueryServiceImpl(
|
||||
SourceMessageInboxRepository inboxRepository,
|
||||
HotelContextService hotelContextService) {
|
||||
this.inboxRepository = inboxRepository;
|
||||
this.hotelContextService = hotelContextService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,9 +42,10 @@ public class SourceMessageQueryServiceImpl implements SourceMessageQueryService
|
||||
*/
|
||||
@Override
|
||||
public SourceMessagePageResult<SourceMessageSummaryResponse> query(SourceMessageQueryRequest request) {
|
||||
int pageNum = normalizePageNum(request.pageNum());
|
||||
int pageSize = normalizePageSize(request.pageSize());
|
||||
SourceMessagePageResult<SourceMessageInboxSnapshot> page = inboxRepository.query(request, pageNum, pageSize);
|
||||
SourceMessageQueryRequest normalizedRequest = normalizeRequest(request);
|
||||
int pageNum = normalizePageNum(normalizedRequest.pageNum());
|
||||
int pageSize = normalizePageSize(normalizedRequest.pageSize());
|
||||
SourceMessagePageResult<SourceMessageInboxSnapshot> page = inboxRepository.query(normalizedRequest, pageNum, pageSize);
|
||||
List<SourceMessageSummaryResponse> items = page.items().stream().map(this::toSummary).toList();
|
||||
return new SourceMessagePageResult<>(items, page.total(), pageNum, pageSize);
|
||||
}
|
||||
@@ -49,7 +55,10 @@ public class SourceMessageQueryServiceImpl implements SourceMessageQueryService
|
||||
*/
|
||||
@Override
|
||||
public List<Long> findIdsBySafeKeyword(String hotelId, String keyword) {
|
||||
return inboxRepository.findIdsBySafeKeyword(hotelId, keyword, MAX_SAFE_KEYWORD_MATCHES);
|
||||
return inboxRepository.findIdsBySafeKeyword(
|
||||
hotelContextService.resolveCurrentHotelId(hotelId),
|
||||
keyword,
|
||||
MAX_SAFE_KEYWORD_MATCHES);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,7 +87,31 @@ public class SourceMessageQueryServiceImpl implements SourceMessageQueryService
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Long> countByExternalConversationIds(String hotelId, List<String> externalConversationIds) {
|
||||
return inboxRepository.countByExternalConversationIds(hotelId, externalConversationIds);
|
||||
return inboxRepository.countByExternalConversationIds(
|
||||
hotelContextService.resolveCurrentHotelId(hotelId),
|
||||
externalConversationIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化查询条件。hotel_id 可选,缺省时由当前用户或单酒店系统上下文解析。
|
||||
*/
|
||||
private SourceMessageQueryRequest normalizeRequest(SourceMessageQueryRequest request) {
|
||||
if (request == null) {
|
||||
return new SourceMessageQueryRequest(
|
||||
hotelContextService.resolveCurrentHotelId(null),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
return new SourceMessageQueryRequest(
|
||||
hotelContextService.resolveCurrentHotelId(request.hotelId()),
|
||||
request.externalMessageId(),
|
||||
request.externalConversationId(),
|
||||
request.captureStatus(),
|
||||
request.pageNum(),
|
||||
request.pageSize());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,7 @@ public interface ReservationAiTaskIntakeService {
|
||||
SuperAgentTaskResultResponse accept(String rawBody, String clientId, String requestId);
|
||||
|
||||
/**
|
||||
* 接收已通过鉴权的 SuperAgent 原始请求体;defaultHotelId 仅用于 S000/S999 文本结果反查 SourceMessage。
|
||||
* 接收已通过鉴权的 SuperAgent 原始请求体;defaultHotelId 用于外部 source_message_id 缺省 hotel_id 时反查 SourceMessage。
|
||||
*/
|
||||
SuperAgentTaskResultResponse accept(String rawBody, String clientId, String requestId, String defaultHotelId);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.platform.common.time.UtcTimeFormatter;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
|
||||
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;
|
||||
@@ -65,6 +67,7 @@ public class ReservationAiQueryServiceImpl implements ReservationAiQueryService
|
||||
private final ReservationAiWorkflowRepository repository;
|
||||
private final SourceMessageInboxRepository sourceMessageInboxRepository;
|
||||
private final SourceMessageHtmlSanitizerService htmlSanitizerService;
|
||||
private final HotelContextService hotelContextService;
|
||||
|
||||
/**
|
||||
* 注入 Reservation 工作流持久化边界、SourceMessage 持久化边界和 HTML 清洗服务。
|
||||
@@ -72,10 +75,12 @@ public class ReservationAiQueryServiceImpl implements ReservationAiQueryService
|
||||
public ReservationAiQueryServiceImpl(
|
||||
ReservationAiWorkflowRepository repository,
|
||||
SourceMessageInboxRepository sourceMessageInboxRepository,
|
||||
SourceMessageHtmlSanitizerService htmlSanitizerService) {
|
||||
SourceMessageHtmlSanitizerService htmlSanitizerService,
|
||||
HotelContextService hotelContextService) {
|
||||
this.repository = repository;
|
||||
this.sourceMessageInboxRepository = sourceMessageInboxRepository;
|
||||
this.htmlSanitizerService = htmlSanitizerService;
|
||||
this.hotelContextService = hotelContextService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +89,7 @@ public class ReservationAiQueryServiceImpl implements ReservationAiQueryService
|
||||
@Override
|
||||
public ReservationAiCaseContextResult queryCaseContext(ReservationAiCaseContextQueryRequest request) {
|
||||
validateCaseContextRequest(request);
|
||||
String hotelId = trimToNull(request.hotelId());
|
||||
String hotelId = resolveHotelId(request.hotelId());
|
||||
String groupCode = trimToNull(request.groupCode());
|
||||
String confirmationNumber = trimToNull(request.confirmationNumber());
|
||||
if (groupCode == null && confirmationNumber == null) {
|
||||
@@ -149,7 +154,7 @@ public class ReservationAiQueryServiceImpl implements ReservationAiQueryService
|
||||
@Override
|
||||
public ReservationAiObjectDetailResult queryObjectDetail(ReservationAiObjectDetailQueryRequest request) {
|
||||
validateObjectDetailRequest(request);
|
||||
String hotelId = trimToNull(request.hotelId());
|
||||
String hotelId = resolveHotelId(request.hotelId());
|
||||
Long orderId = parseOrderObjectId(request.objectId());
|
||||
ReservationAiQueryOrderSnapshot order = repository.findAiQueryOrderById(hotelId, orderId)
|
||||
.orElseThrow(() -> new ReservationAiQueryException(
|
||||
@@ -200,8 +205,8 @@ public class ReservationAiQueryServiceImpl implements ReservationAiQueryService
|
||||
public ReservationMessageConversationTasksResult queryMessageConversationTasks(
|
||||
ReservationMessageConversationQueryRequest request) {
|
||||
validateConversationRequest(request);
|
||||
String hotelId = trimToNull(request.hotelId());
|
||||
List<SourceMessageInboxSnapshot> messages = findConversationMessages(request);
|
||||
String hotelId = resolveHotelId(request.hotelId());
|
||||
List<SourceMessageInboxSnapshot> messages = findConversationMessages(request, hotelId);
|
||||
Map<Long, SourceMessageInboxSnapshot> messageIndex = indexMessages(messages);
|
||||
List<ReservationMessageConversationTasksResult.TaskRecord> tasks = repository
|
||||
.findAiQueryTasksBySourceMessageIds(hotelId, messages.stream().map(SourceMessageInboxSnapshot::id).toList())
|
||||
@@ -232,8 +237,8 @@ public class ReservationAiQueryServiceImpl implements ReservationAiQueryService
|
||||
public ReservationMessageConversationMessagesResult queryMessageConversationMessages(
|
||||
ReservationMessageConversationQueryRequest request) {
|
||||
validateConversationRequest(request);
|
||||
String hotelId = trimToNull(request.hotelId());
|
||||
List<SourceMessageInboxSnapshot> messages = findConversationMessages(request);
|
||||
String hotelId = resolveHotelId(request.hotelId());
|
||||
List<SourceMessageInboxSnapshot> messages = findConversationMessages(request, hotelId);
|
||||
List<ReservationMessageConversationMessagesResult.MessageRecord> resultMessages = messages.stream()
|
||||
.map(this::toConversationMessage)
|
||||
.toList();
|
||||
@@ -247,8 +252,9 @@ public class ReservationAiQueryServiceImpl implements ReservationAiQueryService
|
||||
/**
|
||||
* 根据会话 ID 或外部 SourceMessage ID 找到同一邮件链的全部消息。
|
||||
*/
|
||||
private List<SourceMessageInboxSnapshot> findConversationMessages(ReservationMessageConversationQueryRequest request) {
|
||||
String hotelId = trimToNull(request.hotelId());
|
||||
private List<SourceMessageInboxSnapshot> findConversationMessages(
|
||||
ReservationMessageConversationQueryRequest request,
|
||||
String hotelId) {
|
||||
String externalConversationId = trimToNull(request.externalConversationId());
|
||||
if (externalConversationId != null) {
|
||||
List<SourceMessageInboxSnapshot> messages = sourceMessageInboxRepository.findByExternalConversationId(
|
||||
@@ -547,7 +553,6 @@ public class ReservationAiQueryServiceImpl implements ReservationAiQueryService
|
||||
if (request == null) {
|
||||
throw badRequest("MISSING_REQUEST_BODY", "请求体不能为空");
|
||||
}
|
||||
requireText(request.hotelId(), "HOTEL_ID_REQUIRED", "hotel_id 不能为空");
|
||||
if (trimToNull(request.groupCode()) == null
|
||||
&& trimToNull(request.confirmationNumber()) == null
|
||||
&& trimToNull(request.reservationNo()) == null) {
|
||||
@@ -559,7 +564,6 @@ public class ReservationAiQueryServiceImpl implements ReservationAiQueryService
|
||||
if (request == null) {
|
||||
throw badRequest("MISSING_REQUEST_BODY", "请求体不能为空");
|
||||
}
|
||||
requireText(request.hotelId(), "HOTEL_ID_REQUIRED", "hotel_id 不能为空");
|
||||
requireText(request.objectId(), "OBJECT_ID_REQUIRED", "object_id 不能为空");
|
||||
}
|
||||
|
||||
@@ -567,7 +571,6 @@ public class ReservationAiQueryServiceImpl implements ReservationAiQueryService
|
||||
if (request == null) {
|
||||
throw badRequest("MISSING_REQUEST_BODY", "请求体不能为空");
|
||||
}
|
||||
requireText(request.hotelId(), "HOTEL_ID_REQUIRED", "hotel_id 不能为空");
|
||||
if (trimToNull(request.externalConversationId()) == null && trimToNull(request.sourceMessageId()) == null) {
|
||||
throw badRequest(
|
||||
"MESSAGE_CONVERSATION_QUERY_KEY_REQUIRED",
|
||||
@@ -583,6 +586,20 @@ public class ReservationAiQueryServiceImpl implements ReservationAiQueryService
|
||||
return new ReservationAiQueryException(HttpStatus.NOT_FOUND, code, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 AI 查询酒店上下文。SuperAgent 可不传 hotel_id;如果传入,单酒店阶段必须与系统酒店一致。
|
||||
*/
|
||||
private String resolveHotelId(String requestedHotelId) {
|
||||
try {
|
||||
return hotelContextService.requireAccessibleHotel(requestedHotelId);
|
||||
} catch (HotelContextException exception) {
|
||||
throw new ReservationAiQueryException(
|
||||
exception.getStatus(),
|
||||
exception.getErrorCode(),
|
||||
exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void requireText(String value, String code, String message) {
|
||||
if (trimToNull(value) == null) {
|
||||
throw badRequest(code, message);
|
||||
|
||||
@@ -101,7 +101,7 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
requestBody);
|
||||
}
|
||||
JsonNode root = parseJson(rawBody);
|
||||
ResolvedSourceMessage resolvedSourceMessage = resolveSourceMessage(root);
|
||||
ResolvedSourceMessage resolvedSourceMessage = resolveSourceMessage(root, defaultHotelId);
|
||||
SourceMessageInboxSnapshot sourceMessage = resolvedSourceMessage.snapshot();
|
||||
Long sourceMessageId = sourceMessage.id();
|
||||
String responseSourceMessageId = resolvedSourceMessage.responseSourceMessageId();
|
||||
@@ -743,16 +743,24 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
/**
|
||||
* 解析 SuperAgent 来源消息引用。正式契约使用外部邮件 ID,旧本地夹具仍兼容内部 SourceMessage ID。
|
||||
*/
|
||||
private ResolvedSourceMessage resolveSourceMessage(JsonNode root) {
|
||||
private ResolvedSourceMessage resolveSourceMessage(JsonNode root, String defaultHotelId) {
|
||||
String sourceMessageReference = trimToNull(textAt(root, "source_message_id"));
|
||||
if (sourceMessageReference == null) {
|
||||
throw error(HttpStatus.BAD_REQUEST, "SOURCE_MESSAGE_REQUIRED", "source_message_id 缺失。");
|
||||
}
|
||||
validateLength(sourceMessageReference, "source_message_id", LENGTH_256);
|
||||
|
||||
String hotelId = trimToNull(textAt(root, "hotel_id"));
|
||||
String requestHotelId = trimToNull(textAt(root, "hotel_id"));
|
||||
String systemHotelId = trimToNull(defaultHotelId);
|
||||
if (requestHotelId != null && systemHotelId != null && !requestHotelId.equals(systemHotelId)) {
|
||||
throw error(HttpStatus.BAD_REQUEST, "HOTEL_ID_MISMATCH", "请求 hotel_id 与系统酒店不一致。");
|
||||
}
|
||||
if (requestHotelId == null && isLongText(sourceMessageReference)) {
|
||||
return resolveLegacyInternalSourceMessage(sourceMessageReference, systemHotelId);
|
||||
}
|
||||
String hotelId = requestHotelId == null ? systemHotelId : requestHotelId;
|
||||
if (hotelId == null) {
|
||||
return resolveLegacyInternalSourceMessage(sourceMessageReference);
|
||||
return resolveLegacyInternalSourceMessage(sourceMessageReference, null);
|
||||
}
|
||||
validateLength(hotelId, "hotel_id", LENGTH_64);
|
||||
String sourceProvider = optionalText(textAt(root, "source_provider"), "source_provider", LENGTH_32);
|
||||
@@ -766,9 +774,9 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容历史本地测试和旧接口调用:无 hotel_id 时按内部 SourceMessage ID 查询。
|
||||
* 兼容历史本地测试和旧接口调用:无 hotel_id 时按内部 SourceMessage ID 查询,但必须校验系统酒店边界。
|
||||
*/
|
||||
private ResolvedSourceMessage resolveLegacyInternalSourceMessage(String rawId) {
|
||||
private ResolvedSourceMessage resolveLegacyInternalSourceMessage(String rawId, String expectedHotelId) {
|
||||
Long sourceMessageId;
|
||||
try {
|
||||
sourceMessageId = Long.valueOf(rawId);
|
||||
@@ -777,12 +785,27 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
}
|
||||
SourceMessageInboxSnapshot sourceMessage = sourceMessageInboxRepository.findById(sourceMessageId)
|
||||
.orElseThrow(() -> error(HttpStatus.NOT_FOUND, "SOURCE_MESSAGE_NOT_FOUND", "SourceMessage 不存在。"));
|
||||
if (expectedHotelId != null && !expectedHotelId.equals(sourceMessage.hotelId())) {
|
||||
throw error(HttpStatus.BAD_REQUEST, "HOTEL_ID_MISMATCH", "SourceMessage 所属酒店与系统酒店不一致。");
|
||||
}
|
||||
String responseSourceMessageId = trimToNull(sourceMessage.externalMessageId()) == null
|
||||
? sourceMessageId.toString()
|
||||
: sourceMessage.externalMessageId();
|
||||
return new ResolvedSourceMessage(sourceMessage, responseSourceMessageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断来源消息引用是否为历史本地兼容的内部 SourceMessage ID。
|
||||
*/
|
||||
private boolean isLongText(String value) {
|
||||
try {
|
||||
Long.valueOf(value);
|
||||
return true;
|
||||
} catch (NumberFormatException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从对象节点中读取文本字段,缺失或 null 时返回 null。
|
||||
*/
|
||||
|
||||
@@ -14,8 +14,6 @@ public class ReservationDemoDataProperties {
|
||||
private boolean enabled = false;
|
||||
/** 演示数据 seed 访问口令。 */
|
||||
private String accessKey = "";
|
||||
/** 演示数据默认酒店上下文。 */
|
||||
private String defaultHotelId = "HOTEL-TEST";
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
@@ -32,12 +30,4 @@ public class ReservationDemoDataProperties {
|
||||
public void setAccessKey(String accessKey) {
|
||||
this.accessKey = accessKey;
|
||||
}
|
||||
|
||||
public String getDefaultHotelId() {
|
||||
return defaultHotelId;
|
||||
}
|
||||
|
||||
public void setDefaultHotelId(String defaultHotelId) {
|
||||
this.defaultHotelId = defaultHotelId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
|
||||
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;
|
||||
@@ -64,6 +66,7 @@ public class ReservationDemoDataServiceImpl implements ReservationDemoDataServic
|
||||
private final SourceMessageCaptureService captureService;
|
||||
private final ReservationAiTaskIntakeService intakeService;
|
||||
private final ReservationTaskWorkflowService taskWorkflowService;
|
||||
private final HotelContextService hotelContextService;
|
||||
|
||||
/**
|
||||
* 注入演示配置、JSON 工具和已有业务服务,避免 seed 功能直接写表。
|
||||
@@ -73,12 +76,14 @@ public class ReservationDemoDataServiceImpl implements ReservationDemoDataServic
|
||||
ObjectMapper objectMapper,
|
||||
SourceMessageCaptureService captureService,
|
||||
ReservationAiTaskIntakeService intakeService,
|
||||
ReservationTaskWorkflowService taskWorkflowService) {
|
||||
ReservationTaskWorkflowService taskWorkflowService,
|
||||
HotelContextService hotelContextService) {
|
||||
this.properties = properties;
|
||||
this.objectMapper = objectMapper;
|
||||
this.captureService = captureService;
|
||||
this.intakeService = intakeService;
|
||||
this.taskWorkflowService = taskWorkflowService;
|
||||
this.hotelContextService = hotelContextService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -588,7 +593,6 @@ public class ReservationDemoDataServiceImpl implements ReservationDemoDataServic
|
||||
* 生成前端可直接调用的查询入口。
|
||||
*/
|
||||
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()))
|
||||
@@ -606,12 +610,12 @@ public class ReservationDemoDataServiceImpl implements ReservationDemoDataServic
|
||||
.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("task_list_url", "/api/reservation/tasks?keyword=" + encodedRunId
|
||||
+ "&page_num=1&page_size=20");
|
||||
entrypoints.put("order_list_url", "/api/reservation/orders?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");
|
||||
+ "?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;
|
||||
@@ -641,8 +645,14 @@ public class ReservationDemoDataServiceImpl implements ReservationDemoDataServic
|
||||
* 标准化酒店上下文 ID。
|
||||
*/
|
||||
private String normalizeHotelId(String rawHotelId) {
|
||||
String hotelId = trimToNull(rawHotelId);
|
||||
return hotelId == null ? properties.getDefaultHotelId() : hotelId;
|
||||
try {
|
||||
return hotelContextService.resolveCurrentHotelId(rawHotelId);
|
||||
} catch (HotelContextException exception) {
|
||||
throw new ReservationTaskWorkflowException(
|
||||
exception.getStatus(),
|
||||
exception.getErrorCode(),
|
||||
exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.platform.common.time.UtcTimeFormatter;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
|
||||
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.ReservationAiQueryOrderSnapshot;
|
||||
@@ -38,7 +40,6 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@Service
|
||||
public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQueryService {
|
||||
|
||||
private static final String DEFAULT_HOTEL_ID = "HOTEL-TEST";
|
||||
private static final int DEFAULT_PAGE_NUM = 1;
|
||||
private static final int DEFAULT_PAGE_SIZE = 20;
|
||||
private static final int MAX_PAGE_SIZE = 100;
|
||||
@@ -50,6 +51,7 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
private final ReservationAiWorkflowRepository workflowRepository;
|
||||
private final SourceMessageQueryService sourceMessageQueryService;
|
||||
private final ReservationTaskAvailabilityResolver availabilityResolver;
|
||||
private final HotelContextService hotelContextService;
|
||||
|
||||
/**
|
||||
* 注入持久化边界、SourceMessage 安全摘要服务和可处理状态解析器。
|
||||
@@ -57,10 +59,12 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
public ReservationFrontendQueryServiceImpl(
|
||||
ReservationAiWorkflowRepository workflowRepository,
|
||||
SourceMessageQueryService sourceMessageQueryService,
|
||||
ReservationTaskAvailabilityResolver availabilityResolver) {
|
||||
ReservationTaskAvailabilityResolver availabilityResolver,
|
||||
HotelContextService hotelContextService) {
|
||||
this.workflowRepository = workflowRepository;
|
||||
this.sourceMessageQueryService = sourceMessageQueryService;
|
||||
this.availabilityResolver = availabilityResolver;
|
||||
this.hotelContextService = hotelContextService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,7 +181,7 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
private ReservationTaskWorkbenchQueryRequest normalizeRequest(ReservationTaskWorkbenchQueryRequest request) {
|
||||
if (request == null) {
|
||||
return new ReservationTaskWorkbenchQueryRequest(
|
||||
DEFAULT_HOTEL_ID,
|
||||
normalizeHotelId(null),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
@@ -207,7 +211,7 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
private ReservationOrderListQueryRequest normalizeOrderListRequest(ReservationOrderListQueryRequest request) {
|
||||
if (request == null) {
|
||||
return new ReservationOrderListQueryRequest(
|
||||
DEFAULT_HOTEL_ID,
|
||||
normalizeHotelId(null),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
@@ -556,11 +560,17 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化酒店 ID。第一版未接用户酒店上下文时使用本地默认酒店。
|
||||
* 标准化酒店 ID。前端可不传酒店,后端按当前用户上下文或单酒店系统上下文解析。
|
||||
*/
|
||||
private String normalizeHotelId(String hotelId) {
|
||||
String trimmedHotelId = trimToNull(hotelId);
|
||||
return trimmedHotelId == null ? DEFAULT_HOTEL_ID : trimmedHotelId;
|
||||
try {
|
||||
return hotelContextService.resolveCurrentHotelId(hotelId);
|
||||
} catch (HotelContextException exception) {
|
||||
throw new ReservationTaskWorkflowException(
|
||||
exception.getStatus(),
|
||||
exception.getErrorCode(),
|
||||
exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,7 +30,6 @@ agentbus:
|
||||
connect-timeout: 15s
|
||||
capture:
|
||||
enabled: true
|
||||
default-hotel-id: HOTEL-DEV
|
||||
|
||||
superagent:
|
||||
task-result:
|
||||
|
||||
@@ -28,7 +28,6 @@ agentbus:
|
||||
connect-timeout: 15s
|
||||
capture:
|
||||
enabled: true
|
||||
default-hotel-id: ${AGENTBUS_PROD_DEFAULT_HOTEL_ID}
|
||||
|
||||
superagent:
|
||||
task-result:
|
||||
|
||||
@@ -30,7 +30,6 @@ agentbus:
|
||||
connect-timeout: 15s
|
||||
capture:
|
||||
enabled: true
|
||||
default-hotel-id: HOTEL-TEST
|
||||
|
||||
superagent:
|
||||
task-result:
|
||||
|
||||
@@ -24,7 +24,6 @@ agentbus:
|
||||
max-frame-bytes: 1048576
|
||||
capture:
|
||||
enabled: ${AGENTBUS_CAPTURE_ENABLED:true}
|
||||
default-hotel-id: HOTEL-TEST
|
||||
|
||||
superagent:
|
||||
task-result:
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
-- M005 酒店上下文统一:单酒店阶段数据库级约束平台酒店最多只能有一家 ACTIVE。
|
||||
-- 上线前如已有多家 ACTIVE 酒店,应先人工确认保留哪一家,不能由迁移脚本静默禁用其他酒店。
|
||||
ALTER TABLE platform_hotel
|
||||
ADD COLUMN active_hotel_singleton_key VARCHAR(16) GENERATED ALWAYS AS (
|
||||
CASE WHEN hotel_status = 'ACTIVE' THEN 'ACTIVE' ELSE NULL END
|
||||
);
|
||||
|
||||
-- MySQL 唯一索引允许多个 NULL,因此只会限制 platform_hotel 最多一条 hotel_status='ACTIVE'。
|
||||
CREATE UNIQUE INDEX uk_platform_hotel_single_active
|
||||
ON platform_hotel (active_hotel_singleton_key);
|
||||
Reference in New Issue
Block a user