146 lines
7.2 KiB
Java
146 lines
7.2 KiB
Java
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.baiyue.eventbridge.metrics.EventBridgeMetrics;
|
|
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 jakarta.annotation.Resource;
|
|
|
|
import java.time.Instant;
|
|
import java.time.LocalDateTime;
|
|
import java.time.temporal.ChronoUnit;
|
|
import java.util.Objects;
|
|
|
|
/**
|
|
* 入站事件校验与幂等落库服务。
|
|
*/
|
|
@Service
|
|
public class EventInboxService {
|
|
@Resource private InboxStore inboxStore;
|
|
@Resource private ObjectMapper objectMapper;
|
|
@Resource private EventBridgeMetrics metrics;
|
|
|
|
@Value("${baiyue.region:CN}")
|
|
private String 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);
|
|
}
|
|
String idempotencyKey = idempotencyKey(envelope);
|
|
BridgeInbox sameBusinessEvent = inboxStore.findByIdempotencyKey(idempotencyKey);
|
|
if (sameBusinessEvent != null) {
|
|
return duplicateBusinessEventOrConflict(sameBusinessEvent, envelope);
|
|
}
|
|
BridgeInbox inbox = toInbox(envelope);
|
|
try {
|
|
inboxStore.insert(inbox);
|
|
metrics.recordReceive(envelope, EventBridgeMetrics.ReceiveResult.RECEIVED);
|
|
return new EventReceiveResult(envelope.eventId(), "已接收", inbox.getReceivedAt());
|
|
} catch (DuplicateKeyException exception) {
|
|
BridgeInbox concurrent = inboxStore.findByEventId(envelope.eventId());
|
|
if (concurrent == null) concurrent = inboxStore.findByIdempotencyKey(idempotencyKey);
|
|
if (concurrent == null) {
|
|
throw exception;
|
|
}
|
|
return Objects.equals(concurrent.getEventId(), envelope.eventId())
|
|
? duplicateOrConflict(concurrent, envelope)
|
|
: duplicateBusinessEventOrConflict(concurrent, envelope);
|
|
}
|
|
}
|
|
|
|
private EventReceiveResult duplicateBusinessEventOrConflict(BridgeInbox existing, EventEnvelope envelope) {
|
|
if (!sameBusinessEvent(existing, envelope)) {
|
|
metrics.recordReceive(envelope, EventBridgeMetrics.ReceiveResult.CONFLICT);
|
|
throw new IllegalArgumentException("相同eventKey对应的事件负载不一致,拒绝柜号或事实漂移");
|
|
}
|
|
metrics.recordReceive(envelope, EventBridgeMetrics.ReceiveResult.DUPLICATE);
|
|
return new EventReceiveResult(existing.getEventId(), "重复", existing.getReceivedAt());
|
|
}
|
|
|
|
private EventReceiveResult duplicateOrConflict(BridgeInbox existing, EventEnvelope envelope) {
|
|
if (!sameEnvelope(existing, envelope)) {
|
|
metrics.recordReceive(envelope, EventBridgeMetrics.ReceiveResult.CONFLICT);
|
|
throw new IllegalArgumentException("相同 eventId 对应的事件信封或 payloadHash 不一致,拒绝覆盖既有事件");
|
|
}
|
|
metrics.recordReceive(envelope, EventBridgeMetrics.ReceiveResult.DUPLICATE);
|
|
return new EventReceiveResult(envelope.eventId(), "重复", existing.getReceivedAt());
|
|
}
|
|
|
|
private boolean sameEnvelope(BridgeInbox existing, EventEnvelope envelope) {
|
|
return sameBusinessEvent(existing, envelope)
|
|
&& Objects.equals(normalizeOccurredAt(existing.getOccurredAt()), normalizeOccurredAt(envelope.occurredAt()))
|
|
&& Objects.equals(existing.getTraceId(), envelope.traceId())
|
|
&& Objects.equals(existing.getCausationId(), envelope.causationId());
|
|
}
|
|
|
|
private boolean sameBusinessEvent(BridgeInbox existing, EventEnvelope envelope) {
|
|
return existing.getPayloadHash() != null
|
|
&& existing.getPayloadHash().equalsIgnoreCase(envelope.payloadHash())
|
|
&& Objects.equals(existing.getEventType(), envelope.eventType())
|
|
&& Objects.equals(existing.getEventVersion(), envelope.eventVersion())
|
|
&& Objects.equals(existing.getSourceRegion(), envelope.sourceRegion().name())
|
|
&& Objects.equals(existing.getTargetRegion(), envelope.targetRegion().name())
|
|
&& Objects.equals(existing.getCustomerId(), envelope.tenantId())
|
|
&& Objects.equals(existing.getAggregateType(), envelope.aggregateType())
|
|
&& Objects.equals(existing.getAggregateId(), envelope.aggregateId())
|
|
&& Objects.equals(existing.getAggregateVersion(), envelope.aggregateVersion())
|
|
&& Objects.equals(existing.getHandoverVersion(), envelope.handoverVersion())
|
|
&& Objects.equals(existing.getCorrelationId(), envelope.correlationId());
|
|
}
|
|
|
|
private BridgeInbox toInbox(EventEnvelope envelope) {
|
|
BridgeInbox inbox = new BridgeInbox();
|
|
inbox.setEventId(envelope.eventId());
|
|
inbox.setIdempotencyKey(idempotencyKey(envelope));
|
|
inbox.setEventType(envelope.eventType());
|
|
inbox.setEventVersion(envelope.eventVersion());
|
|
inbox.setSourceRegion(envelope.sourceRegion().name());
|
|
inbox.setTargetRegion(envelope.targetRegion().name());
|
|
inbox.setCustomerId(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(normalizeOccurredAt(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;
|
|
}
|
|
|
|
private static Instant normalizeOccurredAt(Instant value) {
|
|
return value == null ? null : value.truncatedTo(ChronoUnit.MILLIS);
|
|
}
|
|
|
|
private static String idempotencyKey(EventEnvelope envelope) {
|
|
return envelope.sourceRegion() + ":" + envelope.correlationId();
|
|
}
|
|
}
|