This commit is contained in:
2026-08-07 18:03:56 +08:00
parent 645c483649
commit 1a76602872
15 changed files with 273 additions and 6 deletions
@@ -45,6 +45,7 @@ class InboundOrderControllerContractTest {
.contains("/api/platform/inbounds:")
.contains("/api/platform/inbounds/{inboundId}:")
.contains("/api/platform/inbounds/{inboundId}/carton-label-pdf:")
.contains("/api/platform/v1/first-leg/inbounds/{inboundId}/measurement/carton-scans:")
.contains("required: false")
.contains("InboundOrderSummary:")
.contains("required: [id, tenantId]")
@@ -0,0 +1,91 @@
package com.baiyue.firstleg.controller;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.dto.CartonMeasurementCommand;
import com.baiyue.firstleg.dto.CartonMeasurementResult;
import com.baiyue.firstleg.service.CartonMeasurementService;
import com.baiyue.firstleg.service.InboundMeasurementService;
import com.baiyue.firstleg.service.MeasurementSubmissionService;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.mock.web.MockPart;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class PlatformCartonScanControllerTest {
@Test
void bindsJsonDataAndImageToTheFormalCartonMeasurementCommand() throws Exception {
CartonMeasurementService cartonMeasurements = mock(CartonMeasurementService.class);
PlatformInboundMeasurementController controller = new PlatformInboundMeasurementController();
ReflectionTestUtils.setField(controller, "service", mock(InboundMeasurementService.class));
ReflectionTestUtils.setField(controller, "submission", mock(MeasurementSubmissionService.class));
ReflectionTestUtils.setField(controller, "cartonMeasurements", cartonMeasurements);
AuthenticatedUser user = new AuthenticatedUser(90001L, 0L, 0L, null,
"platform_admin", "PLATFORM", "WEB", Set.of("platform:firstleg:inbound:measure:edit"));
MockMvc mvc = MockMvcBuilders.standaloneSetup(controller)
.setCustomArgumentResolvers(authenticationPrincipal(user))
.build();
when(cartonMeasurements.upsertForInbound(eq(10001L), eq(90001L), eq(70001L), any()))
.thenReturn(new CartonMeasurementResult(30001L, 1, 40001L, "RK-001#5-1", 1,
new BigDecimal("60.1"), new BigDecimal("40.2"), new BigDecimal("30.3"),
new BigDecimal("12.5"), 99001L, "SIM-01",
LocalDateTime.parse("2026-08-07T10:30:00"), true));
MockPart data = new MockPart("data", "data", ("""
{"boxCode":"RK-001#5-1","length":60.1,"width":40.2,"height":30.3,
"weight":12.5,"machineNo":"SIM-01","measuredAt":"2026-08-07T10:30:00"}
""").getBytes(java.nio.charset.StandardCharsets.UTF_8));
data.getHeaders().setContentType(MediaType.APPLICATION_JSON);
MockMultipartFile image = new MockMultipartFile(
"image", "carton.jpg", MediaType.IMAGE_JPEG_VALUE, new byte[] {1, 2, 3});
mvc.perform(multipart("/api/platform/v1/first-leg/inbounds/{inboundId}/measurement/carton-scans", 70001L)
.file(image).part(data).param("tenantId", "10001"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("200"))
.andExpect(jsonPath("$.data.boxCode").value("RK-001#5-1"));
ArgumentCaptor<CartonMeasurementCommand> command = ArgumentCaptor.forClass(CartonMeasurementCommand.class);
verify(cartonMeasurements).upsertForInbound(
eq(10001L), eq(90001L), eq(70001L), command.capture());
assertThat(command.getValue().machineNo()).isEqualTo("SIM-01");
assertThat(command.getValue().image().getOriginalFilename()).isEqualTo("carton.jpg");
}
private HandlerMethodArgumentResolver authenticationPrincipal(AuthenticatedUser user) {
return new HandlerMethodArgumentResolver() {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(AuthenticationPrincipal.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
org.springframework.web.bind.support.WebDataBinderFactory binderFactory) {
return user;
}
};
}
}
@@ -5,6 +5,7 @@ import com.baiyue.firstleg.dto.InboundMeasurementRequests;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.multipart.MultipartFile;
import java.lang.reflect.Method;
@@ -25,6 +26,9 @@ class PlatformInboundMeasurementControllerContractTest {
assertAuthority("updateConfirmedPackageCount", "hasAuthority('platform:firstleg:inbound:measure:edit')",
AuthenticatedUser.class, long.class, long.class, long.class,
InboundMeasurementRequests.ConfirmedPackageCountRequest.class);
assertAuthority("simulateCartonScan", "hasAuthority('platform:firstleg:inbound:measure:edit')",
AuthenticatedUser.class, long.class, long.class,
InboundMeasurementRequests.CartonScanRequest.class, MultipartFile.class);
assertAuthority("createWeightResult", "hasAuthority('platform:firstleg:inbound:weight:edit')",
AuthenticatedUser.class, long.class, long.class,
InboundMeasurementRequests.CreateWeightResultRequest.class);
@@ -111,6 +111,15 @@ class CartonMeasurementServiceTest {
verify(images, never()).upload(any(), any(Long.TYPE), any(Long.TYPE), any(Long.TYPE), any());
}
@Test
void platformSimulationRejectsBoxCodeFromAnotherInboundBeforeUploadingImage() {
assertThatThrownBy(() -> service.upsertForInbound(10001L, 90001L, 70002L, command()))
.isInstanceOf(BizException.class)
.hasMessageContaining("箱号不属于当前入库单");
verify(images, never()).upload(any(), any(Long.TYPE), any(Long.TYPE), any(Long.TYPE), any());
}
@Test
void confirmedPackageCountUsesMeasurementOptimisticVersion() {
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
@@ -47,6 +47,7 @@ class InboundCartonLabelServiceTest {
var first = cargo(80001L, 1, 3, "60", "40", "30");
when(orderCargoClient.listForCartonLabels("internal-token", 10001L, 60001L))
.thenReturn(ApiResponse.success(List.of(second, first)));
when(repository.initializeOriginalPackageCount(10001L, 70001L, 5)).thenReturn(true);
List<InboundCartonLabelEntity> inserted = new ArrayList<>();
org.mockito.Mockito.doAnswer(invocation -> {
inserted.add(invocation.getArgument(0));
@@ -63,12 +64,15 @@ class InboundCartonLabelServiceTest {
.containsExactly(80001L, 80001L, 80001L, 80002L, 80002L);
assertThat(batch.labels().get(0).boxSpecification()).isEqualTo("60x40x30cm");
assertThat(batch.labels().get(0).cartonText()).isEqualTo("CTNS:5-1");
assertThat(inbound.getTotalPackageCount()).isEqualTo(5);
assertThat(inserted).hasSize(5);
verify(repository).initializeOriginalPackageCount(10001L, 70001L, 5);
}
@Test
void repeatedAndPartialPrintsReadFrozenMappingWithoutReallocation() {
InboundOrderEntity inbound = inbound();
inbound.setTotalPackageCount(5);
List<InboundCartonLabelEntity> existing = labels(inbound, 5);
when(repository.lockInbound(10001L, 70001L)).thenReturn(inbound);
when(repository.findByInbound(10001L, 70001L)).thenReturn(existing);
@@ -82,6 +86,22 @@ class InboundCartonLabelServiceTest {
verify(repository, never()).insert(any());
}
@Test
void refusesFrozenMappingWhenOriginalPackageCountDoesNotMatch() {
InboundOrderEntity inbound = inbound();
inbound.setTotalPackageCount(4);
when(repository.lockInbound(10001L, 70001L)).thenReturn(inbound);
when(repository.findByInbound(10001L, 70001L)).thenReturn(labels(inbound, 5));
assertThatThrownBy(() -> service.getOrCreate(10001L, 70001L))
.isInstanceOf(BizException.class)
.hasMessageContaining("订单总箱数与已冻结箱号数量不一致");
verify(repository, never()).initializeOriginalPackageCount(
org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(),
org.mockito.ArgumentMatchers.anyInt());
}
@Test
void resolvesBoxCodeInsideCustomerBoundary() {
InboundCartonLabelEntity label = labels(inbound(), 1).get(0);
@@ -81,6 +81,24 @@ class InboundMeasurementServiceTest {
verify(repository, never()).updateInbound(any());
}
@Test
void creatingWeightResultNormalizesProviderConfidenceAtBusinessBoundary() {
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
InboundMeasurementResponses.WeightResult result = service.createWeightResult(
1L, 10L, 9L,
new InboundMeasurementRequests.CreateWeightResultRequest(
120L, "FILE_WEIGHT_OCR", "285.5", new BigDecimal("285.500"), "KG",
new BigDecimal("0.5097191334")));
ArgumentCaptor<InboundWeightResultEntity> saved =
ArgumentCaptor.forClass(InboundWeightResultEntity.class);
verify(repository).insertWeight(saved.capture());
assertThat(saved.getValue().getConfidence()).isEqualByComparingTo("0.50972");
assertThat(result.confidence()).isEqualByComparingTo("0.50972");
}
@Test
void confirmingLbWeightConvertsToKgAndRecalculatesOnlyConfirmedImages() {
InboundOrderEntity inbound = inbound("WAIT_MEASURE");