实现M002 V3 P0.1 Parent Group路由修订
This commit is contained in:
@@ -106,22 +106,6 @@ public enum ReservationAiRouteDefinition {
|
||||
ReservationAiSystemProcessCategory.BUSINESS_TASK,
|
||||
ReservationSystemTaskType.CANCEL_BOOKING,
|
||||
ReservationTaskCardType.CANCEL_BOOKING),
|
||||
LINKED_PARENT_RELEASE_AFTER_CHILD_SPLIT_NORMAL(
|
||||
"R07_LINKED_PARENT_RELEASE_AFTER_CHILD_SPLIT_NORMAL",
|
||||
"normal_task",
|
||||
"Cancel Booking",
|
||||
"linked_parent_release_after_child_split",
|
||||
ReservationAiSystemProcessCategory.BUSINESS_TASK,
|
||||
ReservationSystemTaskType.CANCEL_BOOKING,
|
||||
ReservationTaskCardType.CANCEL_BOOKING),
|
||||
LINKED_PARENT_RELEASE_AFTER_CHILD_SPLIT_REVIEW(
|
||||
"R07_LINKED_PARENT_RELEASE_AFTER_CHILD_SPLIT_REVIEW",
|
||||
"manual_review",
|
||||
"Cancel Booking",
|
||||
"linked_parent_release_after_child_split",
|
||||
ReservationAiSystemProcessCategory.BUSINESS_TASK,
|
||||
ReservationSystemTaskType.CANCEL_BOOKING,
|
||||
ReservationTaskCardType.CANCEL_BOOKING),
|
||||
CANCEL_ALLOTMENT_CONTROL_BLOCK_NORMAL(
|
||||
"R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_NORMAL",
|
||||
"normal_task",
|
||||
|
||||
@@ -213,6 +213,10 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
if (!taskResults.isArray() || taskResults.isEmpty()) {
|
||||
throw error(HttpStatus.BAD_REQUEST, "TASK_RESULTS_EMPTY", "ai_task_results 不能为空。");
|
||||
}
|
||||
V3EventContractIssue legacyIssue = inspectV2TaskResultsContractIssue(taskResults);
|
||||
if (legacyIssue != null) {
|
||||
throw error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", legacyIssue.message());
|
||||
}
|
||||
|
||||
String hotelId = sourceMessage.hotelId();
|
||||
String requestPayloadSha256 = sha256(rawBody == null ? "" : rawBody);
|
||||
@@ -364,10 +368,14 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
}
|
||||
|
||||
List<SuperAgentTaskResultItemResponse> responseItems = new ArrayList<>(itemCount);
|
||||
Set<String> acceptedParentSplitCodes = new LinkedHashSet<>();
|
||||
for (int index = 0; index < messageEvents.size(); index++) {
|
||||
JsonNode event = messageEvents.get(index);
|
||||
int arrayIndex = index + 1;
|
||||
V3EventContractIssue contractIssue = inspectV3EventContractIssue(event, messageEvents);
|
||||
if (contractIssue == null) {
|
||||
contractIssue = inspectV3ParentSplitDuplicateIssue(event, acceptedParentSplitCodes);
|
||||
}
|
||||
if (contractIssue != null) {
|
||||
responseItems.add(createAdapterContractErrorTransition(
|
||||
hotelId,
|
||||
@@ -426,6 +434,32 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 V2 兼容 ai_task_results[] 中不再允许当前 producer 继续提交的旧 Parent split 三元组。
|
||||
*/
|
||||
private V3EventContractIssue inspectV2TaskResultsContractIssue(JsonNode taskResults) {
|
||||
for (JsonNode item : taskResults) {
|
||||
if (isLegacyParentSplitTriplet(
|
||||
textAt(item, "result_type"),
|
||||
textAt(item, "task_type"),
|
||||
textAt(item, "task_subtype"))) {
|
||||
return new V3EventContractIssue(
|
||||
"LINKED_PARENT_RELEASE_LEGACY_CANCEL_BOOKING_UNSUPPORTED",
|
||||
"当前 producer 不再支持 Cancel Booking + linked_parent_release_after_child_split,Parent split 父事件必须使用 Cancel Allotment。");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别 P0.1 已删除的旧 Parent split 三元组。
|
||||
*/
|
||||
private boolean isLegacyParentSplitTriplet(String resultType, String taskType, String taskSubtype) {
|
||||
return ("normal_task".equals(resultType) || "manual_review".equals(resultType))
|
||||
&& "Cancel Booking".equals(taskType)
|
||||
&& "linked_parent_release_after_child_split".equals(taskSubtype);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 V3 业务根的批次级 P0 结构;单个事件内容错误由 event 级 transition 承接。
|
||||
*/
|
||||
@@ -705,13 +739,9 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel Booking 需要先识别 parent split 关系,再识别普通取消对象。
|
||||
* Cancel Booking 只识别普通取消对象;Parent split 在 P0.1 后统一走 Cancel Allotment。
|
||||
*/
|
||||
private String cancelBookingSubtype(JsonNode event, JsonNode extractedFields) {
|
||||
String relationshipType = firstText(extractedFields, event, "relationship_type");
|
||||
if ("linked_parent_release_after_child_split".equals(relationshipType)) {
|
||||
return "linked_parent_release_after_child_split";
|
||||
}
|
||||
return switch (nullToEmpty(firstText(extractedFields, event, "cancel_object_type"))) {
|
||||
case "fit_reservation" -> "cancel_fit_reservation";
|
||||
case "group_block" -> "cancel_group_block";
|
||||
@@ -740,6 +770,11 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
return manualReviewIssue;
|
||||
}
|
||||
}
|
||||
if (isLegacyLinkedParentCancelBooking(event)) {
|
||||
return new V3EventContractIssue(
|
||||
"LINKED_PARENT_RELEASE_LEGACY_CANCEL_BOOKING_UNSUPPORTED",
|
||||
"当前 producer 不再支持 Cancel Booking + linked_parent_release_after_child_split,Parent split 父事件必须使用 Cancel Allotment。");
|
||||
}
|
||||
if (isLinkedParentReleaseCandidate(event) && !validLinkedParentReleaseCandidate(event, messageEvents)) {
|
||||
return new V3EventContractIssue(
|
||||
"LINKED_PARENT_RELEASE_CONTRACT_INCOMPLETE",
|
||||
@@ -770,14 +805,54 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 parent split 候选的最小可追溯关系字段,避免只凭 relationship_type 建业务任务。
|
||||
* 判断当前 producer 是否仍在输出旧 Parent Cancel Booking 组合。
|
||||
*/
|
||||
private boolean isLegacyLinkedParentCancelBooking(JsonNode event) {
|
||||
JsonNode extractedFields = event == null ? null : event.path("extracted_fields");
|
||||
return isLegacyParentSplitTriplet(
|
||||
event == null || isNullOrMissing(event.get("manual_review"))
|
||||
? AiResultType.NORMAL_TASK.code()
|
||||
: AiResultType.MANUAL_REVIEW.code(),
|
||||
trimToNull(textAt(event, "event_type")),
|
||||
firstText(extractedFields, event, "relationship_type"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验同一 V3 业务根中同一个 Parent split cluster 只能有一个 Parent 候选。
|
||||
*/
|
||||
private V3EventContractIssue inspectV3ParentSplitDuplicateIssue(JsonNode event, Set<String> acceptedParentSplitCodes) {
|
||||
if (!isLinkedParentReleaseCandidate(event)) {
|
||||
return null;
|
||||
}
|
||||
String parentGroupCode = trimToNull(textAt(event.path("case_keys"), "group_code"));
|
||||
if (parentGroupCode != null && !acceptedParentSplitCodes.add(parentGroupCode)) {
|
||||
return new V3EventContractIssue(
|
||||
"LINKED_PARENT_RELEASE_DUPLICATE_PARENT",
|
||||
"同一个 Parent split cluster 只能有一个 Parent Cancel Allotment 候选。");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 P0.1 parent split 候选的最小可追溯关系字段,避免只凭 relationship_type 建业务任务。
|
||||
*/
|
||||
private boolean validLinkedParentReleaseCandidate(JsonNode event, JsonNode messageEvents) {
|
||||
JsonNode extractedFields = event == null ? null : event.path("extracted_fields");
|
||||
JsonNode caseKeys = event == null ? null : event.path("case_keys");
|
||||
if (!("Cancel Booking".equals(trimToNull(textAt(event, "event_type")))
|
||||
&& "group_block".equals(firstText(extractedFields, event, "cancel_object_type"))
|
||||
&& trimToNull(textAt(caseKeys, "group_code")) != null
|
||||
String parentGroupCode = trimToNull(textAt(caseKeys, "group_code"));
|
||||
String parentBlockCode = trimToNull(textAt(caseKeys, "block_code"));
|
||||
if (!("Cancel Allotment".equals(trimToNull(textAt(event, "event_type")))
|
||||
&& "entire_allotment_control_block".equals(firstText(extractedFields, event, "cancel_scope"))
|
||||
&& isBooleanTrue(extractedFields.path("parent_release_or_cancel_candidate"))
|
||||
&& "parent_to_child_allocation_split".equals(firstText(extractedFields, event, "release_reason"))
|
||||
&& parentGroupCode != null
|
||||
&& parentGroupCode.equals(firstText(extractedFields, event, "parent_group_code"))
|
||||
&& isBooleanTrue(extractedFields.path("allocation_split_from_parent"))
|
||||
&& "New Booking".equals(trimToNull(textAt(event, "related_event_type")))
|
||||
&& event != null
|
||||
&& isBooleanTrue(event.path("requires_downstream_hard_validation"))
|
||||
&& parentGroupCode != null
|
||||
&& parentGroupCode.equals(parentBlockCode)
|
||||
&& nonEmptyArray(extractedFields, "child_group_codes")
|
||||
&& nonEmptyArray(event, "related_source_event_indices"))) {
|
||||
return false;
|
||||
@@ -789,25 +864,28 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
|| childGroupCodes.size() != relatedSourceEventIndices.size()) {
|
||||
return false;
|
||||
}
|
||||
Set<String> expectedChildGroups = new LinkedHashSet<>(childGroupCodes);
|
||||
Set<String> childGroupCodeSet = new LinkedHashSet<>(childGroupCodes);
|
||||
Set<String> relatedEventIds = new LinkedHashSet<>(relatedSourceEventIndices);
|
||||
if (expectedChildGroups.size() != childGroupCodes.size()
|
||||
if (childGroupCodeSet.size() != childGroupCodes.size()
|
||||
|| relatedEventIds.size() != relatedSourceEventIndices.size()) {
|
||||
return false;
|
||||
}
|
||||
Set<String> actualChildGroups = new LinkedHashSet<>();
|
||||
for (String sourceEventIndex : relatedSourceEventIndices) {
|
||||
for (int index = 0; index < relatedSourceEventIndices.size(); index++) {
|
||||
String sourceEventIndex = relatedSourceEventIndices.get(index);
|
||||
JsonNode childEvent = findV3MessageEventBySourceEventIndex(messageEvents, sourceEventIndex);
|
||||
if (childEvent == null || !"New Booking".equals(trimToNull(textAt(childEvent, "event_type")))) {
|
||||
return false;
|
||||
}
|
||||
String childGroupCode = trimToNull(textAt(childEvent.path("case_keys"), "group_code"));
|
||||
if (childGroupCode == null || !expectedChildGroups.contains(childGroupCode)) {
|
||||
JsonNode childExtractedFields = childEvent.path("extracted_fields");
|
||||
if (!"Group Block".equals(firstText(childExtractedFields, childEvent, "booking_object_type"))) {
|
||||
return false;
|
||||
}
|
||||
String childGroupCode = trimToNull(textAt(childEvent.path("case_keys"), "group_code"));
|
||||
if (!childGroupCodes.get(index).equals(childGroupCode)) {
|
||||
return false;
|
||||
}
|
||||
actualChildGroups.add(childGroupCode);
|
||||
}
|
||||
return actualChildGroups.equals(expectedChildGroups);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user