暂存提交
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
+57
@@ -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) {
|
||||
}
|
||||
+12
@@ -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> {
|
||||
}
|
||||
+32
@@ -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, "内部服务令牌无效");
|
||||
}
|
||||
}
|
||||
}
|
||||
+93
@@ -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);
|
||||
}
|
||||
+25
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user