package com.baiyue.common.event; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; /** * 将 JSON 负载转换为稳定字节序列,避免对象字段顺序不同导致摘要不一致。 */ public final class EventPayloadCanonicalizer { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); private EventPayloadCanonicalizer() { } /** * 递归排序对象字段并输出 UTF-8 JSON 字节。 */ public static byte[] canonicalize(JsonNode payload) { if (payload == null || payload.isNull()) { throw new IllegalArgumentException("事件负载不能为空"); } return write(sort(payload)); } /** 仅用于接收规范化数字规则上线前已经生成的历史摘要。 */ public static byte[] canonicalizeLegacy(JsonNode payload) { if (payload == null || payload.isNull()) { throw new IllegalArgumentException("事件负载不能为空"); } return write(sortLegacy(payload)); } private static byte[] write(JsonNode payload) { try { return OBJECT_MAPPER.writeValueAsBytes(payload); } catch (JsonProcessingException exception) { throw new IllegalArgumentException("事件负载无法规范化", exception); } } private static JsonNode sort(JsonNode node) { if (node.isObject()) { var names = new java.util.ArrayList(); node.fieldNames().forEachRemaining(names::add); names.sort(String::compareTo); var result = OBJECT_MAPPER.createObjectNode(); names.forEach(name -> result.set(name, sort(node.get(name)))); return result; } if (node.isArray()) { var result = OBJECT_MAPPER.createArrayNode(); node.forEach(child -> result.add(sort(child))); return result; } if (node.isNumber()) { return com.fasterxml.jackson.databind.node.DecimalNode.valueOf(node.decimalValue().stripTrailingZeros()); } return node; } private static JsonNode sortLegacy(JsonNode node) { if (node.isObject()) { var names = new java.util.ArrayList(); node.fieldNames().forEachRemaining(names::add); names.sort(String::compareTo); var result = OBJECT_MAPPER.createObjectNode(); names.forEach(name -> result.set(name, sortLegacy(node.get(name)))); return result; } if (node.isArray()) { var result = OBJECT_MAPPER.createArrayNode(); node.forEach(child -> result.add(sortLegacy(child))); return result; } return node; } }