From 912feca56fa2d9efdca1343f8b99e09fbc0929c5 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 16 Sep 2026 12:02:38 +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 --- .../firstleg/client/PrintPdfClient.java | 9 +++ .../service/InboundMeasurementService.java | 43 ++++++++++-- .../service/LoadingPalletLabelService.java | 12 ++-- .../InboundMeasurementServiceTest.java | 59 ++++++++++++++++ .../controller/InternalPdfController.java | 16 +++++ .../controller/PrintRequestSizeFilter.java | 1 + .../baiyue/print/dto/FbaPalletPdfDtos.java | 14 ++++ .../print/renderer/FbaPalletRenderer.java | 44 ++++++++++++ .../print/service/FbaPalletPdfService.java | 68 +++++++++++++++++++ .../warehouse/client/PrintPdfClient.java | 9 +++ .../service/PalletLabelPrintService.java | 17 +++-- 11 files changed, 275 insertions(+), 17 deletions(-) create mode 100644 print/src/main/java/com/baiyue/print/dto/FbaPalletPdfDtos.java create mode 100644 print/src/main/java/com/baiyue/print/renderer/FbaPalletRenderer.java create mode 100644 print/src/main/java/com/baiyue/print/service/FbaPalletPdfService.java diff --git a/firstleg/src/main/java/com/baiyue/firstleg/client/PrintPdfClient.java b/firstleg/src/main/java/com/baiyue/firstleg/client/PrintPdfClient.java index 4fbe1ea09..9eb2c5dcf 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/client/PrintPdfClient.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/client/PrintPdfClient.java @@ -14,6 +14,15 @@ import java.util.Map; @FeignClient(name = "print-service", contextId = "firstlegPrintPdfClient", url = "${baiyue.clients.print.url:}", path = "/internal/print/v1/pdfs") public interface PrintPdfClient { + @PostMapping(value = "/fba-pallet-labels", consumes = "application/json") + ResponseEntity generateFbaPalletLabels( + @RequestHeader("X-Internal-Service-Token") String token, + @RequestHeader(value = "X-Request-Id", required = false) String requestId, + @RequestBody FbaPalletRequest request); + + record FbaPalletRequest(String templateCode, PageSpec pageSpec, String downloadFileName, List labels) {} + record FbaPalletLabel(String fbaWarehouseCode, String palletCode) {} + @PostMapping(value = "/inbound-marks", consumes = "application/json") ResponseEntity generateInboundMarks( @RequestHeader("X-Internal-Service-Token") String token, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java index 506daba74..1deb3c156 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java @@ -32,6 +32,8 @@ import org.slf4j.LoggerFactory; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -636,11 +638,7 @@ public class InboundMeasurementService { || cargo.cargoId() <= 0 || !java.util.Objects.equals(cargo.orderId(), inbound.getOrderId()))) { throw new BizException(ResultCode.CONFLICT, "订单服务返回的货物明细归属不一致"); } - long packages = cargoes.stream() - .map(OrderCargoClient.CargoLabelBinding::packageCount) - .filter(java.util.Objects::nonNull) - .mapToLong(Integer::longValue) - .sum(); + long packages = physicalPackageCount(cargoes, inbound.getFulfillmentType()); if (packages > Integer.MAX_VALUE) { throw new BizException(ResultCode.VALIDATION_FAILED, "客户最新申报总箱数超过系统上限"); } @@ -671,6 +669,41 @@ public class InboundMeasurementService { return new LatestDeclaration(snapshot, Map.copyOf(fbaShipmentNos)); } + /** + * 普通订单每条货物明细代表独立箱规;一件代发允许同一物理箱包含多个 SKU, + * 因此同一 packingGroupNo 只计算一次箱数。该口径与箱唛生成保持一致。 + */ + private long physicalPackageCount(List cargoes, + String fulfillmentType) { + if (!"ONE_CLICK_DROPSHIP".equals(fulfillmentType)) { + return cargoes.stream().mapToLong(this::requireDeclaredPackageCount).sum(); + } + Map> grouped = new LinkedHashMap<>(); + int single = 0; + for (OrderCargoClient.CargoLabelBinding cargo : cargoes) { + requireDeclaredPackageCount(cargo); + String group = StringUtils.hasText(cargo.packingGroupNo()) + ? cargo.packingGroupNo().trim() : "__single_" + (++single); + grouped.computeIfAbsent(group, ignored -> new ArrayList<>()).add(cargo); + } + return grouped.entrySet().stream().mapToLong(entry -> { + List lines = entry.getValue(); + int count = lines.get(0).packageCount(); + if (lines.stream().anyMatch(line -> line.packageCount() != count)) { + throw new BizException(ResultCode.VALIDATION_FAILED, + "同一混箱组合的箱数必须一致:" + entry.getKey()); + } + return count; + }).sum(); + } + + private int requireDeclaredPackageCount(OrderCargoClient.CargoLabelBinding cargo) { + if (cargo.packageCount() == null || cargo.packageCount() < 1) { + throw new BizException(ResultCode.VALIDATION_FAILED, "订单货物明细缺少有效箱数"); + } + return cargo.packageCount(); + } + private List loadDeclarationCargoes(InboundOrderEntity inbound) { try { ApiResponse> response = orderCargoClient.listForCartonLabels( diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPalletLabelService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPalletLabelService.java index ce875d97c..91821161f 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPalletLabelService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPalletLabelService.java @@ -38,7 +38,6 @@ import java.util.Objects; /** 散货托盘码按真实柜号、FBA 地址内部编号和仓内序号组成;一件代发继续使用装箱打托事实。 */ @Service public class LoadingPalletLabelService { - private static final BigDecimal PAGE_MM = new BigDecimal("100"); @Resource private LoadingPalletLabelMapper mapper; @Resource private PrintPdfClient printClient; @Resource private WarehouseMasterDataClient warehouseClient; @@ -85,13 +84,12 @@ public class LoadingPalletLabelService { : manifest.groups().stream().filter(group -> group.destinationWarehouseCode().equalsIgnoreCase(warehouseCode)) .flatMap(group -> group.labels().stream()).limit(count).toList(); if (selected.isEmpty() || (count != null && selected.size() != count)) throw new BizException(ResultCode.BAD_REQUEST, "所选 FBA 仓或打印张数无效"); - List labels = selected.stream().map(label -> new PrintPdfClient.Label( - label.palletCode(), label.destinationWarehouseName(), "PALLET " + label.palletIndex(), - "CONTAINER " + label.containerNo(), label.palletCode(), null)).toList(); + List labels = selected.stream().map(label -> new PrintPdfClient.FbaPalletLabel( + label.destinationWarehouseCode(), label.palletCode())).toList(); String suffix = warehouseCode == null ? "全部" : warehouseCode + "-" + count + "张"; - var pdfRequest = new PrintPdfClient.InboundMarkPdfRequest("INBOUND_MARK_V1", - new PrintPdfClient.PageSpec(PAGE_MM, PAGE_MM), manifest.containerNo() + "-托盘码-" + suffix + ".pdf", labels); - try { return requirePdf(printClient.generateInboundMarks(internalToken, requestId, pdfRequest)); } + var pdfRequest = new PrintPdfClient.FbaPalletRequest("FBA_PALLET_A4_LANDSCAPE_V1", + new PrintPdfClient.PageSpec(new BigDecimal("297"), new BigDecimal("210")), manifest.containerNo() + "-托盘码-" + suffix + ".pdf", labels); + try { return requirePdf(printClient.generateFbaPalletLabels(internalToken, requestId, pdfRequest)); } catch (FeignException | NoFallbackAvailableException exception) { throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "打印服务暂不可用,请稍后重试"); } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java index 3d2e9134c..5daf6a6b5 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java @@ -524,6 +524,27 @@ class InboundMeasurementServiceTest { verify(repository, never()).deleteWeight(anyLong(), anyLong(), anyLong(), anyInt()); } + @Test + void detailCountsSameCartonMultiSkuGroupOnlyOnceForDropship() { + InboundOrderEntity inbound = inbound("MEASURING"); + inbound.setFulfillmentType("ONE_CLICK_DROPSHIP"); + when(repository.findInbound(1L, 10L)).thenReturn(inbound); + when(repository.listWeights(1L, 10L)).thenReturn(List.of()); + when(orderCargoClient.listForCartonLabels("internal-token", 1L, 80L)).thenReturn( + ApiResponse.success(List.of( + new OrderCargoClient.CargoLabelBinding( + 80001L, 80L, 1, "SKU A", "SKU-A", 20, 5, + new BigDecimal("40"), new BigDecimal("30"), new BigDecimal("20"), null, "MIX-1"), + new OrderCargoClient.CargoLabelBinding( + 80002L, 80L, 2, "SKU B", "SKU-B", 20, 3, + new BigDecimal("40"), new BigDecimal("30"), new BigDecimal("20"), null, "MIX-1")))); + + InboundMeasurementResponses.Detail result = service.detail(1L, 10L); + + assertThat(result.latestDeclaration().totalPackageCount()).isEqualTo(20); + assertThat(result.latestDeclaration().items()).hasSize(2); + } + @Test void creatingWeightResultNormalizesProviderConfidenceAtBusinessBoundary() { InboundOrderEntity inbound = inbound("WAIT_MEASURE"); @@ -1215,6 +1236,44 @@ class InboundMeasurementServiceTest { assertThat(inbound.getProcessStatus()).isEqualTo("MEASURE_PENDING"); } + @Test + void submitValidatesDropshipMultiSkuCartonsByPhysicalCartonCount() { + InboundOrderEntity inbound = inbound("MEASURING"); + inbound.setFulfillmentType("ONE_CLICK_DROPSHIP"); + inbound.setPalletCount(1); + InboundMeasurementEntity measurement = new InboundMeasurementEntity(); + measurement.setId(30L); + measurement.setStatus("RECEIVED"); + measurement.setVersion(6); + measurement.setTotalPackageCount(20); + InboundMeasurementItemEntity item = new InboundMeasurementItemEntity(); + item.setPackageCount(20); + InboundWeightResultEntity weight = weight(20L, "197.000", "KG", "CONFIRMED", 1); + weight.setGrossWeightKg(new BigDecimal("197.000")); + weight.setNetWeightKg(new BigDecimal("181.000")); + when(repository.findInbound(1L, 10L)).thenReturn(inbound); + when(repository.findMeasurement(1L, 10L, 30L)).thenReturn(measurement); + when(repository.listItems(1L, 30L)).thenReturn(List.of(item)); + when(repository.listWeights(1L, 10L)).thenReturn(List.of(weight)); + when(repository.updateMeasurement(measurement)).thenReturn(true); + when(repository.updateInbound(inbound)).thenReturn(true); + when(orderCargoClient.listForCartonLabels("internal-token", 1L, 80L)).thenReturn( + ApiResponse.success(List.of( + new OrderCargoClient.CargoLabelBinding( + 80001L, 80L, 1, "SKU A", "SKU-A", 20, 5, + new BigDecimal("40"), new BigDecimal("30"), new BigDecimal("20"), null, "MIX-1"), + new OrderCargoClient.CargoLabelBinding( + 80002L, 80L, 2, "SKU B", "SKU-B", 20, 3, + new BigDecimal("40"), new BigDecimal("30"), new BigDecimal("20"), null, "MIX-1")))); + + service.submit(1L, 10L, 9L, + new InboundMeasurementRequests.SubmitRequest(30L, 6)); + + assertThat(inbound.getTotalPackageCount()).isEqualTo(20); + assertThat(inbound.getProcessStatus()).isEqualTo("MEASURE_PENDING"); + assertThat(measurement.getStatus()).isEqualTo("SUBMITTED"); + } + @Test void submitRejectsWhenNoWeightImageHasBeenConfirmed() { InboundOrderEntity inbound = inbound("MEASURING"); diff --git a/print/src/main/java/com/baiyue/print/controller/InternalPdfController.java b/print/src/main/java/com/baiyue/print/controller/InternalPdfController.java index 48261eaa9..73caf2a75 100644 --- a/print/src/main/java/com/baiyue/print/controller/InternalPdfController.java +++ b/print/src/main/java/com/baiyue/print/controller/InternalPdfController.java @@ -4,6 +4,8 @@ import jakarta.annotation.Resource; import com.baiyue.common.web.CorrelationContext; import com.baiyue.print.dto.FreightA4PdfDtos; +import com.baiyue.print.dto.FbaPalletPdfDtos; +import com.baiyue.print.service.FbaPalletPdfService; import com.baiyue.print.dto.DispatchBolPdfDtos; import com.baiyue.print.dto.DropshipPalletA4PdfDtos; import com.baiyue.print.dto.InboundMarkPdfDtos; @@ -36,8 +38,22 @@ public class InternalPdfController { @Resource private FreightA4PdfService freightA4PdfService; @Resource private DispatchBolPdfService dispatchBolPdfService; @Resource private DropshipPalletA4PdfService dropshipPalletA4PdfService; + @Resource private FbaPalletPdfService fbaPalletPdfService; @Resource private InternalServiceTokenGuard tokenGuard; + @PostMapping(value = "/fba-pallet-labels", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity generateFbaPalletLabels( + @RequestHeader(value = INTERNAL_TOKEN_HEADER, required = false) String internalToken, + @Valid @RequestBody FbaPalletPdfDtos.Request request) { + tokenGuard.verify(internalToken); + FbaPalletPdfService.GeneratedPdf pdf = fbaPalletPdfService.generate(request); + byte[] content = pdf.content(); + return ResponseEntity.ok().cacheControl(CacheControl.noStore()).header(HttpHeaders.PRAGMA, "no-cache") + .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(pdf.fileName(), StandardCharsets.UTF_8).build().toString()) + .header("X-Content-Type-Options", "nosniff").header(PAGE_COUNT_HEADER, Integer.toString(pdf.pageCount())) + .contentType(MediaType.APPLICATION_PDF).contentLength(content.length).body(content); + } + @PostMapping(value = "/inbound-marks", consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity generateInboundMarks( @RequestHeader(value = INTERNAL_TOKEN_HEADER, required = false) String internalToken, diff --git a/print/src/main/java/com/baiyue/print/controller/PrintRequestSizeFilter.java b/print/src/main/java/com/baiyue/print/controller/PrintRequestSizeFilter.java index e55f33db9..00215973d 100644 --- a/print/src/main/java/com/baiyue/print/controller/PrintRequestSizeFilter.java +++ b/print/src/main/java/com/baiyue/print/controller/PrintRequestSizeFilter.java @@ -30,6 +30,7 @@ import org.springframework.web.filter.OncePerRequestFilter; public class PrintRequestSizeFilter extends OncePerRequestFilter { private static final java.util.Set PATHS = java.util.Set.of( "/internal/print/v1/pdfs/inbound-marks", + "/internal/print/v1/pdfs/fba-pallet-labels", "/internal/print/v1/pdfs/freight-a4-documents"); @Resource private PrintPdfProperties properties; diff --git a/print/src/main/java/com/baiyue/print/dto/FbaPalletPdfDtos.java b/print/src/main/java/com/baiyue/print/dto/FbaPalletPdfDtos.java new file mode 100644 index 000000000..a30da32b4 --- /dev/null +++ b/print/src/main/java/com/baiyue/print/dto/FbaPalletPdfDtos.java @@ -0,0 +1,14 @@ +package com.baiyue.print.dto; + +import java.math.BigDecimal; +import java.util.List; + +public final class FbaPalletPdfDtos { + public static final String TEMPLATE = "FBA_PALLET_A4_LANDSCAPE_V1"; + private FbaPalletPdfDtos() {} + + public record Request(String templateCode, PageSpec pageSpec, String downloadFileName, List