From 507763a0d66652c84d41e1353f4a4d9101d60ade Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sun, 20 Sep 2026 10:06:14 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/nacos/cn/firstleg-service.yml | 5 +- config/nacos/local/firstleg-service.yml | 5 +- .../InternalMeasurementImageController.java | 8 ++-- ...nternalMeasurementImageControllerTest.java | 16 +++++++ .../InboundCartonLabelRepository.java | 13 +++++ .../MeasurementRequestAuthenticator.java | 2 +- .../service/InboundCartonLabelService.java | 35 ++++++++++++-- .../MojiaMeasurementOpenApiService.java | 31 +++++++++++- .../main/resources/openapi/firstleg-v1.yaml | 12 +++-- .../MeasurementRequestAuthenticatorTest.java | 17 +++++++ .../InboundCartonLabelServiceTest.java | 32 +++++++++++++ .../MojiaMeasurementOpenApiServiceTest.java | 47 ++++++++++++++++--- 12 files changed, 199 insertions(+), 24 deletions(-) diff --git a/config/nacos/cn/firstleg-service.yml b/config/nacos/cn/firstleg-service.yml index 3ca256ca4..cd8b7d7ec 100644 --- a/config/nacos/cn/firstleg-service.yml +++ b/config/nacos/cn/firstleg-service.yml @@ -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} diff --git a/config/nacos/local/firstleg-service.yml b/config/nacos/local/firstleg-service.yml index 1da4fd0e1..454b7343f 100644 --- a/config/nacos/local/firstleg-service.yml +++ b/config/nacos/local/firstleg-service.yml @@ -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} diff --git a/file/src/main/java/com/baiyue/file/controller/InternalMeasurementImageController.java b/file/src/main/java/com/baiyue/file/controller/InternalMeasurementImageController.java index 7e76c25c9..b093eb0c4 100644 --- a/file/src/main/java/com/baiyue/file/controller/InternalMeasurementImageController.java +++ b/file/src/main/java/com/baiyue/file/controller/InternalMeasurementImageController.java @@ -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); diff --git a/file/src/test/java/com/baiyue/file/controller/InternalMeasurementImageControllerTest.java b/file/src/test/java/com/baiyue/file/controller/InternalMeasurementImageControllerTest.java index 89e0a53e4..8ad4b9308 100644 --- a/file/src/test/java/com/baiyue/file/controller/InternalMeasurementImageControllerTest.java +++ b/file/src/test/java/com/baiyue/file/controller/InternalMeasurementImageControllerTest.java @@ -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); + } } 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 1528937ca..5e97dfc54 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundCartonLabelRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundCartonLabelRepository.java @@ -52,6 +52,19 @@ public class InboundCartonLabelRepository { .eq(InboundCartonLabelEntity::getBoxCode, boxCode)); } + public List findGloballyByBoxCode(String boxCode) { + return labels.selectList(new LambdaQueryWrapper() + .eq(InboundCartonLabelEntity::getBoxCode, boxCode) + .last("LIMIT 2")); + } + + public InboundOrderEntity findInbound(long tenantId, long inboundId) { + return inbounds.selectOne(new LambdaQueryWrapper() + .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() .eq(InboundCartonLabelEntity::getTenantId, tenantId) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/security/MeasurementRequestAuthenticator.java b/firstleg/src/main/java/com/baiyue/firstleg/security/MeasurementRequestAuthenticator.java index f7f9fabf3..5a4f2ba49 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/security/MeasurementRequestAuthenticator.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/security/MeasurementRequestAuthenticator.java @@ -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("应用凭据无效"); } 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 d26123817..28185e5a2 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundCartonLabelService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundCartonLabelService.java @@ -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 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 loadCargoes(InboundOrderEntity inbound) { try { ApiResponse> response = orderCargoClient.listForCartonLabels( diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/MojiaMeasurementOpenApiService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/MojiaMeasurementOpenApiService.java index 1bc4ee8a0..a4e6947e2 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/MojiaMeasurementOpenApiService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/MojiaMeasurementOpenApiService.java @@ -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(); diff --git a/firstleg/src/main/resources/openapi/firstleg-v1.yaml b/firstleg/src/main/resources/openapi/firstleg-v1.yaml index 226880cc9..d8dfba52b 100644 --- a/firstleg/src/main/resources/openapi/firstleg-v1.yaml +++ b/firstleg/src/main/resources/openapi/firstleg-v1.yaml @@ -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小写十六进制。 diff --git a/firstleg/src/test/java/com/baiyue/firstleg/security/MeasurementRequestAuthenticatorTest.java b/firstleg/src/test/java/com/baiyue/firstleg/security/MeasurementRequestAuthenticatorTest.java index da3b39cc3..9f4cd2709 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/security/MeasurementRequestAuthenticatorTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/security/MeasurementRequestAuthenticatorTest.java @@ -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() { 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 505f3dfb2..8cec18a1d 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundCartonLabelServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundCartonLabelServiceTest.java @@ -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(); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/MojiaMeasurementOpenApiServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/MojiaMeasurementOpenApiServiceTest.java index 3c3415160..f7b5959a3 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/MojiaMeasurementOpenApiServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/MojiaMeasurementOpenApiServiceTest.java @@ -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() {