This commit is contained in:
2026-08-20 15:58:25 +08:00
parent cd34cd426e
commit 2847ad1b2d
45 changed files with 921 additions and 68 deletions
@@ -0,0 +1,56 @@
package com.baiyue.lastleg.service;
import com.baiyue.lastleg.entity.LastlegEventOutbox;
import com.baiyue.lastleg.mapper.LastlegEventOutboxMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.lang.management.ManagementFactory;
import java.time.LocalDateTime;
import java.util.UUID;
@Component
@Slf4j
public class ContainerUnloadOutboxRelay {
private static final int MAX_ATTEMPTS = 12;
@Resource private LastlegEventOutboxMapper outbox;
@Resource private EventBridgePublisher publisher;
private final String worker = ManagementFactory.getRuntimeMXBean().getName() + ':' + UUID.randomUUID();
@Scheduled(fixedDelayString = "${baiyue.container-unload-relay.fixed-delay:3000}")
public void relay() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime staleBefore = now.minusMinutes(2);
for (Long id : outbox.relayCandidates(50, MAX_ATTEMPTS, now, staleBefore)) {
relayOne(id, now, staleBefore);
}
}
void relayOne(long id, LocalDateTime now, LocalDateTime staleBefore) {
if (outbox.claim(id, worker, now, staleBefore) != 1) return;
LastlegEventOutbox row = outbox.claimed(id, worker);
if (row == null) return;
try {
publisher.publish(row.getPayloadJson());
outbox.markPublished(id, worker, LocalDateTime.now());
} catch (Exception exception) {
int attempt = row.getRetryCount() == null ? 1 : row.getRetryCount() + 1;
boolean exhausted = attempt >= MAX_ATTEMPTS;
LocalDateTime retryAt = exhausted ? null : LocalDateTime.now().plusSeconds(backoffSeconds(attempt));
outbox.markFailed(id, worker, exhausted ? "FAILED" : "PENDING", retryAt,
abbreviate(exception.getMessage()));
log.warn("海外卸柜事件投递失败 id={}, attempt={}, reason={}", id, attempt, exception.getMessage());
}
}
private static long backoffSeconds(int attempt) {
return Math.min(900L, 5L << Math.min(Math.max(attempt - 1, 0), 8));
}
private static String abbreviate(String value) {
String safe = value == null || value.isBlank() ? "未知投递错误" : value;
return safe.length() <= 1000 ? safe : safe.substring(0, 1000);
}
}
@@ -0,0 +1,39 @@
package com.baiyue.lastleg.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
@Component
public class EventBridgePublisher {
@Resource private ObjectMapper objectMapper;
@Value("${baiyue.clients.event-bridge.url:http://127.0.0.1:9312}") private String baseUrl;
@Value("${baiyue.security.internal-service-token:}") private String serviceToken;
private HttpClient httpClient;
@PostConstruct
void initialize() {
httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build();
}
public void publish(String envelopeJson) throws Exception {
HttpRequest request = HttpRequest.newBuilder(URI.create(baseUrl + "/internal/event-bridge/events"))
.timeout(Duration.ofSeconds(10)).header("Content-Type", "application/json")
.header("X-Internal-Service-Token", serviceToken)
.POST(HttpRequest.BodyPublishers.ofString(envelopeJson)).build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
JsonNode body = objectMapper.readTree(response.body());
if (response.statusCode() / 100 != 2 || body == null || !"200".equals(body.path("code").asText())) {
throw new IllegalStateException("事件桥接收失败: HTTP " + response.statusCode());
}
}
}
@@ -120,8 +120,7 @@ public class LoadingCompletedConsumer {
order.setVolumeWeightKg(source.items().stream().map(FirstLegLoadingPlanCompletedV1.Item::dimensionalWeightKg)
.filter(Objects::nonNull).reduce(BigDecimal.ZERO,BigDecimal::add));
order.setCurrentStatus("PENDING_ASSIGNMENT"); order.setSourceEventId(event.eventId());
order.setSourceLoadingPlanId(payload.loadingPlanId()); order.setSourceContainerBookingId(payload.containerBookingId());
order.setSourceLoadingPlanVersion(event.aggregateVersion());
order.setSourceLoadingPlanId(payload.loadingPlanId()); order.setSourceLoadingPlanVersion(event.aggregateVersion());
order.setSourceHandoverVersion(event.handoverVersion()); order.setSourcePayloadHash(orderHash);
order.setCreatedAt(LocalDateTime.now()); order.setUpdatedAt(LocalDateTime.now());
try {