优化
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.event.EventProtocolException;
|
||||
import com.baiyue.common.event.EventDependencyNotReadyException;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import java.time.Instant;
|
||||
|
||||
@RestControllerAdvice
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class EventConsumptionExceptionHandler {
|
||||
@ExceptionHandler({EventProtocolException.class, EventDependencyNotReadyException.class})
|
||||
public ApiResponse<Void> event(IllegalArgumentException failure) {
|
||||
String code = failure instanceof EventDependencyNotReadyException ? "EVT_DEPENDENCY" : "EVT_PROTOCOL";
|
||||
return new ApiResponse<>(code, failure.getMessage(), null, MDC.get("requestId"), MDC.get("traceId"), Instant.now());
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -37,7 +37,7 @@ public class InternalOverseasContainerEventController {
|
||||
case EventTypes.OVERSEAS_CONTAINER_UNLOADED -> consumer.consume(event);
|
||||
case EventTypes.OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED -> detainedConsumer.consume(event);
|
||||
case EventTypes.OVERSEAS_DELIVERY_COMPLETED -> deliveryCompletedConsumer.consume(event);
|
||||
default -> throw new IllegalArgumentException("不支持的 Firstleg 业务事件类型");
|
||||
default -> throw new com.baiyue.common.event.EventProtocolException("不支持的 Firstleg 业务事件类型");
|
||||
};
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
+9
-28
@@ -2,6 +2,8 @@ package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
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.common.event.EventEnvelopeValidator;
|
||||
import com.baiyue.firstleg.client.LastlegEventClient;
|
||||
@@ -50,15 +52,19 @@ public class LoadingCompletedOutboxRelay {
|
||||
repository.dead(row.getId(), workerId, failures, "自动投递次数已耗尽(含租约超时)");
|
||||
continue;
|
||||
}
|
||||
boolean requestStarted = false;
|
||||
try {
|
||||
EventEnvelope event=objectMapper.readValue(row.getPayloadJson(),EventEnvelope.class);
|
||||
EventEnvelopeValidator.requireValid(event);
|
||||
requestStarted = true;
|
||||
var response=client.receive(internalToken,event);
|
||||
requireAcknowledgement(event, response);
|
||||
repository.published(row.getId(),workerId,LocalDateTime.now());
|
||||
} catch(Exception exception) {
|
||||
String error=safeError(exception);
|
||||
if (policy.exhausted(failures + 1)) repository.dead(row.getId(), workerId, failures + 1, error);
|
||||
boolean permanent = EventDeliveryFailure.isPermanent(exception)
|
||||
|| (!requestStarted && (exception instanceof JsonProcessingException || exception instanceof IllegalArgumentException));
|
||||
String error = EventDeliveryFailure.diagnostic(exception, internalToken);
|
||||
if (permanent || policy.exhausted(failures + 1)) repository.dead(row.getId(), workerId, failures + 1, error);
|
||||
else repository.retry(row.getId(), workerId, LocalDateTime.now().plus(policy.delayAfterFailure(failures + 1)), error);
|
||||
log.warn("头程业务事件投递失败: eventType={}, eventKey={}, reason={}",
|
||||
row.getEventType(), row.getEventKey(), error);
|
||||
@@ -68,33 +74,8 @@ public class LoadingCompletedOutboxRelay {
|
||||
|
||||
private void requireAcknowledgement(EventEnvelope event,
|
||||
ApiResponse<com.baiyue.common.event.DeliveryAck> response) {
|
||||
if(response==null) throw new IllegalStateException("Lastleg未返回响应");
|
||||
if(!response.successful()) {
|
||||
throw new IllegalStateException("Lastleg拒绝头程业务事件: code=" + response.code()
|
||||
+ ", message=" + response.message() + ", requestId=" + response.requestId()
|
||||
+ ", traceId=" + response.traceId());
|
||||
}
|
||||
if (response.data() == null) throw new IllegalStateException("Lastleg ACK 缺失");
|
||||
EventDeliveryFailure.requireSuccessful(response);
|
||||
response.data().requireMatches(event);
|
||||
}
|
||||
|
||||
private String safeError(Exception exception) {
|
||||
Throwable diagnostic=diagnosticCause(exception);
|
||||
String message=diagnostic.getMessage();
|
||||
String value=diagnostic.getClass().getSimpleName()
|
||||
+ ((message==null || message.isBlank())?"":": " + message);
|
||||
if(internalToken!=null && !internalToken.isBlank()) value=value.replace(internalToken,"***");
|
||||
value=value.replaceAll("[\\r\\n\\t]+"," ");
|
||||
return value.length()>500?value.substring(0,500):value;
|
||||
}
|
||||
|
||||
private Throwable diagnosticCause(Throwable failure) {
|
||||
Throwable current=failure;
|
||||
while(current.getCause()!=null && current.getCause()!=current
|
||||
&& ("NoFallbackAvailableException".equals(current.getClass().getSimpleName())
|
||||
|| "No fallback available.".equals(current.getMessage()))) {
|
||||
current=current.getCause();
|
||||
}
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
+14
-12
@@ -1,6 +1,8 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventProtocolException;
|
||||
import com.baiyue.common.event.EventDependencyNotReadyException;
|
||||
import com.baiyue.common.event.DeliveryAck;
|
||||
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||
import com.baiyue.common.event.EventPayloadHash;
|
||||
@@ -33,23 +35,23 @@ public class OverseasContainerEventConsumer {
|
||||
OverseasContainerUnloadedV1 payload = payload(event);
|
||||
if (payload.sourceContainerBookingId() == null || payload.sourceContainerBookingId().isBlank()
|
||||
|| !payload.sourceContainerBookingId().equals(event.aggregateId())) {
|
||||
throw new IllegalArgumentException("事件 aggregateId 与 sourceContainerBookingId 不一致");
|
||||
throw new EventProtocolException("事件 aggregateId 与 sourceContainerBookingId 不一致");
|
||||
}
|
||||
if (payload.containerNo() == null || payload.containerNo().isBlank()) {
|
||||
throw new IllegalArgumentException("货柜号不能为空");
|
||||
throw new EventProtocolException("货柜号不能为空");
|
||||
}
|
||||
if (payload.unloadingCompletedAt() == null || payload.unloadingPhotoUploadedAt() == null) {
|
||||
throw new IllegalArgumentException("卸柜完成时间和卸柜照片上传时间不能为空");
|
||||
throw new EventProtocolException("卸柜完成时间和卸柜照片上传时间不能为空");
|
||||
}
|
||||
if (payload.actualEmptyReportedAt() != null
|
||||
&& payload.actualEmptyReportedAt().isBefore(payload.unloadingPhotoUploadedAt())) {
|
||||
throw new IllegalArgumentException("实际报空时间不能早于卸柜照片上传时间");
|
||||
throw new EventProtocolException("实际报空时间不能早于卸柜照片上传时间");
|
||||
}
|
||||
|
||||
ContainerEventInboxEntity existing = inbox.selectById(event.eventId());
|
||||
if (existing != null) {
|
||||
if (!existing.getPayloadHash().equalsIgnoreCase(event.payloadHash())) {
|
||||
throw new IllegalArgumentException("相同 eventId 对应的事件摘要冲突");
|
||||
throw new EventProtocolException("相同 eventId 对应的事件摘要冲突");
|
||||
}
|
||||
if (!"PROCESSED".equals(existing.getStatus())) throw new IllegalStateException("事件尚未完成消费");
|
||||
return DeliveryAck.of(event, DeliveryAck.Outcome.DUPLICATE);
|
||||
@@ -79,20 +81,20 @@ public class OverseasContainerEventConsumer {
|
||||
private void validateEnvelope(EventEnvelope event) {
|
||||
event.requireValid();
|
||||
if (!EventTypes.OVERSEAS_CONTAINER_UNLOADED.equals(event.eventType()) || event.eventVersion() != 1) {
|
||||
throw new IllegalArgumentException("不支持的海外货柜事件");
|
||||
throw new EventProtocolException("不支持的海外货柜事件");
|
||||
}
|
||||
if (event.sourceRegion() != RegionCode.OVERSEAS || event.targetRegion() != RegionCode.CN
|
||||
|| !"CONTAINER_BOOKING".equals(event.aggregateType())) {
|
||||
throw new IllegalArgumentException("海外货柜事件区域或聚合类型无效");
|
||||
throw new EventProtocolException("海外货柜事件区域或聚合类型无效");
|
||||
}
|
||||
if (!EventPayloadHash.matches(EventPayloadCanonicalizer.canonicalize(event.payload()), event.payloadHash())) {
|
||||
throw new IllegalArgumentException("payloadHash 与事件负载不一致");
|
||||
throw new EventProtocolException("payloadHash 与事件负载不一致");
|
||||
}
|
||||
}
|
||||
|
||||
private OverseasContainerUnloadedV1 payload(EventEnvelope event) {
|
||||
try { return objectMapper.treeToValue(event.payload(), OverseasContainerUnloadedV1.class); }
|
||||
catch (Exception exception) { throw new IllegalArgumentException("海外货柜事件负载无效", exception); }
|
||||
catch (Exception exception) { throw new EventProtocolException("海外货柜事件负载无效", exception); }
|
||||
}
|
||||
|
||||
private ContainerEventInboxEntity inbox(EventEnvelope event) {
|
||||
@@ -110,12 +112,12 @@ public class OverseasContainerEventConsumer {
|
||||
try {
|
||||
bookingId = Long.parseLong(sourceContainerBookingId);
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new IllegalArgumentException("国内货柜订舱 ID 无效", exception);
|
||||
throw new EventProtocolException("国内货柜订舱 ID 无效", exception);
|
||||
}
|
||||
ContainerBookingEntity value = containers.selectActiveBookingForUpdate(bookingId);
|
||||
if (value == null) throw new IllegalArgumentException("头程货柜不存在或不可用");
|
||||
if (value == null) throw new EventDependencyNotReadyException("头程货柜不存在或不可用");
|
||||
if (value.getContainerNo() == null || !value.getContainerNo().equals(containerNo)) {
|
||||
throw new IllegalArgumentException("事件货柜号与国内订舱记录不一致");
|
||||
throw new EventProtocolException("事件货柜号与国内订舱记录不一致");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
+13
-10
@@ -2,6 +2,8 @@ package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventProtocolException;
|
||||
import com.baiyue.common.event.EventDependencyNotReadyException;
|
||||
import com.baiyue.common.event.DeliveryAck;
|
||||
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||
import com.baiyue.common.event.EventPayloadHash;
|
||||
@@ -45,12 +47,12 @@ public class OverseasDeliveryCompletedConsumer {
|
||||
long inboundId = positive(payload.sourceInboundId(), "来源入仓单 ID 无效");
|
||||
long tenantId = positive(event.tenantId(), "客户 ID 无效");
|
||||
if (!Objects.equals(event.aggregateId(), payload.sourceInboundId())) {
|
||||
throw new IllegalArgumentException("事件 aggregateId 与来源入仓单 ID 不一致");
|
||||
throw new EventProtocolException("事件 aggregateId 与来源入仓单 ID 不一致");
|
||||
}
|
||||
ContainerEventInboxEntity existing = inbox.selectById(event.eventId());
|
||||
if (existing != null) {
|
||||
if (!existing.getPayloadHash().equalsIgnoreCase(event.payloadHash())) {
|
||||
throw new IllegalArgumentException("相同 eventId 对应的事件摘要冲突");
|
||||
throw new EventProtocolException("相同 eventId 对应的事件摘要冲突");
|
||||
}
|
||||
if (!"PROCESSED".equals(existing.getStatus())) throw new IllegalStateException("事件尚未完成消费");
|
||||
return DeliveryAck.of(event, DeliveryAck.Outcome.DUPLICATE);
|
||||
@@ -65,7 +67,7 @@ public class OverseasDeliveryCompletedConsumer {
|
||||
}
|
||||
InboundOrderEntity inbound = required(inboundId, tenantId, payload);
|
||||
if (!"LOADED".equals(inbound.getProcessStatus()) && !"DELIVERED".equals(inbound.getProcessStatus())) {
|
||||
throw new IllegalArgumentException("来源头程入仓单尚未装柜完成");
|
||||
throw new EventDependencyNotReadyException("来源头程入仓单尚未装柜完成");
|
||||
}
|
||||
if ("LOADED".equals(inbound.getProcessStatus())) {
|
||||
LocalDateTime completedAt = LocalDateTime.ofInstant(payload.completedAt(), ZoneOffset.UTC);
|
||||
@@ -102,10 +104,10 @@ public class OverseasDeliveryCompletedConsumer {
|
||||
if (!EventTypes.OVERSEAS_DELIVERY_COMPLETED.equals(event.eventType()) || event.eventVersion() != 1
|
||||
|| event.sourceRegion() != RegionCode.OVERSEAS || event.targetRegion() != RegionCode.CN
|
||||
|| !"INBOUND_ORDER".equals(event.aggregateType())) {
|
||||
throw new IllegalArgumentException("不支持的海外派送完成事件");
|
||||
throw new EventProtocolException("不支持的海外派送完成事件");
|
||||
}
|
||||
if (!EventPayloadHash.matches(EventPayloadCanonicalizer.canonicalize(event.payload()), event.payloadHash())) {
|
||||
throw new IllegalArgumentException("payloadHash 与事件负载不一致");
|
||||
throw new EventProtocolException("payloadHash 与事件负载不一致");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,22 +116,23 @@ public class OverseasDeliveryCompletedConsumer {
|
||||
OverseasDeliveryCompletedV1 value = objectMapper.treeToValue(event.payload(), OverseasDeliveryCompletedV1.class);
|
||||
if (value.completedAt() == null || !StringUtils.hasText(value.deliveryNo())
|
||||
|| !StringUtils.hasText(value.sourceOrderId()) || !StringUtils.hasText(value.sourceOrderNo())
|
||||
|| !StringUtils.hasText(value.inboundCode())) throw new IllegalArgumentException("字段不完整");
|
||||
|| !StringUtils.hasText(value.inboundCode())) throw new EventProtocolException("字段不完整");
|
||||
return value;
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalArgumentException("海外派送完成事件负载无效", exception);
|
||||
throw new EventProtocolException("海外派送完成事件负载无效", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private InboundOrderEntity required(long inboundId, long tenantId, OverseasDeliveryCompletedV1 payload) {
|
||||
InboundOrderEntity inbound = inboundOrders.selectActiveByIdForUpdate(inboundId);
|
||||
if (inbound == null || !Objects.equals(inbound.getTenantId(), tenantId)
|
||||
if (inbound == null) throw new EventDependencyNotReadyException("来源头程入仓单不存在");
|
||||
if (!Objects.equals(inbound.getTenantId(), tenantId)
|
||||
|| !Objects.equals(inbound.getInboundCode(), payload.inboundCode())
|
||||
|| !Objects.equals(String.valueOf(inbound.getOrderId()), payload.sourceOrderId())
|
||||
|| !Objects.equals(inbound.getOrderNo(), payload.sourceOrderNo())
|
||||
|| (StringUtils.hasText(payload.fbaWarehouseCode()) && StringUtils.hasText(inbound.getDestinationCode())
|
||||
&& !Objects.equals(inbound.getDestinationCode(), payload.fbaWarehouseCode()))) {
|
||||
throw new IllegalArgumentException("事件与来源头程入仓单归属不一致");
|
||||
throw new EventProtocolException("事件与来源头程入仓单归属不一致");
|
||||
}
|
||||
return inbound;
|
||||
}
|
||||
@@ -146,6 +149,6 @@ public class OverseasDeliveryCompletedConsumer {
|
||||
private static long positive(String value, String message) {
|
||||
try { long parsed = Long.parseLong(value); if (parsed > 0) return parsed; }
|
||||
catch (RuntimeException ignored) { }
|
||||
throw new IllegalArgumentException(message);
|
||||
throw new EventProtocolException(message);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-9
@@ -2,6 +2,8 @@ package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventProtocolException;
|
||||
import com.baiyue.common.event.EventDependencyNotReadyException;
|
||||
import com.baiyue.common.event.DeliveryAck;
|
||||
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||
import com.baiyue.common.event.EventPayloadHash;
|
||||
@@ -41,17 +43,17 @@ public class OverseasInboundOrderDetainedConsumer {
|
||||
long inboundId = parsePositive(payload.sourceInboundId(), "来源入仓单 ID 无效");
|
||||
long tenantId = parsePositive(event.tenantId(), "客户 ID 无效");
|
||||
if (!payload.sourceInboundId().equals(event.aggregateId())) {
|
||||
throw new IllegalArgumentException("事件 aggregateId 与来源入仓单 ID 不一致");
|
||||
throw new EventProtocolException("事件 aggregateId 与来源入仓单 ID 不一致");
|
||||
}
|
||||
if (payload.changedAt() == null || payload.inboundCode() == null || payload.inboundCode().isBlank()
|
||||
|| payload.reason() == null || payload.reason().isBlank()) {
|
||||
throw new IllegalArgumentException("暂扣事件缺少入仓码、原因或变更时间");
|
||||
throw new EventProtocolException("暂扣事件缺少入仓码、原因或变更时间");
|
||||
}
|
||||
|
||||
ContainerEventInboxEntity existing = inbox.selectById(event.eventId());
|
||||
if (existing != null) {
|
||||
if (!existing.getPayloadHash().equalsIgnoreCase(event.payloadHash())) {
|
||||
throw new IllegalArgumentException("相同 eventId 对应的事件摘要冲突");
|
||||
throw new EventProtocolException("相同 eventId 对应的事件摘要冲突");
|
||||
}
|
||||
if (!"PROCESSED".equals(existing.getStatus())) throw new IllegalStateException("事件尚未完成消费");
|
||||
return DeliveryAck.of(event, DeliveryAck.Outcome.DUPLICATE);
|
||||
@@ -87,23 +89,23 @@ public class OverseasInboundOrderDetainedConsumer {
|
||||
if (!EventTypes.OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED.equals(event.eventType())
|
||||
|| event.eventVersion() != 1 || event.sourceRegion() != RegionCode.OVERSEAS
|
||||
|| event.targetRegion() != RegionCode.CN || !"INBOUND_ORDER".equals(event.aggregateType())) {
|
||||
throw new IllegalArgumentException("不支持的海外入仓单暂扣事件");
|
||||
throw new EventProtocolException("不支持的海外入仓单暂扣事件");
|
||||
}
|
||||
if (!EventPayloadHash.matches(EventPayloadCanonicalizer.canonicalize(event.payload()), event.payloadHash())) {
|
||||
throw new IllegalArgumentException("payloadHash 与事件负载不一致");
|
||||
throw new EventProtocolException("payloadHash 与事件负载不一致");
|
||||
}
|
||||
}
|
||||
|
||||
private OverseasInboundOrderDetainedChangedV1 payload(EventEnvelope event) {
|
||||
try { return objectMapper.treeToValue(event.payload(), OverseasInboundOrderDetainedChangedV1.class); }
|
||||
catch (Exception exception) { throw new IllegalArgumentException("海外入仓单暂扣事件负载无效", exception); }
|
||||
catch (Exception exception) { throw new EventProtocolException("海外入仓单暂扣事件负载无效", exception); }
|
||||
}
|
||||
|
||||
private InboundOrderEntity requiredInbound(long inboundId, long tenantId, String inboundCode) {
|
||||
InboundOrderEntity value = inboundOrders.selectActiveByIdForUpdate(inboundId);
|
||||
if (value == null) throw new IllegalArgumentException("来源头程入仓单不存在");
|
||||
if (value == null) throw new EventDependencyNotReadyException("来源头程入仓单不存在");
|
||||
if (!Objects.equals(value.getTenantId(), tenantId) || !Objects.equals(value.getInboundCode(), inboundCode)) {
|
||||
throw new IllegalArgumentException("事件与来源头程入仓单归属不一致");
|
||||
throw new EventProtocolException("事件与来源头程入仓单归属不一致");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -130,7 +132,7 @@ public class OverseasInboundOrderDetainedConsumer {
|
||||
private long parsePositive(String value, String message) {
|
||||
try { long parsed = Long.parseLong(value); if (parsed > 0) return parsed; }
|
||||
catch (RuntimeException ignored) { }
|
||||
throw new IllegalArgumentException(message);
|
||||
throw new EventProtocolException(message);
|
||||
}
|
||||
|
||||
private long parseOperatorId(String value) {
|
||||
|
||||
Reference in New Issue
Block a user