diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementController.java index af28042cc..db9f3d215 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementController.java @@ -10,6 +10,7 @@ import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.firstleg.dto.InboundMeasurementRequests; import com.baiyue.firstleg.dto.InboundMeasurementResponses; import com.baiyue.firstleg.dto.CartonMeasurementResult; +import com.baiyue.firstleg.dto.CartonMeasurementCommand; import com.baiyue.firstleg.service.CartonMeasurementService; import com.baiyue.firstleg.service.FirstlegScopeGuard; import com.baiyue.firstleg.service.InboundMeasurementService; @@ -23,10 +24,13 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.MediaType; +import org.springframework.web.multipart.MultipartFile; /** 平台后台入库测量录入、提交和 OCR 重量结果维护接口。 */ @Tag(name = "平台入库测量", description = "维护测量草稿、箱规明细和可追溯 OCR 重量结果。") @@ -95,6 +99,26 @@ public class PlatformInboundMeasurementController { tenantId, inboundId, itemId, request.measurementVersion(), request.confirmedPackageCount())); } + @BusinessAudit(event = "INBOUND_CARTON_SCAN", resourceType = "INBOUND", + resourceId = "#inboundId", description = "平台模拟设备单箱扫描") + @Operation(summary = "平台模拟单箱设备扫描", + description = "联调入口;data 中传箱号和设备测量值,image 传当前箱图片,复用正式单箱覆盖规则。") + @PostMapping(value = "/measurement/carton-scans", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PreAuthorize("hasAuthority('platform:firstleg:inbound:measure:edit')") + public ApiResponse simulateCartonScan( + @AuthenticationPrincipal AuthenticatedUser user, + @RequestParam long tenantId, + @PathVariable long inboundId, + @Valid @RequestPart("data") InboundMeasurementRequests.CartonScanRequest request, + @RequestPart("image") MultipartFile image) { + requirePlatformTenant(user, tenantId); + CartonMeasurementCommand command = new CartonMeasurementCommand( + request.boxCode(), request.length(), request.width(), request.height(), request.weight(), + request.machineNo(), request.measuredAt(), image); + return ApiResponse.success(cartonMeasurements.upsertForInbound( + tenantId, user.userId(), inboundId, command)); + } + @BusinessAudit(event = "INBOUND_WEIGHT_OCR_RECORD", resourceType = "INBOUND", resourceId = "#inboundId", description = "登记称重图片 OCR 原值") @Operation(summary = "登记 OCR 重量结果", description = "按图片文件 ID 保存不可覆盖的 OCR 原值。") diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementRequests.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementRequests.java index 417473a77..6cdcff982 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementRequests.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementRequests.java @@ -13,6 +13,7 @@ import jakarta.validation.constraints.PositiveOrZero; import jakarta.validation.constraints.Size; import java.math.BigDecimal; +import java.time.LocalDateTime; import java.util.List; /** @@ -62,14 +63,25 @@ public final class InboundMeasurementRequests { ) { } + /** 平台联调使用;最终设备协议在外部适配层转换为相同领域命令。 */ + public record CartonScanRequest( + @NotBlank @Size(max = 128) String boxCode, + @NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal length, + @NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal width, + @NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal height, + @DecimalMin("0.001") @Digits(integer = 15, fraction = 3) BigDecimal weight, + @NotBlank @Size(max = 64) String machineNo, + @NotNull LocalDateTime measuredAt + ) { + } + public record CreateWeightResultRequest( @NotNull @Positive Long fileId, @Size(max = 64) String ocrProvider, @Size(max = 1000) String ocrRawText, @DecimalMin("0.000") @Digits(integer = 15, fraction = 3) BigDecimal ocrWeightValue, @Size(max = 8) String ocrWeightUnit, - @DecimalMin("0.00000") @DecimalMax("1.00000") - @Digits(integer = 1, fraction = 5) BigDecimal confidence + @DecimalMin("0.00000") @DecimalMax("1.00000") BigDecimal confidence ) { } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundCartonLabelRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundCartonLabelRepository.java index 00587e597..b7b8b8abe 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundCartonLabelRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundCartonLabelRepository.java @@ -1,6 +1,7 @@ package com.baiyue.firstleg.repository; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baiyue.firstleg.entity.InboundCartonLabelEntity; import com.baiyue.firstleg.entity.InboundOrderEntity; import com.baiyue.firstleg.mapper.InboundCartonLabelMapper; @@ -36,6 +37,15 @@ public class InboundCartonLabelRepository { .eq(InboundCartonLabelEntity::getBoxCode, boxCode)); } + public boolean initializeOriginalPackageCount(long tenantId, long inboundId, int packageCount) { + return inbounds.update(null, new LambdaUpdateWrapper() + .eq(InboundOrderEntity::getTenantId, tenantId) + .eq(InboundOrderEntity::getId, inboundId) + .isNull(InboundOrderEntity::getTotalPackageCount) + .set(InboundOrderEntity::getTotalPackageCount, packageCount) + .setSql("version = version + 1")) == 1; + } + public void insert(InboundCartonLabelEntity label) { labels.insert(label); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/CartonMeasurementService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/CartonMeasurementService.java index f1b0ba4ad..3767ad761 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/CartonMeasurementService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/CartonMeasurementService.java @@ -59,9 +59,25 @@ public class CartonMeasurementService { } public CartonMeasurementResult upsert(long tenantId, long operatorId, CartonMeasurementCommand command) { + return upsert(tenantId, operatorId, null, command); + } + + public CartonMeasurementResult upsertForInbound(long tenantId, long operatorId, long inboundId, + CartonMeasurementCommand command) { + if (inboundId <= 0) { + throw new BizException(ResultCode.BAD_REQUEST, "入库单ID必须大于0"); + } + return upsert(tenantId, operatorId, inboundId, command); + } + + private CartonMeasurementResult upsert(long tenantId, long operatorId, Long expectedInboundId, + CartonMeasurementCommand command) { validateCommand(command); String boxCode = command.boxCode().trim(); CartonLabelBindingResolver.Binding binding = requireBinding(tenantId, boxCode); + if (expectedInboundId != null && binding.inboundId() != expectedInboundId) { + throw new BizException(ResultCode.NOT_FOUND, "箱号不属于当前入库单"); + } InboundOrderEntity inbound = requireDeviceWritableInbound(tenantId, binding.inboundId()); MeasurementImageClient.UploadResult uploaded = uploadImage( tenantId, operatorId, inbound.getId(), command.image()); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundCartonLabelService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundCartonLabelService.java index a1847d431..73c1c9ea9 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundCartonLabelService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundCartonLabelService.java @@ -41,7 +41,11 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver { if (inbound == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在"); List existing = new ArrayList<>(repository.findByInbound(tenantId, inboundId)); - if (!existing.isEmpty()) return batch(inbound, requireConsistent(existing)); + if (!existing.isEmpty()) { + List consistent = requireConsistent(existing); + freezeOriginalPackageCount(inbound, consistent.get(0).getTotalCartonCount()); + return batch(inbound, consistent); + } if (!ALLOCATABLE_STATUSES.contains(inbound.getProcessStatus())) { throw new BizException(ResultCode.CONFLICT, "当前入库单状态不允许首次生成箱号"); } @@ -51,6 +55,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver { if (total < 1 || total > MAX_LABELS) { throw new BizException(ResultCode.VALIDATION_FAILED, "唛头箱数必须为1至2000箱"); } + freezeOriginalPackageCount(inbound, total); requirePrintableInboundCode(inbound.getInboundCode(), total); int cartonIndex = 0; @@ -146,6 +151,20 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver { } } + private void freezeOriginalPackageCount(InboundOrderEntity inbound, int total) { + Integer original = inbound.getTotalPackageCount(); + if (original != null && original != total) { + throw new BizException(ResultCode.CONFLICT, "订单总箱数与已冻结箱号数量不一致"); + } + if (original == null) { + if (!repository.initializeOriginalPackageCount(inbound.getTenantId(), inbound.getId(), total)) { + throw new BizException(ResultCode.CONFLICT, "订单总箱数已变化,请刷新后重试"); + } + inbound.setTotalPackageCount(total); + inbound.setVersion(inbound.getVersion() == null ? 1 : inbound.getVersion() + 1); + } + } + private InboundCartonLabelEntity label(InboundOrderEntity inbound, OrderCargoClient.CargoLabelBinding cargo, int total, int index, LocalDateTime now) { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java index e9fc6fbba..aeefd08f9 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java @@ -138,7 +138,7 @@ public class InboundMeasurementService { weight.setOcrRawText(request.ocrRawText()); weight.setOcrWeightValue(request.ocrWeightValue()); weight.setOcrWeightUnit(unit); - weight.setConfidence(request.confidence()); + weight.setConfidence(normalizeConfidence(request.confidence())); weight.setRecognitionStatus(isPositive(request.ocrWeightValue()) && isSupportedUnit(unit) ? WEIGHT_PENDING_CONFIRM : WEIGHT_NEEDS_MANUAL); weight.setCreatedBy(operatorId); @@ -356,6 +356,17 @@ public class InboundMeasurementService { return unit.trim().toUpperCase(Locale.ROOT); } + private BigDecimal normalizeConfidence(BigDecimal confidence) { + if (confidence == null) { + return null; + } + if (confidence.compareTo(BigDecimal.ZERO) < 0 + || confidence.compareTo(BigDecimal.ONE) > 0) { + throw new BizException(ResultCode.VALIDATION_FAILED, "OCR 置信度必须在 0 到 1 之间"); + } + return confidence.setScale(5, RoundingMode.HALF_UP); + } + private boolean isSupportedUnit(String unit) { if (!StringUtils.hasText(unit)) { return false; diff --git a/firstleg/src/main/resources/openapi/firstleg-v1.yaml b/firstleg/src/main/resources/openapi/firstleg-v1.yaml index 25bf76235..bd05e15ca 100644 --- a/firstleg/src/main/resources/openapi/firstleg-v1.yaml +++ b/firstleg/src/main/resources/openapi/firstleg-v1.yaml @@ -192,6 +192,38 @@ paths: requestId: {type: string, nullable: true} traceId: {type: string, nullable: true} '403': {description: 无查看权限} + /api/platform/v1/first-leg/inbounds/{inboundId}/measurement/carton-scans: + post: + summary: 平台模拟单箱设备扫描 + description: >- + 需要 platform:firstleg:inbound:measure:edit 权限;仅用于平台联调和代设备录入, + 复用正式单箱覆盖规则,不代表最终第三方设备协议或鉴权方式。 + parameters: + - {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} + - {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [data, image] + properties: + data: + type: object + required: [boxCode, length, width, height, machineNo, measuredAt] + properties: + boxCode: {type: string, maxLength: 128} + length: {type: number, format: double, minimum: 0.001} + width: {type: number, format: double, minimum: 0.001} + height: {type: number, format: double, minimum: 0.001} + weight: {type: number, format: double, minimum: 0.001, nullable: true} + machineNo: {type: string, maxLength: 64} + measuredAt: {type: string, format: date-time} + image: {type: string, format: binary} + responses: + '200': {description: 成功或业务失败,按响应体 code 判断} + '403': {description: 无测量编辑权限} /api/platform/v1/inbounds/{inboundId}/measurements/approve: post: summary: 审核通过测量 diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/InboundOrderControllerContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/InboundOrderControllerContractTest.java index 40fb20538..24fee87f7 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/InboundOrderControllerContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/InboundOrderControllerContractTest.java @@ -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]") diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformCartonScanControllerTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformCartonScanControllerTest.java new file mode 100644 index 000000000..575390695 --- /dev/null +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformCartonScanControllerTest.java @@ -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 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; + } + }; + } +} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementControllerContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementControllerContractTest.java index af8e1c488..880a8db23 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementControllerContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementControllerContractTest.java @@ -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); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java index 5205e533c..63e3d55df 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java @@ -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"); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundCartonLabelServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundCartonLabelServiceTest.java index 6393ce4c1..ac3948572 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundCartonLabelServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundCartonLabelServiceTest.java @@ -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 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 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); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java index 4bc8ca504..5a0766501 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java @@ -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 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"); diff --git a/integration-service/src/main/java/com/baiyue/integration/service/ocr/WeightOcrBatchService.java b/integration-service/src/main/java/com/baiyue/integration/service/ocr/WeightOcrBatchService.java index 5941ecada..6aa42a878 100644 --- a/integration-service/src/main/java/com/baiyue/integration/service/ocr/WeightOcrBatchService.java +++ b/integration-service/src/main/java/com/baiyue/integration/service/ocr/WeightOcrBatchService.java @@ -143,7 +143,7 @@ public class WeightOcrBatchService { if (candidate == null || candidate.text() == null) { continue; } - String text = candidate.text().strip().replaceAll("[\\p{Cc}\\p{Cf}]+", " "); + String text = candidate.text().replaceAll("[\\p{Cc}\\p{Cf}]+", " ").strip(); if (text.isBlank()) { continue; } diff --git a/integration-service/src/test/java/com/baiyue/integration/service/ocr/WeightOcrBatchServiceTest.java b/integration-service/src/test/java/com/baiyue/integration/service/ocr/WeightOcrBatchServiceTest.java index 52c211f71..baa90465c 100644 --- a/integration-service/src/test/java/com/baiyue/integration/service/ocr/WeightOcrBatchServiceTest.java +++ b/integration-service/src/test/java/com/baiyue/integration/service/ocr/WeightOcrBatchServiceTest.java @@ -67,7 +67,7 @@ class WeightOcrBatchServiceTest { assertThat(result.recognized()).isEqualTo(1); assertThat(result.results().get(0).status()).isEqualTo(Status.RECOGNIZED); assertThat(result.results().get(0).confidence()).isEqualByComparingTo("0.50972"); - assertThat(result.results().get(0).ocrRawText()).isEqualTo("12.35\nKG "); + assertThat(result.results().get(0).ocrRawText()).isEqualTo("12.35\nKG"); Path batchOutput = outputRoot.resolve(result.outputDirectory()); assertThat(batchOutput.resolve("cropped/weight.jpg")).isRegularFile(); assertThat(batchOutput.resolve("raw/weight.json")).isRegularFile();