From ee97565f67e4cc9a986ebc8a3da19ceb6b7fda06 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sun, 20 Sep 2026 13:51:06 +0800 Subject: [PATCH 01/26] fix: paginate dropship pallet print details --- .../InboundCartonLabelPrintService.java | 33 ++++++++++++++-- .../InboundCartonLabelPrintServiceTest.java | 38 +++++++++++++++++++ .../controller/PrintRequestSizeFilter.java | 1 + .../PrintRequestSizeFilterTest.java | 11 ++++++ 4 files changed, 79 insertions(+), 4 deletions(-) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundCartonLabelPrintService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundCartonLabelPrintService.java index 8b2e7c047..e3148d064 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundCartonLabelPrintService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundCartonLabelPrintService.java @@ -32,6 +32,8 @@ public class InboundCartonLabelPrintService { private static final BigDecimal PAGE_MM = new BigDecimal("100"); private static final BigDecimal A4_WIDTH_MM = new BigDecimal("210"); private static final BigDecimal A4_HEIGHT_MM = new BigDecimal("297"); + private static final int DROPSHIP_INBOUND_ROWS_PER_PAGE = 10; + private static final int DROPSHIP_SKU_ROWS_PER_PAGE = 34; private static final List SAFE_RESPONSE_HEADERS = List.of( HttpHeaders.CONTENT_DISPOSITION, HttpHeaders.CACHE_CONTROL, HttpHeaders.PRAGMA, "X-Print-Page-Count", "X-Request-Id", "X-Trace-Id"); @@ -134,7 +136,7 @@ public class InboundCartonLabelPrintService { } Map customerNicknameCache = new HashMap<>(); List documents = pallets.stream() - .map(pallet -> dropshipPalletDocument(pallet, customerNicknameCache)).toList(); + .flatMap(pallet -> dropshipPalletDocuments(pallet, customerNicknameCache).stream()).toList(); String fileName = pallets.size() == 1 ? pallets.get(0).palletNo() + "-A4托盘信息单.pdf" : inboundCode + "-" + pallets.size() + "托-A4托盘信息单.pdf"; @@ -148,7 +150,7 @@ public class InboundCartonLabelPrintService { } } - private PrintPdfClient.DropshipPalletDocument dropshipPalletDocument( + private List dropshipPalletDocuments( Pallet pallet, Map customerNicknameCache) { if (pallet == null || pallet.palletId() == null || !StringUtils.hasText(pallet.palletNo())) { throw new BizException(ResultCode.CONFLICT, "托盘码尚未生成,不能打印"); @@ -172,8 +174,31 @@ public class InboundCartonLabelPrintService { .stream().map(row -> new PrintPdfClient.DropshipPalletSkuRow( row.getSkuCode(), number(row.getCartonCount()), number(row.getItemQuantity()), unitsPerCarton(row))).toList(); - return new PrintPdfClient.DropshipPalletDocument( - pallet.palletNo(), destinationWarehouse, pallet.palletNo(), inboundRows, skuRows); + int pageCount = Math.max(1, Math.max(pageCount(inboundRows.size(), DROPSHIP_INBOUND_ROWS_PER_PAGE), + pageCount(skuRows.size(), DROPSHIP_SKU_ROWS_PER_PAGE))); + List documents = new ArrayList<>(pageCount); + for (int page = 0; page < pageCount; page++) { + documents.add(new PrintPdfClient.DropshipPalletDocument( + pallet.palletNo(), destinationWarehouse, pallet.palletNo(), + pageOrRepeat(inboundRows, page, DROPSHIP_INBOUND_ROWS_PER_PAGE), + page(skuRows, page, DROPSHIP_SKU_ROWS_PER_PAGE))); + } + return List.copyOf(documents); + } + + private int pageCount(int size, int pageSize) { + return size == 0 ? 0 : (size + pageSize - 1) / pageSize; + } + + private List page(List rows, int page, int pageSize) { + int from = Math.min(rows.size(), page * pageSize); + int to = Math.min(rows.size(), from + pageSize); + return rows.subList(from, to); + } + + private List pageOrRepeat(List rows, int page, int pageSize) { + List selected = page(rows, page, pageSize); + return selected.isEmpty() ? page(rows, 0, pageSize) : selected; } private String unitsPerCarton(DropshipPalletizationMapper.PrintSkuRow row) { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundCartonLabelPrintServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundCartonLabelPrintServiceTest.java index a9f25abe4..ec288a2f8 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundCartonLabelPrintServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundCartonLabelPrintServiceTest.java @@ -18,6 +18,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.test.util.ReflectionTestUtils; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -108,6 +109,43 @@ class InboundCartonLabelPrintServiceTest { assertThat(response.getBody()).containsExactly(4, 5, 6); } + @Test + void splitsLargeDropshipPalletDetailsAcrossPrintableA4Pages() { + Pallet pallet = dropshipPallet("OPEN"); + stubPalletFacts(90001L); + List skuRows = new ArrayList<>(); + for (int index = 1; index <= 35; index++) { + DropshipPalletizationMapper.PrintSkuRow row = new DropshipPalletizationMapper.PrintSkuRow(); + row.setSkuCode("SKU-" + index); + row.setCartonCount(1); + row.setItemQuantity(index); + row.setMinUnitsPerCarton(index); + row.setMaxUnitsPerCarton(index); + skuRows.add(row); + } + when(palletization.printSkuRows(90001L)).thenReturn(skuRows); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_PDF); + when(print.generateDropshipPalletA4Documents(org.mockito.ArgumentMatchers.anyString(), + org.mockito.ArgumentMatchers.eq("request-large"), org.mockito.ArgumentMatchers.any())) + .thenReturn(new ResponseEntity<>(new byte[] {1}, headers, + org.springframework.http.HttpStatus.OK)); + + service.generateDropshipPallet(pallet, "PCHK2608040174", "request-large"); + + ArgumentCaptor request = + ArgumentCaptor.forClass(PrintPdfClient.DropshipPalletA4PdfRequest.class); + verify(print).generateDropshipPalletA4Documents(org.mockito.ArgumentMatchers.eq("internal-token"), + org.mockito.ArgumentMatchers.eq("request-large"), request.capture()); + assertThat(request.getValue().documents()).hasSize(2).allSatisfy(document -> { + assertThat(document.palletNo()).isEqualTo("DP-260911-0001"); + assertThat(document.inboundRows()).isNotEmpty().hasSizeLessThanOrEqualTo(10); + assertThat(document.skuRows()).hasSizeLessThanOrEqualTo(34); + }); + assertThat(request.getValue().documents().get(0).skuRows()).hasSize(34); + assertThat(request.getValue().documents().get(1).skuRows()).hasSize(1); + } + @Test void forwardsStableLabelsWithoutSkuAndOnlySafePdfHeaders() throws Exception { when(labels.list(10001L, 70001L, List.of(1))).thenReturn(batch()); 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 00215973d..c6c4a9375 100644 --- a/print/src/main/java/com/baiyue/print/controller/PrintRequestSizeFilter.java +++ b/print/src/main/java/com/baiyue/print/controller/PrintRequestSizeFilter.java @@ -31,6 +31,7 @@ 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/dropship-pallet-a4-documents", "/internal/print/v1/pdfs/freight-a4-documents"); @Resource private PrintPdfProperties properties; diff --git a/print/src/test/java/com/baiyue/print/controller/PrintRequestSizeFilterTest.java b/print/src/test/java/com/baiyue/print/controller/PrintRequestSizeFilterTest.java index 2e24244e3..0ed8c97fc 100644 --- a/print/src/test/java/com/baiyue/print/controller/PrintRequestSizeFilterTest.java +++ b/print/src/test/java/com/baiyue/print/controller/PrintRequestSizeFilterTest.java @@ -62,6 +62,17 @@ class PrintRequestSizeFilterTest { assertTooLarge(response); } + @Test + void capsDropshipPalletRequestsBeforeDeserialization() throws Exception { + var response = new MockHttpServletResponse(); + AtomicReference called = new AtomicReference<>(false); + filter.doFilter(request("12345678901", true, + "/internal/print/v1/pdfs/dropship-pallet-a4-documents"), response, + (ignoredRequest, ignoredResponse) -> called.set(true)); + assertThat(called).hasValue(false); + assertTooLarge(response); + } + @Test void rejectsOversizedDeclaredContentLengthWithoutCallingDownstream() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest() { From 493d62e04ca0db7a8f8e92e959e648febc8b95f6 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sun, 20 Sep 2026 14:05:00 +0800 Subject: [PATCH 02/26] fix: keep pallet labels on A4 templates --- print/AGENTS.md | 3 +- .../controller/InternalPdfController.java | 16 +++++ .../controller/PrintRequestSizeFilter.java | 1 + .../print/dto/GenericPalletPdfDtos.java | 13 ++++ .../print/renderer/GenericPalletRenderer.java | 43 ++++++++++++ .../service/GenericPalletPdfService.java | 70 +++++++++++++++++++ .../resources/openapi/print-internal-v1.yaml | 41 +++++++++++ .../controller/InternalPdfControllerTest.java | 5 ++ .../PrintRequestSizeFilterTest.java | 10 +++ ...GenericPalletPdfServiceGenerationTest.java | 43 ++++++++++++ warehouse-overseas-service/AGENTS.md | 2 +- .../warehouse/client/PrintPdfClient.java | 11 +-- .../service/PalletLabelPrintService.java | 11 ++- .../resources/openapi/pallet-label-v1.yaml | 2 +- .../service/PalletLabelPrintServiceTest.java | 25 +++---- 15 files changed, 269 insertions(+), 27 deletions(-) create mode 100644 print/src/main/java/com/baiyue/print/dto/GenericPalletPdfDtos.java create mode 100644 print/src/main/java/com/baiyue/print/renderer/GenericPalletRenderer.java create mode 100644 print/src/main/java/com/baiyue/print/service/GenericPalletPdfService.java create mode 100644 print/src/test/java/com/baiyue/print/service/GenericPalletPdfServiceGenerationTest.java diff --git a/print/AGENTS.md b/print/AGENTS.md index 28efab4c8..0e81b655c 100644 --- a/print/AGENTS.md +++ b/print/AGENTS.md @@ -5,7 +5,7 @@ - 本模块是无数据库的无状态 PDF 渲染服务;调用方传入完整 `labels[]` 或 `documents[]`,模块在内存中生成一次性多页 PDF 并同步返回。 - 不保存模板、任务、历史或文件,不连接数据库、File 服务或打印机,不反查或修正业务数据。 - `INBOUND_MARK_V1` 箱唛不接收或展示 SKU(包括一件代发箱唛),二维码仅编码稳定箱码;SKU 明细由一件代发 A4 托盘信息单展示,不能重新加入箱唛版面。 -- 当前支持 `INBOUND_MARK_V1`(100×100 mm)、`FBA_PALLET_A4_LANDSCAPE_V1`(297×210 mm),以及纵向 A4 的 `DROPSHIP_PALLET_A4_V2`、`SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1`、`BILL_OF_LADING_CONFIRMATION_A4_V1`、`DISPATCH_BILL_OF_LADING_A4_V2`。FBA 模板由国内和海外统一使用,每托一页,只展示 SCK8 等 FBA 仓码、完整托盘号的 Code 128 条码和完整托盘号;141 等内部编号保留在托盘号中。一件代发每托一页,展示托盘 Code 128、目的仓、最多 10 行入仓码/客户昵称和最多 34 行 SKU,不展示状态、重量或体积。新尺寸或布局必须使用新模板编码。 +- 当前只有箱唛 `INBOUND_MARK_V1` 使用 100×100 mm;托盘标不得复用该模板。横向 A4 托盘模板包括 `FBA_PALLET_A4_LANDSCAPE_V1` 与 `PALLET_A4_LANDSCAPE_V1`,纵向 A4 包括 `DROPSHIP_PALLET_A4_V2`,其他 A4 单据为 `SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1`、`BILL_OF_LADING_CONFIRMATION_A4_V1`、`DISPATCH_BILL_OF_LADING_A4_V2`。FBA 模板由国内和海外统一使用,每托一页,只展示 SCK8 等 FBA 仓码、完整托盘号的 Code 128 条码和完整托盘号;141 等内部编号保留在托盘号中。一件代发展示托盘 Code 128、目的仓、每页最多 10 行入仓码/客户昵称和 34 行 SKU,超量由调用方按同一托盘自动续页,不展示状态、重量或体积。新尺寸或布局必须使用新模板编码。 ## 快速索引 @@ -13,6 +13,7 @@ |---|---| | 调用方接入指南 | `README.md` | | 国内/海外统一 FBA 托盘标 | `dto/FbaPalletPdfDtos.java`、`service/FbaPalletPdfService.java`、`renderer/FbaPalletRenderer.java`;内部 `/internal/print/v1/pdfs/fba-pallet-labels`,回归 `src/test/java/com/baiyue/print/service/FbaPalletPdfServiceTest.java` | +| 非 FBA 通用托盘标 | `dto/GenericPalletPdfDtos.java`、`service/GenericPalletPdfService.java`、`renderer/GenericPalletRenderer.java`;内部 `/internal/print/v1/pdfs/pallet-a4-labels` | | 应用入口 | `src/main/java/com/baiyue/print/PrintServiceApplication.java` | | 健康状态 API | `controller/PrintHealthController.java`(`/api/platform/v1/print/status`) | | 内部 PDF API | `controller/InternalPdfController.java`(`/internal/print/v1/pdfs/inbound-marks`、`/internal/print/v1/pdfs/dropship-pallet-a4-documents`、`/internal/print/v1/pdfs/freight-a4-documents`) | 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 73caf2a75..ec8cdeaa8 100644 --- a/print/src/main/java/com/baiyue/print/controller/InternalPdfController.java +++ b/print/src/main/java/com/baiyue/print/controller/InternalPdfController.java @@ -5,7 +5,9 @@ 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.dto.GenericPalletPdfDtos; import com.baiyue.print.service.FbaPalletPdfService; +import com.baiyue.print.service.GenericPalletPdfService; import com.baiyue.print.dto.DispatchBolPdfDtos; import com.baiyue.print.dto.DropshipPalletA4PdfDtos; import com.baiyue.print.dto.InboundMarkPdfDtos; @@ -39,6 +41,7 @@ public class InternalPdfController { @Resource private DispatchBolPdfService dispatchBolPdfService; @Resource private DropshipPalletA4PdfService dropshipPalletA4PdfService; @Resource private FbaPalletPdfService fbaPalletPdfService; + @Resource private GenericPalletPdfService genericPalletPdfService; @Resource private InternalServiceTokenGuard tokenGuard; @PostMapping(value = "/fba-pallet-labels", consumes = MediaType.APPLICATION_JSON_VALUE) @@ -54,6 +57,19 @@ public class InternalPdfController { .contentType(MediaType.APPLICATION_PDF).contentLength(content.length).body(content); } + @PostMapping(value = "/pallet-a4-labels", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity generatePalletA4Labels( + @RequestHeader(value = INTERNAL_TOKEN_HEADER, required = false) String internalToken, + @Valid @RequestBody GenericPalletPdfDtos.Request request) { + tokenGuard.verify(internalToken); + GenericPalletPdfService.GeneratedPdf pdf = genericPalletPdfService.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 c6c4a9375..2d726108b 100644 --- a/print/src/main/java/com/baiyue/print/controller/PrintRequestSizeFilter.java +++ b/print/src/main/java/com/baiyue/print/controller/PrintRequestSizeFilter.java @@ -31,6 +31,7 @@ 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/pallet-a4-labels", "/internal/print/v1/pdfs/dropship-pallet-a4-documents", "/internal/print/v1/pdfs/freight-a4-documents"); diff --git a/print/src/main/java/com/baiyue/print/dto/GenericPalletPdfDtos.java b/print/src/main/java/com/baiyue/print/dto/GenericPalletPdfDtos.java new file mode 100644 index 000000000..c990eecef --- /dev/null +++ b/print/src/main/java/com/baiyue/print/dto/GenericPalletPdfDtos.java @@ -0,0 +1,13 @@ +package com.baiyue.print.dto; + +import java.math.BigDecimal; +import java.util.List; + +public final class GenericPalletPdfDtos { + public static final String TEMPLATE = "PALLET_A4_LANDSCAPE_V1"; + private GenericPalletPdfDtos() {} + + public record Request(String templateCode, PageSpec pageSpec, String downloadFileName, List