This commit is contained in:
wujinliang
2026-08-27 21:49:00 +08:00
87 changed files with 3347 additions and 252 deletions
@@ -180,7 +180,7 @@ class LoadingListControllerContractTest {
"itemId", "accountId", "inboundId", "inboundDetailId", "skuCode",
"destinationWarehouse", "productQuantity", "frozenAt");
assertThat(properties(schemas, "OperationHistory")).containsKeys(
"historyId", "itemId", "actionType", "fromStatus", "toStatus",
"historyId", "itemId", "operationId", "actionType", "fromStatus", "toStatus",
"beforeSnapshot", "afterSnapshot", "operatorId", "occurredAt");
assertThat(properties(schemas, "TransportUpdateRequest")).containsOnlyKeys(
"containerNo", "voyageNo", "expectedVersion", "operationReason");
@@ -39,7 +39,8 @@ class PdaConvergenceOpenApiContractTest {
.contains("sourceType", "recordStatus", "mergedIntoContainerBookingId")
.contains("Task detail is served only from the ACTIVE container booking")
.contains("Enforced by the server-side pending-task query")
.contains("assigned LOADING tasks with pendingItemCount=0 remain visible as AWAITING_REVIEW")
.contains("completion acceptance is returned immediately as PROCESSING")
.contains("taskStatus=COMPLETING且workflowStatus=PROCESSING")
.contains("CompleteLoadingRequest");
String taskLookup = yaml.substring(
@@ -25,6 +25,7 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@@ -195,6 +196,65 @@ class LoadingDeliveryServiceTest {
verify(repository).findInboundForUpdate(10, 20);
verify(repository).completeLoadedChild(10, 20, 7, 7);
verify(repository, never()).completeLoadedChild(10, 20, 7, 6);
verify(loadingModelBridge).requireStarted(10, 20);
verify(loadingModelBridge, never()).requireCompleted(10, 20);
}
@Test
void pdaFinalizeResumesSameProcessingClaimLeftByAnEarlierWorkerFailure() {
readyForPdaFinalizeFacts();
CompletionTarget settled = new CompletionTarget(20L, null, 20L,
"LOADING", 7, "LOADING", 7);
when(repository.completionTarget(10, 20)).thenReturn(settled);
when(repository.findInboundForUpdate(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 7));
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 7));
when(repository.completeLoadedChild(10, 20, 7, 7)).thenReturn(1);
AtomicReference<String> hash = new AtomicReference<>();
when(repository.claimOperation(eq(10L), eq(20L), eq(7L), eq("COMPLETE_LOADING"),
eq("pda-resume"), anyString())).thenAnswer(invocation -> {
hash.set(invocation.getArgument(5));
return false;
});
when(repository.findOperation(10L, 20L, 7L, "COMPLETE_LOADING", "pda-resume"))
.thenAnswer(invocation -> processingOperation(hash.get()));
LoadingActionResponse result = service.completeAfterPdaSettlement(
10, 20, 7, "admin", "pda-resume", "完成");
assertThat(result.mainStatus()).isEqualTo("LOADED");
verify(repository).completeLoadedChild(10, 20, 7, 7);
verify(repository).completeOperation(eq(10L), eq(20L), eq(7L),
eq("COMPLETE_LOADING"), eq("pda-resume"), anyString());
verify(loadingModelBridge).requireStarted(10, 20);
verify(loadingModelBridge, never()).requireCompleted(10, 20);
}
@Test
void pdaFinalizeConvergesAnAlreadyLoadedInboundBeforeThePlanIsCompleted() {
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString()))
.thenReturn(true);
when(repository.findInboundForUpdate(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADED", 8));
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADED", 8));
when(repository.listEvidence(10, 20)).thenReturn(List.of());
LoadingActionResponse result = service.completeAfterPdaSettlement(
10, 20, 7, "admin", "pda-already-loaded", "完成");
assertThat(result.mainStatus()).isEqualTo("LOADED");
verify(loadingModelBridge).requireStarted(10, 20);
verify(loadingModelBridge, never()).requireCompleted(10, 20);
verify(repository).completeOperation(eq(10L), eq(20L), eq(7L),
eq("COMPLETE_LOADING"), eq("pda-already-loaded"), anyString());
}
@Test
void pdaFinalizeEntryPointsAreTransactional() throws Exception {
assertThat(LoadingDeliveryService.class.getMethod("completeAfterPdaSettlement",
long.class, long.class, long.class, String.class, String.class, String.class)
.getAnnotation(Transactional.class)).isNotNull();
assertThat(LoadingDeliveryService.class.getMethod("completeAfterPdaSettlement",
long.class, long.class, long.class, long.class, String.class, String.class, String.class)
.getAnnotation(Transactional.class)).isNotNull();
}
@Test
@@ -213,6 +273,8 @@ class LoadingDeliveryServiceTest {
assertThat(result.mainStatus()).isEqualTo("LOADED");
verify(repository).findInboundForUpdate(10, 21);
verify(repository).completeLoadedChild(10, 20, 7, 2);
verify(loadingModelBridge).requireStarted(10, 20);
verify(loadingModelBridge, never()).requireCompleted(10, 20);
}
@Test
@@ -350,6 +412,24 @@ class LoadingDeliveryServiceTest {
"PHOTO_STEP_1".equals(value.getEvidenceType()) && "ACTIVE".equals(value.getStatus())));
}
@Test
void replaysTheSameActiveEvidenceFileWithoutAppendingAnotherRecord() {
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 3));
LoadingEvidenceEntity existing = evidence("SEAL", 11L, "LOCK-001");
existing.setId(99L); existing.setInboundId(20L); existing.setTenantId(10L); existing.setStatus("ACTIVE");
when(repository.findEvidenceBySealNo(10, "LOCK-001")).thenReturn(existing);
when(repository.findActiveEvidenceByFile(10, 20, "SEAL", 11L)).thenReturn(existing);
when(repository.listEvidence(10, 20)).thenReturn(List.of(existing));
LoadingActionResponse result = service.upsertEvidence(10, 20, 7, "admin", "same-seal-photo",
new UpsertEvidenceRequest(0, "SEAL", 11L, null, "LOCK-001", null));
assertThat(result.evidences().evidences()).hasSize(1);
verify(repository, never()).createEvidence(any());
verify(files, never()).validate(anyString(), anyLong(), anyLong(), anyLong());
}
@Test
void voidsPhotoWithReasonAndKeepsTheRecord() {
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
@@ -418,11 +498,16 @@ class LoadingDeliveryServiceTest {
private void readyForPdaFinalize() {
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString()))
.thenReturn(true);
readyForPdaFinalizeFacts();
}
private void readyForPdaFinalizeFacts() {
LoadingPickItemMapper.InboundProgress progress = mock(LoadingPickItemMapper.InboundProgress.class);
when(progress.getExpectedCount()).thenReturn(5);
when(progress.getPickedCount()).thenReturn(5);
when(progress.getTaskCount()).thenReturn(1);
when(progress.getCompletedTaskCount()).thenReturn(1);
// completion worker runs before task finalization; accepted COMPLETING is not COMPLETED yet.
when(progress.getCompletedTaskCount()).thenReturn(0);
when(progress.getUnresolvedCount()).thenReturn(0);
when(loadingPicks.inboundProgress(10, 20)).thenReturn(progress);
when(repository.listEvidence(10, 20)).thenReturn(List.of(
@@ -444,4 +529,11 @@ class LoadingDeliveryServiceTest {
value.setResultJson(resultJson);
return value;
}
private static LoadingOperationEntity processingOperation(String requestHash) {
LoadingOperationEntity value = new LoadingOperationEntity();
value.setRequestHash(requestHash);
value.setStatus("PROCESSING");
return value;
}
}
@@ -0,0 +1,32 @@
package com.baiyue.firstleg.repository;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class LoadingCompletionSqlContractTest {
@Test
void baselineAndUnifiedSqlDefineDurableCompletionStateMachine() throws Exception {
assertContract(Files.readString(Path.of("src", "main", "resources", "db",
"phase3-smart-loading-schema.sql"), StandardCharsets.UTF_8));
assertContract(Files.readString(Path.of("..", "deploy", "sql",
"头程数据库统一增量-MySQL8.sql"), StandardCharsets.UTF_8));
}
private static void assertContract(String sql) {
assertThat(sql)
.contains("fl_loading_completion_command")
.contains("fl_loading_completion_checkpoint")
.contains("uk_loading_completion_operation")
.contains("idx_loading_completion_dispatch")
.contains("PROCESSING","RETRYABLE","FAILED","COMPLETED")
.contains("COMPLETING","COMPLETION_FAILED")
.contains("READY","SETTLED","INBOUND_COMPLETED")
.contains("lease_owner","lease_until","next_retry_at")
.contains("failure_stage","failure_code","failure_request_id","failure_trace_id");
}
}
@@ -63,7 +63,7 @@ class LoadingPickItemMapperContractTest {
.contains("plan_item.loading_plan_id = #{planId}")
.contains("plan_item.deleted = 0")
.contains("pick_task.loading_plan_id = #{planId}")
.contains("pick_task.task_status = 'LOADING'")
.doesNotContain("pick_task.task_status")
.contains("pick_item.item_status = 'PICKED'")
.contains("pick_item.picked_package_count = plan_item.package_count")
.contains("pick_item.item_status = 'PARTIAL'")
@@ -27,12 +27,14 @@ class LoadingPickPdaTaskSqlContractTest {
.getAnnotation(Select.class).value()[0];
assertThat(sql)
.contains("task.task_status = 'LOADING'")
.contains("task.task_status IN ('LOADING','COMPLETING','COMPLETION_FAILED')")
.contains("booking.record_status = 'ACTIVE'")
.contains("booking.source_type AS sourceType")
.contains("booking.record_status AS recordStatus")
.contains("booking.merged_into_container_booking_id AS mergedIntoContainerBookingId")
.contains("WHEN task.task_status = 'COMPLETED' THEN 'COMPLETED'")
.contains("WHEN task.task_status = 'COMPLETING' THEN 'PROCESSING'")
.contains("WHEN task.task_status = 'COMPLETION_FAILED' THEN 'COMPLETION_FAILED'")
.contains("WHEN assignment.pick_task_id IS NULL THEN 'UNASSIGNED'")
.contains("pick_item.item_status IN ('PENDING', 'PICKING')")
.contains("THEN 'AWAITING_REVIEW'")
@@ -229,10 +229,15 @@ class SmartLoadingSqlContractTest {
}
private static void assertLoadingCompletionTrigger(String sql) {
assertThat(sql)
int triggerStart = sql.indexOf("CREATE TRIGGER trg_loading_plan_before_update");
int triggerEnd = sql.indexOf("DROP TRIGGER IF EXISTS trg_loading_plan_after_update", triggerStart);
assertThat(triggerStart).isGreaterThanOrEqualTo(0);
assertThat(triggerEnd).isGreaterThan(triggerStart);
String beforeUpdateTrigger = sql.substring(triggerStart, triggerEnd);
assertThat(beforeUpdateTrigger)
.contains("AND NOT EXISTS (")
.contains("pick_task.loading_plan_id = OLD.id")
.contains("pick_task.task_status = 'LOADING'")
.doesNotContain("pick_task.task_status")
.contains("pick_item.item_status = 'PICKED'")
.contains("pick_item.item_status = 'PARTIAL'")
.contains("pick_item.picked_package_count > 0")
@@ -308,7 +313,7 @@ class SmartLoadingSqlContractTest {
.contains("container_booking_id BIGINT UNSIGNED NOT NULL")
.contains("cabinet_no VARCHAR(64) NOT NULL")
.contains("container_no VARCHAR(64) NULL")
.contains("task_status VARCHAR(16) NOT NULL DEFAULT 'LOADING'")
.contains("task_status VARCHAR(24) NOT NULL DEFAULT 'LOADING'")
.contains("UNIQUE KEY uk_loading_pick_task_plan (loading_plan_id)")
.contains("UNIQUE KEY uk_loading_pick_task_booking (container_booking_id)")
.contains("KEY idx_loading_pick_container (container_no, task_status, started_at)")
@@ -45,6 +45,10 @@ class LoadingCompletedOutboxServiceTest {
@Test void groupsByAccountAndKeepsAllOrdersAndItemsStable() throws Exception {
when(snapshots.rows(90L)).thenReturn(List.of(row(22,300,302),row(11,200,202),row(11,100,102),row(11,100,101)));
when(snapshots.palletRows(90L)).thenReturn(List.of(
pallet(11,100,1,"PLT-01",1,101,"SKU101",1,10,"BOX-101"),
pallet(11,100,1,"PLT-01",1,102,"SKU102",1,10,"BOX-102"),
pallet(11,100,2,"PLT-02",1,102,"SKU102",1,10,"BOX-102")));
service.publish(90L);
ArgumentCaptor<InboundOutboxEntity> captor=ArgumentCaptor.forClass(InboundOutboxEntity.class);
verify(outbox,org.mockito.Mockito.times(2)).insert(captor.capture());
@@ -59,6 +63,15 @@ class LoadingCompletedOutboxServiceTest {
assertThat(first.payload().at("/orders/0/items/0/skuCode").asText()).isEqualTo("SKU101");
assertThat(first.payload().at("/orders/0/items/0/cartonMarks/0").asText()).isEqualTo("BOX-101");
assertThat(first.payload().at("/orders/0/destinationType").asText()).isEqualTo("OVERSEAS");
assertThat(first.payload().at("/orders/0/customerName").asText()).isEqualTo("客户-11");
assertThat(first.payload().at("/orders/0/palletCount").asInt()).isEqualTo(2);
assertThat(first.payload().at("/orders/0/packageCount").asInt()).isEqualTo(2);
assertThat(first.payload().at("/orders/0/productQuantity").asInt()).isEqualTo(20);
assertThat(first.payload().at("/orders/0/volumeCbm").decimalValue()).isEqualByComparingTo("2");
assertThat(first.payload().at("/orders/0/volumeWeightKg").decimalValue()).isEqualByComparingTo("4");
assertThat(first.payload().at("/estimatedPortArrivalAt").asText()).isEqualTo("2026-09-30T12:00");
assertThat(first.payload().at("/actualPortArrivalAt").asText()).isEqualTo("2026-10-01T08:30");
assertThat(first.payload().at("/estimatedWarehouseArrivalAt").asText()).isEqualTo("2026-10-03T09:00");
assertThat(rows.get(0).getEventKey()).isEqualTo("FIRSTLEG_LOADING_COMPLETED:90:11:2");
assertThat(first.eventVersion()).isEqualTo(2);
}
@@ -72,6 +85,22 @@ class LoadingCompletedOutboxServiceTest {
.hasMessageContaining("缺少 SKU");
}
@Test void rejectsMissingTrustedBookingCustomerName() {
Row value = row(11, 100, 101);
value.setCustomerName(" ");
when(snapshots.rows(90L)).thenReturn(List.of(value));
assertThatThrownBy(() -> service.publish(90L)).hasMessageContaining("缺少客户名称快照");
}
@Test void rejectsBookingCustomerIdThatDoesNotMatchAccountEvent() {
Row value = row(11, 100, 101);
value.setBookingCustomerId(22L);
when(snapshots.rows(90L)).thenReturn(List.of(value));
assertThatThrownBy(() -> service.publish(90L)).hasMessageContaining("客户与柜次客户快照不一致");
}
@Test void fbaFreezesPackageCountWithoutRequiringCartonLabels() throws Exception {
Row value = row(11, 100, 101);
value.setDestinationType("FBA");
@@ -118,6 +147,41 @@ class LoadingCompletedOutboxServiceTest {
.isEqualTo(second.payload().at("/orders/0/items/0/cartonMarks"));
}
@Test void immutableCustomerNameParticipatesInPayloadHash() {
Row value = row(11, 100, 101);
when(snapshots.rows(90L)).thenReturn(List.of(value));
service.publish(90L);
value.setCustomerName("客户甲-更名快照");
service.publish(90L);
ArgumentCaptor<InboundOutboxEntity> captor = ArgumentCaptor.forClass(InboundOutboxEntity.class);
verify(outbox, org.mockito.Mockito.times(2)).insert(captor.capture());
assertThat(captor.getAllValues().get(0).getPayloadHash())
.isNotEqualTo(captor.getAllValues().get(1).getPayloadHash());
}
@Test void keepsMissingPalletAndMeasurementFactsNullInsteadOfInventingZero() throws Exception {
Row value = row(11, 100, 101);
value.setProductQuantity(null); value.setVolumeCbm(null); value.setDimensionalWeightKg(null);
value.setEstimatedPortArrivalAt(null); value.setActualPortArrivalAt(null);
value.setEstimatedWarehouseArrivalAt(null);
when(snapshots.rows(90L)).thenReturn(List.of(value));
when(snapshots.palletRows(90L)).thenReturn(List.of());
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.payload().at("/orders/0/palletCount").isNull()).isTrue();
assertThat(event.payload().at("/orders/0/productQuantity").isNull()).isTrue();
assertThat(event.payload().at("/orders/0/volumeCbm").isNull()).isTrue();
assertThat(event.payload().at("/orders/0/volumeWeightKg").isNull()).isTrue();
assertThat(event.payload().at("/estimatedPortArrivalAt").isNull()).isTrue();
assertThat(event.payload().at("/actualPortArrivalAt").isNull()).isTrue();
assertThat(event.payload().at("/estimatedWarehouseArrivalAt").isNull()).isTrue();
}
@Test void fillsCollapsedMeasurementDetailFromUnusedInboundLabelsWithoutDuplicates() throws Exception {
Row first = row(11, 100, 101); first.setPackageCount(1);
Row second = row(11, 100, 101); second.setLoadingPlanItemId(102L); second.setPackageCount(2);
@@ -144,7 +208,11 @@ class LoadingCompletedOutboxServiceTest {
.getAnnotation(Select.class).value()).replaceAll("\\s+", " ");
assertThat(rowsSql).contains("p.plan_status = 'COMPLETED'")
.contains("o.process_status = 'LOADED'");
.contains("o.process_status = 'LOADED'")
.contains("b.customer_id AS booking_customer_id")
.contains("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)) {
assertThat(sql)
.contains("actual_pick.loading_plan_item_id = i.id")
@@ -158,6 +226,7 @@ class LoadingCompletedOutboxServiceTest {
.doesNotContain("'PICKING'");
}
assertThat(palletsSql)
.contains("a.pallet_id", "a.reservation_id")
.contains("label.tenant_id = i.account_id")
.contains("label.inbound_detail_id = i.inbound_detail_id")
.contains("label.batch_status = 'ACTIVE'")
@@ -219,20 +288,26 @@ 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.setAccountId(account); r.setSourceInboundId(inbound); r.setRootInboundId(99L);
r.setWarehouseName("WH"); r.setBookingCustomerId(account); 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));
r.setAccountId(account); r.setSourceInboundId(inbound); r.setRootInboundId(99L);
r.setSourceOrderId(inbound+1000); r.setSourceOrderNo("FL"+inbound); r.setRootOrderId(1099L);
r.setInboundCode("RK"+inbound); r.setLoadingPlanItemId(detail); r.setDestinationWarehouseId(93L); r.setDestinationWarehouseCode("ONT8");
r.setDestinationWarehouseName("ONT8"); r.setSourceInboundDetailId(detail); r.setSkuCode("SKU"+detail);
r.setDestinationType("OVERSEAS");
r.setFulfillmentType("STANDARD");
r.setPackageCount(1); r.setProductQuantity(10); r.setPalletCount(1); r.setGrossWeightKg(BigDecimal.ONE);
r.setNetWeightKg(BigDecimal.ONE); r.setVolumeCbm(BigDecimal.ONE); return r;
r.setNetWeightKg(BigDecimal.ONE); r.setDimensionalWeightKg(new BigDecimal("2"));
r.setVolumeCbm(BigDecimal.ONE); return r;
}
private PalletRow pallet(long account,long inbound,int allocation,String palletNo,int allocated,
long detail,String sku,int packages,int quantity,String boxCode) {
PalletRow row = new PalletRow(); row.setAccountId(account); row.setSourceInboundId(inbound);
row.setPalletId(inbound * 100 + allocation); row.setReservationId(inbound * 1000 + allocation);
row.setAllocationNo(allocation); row.setPalletCode(palletNo); row.setAllocatedPackageCount(allocated);
row.setSourceInboundDetailId(detail); row.setSkuCode(sku); row.setProductNameZh("产品");
row.setPackageCount(packages); row.setProductQuantity(quantity); row.setBoxCode(boxCode); return row;
@@ -0,0 +1,74 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.entity.LoadingCompletionCommandEntity;
import com.baiyue.firstleg.repository.LoadingCompletionCommandRepository;
import com.baiyue.firstleg.repository.LoadingPickRepository;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.time.LocalDateTime;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(LoadingCompletionWorker.class)
class LoadingCompletionWorkerTest {
@MockBean private LoadingCompletionCommandRepository commands;
@MockBean private LoadingPickRepository picks;
@MockBean private LoadingPickService loadingPicks;
@jakarta.annotation.Resource private LoadingCompletionWorker worker;
@Test
void transientFailureIsReleasedForBackoffRetry() {
LoadingCompletionCommandEntity command = command(1);
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("WAREHOUSE_SETTLEMENT",
new BizException(ResultCode.SERVICE_UNAVAILABLE, "warehouse timeout")))
.when(loadingPicks).processCompletion(eq(command), any(String.class));
when(commands.fail(eq(900L), any(String.class), eq("RETRYABLE"), any(LocalDateTime.class),
eq("WAREHOUSE_SETTLEMENT"), eq(ResultCode.SERVICE_UNAVAILABLE.code()), eq(null), eq(null)))
.thenReturn(true);
worker.dispatch();
verify(commands).fail(eq(900L), any(String.class), eq("RETRYABLE"), any(LocalDateTime.class),
eq("WAREHOUSE_SETTLEMENT"), eq(ResultCode.SERVICE_UNAVAILABLE.code()), eq(null), eq(null));
}
@Test
void fifthFailureBecomesTerminalAndMarksTaskFailed() {
LoadingCompletionCommandEntity command = command(4);
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("PLAN_COMPLETE",
new BizException(ResultCode.CONFLICT, "plan conflict")))
.when(loadingPicks).processCompletion(eq(command), any(String.class));
when(commands.fail(eq(900L), any(String.class), eq("FAILED"), any(LocalDateTime.class),
eq("PLAN_COMPLETE"), 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("PLAN_COMPLETE"), 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);
command.setPickTaskId(20L);
command.setOperationId("complete-1");
command.setAttemptCount(attemptCount);
return command;
}
}
@@ -9,6 +9,7 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.BatchRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.BatchItemRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.ClaimRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
@@ -26,6 +27,7 @@ import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService;
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
import com.baiyue.firstleg.mapper.LoadingPickItemMapper;
import com.baiyue.firstleg.repository.LoadingPickRepository;
import com.baiyue.firstleg.repository.LoadingCompletionCommandRepository;
import com.baiyue.firstleg.repository.SmartLoadingRepository;
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
@@ -34,6 +36,7 @@ import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.InOrder;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@@ -44,6 +47,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder;
@@ -53,6 +57,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.clearInvocations;
import static org.assertj.core.api.Assertions.assertThat;
@SpringJUnitConfig(LoadingPickService.class)
@@ -60,6 +65,8 @@ class LoadingPickServiceTest {
@MockBean
private LoadingPickRepository repository;
@MockBean
private LoadingCompletionCommandRepository completionCommands;
@MockBean
private SmartLoadingRepository smartLoadingRepository;
@MockBean
private OceanBookingRepository oceanBookingRepository;
@@ -85,9 +92,13 @@ class LoadingPickServiceTest {
ContainerBookingEntity booking = new ContainerBookingEntity();
booking.setId(99L);
booking.setContainerNo("CSLU1234567");
booking.setWarehouseId(101L);
booking.setSourceType("SIMULATION");
booking.setRecordStatus("ACTIVE");
when(repository.activeBookingByTask(20L)).thenReturn(booking);
when(completionCommands.insert(any())).thenReturn(1);
when(completionCommands.renew(anyLong(), anyString(), any(java.time.LocalDateTime.class))).thenReturn(true);
when(repository.acceptCompletion(anyLong(), anyInt())).thenReturn(1);
}
@Test
@@ -232,6 +243,7 @@ class LoadingPickServiceTest {
task.setContainerBookingId(40L); task.setCabinetNo("CAB-001");
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
booking.setId(40L); booking.setContainerNo("MSCU-NEW");
booking.setWarehouseId(101L);
booking.setSourceType("SIMULATION"); booking.setRecordStatus("ACTIVE");
when(repository.taskByCabinet("CAB-001", 101L)).thenReturn(task);
when(repository.taskByPlan(10L, 101L)).thenReturn(task);
@@ -414,6 +426,37 @@ class LoadingPickServiceTest {
assertThat(result.sourceType()).isEqualTo("SIMULATION");
assertThat(result.recordStatus()).isEqualTo("ACTIVE");
assertThat(result.mergedIntoContainerBookingId()).isNull();
verify(completionCommands, never()).latest(20L);
}
@Test
void loadingTaskDetailDoesNotDependOnCompletionCommandStorage() {
LoadingPickTaskEntity task = task();
when(repository.task(20L)).thenReturn(task);
when(repository.items(20L, 101L)).thenReturn(List.of(item()));
when(repository.cartonLabels(20L)).thenReturn(List.of());
when(repository.inboundOwners(20L)).thenReturn(List.of());
doThrow(new IllegalStateException("completion command schema unavailable"))
.when(completionCommands).latest(20L);
var result = service.task(20L, 101L);
assertThat(result.taskStatus()).isEqualTo("LOADING");
assertThat(result.completionCommand()).isNull();
verify(completionCommands, never()).latest(20L);
}
@Test
void taskDetailRejectsWarehouseThatDoesNotOwnTheContainer() {
LoadingPickTaskEntity task = task();
when(repository.task(20L)).thenReturn(task);
assertThatThrownBy(() -> service.task(20L, 999L))
.isInstanceOf(BizException.class).hasMessageContaining("不属于当前仓库");
verify(repository, never()).items(20L, 999L);
verify(repository, never()).cartonLabels(20L);
verify(completionCommands, never()).latest(20L);
}
@Test
@@ -488,6 +531,7 @@ class LoadingPickServiceTest {
}
@Test
@org.junit.jupiter.api.Disabled("Replaced by durable asynchronous completion command contract")
void manualCompletionSettlesResolvedItemsCompletesPlanAndReplaysSameOperation() {
LoadingPickTaskEntity task = task();
LoadingPickItemEntity item = item();
@@ -538,6 +582,7 @@ class LoadingPickServiceTest {
}
@Test
@org.junit.jupiter.api.Disabled("Replaced by durable asynchronous completion command contract")
void conflictingTruckAppointmentsDoNotBlockPdaCompletion() {
LoadingPickTaskEntity task = task();
LoadingPickItemEntity item = item();
@@ -568,6 +613,7 @@ class LoadingPickServiceTest {
}
@Test
@org.junit.jupiter.api.Disabled("Replaced by durable asynchronous completion command contract")
void doesNotPublishCompletedEventWhenInboundFinalizationFails() {
LoadingPickTaskEntity task = task();
when(repository.taskForUpdate(20L)).thenReturn(task);
@@ -592,6 +638,7 @@ class LoadingPickServiceTest {
}
@Test
@org.junit.jupiter.api.Disabled("Replaced by durable asynchronous completion command contract")
void translatesUnexpectedSettlementFailureIntoActionableStageMessage() {
LoadingPickTaskEntity task = task();
when(repository.taskForUpdate(20L)).thenReturn(task);
@@ -613,6 +660,199 @@ class LoadingPickServiceTest {
verify(loadingListService, never()).finishFromPda(anyLong(), anyLong(), anyLong(), anyString());
}
@Test
@org.junit.jupiter.api.Disabled("Replaced by durable asynchronous completion command contract")
void warehouseSettlementConflictLeavesCompletionClaimAndDownstreamFinalizationUnwritten() {
LoadingPickTaskEntity task = task();
task.setVersion(1);
when(repository.taskForUpdate(20L)).thenReturn(task);
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
var owner = new LoadingPickItemMapper.InboundOwner(101L, 50L);
when(repository.inboundOwners(20L)).thenReturn(List.of(owner));
when(repository.inboundForUpdate(101L, 50L)).thenReturn(
new LoadingInboundMapper.Snapshot(50L, 101L, "LOADING", 7));
doThrow(new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT,
"释放库位资源时发生并发变更"))
.when(settlementService).settle(20L, 9L, "operator");
assertThatThrownBy(() -> service.complete(20L,
new CompleteLoadingRequest(1, "complete-real-a0409", "PDA确认装柜完成"), 9L, "operator"))
.isInstanceOf(BizException.class)
.hasMessageContaining("释放库位资源时发生并发变更");
verify(repository, never()).approve(anyLong(), anyLong(), anyString(), anyString());
verify(repository, never()).insert(any(LoadingPickOperationEntity.class));
verify(repository, never()).syncInboundProjections(anyLong(), anyLong(), anyString());
verify(loadingListService, never()).finishFromPda(anyLong(), anyLong(), anyLong(), anyString());
verify(loadingListService, never()).publishCompletedEvent(anyLong());
verify(loadingDeliveryService, never()).completeAfterPdaSettlement(anyLong(), anyLong(), anyLong(),
anyLong(), anyString(), anyString(), anyString());
}
@Test
@org.junit.jupiter.api.Disabled("Replaced by durable asynchronous completion command contract")
void currentCompleteRequestFinishesAfterUniqueResourceSettlementSucceeds() {
LoadingPickTaskEntity task = task();
task.setVersion(1);
LoadingPickItemEntity item = item();
when(repository.taskForUpdate(20L)).thenReturn(task);
when(repository.task(20L)).thenReturn(task);
when(repository.items(20L)).thenReturn(List.of(item));
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
when(repository.allPlanItemsPicked(10L)).thenReturn(true);
when(repository.approve(20L, 9L, "operator", "PDA确认装柜完成")).thenReturn(1);
var owner = new LoadingPickItemMapper.InboundOwner(101L, 50L);
when(repository.inboundOwners(20L)).thenReturn(List.of(owner));
when(repository.inboundForUpdate(101L, 50L)).thenReturn(
new LoadingInboundMapper.Snapshot(50L, 101L, "LOADING", 7));
Task result = service.complete(20L, new CompleteLoadingRequest(1,
"complete-1000001-2092817774453780481-2092787765815078913--45b8eb0887ff317c",
"PDA确认装柜完成"), 9L, "operator");
assertThat(result.id()).isEqualTo(20L);
verify(settlementService).settle(20L, 9L, "operator");
verify(repository).insert(org.mockito.ArgumentMatchers.argThat(
(LoadingPickOperationEntity operation) -> "COMPLETE_LOADING".equals(operation.getActionType())));
verify(loadingListService).finishFromPda(10L, 20L, 9L, "operator");
verify(loadingListService).publishCompletedEvent(10L);
}
@Test
void sameOperationIdReplayReturnsProcessingWithoutAnotherTaskForUpdate() {
LoadingPickTaskEntity loading = task();
LoadingPickTaskEntity completing = task();
completing.setTaskStatus("COMPLETING");
completing.setVersion(1);
AtomicReference<com.baiyue.firstleg.entity.LoadingCompletionCommandEntity> accepted = new AtomicReference<>();
when(completionCommands.operation("async-complete-1")).thenAnswer(invocation -> accepted.get());
doAnswer(invocation -> {
accepted.set(invocation.getArgument(0));
return 1;
}).when(completionCommands).insert(any());
when(repository.taskForUpdate(20L)).thenReturn(loading);
when(repository.task(20L)).thenReturn(completing);
when(repository.items(20L)).thenReturn(List.of(item()));
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
when(repository.inboundOwners(20L)).thenReturn(List.of(new LoadingPickItemMapper.InboundOwner(101L, 50L)));
Task first = service.complete(20L,
new CompleteLoadingRequest(0, "async-complete-1", "checked"), 9L, "operator");
verify(repository).taskForUpdate(20L);
clearInvocations(repository);
Task replay = org.junit.jupiter.api.Assertions.assertTimeout(java.time.Duration.ofMillis(200), () ->
service.complete(20L,
new CompleteLoadingRequest(0, "async-complete-1", "checked"), 9L, "operator"));
assertThat(first.taskStatus()).isEqualTo("COMPLETING");
assertThat(first.workflowStatus()).isEqualTo("PROCESSING");
assertThat(replay.taskStatus()).isEqualTo("COMPLETING");
verify(repository, never()).taskForUpdate(20L);
verify(completionCommands, times(1)).insert(any());
verify(settlementService, never()).settleOwner(anyLong(), anyLong(), anyLong(), anyString());
verify(loadingListService, never()).finishFromPda(anyLong(), anyLong(), anyLong(), anyString());
}
@Test
void differentOperationIdIsRejectedImmediatelyWithConflict() {
// different operationId is a canonical A0409 CONFLICT while the durable command is active.
LoadingPickTaskEntity loading = task();
when(repository.taskForUpdate(20L)).thenReturn(loading);
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
when(repository.inboundOwners(20L)).thenReturn(List.of(new LoadingPickItemMapper.InboundOwner(101L, 50L)));
var active = new com.baiyue.firstleg.entity.LoadingCompletionCommandEntity();
active.setOperationId("already-processing");
when(completionCommands.active(20L)).thenReturn(active);
assertThatThrownBy(() -> service.complete(20L,
new CompleteLoadingRequest(0, "different-operation", "checked"), 9L, "operator"))
.isInstanceOf(BizException.class).hasMessageContaining("其他装柜完成命令");
verify(completionCommands, never()).insert(any());
verify(settlementService, never()).settleOwner(anyLong(), anyLong(), anyLong(), anyString());
}
@Test
void failedSameOperationIdCanResumeWithoutCreatingAnotherCommand() {
LoadingPickTaskEntity failed = task();
failed.setTaskStatus("COMPLETION_FAILED");
LoadingPickTaskEntity completing = task();
completing.setTaskStatus("COMPLETING");
var command = completionCommand(902L, "resume-same-operation");
command.setRequestHash("48ea6be16079a761f4e77abcf58efaafebc2ab629250ce06118315232e9f3070");
command.setCommandStatus("FAILED");
command.setExpectedTaskVersion(0);
when(completionCommands.operation("resume-same-operation")).thenReturn(command);
when(completionCommands.resume(902L)).thenReturn(true);
when(repository.taskForUpdate(20L)).thenReturn(failed);
when(repository.resumeCompletion(20L)).thenReturn(1);
when(repository.task(20L)).thenReturn(completing);
when(repository.items(20L)).thenReturn(List.of(item()));
when(repository.inboundOwners(20L)).thenReturn(List.of(new LoadingPickItemMapper.InboundOwner(101L, 50L)));
Task result = service.complete(20L,
new CompleteLoadingRequest(0, "resume-same-operation", "checked"), 9L, "operator");
assertThat(result.workflowStatus()).isEqualTo("PROCESSING");
verify(completionCommands).resume(902L);
verify(repository).resumeCompletion(20L);
verify(completionCommands, never()).insert(any());
}
@Test
void workerProcessesFifteenInboundOwnersInStableOrderAndFinalizesOnce() {
LoadingPickTaskEntity completing = task();
completing.setTaskStatus("COMPLETING");
when(repository.task(20L)).thenReturn(completing);
List<LoadingPickItemMapper.InboundOwner> owners = java.util.stream.IntStream.range(0, 15)
.mapToObj(index -> new LoadingPickItemMapper.InboundOwner(101L, 1014L - index)).toList();
when(repository.inboundOwners(20L)).thenReturn(owners);
when(repository.allPlanItemsPicked(10L)).thenReturn(true);
when(repository.review(20L)).thenReturn(null);
when(repository.approve(20L, 9L, "operator", "checked")).thenReturn(1);
when(repository.completeAccepted(20L, 9L, "operator")).thenReturn(1);
when(completionCommands.complete(900L, "worker-1")).thenReturn(true);
var command = completionCommand(900L, "async-15");
service.processCompletion(command, "worker-1");
ArgumentCaptor<Long> inboundIds = ArgumentCaptor.forClass(Long.class);
verify(settlementService, times(15)).settleOwner(eq(20L), inboundIds.capture(), eq(9L), eq("operator"));
assertThat(inboundIds.getAllValues()).containsExactly(
1000L, 1001L, 1002L, 1003L, 1004L, 1005L, 1006L, 1007L,
1008L, 1009L, 1010L, 1011L, 1012L, 1013L, 1014L);
verify(loadingListService).publishCompletedEvent(10L);
verify(repository).completeAccepted(20L, 9L, "operator");
verify(completionCommands).complete(900L, "worker-1");
}
@Test
void workerRestartResumesFromOwnerCheckpointsWithoutRepeatingSettlements() {
LoadingPickTaskEntity completing = task();
completing.setTaskStatus("COMPLETING");
when(repository.task(20L)).thenReturn(completing);
when(repository.inboundOwners(20L)).thenReturn(List.of(
new LoadingPickItemMapper.InboundOwner(101L, 50L),
new LoadingPickItemMapper.InboundOwner(101L, 60L)));
when(completionCommands.checkpoint(901L, 101L, 50L)).thenReturn("INBOUND_COMPLETED");
when(completionCommands.checkpoint(901L, 101L, 60L)).thenReturn("SETTLED");
when(repository.allPlanItemsPicked(10L)).thenReturn(true);
when(repository.approve(20L, 9L, "operator", "checked")).thenReturn(1);
when(repository.completeAccepted(20L, 9L, "operator")).thenReturn(1);
when(completionCommands.complete(901L, "worker-2")).thenReturn(true);
service.processCompletion(completionCommand(901L, "async-resume"), "worker-2");
verify(settlementService, never()).settleOwner(anyLong(), anyLong(), anyLong(), anyString());
verify(loadingDeliveryService, never()).completeAfterPdaSettlement(eq(101L), eq(50L), anyLong(),
anyLong(), anyString(), anyString(), anyString());
verify(loadingDeliveryService).completeAfterPdaSettlement(101L, 60L, 50L, 9L, "operator",
"PDA-async-resume-60", "checked");
verify(loadingListService).publishCompletedEvent(10L);
verify(repository).insert(org.mockito.ArgumentMatchers.<LoadingPickOperationEntity>argThat(
operation -> "COMPLETE_LOADING".equals(operation.getActionType())));
}
@Test
void atomicallySubmitsCompleteBatchAndReplaysSameResult() {
LoadingPickTaskEntity task = task();
@@ -814,6 +1054,21 @@ class LoadingPickServiceTest {
when(repository.items(20L, 101L)).thenReturn(List.of(item));
}
private static com.baiyue.firstleg.entity.LoadingCompletionCommandEntity completionCommand(
long id, String operationId) {
var command = new com.baiyue.firstleg.entity.LoadingCompletionCommandEntity();
command.setId(id);
command.setPickTaskId(20L);
command.setOperationId(operationId);
command.setRequestHash("hash");
command.setCommandStatus("PROCESSING");
command.setOperatorId(9L);
command.setOperatorName("operator");
command.setRemark("checked");
command.setAttemptCount(1);
return command;
}
private static LoadingPickTaskEntity task() {
LoadingPickTaskEntity task = new LoadingPickTaskEntity();
task.setId(20L);
@@ -179,10 +179,13 @@ class PdaPickSettlementServiceTest {
when(splits.lockParent(31L)).thenReturn(parent(20));
when(splits.lockLatestApprovedItemsForInbound(10001L, 31L))
.thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 20)));
when(allocations.lockConfirmed(10001L, 31L))
.thenReturn(List.of(allocation(1L, 1, 501L, 10), allocation(2L, 2, 502L, 10)));
InboundPalletAllocationEntity first = allocation(1L, 1, 501L, 10);
InboundPalletAllocationEntity second = allocation(2L, 2, 501L, 10);
first.setPalletId(801L);
second.setPalletId(801L);
when(allocations.lockConfirmed(10001L, 31L)).thenReturn(List.of(first, second));
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
new WarehousePalletLocationClient.SettlementResult(List.of(released(91L, 501L), released(92L, 502L)),
new WarehousePalletLocationClient.SettlementResult(List.of(released(91L, 501L), released(92L, 501L)),
List.of())));
writes(0, 20);
@@ -11,6 +11,7 @@ import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
@SpringBootTest(classes = TaillegRealProducerAcceptanceTest.AcceptanceApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = {
@@ -50,12 +51,29 @@ class TaillegRealProducerAcceptanceTest {
assertEquals(2, jdbc.queryForObject("SELECT JSON_LENGTH(JSON_EXTRACT(payload_json,'$.payload.orders')) " +
"FROM fl_inbound_outbox WHERE event_key=?", Integer.class,
eventKey));
assertEquals("真实验收客户", jdbc.queryForObject(
"SELECT JSON_UNQUOTE(JSON_EXTRACT(payload_json,'$.payload.orders[0].customerName')) " +
"FROM fl_inbound_outbox WHERE event_key=?", String.class, eventKey));
assertEquals("2026-09-30T12:00", jdbc.queryForObject(
"SELECT JSON_UNQUOTE(JSON_EXTRACT(payload_json,'$.payload.estimatedPortArrivalAt')) " +
"FROM fl_inbound_outbox WHERE event_key=?", String.class, eventKey));
assertNull(jdbc.queryForObject(
"SELECT JSON_UNQUOTE(JSON_EXTRACT(payload_json,'$.payload.actualPortArrivalAt')) " +
"FROM fl_inbound_outbox WHERE event_key=?", String.class, eventKey));
assertEquals(1, jdbc.queryForObject(
"SELECT JSON_EXTRACT(payload_json,'$.payload.orders[0].palletCount') " +
"FROM fl_inbound_outbox WHERE event_key=?", Integer.class, eventKey));
assertEquals("35.0", jdbc.queryForObject(
"SELECT JSON_UNQUOTE(JSON_EXTRACT(payload_json,'$.payload.orders[0].volumeWeightKg')) " +
"FROM fl_inbound_outbox WHERE event_key=?", String.class, eventKey));
}
private void seedCompletedLoadingPlan() {
jdbc.update("INSERT IGNORE INTO fl_container_booking(id,booking_no,container_no,cabinet_no,warehouse_id," +
"warehouse_name,container_type,created_by,updated_by) VALUES(?,?,?,?,?,?,?,1,1)",
BOOKING, MARKER + "-BOOK", MARKER + "-CNTR", MARKER + "-CAB", 92L, "真实验收海外仓", "40HQ");
"warehouse_name,customer_id,customer_name,estimated_port_arrival_at,container_type,created_by,updated_by) " +
"VALUES(?,?,?,?,?,?,?,?,?,?,1,1)",
BOOKING, MARKER + "-BOOK", MARKER + "-CNTR", MARKER + "-CAB", 92L, "真实验收海外仓",
CUSTOMER, "真实验收客户", java.time.LocalDateTime.of(2026, 9, 30, 12, 0), "40HQ");
jdbc.update("INSERT IGNORE INTO fl_loading_plan(id,plan_no,container_booking_id,plan_status,created_by," +
"updated_by,version) VALUES(?,?,?,'LOADING',1,1,0)", PLAN, MARKER + "-PLAN", BOOKING);
seedInbound(FIRST_INBOUND, 990022221L, MARKER + "-FIRST-IN", "FBA_SEA", "STANDARD", "LOADED", "ONT8");
@@ -66,12 +84,8 @@ class TaillegRealProducerAcceptanceTest {
MARKER + "-FIRST-IN", "FIRST-BOX-" + i, i, 3);
for (int i = 1; i <= 2; i++) seedLabel(990022550L + i, DROPSHIP_INBOUND, 990022222L, 990022422L,
MARKER + "-DROP-IN", "DROP-BOX-" + i, i, 2);
jdbc.update("INSERT IGNORE INTO fl_inbound_pallet_allocation(id,tenant_id,inbound_id,allocation_no," +
"warehouse_id,warehouse_resource_tenant_id,pallet_id,pallet_code,pallet_capacity_cbm," +
"allocated_volume_cbm,allocated_package_count,picked_package_count,location_id,location_code," +
"reservation_no,allocation_status) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'CONFIRMED')",
990022621L, CUSTOMER, DROPSHIP_INBOUND, 1, 92L, 0L, 990022721L, MARKER + "-PALLET-D",
1.2, 0.4, 2, 2, 990022821L, "A-01", MARKER + "-RES");
seedAllocation(990022620L, FIRST_INBOUND, 990022720L, 990022820L, 3, "F");
seedAllocation(990022621L, DROPSHIP_INBOUND, 990022721L, 990022821L, 2, "D");
jdbc.update("INSERT INTO fl_loading_pick_task(id,loading_plan_id,container_booking_id,cabinet_no," +
"container_no,task_status,started_by,started_by_name) VALUES(?,?,?,?,?,'LOADING',1,'真实验收')",
990022901L, PLAN, BOOKING, MARKER + "-CAB", MARKER + "-CNTR");
@@ -80,6 +94,16 @@ class TaillegRealProducerAcceptanceTest {
jdbc.update("UPDATE fl_loading_plan SET plan_status='COMPLETED',confirmed_at=NOW(3),version=version+1 WHERE id=?", PLAN);
}
private void seedAllocation(long id, long inboundId, long palletId, long locationId,
int packages, String suffix) {
jdbc.update("INSERT IGNORE INTO fl_inbound_pallet_allocation(id,tenant_id,inbound_id,allocation_no," +
"warehouse_id,warehouse_resource_tenant_id,pallet_id,pallet_code,pallet_capacity_cbm," +
"allocated_volume_cbm,allocated_package_count,picked_package_count,location_id,location_code," +
"reservation_id,reservation_no,allocation_status) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'CONFIRMED')",
id, CUSTOMER, inboundId, 1, 92L, 0L, palletId, MARKER + "-PALLET-" + suffix,
1.2, 0.4, packages, packages, locationId, "A-01", id + 1000, MARKER + "-RES-" + suffix);
}
private void seedPickItem(long id, long planItemId, long inboundId, long detailId, int packages) {
jdbc.update("INSERT INTO fl_loading_pick_item(id,pick_task_id,loading_plan_item_id,inbound_id," +
"inbound_detail_id,planned_package_count,picked_package_count,item_status) VALUES(?,?,?,?,?,?,?,'PICKED')",