实现酒店上下文单酒店收口
This commit is contained in:
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user