收口V4业务卡展示字段白名单
This commit is contained in:
@@ -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 派生用副本,不修改请求原文。
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user