优化
This commit is contained in:
@@ -16,7 +16,7 @@ public interface LoadingCompletedEventMapper {
|
||||
@Select("""
|
||||
SELECT p.id AS loading_plan_id, p.plan_no, p.container_booking_id, p.version AS aggregate_version,
|
||||
b.container_no, b.cabinet_no, b.warehouse_id, b.warehouse_name,
|
||||
b.customer_id AS booking_customer_id, b.customer_name,
|
||||
i.customer_name,
|
||||
b.estimated_port_arrival_at, b.actual_port_arrival_at,
|
||||
b.estimated_warehouse_arrival_at,
|
||||
i.account_id, i.inbound_id AS source_inbound_id,
|
||||
@@ -84,8 +84,8 @@ public interface LoadingCompletedEventMapper {
|
||||
class Row {
|
||||
private Long loadingPlanId; private String planNo; private Long containerBookingId;
|
||||
private Integer aggregateVersion; private String containerNo; private String cabinetNo;
|
||||
private Long warehouseId; private String warehouseName; private Long bookingCustomerId;
|
||||
private String customerName; private Long accountId;
|
||||
private Long warehouseId; private String warehouseName; private String customerName;
|
||||
private Long accountId;
|
||||
private LocalDateTime estimatedPortArrivalAt; private LocalDateTime actualPortArrivalAt;
|
||||
private LocalDateTime estimatedWarehouseArrivalAt;
|
||||
private Long sourceInboundId; private Long rootInboundId; private Long sourceOrderId;
|
||||
|
||||
+3
-3
@@ -120,11 +120,11 @@ public class LoadingCompletedOutboxService {
|
||||
|
||||
private void validateCustomerSnapshots(List<Row> rows) {
|
||||
for (Row row : rows) {
|
||||
if (row.getAccountId() == null || !Objects.equals(row.getAccountId(), row.getBookingCustomerId())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "装柜计划客户与柜次客户快照不一致,不能生成尾程交接事件");
|
||||
if (row.getAccountId() == null || row.getAccountId() <= 0) {
|
||||
throw new BizException(ResultCode.CONFLICT, "装柜明细缺少有效客户标识,不能生成尾程交接事件");
|
||||
}
|
||||
if (row.getCustomerName() == null || row.getCustomerName().isBlank()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "柜次缺少客户名称快照,不能生成尾程交接事件");
|
||||
throw new BizException(ResultCode.CONFLICT, "装柜明细缺少客户名称快照,不能生成尾程交接事件");
|
||||
}
|
||||
}
|
||||
rows.stream().collect(java.util.stream.Collectors.groupingBy(Row::getAccountId)).forEach((accountId, values) -> {
|
||||
|
||||
@@ -50,7 +50,7 @@ public class LoadingCompletionWorker {
|
||||
String traceId = cause instanceof PdaPickSettlementService.SettlementFailure remote
|
||||
? remote.traceId() : null;
|
||||
int attempt = command.getAttemptCount() == null ? 1 : command.getAttemptCount() + 1;
|
||||
boolean terminal = attempt >= MAX_ATTEMPTS;
|
||||
boolean terminal = isDeterministicLocalBusinessFailure(stage, cause) || attempt >= MAX_ATTEMPTS;
|
||||
LocalDateTime retryAt = LocalDateTime.now().plusSeconds(Math.min(300L, 15L << Math.min(attempt - 1, 4)));
|
||||
boolean recorded = commands.fail(command.getId(), workerId, terminal ? "FAILED" : "RETRYABLE", retryAt,
|
||||
stage, code, requestId, traceId);
|
||||
@@ -61,4 +61,10 @@ public class LoadingCompletionWorker {
|
||||
+ "requestId={} traceId={} attempt={} terminal={}", command.getId(), command.getPickTaskId(),
|
||||
command.getOperationId(), stage, code, requestId, traceId, attempt, terminal, cause);
|
||||
}
|
||||
|
||||
private static boolean isDeterministicLocalBusinessFailure(String stage, Throwable cause) {
|
||||
return cause instanceof BizException && java.util.Set.of(
|
||||
"OWNER_DISCOVERY", "OWNER_READY", "PLAN_GATE", "TASK_REVIEW",
|
||||
"PLAN_COMPLETE", "COMPLETED_EVENT").contains(stage);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-7
@@ -85,7 +85,7 @@ class LoadingCompletedOutboxServiceTest {
|
||||
.hasMessageContaining("缺少 SKU");
|
||||
}
|
||||
|
||||
@Test void rejectsMissingTrustedBookingCustomerName() {
|
||||
@Test void rejectsMissingTrustedPlanItemCustomerName() {
|
||||
Row value = row(11, 100, 101);
|
||||
value.setCustomerName(" ");
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(value));
|
||||
@@ -93,12 +93,19 @@ class LoadingCompletedOutboxServiceTest {
|
||||
assertThatThrownBy(() -> service.publish(90L)).hasMessageContaining("缺少客户名称快照");
|
||||
}
|
||||
|
||||
@Test void rejectsBookingCustomerIdThatDoesNotMatchAccountEvent() {
|
||||
@Test void publishesFrozenPlanItemCustomerSnapshotWithoutDependingOnBookingOwner() throws Exception {
|
||||
Row value = row(11, 100, 101);
|
||||
value.setBookingCustomerId(22L);
|
||||
value.setCustomerName("南玻万");
|
||||
value.setDestinationType("FBA");
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(value));
|
||||
|
||||
assertThatThrownBy(() -> service.publish(90L)).hasMessageContaining("客户与柜次客户快照不一致");
|
||||
service.publish(90L);
|
||||
|
||||
ArgumentCaptor<InboundOutboxEntity> captor = ArgumentCaptor.forClass(InboundOutboxEntity.class);
|
||||
verify(outbox).insert(captor.capture());
|
||||
EventEnvelope event = mapper.readValue(captor.getValue().getPayloadJson(), EventEnvelope.class);
|
||||
assertThat(event.tenantId()).isEqualTo("11");
|
||||
assertThat(event.payload().at("/orders/0/customerName").asText()).isEqualTo("南玻万");
|
||||
}
|
||||
|
||||
@Test void fbaFreezesPackageCountWithoutRequiringCartonLabels() throws Exception {
|
||||
@@ -209,8 +216,9 @@ class LoadingCompletedOutboxServiceTest {
|
||||
|
||||
assertThat(rowsSql).contains("p.plan_status = 'COMPLETED'")
|
||||
.contains("o.process_status = 'LOADED'")
|
||||
.contains("b.customer_id AS booking_customer_id")
|
||||
.contains("b.customer_name")
|
||||
.contains("i.customer_name")
|
||||
.doesNotContain("b.customer_id AS booking_customer_id")
|
||||
.doesNotContain("b.customer_name")
|
||||
.contains("b.estimated_port_arrival_at", "b.actual_port_arrival_at")
|
||||
.contains("b.estimated_warehouse_arrival_at");
|
||||
for (String sql : List.of(rowsSql, palletsSql)) {
|
||||
@@ -288,7 +296,7 @@ class LoadingCompletedOutboxServiceTest {
|
||||
private Row row(long account,long inbound,long detail) {
|
||||
Row r=new Row(); r.setLoadingPlanId(90L); r.setPlanNo("P90"); r.setContainerBookingId(91L);
|
||||
r.setAggregateVersion(8); r.setContainerNo("CONT"); r.setCabinetNo("CAB"); r.setWarehouseId(92L);
|
||||
r.setWarehouseName("WH"); r.setBookingCustomerId(account); r.setCustomerName("客户-" + account);
|
||||
r.setWarehouseName("WH"); r.setCustomerName("客户-" + account);
|
||||
r.setEstimatedPortArrivalAt(java.time.LocalDateTime.of(2026, 9, 30, 12, 0));
|
||||
r.setActualPortArrivalAt(java.time.LocalDateTime.of(2026, 10, 1, 8, 30));
|
||||
r.setEstimatedWarehouseArrivalAt(java.time.LocalDateTime.of(2026, 10, 3, 9, 0));
|
||||
|
||||
@@ -63,6 +63,24 @@ class LoadingCompletionWorkerTest {
|
||||
verify(picks).failCompletion(20L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void completedEventBusinessFailureIsTerminalOnFirstAttempt() {
|
||||
LoadingCompletionCommandEntity command = command(0);
|
||||
when(commands.due(10)).thenReturn(List.of(command));
|
||||
when(commands.claim(eq(900L), any(String.class), any(LocalDateTime.class))).thenReturn(true);
|
||||
doThrow(new LoadingPickService.CompletionProcessingException("COMPLETED_EVENT",
|
||||
new BizException(ResultCode.CONFLICT, "customer snapshot conflict")))
|
||||
.when(loadingPicks).processCompletion(eq(command), any(String.class));
|
||||
when(commands.fail(eq(900L), any(String.class), eq("FAILED"), any(LocalDateTime.class),
|
||||
eq("COMPLETED_EVENT"), eq(ResultCode.CONFLICT.code()), eq(null), eq(null))).thenReturn(true);
|
||||
|
||||
worker.dispatch();
|
||||
|
||||
verify(commands).fail(eq(900L), any(String.class), eq("FAILED"), any(LocalDateTime.class),
|
||||
eq("COMPLETED_EVENT"), eq(ResultCode.CONFLICT.code()), eq(null), eq(null));
|
||||
verify(picks).failCompletion(20L);
|
||||
}
|
||||
|
||||
private static LoadingCompletionCommandEntity command(int attemptCount) {
|
||||
LoadingCompletionCommandEntity command = new LoadingCompletionCommandEntity();
|
||||
command.setId(900L);
|
||||
|
||||
Reference in New Issue
Block a user