feat: generate dispatch BOL PDFs
This commit is contained in:
+3
-3
@@ -15,9 +15,9 @@
|
||||
| 健康状态 API | `controller/PrintHealthController.java`(`/api/platform/v1/print/status`) |
|
||||
| 内部 PDF API | `controller/InternalPdfController.java`(`/internal/print/v1/pdfs/inbound-marks`、`/internal/print/v1/pdfs/freight-a4-documents`) |
|
||||
| 内部令牌校验 | `controller/InternalServiceTokenGuard.java` |
|
||||
| 请求与响应模型 | `dto/InboundMarkPdfDtos.java`、`dto/FreightA4PdfDtos.java` |
|
||||
| 固定模板与渲染 | `renderer/InboundMarkV1Renderer.java`、`renderer/FreightA4Renderer.java`、`renderer/PdfQrRenderer.java`、`renderer/PdfUnits.java` |
|
||||
| 请求校验与资源保护 | `controller/PrintRequestSizeFilter.java`、`service/InboundMarkPdfService.java`、`service/FreightA4PdfService.java`、`service/PdfGenerationLimiter.java` |
|
||||
| 请求与响应模型 | `dto/InboundMarkPdfDtos.java`、`dto/FreightA4PdfDtos.java`、`dto/DispatchBolPdfDtos.java` |
|
||||
| 固定模板与渲染 | `renderer/InboundMarkV1Renderer.java`、`renderer/FreightA4Renderer.java`、`renderer/DispatchBolRenderer.java`、`renderer/PdfQrRenderer.java`、`renderer/PdfUnits.java` |
|
||||
| 请求校验与资源保护 | `controller/PrintRequestSizeFilter.java`、`service/InboundMarkPdfService.java`、`service/FreightA4PdfService.java`、`service/DispatchBolPdfService.java`、`service/PdfGenerationLimiter.java` |
|
||||
| PDF 参数配置 | `config/PrintPdfProperties.java` |
|
||||
| 接口契约 | `src/main/resources/openapi/print-internal-v1.yaml` |
|
||||
| 字体及授权 | `src/main/resources/fonts/NotoSansSC-Bold.ttf`、`src/main/resources/fonts/OFL.txt` |
|
||||
|
||||
@@ -9,6 +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 行明细。
|
||||
- 本地直连:`http://127.0.0.1:9207`。
|
||||
- 请求必须携带 `X-Internal-Service-Token`,值使用各服务已有的 `baiyue.security.internal-service-token` 配置。
|
||||
- 不把该接口加入 Gateway,也不允许 Admin 浏览器直接携带内部令牌访问 Print。
|
||||
|
||||
@@ -4,8 +4,10 @@ import jakarta.annotation.Resource;
|
||||
|
||||
import com.baiyue.common.web.CorrelationContext;
|
||||
import com.baiyue.print.dto.FreightA4PdfDtos;
|
||||
import com.baiyue.print.dto.DispatchBolPdfDtos;
|
||||
import com.baiyue.print.dto.InboundMarkPdfDtos;
|
||||
import com.baiyue.print.service.FreightA4PdfService;
|
||||
import com.baiyue.print.service.DispatchBolPdfService;
|
||||
import com.baiyue.print.service.InboundMarkPdfService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -30,6 +32,7 @@ public class InternalPdfController {
|
||||
|
||||
@Resource private InboundMarkPdfService pdfService;
|
||||
@Resource private FreightA4PdfService freightA4PdfService;
|
||||
@Resource private DispatchBolPdfService dispatchBolPdfService;
|
||||
@Resource private InternalServiceTokenGuard tokenGuard;
|
||||
|
||||
@PostMapping(value = "/inbound-marks", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
@@ -85,4 +88,17 @@ public class InternalPdfController {
|
||||
|
||||
return response.body(content);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/dispatch-bol-documents", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<byte[]> generateDispatchBolDocuments(
|
||||
@RequestHeader(value = INTERNAL_TOKEN_HEADER, required = false) String internalToken,
|
||||
@Valid @RequestBody DispatchBolPdfDtos.Request request) {
|
||||
tokenGuard.verify(internalToken);
|
||||
DispatchBolPdfService.GeneratedPdf pdf = dispatchBolPdfService.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.print.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
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";
|
||||
|
||||
private DispatchBolPdfDtos() {}
|
||||
|
||||
public record Request(String templateCode, PageSpec pageSpec, String downloadFileName,
|
||||
List<Document> documents) {}
|
||||
public record PageSpec(BigDecimal widthMm, BigDecimal heightMm) {}
|
||||
public record Document(String shipFromName, String shipFromAddress, String carrierName,
|
||||
String isaNumber, String appointmentTime, String trailerNumber,
|
||||
String sealNumber, String fbaWarehouseCode, String shipToName,
|
||||
String shipToAddress, String dispatchNo, String fbaShipmentIds,
|
||||
String poReferenceNos, int totalPackages, int pageNumber, int pageCount,
|
||||
List<CargoRow> cargoRows) {}
|
||||
public record CargoRow(String containerNo, String palletNo, String fbaWarehouseCode,
|
||||
String warehouseNo, String fbaShipmentId, String poReferenceNo,
|
||||
int packageCount) {}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.baiyue.print.renderer;
|
||||
|
||||
import com.baiyue.print.dto.DispatchBolPdfDtos;
|
||||
import java.awt.Color;
|
||||
import java.io.IOException;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||
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"};
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 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 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 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 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;}
|
||||
private void text(PDPageContentStream s,PdfUnits u,PDFont f,String value,float size,double x,double y)throws IOException{s.beginText();s.setFont(f,size);s.newLineAtOffset(PdfUnits.mmToPt(x),u.topDownMmToPdfY(y));s.showText(value==null?"—":value);s.endText();}
|
||||
private void line(PDPageContentStream s,PdfUnits u,double x1,double y1,double x2,double y2)throws IOException{s.moveTo(PdfUnits.mmToPt(x1),u.topDownMmToPdfY(y1));s.lineTo(PdfUnits.mmToPt(x2),u.topDownMmToPdfY(y2));s.stroke();}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.baiyue.print.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.print.config.PrintPdfProperties;
|
||||
import com.baiyue.print.dto.DispatchBolPdfDtos;
|
||||
import com.baiyue.print.renderer.DispatchBolRenderer;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.text.Normalizer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
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;
|
||||
|
||||
@Resource private PrintPdfProperties properties;
|
||||
@Resource private PdfGenerationLimiter limiter;
|
||||
@Resource private DispatchBolRenderer renderer;
|
||||
|
||||
public GeneratedPdf generate(DispatchBolPdfDtos.Request request) {
|
||||
DispatchBolPdfDtos.Request normalized = normalize(request);
|
||||
try (PdfGenerationLimiter.Permit ignored = limiter.acquire();
|
||||
PDDocument pdf = new PDDocument(); InputStream fontStream = openFont();
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||
PDType0Font font = PDType0Font.load(pdf, fontStream, true);
|
||||
for (DispatchBolPdfDtos.Document document : normalized.documents()) renderer.renderPage(pdf, font, document);
|
||||
pdf.save(output);
|
||||
byte[] content = output.toByteArray();
|
||||
if (content.length > properties.getMaxOutputBytes()) invalid("生成的 PDF 超过输出大小上限");
|
||||
return new GeneratedPdf(content, normalized.downloadFileName(), normalized.documents().size());
|
||||
} catch (IOException exception) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "PDF 生成失败");
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream openFont() throws IOException {
|
||||
return new ClassPathResource(properties.getFontResource()).getInputStream();
|
||||
}
|
||||
|
||||
private DispatchBolPdfDtos.Request normalize(DispatchBolPdfDtos.Request request) {
|
||||
if (request == null || !DispatchBolPdfDtos.TEMPLATE.equals(line(request.templateCode()))) invalid("templateCode is not supported");
|
||||
if (request.pageSpec() == null || request.pageSpec().widthMm() == null || request.pageSpec().heightMm() == null
|
||||
|| request.pageSpec().widthMm().compareTo(A4_WIDTH) != 0 || request.pageSpec().heightMm().compareTo(A4_HEIGHT) != 0) invalid("pageSpec must be A4");
|
||||
String fileName = line(request.downloadFileName());
|
||||
if (fileName.isBlank() || !fileName.toLowerCase(java.util.Locale.ROOT).endsWith(".pdf") || fileName.length() > 120
|
||||
|| fileName.chars().anyMatch(c -> FORBIDDEN_FILE_CHARS.contains((char) c))) invalid("downloadFileName is invalid");
|
||||
if (request.documents() == null || request.documents().isEmpty() || request.documents().size() > properties.getMaxPages()) invalid("documents count is invalid");
|
||||
List<DispatchBolPdfDtos.Document> documents = new ArrayList<>();
|
||||
for (int i = 0; i < request.documents().size(); i++) documents.add(normalizeDocument(request.documents().get(i), i));
|
||||
return new DispatchBolPdfDtos.Request(DispatchBolPdfDtos.TEMPLATE, request.pageSpec(), fileName, List.copyOf(documents));
|
||||
}
|
||||
|
||||
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");
|
||||
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()) {
|
||||
if (row == null || row.packageCount() < 0) invalid("cargo row is invalid");
|
||||
rows.add(new DispatchBolPdfDtos.CargoRow(text(row.containerNo()), text(row.palletNo()), text(row.fbaWarehouseCode()),
|
||||
text(row.warehouseNo()), text(row.fbaShipmentId()), text(row.poReferenceNo()), row.packageCount()));
|
||||
}
|
||||
return new DispatchBolPdfDtos.Document(text(value.shipFromName()), text(value.shipFromAddress()), text(value.carrierName()),
|
||||
text(value.isaNumber()), text(value.appointmentTime()), text(value.trailerNumber()), text(value.sealNumber()),
|
||||
text(value.fbaWarehouseCode()), text(value.shipToName()), text(value.shipToAddress()), text(value.dispatchNo()),
|
||||
text(value.fbaShipmentIds()), text(value.poReferenceNos()), value.totalPackages(), value.pageNumber(), value.pageCount(), List.copyOf(rows));
|
||||
}
|
||||
|
||||
private String text(String value) {
|
||||
String normalized = line(value);
|
||||
if (normalized.codePointCount(0, normalized.length()) > 300 || normalized.codePoints().anyMatch(c -> Character.isISOControl(c))) invalid("document text is invalid");
|
||||
return normalized.isBlank() ? "—" : normalized;
|
||||
}
|
||||
private String line(String value) { return value == null ? "" : Normalizer.normalize(value, Normalizer.Form.NFC).strip(); }
|
||||
private void invalid(String message) { throw new BizException(ResultCode.VALIDATION_FAILED, message); }
|
||||
|
||||
public record GeneratedPdf(byte[] content, String fileName, int pageCount) {}
|
||||
}
|
||||
@@ -100,6 +100,24 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiError'
|
||||
/internal/print/v1/pdfs/dispatch-bol-documents:
|
||||
post:
|
||||
summary: Generate vehicle-dispatch BOL documents as fixed A4 PDF pages
|
||||
operationId: generateDispatchBolPdf
|
||||
security: [{ internalServiceToken: [] }]
|
||||
parameters: [{ $ref: '#/components/parameters/RequestId' }]
|
||||
requestBody:
|
||||
required: true
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/DispatchBolPdfRequest' } } }
|
||||
responses:
|
||||
'200':
|
||||
description: PDF generation succeeded, or a business error was returned as ApiResponse JSON.
|
||||
headers:
|
||||
Content-Disposition: { schema: { type: string } }
|
||||
X-Print-Page-Count: { schema: { type: integer, minimum: 1, maximum: 2000 } }
|
||||
content:
|
||||
application/pdf: { schema: { type: string, format: binary } }
|
||||
application/json: { schema: { $ref: '#/components/schemas/ApiError' } }
|
||||
components:
|
||||
securitySchemes:
|
||||
internalServiceToken:
|
||||
@@ -186,6 +204,51 @@ components:
|
||||
description: One document item renders one A4 page in request order.
|
||||
items:
|
||||
$ref: '#/components/schemas/FreightA4Document'
|
||||
DispatchBolPdfRequest:
|
||||
type: object
|
||||
required: [templateCode, pageSpec, downloadFileName, documents]
|
||||
properties:
|
||||
templateCode: { type: string, enum: [DISPATCH_BILL_OF_LADING_A4_V1] }
|
||||
pageSpec: { $ref: '#/components/schemas/FreightA4PageSpec' }
|
||||
downloadFileName: { type: string, maxLength: 120 }
|
||||
documents:
|
||||
type: array
|
||||
minItems: 1
|
||||
maxItems: 2000
|
||||
description: One item renders one A4 page; the caller groups and chunks BOL rows in stable order.
|
||||
items: { $ref: '#/components/schemas/DispatchBolDocument' }
|
||||
DispatchBolDocument:
|
||||
type: object
|
||||
required: [shipFromName, carrierName, fbaWarehouseCode, shipToName, dispatchNo, totalPackages, pageNumber, pageCount, cargoRows]
|
||||
properties:
|
||||
shipFromName: { type: string }
|
||||
shipFromAddress: { type: string }
|
||||
carrierName: { type: string }
|
||||
isaNumber: { type: string }
|
||||
appointmentTime: { type: string }
|
||||
trailerNumber: { type: string }
|
||||
sealNumber: { type: string }
|
||||
fbaWarehouseCode: { type: string }
|
||||
shipToName: { type: string }
|
||||
shipToAddress: { type: string }
|
||||
dispatchNo: { type: string }
|
||||
fbaShipmentIds: { type: string }
|
||||
poReferenceNos: { type: string }
|
||||
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' } }
|
||||
DispatchBolCargoRow:
|
||||
type: object
|
||||
required: [containerNo, palletNo, fbaWarehouseCode, warehouseNo, fbaShipmentId, poReferenceNo, packageCount]
|
||||
properties:
|
||||
containerNo: { type: string }
|
||||
palletNo: { type: string }
|
||||
fbaWarehouseCode: { type: string }
|
||||
warehouseNo: { type: string }
|
||||
fbaShipmentId: { type: string }
|
||||
poReferenceNo: { type: string }
|
||||
packageCount: { type: integer, minimum: 0 }
|
||||
FreightA4PageSpec:
|
||||
type: object
|
||||
required: [widthMm, heightMm]
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.baiyue.common.web.GlobalExceptionHandler;
|
||||
import com.baiyue.print.dto.FreightA4PdfDtos;
|
||||
import com.baiyue.print.dto.InboundMarkPdfDtos;
|
||||
import com.baiyue.print.service.FreightA4PdfService;
|
||||
import com.baiyue.print.service.DispatchBolPdfService;
|
||||
import com.baiyue.print.service.InboundMarkPdfService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -42,6 +43,8 @@ class InternalPdfControllerTest {
|
||||
private InboundMarkPdfService service;
|
||||
@MockBean
|
||||
private FreightA4PdfService freightA4PdfService;
|
||||
@MockBean
|
||||
private DispatchBolPdfService dispatchBolPdfService;
|
||||
@Resource
|
||||
private InternalPdfController controller;
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.baiyue.print.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.baiyue.print.config.PrintPdfProperties;
|
||||
import com.baiyue.print.dto.DispatchBolPdfDtos;
|
||||
import com.baiyue.print.renderer.DispatchBolRenderer;
|
||||
import com.baiyue.print.renderer.PdfUnits;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
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 = {DispatchBolPdfService.class, PrintPdfProperties.class,
|
||||
PdfGenerationLimiter.class, DispatchBolRenderer.class})
|
||||
class DispatchBolPdfServiceGenerationTest {
|
||||
@Resource private DispatchBolPdfService service;
|
||||
|
||||
@Test
|
||||
void generatesStableA4BolWithEmbeddedChineseFontAndBlackHeaders() throws Exception {
|
||||
DispatchBolPdfDtos.Document page = new DispatchBolPdfDtos.Document(
|
||||
"洛杉矶仓", "—", "五月开发验收车队", "ISA-446646", "2026-09-11 18:39:37",
|
||||
"DEV-83222028", "—", "ABE4", "ABE4", "—", "DS-WH-20260911-692865",
|
||||
"FBA-SHIP-1", "PO-1", 4, 1, 1,
|
||||
List.of(new DispatchBolPdfDtos.CargoRow("TSTU2186171", "TSTU2186171-126-001", "ABE4",
|
||||
"NYEXX2609110181", "FBA-SHIP-1", "PO-1", 4)));
|
||||
var generated = service.generate(new DispatchBolPdfDtos.Request(DispatchBolPdfDtos.TEMPLATE,
|
||||
new DispatchBolPdfDtos.PageSpec(new BigDecimal("210"), new BigDecimal("297")),
|
||||
"DS-WH-20260911-692865-BOL.pdf", List.of(page)));
|
||||
|
||||
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("BILL OF LADING", "洛杉矶仓", "DS-WH-20260911-692865", "TSTU2186171", "Total CTNS:", "4");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user