feat: match warehouse BOL print layout

This commit is contained in:
2026-09-11 11:35:37 +08:00
parent 7fd2e70024
commit d60689ce02
8 changed files with 191 additions and 47 deletions
@@ -20,8 +20,9 @@ import org.springframework.stereotype.Service;
@Service
public class DispatchBolPrintService {
private static final String TEMPLATE = "DISPATCH_BILL_OF_LADING_A4_V1";
private static final int ROWS_PER_PAGE = 12;
private static final String TEMPLATE = "DISPATCH_BILL_OF_LADING_A4_V2";
private static final int FIRST_PAGE_ROWS = 18;
private static final int CONTINUATION_PAGE_ROWS = 26;
private static final PrintPdfClient.PageSpec A4 = new PrintPdfClient.PageSpec(new BigDecimal("210"), new BigDecimal("297"));
@Resource private DispatchService dispatches;
@@ -56,22 +57,31 @@ public class DispatchBolPrintService {
List<PrintPdfClient.Document> documents = new ArrayList<>();
for (Map.Entry<String, List<DispatchResponse.BolLine>> group : groups.entrySet()) {
List<DispatchResponse.BolLine> lines = group.getValue();
int pages = (lines.size() + ROWS_PER_PAGE - 1) / ROWS_PER_PAGE;
int pages = pageCount(lines.size());
int total = lines.stream().mapToInt(DispatchResponse.BolLine::packageCount).sum();
String destination = lines.stream().map(DispatchResponse.BolLine::destinationWarehouseName).filter(v -> v != null && !v.isBlank()).findFirst().orElse("—");
int offset = 0;
for (int page = 0; page < pages; page++) {
List<PrintPdfClient.CargoRow> rows = lines.subList(page * ROWS_PER_PAGE, Math.min(lines.size(), (page + 1) * ROWS_PER_PAGE)).stream()
int capacity = page == 0 ? FIRST_PAGE_ROWS : CONTINUATION_PAGE_ROWS;
int end = Math.min(lines.size(), offset + capacity);
List<PrintPdfClient.CargoRow> rows = lines.subList(offset, end).stream()
.map(line -> new PrintPdfClient.CargoRow(text(line.containerNo()), text(line.palletNo()), text(line.fbaWarehouseCode()),
text(line.warehouseNo()), text(line.fbaShipmentId()), text(line.poReferenceNo()), line.packageCount())).toList();
documents.add(new PrintPdfClient.Document(text(detail.overseasWarehouseName()), "—", text(detail.carrierName()),
text(detail.fbaAppointmentNo()), dateTime(detail.estimatedArrivalAt()), text(detail.vehiclePlate()), "—",
group.getKey(), destination, "—", text(detail.dispatchNo()), unique(lines, true), unique(lines, false),
total, page + 1, pages, rows));
offset = end;
}
}
return List.copyOf(documents);
}
static int pageCount(int rowCount) {
if (rowCount <= FIRST_PAGE_ROWS) return 1;
return 1 + (rowCount - FIRST_PAGE_ROWS + CONTINUATION_PAGE_ROWS - 1) / CONTINUATION_PAGE_ROWS;
}
private String unique(List<DispatchResponse.BolLine> lines, boolean shipment) {
LinkedHashSet<String> values = new LinkedHashSet<>();
for (DispatchResponse.BolLine line : lines) {
@@ -0,0 +1,18 @@
package com.baiyue.lastleg.dispatch.service;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
class DispatchBolPrintServiceTest {
@Test
void usesDenseFirstAndContinuationPageCapacities() {
assertThat(DispatchBolPrintService.pageCount(1)).isEqualTo(1);
assertThat(DispatchBolPrintService.pageCount(18)).isEqualTo(1);
assertThat(DispatchBolPrintService.pageCount(19)).isEqualTo(2);
assertThat(DispatchBolPrintService.pageCount(44)).isEqualTo(2);
assertThat(DispatchBolPrintService.pageCount(45)).isEqualTo(3);
assertThat(DispatchBolPrintService.pageCount(70)).isEqualTo(3);
assertThat(DispatchBolPrintService.pageCount(71)).isEqualTo(4);
}
}
+1 -1
View File
@@ -9,7 +9,7 @@
- 服务发现名:`print-service`。
- 内部地址:`POST /internal/print/v1/pdfs/inbound-marks`。
- A4 海运单据内部地址:`POST /internal/print/v1/pdfs/freight-a4-documents`。
- 海外仓排车 BOL 内部地址:`POST /internal/print/v1/pdfs/dispatch-bol-documents`;模板固定为 `DISPATCH_BILL_OF_LADING_A4_V1`,每个 document 对应一张 A4、包含 1-12 行明细。
- 海外仓排车 BOL 内部地址:`POST /internal/print/v1/pdfs/dispatch-bol-documents`;模板固定为 `DISPATCH_BILL_OF_LADING_A4_V2`,每个 document 对应一张 A4、包含 1-26 行明细。调用方首张按 18 行、续页按 26 行稳定分页;同页连续相同柜号和托盘号由 Print 纵向合并显示。
- 本地直连:`http://127.0.0.1:9207`。
- 请求必须携带 `X-Internal-Service-Token`,值使用各服务已有的 `baiyue.security.internal-service-token` 配置。
- 不把该接口加入 Gateway,也不允许 Admin 浏览器直接携带内部令牌访问 Print。
@@ -5,7 +5,7 @@ import java.util.List;
/** A4 vehicle-dispatch bill of lading request contract. */
public final class DispatchBolPdfDtos {
public static final String TEMPLATE = "DISPATCH_BILL_OF_LADING_A4_V1";
public static final String TEMPLATE = "DISPATCH_BILL_OF_LADING_A4_V2";
private DispatchBolPdfDtos() {}
@@ -3,6 +3,8 @@ package com.baiyue.print.renderer;
import com.baiyue.print.dto.DispatchBolPdfDtos;
import java.awt.Color;
import java.io.IOException;
import java.util.List;
import java.util.function.Function;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
@@ -12,62 +14,125 @@ import org.springframework.stereotype.Component;
@Component
public class DispatchBolRenderer {
private static final float[] COLS = {8, 31, 58, 84, 119, 160, 195, 202};
private static final String[] HEADERS = {"Container #", "Pallet #", "FBA Warehouse #", "Warehouse No #", "FBA Shipment ID", "PO / Reference #", "CTNS"};
private static final float[] COLS = {8, 23, 39, 62, 89, 123, 160, 190, 202};
private static final String[][] HEADERS = {
{""}, {"Container", "#"}, {"Pallet", "#"}, {"FBA", "Warehouse #"},
{"Warehouse No #"}, {"FBA Shipment ID"}, {"PO / Reference #"}, {"CTNS"}
};
private static final double FIRST_TABLE_TOP = 82;
private static final double CONTINUATION_TABLE_TOP = 10;
private static final double HEADER_HEIGHT = 10;
private static final double ROW_HEIGHT = 7.2;
public void renderPage(PDDocument document, PDFont font, DispatchBolPdfDtos.Document data) throws IOException {
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
PdfUnits units = new PdfUnits(297);
try (PDPageContentStream stream = new PDPageContentStream(document, page)) {
stream.setLineWidth(PdfUnits.mmToPt(.35));
centered(stream, units, font, "BILL OF LADING", 17, 105, 13);
block(stream, units, font, data, 18, 202, 18, 82);
table(stream, units, font, data, 92);
line(stream, units, 12, 244, 198, 244);
text(stream, units, font, "总箱数 / Total CTNS:", 8, 12, 241);
right(stream, units, font, Integer.toString(data.totalPackages()), 12, 198, 241);
if (data.pageCount() > 1) right(stream, units, font, data.pageNumber() + " / " + data.pageCount(), 7, 198, 251);
signature(stream, units, font, 24, "Driver signature");
signature(stream, units, font, 93, "Warehouse signature");
signature(stream, units, font, 160, "Date");
stream.setLineWidth(PdfUnits.mmToPt(.3));
double tableTop;
if (data.pageNumber() == 1) {
title(stream, units, font);
block(stream, units, font, data, 8, 202, 21, FIRST_TABLE_TOP);
tableTop = FIRST_TABLE_TOP;
} else {
tableTop = CONTINUATION_TABLE_TOP;
}
double tableBottom = table(stream, units, font, data, tableTop);
if (data.pageNumber() == data.pageCount()) {
summaryAndSignatures(stream, units, font, data, tableBottom);
}
if (data.pageCount() > 1) {
right(stream, units, font, data.pageNumber() + " / " + data.pageCount(), 6.5f, 202, 290);
}
}
}
private void title(PDPageContentStream s, PdfUnits u, PDFont f) throws IOException {
fill(s, u, Color.BLACK, 8, 8, 202, 21);
s.setNonStrokingColor(Color.WHITE);
centered(s, u, f, "BILL OF LADING", 15, 105, 17);
s.setNonStrokingColor(Color.BLACK);
}
private void block(PDPageContentStream s, PdfUnits u, PDFont f, DispatchBolPdfDtos.Document d,
double left, double right, double top, double bottom) throws IOException {
line(s,u,left,top,right,top); line(s,u,left,bottom,right,bottom); line(s,u,left,top,left,bottom); line(s,u,right,top,right,bottom);
line(s,u,105,top,105,bottom); line(s,u,left,51,right,51);
blackHeader(s,u,f,"SHIP FROM",left,top,105,24); blackHeader(s,u,f,"CARRIER",105,top,right,24);
blackHeader(s,u,f,"SHIP TO",left,51,105,57); blackHeader(s,u,f,"REFERENCE",105,51,right,57);
text(s,u,f,d.shipFromName(),10,left+2,30); text(s,u,f,"Address: " + d.shipFromAddress(),8,left+2,37);
text(s,u,f,"Carrier Name: " + d.carrierName(),8,107,29); text(s,u,f,"ISA #: " + d.isaNumber(),8,107,34);
text(s,u,f,"Appointment time: " + d.appointmentTime(),8,107,39); text(s,u,f,"Trailer #: " + d.trailerNumber(),8,107,44);
text(s,u,f,"Seal #: " + d.sealNumber(),8,107,49);
text(s,u,f,"Amazon FBA Warehouse",8,left+2,63); text(s,u,f,"FC Code: " + d.fbaWarehouseCode(),8,left+2,69);
text(s,u,f,"Name: " + d.shipToName(),8,left+2,75); text(s,u,f,"Address: " + d.shipToAddress(),8,left+2,80);
text(s,u,f,"BOL / Dispatch #: " + d.dispatchNo(),8,107,63); text(s,u,f,"FBA Shipment ID: " + d.fbaShipmentIds(),8,107,70);
text(s,u,f,"PO / Reference #: " + d.poReferenceNos(),8,107,77);
line(s,u,105,top,105,bottom); line(s,u,left,50,right,50);
blackHeader(s,u,f,"SHIP FROM",left,top,105,27); blackHeader(s,u,f,"CARRIER",105,top,right,27);
blackHeader(s,u,f,"SHIP TO",left,50,105,56); blackHeader(s,u,f,"REFERENCE",105,50,right,56);
text(s,u,f,d.shipFromName(),9.5f,left+2,33); text(s,u,f,"Address: " + d.shipFromAddress(),7.5f,left+2,40);
text(s,u,f,"Carrier Name: " + d.carrierName(),7.5f,107,32); text(s,u,f,"ISA #: " + d.isaNumber(),7.5f,107,36.5);
text(s,u,f,"Appointment time: " + d.appointmentTime(),7.5f,107,41); text(s,u,f,"Trailer #: " + d.trailerNumber(),7.5f,107,45.5);
text(s,u,f,"Seal #: " + d.sealNumber(),7.5f,107,49);
text(s,u,f,"Amazon FBA Warehouse",7.5f,left+2,61); text(s,u,f,"FC Code: " + d.fbaWarehouseCode(),7.5f,left+2,66);
text(s,u,f,"Name: " + d.shipToName(),7.5f,left+2,71); text(s,u,f,"Address: " + d.shipToAddress(),7.5f,left+2,76);
text(s,u,f,"BOL / Dispatch #: " + d.dispatchNo(),7.5f,107,61); text(s,u,f,"FBA Shipment ID: " + d.fbaShipmentIds(),7.5f,107,68);
text(s,u,f,"PO / Reference #: " + d.poReferenceNos(),7.5f,107,75);
}
private void table(PDPageContentStream s, PdfUnits u, PDFont f, DispatchBolPdfDtos.Document d, double top) throws IOException {
double headerBottom=101, rowHeight=9, bottom=headerBottom+12*rowHeight;
for(float x:COLS) line(s,u,x,top,x,bottom);
line(s,u,COLS[0],top,COLS[COLS.length-1],top); line(s,u,COLS[0],headerBottom,COLS[COLS.length-1],headerBottom);
for(int i=1;i<=12;i++) line(s,u,COLS[0],headerBottom+i*rowHeight,COLS[COLS.length-1],headerBottom+i*rowHeight);
for(int i=0;i<HEADERS.length;i++) centeredCell(s,u,f,HEADERS[i],5.3f,COLS[i],COLS[i+1],97.5);
for(int r=0;r<d.cargoRows().size();r++){
DispatchBolPdfDtos.CargoRow row=d.cargoRows().get(r); String[] values={row.containerNo(),row.palletNo(),row.fbaWarehouseCode(),row.warehouseNo(),row.fbaShipmentId(),row.poReferenceNo(),Integer.toString(row.packageCount())};
for(int c=0;c<values.length;c++) centeredCell(s,u,f,values[c],5.5f,COLS[c]+.7,COLS[c+1]-.7,107+r*rowHeight);
private double table(PDPageContentStream s, PdfUnits u, PDFont f, DispatchBolPdfDtos.Document d, double top) throws IOException {
List<DispatchBolPdfDtos.CargoRow> rows = d.cargoRows();
double headerBottom = top + HEADER_HEIGHT;
double bottom = headerBottom + rows.size() * ROW_HEIGHT;
for (float x : COLS) line(s,u,x,top,x,bottom);
line(s,u,COLS[0],top,COLS[COLS.length-1],top);
line(s,u,COLS[0],headerBottom,COLS[COLS.length-1],headerBottom);
for (int i = 0; i < HEADERS.length; i++) centeredLines(s,u,f,HEADERS[i],5.2f,COLS[i],COLS[i+1],top+4.2);
for (int boundary = 1; boundary <= rows.size(); boundary++) {
double y = headerBottom + boundary * ROW_HEIGHT;
line(s,u,COLS[0],y,COLS[1],y);
boolean last = boundary == rows.size();
if (last || !same(rows.get(boundary - 1).containerNo(), rows.get(boundary).containerNo())) line(s,u,COLS[1],y,COLS[2],y);
if (last || !same(rows.get(boundary - 1).palletNo(), rows.get(boundary).palletNo())) line(s,u,COLS[2],y,COLS[3],y);
line(s,u,COLS[3],y,COLS[COLS.length-1],y);
}
drawMergedColumn(s,u,f,rows,DispatchBolPdfDtos.CargoRow::containerNo,1,headerBottom);
drawMergedColumn(s,u,f,rows,DispatchBolPdfDtos.CargoRow::palletNo,2,headerBottom);
for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
DispatchBolPdfDtos.CargoRow row = rows.get(rowIndex);
String[] values = {row.fbaWarehouseCode(), row.warehouseNo(), row.fbaShipmentId(), row.poReferenceNo(), Integer.toString(row.packageCount())};
double baseline = headerBottom + rowIndex * ROW_HEIGHT + 4.7;
for (int column = 0; column < values.length; column++) {
centeredCell(s,u,f,values[column],5.25f,COLS[column+3]+.5,COLS[column+4]-.5,baseline);
}
}
return bottom;
}
private void drawMergedColumn(PDPageContentStream s, PdfUnits u, PDFont f, List<DispatchBolPdfDtos.CargoRow> rows,
Function<DispatchBolPdfDtos.CargoRow, String> value, int column, double headerBottom) throws IOException {
int start = 0;
while (start < rows.size()) {
int end = start + 1;
while (end < rows.size() && same(value.apply(rows.get(start)), value.apply(rows.get(end)))) end++;
double baseline = headerBottom + ((start + end) * ROW_HEIGHT / 2) + 1.8;
centeredCell(s,u,f,value.apply(rows.get(start)),5.25f,COLS[column]+.5,COLS[column+1]-.5,baseline);
start = end;
}
}
private void blackHeader(PDPageContentStream s,PdfUnits u,PDFont f,String label,double x1,double y1,double x2,double y2)throws IOException{
s.setNonStrokingColor(Color.BLACK); s.addRect(PdfUnits.mmToPt(x1),u.topDownMmToPdfY(y2),PdfUnits.mmToPt(x2-x1),PdfUnits.mmToPt(y2-y1)); s.fill();
s.setNonStrokingColor(Color.WHITE); text(s,u,f,label,6.5f,x1+2,y2-1.7); s.setNonStrokingColor(Color.BLACK);
private void summaryAndSignatures(PDPageContentStream s, PdfUnits u, PDFont f, DispatchBolPdfDtos.Document d, double tableBottom) throws IOException {
double summaryY = Math.max(tableBottom + 14, 236);
line(s,u,12,summaryY+4,198,summaryY+4);
text(s,u,f,"总箱数 / Total CTNS:",8,12,summaryY);
right(s,u,f,Integer.toString(d.totalPackages()),12,198,summaryY);
double signatureY = Math.max(summaryY + 30, 266);
signature(s,u,f,24,signatureY,"Driver signature");
signature(s,u,f,93,signatureY,"Warehouse signature");
signature(s,u,f,160,signatureY,"Date");
}
private void signature(PDPageContentStream s,PdfUnits u,PDFont f,double x,String label)throws IOException{line(s,u,x,266,x+52,266); centeredCell(s,u,f,label,6.5f,x,x+52,274);}
private boolean same(String left, String right) { return left != null && left.equals(right); }
private void blackHeader(PDPageContentStream s,PdfUnits u,PDFont f,String label,double x1,double y1,double x2,double y2)throws IOException{
fill(s,u,Color.BLACK,x1,y1,x2,y2); s.setNonStrokingColor(Color.WHITE); text(s,u,f,label,6.2f,x1+2,y2-1.7); s.setNonStrokingColor(Color.BLACK);
}
private void fill(PDPageContentStream s,PdfUnits u,Color color,double x1,double y1,double x2,double y2)throws IOException{s.setNonStrokingColor(color);s.addRect(PdfUnits.mmToPt(x1),u.topDownMmToPdfY(y2),PdfUnits.mmToPt(x2-x1),PdfUnits.mmToPt(y2-y1));s.fill();}
private void signature(PDPageContentStream s,PdfUnits u,PDFont f,double x,double y,String label)throws IOException{line(s,u,x,y,x+52,y);centeredCell(s,u,f,label,6.2f,x,x+52,y+8);}
private void centered(PDPageContentStream s,PdfUnits u,PDFont f,String value,float size,double centerX,double y)throws IOException{float w=f.getStringWidth(value)/1000*size;text(s,u,f,value,size,centerX-w/PdfUnits.mmToPt(1)/2,y);}
private void centeredLines(PDPageContentStream s,PdfUnits u,PDFont f,String[] values,float size,double x1,double x2,double y)throws IOException{for(int i=0;i<values.length;i++)centeredCell(s,u,f,values[i],size,x1+.3,x2-.3,y+i*3.5);}
private void centeredCell(PDPageContentStream s,PdfUnits u,PDFont f,String value,float size,double x1,double x2,double y)throws IOException{String fit=fit(f,value,size,PdfUnits.mmToPt(x2-x1));float w=f.getStringWidth(fit)/1000*size;text(s,u,f,fit,size,x1+(x2-x1-w/PdfUnits.mmToPt(1))/2,y);}
private void right(PDPageContentStream s,PdfUnits u,PDFont f,String value,float size,double x,double y)throws IOException{float w=f.getStringWidth(value)/1000*size;text(s,u,f,value,size,x-w/PdfUnits.mmToPt(1),y);}
private String fit(PDFont f,String v,float size,float max)throws IOException{String value=v==null?"—":v;if(f.getStringWidth(value)/1000*size<=max)return value;String ell="…";int end=value.length();while(end>0&&f.getStringWidth(value.substring(0,end)+ell)/1000*size>max)end--;return end==0?ell:value.substring(0,end)+ell;}
@@ -24,7 +24,8 @@ public class DispatchBolPdfService {
private static final BigDecimal A4_WIDTH = new BigDecimal("210");
private static final BigDecimal A4_HEIGHT = new BigDecimal("297");
private static final Set<Character> FORBIDDEN_FILE_CHARS = Set.of('/', '\\', ':', '*', '?', '"', '<', '>', '|');
private static final int MAX_ROWS_PER_PAGE = 12;
private static final int MAX_FIRST_PAGE_ROWS = 18;
private static final int MAX_CONTINUATION_PAGE_ROWS = 26;
@Resource private PrintPdfProperties properties;
@Resource private PdfGenerationLimiter limiter;
@@ -64,7 +65,10 @@ public class DispatchBolPdfService {
}
private DispatchBolPdfDtos.Document normalizeDocument(DispatchBolPdfDtos.Document value, int index) {
if (value == null || value.cargoRows() == null || value.cargoRows().isEmpty() || value.cargoRows().size() > MAX_ROWS_PER_PAGE) invalid("documents[" + index + "].cargoRows must contain 1 to 12 items");
int maxRows = value != null && value.pageNumber() == 1 ? MAX_FIRST_PAGE_ROWS : MAX_CONTINUATION_PAGE_ROWS;
if (value == null || value.cargoRows() == null || value.cargoRows().isEmpty() || value.cargoRows().size() > maxRows) {
invalid("documents[" + index + "].cargoRows exceeds the page layout capacity");
}
if (value.totalPackages() < 0 || value.pageNumber() < 1 || value.pageCount() < value.pageNumber()) invalid("documents[" + index + "] page metadata is invalid");
List<DispatchBolPdfDtos.CargoRow> rows = new ArrayList<>();
for (DispatchBolPdfDtos.CargoRow row : value.cargoRows()) {
@@ -208,7 +208,7 @@ components:
type: object
required: [templateCode, pageSpec, downloadFileName, documents]
properties:
templateCode: { type: string, enum: [DISPATCH_BILL_OF_LADING_A4_V1] }
templateCode: { type: string, enum: [DISPATCH_BILL_OF_LADING_A4_V2] }
pageSpec: { $ref: '#/components/schemas/FreightA4PageSpec' }
downloadFileName: { type: string, maxLength: 120 }
documents:
@@ -237,7 +237,12 @@ components:
totalPackages: { type: integer, minimum: 0 }
pageNumber: { type: integer, minimum: 1 }
pageCount: { type: integer, minimum: 1 }
cargoRows: { type: array, minItems: 1, maxItems: 12, items: { $ref: '#/components/schemas/DispatchBolCargoRow' } }
cargoRows:
type: array
minItems: 1
maxItems: 26
description: First page accepts at most 18 rows; continuation pages accept at most 26 rows.
items: { $ref: '#/components/schemas/DispatchBolCargoRow' }
DispatchBolCargoRow:
type: object
required: [containerNo, palletNo, fbaWarehouseCode, warehouseNo, fbaShipmentId, poReferenceNo, packageCount]
@@ -8,6 +8,7 @@ import com.baiyue.print.renderer.DispatchBolRenderer;
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;
@@ -40,4 +41,45 @@ class DispatchBolPdfServiceGenerationTest {
assertThat(new PDFTextStripper().getText(pdf)).contains("BILL OF LADING", "洛杉矶仓", "DS-WH-20260911-692865", "TSTU2186171", "Total CTNS:", "4");
}
}
@Test
void rendersDenseContinuationPageAndPrintsTotalOnlyOnLastPage() throws Exception {
List<DispatchBolPdfDtos.CargoRow> firstRows = rows(1, 18);
List<DispatchBolPdfDtos.CargoRow> continuationRows = rows(19, 44);
DispatchBolPdfDtos.Document first = page(1, 2, firstRows);
DispatchBolPdfDtos.Document second = page(2, 2, continuationRows);
var generated = service.generate(new DispatchBolPdfDtos.Request(DispatchBolPdfDtos.TEMPLATE,
new DispatchBolPdfDtos.PageSpec(new BigDecimal("210"), new BigDecimal("297")),
"dense-bol.pdf", List.of(first, second)));
try (PDDocument pdf = Loader.loadPDF(generated.content())) {
assertThat(pdf.getNumberOfPages()).isEqualTo(2);
PDFTextStripper stripper = new PDFTextStripper();
stripper.setStartPage(1);
stripper.setEndPage(1);
String firstText = stripper.getText(pdf);
stripper.setStartPage(2);
stripper.setEndPage(2);
String secondText = stripper.getText(pdf);
assertThat(firstText).contains("BILL OF LADING", "PALLET-01", "1 / 2").doesNotContain("Total CTNS:");
assertThat(secondText).contains("PALLET-44", "Total CTNS:", "44", "2 / 2").doesNotContain("SHIP FROM");
}
}
private DispatchBolPdfDtos.Document page(int pageNumber, int pageCount, List<DispatchBolPdfDtos.CargoRow> rows) {
return new DispatchBolPdfDtos.Document("NOBLE LOGISTICS INC", "10767 7th Street", "ECHO", "14068390973 F",
"09/14/2026 08:00 EDT", "—", "—", "IND9", "IND9", "—", "DS-WH-20260911-692865",
"FBA19TEST", "PO-TEST", 44, pageNumber, pageCount, rows);
}
private List<DispatchBolPdfDtos.CargoRow> rows(int start, int end) {
List<DispatchBolPdfDtos.CargoRow> rows = new ArrayList<>();
for (int index = start; index <= end; index++) {
String pallet = index <= 4 ? "PALLET-01" : "PALLET-" + String.format("%02d", index);
rows.add(new DispatchBolPdfDtos.CargoRow("D2250", pallet, "CLT2转IND9", "HDHK2608" + String.format("%06d", index),
"FBA19TEST" + index + "-" + index + "件", "PO" + index, 1));
}
return rows;
}
}