This commit is contained in:
2026-09-01 11:09:14 +08:00
191 changed files with 6486 additions and 403 deletions
@@ -24,6 +24,13 @@ class PlatformLoadingPdaControllerContractTest {
authority("review", "platform:firstleg:pda:review", AuthenticatedUser.class, long.class, ReviewRequest.class);
authority("complete", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class, CompleteLoadingRequest.class);
authority("operations", "platform:firstleg:pda:view", AuthenticatedUser.class, long.class);
authority("pallets", "platform:firstleg:pda:view", AuthenticatedUser.class, long.class, long.class);
authority("openPallet", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class,
com.baiyue.firstleg.dto.LoadingPalletDtos.OpenRequest.class);
authority("scanCarton", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class, long.class,
com.baiyue.firstleg.dto.LoadingPalletDtos.ScanRequest.class);
authority("sealPallet", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class, long.class,
com.baiyue.firstleg.dto.LoadingPalletDtos.SealRequest.class);
}
private void authority(String name, String expected, Class<?>... parameterTypes) throws Exception {
@@ -290,6 +290,30 @@ class LoadingCompletedOutboxServiceTest {
assertThat(event.payload().at("/orders/0/palletCount").asInt()).isEqualTo(2);
}
@Test void publishesExactPdaCartonToPalletBindingsWithoutSequentialRedistribution() throws Exception {
Row first = row(11, 100, 101); first.setFulfillmentType("ONE_CLICK_DROPSHIP");
first.setPackageCount(2); first.setProductQuantity(20); first.setSkuCode("SKU-A");
Row second = row(11, 100, 102); second.setFulfillmentType("ONE_CLICK_DROPSHIP");
second.setPackageCount(2); second.setProductQuantity(10); second.setSkuCode("SKU-B");
when(snapshots.rows(90L)).thenReturn(List.of(first, second));
when(snapshots.workPalletCount(90L)).thenReturn(2);
when(snapshots.actualPalletRows(90L)).thenReturn(List.of(
actualPallet(11,100,"PLT-01",101,"SKU-A","BOX-A2"),
actualPallet(11,100,"PLT-01",102,"SKU-B","BOX-B1"),
actualPallet(11,100,"PLT-02",101,"SKU-A","BOX-A1"),
actualPallet(11,100,"PLT-02",102,"SKU-B","BOX-B2")));
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/pallets/0/skuPackages/0/cartonMarks/0").asText())
.isEqualTo("BOX-A2");
assertThat(event.payload().at("/orders/0/pallets/1/skuPackages/0/cartonMarks/0").asText())
.isEqualTo("BOX-A1");
}
@Test
void pricingBusinessTypeCannotTurnAStandardOrderIntoDropship() throws Exception {
Row value = row(11, 100, 101);
@@ -347,6 +371,13 @@ class LoadingCompletedOutboxServiceTest {
row.setPackageCount(packages); row.setProductQuantity(quantity); row.setBoxCode(boxCode); return row;
}
private PalletRow actualPallet(long account,long inbound,String palletNo,long detail,
String sku,String boxCode) {
PalletRow row = pallet(account,inbound,0,palletNo,1,detail,sku,1,1,boxCode);
row.setActualBinding(true);
return row;
}
private LabelRow label(long detailId, String boxCode) {
LabelRow row = new LabelRow(); row.setInboundDetailId(detailId); row.setBoxCode(boxCode); return row;
}
@@ -0,0 +1,86 @@
package com.baiyue.firstleg.service;
import com.baiyue.firstleg.dto.LoadingPalletDtos.ScanRequest;
import com.baiyue.firstleg.dto.LoadingPalletDtos.SealRequest;
import com.baiyue.firstleg.entity.LoadingWorkPalletCartonEntity;
import com.baiyue.firstleg.entity.LoadingWorkPalletEntity;
import com.baiyue.firstleg.mapper.LoadingWorkPalletCartonMapper;
import com.baiyue.firstleg.mapper.LoadingWorkPalletCartonMapper.CartonCandidate;
import com.baiyue.firstleg.mapper.LoadingWorkPalletMapper;
import com.baiyue.firstleg.mapper.LoadingWorkPalletMapper.TaskContext;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(LoadingWorkPalletService.class)
class LoadingWorkPalletServiceTest {
@MockBean private LoadingWorkPalletMapper pallets;
@MockBean private LoadingWorkPalletCartonMapper cartons;
@Resource private LoadingWorkPalletService service;
@Test
void scansDropshipCartonOntoMixedCustomerWorkPalletWithoutChangingPickWorkflow() {
when(pallets.selectTaskContext(10L, 20L)).thenReturn(context());
LoadingWorkPalletEntity pallet = pallet(30L, "OPEN");
when(pallets.selectScoped(10L, 30L, 20L)).thenReturn(pallet);
CartonCandidate candidate = candidate();
when(cartons.selectCandidates(10L, 20L, "BOX-A")).thenReturn(List.of(candidate));
doAnswer(invocation -> {
LoadingWorkPalletCartonEntity value = invocation.getArgument(0);
value.setId(50L);
return 1;
}).when(cartons).insert(any(LoadingWorkPalletCartonEntity.class));
when(cartons.selectPalletCartons(30L)).thenAnswer(invocation -> List.of());
service.scan(10L, 30L, new ScanRequest(20L, "BOX-A", "SCAN-1"), 99L, "作业员");
var captor = org.mockito.ArgumentCaptor.forClass(LoadingWorkPalletCartonEntity.class);
verify(cartons).insert(captor.capture());
assertThat(captor.getValue().getTenantId()).isEqualTo(88L);
assertThat(captor.getValue().getLoadingPlanItemId()).isEqualTo(41L);
assertThat(captor.getValue().getSkuCode()).isEqualTo("SKU-A");
}
@Test
void refusesToSealUntilScannedCartonsAreReconciledByExistingPickSubmission() {
when(pallets.selectTaskContext(10L, 20L)).thenReturn(context());
when(pallets.selectScoped(10L, 30L, 20L)).thenReturn(pallet(30L, "OPEN"));
when(cartons.countPalletCartons(30L)).thenReturn(2);
when(cartons.countUnreconciled(10L)).thenReturn(1);
assertThatThrownBy(() -> service.seal(10L, 30L,
new SealRequest(20L, 0, "SEAL-1"), 99L, "作业员"))
.hasMessageContaining("原PDA拣货提交");
}
private static TaskContext context() {
return new TaskContext(10L, 11L, "LOADING", 20L, 99L, 2);
}
private static LoadingWorkPalletEntity pallet(long id, String status) {
LoadingWorkPalletEntity value = new LoadingWorkPalletEntity();
value.setId(id); value.setPickTaskId(10L); value.setLoadingPlanId(11L);
value.setWarehouseId(20L); value.setPalletCode("PLT-01");
value.setPalletStatus(status); value.setVersion(0);
return value;
}
private static CartonCandidate candidate() {
CartonCandidate value = new CartonCandidate();
value.setCartonLabelId(40L); value.setTenantId(88L); value.setInboundId(81L);
value.setInboundDetailId(82L); value.setBoxCode("BOX-A");
value.setLoadingPlanItemId(41L); value.setPickItemId(42L);
value.setSkuCode("SKU-A"); value.setUnitsPerCarton(20);
return value;
}
}
@@ -0,0 +1,88 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.event.EventEnvelope;
import com.baiyue.common.event.EventPayloadCanonicalizer;
import com.baiyue.common.event.EventPayloadHash;
import com.baiyue.common.event.EventTypes;
import com.baiyue.common.event.OverseasInboundOrderDetainedChangedV1;
import com.baiyue.common.event.RegionCode;
import com.baiyue.firstleg.entity.InboundHoldRecordEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.entity.ContainerEventInboxEntity;
import com.baiyue.firstleg.mapper.ContainerEventInboxMapper;
import com.baiyue.firstleg.mapper.InboundHoldRecordMapper;
import com.baiyue.firstleg.mapper.InboundOrderMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.Instant;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class OverseasInboundOrderDetainedConsumerTest {
@Mock InboundOrderMapper inboundOrders;
@Mock InboundHoldRecordMapper holdRecords;
@Mock ContainerEventInboxMapper inbox;
ObjectMapper objectMapper;
OverseasInboundOrderDetainedConsumer consumer;
@BeforeEach
void setUp() {
objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
consumer = new OverseasInboundOrderDetainedConsumer(inboundOrders, holdRecords, inbox, objectMapper);
}
@Test
void appliesHoldBySourceInboundIdAndWritesHistory() {
InboundOrderEntity inbound = inbound(null);
when(inboundOrders.selectActiveByIdForUpdate(1001L)).thenReturn(inbound);
when(inboundOrders.applyOverseasHold(88L, 1001L, true, "等待查验", 7L, 66L)).thenReturn(1);
consumer.consume(event(7, true, "等待查验"));
verify(inboundOrders).applyOverseasHold(88L, 1001L, true, "等待查验", 7L, 66L);
ArgumentCaptor<InboundHoldRecordEntity> history = ArgumentCaptor.forClass(InboundHoldRecordEntity.class);
verify(holdRecords).insert(history.capture());
assertThat(history.getValue().getInboundId()).isEqualTo(1001L);
assertThat(history.getValue().getHoldAction()).isEqualTo("HOLD");
verify(inbox).updateById(argThat((ContainerEventInboxEntity row) -> "PROCESSED".equals(row.getStatus())));
}
@Test
void staleReleaseCannotOverwriteNewerHoldState() {
when(inboundOrders.selectActiveByIdForUpdate(1001L)).thenReturn(inbound(9L));
consumer.consume(event(7, false, "解除"));
verify(inboundOrders, never()).applyOverseasHold(anyLong(), anyLong(), anyBoolean(), any(), anyLong(), anyLong());
verifyNoInteractions(holdRecords);
verify(inbox).updateById(argThat((ContainerEventInboxEntity row) -> "PROCESSED".equals(row.getStatus())));
}
private InboundOrderEntity inbound(Long sourceVersion) {
InboundOrderEntity value = new InboundOrderEntity();
value.setId(1001L); value.setTenantId(88L); value.setInboundCode("IN-1001");
value.setLastOverseasHoldVersion(sourceVersion);
return value;
}
private EventEnvelope event(long version, boolean detained, String reason) {
var payload = new OverseasInboundOrderDetainedChangedV1("1001", "IN-1001", "9001",
"LM-9001", detained, reason, "66", "海外操作员", Instant.parse("2026-08-31T06:00:00Z"));
var node = objectMapper.valueToTree(payload);
return new EventEnvelope(String.format("00000000-0000-4000-8000-%012d", version),
EventTypes.OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED, 1, payload.changedAt(),
RegionCode.OVERSEAS, RegionCode.CN, "88", "INBOUND_ORDER", "1001", version, 1L,
"00000000-0000-4000-8000-000000000099", "hold-1001-" + version, null, node,
EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)));
}
}