feat: support mixed-SKU dropship cartons

This commit is contained in:
2026-09-11 15:38:19 +08:00
parent 0db11517db
commit cc7c2f0954
42 changed files with 1263 additions and 128 deletions
@@ -4,9 +4,11 @@ import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.web.GlobalExceptionHandler;
import com.baiyue.print.dto.FreightA4PdfDtos;
import com.baiyue.print.dto.DropshipPalletA4PdfDtos;
import com.baiyue.print.dto.InboundMarkPdfDtos;
import com.baiyue.print.service.FreightA4PdfService;
import com.baiyue.print.service.DispatchBolPdfService;
import com.baiyue.print.service.DropshipPalletA4PdfService;
import com.baiyue.print.service.InboundMarkPdfService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
@@ -45,6 +47,8 @@ class InternalPdfControllerTest {
private FreightA4PdfService freightA4PdfService;
@MockBean
private DispatchBolPdfService dispatchBolPdfService;
@MockBean
private DropshipPalletA4PdfService dropshipPalletA4PdfService;
@Resource
private InternalPdfController controller;
private MockMvc mockMvc;
@@ -169,6 +173,27 @@ class InternalPdfControllerTest {
verify(freightA4PdfService).generate(any(FreightA4PdfDtos.Request.class));
}
@Test
void returnsDropshipPalletA4PdfWithDownloadHeaders() throws Exception {
byte[] pdf = "%PDF-pallet-a4".getBytes(StandardCharsets.US_ASCII);
when(dropshipPalletA4PdfService.generate(any())).thenReturn(
new DropshipPalletA4PdfService.GeneratedPdf(pdf, "DP-260911-0034-A4托盘信息单.pdf", 1));
mockMvc.perform(post("/internal/print/v1/pdfs/dropship-pallet-a4-documents")
.header(InternalPdfController.INTERNAL_TOKEN_HEADER, TOKEN)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_PDF, MediaType.APPLICATION_JSON)
.content(validDropshipPalletA4RequestJson()))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_PDF))
.andExpect(content().bytes(pdf))
.andExpect(header().string("X-Print-Page-Count", "1"))
.andExpect(header().string("Content-Disposition",
org.hamcrest.Matchers.containsString("filename*=")));
verify(dropshipPalletA4PdfService).generate(any(DropshipPalletA4PdfDtos.Request.class));
}
@Test
void endpointDoesNotPinProducedMediaTypeToPdf() throws Exception {
PostMapping mapping = InternalPdfController.class
@@ -185,11 +210,13 @@ class InternalPdfControllerTest {
assertThat(contract)
.contains("/internal/print/v1/pdfs/inbound-marks:")
.contains("/internal/print/v1/pdfs/freight-a4-documents:")
.contains("/internal/print/v1/pdfs/dropship-pallet-a4-documents:")
.contains("application/pdf:")
.contains("application/json:")
.contains("name: X-Internal-Service-Token")
.contains("SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1:")
.contains("BILL_OF_LADING_CONFIRMATION_A4_V1:")
.contains("DROPSHIP_PALLET_A4_V1")
.contains("additionalProperties: false")
.contains("minItems: 1")
.contains("required: [marksAndNos, quantity, packaging, descriptionOfGoods, grossWeight, measurement]")
@@ -238,4 +265,30 @@ class InternalPdfControllerTest {
""";
}
private String validDropshipPalletA4RequestJson() {
return """
{
"templateCode": "DROPSHIP_PALLET_A4_V1",
"pageSpec": {"widthMm": 210, "heightMm": 297},
"downloadFileName": "DP-260911-0034-A4托盘信息单.pdf",
"documents": [{
"palletNo": "DP-260911-0034",
"inboundCodes": "NYEXX2609110249",
"cartonCount": 3,
"skuCount": 1,
"weightKg": 90,
"volumeCbm": 0.534,
"barcodeContent": "DP-260911-0034",
"skuRows": [{
"inboundCode": "NYEXX2609110249",
"skuCode": "SKU-001",
"cargoName": "蓝牙音箱",
"cartonCount": 3,
"skuQuantity": 90
}]
}]
}
""";
}
}
@@ -0,0 +1,22 @@
package com.baiyue.print.renderer;
import static org.assertj.core.api.Assertions.assertThat;
import com.google.zxing.common.BitMatrix;
import org.junit.jupiter.api.Test;
class PdfBarcodeRendererTest {
@Test
void encodesCode128DeterministicallyWithQuietZone() {
PdfBarcodeRenderer renderer = new PdfBarcodeRenderer();
BitMatrix first = renderer.encode("DP-260911-0034");
BitMatrix second = renderer.encode("DP-260911-0034");
assertThat(first).isEqualTo(second);
assertThat(first.getHeight()).isEqualTo(1);
assertThat(first.getWidth()).isGreaterThan(100);
assertThat(first.getEnclosingRectangle()).isNotNull();
assertThat(first.getEnclosingRectangle()[0]).isGreaterThanOrEqualTo(5);
}
}
@@ -0,0 +1,93 @@
package com.baiyue.print.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.print.config.PrintPdfProperties;
import com.baiyue.print.dto.DropshipPalletA4PdfDtos;
import com.baiyue.print.renderer.DropshipPalletA4Renderer;
import com.baiyue.print.renderer.PdfBarcodeRenderer;
import com.baiyue.print.renderer.PdfUnits;
import jakarta.annotation.Resource;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(classes = {DropshipPalletA4PdfService.class, PrintPdfProperties.class,
PdfGenerationLimiter.class, DropshipPalletA4Renderer.class, PdfBarcodeRenderer.class})
class DropshipPalletA4PdfServiceGenerationTest {
@Resource private DropshipPalletA4PdfService service;
@Test
void generatesOneA4PageWithPalletBarcodeAndSkuDetails() throws Exception {
DropshipPalletA4PdfDtos.Document document = new DropshipPalletA4PdfDtos.Document(
"DP-260911-0034", "NYEXX2609110249", 3, 2,
new BigDecimal("90"), new BigDecimal("0.534"), "DP-260911-0034",
List.of(
new DropshipPalletA4PdfDtos.SkuRow("NYEXX2609110249", "SKU-001", "蓝牙音箱", 2, 40),
new DropshipPalletA4PdfDtos.SkuRow("NYEXX2609110249", "SKU-002", "充电线", 1, 50)));
var generated = service.generate(new DropshipPalletA4PdfDtos.Request(
DropshipPalletA4PdfDtos.TEMPLATE,
new DropshipPalletA4PdfDtos.PageSpec(new BigDecimal("210"), new BigDecimal("297")),
"DP-260911-0034-A4托盘信息单.pdf", List.of(document)));
assertThat(generated.pageCount()).isEqualTo(1);
assertThat(generated.content()).startsWith("%PDF-".getBytes(java.nio.charset.StandardCharsets.US_ASCII));
try (PDDocument pdf = Loader.loadPDF(generated.content())) {
assertThat(pdf.getNumberOfPages()).isEqualTo(1);
assertThat(pdf.getPage(0).getMediaBox().getWidth()).isCloseTo(PdfUnits.mmToPt(210), org.assertj.core.data.Offset.offset(.002f));
assertThat(pdf.getPage(0).getMediaBox().getHeight()).isCloseTo(PdfUnits.mmToPt(297), org.assertj.core.data.Offset.offset(.002f));
assertThat(new PDFTextStripper().getText(pdf))
.contains("DP-260911-0034", "NYEXX2609110249", "SKU 明细", "SKU-001", "SKU-002", "40", "50")
.containsOnlyOnce("DP-260911-0034")
.doesNotContain("状态", "待封托", "已封托");
}
}
@Test
void keepsTenInboundCodesAndThirtyTwoSkuRowsOnOneA4Page() throws Exception {
List<String> inboundCodes = new ArrayList<>();
for (int index = 1; index <= 10; index++) inboundCodes.add("NYEXX260911" + String.format("%04d", 240 + index));
List<DropshipPalletA4PdfDtos.SkuRow> rows = new ArrayList<>();
for (int index = 1; index <= 32; index++) {
rows.add(new DropshipPalletA4PdfDtos.SkuRow(inboundCodes.get((index - 1) % inboundCodes.size()),
"SKU-" + String.format("%03d", index), "测试商品" + index, 2, index * 5));
}
DropshipPalletA4PdfDtos.Document document = new DropshipPalletA4PdfDtos.Document(
"DP-260911-0034", String.join("/", inboundCodes), 64, 32,
new BigDecimal("860.5"), new BigDecimal("6.728"), "DP-260911-0034", rows);
var generated = service.generate(new DropshipPalletA4PdfDtos.Request(
DropshipPalletA4PdfDtos.TEMPLATE,
new DropshipPalletA4PdfDtos.PageSpec(new BigDecimal("210"), new BigDecimal("297")),
"DP-260911-0034-高密度预览.pdf", List.of(document)));
try (PDDocument pdf = Loader.loadPDF(generated.content())) {
assertThat(pdf.getNumberOfPages()).isEqualTo(1);
assertThat(new PDFTextStripper().getText(pdf))
.contains("NYEXX2609110241", "NYEXX2609110250", "SKU-001", "SKU-032")
.containsOnlyOnce("DP-260911-0034");
}
}
@Test
void rejectsSkuRowsThatCannotFitOnOneA4Page() {
List<DropshipPalletA4PdfDtos.SkuRow> rows = new ArrayList<>();
for (int index = 0; index < 35; index++) {
rows.add(new DropshipPalletA4PdfDtos.SkuRow("INBOUND", "SKU-" + index, "商品", 1, 1));
}
DropshipPalletA4PdfDtos.Document document = new DropshipPalletA4PdfDtos.Document(
"DP-260911-0034", "INBOUND", 35, 35,
BigDecimal.ZERO, BigDecimal.ZERO, "DP-260911-0034", rows);
assertThatThrownBy(() -> service.generate(new DropshipPalletA4PdfDtos.Request(
DropshipPalletA4PdfDtos.TEMPLATE,
new DropshipPalletA4PdfDtos.PageSpec(new BigDecimal("210"), new BigDecimal("297")),
"pallet.pdf", List.of(document))))
.isInstanceOf(BizException.class)
.hasMessageContaining("one-page A4 capacity");
}
}