暂存提交

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,16 @@
package com.baiyue.eventbridge;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 跨区域事件桥启动入口。
*/
@MapperScan("com.baiyue.eventbridge.mapper")
@SpringBootApplication
public class EventBridgeApplication {
public static void main(String[] args) {
SpringApplication.run(EventBridgeApplication.class, args);
}
}
@@ -0,0 +1,57 @@
package com.baiyue.eventbridge.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.event.EventEnvelope;
import com.baiyue.eventbridge.dto.BridgeStatus;
import com.baiyue.eventbridge.dto.EventReceiveResult;
import com.baiyue.eventbridge.service.EventInboxService;
import com.baiyue.eventbridge.security.InternalServiceTokenGuard;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 事件桥内部接口。生产环境必须在网关或基础设施层增加 mTLS 与来源白名单。
*/
@Tag(name = "事件桥内部接口", description = "跨区域事件的幂等接收与能力状态")
@RestController
@RequestMapping("/internal/event-bridge")
@RequiredArgsConstructor
public class EventBridgeController {
private final EventInboxService eventInboxService;
private final InternalServiceTokenGuard internalServiceTokenGuard;
@Value("${baiyue.region:CN}")
private String region;
@Operation(summary = "幂等接收事件", description = "当前仅完成数据库接收骨架,不代表生产跨区传输和 mTLS 已开通")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "事件已接收或判定为重复"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "内部服务令牌无效"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "503", description = "内部服务令牌未配置,接收接口关闭")
})
@PostMapping("/events")
public ApiResponse<EventReceiveResult> receive(
@Parameter(description = "内部服务共享令牌")
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@RequestBody EventEnvelope envelope) {
internalServiceTokenGuard.verify(token);
return ApiResponse.success(eventInboxService.receive(envelope));
}
@Operation(summary = "查询事件桥能力状态")
@ApiResponses(@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功"))
@GetMapping("/status")
public ApiResponse<BridgeStatus> status() {
return ApiResponse.success(new BridgeStatus("运行中", region, true, false));
}
}
@@ -0,0 +1,36 @@
package com.baiyue.eventbridge.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
import java.time.Instant;
/**
* 事件桥入站幂等记录。
*/
@Data
@TableName("bridge_inbox")
public class BridgeInbox {
@TableId
private String eventId;
private String idempotencyKey;
private String eventType;
private Integer eventVersion;
private String sourceRegion;
private String targetRegion;
private String tenantId;
private String aggregateType;
private String aggregateId;
private Long aggregateVersion;
private Long handoverVersion;
private String traceId;
private String correlationId;
private String causationId;
private Instant occurredAt;
private String payloadHash;
private String payloadJson;
private String status;
private LocalDateTime receivedAt;
}
@@ -0,0 +1,14 @@
package com.baiyue.eventbridge.dto;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* 事件桥能力状态,不代表生产网络链路已经开通。
*/
@Schema(description = "事件桥能力状态")
public record BridgeStatus(
@Schema(description = "服务状态") String status,
@Schema(description = "当前区域") String region,
@Schema(description = "数据库幂等接收是否启用") boolean inboxEnabled,
@Schema(description = "生产级跨区传输是否启用") boolean productionTransportEnabled) {
}
@@ -0,0 +1,15 @@
package com.baiyue.eventbridge.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
/**
* 事件接收结果。
*/
@Schema(description = "事件接收结果")
public record EventReceiveResult(
@Schema(description = "事件标识") String eventId,
@Schema(description = "接收结果:已接收或重复") String result,
@Schema(description = "接收时间") LocalDateTime receivedAt) {
}
@@ -0,0 +1,12 @@
package com.baiyue.eventbridge.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.eventbridge.domain.BridgeInbox;
import org.apache.ibatis.annotations.Mapper;
/**
* 入站事件持久化映射。
*/
@Mapper
public interface BridgeInboxMapper extends BaseMapper<BridgeInbox> {
}
@@ -0,0 +1,32 @@
package com.baiyue.eventbridge.security;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
/**
* 内部事件接收接口的共享令牌校验器;未配置令牌时默认关闭接收。
*/
@Component
public class InternalServiceTokenGuard {
private final String expectedToken;
public InternalServiceTokenGuard(@Value("${baiyue.security.internal-service-token:}") String expectedToken) {
this.expectedToken = expectedToken;
}
public void verify(String actualToken) {
if (expectedToken == null || expectedToken.isBlank()) {
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "内部事件接收令牌未配置,接口已关闭");
}
byte[] expected = expectedToken.getBytes(StandardCharsets.UTF_8);
byte[] actual = actualToken == null ? new byte[0] : actualToken.getBytes(StandardCharsets.UTF_8);
if (!MessageDigest.isEqual(expected, actual)) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "内部服务令牌无效");
}
}
}
@@ -0,0 +1,93 @@
package com.baiyue.eventbridge.service;
import com.baiyue.common.event.EventEnvelope;
import com.baiyue.common.event.EventPayloadCanonicalizer;
import com.baiyue.common.event.EventPayloadHash;
import com.baiyue.eventbridge.domain.BridgeInbox;
import com.baiyue.eventbridge.dto.EventReceiveResult;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
/**
* 入站事件校验与幂等落库服务。
*/
@Service
public class EventInboxService {
private final InboxStore inboxStore;
private final ObjectMapper objectMapper;
private final String localRegion;
public EventInboxService(InboxStore inboxStore, ObjectMapper objectMapper,
@Value("${baiyue.region:CN}") String localRegion) {
this.inboxStore = inboxStore;
this.objectMapper = objectMapper;
this.localRegion = localRegion;
}
@Transactional
public EventReceiveResult receive(EventEnvelope envelope) {
envelope.requireValid();
if (!envelope.targetRegion().name().equalsIgnoreCase(localRegion)) {
throw new IllegalArgumentException("事件目标区域与当前事件桥区域不一致");
}
if (!EventPayloadHash.matches(EventPayloadCanonicalizer.canonicalize(envelope.payload()), envelope.payloadHash())) {
throw new IllegalArgumentException("payloadHash 与规范化事件负载不一致");
}
BridgeInbox existing = inboxStore.findByEventId(envelope.eventId());
if (existing != null) {
return duplicateOrConflict(existing, envelope);
}
BridgeInbox inbox = toInbox(envelope);
try {
inboxStore.insert(inbox);
return new EventReceiveResult(envelope.eventId(), "已接收", inbox.getReceivedAt());
} catch (DuplicateKeyException exception) {
BridgeInbox concurrent = inboxStore.findByEventId(envelope.eventId());
if (concurrent == null) {
throw exception;
}
return duplicateOrConflict(concurrent, envelope);
}
}
private EventReceiveResult duplicateOrConflict(BridgeInbox existing, EventEnvelope envelope) {
if (!existing.getPayloadHash().equalsIgnoreCase(envelope.payloadHash())) {
throw new IllegalArgumentException("相同 eventId 对应的 payloadHash 不一致,拒绝覆盖既有事件");
}
return new EventReceiveResult(envelope.eventId(), "重复", existing.getReceivedAt());
}
private BridgeInbox toInbox(EventEnvelope envelope) {
BridgeInbox inbox = new BridgeInbox();
inbox.setEventId(envelope.eventId());
inbox.setIdempotencyKey(envelope.sourceRegion() + ":" + envelope.eventId());
inbox.setEventType(envelope.eventType());
inbox.setEventVersion(envelope.eventVersion());
inbox.setSourceRegion(envelope.sourceRegion().name());
inbox.setTargetRegion(envelope.targetRegion().name());
inbox.setTenantId(envelope.tenantId());
inbox.setAggregateType(envelope.aggregateType());
inbox.setAggregateId(envelope.aggregateId());
inbox.setAggregateVersion(envelope.aggregateVersion());
inbox.setHandoverVersion(envelope.handoverVersion());
inbox.setTraceId(envelope.traceId());
inbox.setCorrelationId(envelope.correlationId());
inbox.setCausationId(envelope.causationId());
inbox.setOccurredAt(envelope.occurredAt());
inbox.setPayloadHash(envelope.payloadHash().toLowerCase(java.util.Locale.ROOT));
try {
inbox.setPayloadJson(objectMapper.writeValueAsString(envelope.payload()));
} catch (JsonProcessingException exception) {
throw new IllegalArgumentException("事件负载无法序列化", exception);
}
inbox.setStatus("RECEIVED");
inbox.setReceivedAt(LocalDateTime.now());
return inbox;
}
}
@@ -0,0 +1,11 @@
package com.baiyue.eventbridge.service;
import com.baiyue.eventbridge.domain.BridgeInbox;
/**
* 入站幂等存储边界,便于隔离数据库实现并进行单元测试。
*/
public interface InboxStore {
BridgeInbox findByEventId(String eventId);
void insert(BridgeInbox inbox);
}
@@ -0,0 +1,25 @@
package com.baiyue.eventbridge.service;
import com.baiyue.eventbridge.domain.BridgeInbox;
import com.baiyue.eventbridge.mapper.BridgeInboxMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
/**
* 基于 MyBatis-Plus 的入站幂等存储。
*/
@Repository
@RequiredArgsConstructor
public class MybatisInboxStore implements InboxStore {
private final BridgeInboxMapper mapper;
@Override
public BridgeInbox findByEventId(String eventId) {
return mapper.selectById(eventId);
}
@Override
public void insert(BridgeInbox inbox) {
mapper.insert(inbox);
}
}