优化
This commit is contained in:
+24
@@ -10,6 +10,7 @@ import com.baiyue.common.security.AuthenticatedUser;
|
|||||||
import com.baiyue.firstleg.dto.InboundMeasurementRequests;
|
import com.baiyue.firstleg.dto.InboundMeasurementRequests;
|
||||||
import com.baiyue.firstleg.dto.InboundMeasurementResponses;
|
import com.baiyue.firstleg.dto.InboundMeasurementResponses;
|
||||||
import com.baiyue.firstleg.dto.CartonMeasurementResult;
|
import com.baiyue.firstleg.dto.CartonMeasurementResult;
|
||||||
|
import com.baiyue.firstleg.dto.CartonMeasurementCommand;
|
||||||
import com.baiyue.firstleg.service.CartonMeasurementService;
|
import com.baiyue.firstleg.service.CartonMeasurementService;
|
||||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||||
import com.baiyue.firstleg.service.InboundMeasurementService;
|
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.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
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.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
/** 平台后台入库测量录入、提交和 OCR 重量结果维护接口。 */
|
/** 平台后台入库测量录入、提交和 OCR 重量结果维护接口。 */
|
||||||
@Tag(name = "平台入库测量", description = "维护测量草稿、箱规明细和可追溯 OCR 重量结果。")
|
@Tag(name = "平台入库测量", description = "维护测量草稿、箱规明细和可追溯 OCR 重量结果。")
|
||||||
@@ -95,6 +99,26 @@ public class PlatformInboundMeasurementController {
|
|||||||
tenantId, inboundId, itemId, request.measurementVersion(), request.confirmedPackageCount()));
|
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<CartonMeasurementResult> 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",
|
@BusinessAudit(event = "INBOUND_WEIGHT_OCR_RECORD", resourceType = "INBOUND",
|
||||||
resourceId = "#inboundId", description = "登记称重图片 OCR 原值")
|
resourceId = "#inboundId", description = "登记称重图片 OCR 原值")
|
||||||
@Operation(summary = "登记 OCR 重量结果", description = "按图片文件 ID 保存不可覆盖的 OCR 原值。")
|
@Operation(summary = "登记 OCR 重量结果", description = "按图片文件 ID 保存不可覆盖的 OCR 原值。")
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import jakarta.validation.constraints.PositiveOrZero;
|
|||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
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(
|
public record CreateWeightResultRequest(
|
||||||
@NotNull @Positive Long fileId,
|
@NotNull @Positive Long fileId,
|
||||||
@Size(max = 64) String ocrProvider,
|
@Size(max = 64) String ocrProvider,
|
||||||
@Size(max = 1000) String ocrRawText,
|
@Size(max = 1000) String ocrRawText,
|
||||||
@DecimalMin("0.000") @Digits(integer = 15, fraction = 3) BigDecimal ocrWeightValue,
|
@DecimalMin("0.000") @Digits(integer = 15, fraction = 3) BigDecimal ocrWeightValue,
|
||||||
@Size(max = 8) String ocrWeightUnit,
|
@Size(max = 8) String ocrWeightUnit,
|
||||||
@DecimalMin("0.00000") @DecimalMax("1.00000")
|
@DecimalMin("0.00000") @DecimalMax("1.00000") BigDecimal confidence
|
||||||
@Digits(integer = 1, fraction = 5) BigDecimal confidence
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
@@ -1,6 +1,7 @@
|
|||||||
package com.baiyue.firstleg.repository;
|
package com.baiyue.firstleg.repository;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
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.InboundCartonLabelEntity;
|
||||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||||
import com.baiyue.firstleg.mapper.InboundCartonLabelMapper;
|
import com.baiyue.firstleg.mapper.InboundCartonLabelMapper;
|
||||||
@@ -36,6 +37,15 @@ public class InboundCartonLabelRepository {
|
|||||||
.eq(InboundCartonLabelEntity::getBoxCode, boxCode));
|
.eq(InboundCartonLabelEntity::getBoxCode, boxCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean initializeOriginalPackageCount(long tenantId, long inboundId, int packageCount) {
|
||||||
|
return inbounds.update(null, new LambdaUpdateWrapper<InboundOrderEntity>()
|
||||||
|
.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) {
|
public void insert(InboundCartonLabelEntity label) {
|
||||||
labels.insert(label);
|
labels.insert(label);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,9 +59,25 @@ public class CartonMeasurementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CartonMeasurementResult upsert(long tenantId, long operatorId, CartonMeasurementCommand command) {
|
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);
|
validateCommand(command);
|
||||||
String boxCode = command.boxCode().trim();
|
String boxCode = command.boxCode().trim();
|
||||||
CartonLabelBindingResolver.Binding binding = requireBinding(tenantId, boxCode);
|
CartonLabelBindingResolver.Binding binding = requireBinding(tenantId, boxCode);
|
||||||
|
if (expectedInboundId != null && binding.inboundId() != expectedInboundId) {
|
||||||
|
throw new BizException(ResultCode.NOT_FOUND, "箱号不属于当前入库单");
|
||||||
|
}
|
||||||
InboundOrderEntity inbound = requireDeviceWritableInbound(tenantId, binding.inboundId());
|
InboundOrderEntity inbound = requireDeviceWritableInbound(tenantId, binding.inboundId());
|
||||||
MeasurementImageClient.UploadResult uploaded = uploadImage(
|
MeasurementImageClient.UploadResult uploaded = uploadImage(
|
||||||
tenantId, operatorId, inbound.getId(), command.image());
|
tenantId, operatorId, inbound.getId(), command.image());
|
||||||
|
|||||||
@@ -41,7 +41,11 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
|||||||
if (inbound == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
if (inbound == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
||||||
|
|
||||||
List<InboundCartonLabelEntity> existing = new ArrayList<>(repository.findByInbound(tenantId, inboundId));
|
List<InboundCartonLabelEntity> existing = new ArrayList<>(repository.findByInbound(tenantId, inboundId));
|
||||||
if (!existing.isEmpty()) return batch(inbound, requireConsistent(existing));
|
if (!existing.isEmpty()) {
|
||||||
|
List<InboundCartonLabelEntity> consistent = requireConsistent(existing);
|
||||||
|
freezeOriginalPackageCount(inbound, consistent.get(0).getTotalCartonCount());
|
||||||
|
return batch(inbound, consistent);
|
||||||
|
}
|
||||||
if (!ALLOCATABLE_STATUSES.contains(inbound.getProcessStatus())) {
|
if (!ALLOCATABLE_STATUSES.contains(inbound.getProcessStatus())) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "当前入库单状态不允许首次生成箱号");
|
throw new BizException(ResultCode.CONFLICT, "当前入库单状态不允许首次生成箱号");
|
||||||
}
|
}
|
||||||
@@ -51,6 +55,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
|||||||
if (total < 1 || total > MAX_LABELS) {
|
if (total < 1 || total > MAX_LABELS) {
|
||||||
throw new BizException(ResultCode.VALIDATION_FAILED, "唛头箱数必须为1至2000箱");
|
throw new BizException(ResultCode.VALIDATION_FAILED, "唛头箱数必须为1至2000箱");
|
||||||
}
|
}
|
||||||
|
freezeOriginalPackageCount(inbound, total);
|
||||||
requirePrintableInboundCode(inbound.getInboundCode(), total);
|
requirePrintableInboundCode(inbound.getInboundCode(), total);
|
||||||
|
|
||||||
int cartonIndex = 0;
|
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,
|
private InboundCartonLabelEntity label(InboundOrderEntity inbound,
|
||||||
OrderCargoClient.CargoLabelBinding cargo,
|
OrderCargoClient.CargoLabelBinding cargo,
|
||||||
int total, int index, LocalDateTime now) {
|
int total, int index, LocalDateTime now) {
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ public class InboundMeasurementService {
|
|||||||
weight.setOcrRawText(request.ocrRawText());
|
weight.setOcrRawText(request.ocrRawText());
|
||||||
weight.setOcrWeightValue(request.ocrWeightValue());
|
weight.setOcrWeightValue(request.ocrWeightValue());
|
||||||
weight.setOcrWeightUnit(unit);
|
weight.setOcrWeightUnit(unit);
|
||||||
weight.setConfidence(request.confidence());
|
weight.setConfidence(normalizeConfidence(request.confidence()));
|
||||||
weight.setRecognitionStatus(isPositive(request.ocrWeightValue()) && isSupportedUnit(unit)
|
weight.setRecognitionStatus(isPositive(request.ocrWeightValue()) && isSupportedUnit(unit)
|
||||||
? WEIGHT_PENDING_CONFIRM : WEIGHT_NEEDS_MANUAL);
|
? WEIGHT_PENDING_CONFIRM : WEIGHT_NEEDS_MANUAL);
|
||||||
weight.setCreatedBy(operatorId);
|
weight.setCreatedBy(operatorId);
|
||||||
@@ -356,6 +356,17 @@ public class InboundMeasurementService {
|
|||||||
return unit.trim().toUpperCase(Locale.ROOT);
|
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) {
|
private boolean isSupportedUnit(String unit) {
|
||||||
if (!StringUtils.hasText(unit)) {
|
if (!StringUtils.hasText(unit)) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -192,6 +192,38 @@ paths:
|
|||||||
requestId: {type: string, nullable: true}
|
requestId: {type: string, nullable: true}
|
||||||
traceId: {type: string, nullable: true}
|
traceId: {type: string, nullable: true}
|
||||||
'403': {description: 无查看权限}
|
'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:
|
/api/platform/v1/inbounds/{inboundId}/measurements/approve:
|
||||||
post:
|
post:
|
||||||
summary: 审核通过测量
|
summary: 审核通过测量
|
||||||
|
|||||||
+1
@@ -45,6 +45,7 @@ class InboundOrderControllerContractTest {
|
|||||||
.contains("/api/platform/inbounds:")
|
.contains("/api/platform/inbounds:")
|
||||||
.contains("/api/platform/inbounds/{inboundId}:")
|
.contains("/api/platform/inbounds/{inboundId}:")
|
||||||
.contains("/api/platform/inbounds/{inboundId}/carton-label-pdf:")
|
.contains("/api/platform/inbounds/{inboundId}/carton-label-pdf:")
|
||||||
|
.contains("/api/platform/v1/first-leg/inbounds/{inboundId}/measurement/carton-scans:")
|
||||||
.contains("required: false")
|
.contains("required: false")
|
||||||
.contains("InboundOrderSummary:")
|
.contains("InboundOrderSummary:")
|
||||||
.contains("required: [id, tenantId]")
|
.contains("required: [id, tenantId]")
|
||||||
|
|||||||
+91
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
@@ -5,6 +5,7 @@ import com.baiyue.firstleg.dto.InboundMeasurementRequests;
|
|||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
@@ -25,6 +26,9 @@ class PlatformInboundMeasurementControllerContractTest {
|
|||||||
assertAuthority("updateConfirmedPackageCount", "hasAuthority('platform:firstleg:inbound:measure:edit')",
|
assertAuthority("updateConfirmedPackageCount", "hasAuthority('platform:firstleg:inbound:measure:edit')",
|
||||||
AuthenticatedUser.class, long.class, long.class, long.class,
|
AuthenticatedUser.class, long.class, long.class, long.class,
|
||||||
InboundMeasurementRequests.ConfirmedPackageCountRequest.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')",
|
assertAuthority("createWeightResult", "hasAuthority('platform:firstleg:inbound:weight:edit')",
|
||||||
AuthenticatedUser.class, long.class, long.class,
|
AuthenticatedUser.class, long.class, long.class,
|
||||||
InboundMeasurementRequests.CreateWeightResultRequest.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());
|
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
|
@Test
|
||||||
void confirmedPackageCountUsesMeasurementOptimisticVersion() {
|
void confirmedPackageCountUsesMeasurementOptimisticVersion() {
|
||||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class InboundCartonLabelServiceTest {
|
|||||||
var first = cargo(80001L, 1, 3, "60", "40", "30");
|
var first = cargo(80001L, 1, 3, "60", "40", "30");
|
||||||
when(orderCargoClient.listForCartonLabels("internal-token", 10001L, 60001L))
|
when(orderCargoClient.listForCartonLabels("internal-token", 10001L, 60001L))
|
||||||
.thenReturn(ApiResponse.success(List.of(second, first)));
|
.thenReturn(ApiResponse.success(List.of(second, first)));
|
||||||
|
when(repository.initializeOriginalPackageCount(10001L, 70001L, 5)).thenReturn(true);
|
||||||
List<InboundCartonLabelEntity> inserted = new ArrayList<>();
|
List<InboundCartonLabelEntity> inserted = new ArrayList<>();
|
||||||
org.mockito.Mockito.doAnswer(invocation -> {
|
org.mockito.Mockito.doAnswer(invocation -> {
|
||||||
inserted.add(invocation.getArgument(0));
|
inserted.add(invocation.getArgument(0));
|
||||||
@@ -63,12 +64,15 @@ class InboundCartonLabelServiceTest {
|
|||||||
.containsExactly(80001L, 80001L, 80001L, 80002L, 80002L);
|
.containsExactly(80001L, 80001L, 80001L, 80002L, 80002L);
|
||||||
assertThat(batch.labels().get(0).boxSpecification()).isEqualTo("60x40x30cm");
|
assertThat(batch.labels().get(0).boxSpecification()).isEqualTo("60x40x30cm");
|
||||||
assertThat(batch.labels().get(0).cartonText()).isEqualTo("CTNS:5-1");
|
assertThat(batch.labels().get(0).cartonText()).isEqualTo("CTNS:5-1");
|
||||||
|
assertThat(inbound.getTotalPackageCount()).isEqualTo(5);
|
||||||
assertThat(inserted).hasSize(5);
|
assertThat(inserted).hasSize(5);
|
||||||
|
verify(repository).initializeOriginalPackageCount(10001L, 70001L, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void repeatedAndPartialPrintsReadFrozenMappingWithoutReallocation() {
|
void repeatedAndPartialPrintsReadFrozenMappingWithoutReallocation() {
|
||||||
InboundOrderEntity inbound = inbound();
|
InboundOrderEntity inbound = inbound();
|
||||||
|
inbound.setTotalPackageCount(5);
|
||||||
List<InboundCartonLabelEntity> existing = labels(inbound, 5);
|
List<InboundCartonLabelEntity> existing = labels(inbound, 5);
|
||||||
when(repository.lockInbound(10001L, 70001L)).thenReturn(inbound);
|
when(repository.lockInbound(10001L, 70001L)).thenReturn(inbound);
|
||||||
when(repository.findByInbound(10001L, 70001L)).thenReturn(existing);
|
when(repository.findByInbound(10001L, 70001L)).thenReturn(existing);
|
||||||
@@ -82,6 +86,22 @@ class InboundCartonLabelServiceTest {
|
|||||||
verify(repository, never()).insert(any());
|
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
|
@Test
|
||||||
void resolvesBoxCodeInsideCustomerBoundary() {
|
void resolvesBoxCodeInsideCustomerBoundary() {
|
||||||
InboundCartonLabelEntity label = labels(inbound(), 1).get(0);
|
InboundCartonLabelEntity label = labels(inbound(), 1).get(0);
|
||||||
|
|||||||
@@ -81,6 +81,24 @@ class InboundMeasurementServiceTest {
|
|||||||
verify(repository, never()).updateInbound(any());
|
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
|
@Test
|
||||||
void confirmingLbWeightConvertsToKgAndRecalculatesOnlyConfirmedImages() {
|
void confirmingLbWeightConvertsToKgAndRecalculatesOnlyConfirmedImages() {
|
||||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||||
|
|||||||
+1
-1
@@ -143,7 +143,7 @@ public class WeightOcrBatchService {
|
|||||||
if (candidate == null || candidate.text() == null) {
|
if (candidate == null || candidate.text() == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String text = candidate.text().strip().replaceAll("[\\p{Cc}\\p{Cf}]+", " ");
|
String text = candidate.text().replaceAll("[\\p{Cc}\\p{Cf}]+", " ").strip();
|
||||||
if (text.isBlank()) {
|
if (text.isBlank()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -67,7 +67,7 @@ class WeightOcrBatchServiceTest {
|
|||||||
assertThat(result.recognized()).isEqualTo(1);
|
assertThat(result.recognized()).isEqualTo(1);
|
||||||
assertThat(result.results().get(0).status()).isEqualTo(Status.RECOGNIZED);
|
assertThat(result.results().get(0).status()).isEqualTo(Status.RECOGNIZED);
|
||||||
assertThat(result.results().get(0).confidence()).isEqualByComparingTo("0.50972");
|
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());
|
Path batchOutput = outputRoot.resolve(result.outputDirectory());
|
||||||
assertThat(batchOutput.resolve("cropped/weight.jpg")).isRegularFile();
|
assertThat(batchOutput.resolve("cropped/weight.jpg")).isRegularFile();
|
||||||
assertThat(batchOutput.resolve("raw/weight.json")).isRegularFile();
|
assertThat(batchOutput.resolve("raw/weight.json")).isRegularFile();
|
||||||
|
|||||||
Reference in New Issue
Block a user