Merge pull request '优化' (#8) from test into main

Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
2026-09-20 10:35:17 +08:00
12 changed files with 199 additions and 24 deletions
+3 -2
View File
@@ -47,14 +47,15 @@ firstleg:
max-clock-skew: ${MEASUREMENT_OPENAPI_MAX_CLOCK_SKEW:5m}
nonce-ttl: ${MEASUREMENT_OPENAPI_NONCE_TTL:10m}
apps:
# 墨家设备群共用固定 AppKey;签名恢复后再启用 Secret。每台机器用 data.deviceCode 区分。
# 墨家使用平台级 AppKey;tenant-id 固定为 0,按全局箱码解析真实客户。
# warehouse-bindings 的键必须与设备上传的 machineNo 完全一致。
mojia:
enabled: ${MOJIA_MEASUREMENT_ENABLED:false}
secret: ${MOJIA_MEASUREMENT_SECRET:}
tenant-id: ${MOJIA_MEASUREMENT_TENANT_ID:0}
subject-id: ${MOJIA_MEASUREMENT_SUBJECT_ID:0}
warehouse-bindings:
CN-YW-01: ${MOJIA_MEASUREMENT_WAREHOUSE_ID:0}
mojia-yiwu-01: ${MOJIA_MEASUREMENT_WAREHOUSE_ID:0}
# 历史通用应用配置,保留兼容;新接入请使用 mojia。
device-provider:
enabled: ${MEASUREMENT_DEVICE_PROVIDER_ENABLED:false}
+3 -2
View File
@@ -34,14 +34,15 @@ firstleg:
max-clock-skew: ${MEASUREMENT_OPENAPI_MAX_CLOCK_SKEW:5m}
nonce-ttl: ${MEASUREMENT_OPENAPI_NONCE_TTL:10m}
apps:
# 墨家设备群共用固定 AppKey;签名恢复后再启用 Secret。每台机器用 data.deviceCode 区分。
# 墨家使用平台级 AppKey;tenant-id 固定为 0,按全局箱码解析真实客户。
# warehouse-bindings 的键必须与设备上传的 machineNo 完全一致。
mojia:
enabled: ${MOJIA_MEASUREMENT_ENABLED:false}
secret: ${MOJIA_MEASUREMENT_SECRET:}
tenant-id: ${MOJIA_MEASUREMENT_TENANT_ID:0}
subject-id: ${MOJIA_MEASUREMENT_SUBJECT_ID:0}
warehouse-bindings:
CN-YW-01: ${MOJIA_MEASUREMENT_WAREHOUSE_ID:0}
mojia-yiwu-01: ${MOJIA_MEASUREMENT_WAREHOUSE_ID:0}
# 历史通用应用配置,保留兼容;新接入请使用 mojia。
device-provider:
enabled: ${MEASUREMENT_DEVICE_PROVIDER_ENABLED:false}
@@ -64,8 +64,8 @@ public class InternalMeasurementImageController {
@PathVariable long receiptId,
@RequestPart("file") MultipartFile file) {
requireToken(token);
if (tenantId <= 0 || receiptId <= 0 || operatorId <= 0) {
throw new BizException(ResultCode.BAD_REQUEST, "客户、收件记录和操作主体ID必须大于0");
if (tenantId < 0 || receiptId <= 0 || operatorId <= 0) {
throw new BizException(ResultCode.BAD_REQUEST, "收件作用域不能为负,收件记录和操作主体ID必须大于0");
}
return ApiResponse.success(images.uploadMojiaReceiptEvidence(
tenantId, receiptId, operatorId, file));
@@ -94,8 +94,8 @@ public class InternalMeasurementImageController {
@PathVariable long receiptId,
@PathVariable long fileId) {
requireToken(token);
if (tenantId <= 0 || receiptId <= 0 || fileId <= 0 || operatorId <= 0) {
throw new BizException(ResultCode.BAD_REQUEST, "客户、收件记录、文件和操作主体ID必须大于0");
if (tenantId < 0 || receiptId <= 0 || fileId <= 0 || operatorId <= 0) {
throw new BizException(ResultCode.BAD_REQUEST, "收件作用域不能为负,收件记录、文件和操作主体ID必须大于0");
}
images.deleteMojiaReceiptEvidence(tenantId, receiptId, fileId, operatorId);
return ApiResponse.success(null);
@@ -73,4 +73,20 @@ class InternalMeasurementImageControllerTest {
assertThat(response.data().fileId()).isEqualTo(41L);
verify(images).uploadMojiaReceiptEvidence(11L, 51L, 31L, file);
}
@Test
void storesAndDeletesPlatformScopedUnmatchedMojiaReceiptImage() {
var file = new MockMultipartFile("file", "mojia.jpg", MediaType.IMAGE_JPEG_VALUE,
new byte[]{1, 2, 3});
when(images.uploadMojiaReceiptEvidence(0L, 51L, 31L, file)).thenReturn(
new MeasurementImageService.UploadResult(41L, "mojia.jpg",
MediaType.IMAGE_JPEG_VALUE, 3L, "NOT_REQUIRED", null, null, null, null, null));
assertThat(controller.uploadReceipt("internal-token", 31L, 0L, 51L, file).data().fileId())
.isEqualTo(41L);
controller.deleteReceipt("internal-token", 31L, 0L, 51L, 41L);
verify(images).uploadMojiaReceiptEvidence(0L, 51L, 31L, file);
verify(images).deleteMojiaReceiptEvidence(0L, 51L, 41L, 31L);
}
}
@@ -52,6 +52,19 @@ public class InboundCartonLabelRepository {
.eq(InboundCartonLabelEntity::getBoxCode, boxCode));
}
public List<InboundCartonLabelEntity> findGloballyByBoxCode(String boxCode) {
return labels.selectList(new LambdaQueryWrapper<InboundCartonLabelEntity>()
.eq(InboundCartonLabelEntity::getBoxCode, boxCode)
.last("LIMIT 2"));
}
public InboundOrderEntity findInbound(long tenantId, long inboundId) {
return inbounds.selectOne(new LambdaQueryWrapper<InboundOrderEntity>()
.eq(InboundOrderEntity::getTenantId, tenantId)
.eq(InboundOrderEntity::getId, inboundId)
.eq(InboundOrderEntity::getDeleted, false));
}
public void initializeLegacyFingerprint(long tenantId, long inboundId, int version, String fingerprint) {
labels.update(null, new LambdaUpdateWrapper<InboundCartonLabelEntity>()
.eq(InboundCartonLabelEntity::getTenantId, tenantId)
@@ -45,7 +45,7 @@ public class MeasurementRequestAuthenticator {
MeasurementOpenApiProperties.AppCredential credential = properties.getApps().get(headers.appKey());
if (credential == null || !credential.isEnabled()
|| properties.isSignatureRequired() && !StringUtils.hasText(credential.getSecret())
|| credential.getTenantId() == null || credential.getTenantId() <= 0
|| credential.getTenantId() == null || credential.getTenantId() < 0
|| credential.getSubjectId() == null || credential.getSubjectId() <= 0) {
throw unauthorized("应用凭据无效");
}
@@ -154,9 +154,28 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
@Override
public CartonLabelBindingResolver.Binding resolveByBoxCode(long tenantId, String boxCode) {
InboundCartonLabelEntity label = requireByBoxCode(tenantId, boxCode);
return new CartonLabelBindingResolver.Binding(label.getInboundId(), label.getInboundDetailId(),
label.getInboundCode(), label.getCartonIndex(), label.getTotalCartonCount(),
label.getBoxSpecification());
return binding(label);
}
public PlatformBinding resolvePlatformByBoxCode(String boxCode) {
if (!StringUtils.hasText(boxCode)) {
throw new BizException(ResultCode.BAD_REQUEST, "箱号不能为空");
}
List<InboundCartonLabelEntity> matches = repository.findGloballyByBoxCode(boxCode.trim());
if (matches.isEmpty()) throw new BizException(ResultCode.NOT_FOUND, "箱号不存在");
if (matches.size() != 1) {
throw new BizException(ResultCode.CONFLICT, "箱号归属不唯一,禁止设备写入");
}
InboundCartonLabelEntity label = matches.get(0);
if ("VOID".equals(label.getBatchStatus())) {
throw new BizException(ResultCode.CONFLICT, "箱码已失效,请重新打印");
}
InboundOrderEntity inbound = repository.findInbound(label.getTenantId(), label.getInboundId());
if (inbound == null) throw new BizException(ResultCode.NOT_FOUND, "箱号关联的入库单不存在");
if (inbound.getWarehouseId() == null || inbound.getWarehouseId() <= 0) {
throw new BizException(ResultCode.CONFLICT, "入库单未绑定有效国内仓库");
}
return new PlatformBinding(label.getTenantId(), inbound.getWarehouseId(), binding(label));
}
public InboundCartonLabels.Resolution resolveDetailsByBoxCode(long tenantId, String boxCode) {
@@ -179,6 +198,16 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
return label;
}
private CartonLabelBindingResolver.Binding binding(InboundCartonLabelEntity label) {
return new CartonLabelBindingResolver.Binding(label.getInboundId(), label.getInboundDetailId(),
label.getInboundCode(), label.getCartonIndex(), label.getTotalCartonCount(),
label.getBoxSpecification());
}
public record PlatformBinding(long tenantId, long warehouseId,
CartonLabelBindingResolver.Binding binding) {
}
private List<OrderCargoClient.CargoLabelBinding> loadCargoes(InboundOrderEntity inbound) {
try {
ApiResponse<List<OrderCargoClient.CargoLabelBinding>> response = orderCargoClient.listForCartonLabels(
@@ -48,6 +48,7 @@ public class MojiaMeasurementOpenApiService {
private final Validator validator;
private final MeasurementImageClient imageClient;
private final MojiaMeasurementReceiptRepository receipts;
private final InboundCartonLabelService cartonLabels;
@Value("${baiyue.security.internal-service-token}")
private String internalToken;
@@ -57,13 +58,15 @@ public class MojiaMeasurementOpenApiService {
ObjectMapper objectMapper,
Validator validator,
MeasurementImageClient imageClient,
MojiaMeasurementReceiptRepository receipts) {
MojiaMeasurementReceiptRepository receipts,
InboundCartonLabelService cartonLabels) {
this.authenticator = authenticator;
this.cartonMeasurements = cartonMeasurements;
this.objectMapper = objectMapper;
this.validator = validator;
this.imageClient = imageClient;
this.receipts = receipts;
this.cartonLabels = cartonLabels;
}
public UploadResponse receive(MeasurementRequestHeaders headers, String rawJson) {
@@ -73,10 +76,26 @@ public class MojiaMeasurementOpenApiService {
"POST", CALLBACK_PATH, headers, rawJson, false);
request = parseAndValidate(rawJson);
MultipartFile image = decodeJpeg(request.picture());
if (app.tenantId() != 0) {
throw new BizException(ResultCode.UNAUTHORIZED, "墨家设备必须使用平台级应用凭据");
}
long machineWarehouseId = requireMachineWarehouse(app, request.machineNo());
final InboundCartonLabelService.PlatformBinding platformBinding;
try {
platformBinding = cartonLabels.resolvePlatformByBoxCode(request.boxCode());
} catch (BizException exception) {
if (exception.getResultCode() == ResultCode.NOT_FOUND) {
return saveUnmatched(app, request, image, exception);
}
throw exception;
}
if (platformBinding.warehouseId() != machineWarehouseId) {
throw new BizException(ResultCode.FORBIDDEN, "箱号不属于当前设备绑定仓库");
}
CartonMeasurementResult result;
try {
result = cartonMeasurements.upsert(
app.tenantId(), app.subjectId(), new CartonMeasurementCommand(
platformBinding.tenantId(), app.subjectId(), new CartonMeasurementCommand(
request.boxCode(), request.length(), request.width(), request.height(),
request.machineNo(), request.measuredAt() == null ? LocalDateTime.now() : request.measuredAt(),
image, optionalTrim(request.shipmentId())));
@@ -106,6 +125,14 @@ public class MojiaMeasurementOpenApiService {
}
}
private long requireMachineWarehouse(AuthenticatedMeasurementApp app, String machineNo) {
Long warehouseId = app.warehouseBindings().get(machineNo.trim());
if (warehouseId == null || warehouseId <= 0) {
throw new BizException(ResultCode.UNAUTHORIZED, "设备编号未绑定有效仓库");
}
return warehouseId;
}
private UploadResponse saveUnmatched(AuthenticatedMeasurementApp app, UploadRequest request,
MultipartFile image, BizException matchFailure) {
long receiptId = IdWorker.getId();
@@ -836,10 +836,14 @@ paths:
post:
summary: 提交一条第三方箱规测量结果和一张图片
description: >-
data 中的 inboundCode 匹配入库单,inboundDetailId 必须匹配该入库单关联订单中的已有货物明细;
每次只保存一条箱规明细和一张图片,不审核、不计费、不入库,也不改变入库单状态。
首轮联调可由服务端配置 signature-required=false:此时只强制固定 X-App-Key 和
Idempotency-Key,X-Timestamp、X-Nonce、X-Signature 可不传;恢复签名后按下述规则计算。
墨家 JSON 协议使用平台级 AppKey(tenant-id=0)和正数设备服务主体 ID;
按全平台唯一 boxCode 找到入库单真实客户,再校验 machineNo 绑定的仓库与入库单仓库一致。
匹配结果按真实客户保存,未匹配的联调收件按平台作用域保存;
每次只保存一条箱规明细和一张图片,不审核、不计费、不入库。
multipart 协议的 data 中则以 inboundCode 和 inboundDetailId 匹配指定客户的入库单及货物明细。
首轮联调可由服务端配置 signature-required=false:此时 JSON 协议只强制 X-App-Key,
multipart 协议还要求 Idempotency-Key;X-Timestamp、X-Nonce、X-Signature 可不传。
恢复签名后按下述规则计算。
签名串依次为 METHOD、原始 PATH、X-Timestamp、X-Nonce、Idempotency-Key、
签名body的 SHA-256 小写十六进制,六部分使用 LF(\n)连接且末尾不追加换行。
签名body = data原始UTF-8文本 + LF + image文件内容SHA-256小写十六进制。
@@ -117,6 +117,23 @@ class MeasurementRequestAuthenticatorTest {
verifyNoInteractions(guard);
}
@Test
@DisplayName("平台仓库设备允许tenantId为0并保留正数审计主体")
void shouldAcceptPlatformScopedWarehouseDevice() {
properties.setSignatureRequired(false);
MeasurementOpenApiProperties.AppCredential credential = properties.getApps().get("device-provider");
credential.setSecret(null);
credential.setTenantId(0L);
AuthenticatedMeasurementApp app = authenticator.authenticate("POST",
"/openapi/v1/inbound-measurements",
new MeasurementRequestHeaders("device-provider", null, null, null, null), "{}", false);
assertThat(app.tenantId()).isZero();
assertThat(app.subjectId()).isEqualTo(90001L);
assertThat(app.warehouseBindings()).containsEntry("CN-YW-01", 30001L);
}
@Test
@DisplayName("关闭签名仍强制上传请求携带幂等键")
void shouldStillRequireIdempotencyKeyWithoutSignature() {
@@ -48,6 +48,38 @@ class InboundCartonLabelServiceTest {
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
}
@Test
void platformLookupUsesUniqueBoxCodeAndReturnsActualCustomerAndWarehouse() {
InboundCartonLabelEntity label = new InboundCartonLabelEntity();
label.setTenantId(10002L);
label.setInboundId(70002L);
label.setInboundDetailId(80002L);
label.setInboundCode("RK-002");
label.setBoxCode("RK-002#1-1");
label.setBatchStatus("ACTIVE");
label.setCartonIndex(1);
label.setTotalCartonCount(1);
InboundOrderEntity inbound = new InboundOrderEntity();
inbound.setWarehouseId(30001L);
when(repository.findGloballyByBoxCode("RK-002#1-1")).thenReturn(List.of(label));
when(repository.findInbound(10002L, 70002L)).thenReturn(inbound);
var resolved = service.resolvePlatformByBoxCode("RK-002#1-1");
assertThat(resolved.tenantId()).isEqualTo(10002L);
assertThat(resolved.warehouseId()).isEqualTo(30001L);
assertThat(resolved.binding().inboundId()).isEqualTo(70002L);
}
@Test
void platformLookupRejectsAmbiguousBoxCodeAcrossCustomers() {
when(repository.findGloballyByBoxCode("RK-002#1-1"))
.thenReturn(List.of(new InboundCartonLabelEntity(), new InboundCartonLabelEntity()));
assertThatThrownBy(() -> service.resolvePlatformByBoxCode("RK-002#1-1"))
.isInstanceOf(BizException.class).hasMessageContaining("归属不唯一");
}
@Test
void firstAllocationUsesCargoOrderAndFreezesEveryPhysicalCarton() {
InboundOrderEntity inbound = inbound();
@@ -55,12 +55,18 @@ class MojiaMeasurementOpenApiServiceTest {
CartonMeasurementService cartons = mock(CartonMeasurementService.class);
MeasurementImageClient images = mock(MeasurementImageClient.class);
MojiaMeasurementReceiptRepository receipts = mock(MojiaMeasurementReceiptRepository.class);
InboundCartonLabelService labels = mock(InboundCartonLabelService.class);
var service = new MojiaMeasurementOpenApiService(authenticator, cartons,
new ObjectMapper().findAndRegisterModules(), Validation.buildDefaultValidatorFactory().getValidator(),
images, receipts);
images, receipts, labels);
MeasurementRequestHeaders headers = new MeasurementRequestHeaders("mojia", null, null, null, null);
when(authenticator.authenticate("POST", MojiaMeasurementOpenApiService.CALLBACK_PATH,
headers, json(), false)).thenReturn(new AuthenticatedMeasurementApp("mojia", 10001L, 90001L, Map.of()));
headers, json(), false)).thenReturn(new AuthenticatedMeasurementApp(
"mojia", 0L, 90001L, Map.of("MJ-01", 30001L)));
when(labels.resolvePlatformByBoxCode("PCHK2608140001#5-1"))
.thenReturn(new InboundCartonLabelService.PlatformBinding(10001L, 30001L,
new CartonLabelBindingResolver.Binding(
70001L, 80001L, "PCHK2608140001", 1, 5, "60x40x30cm")));
when(cartons.upsert(eq(10001L), eq(90001L), any())).thenReturn(new CartonMeasurementResult(
1L, 1, 2L, "PCHK2608140001#5-1", "FBA19L5KMY2CU000002", 1,
new BigDecimal("60.1"), new BigDecimal("40.2"),
@@ -85,17 +91,19 @@ class MojiaMeasurementOpenApiServiceTest {
CartonMeasurementService cartons = mock(CartonMeasurementService.class);
MeasurementImageClient images = mock(MeasurementImageClient.class);
MojiaMeasurementReceiptRepository receipts = mock(MojiaMeasurementReceiptRepository.class);
InboundCartonLabelService labels = mock(InboundCartonLabelService.class);
var service = new MojiaMeasurementOpenApiService(authenticator, cartons,
new ObjectMapper().findAndRegisterModules(), Validation.buildDefaultValidatorFactory().getValidator(),
images, receipts);
images, receipts, labels);
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
MeasurementRequestHeaders headers = new MeasurementRequestHeaders("mojia", null, null, null, null);
String requestJson = json().replace("\"shipment_id\":\"FBA19L5KMY2CU000002\",", "");
when(authenticator.authenticate("POST", MojiaMeasurementOpenApiService.CALLBACK_PATH,
headers, requestJson, false)).thenReturn(new AuthenticatedMeasurementApp("mojia", 10001L, 90001L, Map.of()));
when(cartons.upsert(eq(10001L), eq(90001L), any()))
headers, requestJson, false)).thenReturn(new AuthenticatedMeasurementApp(
"mojia", 0L, 90001L, Map.of("MJ-01", 30001L)));
when(labels.resolvePlatformByBoxCode("PCHK2608140001#5-1"))
.thenThrow(new BizException(ResultCode.NOT_FOUND, "箱号不存在或不属于当前客户"));
when(images.uploadReceipt(eq("internal-token"), eq(90001L), eq(10001L), any(Long.TYPE), any()))
when(images.uploadReceipt(eq("internal-token"), eq(90001L), eq(0L), any(Long.TYPE), any()))
.thenReturn(ApiResponse.success(new MeasurementImageClient.UploadResult(
3L, "mojia.jpg", "image/jpeg", 4L, "NOT_REQUIRED", null, null, null, null)));
@@ -113,6 +121,33 @@ class MojiaMeasurementOpenApiServiceTest {
assertThat(receipt.getValue().getLengthCm()).isEqualByComparingTo("60.1");
assertThat(receipt.getValue().getImageFileId()).isEqualTo(3L);
assertThat(receipt.getValue().getStatus()).isEqualTo("UNMATCHED");
assertThat(receipt.getValue().getTenantId()).isZero();
}
@Test
void platformMachineCannotWriteAnInboundFromAnotherWarehouse() {
MeasurementRequestAuthenticator authenticator = mock(MeasurementRequestAuthenticator.class);
CartonMeasurementService cartons = mock(CartonMeasurementService.class);
MeasurementImageClient images = mock(MeasurementImageClient.class);
MojiaMeasurementReceiptRepository receipts = mock(MojiaMeasurementReceiptRepository.class);
InboundCartonLabelService labels = mock(InboundCartonLabelService.class);
var service = new MojiaMeasurementOpenApiService(authenticator, cartons,
new ObjectMapper().findAndRegisterModules(), Validation.buildDefaultValidatorFactory().getValidator(),
images, receipts, labels);
MeasurementRequestHeaders headers = new MeasurementRequestHeaders("mojia", null, null, null, null);
when(authenticator.authenticate("POST", MojiaMeasurementOpenApiService.CALLBACK_PATH,
headers, json(), false)).thenReturn(new AuthenticatedMeasurementApp(
"mojia", 0L, 90001L, Map.of("MJ-01", 30001L)));
when(labels.resolvePlatformByBoxCode("PCHK2608140001#5-1"))
.thenReturn(new InboundCartonLabelService.PlatformBinding(10002L, 30002L,
new CartonLabelBindingResolver.Binding(
70002L, 80002L, "PCHK2608140001", 1, 5, "60x40x30cm")));
org.assertj.core.api.Assertions.assertThatThrownBy(() -> service.receive(headers, json()))
.isInstanceOf(BizException.class)
.hasMessageContaining("不属于当前设备绑定仓库");
verify(cartons, org.mockito.Mockito.never()).upsert(any(Long.TYPE), any(Long.TYPE), any());
}
private String json() {