修复V4房型信息复核与自动DEF展示

This commit is contained in:
andy
2026-07-21 10:40:44 +07:00
parent c28130b3da
commit de6a52d708
10 changed files with 270 additions and 50 deletions

View File

@@ -61,7 +61,8 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
private static final List<String> V4_ORDER_TASK_AUDIT_ACTIONS = List.of(
"V4_CARD_CONFIRM",
"V4_CARD_REVIEW_RESOLVE");
"V4_CARD_REVIEW_RESOLVE",
"V4_ROOMING_LIST_AUTO_DEF");
private static final String ACTION_V4_SOURCE_NOTIFICATION_ACK = "V4_SOURCE_NOTIFICATION_ACK";
private final ReservationAiBatchMapper batchMapper;

View File

@@ -594,7 +594,7 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
JsonNode submittedFinalValues = submittedPayload.path("room_information").path("final_values");
JsonNode submittedRoot = submittedFinalValues.isObject() ? submittedFinalValues : submittedPayload;
String bookingType = textAt(payload.path("room_information"), "booking_type");
overlayEditableRoomInformationLeaves(card, bookingType, finalValues, submittedRoot, List.of());
overlayEditableRoomInformationLeaves(card, bookingType, finalValues, finalValues, submittedRoot, List.of());
normalizeRoomInformationDerivedFields(bookingType, finalValues);
payload.withObject("/room_information").set("change_summary",
changeSummary(payload.path("room_information").path("current_values"), finalValues));
@@ -603,6 +603,7 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
private void overlayEditableRoomInformationLeaves(
ReservationV4TaskCardSnapshot card,
String bookingType,
JsonNode finalValues,
ObjectNode target,
JsonNode submitted,
List<String> path) {
@@ -613,11 +614,11 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
while (fields.hasNext()) {
Map.Entry<String, JsonNode> field = fields.next();
List<String> childPath = appendPath(path, field.getKey());
if (!isRoomInformationPathWritable(card, bookingType, childPath)) {
if (!isRoomInformationPathWritable(card, bookingType, childPath, finalValues)) {
continue;
}
JsonNode submittedValue = submitted.get(field.getKey());
overlayEditableRoomInformationValue(card, bookingType, target, field.getKey(), field.getValue(),
overlayEditableRoomInformationValue(card, bookingType, finalValues, target, field.getKey(), field.getValue(),
submittedValue, childPath);
}
}
@@ -625,6 +626,7 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
private void overlayEditableRoomInformationArray(
ReservationV4TaskCardSnapshot card,
String bookingType,
JsonNode finalValues,
ArrayNode target,
JsonNode submitted,
List<String> path) {
@@ -634,7 +636,7 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
int size = Math.min(target.size(), submitted.size());
for (int index = 0; index < size; index++) {
List<String> childPath = appendPath(path, String.valueOf(index));
overlayEditableRoomInformationValue(card, bookingType, target, index, target.get(index), submitted.get(index),
overlayEditableRoomInformationValue(card, bookingType, finalValues, target, index, target.get(index), submitted.get(index),
childPath);
}
}
@@ -642,21 +644,22 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
private void overlayEditableRoomInformationValue(
ReservationV4TaskCardSnapshot card,
String bookingType,
JsonNode finalValues,
ObjectNode parent,
String fieldName,
JsonNode currentValue,
JsonNode submittedValue,
List<String> path) {
if (currentValue != null && currentValue.isObject()) {
overlayEditableRoomInformationLeaves(card, bookingType, (ObjectNode) currentValue, submittedValue, path);
overlayEditableRoomInformationLeaves(card, bookingType, finalValues, (ObjectNode) currentValue, submittedValue, path);
return;
}
if (currentValue != null && currentValue.isArray()) {
overlayEditableRoomInformationArray(card, bookingType, (ArrayNode) currentValue, submittedValue, path);
overlayEditableRoomInformationArray(card, bookingType, finalValues, (ArrayNode) currentValue, submittedValue, path);
return;
}
if (submittedValue != null && !submittedValue.isMissingNode() && !submittedValue.isContainerNode()
&& isRoomInformationPathWritable(card, bookingType, path)) {
&& isRoomInformationPathWritable(card, bookingType, path, finalValues)) {
parent.set(fieldName, submittedValue);
}
}
@@ -664,21 +667,22 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
private void overlayEditableRoomInformationValue(
ReservationV4TaskCardSnapshot card,
String bookingType,
JsonNode finalValues,
ArrayNode parent,
int index,
JsonNode currentValue,
JsonNode submittedValue,
List<String> path) {
if (currentValue != null && currentValue.isObject()) {
overlayEditableRoomInformationLeaves(card, bookingType, (ObjectNode) currentValue, submittedValue, path);
overlayEditableRoomInformationLeaves(card, bookingType, finalValues, (ObjectNode) currentValue, submittedValue, path);
return;
}
if (currentValue != null && currentValue.isArray()) {
overlayEditableRoomInformationArray(card, bookingType, (ArrayNode) currentValue, submittedValue, path);
overlayEditableRoomInformationArray(card, bookingType, finalValues, (ArrayNode) currentValue, submittedValue, path);
return;
}
if (submittedValue != null && !submittedValue.isMissingNode() && !submittedValue.isContainerNode()
&& isRoomInformationPathWritable(card, bookingType, path)) {
&& isRoomInformationPathWritable(card, bookingType, path, finalValues)) {
parent.set(index, submittedValue);
}
}
@@ -686,7 +690,8 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
private boolean isRoomInformationPathWritable(
ReservationV4TaskCardSnapshot card,
String bookingType,
List<String> path) {
List<String> path,
JsonNode finalValues) {
if (path == null || path.isEmpty() || EVENT_CANCEL_BOOKING.equals(card.eventType())) {
return false;
}
@@ -708,7 +713,8 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
return EVENT_NEW_BOOKING.equals(card.eventType());
}
if ("breakfast_included".equals(fieldName)) {
return BOOKING_TYPE_FIT.equals(bookingType);
JsonNode currentBreakfast = finalValues == null ? null : finalValues.path("breakfast_included");
return BOOKING_TYPE_FIT.equals(bookingType) && isMissingOrNull(currentBreakfast);
}
if ("group_booking_status".equals(fieldName)) {
return BOOKING_TYPE_GROUP.equals(bookingType);
@@ -1739,7 +1745,8 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
throw error(HttpStatus.BAD_REQUEST, "V4_REVIEW_POINTER_READONLY", "该复核字段为只读字段,不允许修改。");
}
String bookingType = textAt(confirmedPayload.path("room_information"), "booking_type");
if (!isRoomInformationPathWritable(card, bookingType, finalValuePath)) {
JsonNode finalValues = confirmedPayload.path("room_information").path("final_values");
if (!isRoomInformationPathWritable(card, bookingType, finalValuePath, finalValues)) {
throw error(HttpStatus.BAD_REQUEST, "V4_REVIEW_POINTER_NOT_ALLOWED", "复核字段不在当前 Room Information 卡允许编辑字段内。");
}
}
@@ -1829,6 +1836,10 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
ObjectNode confirmedPayload,
String pointer,
JsonNode current) {
if (isStableRoomInformationPayload(confirmedPayload)
&& pointer.startsWith("/room_information/final_values/")) {
return true;
}
Set<String> validationPointers = collectValidationErrorPointers(card.validationErrorsJson());
if (validationPointers.contains(pointer)) {
return true;

View File

@@ -88,6 +88,8 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
private static final String BOOKING_TYPE_GROUP = "GROUP";
private static final String BOOKING_TYPE_FIT = "FIT";
private static final String LOCATOR_TYPE_GROUP_CODE = "GROUP_CODE";
private static final String WRITE_TARGET_CONFIRMED_PAYLOAD = "confirmed_payload";
private static final String WRITE_TARGET_REVIEW_FIELD_OVERRIDES = "review_resolution.field_overrides";
private static final String GROUP_BOOKING_STATUS_TEN = "TEN";
private static final Map<String, String> GROUP_BOOKING_STATUS_LABELS = Map.of(
"TEN", "TEN-Tentative",
@@ -397,7 +399,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
ReservationV4ActionAvailabilityResult orderAvailability,
ReservationV4TaskCardSnapshot basicCard) {
JsonNode displayPayload = displayPayloadForCard(orderTask, card);
JsonNode confirmedPayload = parseJson(card.confirmedPayloadJson());
JsonNode confirmedPayload = confirmedPayloadForCard(card);
JsonNode reviewResolution = parseJson(card.reviewResolutionJson());
JsonNode validationErrors = parseJson(card.validationErrorsJson());
ReservationV4ActionAvailabilityResult availability = cardAvailability(card, orderAvailability, basicCard);
@@ -429,6 +431,9 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
ReservationV4OrderTaskSnapshot orderTask,
ReservationV4TaskCardSnapshot card) {
JsonNode displayPayload = parseJson(card.displayPayloadJson());
if (ReservationV4CardType.BASIC_INFORMATION.name().equals(card.cardType())) {
return safeBasicInformationPayload(displayPayload);
}
if (!isRoomInformationEventCard(card) || !displayPayload.isObject()) {
return displayPayload;
}
@@ -443,6 +448,52 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
return safePayload;
}
/**
* 生成卡片安全确认 payloadBasic Information 不向前端暴露 Agent target_order 或其它定位原文。
*/
private JsonNode confirmedPayloadForCard(ReservationV4TaskCardSnapshot card) {
JsonNode confirmedPayload = parseJson(card.confirmedPayloadJson());
if (ReservationV4CardType.BASIC_INFORMATION.name().equals(card.cardType())) {
return safeBasicInformationPayload(confirmedPayload);
}
return confirmedPayload;
}
/**
* 清洗 Basic Information 展示 / 确认 payload只保留前端需要的订单级基础字段。
*/
private JsonNode safeBasicInformationPayload(JsonNode payload) {
if (payload == null || !payload.isObject()) {
return payload == null ? NullNode.getInstance() : payload;
}
ObjectNode safePayload = objectMapper.createObjectNode();
safePayload.put("card_type", ReservationV4CardType.BASIC_INFORMATION.name());
String orderRef = textAt(payload, "order_ref");
if (hasText(orderRef)) {
safePayload.put("order_ref", orderRef);
}
JsonNode source = payload.path("basic_information").isObject()
? payload.path("basic_information")
: payload;
ObjectNode basicInformation = objectMapper.createObjectNode();
copySafeBasicInformationField(source, basicInformation, "account_code");
copySafeBasicInformationField(source, basicInformation, "account_name");
copySafeBasicInformationField(source, basicInformation, "market_code");
copySafeBasicInformationField(source, basicInformation, "source_code");
copySafeBasicInformationField(source, basicInformation, "manual_review");
copySafeBasicInformationField(source, basicInformation, "missing_fields");
safePayload.set("basic_information", basicInformation);
return safePayload;
}
private void copySafeBasicInformationField(JsonNode source, ObjectNode target, String fieldName) {
JsonNode value = source == null ? null : source.get(fieldName);
if (value == null || value.isMissingNode() || value.isNull()) {
return;
}
target.set(fieldName, value);
}
private ReservationV4TaskCardResult toSourceNotificationCard(
ReservationV4SourceNotificationSnapshot notification,
ReservationV4ActionAvailabilityResult availability) {
@@ -1019,6 +1070,11 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
ObjectNode currentValues = currentRoomInformationProjection(orderTask, card);
ObjectNode proposedValues = proposedRoomInformationValues(eventType, bookingType, locatorType, locatorValue, displayPayload);
ObjectNode finalValues = finalRoomInformationValues(eventType, bookingType, locatorType, locatorValue, currentValues, proposedValues);
ObjectNode confirmedFinalValues = confirmedStableRoomInformationFinalValues(card);
if (confirmedFinalValues != null) {
normalizeRoomInformationDerivedFields(bookingType, confirmedFinalValues);
finalValues = roomInformationDisplayValues(bookingType, confirmedFinalValues, true);
}
ObjectNode model = objectMapper.createObjectNode();
model.put("event_type", eventType);
@@ -1031,6 +1087,12 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
return model;
}
private ObjectNode confirmedStableRoomInformationFinalValues(ReservationV4TaskCardSnapshot card) {
JsonNode payload = parseJson(card.confirmedPayloadJson());
JsonNode finalValues = payload.path("room_information").path("final_values");
return finalValues.isObject() ? ((ObjectNode) finalValues).deepCopy() : null;
}
/**
* 从同一本地订单更早已确认 Room Information 卡中提取当前订单投影。
*/
@@ -1334,7 +1396,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
true,
"select",
availability.reviewable() ? "manual_review_only" : "confirm",
availability.reviewable() ? "review_resolution.field_overrides" : "confirmed_payload_json",
availability.reviewable() ? WRITE_TARGET_REVIEW_FIELD_OVERRIDES : WRITE_TARGET_CONFIRMED_PAYLOAD,
"reservation_v4_account_catalog",
false,
validationMessages(validationErrors, "/basic_information/account_code", "basic_information.account_code"),
@@ -1462,7 +1524,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
required,
controlType,
availability.reviewable() ? "manual_review_only" : "confirm",
availability.reviewable() ? "review_resolution.field_overrides" : "confirmed_payload_json",
availability.reviewable() ? WRITE_TARGET_REVIEW_FIELD_OVERRIDES : WRITE_TARGET_CONFIRMED_PAYLOAD,
optionsSource,
false,
validationMessages(validationErrors, pointer, fieldPath),
@@ -1491,7 +1553,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
required,
controlType,
availability.reviewable() ? "manual_review_only" : "confirm",
availability.reviewable() ? "review_resolution.field_overrides" : "confirmed_payload_json",
availability.reviewable() ? WRITE_TARGET_REVIEW_FIELD_OVERRIDES : WRITE_TARGET_CONFIRMED_PAYLOAD,
optionsSource,
false,
validationMessages(validationErrors, pointer, fieldPath),
@@ -1506,13 +1568,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
if (!availability.editable()) {
return false;
}
if (!availability.reviewable()) {
return true;
}
if (validationErrorPointers(validationErrors).contains(pointer)) {
return true;
}
return isUnresolvedReviewLeaf(currentValue);
return true;
}
private Set<String> validationErrorPointers(JsonNode validationErrors) {
@@ -1641,7 +1697,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
false,
controlType(fieldPath),
availability.reviewable() ? "manual_review_only" : "confirm",
availability.reviewable() ? "review_resolution.field_overrides" : "confirmed_payload_json",
availability.reviewable() ? WRITE_TARGET_REVIEW_FIELD_OVERRIDES : WRITE_TARGET_CONFIRMED_PAYLOAD,
optionsSource(fieldPath),
false,
fieldValidationErrors,