优化
This commit is contained in:
@@ -179,6 +179,39 @@ class FinanceServiceValidationTest {
|
||||
assertThrows(RuntimeException.class, () -> service.createCost(9L, 2L, request, "k5"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsAuthoritativeContainerSnapshotBeforeFeeSourceArrival() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class, invocation ->
|
||||
"update".equals(invocation.getMethod().getName()) && invocation.getMethod().getReturnType() == int.class
|
||||
? 1 : RETURNS_DEFAULTS.answer(invocation));
|
||||
FinanceFileClient files = mock(FinanceFileClient.class);
|
||||
when(files.metadata("internal-token", 9L, 9L, 77L)).thenReturn(ApiResponse.success(
|
||||
new FinanceFileClient.Metadata(77L, 9L, "FINANCE_VOUCHER", "9", "cost.jpg", "sha", "image/jpeg", 9L)));
|
||||
when(jdbc.queryForList(startsWith("SELECT request_hash"), eq(9L), eq(2L), eq("COST_CREATE"), eq("container-cost-key")))
|
||||
.thenReturn(List.of());
|
||||
when(jdbc.queryForMap(startsWith("SELECT * FROM fin_cost WHERE id="), eq(701L)))
|
||||
.thenReturn(java.util.Map.of("id", 701L));
|
||||
doAnswer(invocation -> {
|
||||
org.springframework.jdbc.support.KeyHolder holder = invocation.getArgument(1);
|
||||
holder.getKeyList().add(java.util.Map.of("GENERATED_KEY", 701L));
|
||||
return 1;
|
||||
}).when(jdbc).update(any(org.springframework.jdbc.core.PreparedStatementCreator.class), any(org.springframework.jdbc.support.KeyHolder.class));
|
||||
FinanceService service = new FinanceService(jdbc, new ObjectMapper());
|
||||
ReflectionTestUtils.setField(service, "fileClient", files);
|
||||
ReflectionTestUtils.setField(service, "fileToken", "internal-token");
|
||||
var request = new FinanceService.CostRequest("C-3", "CONTAINER_SHARED", null, null, 88L,
|
||||
null, null, "MANUAL", "整柜成本", new BigDecimal("300.00"), List.of("77"), null);
|
||||
var snapshot = List.of(new FinanceService.ContainerOrderVolume(
|
||||
99L, 1001L, 88L, 9L, new BigDecimal("1.25")));
|
||||
|
||||
service.createCost(9L, 2L, request, snapshot, "container-cost-key");
|
||||
|
||||
verify(jdbc, never()).queryForObject(startsWith("SELECT COUNT(*) FROM fin_fee_source"), eq(Number.class), any(), any());
|
||||
verify(jdbc).update(startsWith("INSERT INTO fin_cost_allocation"), eq(9L), eq(701L), eq(99L),
|
||||
eq(1001L), eq(new BigDecimal("1.25")), eq(new BigDecimal("1.25")),
|
||||
any(BigDecimal.class), any(BigDecimal.class), eq(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsBillRevisionHistoryOnlyForOwningCustomer() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
|
||||
+20
@@ -120,6 +120,26 @@ public class PlatformInboundMeasurementController {
|
||||
tenantId, user.userId(), inboundId, command));
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "INBOUND_CARTON_MANUAL_ENTRY", resourceType = "INBOUND",
|
||||
resourceId = "#inboundId", description = "平台人工录入单箱测量")
|
||||
@Operation(summary = "平台人工录入单箱测量",
|
||||
description = "data 中传箱号和人工测量值,image 传当前箱图片,复用正式单箱覆盖规则。")
|
||||
@PostMapping(value = "/measurement/manual-cartons", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:measure:edit')")
|
||||
public ApiResponse<CartonMeasurementResult> createManualCarton(
|
||||
@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, request.scannedShipmentId());
|
||||
return ApiResponse.success(cartonMeasurements.upsertManualForInbound(
|
||||
tenantId, user.userId(), inboundId, command));
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "INBOUND_WEIGHT_OCR_RECORD", resourceType = "INBOUND",
|
||||
resourceId = "#inboundId", description = "登记称重图片 OCR 原值")
|
||||
@Operation(summary = "登记 OCR 重量结果", description = "按图片文件 ID 保存不可覆盖的 OCR 原值。")
|
||||
|
||||
@@ -64,7 +64,7 @@ public final class InboundMeasurementRequests {
|
||||
) {
|
||||
}
|
||||
|
||||
/** 平台联调使用;最终设备协议在外部适配层转换为相同领域命令。 */
|
||||
/** 平台设备联调及人工录入使用;设备协议在外部适配层转换为相同领域命令。 */
|
||||
public record CartonScanRequest(
|
||||
@NotBlank @Size(max = 128) String boxCode,
|
||||
@NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal length,
|
||||
|
||||
@@ -60,7 +60,8 @@ public class CartonMeasurementService {
|
||||
}
|
||||
|
||||
public CartonMeasurementResult upsert(long tenantId, long operatorId, CartonMeasurementCommand command) {
|
||||
return upsert(tenantId, operatorId, null, command);
|
||||
return upsert(tenantId, operatorId, null, command,
|
||||
MeasurementBatchService.SOURCE_THIRD_PARTY, "DEVICE");
|
||||
}
|
||||
|
||||
public CartonMeasurementResult upsertForInbound(long tenantId, long operatorId, long inboundId,
|
||||
@@ -68,11 +69,22 @@ public class CartonMeasurementService {
|
||||
if (inboundId <= 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "入库单ID必须大于0");
|
||||
}
|
||||
return upsert(tenantId, operatorId, inboundId, command);
|
||||
return upsert(tenantId, operatorId, inboundId, command,
|
||||
MeasurementBatchService.SOURCE_THIRD_PARTY, "DEVICE");
|
||||
}
|
||||
|
||||
public CartonMeasurementResult upsertManualForInbound(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,
|
||||
MeasurementBatchService.SOURCE_MANUAL, "PLATFORM");
|
||||
}
|
||||
|
||||
private CartonMeasurementResult upsert(long tenantId, long operatorId, Long expectedInboundId,
|
||||
CartonMeasurementCommand command) {
|
||||
CartonMeasurementCommand command,
|
||||
String sourceType, String sourceCode) {
|
||||
validateCommand(command);
|
||||
String boxCode = command.boxCode().trim();
|
||||
CartonLabelBindingResolver.Binding binding = requireBinding(tenantId, boxCode);
|
||||
@@ -85,7 +97,7 @@ public class CartonMeasurementService {
|
||||
UpsertOutcome outcome;
|
||||
try {
|
||||
outcome = transactionTemplate.execute(status -> persist(
|
||||
tenantId, operatorId, binding, boxCode, command, uploaded.fileId()));
|
||||
tenantId, operatorId, binding, boxCode, command, uploaded.fileId(), sourceType, sourceCode));
|
||||
} catch (RuntimeException exception) {
|
||||
cleanupImage(tenantId, operatorId, inbound.getId(), uploaded.fileId(), "回传落库失败补偿");
|
||||
throw exception;
|
||||
@@ -150,8 +162,8 @@ public class CartonMeasurementService {
|
||||
|
||||
private UpsertOutcome persist(long tenantId, long operatorId,
|
||||
CartonLabelBindingResolver.Binding binding,
|
||||
String boxCode,
|
||||
CartonMeasurementCommand command, long imageFileId) {
|
||||
String boxCode, CartonMeasurementCommand command, long imageFileId,
|
||||
String sourceType, String sourceCode) {
|
||||
InboundOrderEntity inbound = repository.lockInbound(tenantId, binding.inboundId());
|
||||
if (inbound == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
||||
@@ -166,7 +178,7 @@ public class CartonMeasurementService {
|
||||
if (created) {
|
||||
measurement = repository.findLatestEditableMeasurement(tenantId, inbound.getId());
|
||||
if (measurement == null) {
|
||||
measurement = newMeasurement(tenantId, operatorId, inbound.getId());
|
||||
measurement = newMeasurement(tenantId, operatorId, inbound.getId(), sourceType, sourceCode);
|
||||
repository.insertMeasurement(measurement);
|
||||
}
|
||||
item = new InboundMeasurementItemEntity();
|
||||
@@ -208,8 +220,8 @@ public class CartonMeasurementService {
|
||||
}
|
||||
|
||||
measurement.setStatus(MeasurementBatchService.STATUS_RECEIVED);
|
||||
measurement.setSourceType(MeasurementBatchService.SOURCE_THIRD_PARTY);
|
||||
measurement.setSourceCode("DEVICE");
|
||||
measurement.setSourceType(sourceType);
|
||||
measurement.setSourceCode(sourceCode);
|
||||
measurement.setDeviceCode(command.machineNo().trim());
|
||||
measurement.setMeasuredAt(command.measuredAt());
|
||||
measurement.setUpdatedAt(LocalDateTime.now());
|
||||
@@ -220,13 +232,14 @@ public class CartonMeasurementService {
|
||||
return new UpsertOutcome(result(measurement, item, created), oldImageFileId);
|
||||
}
|
||||
|
||||
private InboundMeasurementEntity newMeasurement(long tenantId, long operatorId, long inboundId) {
|
||||
private InboundMeasurementEntity newMeasurement(long tenantId, long operatorId, long inboundId,
|
||||
String sourceType, String sourceCode) {
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(IdWorker.getId());
|
||||
measurement.setTenantId(tenantId);
|
||||
measurement.setInboundId(inboundId);
|
||||
measurement.setSourceType(MeasurementBatchService.SOURCE_THIRD_PARTY);
|
||||
measurement.setSourceCode("DEVICE");
|
||||
measurement.setSourceType(sourceType);
|
||||
measurement.setSourceCode(sourceCode);
|
||||
measurement.setStatus(MeasurementBatchService.STATUS_RECEIVED);
|
||||
measurement.setCreatedBy(operatorId);
|
||||
measurement.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
@@ -427,6 +427,38 @@ paths:
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,按响应体 code 判断}
|
||||
'403': {description: 无测量编辑权限}
|
||||
/api/platform/v1/first-leg/inbounds/{inboundId}/measurement/manual-cartons:
|
||||
post:
|
||||
summary: 平台人工录入单箱测量
|
||||
description: >-
|
||||
需要 platform:firstleg:inbound:measure:edit 权限;人工录入独立记录为 MANUAL/PLATFORM,
|
||||
复用正式单箱绑定、图片、计算和覆盖规则,不影响设备扫描入口。
|
||||
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/first-leg/inbounds/{inboundId}/weight-results/{resultId}/confirm:
|
||||
post:
|
||||
summary: 确认 OCR 重量
|
||||
|
||||
+33
@@ -76,6 +76,39 @@ class PlatformCartonScanControllerTest {
|
||||
assertThat(command.getValue().image().getOriginalFilename()).isEqualTo("carton.jpg");
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualCartonRouteUsesTheDedicatedManualServiceMethod() 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.upsertManualForInbound(eq(10001L), eq(90001L), eq(70001L), any()))
|
||||
.thenReturn(new CartonMeasurementResult(30001L, 1, 40001L, "RK-001#5-1",
|
||||
null, 1, new BigDecimal("60.1"), new BigDecimal("40.2"), new BigDecimal("30.3"),
|
||||
new BigDecimal("12.5"), 99001L, "MANUAL-ADMIN",
|
||||
LocalDateTime.parse("2026-08-12T14:00: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":"MANUAL-ADMIN","measuredAt":"2026-08-12T14:00: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/manual-cartons", 70001L)
|
||||
.file(image).part(data).param("tenantId", "10001"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value("200"));
|
||||
|
||||
verify(cartonMeasurements).upsertManualForInbound(eq(10001L), eq(90001L), eq(70001L), any());
|
||||
}
|
||||
|
||||
private HandlerMethodArgumentResolver authenticationPrincipal(AuthenticatedUser user) {
|
||||
return new HandlerMethodArgumentResolver() {
|
||||
@Override
|
||||
|
||||
@@ -93,6 +93,24 @@ class CartonMeasurementServiceTest {
|
||||
assertThat(insertedItem.get().getOuterLengthCm()).isEqualByComparingTo("64.000");
|
||||
assertThat(insertedItem.get().getOuterWidthCm()).isEqualByComparingTo("54.000");
|
||||
assertThat(insertedItem.get().getOuterHeightCm()).isEqualByComparingTo("54.000");
|
||||
assertThat(measurement.getSourceType()).isEqualTo(MeasurementBatchService.SOURCE_THIRD_PARTY);
|
||||
assertThat(measurement.getSourceCode()).isEqualTo("DEVICE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualEntryCreatesManualMeasurementWithoutChangingDeviceEntrySemantics() {
|
||||
InboundMeasurementEntity measurement = measurement(30001L, "RECEIVED", 0);
|
||||
when(repository.findItemByBoxCode(10001L, "RK-001#5-1")).thenReturn(null);
|
||||
when(repository.findLatestEditableMeasurement(10001L, 70001L)).thenReturn(measurement);
|
||||
when(repository.listItems(10001L, 30001L)).thenAnswer(invocation -> List.of(insertedItem.get()));
|
||||
when(repository.updateMeasurement(measurement)).thenReturn(true);
|
||||
|
||||
CartonMeasurementResult result = service.upsertManualForInbound(
|
||||
10001L, 90001L, 70001L, acceptanceCommand());
|
||||
|
||||
assertThat(result.created()).isTrue();
|
||||
assertThat(measurement.getSourceType()).isEqualTo(MeasurementBatchService.SOURCE_MANUAL);
|
||||
assertThat(measurement.getSourceCode()).isEqualTo("PLATFORM");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user