From c3bf4edd536d4eb46c078ee81558617d486ffd7a Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Tue, 4 Aug 2026 19:48:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=8C=E6=9C=9F=E5=86=85=E5=AE=B9=E6=8F=90?= =?UTF-8?q?=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InternalMeasurementImageController.java | 25 +++++ ...nternalMeasurementImageControllerTest.java | 12 ++ .../client/MeasurementImageClient.java | 8 ++ .../MerchantInboundBillingController.java | 19 ++++ .../dto/MerchantInboundBillingResponse.java | 41 +++++++ .../MerchantInboundBillingService.java | 105 +++++++++++++++++- ...tInboundBillingControllerContractTest.java | 7 ++ .../MerchantInboundBillingServiceTest.java | 70 +++++++++++- 8 files changed, 281 insertions(+), 6 deletions(-) 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 4f6ded984..31ac02e52 100644 --- a/file/src/main/java/com/baiyue/file/controller/InternalMeasurementImageController.java +++ b/file/src/main/java/com/baiyue/file/controller/InternalMeasurementImageController.java @@ -9,6 +9,7 @@ import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; @@ -17,6 +18,10 @@ import org.springframework.web.multipart.MultipartFile; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; +import org.springframework.http.CacheControl; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; /** Firstleg 保存第三方尺寸测量证据图片的内部入口。 */ @RestController @@ -62,6 +67,26 @@ public class InternalMeasurementImageController { return ApiResponse.success(null); } + @GetMapping("/{tenantId}/{inboundId}/{fileId}") + public ResponseEntity download( + @RequestHeader(value = "X-Internal-Service-Token", required = false) String token, + @PathVariable long tenantId, + @PathVariable long inboundId, + @PathVariable long fileId) { + requireToken(token); + if (tenantId <= 0 || inboundId <= 0 || fileId <= 0) { + throw new BizException(ResultCode.BAD_REQUEST, "租户、入库单和文件ID必须大于0"); + } + var image = images.download(tenantId, inboundId, fileId); + return ResponseEntity.ok() + .cacheControl(CacheControl.noStore()) + .contentType(MediaType.parseMediaType(image.contentType())) + .contentLength(image.content().length) + .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.inline() + .filename(image.fileName(), StandardCharsets.UTF_8).build().toString()) + .body(image.content()); + } + private void requireToken(String token) { if (token == null || !MessageDigest.isEqual(expectedToken, token.getBytes(StandardCharsets.UTF_8))) { throw new BizException(ResultCode.FORBIDDEN, "内部服务凭证无效"); 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 8aace9cc4..0a2de2ea4 100644 --- a/file/src/test/java/com/baiyue/file/controller/InternalMeasurementImageControllerTest.java +++ b/file/src/test/java/com/baiyue/file/controller/InternalMeasurementImageControllerTest.java @@ -40,4 +40,16 @@ class InternalMeasurementImageControllerTest { .isInstanceOf(BizException.class) .hasMessageContaining("凭证无效"); } + @Test + void 内部下载同时校验租户入库单和文件ID() { + when(images.download(11L, 21L, 41L)).thenReturn( + new MeasurementImageService.Download("measure.png", MediaType.IMAGE_PNG_VALUE, + new byte[]{1, 2, 3})); + + var response = controller.download("internal-token", 11L, 21L, 41L); + + assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.IMAGE_PNG); + assertThat(response.getBody()).containsExactly(1, 2, 3); + verify(images).download(11L, 21L, 41L); + } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/client/MeasurementImageClient.java b/firstleg/src/main/java/com/baiyue/firstleg/client/MeasurementImageClient.java index 1ce3a8f68..f72bcc0ca 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/client/MeasurementImageClient.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/client/MeasurementImageClient.java @@ -5,6 +5,7 @@ import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; @@ -28,6 +29,13 @@ public interface MeasurementImageClient { @PathVariable long inboundId, @PathVariable long fileId); + @GetMapping("/{tenantId}/{inboundId}/{fileId}") + org.springframework.http.ResponseEntity download( + @RequestHeader("X-Internal-Service-Token") String token, + @PathVariable long tenantId, + @PathVariable long inboundId, + @PathVariable long fileId); + record UploadResult(Long fileId, String fileName, String contentType, Long sizeBytes, String recognitionStatus, BigDecimal weightValue, String weightUnit, Double confidence, String error) { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/MerchantInboundBillingController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/MerchantInboundBillingController.java index 0e21241e2..308b766f7 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/MerchantInboundBillingController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/MerchantInboundBillingController.java @@ -14,6 +14,9 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.CacheControl; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; @Tag(name = "客户端入库结果与费用") @RestController @@ -36,4 +39,20 @@ public class MerchantInboundBillingController { } return ApiResponse.success(service.find(user.tenantId(), orderId)); } + + @GetMapping("/{orderId}/inbound-billing/images/{fileId}") + @PreAuthorize("hasAuthority('merchant:order:view')") + @Operation(summary = "查看已发布计费明细中的测量或称重图片") + public ResponseEntity image( + @AuthenticationPrincipal AuthenticatedUser user, + @PathVariable long orderId, + @PathVariable long fileId) { + if (user == null || !"MERCHANT".equals(user.appCode()) || user.tenantId() == null) { + throw new BizException(ResultCode.FORBIDDEN, "仅允许客户端访问"); + } + var image = service.downloadImage(user.tenantId(), orderId, fileId); + return ResponseEntity.ok().cacheControl(CacheControl.noStore()) + .contentType(MediaType.parseMediaType(image.contentType())) + .contentLength(image.content().length).body(image.content()); + } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/MerchantInboundBillingResponse.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/MerchantInboundBillingResponse.java index bcef6954a..82198fd99 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/MerchantInboundBillingResponse.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/MerchantInboundBillingResponse.java @@ -2,6 +2,47 @@ package com.baiyue.firstleg.dto; import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse; +import java.math.BigDecimal; +import java.util.List; + public record MerchantInboundBillingResponse(InboundOrderDetail inbound, + MeasurementSummary measurementSummary, + List measurementItems, + List weightResults, + List measurementImages, + List weightImages, + BillingBasis billingBasis, FeeSnapshotResponse latestFee) { + + public record MeasurementSummary( + Long measurementId, String sourceType, String measurementStatus, + Integer actualPackageCount, BigDecimal actualGrossWeightKg, + BigDecimal actualVolumeCbm, BigDecimal volumeWeightKg, String actualGoodsName) { + } + + public record ImageReference( + Long fileId, String imageType, Long measurementItemId, + Long weightResultId, String previewUrl) { + } + + public record MeasurementItem( + Long id, String itemNo, String boxSpecification, Integer packageCount, + BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm, + BigDecimal unitWeightKg, BigDecimal totalWeightKg, + BigDecimal unitVolumeCbm, BigDecimal totalVolumeCbm, + BigDecimal volumeWeightKg) { + } + + public record WeightResult( + Long id, Long fileId, BigDecimal recognizedWeightValue, String recognizedWeightUnit, + BigDecimal adjustedWeightValue, String adjustedWeightUnit, + BigDecimal finalWeightKg, String recognitionStatus) { + } + + public record BillingBasis( + String chargeMode, String chargeUnit, String chargeableBasis, + BigDecimal actualWeightKg, BigDecimal actualVolumeCbm, + BigDecimal convertedQuantity, BigDecimal chargeableQuantity, + BigDecimal chargeableWeightKg, BigDecimal chargeableVolumeCbm) { + } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/MerchantInboundBillingService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/MerchantInboundBillingService.java index c5211077e..8ae6d844a 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/MerchantInboundBillingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/MerchantInboundBillingService.java @@ -3,18 +3,34 @@ package com.baiyue.firstleg.service; import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import com.baiyue.firstleg.dto.MerchantInboundBillingResponse; +import com.baiyue.firstleg.dto.InboundMeasurementResponses; +import com.baiyue.firstleg.dto.InboundOrderDetail; +import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse; +import com.baiyue.firstleg.client.MeasurementImageClient; import com.baiyue.firstleg.entity.InboundOrderEntity; import com.baiyue.firstleg.repository.InboundOrderRepository; import org.springframework.stereotype.Service; +import java.util.List; + @Service public class MerchantInboundBillingService { private final InboundOrderRepository orders; private final FbaPricingService pricing; + private final InboundMeasurementService measurements; + private final MeasurementImageClient imageClient; + private final String internalToken; - public MerchantInboundBillingService(InboundOrderRepository orders, FbaPricingService pricing) { + public MerchantInboundBillingService(InboundOrderRepository orders, FbaPricingService pricing, + InboundMeasurementService measurements, + MeasurementImageClient imageClient, + @org.springframework.beans.factory.annotation.Value( + "${baiyue.security.internal-service-token}") String internalToken) { this.orders = orders; this.pricing = pricing; + this.measurements = measurements; + this.imageClient = imageClient; + this.internalToken = internalToken; } public MerchantInboundBillingResponse find(long tenantId, long orderId) { @@ -25,7 +41,90 @@ public class MerchantInboundBillingService { if (!"INBOUNDED".equals(inbound.getProcessStatus()) || !"SENT".equals(inbound.getFeeStatus())) { throw new BizException(ResultCode.NOT_FOUND, "入库费用尚未由客服审核并发布"); } - return new MerchantInboundBillingResponse(orders.detail(tenantId, inbound), - pricing.latest(tenantId, inbound.getId())); + InboundOrderDetail platformDetail = orders.detail(tenantId, inbound); + InboundOrderDetail merchantDetail = new InboundOrderDetail( + platformDetail.summary(), platformDetail.publicRemark(), platformDetail.palletLocations(), + platformDetail.files().stream() + .filter(file -> "PUBLIC".equals(file.visibility()) || "MERCHANT".equals(file.visibility())) + .toList(), List.of(), List.of()); + InboundMeasurementResponses.Detail measurement = measurements.detail(tenantId, inbound.getId()); + FeeSnapshotResponse fee = pricing.latest(tenantId, inbound.getId()); + var summary = new MerchantInboundBillingResponse.MeasurementSummary( + measurement.measurementId(), measurement.sourceType(), measurement.measurementStatus(), + measurement.actualPackageCount(), measurement.actualGrossWeightKg(), + measurement.actualVolumeCbm(), measurement.volumeWeightKg(), measurement.actualGoodsName()); + var measurementImages = measurement.items().stream() + .filter(item -> item.intelligentImageFileId() != null) + .map(item -> new MerchantInboundBillingResponse.ImageReference( + item.intelligentImageFileId(), "MEASUREMENT", item.id(), null, + previewUrl(orderId, item.intelligentImageFileId()))) + .toList(); + var weightImages = measurement.weightResults().stream() + .filter(weight -> weight.fileId() != null) + .map(weight -> new MerchantInboundBillingResponse.ImageReference( + weight.fileId(), "WEIGHT", null, weight.id(), previewUrl(orderId, weight.fileId()))) + .toList(); + var measurementItems = measurement.items().stream() + .map(item -> new MerchantInboundBillingResponse.MeasurementItem( + item.id(), item.itemNo(), item.boxSpecification(), item.packageCount(), + item.lengthCm(), item.widthCm(), item.heightCm(), item.unitWeightKg(), + item.totalWeightKg(), item.unitVolumeCbm(), item.totalVolumeCbm(), item.volumeWeightKg())) + .toList(); + var weightResults = measurement.weightResults().stream() + .map(weight -> new MerchantInboundBillingResponse.WeightResult( + weight.id(), weight.fileId(), weight.ocrWeightValue(), weight.ocrWeightUnit(), + weight.adjustedWeightValue(), weight.adjustedWeightUnit(), + weight.finalWeightKg(), weight.recognitionStatus())) + .toList(); + var basis = new MerchantInboundBillingResponse.BillingBasis( + fee.chargeMode(), fee.chargeUnit(), chargeableBasis(fee), fee.actualWeightKg(), + fee.actualVolumeCbm(), fee.convertedQuantity(), fee.chargeableQuantity(), + platformDetail.summary().chargeableWeightKg(), + platformDetail.summary().chargeableVolumeCbm()); + return new MerchantInboundBillingResponse(merchantDetail, summary, measurementItems, + weightResults, measurementImages, weightImages, basis, fee); + } + + public ImageDownload downloadImage(long tenantId, long orderId, long fileId) { + InboundOrderEntity inbound = requirePublished(tenantId, orderId); + InboundMeasurementResponses.Detail measurement = measurements.detail(tenantId, inbound.getId()); + boolean referenced = measurement.items().stream() + .anyMatch(item -> Long.valueOf(fileId).equals(item.intelligentImageFileId())) + || measurement.weightResults().stream() + .anyMatch(weight -> Long.valueOf(fileId).equals(weight.fileId())); + if (!referenced) throw new BizException(ResultCode.NOT_FOUND, "计费明细中不存在该测量图片"); + org.springframework.http.ResponseEntity response = imageClient.download( + internalToken, tenantId, inbound.getId(), fileId); + if (response == null || response.getBody() == null) { + throw new BizException(ResultCode.NOT_FOUND, "测量图片不存在"); + } + String contentType = response.getHeaders().getContentType() == null + ? "application/octet-stream" : response.getHeaders().getContentType().toString(); + return new ImageDownload(response.getBody(), contentType); + } + + private InboundOrderEntity requirePublished(long tenantId, long orderId) { + InboundOrderEntity inbound = orders.findByOrderId(tenantId, orderId); + if (inbound == null) throw new BizException(ResultCode.NOT_FOUND, "订单尚未生成入库单"); + if (!"INBOUNDED".equals(inbound.getProcessStatus()) || !"SENT".equals(inbound.getFeeStatus())) { + throw new BizException(ResultCode.NOT_FOUND, "入库费用尚未由客服审核并发布"); + } + return inbound; + } + + private String previewUrl(long orderId, long fileId) { + return "/api/merchant/v1/first-leg/orders/" + orderId + + "/inbound-billing/images/" + fileId; + } + + private String chargeableBasis(FeeSnapshotResponse fee) { + int compared = ("KG".equals(fee.chargeMode()) ? fee.actualWeightKg() : fee.actualVolumeCbm()) + .compareTo(fee.convertedQuantity()); + if (compared == 0) return "EQUAL"; + if ("KG".equals(fee.chargeMode())) return compared > 0 ? "ACTUAL_WEIGHT" : "VOLUME_WEIGHT"; + return compared > 0 ? "ACTUAL_VOLUME" : "WEIGHT_CONVERTED_VOLUME"; + } + + public record ImageDownload(byte[] content, String contentType) { } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/MerchantInboundBillingControllerContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/MerchantInboundBillingControllerContractTest.java index 3aee456fb..7db539597 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/MerchantInboundBillingControllerContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/MerchantInboundBillingControllerContractTest.java @@ -32,4 +32,11 @@ class MerchantInboundBillingControllerContractTest { "admin", "PLATFORM", "WEB", Set.of()); assertThatThrownBy(() -> controller.find(platform, 77L)).isInstanceOf(BizException.class); } + @Test + void 商户测量图片复用订单查看权限() throws Exception { + var method = MerchantInboundBillingController.class.getDeclaredMethod( + "image", AuthenticatedUser.class, long.class, long.class); + assertThat(method.getAnnotation(PreAuthorize.class).value()) + .isEqualTo("hasAuthority('merchant:order:view')"); + } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/MerchantInboundBillingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/MerchantInboundBillingServiceTest.java index b69ae1d3f..5d7b19af6 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/MerchantInboundBillingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/MerchantInboundBillingServiceTest.java @@ -3,10 +3,15 @@ package com.baiyue.firstleg.service; import com.baiyue.common.core.exception.BizException; import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse; import com.baiyue.firstleg.dto.InboundOrderDetail; +import com.baiyue.firstleg.dto.InboundMeasurementResponses; +import com.baiyue.firstleg.client.MeasurementImageClient; import com.baiyue.firstleg.entity.InboundOrderEntity; import com.baiyue.firstleg.repository.InboundOrderRepository; import org.junit.jupiter.api.Test; +import java.math.BigDecimal; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; @@ -17,20 +22,43 @@ import static org.mockito.Mockito.when; class MerchantInboundBillingServiceTest { private final InboundOrderRepository orders = mock(InboundOrderRepository.class); private final FbaPricingService pricing = mock(FbaPricingService.class); - private final MerchantInboundBillingService service = new MerchantInboundBillingService(orders, pricing); + private final InboundMeasurementService measurements = mock(InboundMeasurementService.class); + private final MeasurementImageClient imageClient = mock(MeasurementImageClient.class); + private final MerchantInboundBillingService service = new MerchantInboundBillingService( + orders, pricing, measurements, imageClient, "internal-token"); @Test void 客户只能看到本租户已完成入库且已发布的费用() { InboundOrderEntity inbound = inbound("INBOUNDED", "SENT"); - InboundOrderDetail detail = mock(InboundOrderDetail.class); + InboundOrderDetail detail = new InboundOrderDetail(mock(com.baiyue.firstleg.dto.InboundOrderSummary.class), + "客户备注", List.of(), List.of( + new InboundOrderDetail.FileSummary(1L, "RELATED", "public.pdf", "PUBLIC", 1, 1L, null), + new InboundOrderDetail.FileSummary(2L, "INTERNAL", "internal.pdf", "INTERNAL", 1, 1L, null)), + List.of(new InboundOrderDetail.InternalNoteSummary(1L, "内部备注", 1L, "客服", null)), + List.of(new InboundOrderDetail.StatusHistorySummary(null, "INBOUNDED", "COMPLETE", + "内部原因", 1L, "客服", null))); + InboundMeasurementResponses.Detail measurement = mock(InboundMeasurementResponses.Detail.class); FeeSnapshotResponse fee = mock(FeeSnapshotResponse.class); when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound); when(orders.detail(10001L, inbound)).thenReturn(detail); + when(measurements.detail(10001L, 88L)).thenReturn(measurement); + when(measurement.items()).thenReturn(List.of()); + when(measurement.weightResults()).thenReturn(List.of()); when(pricing.latest(10001L, 88L)).thenReturn(fee); + when(fee.chargeMode()).thenReturn("KG"); + when(fee.chargeUnit()).thenReturn("KG"); + when(fee.actualWeightKg()).thenReturn(new BigDecimal("20")); + when(fee.actualVolumeCbm()).thenReturn(new BigDecimal("0.24")); + when(fee.convertedQuantity()).thenReturn(new BigDecimal("40.08")); + when(fee.chargeableQuantity()).thenReturn(new BigDecimal("40.08")); var result = service.find(10001L, 77L); - assertThat(result.inbound()).isSameAs(detail); + assertThat(result.inbound().files()).extracting(InboundOrderDetail.FileSummary::fileId) + .containsExactly(1L); + assertThat(result.inbound().internalNotes()).isEmpty(); + assertThat(result.inbound().statusHistory()).isEmpty(); + assertThat(result.billingBasis().chargeableBasis()).isEqualTo("VOLUME_WEIGHT"); assertThat(result.latestFee()).isSameAs(fee); verify(orders).findByOrderId(10001L, 77L); } @@ -56,6 +84,42 @@ class MerchantInboundBillingServiceTest { verify(pricing, never()).latest(20002L, 88L); } + @Test + void 只能下载已发布计费明细实际引用的图片() { + InboundOrderEntity inbound = inbound("INBOUNDED", "SENT"); + InboundMeasurementResponses.Detail measurement = mock(InboundMeasurementResponses.Detail.class); + InboundMeasurementResponses.Item item = mock(InboundMeasurementResponses.Item.class); + when(item.intelligentImageFileId()).thenReturn(41L); + when(measurement.items()).thenReturn(List.of(item)); + when(measurement.weightResults()).thenReturn(List.of()); + when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound); + when(measurements.detail(10001L, 88L)).thenReturn(measurement); + when(imageClient.download("internal-token", 10001L, 88L, 41L)).thenReturn( + org.springframework.http.ResponseEntity.ok() + .contentType(org.springframework.http.MediaType.IMAGE_PNG) + .body(new byte[]{1, 2, 3})); + + var image = service.downloadImage(10001L, 77L, 41L); + + assertThat(image.contentType()).isEqualTo("image/png"); + assertThat(image.content()).containsExactly(1, 2, 3); + } + + @Test + void 拒绝下载未被当前计费明细引用的图片() { + InboundOrderEntity inbound = inbound("INBOUNDED", "SENT"); + InboundMeasurementResponses.Detail measurement = mock(InboundMeasurementResponses.Detail.class); + when(measurement.items()).thenReturn(List.of()); + when(measurement.weightResults()).thenReturn(List.of()); + when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound); + when(measurements.detail(10001L, 88L)).thenReturn(measurement); + + assertThatThrownBy(() -> service.downloadImage(10001L, 77L, 99L)) + .isInstanceOf(BizException.class) + .hasMessageContaining("不存在该测量图片"); + verify(imageClient, never()).download("internal-token", 10001L, 88L, 99L); + } + private static InboundOrderEntity inbound(String process, String feeStatus) { InboundOrderEntity inbound = new InboundOrderEntity(); inbound.setId(88L);