修复V4复核指针与任务详情脱敏

This commit is contained in:
andy
2026-07-21 11:35:17 +07:00
parent de6a52d708
commit f228c2b704
9 changed files with 452 additions and 20 deletions

View File

@@ -1144,6 +1144,9 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
ReservationV4OrderTaskSnapshot orderTask,
ReservationV4TaskCardSnapshot card,
ObjectNode displayPayload) {
if (displayPayload.path("room_information").path("final_values").isObject()) {
return stableRoomInformationDisplayModel(orderTask, card, displayPayload);
}
String eventType = firstText(card.eventType(), textAt(displayPayload, "event_type"));
JsonNode targetOrder = displayPayload.path("target_order");
String bookingType = firstText(textAt(targetOrder, "booking_type"), orderTask.targetBookingType());
@@ -1164,6 +1167,32 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
return model;
}
/**
* 兼容已经持久化为稳定 room_information 的展示快照,复核时直接以该稳定模型作为白名单基准。
*/
private ObjectNode stableRoomInformationDisplayModel(
ReservationV4OrderTaskSnapshot orderTask,
ReservationV4TaskCardSnapshot card,
ObjectNode displayPayload) {
JsonNode roomInformation = displayPayload.path("room_information");
String eventType = firstText(card.eventType(), firstText(textAt(roomInformation, "event_type"), textAt(displayPayload, "event_type")));
String bookingType = firstText(textAt(roomInformation, "booking_type"), orderTask.targetBookingType());
ObjectNode currentValues = roomInformationDisplayValues(bookingType, roomInformation.path("current_values"), true);
ObjectNode proposedValues = roomInformationDisplayValues(bookingType, roomInformation.path("proposed_values"), true);
ObjectNode finalValues = roomInformationDisplayValues(bookingType, roomInformation.path("final_values"), true);
ensureRoomInformationEditablePlaceholders(card, bookingType, finalValues);
normalizeRoomInformationDerivedFields(bookingType, finalValues);
ObjectNode model = objectMapper.createObjectNode();
model.put("event_type", eventType);
model.put("booking_type", bookingType);
model.set("current_values", currentValues);
model.set("proposed_values", proposedValues);
model.set("final_values", finalValues);
model.set("change_summary", changeSummary(currentValues, finalValues));
return model;
}
private ObjectNode currentRoomInformationProjection(ReservationV4OrderTaskSnapshot orderTask) {
ObjectNode current = objectMapper.createObjectNode();
if (orderTask.orderId() == null) {

View File

@@ -111,6 +111,13 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
"order_ref",
"route_code",
"target_order");
private static final Set<String> V4_BUSINESS_PAYLOAD_SENSITIVE_FIELDS = Set.of(
"attachment_url",
"html_body",
"html_body_sanitized",
"pms_raw_response",
"raw_evidence",
"target_order");
private final ReservationV4WorkflowRepository workflowRepository;
private final ReservationAiWorkflowRepository aiWorkflowRepository;
@@ -434,9 +441,13 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
if (ReservationV4CardType.BASIC_INFORMATION.name().equals(card.cardType())) {
return safeBasicInformationPayload(displayPayload);
}
if (!isRoomInformationEventCard(card) || !displayPayload.isObject()) {
if (ReservationV4CardType.SOURCE_MESSAGE_DISPLAY.name().equals(card.cardType())
|| ReservationV4CardType.SOURCE_MESSAGE_NOTIFICATION.name().equals(card.cardType())) {
return displayPayload;
}
if (!isRoomInformationEventCard(card) || !displayPayload.isObject()) {
return safeBusinessPayload(displayPayload);
}
ObjectNode source = (ObjectNode) displayPayload;
ObjectNode safePayload = objectMapper.createObjectNode();
safePayload.put("card_type", firstText(textAt(source, "card_type"), card.cardType()));
@@ -456,7 +467,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
if (ReservationV4CardType.BASIC_INFORMATION.name().equals(card.cardType())) {
return safeBasicInformationPayload(confirmedPayload);
}
return confirmedPayload;
return safeBusinessPayload(confirmedPayload);
}
/**
@@ -494,6 +505,83 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
target.set(fieldName, value);
}
/**
* 清洗普通业务卡展示 / 确认 payload避免 Agent 定位三元组、邮件正文或外部原始证据混入任务详情。
*/
private JsonNode safeBusinessPayload(JsonNode payload) {
if (payload == null || payload.isMissingNode() || payload.isNull()) {
return NullNode.getInstance();
}
JsonNode copied = payload.deepCopy();
removeSensitiveBusinessPayloadFields(copied);
return copied;
}
private void removeSensitiveBusinessPayloadFields(JsonNode node) {
if (node == null || node.isMissingNode() || node.isNull()) {
return;
}
if (node.isObject()) {
ObjectNode objectNode = (ObjectNode) node;
objectNode.remove(V4_BUSINESS_PAYLOAD_SENSITIVE_FIELDS);
Iterator<Map.Entry<String, JsonNode>> iterator = objectNode.fields();
List<String> fieldsToRemove = new ArrayList<>();
while (iterator.hasNext()) {
Map.Entry<String, JsonNode> entry = iterator.next();
if (isSensitiveBusinessPayloadField(entry.getKey()) || isSensitiveBusinessPayloadValue(entry.getValue())) {
fieldsToRemove.add(entry.getKey());
continue;
}
removeSensitiveBusinessPayloadFields(entry.getValue());
}
if (!fieldsToRemove.isEmpty()) {
objectNode.remove(fieldsToRemove);
}
return;
}
if (node.isArray()) {
ArrayNode arrayNode = (ArrayNode) node;
for (int index = 0; index < arrayNode.size(); index++) {
JsonNode item = arrayNode.get(index);
if (isSensitiveBusinessPayloadValue(item)) {
arrayNode.set(index, NullNode.getInstance());
continue;
}
removeSensitiveBusinessPayloadFields(item);
}
}
}
private boolean isSensitiveBusinessPayloadField(String fieldName) {
String normalized = fieldName == null ? "" : fieldName.toLowerCase(Locale.ROOT);
String compact = normalized.replace("_", "").replace("-", "");
return V4_BUSINESS_PAYLOAD_SENSITIVE_FIELDS.contains(normalized)
|| "url".equals(normalized)
|| normalized.endsWith("_url")
|| normalized.contains("private_url")
|| normalized.contains("download_url")
|| normalized.contains("signed_url")
|| normalized.contains("attachment_url")
|| normalized.contains("external_url")
|| compact.endsWith("url")
|| compact.contains("externalurl")
|| compact.contains("signedurl")
|| compact.contains("privateurl")
|| compact.contains("downloadurl");
}
private boolean isSensitiveBusinessPayloadValue(JsonNode value) {
if (value == null || !value.isTextual()) {
return false;
}
String text = value.asText("");
String normalized = text.toLowerCase(Locale.ROOT);
return normalized.startsWith("http://")
|| normalized.startsWith("https://")
|| normalized.startsWith("oss://")
|| normalized.startsWith("file://");
}
private ReservationV4TaskCardResult toSourceNotificationCard(
ReservationV4SourceNotificationSnapshot notification,
ReservationV4ActionAvailabilityResult availability) {
@@ -1062,6 +1150,34 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
ReservationV4OrderTaskSnapshot orderTask,
ReservationV4TaskCardSnapshot card,
ObjectNode displayPayload) {
ObjectNode model;
if (displayPayload.path("room_information").path("final_values").isObject()) {
model = stableRoomInformationDisplayModel(orderTask, card, displayPayload);
} else {
model = derivedRoomInformationDisplayModel(orderTask, card, displayPayload);
}
ObjectNode confirmedFinalValues = confirmedStableRoomInformationFinalValues(card);
if (confirmedFinalValues != null) {
String bookingType = textAt(model, "booking_type");
normalizeRoomInformationDerivedFields(bookingType, confirmedFinalValues);
ObjectNode finalValues = roomInformationDisplayValues(bookingType, confirmedFinalValues, true);
ObjectNode currentValues = model.path("current_values").isObject()
? (ObjectNode) model.path("current_values")
: objectMapper.createObjectNode();
model.set("final_values", finalValues);
model.set("change_summary", changeSummary(currentValues, finalValues));
}
model.set("group_booking_status_options", groupBookingStatusOptions());
return model;
}
/**
* 从 Agent 原始 Room Information payload 生成稳定展示模型。
*/
private ObjectNode derivedRoomInformationDisplayModel(
ReservationV4OrderTaskSnapshot orderTask,
ReservationV4TaskCardSnapshot card,
ObjectNode displayPayload) {
String eventType = firstText(card.eventType(), textAt(displayPayload, "event_type"));
JsonNode targetOrder = displayPayload.path("target_order");
String bookingType = firstText(textAt(targetOrder, "booking_type"), orderTask.targetBookingType());
@@ -1070,11 +1186,6 @@ 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);
@@ -1083,7 +1194,31 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
model.set("proposed_values", EVENT_CANCEL_BOOKING.equals(eventType) ? objectMapper.createObjectNode() : proposedValues);
model.set("final_values", finalValues);
model.set("change_summary", changeSummary(EVENT_NEW_BOOKING.equals(eventType) ? objectMapper.createObjectNode() : currentValues, finalValues));
model.set("group_booking_status_options", groupBookingStatusOptions());
return model;
}
/**
* 兼容已经持久化为稳定 room_information 结构的任务卡,避免刷新或复核时丢失前端白名单字段。
*/
private ObjectNode stableRoomInformationDisplayModel(
ReservationV4OrderTaskSnapshot orderTask,
ReservationV4TaskCardSnapshot card,
ObjectNode displayPayload) {
JsonNode roomInformation = displayPayload.path("room_information");
String eventType = firstText(card.eventType(), firstText(textAt(roomInformation, "event_type"), textAt(displayPayload, "event_type")));
String bookingType = firstText(textAt(roomInformation, "booking_type"), orderTask.targetBookingType());
ObjectNode currentValues = roomInformationDisplayValues(bookingType, roomInformation.path("current_values"), true);
ObjectNode proposedValues = roomInformationDisplayValues(bookingType, roomInformation.path("proposed_values"), true);
ObjectNode finalValues = roomInformationDisplayValues(bookingType, roomInformation.path("final_values"), true);
normalizeRoomInformationDerivedFields(bookingType, finalValues);
ObjectNode model = objectMapper.createObjectNode();
model.put("event_type", eventType);
model.put("booking_type", bookingType);
model.set("current_values", currentValues);
model.set("proposed_values", proposedValues);
model.set("final_values", finalValues);
model.set("change_summary", changeSummary(currentValues, finalValues));
return model;
}