优化
This commit is contained in:
@@ -5,12 +5,17 @@ import com.baiyue.common.event.DeliveryAck;
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import com.baiyue.common.web.EventDeliveryFailure;
|
||||
import java.util.Set;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Dispatches each supported business event directly to its owning service. */
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DirectEventClient {
|
||||
private static final Set<String> FINANCE_PERMANENT_CODES = Set.of(
|
||||
"FIN-EVT-001", "FIN-EVT-002", "FIN-EVT-004", "FIN-EVT-007", "FIN-EVT-008",
|
||||
"FIN-EVT-012", "FIN-EVT-015", "FIN-EVT-016", "FIN-EVT-401");
|
||||
private final FirstlegEventClient firstleg;
|
||||
private final FinanceBusinessEventClient finance;
|
||||
|
||||
@@ -20,9 +25,13 @@ public class DirectEventClient {
|
||||
case EventTypes.OVERSEAS_CONTAINER_UNLOADED,
|
||||
EventTypes.OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED,
|
||||
EventTypes.OVERSEAS_DELIVERY_COMPLETED -> firstleg.receive(token, event);
|
||||
case EventTypes.WALLET_COMMAND_REQUESTED -> finance.wallet(token, event);
|
||||
case EventTypes.BUSINESS_FEE_SOURCE_CHANGED -> finance.fee(token, event);
|
||||
default -> throw new IllegalArgumentException("Unsupported outbound event type: " + event.eventType());
|
||||
case EventTypes.WALLET_COMMAND_REQUESTED -> checkedFinance(finance.wallet(token, event));
|
||||
case EventTypes.BUSINESS_FEE_SOURCE_CHANGED -> checkedFinance(finance.fee(token, event));
|
||||
default -> throw new EventDeliveryFailure(true, "Unsupported outbound event type: " + event.eventType());
|
||||
};
|
||||
}
|
||||
private ApiResponse<DeliveryAck> checkedFinance(ApiResponse<DeliveryAck> response) {
|
||||
EventDeliveryFailure.requireSuccessful(response, FINANCE_PERMANENT_CODES);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.lastleg.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
@@ -73,7 +73,7 @@ public class LastlegOrderController {
|
||||
} else if (EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED.equals(event.eventType())) {
|
||||
consumed = consumer.consume(event);
|
||||
} else {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Lastleg 不消费该事件类型: " + event.eventType());
|
||||
throw new com.baiyue.common.event.EventProtocolException("Lastleg 不消费该事件类型: " + event.eventType());
|
||||
}
|
||||
return ApiResponse.success(consumed);
|
||||
}
|
||||
|
||||
+10
-4
@@ -1,6 +1,8 @@
|
||||
package com.baiyue.lastleg.financefee.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.lastleg.client.DirectEventClient;
|
||||
import com.baiyue.lastleg.entity.LastlegEventOutbox;
|
||||
@@ -46,17 +48,21 @@ public class OverseasFeeOutboxRelay {
|
||||
repository.dead(row.getId(), worker, failures, "自动投递次数已耗尽(含租约超时)");
|
||||
continue;
|
||||
}
|
||||
boolean requestStarted = false;
|
||||
try {
|
||||
EventEnvelope envelope = objectMapper.readValue(row.getPayloadJson(), EventEnvelope.class).requireValid();
|
||||
requestStarted = true;
|
||||
var response = client.receive(token, envelope);
|
||||
if (response == null || !response.successful() || response.data() == null)
|
||||
throw new IllegalStateException("Finance未确认费用事件");
|
||||
EventDeliveryFailure.requireSuccessful(response);
|
||||
response.data().requireMatches(envelope);
|
||||
repository.published(row.getId(), worker, LocalDateTime.now(ZoneOffset.UTC));
|
||||
} catch (Exception exception) {
|
||||
String error = EventDeliveryFailure.diagnostic(exception, token);
|
||||
boolean permanent = EventDeliveryFailure.isPermanent(exception)
|
||||
|| (!requestStarted && (exception instanceof JsonProcessingException || exception instanceof IllegalArgumentException));
|
||||
int retries = failures + 1;
|
||||
if (policy.exhausted(retries)) repository.dead(row.getId(), worker, retries, exception.getMessage());
|
||||
else repository.retry(row.getId(), worker, retries, LocalDateTime.now(ZoneOffset.UTC).plus(policy.delayAfterFailure(retries)), exception.getMessage());
|
||||
if (permanent || policy.exhausted(retries)) repository.dead(row.getId(), worker, retries, error);
|
||||
else repository.retry(row.getId(), worker, retries, LocalDateTime.now(ZoneOffset.UTC).plus(policy.delayAfterFailure(retries)), error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-9
@@ -1,6 +1,8 @@
|
||||
package com.baiyue.lastleg.inboundplan.pda.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.common.event.DeliveryAck;
|
||||
import com.baiyue.common.event.EventEnvelopeValidator;
|
||||
@@ -51,29 +53,29 @@ public class InboundUnloadCompletedOutboxRelay {
|
||||
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) {
|
||||
if (policy.exhausted(failures + 1)) repository.dead(row.getId(), workerId, failures + 1, exception.getMessage());
|
||||
String error = EventDeliveryFailure.diagnostic(exception, internalToken);
|
||||
boolean permanent = EventDeliveryFailure.isPermanent(exception)
|
||||
|| (!requestStarted && (exception instanceof JsonProcessingException || exception instanceof IllegalArgumentException));
|
||||
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)),
|
||||
exception.getMessage());
|
||||
log.warn("海外到国内事件投递失败: eventKey={}, reason={}", row.getEventKey(), exception.getMessage());
|
||||
error);
|
||||
log.warn("海外到国内事件投递失败: eventKey={}, reason={}", row.getEventKey(), error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void requireAcknowledgement(EventEnvelope event,
|
||||
com.baiyue.common.core.api.ApiResponse<DeliveryAck> response) {
|
||||
if (response == null) throw new IllegalStateException("消费服务返回空响应");
|
||||
if (!response.successful()) {
|
||||
throw new IllegalStateException("消费服务拒绝海外事件: code=" + response.code()
|
||||
+ ", message=" + response.message() + ", requestId=" + response.requestId());
|
||||
}
|
||||
if (response.data() == null) throw new IllegalStateException("消费服务成功响应缺少确认数据");
|
||||
EventDeliveryFailure.requireSuccessful(response);
|
||||
response.data().requireMatches(event);
|
||||
}
|
||||
}
|
||||
|
||||
+40
-40
@@ -55,23 +55,23 @@ public class LoadingCompletedConsumer {
|
||||
envelope.requireValid();
|
||||
if (!EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED.equals(envelope.eventType())
|
||||
|| (envelope.eventVersion() != 1 && envelope.eventVersion() != 2 && envelope.eventVersion() != 3))
|
||||
throw new IllegalArgumentException("不支持的尾程交接事件");
|
||||
throw new EventProtocolException("不支持的尾程交接事件");
|
||||
if (envelope.sourceRegion() != RegionCode.CN || envelope.targetRegion() != RegionCode.OVERSEAS)
|
||||
throw new IllegalArgumentException("尾程交接事件区域无效");
|
||||
throw new EventProtocolException("尾程交接事件区域无效");
|
||||
if (!"LOADING_PLAN".equals(envelope.aggregateType()))
|
||||
throw new IllegalArgumentException("尾程交接事件聚合类型无效");
|
||||
throw new EventProtocolException("尾程交接事件聚合类型无效");
|
||||
if (!EventPayloadHash.matches(EventPayloadCanonicalizer.canonicalize(envelope.payload()),
|
||||
envelope.payloadHash())) throw new IllegalArgumentException("payloadHash 与尾程事件负载不一致");
|
||||
envelope.payloadHash())) throw new EventProtocolException("payloadHash 与尾程事件负载不一致");
|
||||
FirstLegLoadingPlanCompletedV1 payload = parsePayload(envelope);
|
||||
String containerNo = ContainerNumbers.requireCanonical(payload.containerNo());
|
||||
String sourceContainerBookingId = sourceContainerBookingId(envelope, payload);
|
||||
if(payload.orders()==null||payload.orders().isEmpty()) throw new IllegalArgumentException("事件没有入仓单");
|
||||
if(payload.orders()==null||payload.orders().isEmpty()) throw new EventProtocolException("事件没有入仓单");
|
||||
OwnerSnapshot owner = requireOwnerSnapshot(payload.orders(), envelope.tenantId());
|
||||
if (envelope.eventVersion() >= 2) validateCartonSkuMappings(payload, envelope.payload());
|
||||
freezeContainerIdentity(envelope, sourceContainerBookingId, containerNo);
|
||||
LastlegEventInbox existing=inboxMapper.selectById(envelope.eventId());
|
||||
if(existing!=null) {
|
||||
if(!existing.getPayloadHash().equalsIgnoreCase(envelope.payloadHash())) throw new IllegalArgumentException("事件摘要冲突");
|
||||
if(!existing.getPayloadHash().equalsIgnoreCase(envelope.payloadHash())) throw new EventProtocolException("事件摘要冲突");
|
||||
if (!"PROCESSED".equals(existing.getStatus())) throw new IllegalStateException("事件尚未完成消费");
|
||||
return DeliveryAck.of(envelope, DeliveryAck.Outcome.DUPLICATE);
|
||||
}
|
||||
@@ -84,7 +84,7 @@ public class LoadingCompletedConsumer {
|
||||
LastlegEventInbox raced = inboxMapper.selectById(envelope.eventId());
|
||||
if (raced == null || raced.getPayloadHash() == null
|
||||
|| !raced.getPayloadHash().equalsIgnoreCase(envelope.payloadHash())) {
|
||||
throw new IllegalArgumentException("事件摘要冲突", exception);
|
||||
throw new EventProtocolException("事件摘要冲突", exception);
|
||||
}
|
||||
if (!"PROCESSED".equals(raced.getStatus())) throw new IllegalStateException("事件尚未完成消费");
|
||||
return DeliveryAck.of(envelope, DeliveryAck.Outcome.DUPLICATE);
|
||||
@@ -111,7 +111,7 @@ public class LoadingCompletedConsumer {
|
||||
if (envelope.eventVersion() < 3) return;
|
||||
JsonNode values = envelope.payload().path("loadingEvidence");
|
||||
if (!values.isArray() || values.size() != 5) {
|
||||
throw new IllegalArgumentException("V3 尾程交接事件必须包含五类国内装柜照片");
|
||||
throw new EventProtocolException("V3 尾程交接事件必须包含五类国内装柜照片");
|
||||
}
|
||||
Set<String> expected = Set.of("PHOTO_STEP_1", "PHOTO_STEP_2", "PHOTO_STEP_3", "PHOTO_STEP_4", "SEAL");
|
||||
Set<String> actual = new HashSet<>();
|
||||
@@ -121,7 +121,7 @@ public class LoadingCompletedConsumer {
|
||||
String fileName = value.path("fileName").asText("").trim();
|
||||
if (!expected.contains(type) || !actual.add(type) || fileId <= 0
|
||||
|| fileName.isBlank() || fileName.length() > 255) {
|
||||
throw new IllegalArgumentException("V3 尾程交接事件的国内装柜照片引用无效");
|
||||
throw new EventProtocolException("V3 尾程交接事件的国内装柜照片引用无效");
|
||||
}
|
||||
ContainerLoadingEvidence existing = loadingEvidence.find(sourceContainerBookingId, type);
|
||||
if (existing != null) {
|
||||
@@ -143,7 +143,7 @@ public class LoadingCompletedConsumer {
|
||||
|| !Objects.equals(raced.getFileName(), fileName)) throw duplicate;
|
||||
}
|
||||
}
|
||||
if (!actual.equals(expected)) throw new IllegalArgumentException("V3 尾程交接事件的国内装柜照片类型不完整");
|
||||
if (!actual.equals(expected)) throw new EventProtocolException("V3 尾程交接事件的国内装柜照片类型不完整");
|
||||
}
|
||||
|
||||
private FirstLegLoadingPlanCompletedV1 parsePayload(EventEnvelope envelope) {
|
||||
@@ -154,7 +154,7 @@ public class LoadingCompletedConsumer {
|
||||
// identity is validated separately from the original immutable payload.
|
||||
return compatibleMapper.treeToValue(envelope.payload(), FirstLegLoadingPlanCompletedV1.class);
|
||||
}
|
||||
catch(Exception exception) { throw new IllegalArgumentException("尾程事件负载无效",exception); }
|
||||
catch(Exception exception) { throw new EventProtocolException("尾程事件负载无效",exception); }
|
||||
}
|
||||
|
||||
private int countOrders(EventEnvelope envelope,String containerNo) {
|
||||
@@ -184,7 +184,7 @@ public class LoadingCompletedConsumer {
|
||||
? envelope.payload().path("sourceContainerBookingId").asText(null)
|
||||
: payload.containerBookingId();
|
||||
if (value == null || value.isBlank() || value.trim().length() > 128) {
|
||||
throw new IllegalArgumentException("事件缺少稳定来源柜预订 ID sourceContainerBookingId");
|
||||
throw new EventProtocolException("事件缺少稳定来源柜预订 ID sourceContainerBookingId");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
@@ -194,12 +194,12 @@ public class LoadingCompletedConsumer {
|
||||
String customerId = requiredOwnerValue(orders.get(0).customerId(), "customerId");
|
||||
String customerName = requiredOwnerValue(orders.get(0).customerName(), "customerName");
|
||||
if (!trustedCustomerId.equals(customerId)) {
|
||||
throw new IllegalArgumentException("Order customerId does not match the trusted event customer");
|
||||
throw new EventProtocolException("Order customerId does not match the trusted event customer");
|
||||
}
|
||||
for (FirstLegLoadingPlanCompletedV1.Order order : orders) {
|
||||
if (!customerId.equals(requiredOwnerValue(order.customerId(), "customerId"))
|
||||
|| !customerName.equals(requiredOwnerValue(order.customerName(), "customerName"))) {
|
||||
throw new IllegalArgumentException("同一装柜完成事件的货主快照不一致");
|
||||
throw new EventProtocolException("同一装柜完成事件的货主快照不一致");
|
||||
}
|
||||
}
|
||||
return new OwnerSnapshot(customerId, customerName);
|
||||
@@ -207,11 +207,11 @@ public class LoadingCompletedConsumer {
|
||||
|
||||
private String requiredOwnerValue(String value, String field) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("尾程事件缺少不可变货主快照 " + field);
|
||||
throw new EventProtocolException("尾程事件缺少不可变货主快照 " + field);
|
||||
}
|
||||
String normalized = value.trim();
|
||||
if (normalized.length() > 128) {
|
||||
throw new IllegalArgumentException("尾程事件货主快照过长 " + field);
|
||||
throw new EventProtocolException("尾程事件货主快照过长 " + field);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
@@ -231,11 +231,11 @@ public class LoadingCompletedConsumer {
|
||||
}
|
||||
int manifestPackages = rawPallets.path(palletIndex).path("manifestPackageCount").asInt(-1);
|
||||
if (palletCartonMarks.size() != manifestPackages) {
|
||||
throw new IllegalArgumentException("一件代发托盘物理箱数与清单箱数不一致: " + pallet.palletNo());
|
||||
throw new EventProtocolException("一件代发托盘物理箱数与清单箱数不一致: " + pallet.palletNo());
|
||||
}
|
||||
for (String mark : palletCartonMarks) {
|
||||
if (!orderCartonMarks.add(mark)) {
|
||||
throw new IllegalArgumentException("一件代发物理箱不能跨托盘重复: " + mark);
|
||||
throw new EventProtocolException("一件代发物理箱不能跨托盘重复: " + mark);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,7 +257,7 @@ public class LoadingCompletedConsumer {
|
||||
Set<String> orderCartonMarks) {
|
||||
List<String> marks = cartonMarks == null ? List.of() : cartonMarks;
|
||||
if (skuCode == null || skuCode.isBlank()) {
|
||||
throw new IllegalArgumentException("V2 尾程交接事件缺少 SKU");
|
||||
throw new EventProtocolException("V2 尾程交接事件缺少 SKU");
|
||||
}
|
||||
if (marks.isEmpty()) return;
|
||||
requireSkuCartonMapping(skuCode, loadedPackages, marks, orderCartonMarks);
|
||||
@@ -266,15 +266,15 @@ public class LoadingCompletedConsumer {
|
||||
private void requireSkuCartonMapping(String skuCode, int loadedPackages, List<String> cartonMarks,
|
||||
Set<String> orderCartonMarks) {
|
||||
if (skuCode == null || skuCode.isBlank()) {
|
||||
throw new IllegalArgumentException("V2 尾程交接事件的箱唛映射缺少 SKU");
|
||||
throw new EventProtocolException("V2 尾程交接事件的箱唛映射缺少 SKU");
|
||||
}
|
||||
List<String> marks = cartonMarks == null ? List.of() : cartonMarks;
|
||||
if (marks.size() != loadedPackages) {
|
||||
throw new IllegalArgumentException("V2 尾程交接事件的 SKU 箱唛数量必须等于实装箱数: " + skuCode);
|
||||
throw new EventProtocolException("V2 尾程交接事件的 SKU 箱唛数量必须等于实装箱数: " + skuCode);
|
||||
}
|
||||
for (String mark : marks) {
|
||||
if (mark == null || mark.isBlank() || !orderCartonMarks.add(mark)) {
|
||||
throw new IllegalArgumentException("V2 尾程交接事件包含空值或重复箱唛: " + skuCode);
|
||||
throw new EventProtocolException("V2 尾程交接事件包含空值或重复箱唛: " + skuCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,7 +298,7 @@ public class LoadingCompletedConsumer {
|
||||
List<DropshipRequests.Pallet> dropshipPallets = "ONE_CLICK_DROPSHIP".equals(orderType)
|
||||
? pallets(source, rawSource) : List.of();
|
||||
if ("ONE_CLICK_DROPSHIP".equals(orderType) && dropshipPallets.isEmpty()) {
|
||||
throw new IllegalArgumentException("One-click dropship event has no pallet snapshot");
|
||||
throw new EventProtocolException("One-click dropship event has no pallet snapshot");
|
||||
}
|
||||
order.setId(IdWorker.getId()); order.setOrderNo(orderNo); order.setCustomerId(customerId); order.setOrderType(orderType);
|
||||
order.setDestinationType(destinationType);
|
||||
@@ -346,12 +346,12 @@ public class LoadingCompletedConsumer {
|
||||
for (int palletIndex = 0; palletIndex < pallets.size(); palletIndex++) {
|
||||
String palletNo = pallets.get(palletIndex).palletNo();
|
||||
if (palletNo == null || palletNo.isBlank() || palletNo.length() > 128 || !palletNos.add(palletNo)) {
|
||||
throw new IllegalArgumentException("散货来源托盘码为空、重复或过长");
|
||||
throw new EventProtocolException("散货来源托盘码为空、重复或过长");
|
||||
}
|
||||
sourcePallets.insert(IdWorker.getId(), customerId, order.getId(), palletNo.trim(), palletIndex + 1, event.eventId());
|
||||
}
|
||||
if (!pallets.isEmpty() && order.getPalletCount() > 0 && pallets.size() != order.getPalletCount()) {
|
||||
throw new IllegalArgumentException("散货来源托盘码数量与托盘数不一致");
|
||||
throw new EventProtocolException("散货来源托盘码数量与托盘数不一致");
|
||||
}
|
||||
}
|
||||
for(var item:items) {
|
||||
@@ -379,7 +379,7 @@ public class LoadingCompletedConsumer {
|
||||
if ("ONE_CLICK_DROPSHIP".equals(orderType)) {
|
||||
long warehouseId;
|
||||
try { warehouseId = Long.parseLong(payload.warehouseId()); }
|
||||
catch (Exception exception) { throw new IllegalArgumentException("One-click dropship warehouseId is invalid", exception); }
|
||||
catch (Exception exception) { throw new EventProtocolException("One-click dropship warehouseId is invalid", exception); }
|
||||
dropshipOrders.receive(new DropshipRequests.Receive(customerId, order.getId(), warehouseId,
|
||||
event.eventId() + ":DROPSHIP:" + source.sourceInboundId(), dropshipPallets));
|
||||
}
|
||||
@@ -390,7 +390,7 @@ public class LoadingCompletedConsumer {
|
||||
try {
|
||||
return LocalDateTime.parse(value.trim());
|
||||
} catch (java.time.format.DateTimeParseException exception) {
|
||||
throw new IllegalArgumentException("事件计划到仓时间 estimatedWarehouseArrivalAt 无效", exception);
|
||||
throw new EventProtocolException("事件计划到仓时间 estimatedWarehouseArrivalAt 无效", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,7 +447,7 @@ public class LoadingCompletedConsumer {
|
||||
String detailId = sku.path("sourceInboundDetailId").asText(null);
|
||||
var item = itemsByDetail.get(detailId);
|
||||
if (item == null || !Objects.equals(item.skuCode(), sku.path("skuCode").asText(null))) {
|
||||
throw new IllegalArgumentException("一件代发托盘来源明细缺失或与 SKU 不一致");
|
||||
throw new EventProtocolException("一件代发托盘来源明细缺失或与 SKU 不一致");
|
||||
}
|
||||
List<String> marks = marksByDetail.computeIfAbsent(detailId, ignored -> new java.util.ArrayList<>());
|
||||
sku.path("cartonMarks").forEach(mark -> marks.add(mark.asText()));
|
||||
@@ -455,7 +455,7 @@ public class LoadingCompletedConsumer {
|
||||
}
|
||||
for (var item : itemsByDetail.values()) {
|
||||
if (marksByDetail.getOrDefault(item.sourceInboundDetailId(), List.of()).size() != item.packageCount()) {
|
||||
throw new IllegalArgumentException("一件代发托盘箱唛与来源明细箱数不一致");
|
||||
throw new EventProtocolException("一件代发托盘箱唛与来源明细箱数不一致");
|
||||
}
|
||||
}
|
||||
return marksByDetail;
|
||||
@@ -466,7 +466,7 @@ public class LoadingCompletedConsumer {
|
||||
Map<String, List<FirstLegLoadingPlanCompletedV1.Item>> byDetail = new java.util.LinkedHashMap<>();
|
||||
for (var item : source == null ? List.<FirstLegLoadingPlanCompletedV1.Item>of() : source) {
|
||||
if (item.sourceInboundDetailId() == null || item.sourceInboundDetailId().isBlank()) {
|
||||
throw new IllegalArgumentException("一件代发来源明细标识缺失");
|
||||
throw new EventProtocolException("一件代发来源明细标识缺失");
|
||||
}
|
||||
byDetail.computeIfAbsent(item.sourceInboundDetailId(), ignored -> new ArrayList<>()).add(item);
|
||||
}
|
||||
@@ -476,7 +476,7 @@ public class LoadingCompletedConsumer {
|
||||
if (fragments.stream().anyMatch(item -> !Objects.equals(first.skuCode(), item.skuCode())
|
||||
|| !Objects.equals(first.destinationWarehouseId(), item.destinationWarehouseId())
|
||||
|| !Objects.equals(first.destinationWarehouseCode(), item.destinationWarehouseCode()))) {
|
||||
throw new IllegalArgumentException("同一来源明细的尾程事件分片快照不一致");
|
||||
throw new EventProtocolException("同一来源明细的尾程事件分片快照不一致");
|
||||
}
|
||||
List<String> marks = fragments.stream().flatMap(item -> item.cartonMarks().stream()).toList();
|
||||
result.add(new FirstLegLoadingPlanCompletedV1.Item(first.sourceInboundDetailId(), first.skuCode(),
|
||||
@@ -512,7 +512,7 @@ public class LoadingCompletedConsumer {
|
||||
List<String> marks = sourceMarks == null ? List.of() : sourceMarks;
|
||||
if (marks.isEmpty()) return;
|
||||
if (marks.stream().anyMatch(mark -> mark == null || mark.isBlank()) || marks.stream().distinct().count() != marks.size()) {
|
||||
throw new IllegalArgumentException("装柜事件的箱唛池不可包含空值或重复箱唛");
|
||||
throw new EventProtocolException("装柜事件的箱唛池不可包含空值或重复箱唛");
|
||||
}
|
||||
for (String mark : marks) {
|
||||
InboundPlanCarton carton = new InboundPlanCarton();
|
||||
@@ -536,18 +536,18 @@ public class LoadingCompletedConsumer {
|
||||
boolean overseasWarehouseIdentity = hasText(payload.warehouseId())
|
||||
&& Objects.equals(payload.warehouseId(), order.destinationWarehouseId());
|
||||
if (fbaEvidence && overseasWarehouseIdentity) {
|
||||
throw new IllegalArgumentException("历史尾程事件目的类型证据冲突: " + order.sourceInboundId());
|
||||
throw new EventProtocolException("历史尾程事件目的类型证据冲突: " + order.sourceInboundId());
|
||||
}
|
||||
if (fbaEvidence) return "FBA";
|
||||
if (overseasWarehouseIdentity) return "OVERSEAS";
|
||||
throw new IllegalArgumentException("历史尾程事件缺少 destinationType 且无法安全推断: " + order.sourceInboundId());
|
||||
throw new EventProtocolException("历史尾程事件缺少 destinationType 且无法安全推断: " + order.sourceInboundId());
|
||||
}
|
||||
|
||||
private String normalizeDestinationType(String value) {
|
||||
if (!hasText(value)) return null;
|
||||
String normalized = value.trim().toUpperCase(java.util.Locale.ROOT);
|
||||
if (!"FBA".equals(normalized) && !"OVERSEAS".equals(normalized)) {
|
||||
throw new IllegalArgumentException("尾程事件 destinationType 必须为 FBA 或 OVERSEAS");
|
||||
throw new EventProtocolException("尾程事件 destinationType 必须为 FBA 或 OVERSEAS");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
@@ -577,7 +577,7 @@ public class LoadingCompletedConsumer {
|
||||
List<String> marks = new ArrayList<>(left.cartonMarks()); marks.addAll(right.cartonMarks());
|
||||
Map<String, Long> quantities = new java.util.LinkedHashMap<>(left.cartonQuantities());
|
||||
right.cartonQuantities().forEach((mark, quantity) -> {
|
||||
if (quantities.putIfAbsent(mark, quantity) != null) throw new IllegalArgumentException("Duplicate carton quantity: " + mark);
|
||||
if (quantities.putIfAbsent(mark, quantity) != null) throw new EventProtocolException("Duplicate carton quantity: " + mark);
|
||||
});
|
||||
return new DropshipRequests.SkuPackages(left.skuCode(), left.productName(),
|
||||
Math.addExact(left.packageCount(), right.packageCount()),
|
||||
@@ -620,25 +620,25 @@ public class LoadingCompletedConsumer {
|
||||
|
||||
private int positiveManifest(JsonNode source, String field) {
|
||||
int value = source.path(field).asInt(0);
|
||||
if (value <= 0) throw new IllegalArgumentException("One-click dropship pallet is missing " + field);
|
||||
if (value <= 0) throw new EventProtocolException("One-click dropship pallet is missing " + field);
|
||||
return value;
|
||||
}
|
||||
|
||||
private String requiredManifestHash(JsonNode source) {
|
||||
String value = source.path("manifestHash").asText(null);
|
||||
if (value == null || !value.matches("(?i)[0-9a-f]{64}")) {
|
||||
throw new IllegalArgumentException("One-click dropship pallet manifestHash is invalid");
|
||||
throw new EventProtocolException("One-click dropship pallet manifestHash is invalid");
|
||||
}
|
||||
return value.toLowerCase(java.util.Locale.ROOT);
|
||||
}
|
||||
|
||||
private Map<String, Long> cartonQuantities(JsonNode sku) {
|
||||
JsonNode node = sku.path("cartonQuantities");
|
||||
if (!node.isObject()) throw new IllegalArgumentException("One-click dropship cartonQuantities is required");
|
||||
if (!node.isObject()) throw new EventProtocolException("One-click dropship cartonQuantities is required");
|
||||
Map<String, Long> result = new java.util.LinkedHashMap<>();
|
||||
node.fields().forEachRemaining(entry -> {
|
||||
if (!entry.getValue().isIntegralNumber() || !entry.getValue().canConvertToLong() || entry.getValue().longValue() <= 0) {
|
||||
throw new IllegalArgumentException("Invalid carton quantity: " + entry.getKey());
|
||||
throw new EventProtocolException("Invalid carton quantity: " + entry.getKey());
|
||||
}
|
||||
result.put(entry.getKey(), entry.getValue().longValue());
|
||||
});
|
||||
|
||||
+11
-8
@@ -1,6 +1,8 @@
|
||||
package com.baiyue.lastleg.shipping.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;
|
||||
@@ -36,7 +38,7 @@ public class ContainerShippingNodeConsumer {
|
||||
LastlegEventInbox existing = inboxes.selectById(envelope.eventId());
|
||||
if (existing != null) {
|
||||
if (!existing.getPayloadHash().equalsIgnoreCase(envelope.payloadHash())) {
|
||||
throw new IllegalArgumentException("船节点事件摘要冲突");
|
||||
throw new EventProtocolException("船节点事件摘要冲突");
|
||||
}
|
||||
if (!"PROCESSED".equals(existing.getStatus())) throw new IllegalStateException("事件尚未完成消费");
|
||||
return DeliveryAck.of(envelope, DeliveryAck.Outcome.DUPLICATE);
|
||||
@@ -46,7 +48,7 @@ public class ContainerShippingNodeConsumer {
|
||||
catch (DuplicateKeyException raced) {
|
||||
LastlegEventInbox concurrent = inboxes.selectById(envelope.eventId());
|
||||
if (concurrent == null || !concurrent.getPayloadHash().equalsIgnoreCase(envelope.payloadHash())) {
|
||||
throw new IllegalArgumentException("船节点事件摘要冲突", raced);
|
||||
throw new EventProtocolException("船节点事件摘要冲突", raced);
|
||||
}
|
||||
if (!"PROCESSED".equals(concurrent.getStatus())) throw new IllegalStateException("事件尚未完成消费");
|
||||
return DeliveryAck.of(envelope, DeliveryAck.Outcome.DUPLICATE);
|
||||
@@ -55,8 +57,9 @@ public class ContainerShippingNodeConsumer {
|
||||
gates.initialize(payload.containerBookingId(), payload.oceanBookingId(),
|
||||
payload.containerNo(), 0, 1);
|
||||
ContainerShippingGate gate = gates.lock(payload.containerBookingId());
|
||||
if (gate == null || !payload.containerNo().equals(gate.getContainerNo())) {
|
||||
throw new IllegalArgumentException("船节点柜号快照与装柜交接不一致");
|
||||
if (gate == null) throw new EventDependencyNotReadyException("船节点柜级投影尚未就绪");
|
||||
if (!payload.containerNo().equals(gate.getContainerNo())) {
|
||||
throw new EventProtocolException("船节点柜号快照与装柜交接不一致");
|
||||
}
|
||||
boolean applied = apply(gate, envelope, payload);
|
||||
gates.updateById(gate);
|
||||
@@ -98,7 +101,7 @@ public class ContainerShippingNodeConsumer {
|
||||
envelope.requireValid();
|
||||
if (!EventPayloadHash.matches(EventPayloadCanonicalizer.canonicalize(envelope.payload()),
|
||||
envelope.payloadHash())) {
|
||||
throw new IllegalArgumentException("payloadHash 与船节点事件负载不一致");
|
||||
throw new EventProtocolException("payloadHash 与船节点事件负载不一致");
|
||||
}
|
||||
if (!EventTypes.FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED.equals(envelope.eventType())
|
||||
|| envelope.eventVersion() != 1 || envelope.sourceRegion() != RegionCode.CN
|
||||
@@ -106,19 +109,19 @@ public class ContainerShippingNodeConsumer {
|
||||
|| !"CONTAINER_BOOKING".equals(envelope.aggregateType())
|
||||
|| envelope.handoverVersion() == null
|
||||
|| envelope.handoverVersion() != envelope.aggregateVersion() + 1) {
|
||||
throw new IllegalArgumentException("船节点交接信封无效");
|
||||
throw new EventProtocolException("船节点交接信封无效");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateEnvelopePayload(EventEnvelope envelope, FirstLegContainerShippingNodeRecordedV1 payload) {
|
||||
if (!"0".equals(envelope.tenantId()) || !envelope.aggregateId().equals(payload.containerBookingId())) {
|
||||
throw new IllegalArgumentException("船节点交接平台作用域或聚合标识不一致");
|
||||
throw new EventProtocolException("船节点交接平台作用域或聚合标识不一致");
|
||||
}
|
||||
}
|
||||
|
||||
private FirstLegContainerShippingNodeRecordedV1 payload(EventEnvelope envelope) {
|
||||
try { return objectMapper.treeToValue(envelope.payload(), FirstLegContainerShippingNodeRecordedV1.class); }
|
||||
catch (Exception exception) { throw new IllegalArgumentException("船节点交接负载无效", exception); }
|
||||
catch (Exception exception) { throw new EventProtocolException("船节点交接负载无效", exception); }
|
||||
}
|
||||
|
||||
private LastlegEventInbox inbox(EventEnvelope envelope) {
|
||||
|
||||
+4
-4
@@ -14,11 +14,11 @@ import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import com.baiyue.common.event.EventProtocolException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -67,12 +67,12 @@ public class WalletCommandCompletedConsumer {
|
||||
|| result.businessType() == null || result.businessType().isBlank()
|
||||
|| result.businessNo() == null || result.businessNo().isBlank()) throw bad("资金结果事件字段无效");
|
||||
return result;
|
||||
} catch (ResponseStatusException exception) { throw exception; }
|
||||
} catch (EventProtocolException exception) { throw exception; }
|
||||
catch (Exception exception) { throw bad("资金结果事件载荷无法解析"); }
|
||||
}
|
||||
private String json(Object value) {
|
||||
try { return objectMapper.writeValueAsString(value); }
|
||||
catch (Exception exception) { throw bad("资金结果事件序列化失败"); }
|
||||
}
|
||||
private static ResponseStatusException bad(String message) { return new ResponseStatusException(BAD_REQUEST, message); }
|
||||
private static EventProtocolException bad(String message) { return new EventProtocolException(message); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user