收口V4业务卡展示字段白名单
This commit is contained in:
@@ -455,6 +455,7 @@ RESERVATION_ROOMING_LIST_GENERATE
|
||||
- 普通任务切换订单接口继续后置。
|
||||
- M002 V4 入站解析与数据模型基线已完成第一版:后端可接收 `source_message + order_contexts[] + message_events[]`,识别 `NEW_BOOKING`、`UPDATE_BOOKING`、`CANCEL_BOOKING`、`TRACE_RESERVATION_NOTES`、`ROOMING_LIST`、`PAYMENT`,并保存 V4 原始 payload、`route_code`、系统处理分类和 `field_contract_version=20260718-v4`。前端暂不需要直接调用 V4 回调接口。
|
||||
- M002 V4 CP4 已完成入站写入新模型:普通 V4 业务包会写入 V4 订单任务、来源邮件展示卡、Basic Information 卡和业务卡;但 V4 工作台、订单任务详情、来源通知详情和卡片确认接口还没开放,前端当前不要调用 CP2 草案路径。
|
||||
- V4 任务卡的 `display_payload_json` 只保留后端白名单展示字段;`ai_payload_json` 才包含完整 SuperAgent 原始 event。后续 V4 查询接口不得把 `ai_payload_json`、附件 URL 或 raw evidence 直接给普通页面渲染。
|
||||
- V4 可映射 event 现阶段仍保留现有任务详情结构作为过渡兼容;任务详情中若出现 `field_contract_version=20260718-v4` 或 AI payload 内的 `v4_source_message`、`v4_order_context`、`v4_message_event`,前端第一版只读展示即可,不要据此假定完整 V4 多卡页面已经完成。
|
||||
- V4 `PAYMENT.attachment_ids[]` 不匹配、`UPDATE_BOOKING` 携带 `rate_code` 等问题会出现在任务详情同批次的 `adapter_contract_errors[]` 只读诊断块中,不展示保存、确认、执行或重试按钮。
|
||||
- V4 包级契约错误只会保存在 AI transition 中,不会出现在普通任务列表;V4 event 级契约错误如果同批次存在其它业务任务,前端仍按任务详情里的 `adapter_contract_errors[]` 只读展示诊断信息。
|
||||
|
||||
@@ -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