@@ -45,9 +45,12 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
private static final String FIELD_CONTRACT_VERSION = " code-v1 " ;
private static final String BATCH_KEY_PREFIX = " superagent-task-result-batch:v1 " ;
private static final String ITEM_KEY_PREFIX = " superagent-task-result-item:v1 " ;
private static final String DEFAULT_SOURCE_PROVIDER = " AGENTBUS " ;
private static final String DEFAULT_SOURCE_CHANNEL = " EMAIL " ;
private static final int LENGTH_32 = 32 ;
private static final int LENGTH_64 = 64 ;
private static final int LENGTH_128 = 128 ;
private static final int LENGTH_256 = 256 ;
private static final int MAX_QUEUE_ORDER_RETRY = 5 ;
private final ObjectMapper objectMapper ;
@@ -73,14 +76,15 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
@Transactional
public SuperAgentTaskResultResponse accept ( String rawBody , String clientId , String requestId ) {
JsonNode root = parseJson ( rawBody ) ;
Long s ourceMessageId = pars eSourceMessageId ( root ) ;
ResolvedSourceMessage resolvedS ourceMessage = resolv eSourceMessage( root ) ;
SourceMessageInboxSnapshot sourceMessage = resolvedSourceMessage . snapshot ( ) ;
Long sourceMessageId = sourceMessage . id ( ) ;
String responseSourceMessageId = resolvedSourceMessage . responseSourceMessageId ( ) ;
JsonNode taskResults = root . path ( " ai_task_results " ) ;
if ( ! taskResults . isArray ( ) | | taskResults . isEmpty ( ) ) {
throw error ( HttpStatus . BAD_REQUEST , " TASK_RESULTS_EMPTY " , " ai_task_results 不能为空。 " ) ;
}
SourceMessageInboxSnapshot sourceMessage = sourceMessageInboxRepository . findById ( sourceMessageId )
. orElseThrow ( ( ) - > error ( HttpStatus . NOT_FOUND , " SOURCE_MESSAGE_NOT_FOUND " , " SourceMessage 不存在。 " ) ) ;
String hotelId = sourceMessage . hotelId ( ) ;
String requestPayloadSha256 = sha256 ( rawBody = = null ? " " : rawBody ) ;
String batchIdempotencyKey = sha256 ( BATCH_KEY_PREFIX + " | " + sourceMessageId + " | " + requestPayloadSha256 ) ;
@@ -89,7 +93,7 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
. findBatchBySourceMessageId ( hotelId , sourceMessageId )
. orElse ( null ) ;
if ( existingBatch ! = null ) {
return handleExistingBatch ( requestId , s ourceMessageId, requestPayloadSha256 , existingBatch ) ;
return handleExistingBatch ( requestId , responseS ourceMessageId, requestPayloadSha256 , existingBatch ) ;
}
LocalDateTime now = nowUtc ( ) ;
@@ -109,7 +113,7 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
if ( batchId = = null ) {
return handleExistingBatch (
requestId ,
s ourceMessageId,
responseS ourceMessageId,
requestPayloadSha256 ,
workflowRepository . findBatchBySourceMessageId ( hotelId , sourceMessageId )
. orElseThrow ( ( ) - > error ( HttpStatus . CONFLICT , " IDEMPOTENCY_CONFLICT " , " AI 批次并发写入状态不确定。 " ) ) ) ;
@@ -123,7 +127,7 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
return new SuperAgentTaskResultResponse (
safeRequestId ,
s ourceMessageId. toString ( ) ,
responseS ourceMessageId,
batchId . toString ( ) ,
false ,
responseItems . size ( ) ,
@@ -148,7 +152,7 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
*/
private SuperAgentTaskResultResponse handleExistingBatch (
String requestId ,
Lo ng s ourceMessageId,
Stri ng responseS ourceMessageId,
String requestPayloadSha256 ,
ReservationAiBatchSnapshot existingBatch ) {
if ( ! requestPayloadSha256 . equals ( existingBatch . requestPayloadSha256 ( ) ) ) {
@@ -156,7 +160,7 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
}
return new SuperAgentTaskResultResponse (
requestId ,
s ourceMessageId. toString ( ) ,
responseS ourceMessageId,
existingBatch . id ( ) . toString ( ) ,
true ,
existingBatch . itemCount ( ) = = null ? 0 : existingBatch . itemCount ( ) ,
@@ -484,18 +488,46 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
}
/**
* 解析 SourceMessage ID,接口一次只能处理一个来源消息 。
* 解析 SuperAgent 来源消息引用。正式契约使用外部邮件 ID, 旧本地夹具仍兼容内部 S ourceMessage ID。
*/
private Long pars eSourceMessageId ( JsonNode root ) {
String rawId = trimToNull ( textAt ( root , " source_message_id " ) ) ;
if ( rawId = = null ) {
private ResolvedSourceMessage resolv eSourceMessage( JsonNode root ) {
String sourceMessageReference = trimToNull ( textAt ( root , " source_message_id " ) ) ;
if ( sourceMessageReference = = null ) {
throw error ( HttpStatus . BAD_REQUEST , " SOURCE_MESSAGE_REQUIRED " , " source_message_id 缺失。 " ) ;
}
try {
return Long . valueOf ( rawId ) ;
} catch ( NumberFormatException exception ) {
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 " ) ) ;
if ( hotelId = = null ) {
return resolveLegacyInternalSourceMessage ( sourceMessageReference ) ;
}
validateLength ( hotelId , " hotel_id " , LENGTH_64 ) ;
String sourceProvider = optionalText ( textAt ( root , " source_provider " ) , " source_provider " , LENGTH_32 ) ;
String sourceChannel = optionalText ( textAt ( root , " source_channel " ) , " source_channel " , LENGTH_32 ) ;
String provider = sourceProvider = = null ? DEFAULT_SOURCE_PROVIDER : sourceProvider ;
String channel = sourceChannel = = null ? DEFAULT_SOURCE_CHANNEL : sourceChannel ;
SourceMessageInboxSnapshot sourceMessage = sourceMessageInboxRepository
. findByIdempotencyKey ( hotelId , provider , channel , sourceMessageReference )
. orElseThrow ( ( ) - > error ( HttpStatus . NOT_FOUND , " SOURCE_MESSAGE_NOT_FOUND " , " SourceMessage 不存在。 " ) ) ;
return new ResolvedSourceMessage ( sourceMessage , sourceMessageReference ) ;
}
/**
* 兼容历史本地测试和旧接口调用:无 hotel_id 时按内部 SourceMessage ID 查询。
*/
private ResolvedSourceMessage resolveLegacyInternalSourceMessage ( String rawId ) {
Long sourceMessageId ;
try {
sourceMessageId = Long . valueOf ( rawId ) ;
} catch ( NumberFormatException exception ) {
throw error ( HttpStatus . BAD_REQUEST , " HOTEL_ID_REQUIRED " , " 使用外部 source_message_id 时 hotel_id 不能为空。 " ) ;
}
SourceMessageInboxSnapshot sourceMessage = sourceMessageInboxRepository . findById ( sourceMessageId )
. orElseThrow ( ( ) - > error ( HttpStatus . NOT_FOUND , " SOURCE_MESSAGE_NOT_FOUND " , " SourceMessage 不存在。 " ) ) ;
String responseSourceMessageId = trimToNull ( sourceMessage . externalMessageId ( ) ) = = null
? sourceMessageId . toString ( )
: sourceMessage . externalMessageId ( ) ;
return new ResolvedSourceMessage ( sourceMessage , responseSourceMessageId ) ;
}
/**
@@ -619,6 +651,15 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
return value ! = null & & ! value . trim ( ) . isEmpty ( ) ;
}
/**
* 已解析的来源消息。snapshot 使用内部 ID 参与落库, responseSourceMessageId 面向 SuperAgent 回显外部 ID。
*/
private record ResolvedSourceMessage (
SourceMessageInboxSnapshot snapshot ,
String responseSourceMessageId
) {
}
/**
* 将空白字符串转换为 null。
*/