收口V4业务卡展示字段白名单

This commit is contained in:
andy
2026-07-19 01:09:31 +07:00
parent e59ac2f3bf
commit 99528b483f
3 changed files with 127 additions and 2 deletions

View File

@@ -31,6 +31,7 @@ import cn.nianxx.thhotel.workflows.reservation.service.ReservationAiTaskIntakeSe
import cn.nianxx.thhotel.workflows.reservation.service.ReservationV4TaskIntakeService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
@@ -1144,11 +1145,119 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
payload.put("result_type", route.resultType());
payload.put("task_type", route.taskType());
payload.put("task_subtype", route.taskSubtype());
payload.set("target_order", event.path("target_order"));
payload.set("business_fields", event);
payload.set("target_order", v4TargetOrderDisplayPayload(event.path("target_order")));
payload.set("business_fields", v4BusinessFieldsDisplayPayload(event));
return payload;
}
/**
* 生成 V4 target_order 展示快照,只保留契约内定位三元组,避免未知字段进入前端展示 payload。
*/
private ObjectNode v4TargetOrderDisplayPayload(JsonNode targetOrder) {
ObjectNode display = objectMapper.createObjectNode();
copyJsonFieldIfPresent(display, targetOrder, "booking_type");
copyJsonFieldIfPresent(display, targetOrder, "locator_type");
copyJsonFieldIfPresent(display, targetOrder, "locator_value");
return display;
}
/**
* 生成 V4 业务字段展示快照。展示 payload 只复制白名单业务字段;完整 event 仅保留在 ai_payload_json。
*/
private ObjectNode v4BusinessFieldsDisplayPayload(JsonNode event) {
ObjectNode fields = objectMapper.createObjectNode();
copyJsonFieldIfPresent(fields, event, "order_ref");
copyJsonFieldIfPresent(fields, event, "event_type");
copyJsonFieldIfPresent(fields, event, "manual_review");
switch (nullToEmpty(textAt(event, "event_type"))) {
case "NEW_BOOKING" -> copyV4NewBookingDisplayFields(fields, event);
case "UPDATE_BOOKING" -> copyV4UpdateBookingDisplayFields(fields, event);
case "TRACE_RESERVATION_NOTES" -> fields.set("trace_items", v4TraceItemsDisplayPayload(event.path("trace_items")));
case "PAYMENT" -> copyJsonFieldIfPresent(fields, event, "attachment_ids");
case "CANCEL_BOOKING", "ROOMING_LIST" -> {
// 当前 V4 契约下这两类 event 没有额外可编辑展示字段。
}
default -> {
// 已由 V4 validator 拦截;这里保守返回公共字段。
}
}
return fields;
}
/**
* 复制 NEW_BOOKING 展示字段room_items 逐项白名单化。
*/
private void copyV4NewBookingDisplayFields(ObjectNode fields, JsonNode event) {
copyJsonFieldIfPresent(fields, event, "arrival_date");
copyJsonFieldIfPresent(fields, event, "departure_date");
copyJsonFieldIfPresent(fields, event, "rate_code");
copyJsonFieldIfPresent(fields, event, "booking_scenario");
copyJsonFieldIfPresent(fields, event, "guest_name");
fields.set("room_items", v4RoomItemsDisplayPayload(event.path("room_items")));
}
/**
* 复制 UPDATE_BOOKING 展示字段after 只保留第一版契约允许的稀疏更新字段。
*/
private void copyV4UpdateBookingDisplayFields(ObjectNode fields, JsonNode event) {
JsonNode after = event.path("after");
ObjectNode afterDisplay = objectMapper.createObjectNode();
copyJsonFieldIfPresent(afterDisplay, after, "guest_name");
copyJsonFieldIfPresent(afterDisplay, after, "arrival_date");
copyJsonFieldIfPresent(afterDisplay, after, "departure_date");
if (after.has("room_items")) {
JsonNode roomItems = after.path("room_items");
afterDisplay.set("room_items", roomItems.isNull() ? objectMapper.nullNode() : v4RoomItemsDisplayPayload(roomItems));
}
fields.set("after", afterDisplay);
}
/**
* 生成 room_items[] 展示快照,只保留房型代码和房量。
*/
private ArrayNode v4RoomItemsDisplayPayload(JsonNode roomItems) {
ArrayNode display = objectMapper.createArrayNode();
if (roomItems == null || !roomItems.isArray()) {
return display;
}
for (JsonNode roomItem : roomItems) {
ObjectNode item = objectMapper.createObjectNode();
copyJsonFieldIfPresent(item, roomItem, "room_type_code");
copyJsonFieldIfPresent(item, roomItem, "room_count");
display.add(item);
}
return display;
}
/**
* 生成 trace_items[] 展示快照,只保留 Trace 契约字段。
*/
private ArrayNode v4TraceItemsDisplayPayload(JsonNode traceItems) {
ArrayNode display = objectMapper.createArrayNode();
if (traceItems == null || !traceItems.isArray()) {
return display;
}
for (JsonNode traceItem : traceItems) {
ObjectNode item = objectMapper.createObjectNode();
copyJsonFieldIfPresent(item, traceItem, "item_type");
copyJsonFieldIfPresent(item, traceItem, "text");
copyJsonFieldIfPresent(item, traceItem, "department_code");
copyJsonFieldIfPresent(item, traceItem, "target_room_type_code");
copyJsonFieldIfPresent(item, traceItem, "extra_bed_room_count");
display.add(item);
}
return display;
}
/**
* 按字段名复制 JSON 值,保留 null 语义,但不复制缺失字段。
*/
private void copyJsonFieldIfPresent(ObjectNode target, JsonNode source, String fieldName) {
if (source != null && source.has(fieldName)) {
target.set(fieldName, source.get(fieldName));
}
}
/**
* 归一 Parent split 的 Parent Group key只补齐 adapter 派生用副本,不修改请求原文。
*/

View File

@@ -1081,6 +1081,14 @@ class SuperAgentTaskResultControllerTest {
.contains("att-pay-1")
.contains("payment-slip.jpg")
.doesNotContain("https://oss.example.test");
List<String> displayPayloads = jdbcTemplate.queryForList("""
SELECT display_payload_json
FROM workflow_reservation_v4_task_card
WHERE source_message_id = ?
""", String.class, source.inboxId());
assertThat(displayPayloads).allSatisfy(displayPayload -> assertThat(displayPayload)
.doesNotContain("https://oss.example.test")
.doesNotContain("raw-event-attachment.jpg"));
mockMvc.perform(signedPost(body, "nonce-v4-business-root-replay-001"))
.andExpect(status().isOk())
@@ -3756,6 +3764,13 @@ class SuperAgentTaskResultControllerTest {
"locator_value": "GRP-V4-001"
},
"attachment_ids": ["att-pay-1"],
"evidence_url": "https://oss.example.test/raw-event-attachment.jpg",
"attachments": [
{
"name": "raw-event-attachment.jpg",
"url": "https://oss.example.test/raw-event-attachment.jpg"
}
],
"manual_review": null
}
]