暂存提交

This commit is contained in:
2026-07-30 18:01:36 +08:00
commit 2f4e159356
368 changed files with 20370 additions and 0 deletions
@@ -0,0 +1,55 @@
package com.baiyue.common.event;
import com.fasterxml.jackson.databind.JsonNode;
import java.time.Instant;
/**
* 跨服务和跨区域传输使用的不可变事件信封。
*
* <p>租户 ID 和聚合 ID 使用字符串,避免跨语言传输时发生大整数精度丢失。
* {@code payloadHash} 是原始规范化负载字节的 SHA-256 摘要。</p>
*
* @param eventId 事件全局唯一 ID,使用标准 UUID 或 26 位 ULID
* @param eventType 事件类型,不包含版本号
* @param eventVersion 事件契约版本,从 1 开始
* @param occurredAt 业务事实实际发生时间,使用 UTC 时间点
* @param sourceRegion 事件来源业务单元
* @param targetRegion 事件目标业务单元;国内一期允许与来源区域相同
* @param tenantId 事件所属客户租户 ID
* @param aggregateType 聚合类型
* @param aggregateId 聚合 ID
* @param aggregateVersion 聚合版本,用于阻止乱序事件覆盖新状态
* @param handoverVersion 跨区交接版本;非交接事件可为空
* @param traceId 分布式链路 ID
* @param correlationId 同一业务流程的关联 ID
* @param causationId 直接触发当前事件的上游事件 ID;根事件可为空
* @param payload 事件业务负载
* @param payloadHash 负载摘要,格式为 sha256 加冒号和 64 位十六进制字符
*/
public record EventEnvelope(
String eventId,
String eventType,
int eventVersion,
Instant occurredAt,
RegionCode sourceRegion,
RegionCode targetRegion,
String tenantId,
String aggregateType,
String aggregateId,
long aggregateVersion,
Long handoverVersion,
String traceId,
String correlationId,
String causationId,
JsonNode payload,
String payloadHash
) {
/**
* 校验事件信封,不符合基础契约时抛出异常。
*/
public EventEnvelope requireValid() {
EventEnvelopeValidator.requireValid(this);
return this;
}
}
@@ -0,0 +1,89 @@
package com.baiyue.common.event;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* 事件信封基础校验器。
*
* <p>这里只校验所有事件共有的传输约束,具体事件负载由对应业务消费者按事件版本校验。</p>
*/
public final class EventEnvelopeValidator {
private static final Pattern EVENT_TYPE_PATTERN = Pattern.compile("^[A-Z][A-Za-z0-9]{1,63}$");
private static final Pattern AGGREGATE_TYPE_PATTERN = Pattern.compile("^[A-Z][A-Z0-9_]{1,63}$");
private static final int MAX_ID_LENGTH = 128;
private EventEnvelopeValidator() {
}
/**
* 返回全部基础校验错误;合法时返回空列表。
*/
public static List<String> validate(EventEnvelope envelope) {
List<String> errors = new ArrayList<>();
if (envelope == null) {
return List.of("事件信封不能为空");
}
if (!EventIdentifiers.isSupported(envelope.eventId())) {
errors.add("eventId 必须是标准 UUID 或 26 位 ULID");
}
if (envelope.eventType() == null || !EVENT_TYPE_PATTERN.matcher(envelope.eventType()).matches()) {
errors.add("eventType 必须以大写字母开头且只能包含字母和数字,长度为 2 至 64 位");
}
if (envelope.eventVersion() < 1) {
errors.add("eventVersion 必须大于等于 1");
}
if (envelope.occurredAt() == null) {
errors.add("occurredAt 不能为空");
}
if (envelope.sourceRegion() == null) {
errors.add("sourceRegion 不能为空");
}
if (envelope.targetRegion() == null) {
errors.add("targetRegion 不能为空");
}
requireText(errors, "tenantId", envelope.tenantId(), MAX_ID_LENGTH);
if (envelope.aggregateType() == null
|| !AGGREGATE_TYPE_PATTERN.matcher(envelope.aggregateType()).matches()) {
errors.add("aggregateType 必须使用大写字母、数字或下划线,长度为 2 至 64 位");
}
requireText(errors, "aggregateId", envelope.aggregateId(), MAX_ID_LENGTH);
if (envelope.aggregateVersion() < 0) {
errors.add("aggregateVersion 不能小于 0");
}
if (envelope.handoverVersion() != null && envelope.handoverVersion() < 1) {
errors.add("handoverVersion 存在时必须大于等于 1");
}
requireText(errors, "traceId", envelope.traceId(), MAX_ID_LENGTH);
requireText(errors, "correlationId", envelope.correlationId(), MAX_ID_LENGTH);
if (envelope.causationId() != null && !EventIdentifiers.isSupported(envelope.causationId())) {
errors.add("causationId 存在时必须是标准 UUID 或 26 位 ULID");
}
if (envelope.payload() == null || envelope.payload().isNull()) {
errors.add("payload 不能为空");
}
if (!EventPayloadHash.hasValidFormat(envelope.payloadHash())) {
errors.add("payloadHash 必须使用 sha256 加冒号和 64 位十六进制字符格式");
}
return List.copyOf(errors);
}
/**
* 校验事件信封,存在错误时抛出包含全部原因的异常。
*/
public static void requireValid(EventEnvelope envelope) {
List<String> errors = validate(envelope);
if (!errors.isEmpty()) {
throw new IllegalArgumentException(String.join(";", errors));
}
}
private static void requireText(List<String> errors, String field, String value, int maxLength) {
if (value == null || value.isBlank()) {
errors.add(field + " 不能为空");
} else if (value.length() > maxLength) {
errors.add(field + " 长度不能超过 " + maxLength + " 位");
}
}
}
@@ -0,0 +1,31 @@
package com.baiyue.common.event;
import java.util.UUID;
import java.util.regex.Pattern;
/**
* 事件标识生成与格式判断工具。
*/
public final class EventIdentifiers {
private static final Pattern UUID_PATTERN = Pattern.compile(
"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$");
private static final Pattern ULID_PATTERN = Pattern.compile(
"^[0-7][0-9A-HJKMNP-TV-Z]{25}$", Pattern.CASE_INSENSITIVE);
private EventIdentifiers() {
}
/**
* 生成标准 UUID 字符串。
*/
public static String newUuid() {
return UUID.randomUUID().toString();
}
/**
* 判断字符串是否为标准 UUID 或 26 位 ULID。
*/
public static boolean isSupported(String value) {
return value != null && (UUID_PATTERN.matcher(value).matches() || ULID_PATTERN.matcher(value).matches());
}
}
@@ -0,0 +1,48 @@
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("事件负载不能为空");
}
try {
return OBJECT_MAPPER.writeValueAsBytes(sort(payload));
} catch (JsonProcessingException exception) {
throw new IllegalArgumentException("事件负载无法规范化", exception);
}
}
private static JsonNode sort(JsonNode node) {
if (node.isObject()) {
var names = new java.util.ArrayList<String>();
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;
}
return node;
}
}
@@ -0,0 +1,52 @@
package com.baiyue.common.event;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.Locale;
import java.util.regex.Pattern;
/**
* 事件负载 SHA-256 摘要工具。
*/
public final class EventPayloadHash {
private static final Pattern HASH_PATTERN = Pattern.compile("^sha256:[0-9a-fA-F]{64}$");
private EventPayloadHash() {
}
/**
* 计算调用方提供的规范化负载字节摘要。
*/
public static String sha256(byte[] canonicalPayload) {
if (canonicalPayload == null) {
throw new IllegalArgumentException("规范化负载字节不能为空");
}
try {
byte[] digest = MessageDigest.getInstance("SHA-256").digest(canonicalPayload);
return "sha256:" + HexFormat.of().formatHex(digest);
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("当前运行环境不支持 SHA-256", exception);
}
}
/**
* 判断摘要是否符合约定格式。
*/
public static boolean hasValidFormat(String value) {
return value != null && HASH_PATTERN.matcher(value).matches();
}
/**
* 使用常量时间比较校验规范化负载字节与摘要是否一致。
*/
public static boolean matches(byte[] canonicalPayload, String expectedHash) {
if (canonicalPayload == null || !hasValidFormat(expectedHash)) {
return false;
}
byte[] actual = sha256(canonicalPayload).getBytes(StandardCharsets.US_ASCII);
byte[] expected = expectedHash.toLowerCase(Locale.ROOT).getBytes(StandardCharsets.US_ASCII);
return MessageDigest.isEqual(actual, expected);
}
}
@@ -0,0 +1,20 @@
package com.baiyue.common.event;
/**
* 跨服务和跨区域事件类型常量。
*
* <p>事件版本由事件信封的 {@code eventVersion} 单独表达,类型名称本身不携带版本号。</p>
*/
public final class EventTypes {
public static final String ORDER_APPROVED = "OrderApproved";
public static final String ORDER_CANCELLED = "OrderCancelled";
public static final String FIRST_LEG_SHIPMENT_DEPARTED = "FirstLegShipmentDeparted";
public static final String LAST_MILE_HANDOFF_ACCEPTED = "LastMileHandoffAccepted";
public static final String LAST_MILE_HANDOFF_REJECTED = "LastMileHandoffRejected";
public static final String LAST_MILE_MILESTONE_CHANGED = "LastMileMilestoneChanged";
public static final String LAST_MILE_COMPLETED = "LastMileCompleted";
public static final String TRACKING_MILESTONE_PROJECTED = "TrackingMilestoneProjected";
private EventTypes() {
}
}
@@ -0,0 +1,13 @@
package com.baiyue.common.event;
/**
* 事件来源与目标部署区域。
*
* <p>区域表示独立业务单元,不表示用户当前所在地。</p>
*/
public enum RegionCode {
/** 国内业务单元。 */
CN,
/** 海外业务单元。 */
OVERSEAS
}
@@ -0,0 +1,4 @@
/**
* 国内与海外业务单元共享的稳定事件契约。
*/
package com.baiyue.common.event;