This commit is contained in:
2026-09-29 18:06:18 +08:00
parent 54ad26c607
commit 2c60a29e0f
30 changed files with 384 additions and 139 deletions
@@ -40,6 +40,9 @@ public class BusinessFeeSourceEventService {
|| envelope.targetRegion() != RegionCode.CN) {
throw error("FIN-EVT-001", "跨区费用事件类型、版本或区域无效");
}
if (!EventPayloadHash.matches(EventPayloadCanonicalizer.canonicalize(envelope.payload()), envelope.payloadHash())) {
throw new FinanceEventException("FIN-EVT-002", "事件信封摘要与实际载荷不一致");
}
BusinessFeeSourceChangedV1 source = payload(envelope);
validate(envelope, source);
String sourceSystem = source.sourceSystem().trim().toUpperCase();
@@ -112,6 +112,9 @@ public class WalletCommandEventService {
|| envelope.sourceRegion() != RegionCode.OVERSEAS || envelope.targetRegion() != RegionCode.CN) {
throw new FinanceEventException("FIN-EVT-001", "资金指令类型、版本或区域无效");
}
if (!EventPayloadHash.matches(EventPayloadCanonicalizer.canonicalize(envelope.payload()), envelope.payloadHash())) {
throw new FinanceEventException("FIN-EVT-002", "事件信封摘要与实际载荷不一致");
}
try {
WalletCommandRequestedV1 value = objectMapper.treeToValue(envelope.payload(), WalletCommandRequestedV1.class);
String operation = value.operation() == null ? "" : value.operation().trim().toUpperCase(Locale.ROOT);
@@ -1,6 +1,8 @@
package com.baiyue.finance.service;
import com.baiyue.common.event.EventEnvelope;
import com.baiyue.common.web.EventDeliveryFailure;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.baiyue.common.core.retry.BoundedRetryPolicy;
import com.baiyue.finance.client.WalletResultLastlegClient;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -50,21 +52,25 @@ public class WalletResultOutboxRelay {
"自动投递次数已耗尽(含租约超时)", eventId, worker);
return;
}
boolean requestStarted = false;
try {
EventEnvelope envelope = objectMapper.readValue(String.valueOf(row.get("payload_json")), EventEnvelope.class).requireValid();
requestStarted = true;
var response = client.receive(internalToken, envelope);
if (response == null || !response.successful() || response.data() == null)
throw new IllegalStateException("Lastleg 未确认资金结果事件");
EventDeliveryFailure.requireSuccessful(response);
response.data().requireMatches(envelope);
jdbc.update("UPDATE fin_wallet_event_outbox SET status='PUBLISHED',published_at=NOW(3),locked_by=NULL,locked_at=NULL,last_error=NULL,updated_at=NOW(3) WHERE event_id=? AND status='SENDING' AND locked_by=?",
eventId, worker);
} catch (Exception exception) {
String error = EventDeliveryFailure.diagnostic(exception, internalToken);
boolean permanent = EventDeliveryFailure.isPermanent(exception)
|| (!requestStarted && (exception instanceof JsonProcessingException || exception instanceof IllegalArgumentException));
int retries = failures + 1;
String status = policy.exhausted(retries) ? "DEAD" : "PENDING";
LocalDateTime next = policy.exhausted(retries) ? LocalDateTime.now() : LocalDateTime.now().plus(policy.delayAfterFailure(retries));
String status = permanent || policy.exhausted(retries) ? "DEAD" : "PENDING";
LocalDateTime next = permanent || policy.exhausted(retries) ? LocalDateTime.now() : LocalDateTime.now().plus(policy.delayAfterFailure(retries));
jdbc.update("UPDATE fin_wallet_event_outbox SET status=?,retry_count=?,next_retry_at=?,locked_by=NULL,locked_at=NULL,last_error=?,updated_at=NOW(3) WHERE event_id=? AND status='SENDING' AND locked_by=?",
status, retries, next, safe(exception.getMessage()), eventId, worker);
log.warn("资金结果事件投递失败 eventId={}, reason={}", eventId, exception.getMessage());
status, retries, next, safe(error), eventId, worker);
log.warn("资金结果事件投递失败 eventId={}, reason={}", eventId, error);
}
}
@@ -40,6 +40,18 @@ class WalletCommandEventServiceTest {
when(transactions.getTransaction(any())).thenAnswer(invocation -> mock(TransactionStatus.class));
}
@Test
void rejectsTamperedEnvelopeBeforeOpeningWalletTransaction() {
EventEnvelope valid = envelope("FREEZE", 1L, BigDecimal.TEN);
EventEnvelope tampered = new EventEnvelope(valid.eventId(), valid.eventType(), valid.eventVersion(),
valid.occurredAt(), valid.sourceRegion(), valid.targetRegion(), valid.tenantId(), valid.aggregateType(),
valid.aggregateId(), valid.aggregateVersion(), valid.handoverVersion(), valid.traceId(),
valid.correlationId(), valid.causationId(), valid.payload(), "sha256:" + "0".repeat(64));
assertThatThrownBy(() -> service.consume(tampered)).isInstanceOf(FinanceEventException.class)
.hasMessageContaining("摘要与实际载荷不一致");
verifyNoInteractions(jdbc, wallet);
}
@Test
void freezesWalletAndPublishesCompletedEvent() {
EventEnvelope envelope = envelope("FREEZE", 31L, new BigDecimal("88.50"));