This commit is contained in:
2026-08-17 17:49:03 +08:00
parent c71db4b481
commit 7c66097672
21 changed files with 1227 additions and 14 deletions
@@ -558,6 +558,26 @@ paths:
requestId: {type: string, nullable: true}
traceId: {type: string, nullable: true}
'403': {description: 无查看权限}
/api/platform/v1/first-leg/inbounds/{inboundId}/measurement:
get:
summary: 查询入库测量详情
description: >-
需要 platform:firstleg:inbound:measure:view 权限。尺寸测量图片只随 items 返回,
由稳定的 item.id(measurementItemId)与 intelligentImageFileId 关联;称重图片只随
weightResults 返回,由稳定的 weightResult.id 与 fileId 关联,并携带确认状态及服务端确认的
grossWeightKg/netWeightKg。两类证据不得合并为无明细归属的统一图片列表。
actualGrossWeightKg 是服务端权威的货物总重量;存在已确认称重结果时为所有已确认
netWeightKg 之和,消费者不得根据图片或 OCR 值自行重算。
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]*$'}}
responses:
'200':
description: 成功或业务失败,按响应体 code 判断
content:
application/json:
schema: {$ref: '#/components/schemas/InboundMeasurementDetailEnvelope'}
'403': {description: 无入库测量查看权限}
/api/platform/v1/first-leg/inbounds/{inboundId}/measurement/carton-scans:
post:
summary: 平台模拟单箱设备扫描
@@ -1204,6 +1224,96 @@ components:
confirmedBy: {type: string, nullable: true}
confirmedAt: {type: string, format: date-time, nullable: true}
version: {type: integer, minimum: 0}
InboundMeasurementItem:
type: object
description: >-
单条尺寸测量明细。id 是稳定 measurementItemId;intelligentImageFileId 仅表示该明细的
尺寸测量原图,不得与 weightResults.fileId 混用。
required: [id]
properties:
id: {type: string, pattern: '^[1-9][0-9]*$', description: 稳定 measurementItemId}
inboundDetailId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
itemNo: {type: string, nullable: true}
boxSpecification: {type: string, nullable: true}
packageCount: {type: integer, nullable: true}
lengthCm: {type: number, format: double, nullable: true}
widthCm: {type: number, format: double, nullable: true}
heightCm: {type: number, format: double, nullable: true}
girthCm: {type: number, format: double, nullable: true}
unitWeightKg: {type: number, format: double, nullable: true}
unitVolumeCbm: {type: number, format: double, nullable: true}
totalWeightKg: {type: number, format: double, nullable: true}
totalVolumeCbm: {type: number, format: double, nullable: true}
volumeWeightKg: {type: number, format: double, nullable: true}
intelligentImageFileId:
type: string
pattern: '^[1-9][0-9]*$'
nullable: true
description: 当前尺寸测量明细的 File 文件 ID;图片类型固定为 DIMENSION
overLimitWarning: {type: string, nullable: true}
outerLengthCm: {type: number, format: double, nullable: true}
outerWidthCm: {type: number, format: double, nullable: true}
outerHeightCm: {type: number, format: double, nullable: true}
remark: {type: string, nullable: true}
boxCode: {type: string, nullable: true}
scannedShipmentId: {type: string, nullable: true}
confirmedPackageCount: {type: integer, nullable: true}
machineNo: {type: string, nullable: true}
deviceMeasuredAt: {type: string, format: date-time, nullable: true}
updatedAt: {type: string, format: date-time, nullable: true}
InboundMeasurementDetail:
type: object
description: >-
入库测量权威详情。尺寸证据天然归属 items,称重证据天然归属 weightResults,响应不提供统一 images。
required: [inboundId, inboundVersion, items, weightResults]
properties:
inboundId: {type: string, pattern: '^[1-9][0-9]*$'}
inboundCode: {type: string, nullable: true}
companyName: {type: string, nullable: true}
channelCode: {type: string, nullable: true}
channelName: {type: string, nullable: true}
processStatus: {type: string, nullable: true}
measureReviewStatus: {type: string, nullable: true}
recommendedPalletType: {type: string, nullable: true}
reviewReason: {type: string, nullable: true}
reviewerId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
reviewerName: {type: string, nullable: true}
measurementId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
sourceType: {type: string, nullable: true}
measurementStatus: {type: string, nullable: true}
inboundVersion: {type: integer, minimum: 0}
version: {type: integer, minimum: 0, nullable: true}
originalDocumentNo: {type: string, nullable: true}
originalGrossWeightKg: {type: number, format: double, nullable: true}
originalVolumeCbm: {type: number, format: double, nullable: true}
originalPackageCount: {type: integer, nullable: true}
actualPackageCount: {type: integer, nullable: true}
actualGrossWeightKg:
type: number
format: double
nullable: true
multipleOf: 0.001
description: 服务端权威货物总重量 KG;存在已确认称重结果时为所有 CONFIRMED netWeightKg 之和
actualVolumeCbm: {type: number, format: double, nullable: true}
volumeWeightKg: {type: number, format: double, nullable: true}
actualGoodsName: {type: string, nullable: true}
items:
type: array
description: DIMENSION 尺寸测量明细及其图片证据
items: {$ref: '#/components/schemas/InboundMeasurementItem'}
weightResults:
type: array
description: WEIGHT 称重结果及其图片、确认状态和服务端重量事实
items: {$ref: '#/components/schemas/InboundWeightResult'}
InboundMeasurementDetailEnvelope:
type: object
required: [code, message]
properties:
code: {type: string, example: '200'}
message: {type: string}
data: {$ref: '#/components/schemas/InboundMeasurementDetail'}
requestId: {type: string, nullable: true}
traceId: {type: string, nullable: true}
ConfirmWeightResultRequest:
type: object
required: [version]
@@ -0,0 +1,48 @@
package com.baiyue.firstleg.controller;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.yaml.snakeyaml.Yaml;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class InboundMeasurementEvidenceOpenApiContractTest {
@Test
void measurementDetailSeparatesDimensionAndWeightEvidenceByStableRecord() throws Exception {
String source = new ClassPathResource("openapi/firstleg-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
Map<String, Object> document = stringMap(new Yaml().load(source));
Map<String, Object> paths = stringMap(document.get("paths"));
Map<String, Object> detailPath = stringMap(paths.get(
"/api/platform/v1/first-leg/inbounds/{inboundId}/measurement"));
assertThat(detailPath).containsKey("get");
Map<String, Object> schemas = stringMap(stringMap(document.get("components")).get("schemas"));
Map<String, Object> detailProperties = properties(schemas, "InboundMeasurementDetail");
assertThat(detailProperties).containsKeys("actualGrossWeightKg", "items", "weightResults");
assertThat(stringMap(detailProperties.get("items")))
.containsEntry("description", "DIMENSION 尺寸测量明细及其图片证据");
assertThat(stringMap(detailProperties.get("weightResults")))
.containsEntry("description", "WEIGHT 称重结果及其图片、确认状态和服务端重量事实");
assertThat(detailProperties).doesNotContainKey("images");
Map<String, Object> itemProperties = properties(schemas, "InboundMeasurementItem");
assertThat(itemProperties).containsKeys("id", "intelligentImageFileId");
Map<String, Object> weightProperties = properties(schemas, "InboundWeightResult");
assertThat(weightProperties).containsKeys(
"id", "fileId", "recognitionStatus", "grossWeightKg", "netWeightKg", "confirmedAt");
}
private static Map<String, Object> properties(Map<String, Object> schemas, String schemaName) {
return stringMap(stringMap(schemas.get(schemaName)).get("properties"));
}
@SuppressWarnings("unchecked")
private static Map<String, Object> stringMap(Object value) {
return (Map<String, Object>) value;
}
}
@@ -26,6 +26,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.TestPropertySource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -136,6 +137,45 @@ class InboundMeasurementServiceTest {
verify(repository, never()).updateInbound(any());
}
@Test
void detailKeepsDimensionAndWeightEvidenceOnTheirStableBusinessRecords() {
InboundOrderEntity inbound = inbound("MEASURE_PENDING");
inbound.setActualGrossWeightKg(new BigDecimal("11.500"));
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
measurement.setId(30L);
measurement.setVersion(4);
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
item.setId(40L);
item.setTenantId(1L);
item.setMeasurementId(30L);
item.setSourceImageFileId(140L);
InboundWeightResultEntity weight = weight(50L, "30.000", "KG", "CONFIRMED", 2);
weight.setGrossWeightKg(new BigDecimal("30.000"));
weight.setPalletTareWeightKg(new BigDecimal("18.500"));
weight.setNetWeightKg(new BigDecimal("11.500"));
weight.setConfirmedAt(LocalDateTime.of(2026, 8, 17, 10, 30));
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
when(repository.findLatestMeasurement(1L, 10L)).thenReturn(measurement);
when(repository.listItems(1L, 30L)).thenReturn(List.of(item));
when(repository.listWeights(1L, 10L)).thenReturn(List.of(weight));
InboundMeasurementResponses.Detail result = service.detail(1L, 10L);
assertThat(result.actualGrossWeightKg()).isEqualByComparingTo("11.500");
assertThat(result.items()).singleElement().satisfies(dimension -> {
assertThat(dimension.id()).isEqualTo(40L);
assertThat(dimension.intelligentImageFileId()).isEqualTo(140L);
});
assertThat(result.weightResults()).singleElement().satisfies(confirmedWeight -> {
assertThat(confirmedWeight.id()).isEqualTo(50L);
assertThat(confirmedWeight.fileId()).isEqualTo(150L);
assertThat(confirmedWeight.recognitionStatus()).isEqualTo("CONFIRMED");
assertThat(confirmedWeight.grossWeightKg()).isEqualByComparingTo("30.000");
assertThat(confirmedWeight.netWeightKg()).isEqualByComparingTo("11.500");
assertThat(confirmedWeight.confirmedAt()).isNotNull();
});
}
@Test
void creatingWeightResultNormalizesProviderConfidenceAtBusinessBoundary() {
InboundOrderEntity inbound = inbound("WAIT_MEASURE");