This commit is contained in:
2026-08-06 11:49:51 +08:00
parent a11e47fe3b
commit 7dd92f1c84
91 changed files with 6233 additions and 174 deletions
@@ -3,7 +3,9 @@ package com.baiyue.print.controller;
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.InboundMarkPdfDtos;
import com.baiyue.print.service.FreightA4PdfService;
import com.baiyue.print.service.InboundMarkPdfService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
@@ -38,6 +40,8 @@ class InternalPdfControllerTest {
private static final String TOKEN = "0123456789abcdef0123456789abcdef";
@MockBean
private InboundMarkPdfService service;
@MockBean
private FreightA4PdfService freightA4PdfService;
@Resource
private InternalPdfController controller;
private MockMvc mockMvc;
@@ -127,6 +131,28 @@ class InternalPdfControllerTest {
assertThat(captor.getValue().downloadFileName()).isEqualTo("inbound-marks-request-123.pdf");
}
@Test
void returnsFreightA4PdfWithDownloadHeaders() throws Exception {
byte[] pdf = "%PDF-a4-test".getBytes(StandardCharsets.US_ASCII);
when(freightA4PdfService.generate(any())).thenReturn(new FreightA4PdfService.GeneratedPdf(
pdf, "BYSE2607044-托运书.pdf", 1));
mockMvc.perform(post("/internal/print/v1/pdfs/freight-a4-documents")
.header(InternalPdfController.INTERNAL_TOKEN_HEADER, TOKEN)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_PDF, MediaType.APPLICATION_JSON)
.content(validFreightA4RequestJson()))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_PDF))
.andExpect(content().bytes(pdf))
.andExpect(header().string("Cache-Control", "no-store"))
.andExpect(header().string("X-Print-Page-Count", "1"))
.andExpect(header().string("Content-Disposition",
org.hamcrest.Matchers.containsString("filename*=")));
verify(freightA4PdfService).generate(any(FreightA4PdfDtos.Request.class));
}
@Test
void endpointDoesNotPinProducedMediaTypeToPdf() throws Exception {
PostMapping mapping = InternalPdfController.class
@@ -142,6 +168,7 @@ class InternalPdfControllerTest {
.getContentAsString(StandardCharsets.UTF_8);
assertThat(contract)
.contains("/internal/print/v1/pdfs/inbound-marks:")
.contains("/internal/print/v1/pdfs/freight-a4-documents:")
.contains("application/pdf:")
.contains("application/json:")
.contains("name: X-Internal-Service-Token")
@@ -165,4 +192,29 @@ class InternalPdfControllerTest {
""";
}
private String validFreightA4RequestJson() {
return """
{
"templateCode": "SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1",
"pageSpec": {"widthMm": 210, "heightMm": 297},
"downloadFileName": "BYSE2607044-托运书.pdf",
"documents": [{
"fields": {
"toCompany": "宁波胜泽远航国际物流有限公司",
"shipper": "ZHEJIANG BAIYUE TRADE CO.,LIMITED",
"jobNumber": "BYSE2607044"
},
"cargoRows": [{
"marksAndNos": "N/M",
"quantity": "700",
"packaging": "PACKAGES",
"descriptionOfGoods": "CAR ACCESSORIES",
"grossWeight": "28000.00 KGS",
"measurement": "28.000 CBM"
}]
}]
}
""";
}
}
@@ -39,6 +39,19 @@ class PrintRequestSizeFilterTest {
assertThat(response.getStatus()).isEqualTo(200);
}
@Test
void alsoCapsFreightA4DocumentRequests() throws Exception {
MockHttpServletRequest request = request("12345678901", true,
"/internal/print/v1/pdfs/freight-a4-documents");
MockHttpServletResponse response = new MockHttpServletResponse();
AtomicReference<Boolean> called = new AtomicReference<>(false);
filter.doFilter(request, response, (ignoredRequest, ignoredResponse) -> called.set(true));
assertThat(called).hasValue(false);
assertTooLarge(response);
}
@Test
void rejectsOversizedDeclaredContentLengthWithoutCallingDownstream() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest() {
@@ -68,17 +81,25 @@ class PrintRequestSizeFilterTest {
}
private MockHttpServletRequest request(String content, boolean unknownLength) {
return request(content, unknownLength, "/internal/print/v1/pdfs/inbound-marks");
}
private MockHttpServletRequest request(String content, boolean unknownLength, String path) {
MockHttpServletRequest request = unknownLength ? new MockHttpServletRequest() {
@Override public int getContentLength() { return -1; }
@Override public long getContentLengthLong() { return -1; }
} : new MockHttpServletRequest();
initialize(request, content);
initialize(request, content, path);
return request;
}
private void initialize(MockHttpServletRequest request, String content) {
initialize(request, content, "/internal/print/v1/pdfs/inbound-marks");
}
private void initialize(MockHttpServletRequest request, String content, String path) {
request.setMethod("POST");
request.setRequestURI("/internal/print/v1/pdfs/inbound-marks");
request.setRequestURI(path);
request.setContent(content.getBytes(StandardCharsets.UTF_8));
request.setAttribute("requestId", "request-123");
request.setAttribute("traceId", "trace-123");
@@ -0,0 +1,134 @@
package com.baiyue.print.service;
import static org.assertj.core.api.Assertions.assertThat;
import com.baiyue.print.config.PrintPdfProperties;
import com.baiyue.print.dto.FreightA4PdfDtos;
import com.baiyue.print.renderer.FreightA4Renderer;
import com.baiyue.print.renderer.PdfUnits;
import jakarta.annotation.Resource;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
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 = {FreightA4PdfService.class, PrintPdfProperties.class,
PdfGenerationLimiter.class, FreightA4Renderer.class})
class FreightA4PdfServiceGenerationTest {
@Resource
private FreightA4PdfService service;
@Test
void generatesShippingInstructionAsStrictA4Pdf() throws IOException {
FreightA4PdfService.GeneratedPdf generated = service.generate(request(
FreightA4PdfDtos.SHIPPING_INSTRUCTION_TEMPLATE,
"BYSE2607044-shipping-instruction.pdf",
shippingDocument()));
assertThat(generated.fileName()).isEqualTo("BYSE2607044-shipping-instruction.pdf");
assertThat(generated.pageCount()).isEqualTo(1);
assertThat(generated.content()).startsWith("%PDF-".getBytes(java.nio.charset.StandardCharsets.US_ASCII));
try (PDDocument document = Loader.loadPDF(generated.content())) {
assertA4(document);
String text = new PDFTextStripper().getText(document);
assertThat(text).contains("Sea Freight Shipping Instructions", "Job Nbr",
"BYSE2607044", "CAR ACCESSORIES", "SAY SIX HUNDRED AND FIFTY NINE PACKAGES ONLY.");
}
}
@Test
void generatesBillConfirmationAsStrictA4Pdf() throws IOException {
FreightA4PdfService.GeneratedPdf generated = service.generate(request(
FreightA4PdfDtos.BILL_CONFIRMATION_TEMPLATE,
"BYSE2607044-bill-confirmation.pdf",
billConfirmationDocument()));
assertThat(generated.pageCount()).isEqualTo(1);
try (PDDocument document = Loader.loadPDF(generated.content())) {
assertA4(document);
String text = new PDFTextStripper().getText(document);
assertThat(text).contains("WHY074G504054", "D R A F T", "SAID TO CONTAIN(S)",
"WHSU2884669/WHLX730243/20GP/1028PACKAGES/28000KGS/ 28CBM");
}
}
private FreightA4PdfDtos.Request request(String templateCode, String fileName,
FreightA4PdfDtos.Document document) {
return new FreightA4PdfDtos.Request(templateCode,
new FreightA4PdfDtos.PageSpec(new BigDecimal("210"), new BigDecimal("297.0")),
fileName, List.of(document));
}
private FreightA4PdfDtos.Document shippingDocument() {
return new FreightA4PdfDtos.Document(Map.ofEntries(
Map.entry("toCompany", "宁波胜泽远航国际物流有限公司"),
Map.entry("issuerNameCn", "浙江柏悦贸易有限公司(香港)"),
Map.entry("issuerNameEn", "ZHEJIANG BAIYUE TRADE CO.,LIMITED"),
Map.entry("issuerAddress", "地址:浙江省义乌市稠州北路 1399 号新光国际 8613 室"),
Map.entry("issuerPostalCode", "322000"),
Map.entry("issuerTel", "0579-85875557"),
Map.entry("issuerFax", "0579-85601277"),
Map.entry("shipper", "ZHEJIANG BAIYUE TRADE CO.,LIMITED\nROOM 8613,NO.1399,CHOUZHOU NORTH ROAD,FUTIAN STREET,YIWU CITY,ZHEJIANG PROVINCE\nTEL:0579-85695557"),
Map.entry("consignee", "SANATRK FOR IMPORT AND TRADE OF AUTO SPARE PARTS\n3 ATIA ELZEKRY ST- OMAR EBN ELKHTAB CITY SHOBRA\nELKHIMA QALYUBIA SECOND-OWNER/ MENA MATTA GABALLA\nMATTA\nTEL:01225672354"),
Map.entry("notifyParty", "SAME AS CONSIGNEE"),
Map.entry("picName", "苏天翔"),
Map.entry("jobNumber", "BYSE2607044"),
Map.entry("etdDate", "2026/07/24"),
Map.entry("serviceTerm", "CY-CY"),
Map.entry("freightTerm", "FREIGHT PREPAID"),
Map.entry("cargoQuantity", "1*20GP"),
Map.entry("portOfLoading", "NINGBO"),
Map.entry("portOfDischarge", "EL DEKHEILA"),
Map.entry("placeOfDelivery", "EL DEKHEILA"),
Map.entry("totalPackagesInWords", "SAY SIX HUNDRED AND FIFTY NINE PACKAGES ONLY."),
Map.entry("carrier", "万海航运(香港)股份有限公司"),
Map.entry("bookingAgent", "宁波胜泽远航国际物流有限公司"),
Map.entry("printDate", "2026 年 07 月 16 日")
), List.of(new FreightA4PdfDtos.CargoRow("N/M", "700", "PACKAGES",
"CAR ACCESSORIES\nACID: 4120661061019510011\n申请目的港 21 天免用箱",
"28000.00 KGS", "28.000 CBM")));
}
private FreightA4PdfDtos.Document billConfirmationDocument() {
return new FreightA4PdfDtos.Document(Map.ofEntries(
Map.entry("shipper", "ZHEJIANG BAIYUE TRADE CO.,LIMITED\nROOM 8613,NO.1399,CHOUZHOU NORTH ROAD,FUTIAN\nSTREET,YIWU CITY,ZHEJIANG PROVINCE\nTEL:0579-85695557"),
Map.entry("consignee", "SANATRK FOR IMPORT AND TRADE OF AUTO SPARE PARTS\n3 ATIA ELZEKRY ST- OMAR EBN ELKHTAB CITY SHOBRA\nELKHIMA QALYUBIA SECOND-OWNER/ MENA MATTA GABALLA\nMATTA\nTEL:01225672354"),
Map.entry("notifyParty", "SAME AS CONSIGNEE"),
Map.entry("bookingNumber", "WHY074G504054"),
Map.entry("ensAmsCutoffDate", "2026/07/20"),
Map.entry("expectedDepartureDate", "2026/07/24"),
Map.entry("oceanVesselVoyage", "WAN HAI 622 W027"),
Map.entry("portOfLoading", "NINGBO"),
Map.entry("portOfDischarge", "EL DEKHEILA"),
Map.entry("placeOfDelivery", "EL DEKHEILA"),
Map.entry("finalDestination", "EL DEKHEILA"),
Map.entry("serviceTerm", "CY-CY"),
Map.entry("freightTerm", "FREIGHT PREPAID"),
Map.entry("shipperLoadCountSeal", "SHPR'S LOAD,COUNT AND SEAL"),
Map.entry("containerSpec", "1*20GP"),
Map.entry("shippedOnBoard", "24-JUL-2026"),
Map.entry("draftText", "D R A F T"),
Map.entry("totalPackagesInWords", "SAY ONE THOUSAND AND TWENTY EIGHT PACKAGES ONLY."),
Map.entry("containerSummary", "WHSU2884669/WHLX730243/20GP/1028PACKAGES/28000KGS/ 28CBM"),
Map.entry("notes", "备注:1.多个箱子请分别提供分箱数,分箱品名,若未指出默认品名混装.\n2.请在 ENS/AMS 规定时间内确认好提单内容,否则有上了不船的风险."),
Map.entry("issuePlace", "NINGBO"),
Map.entry("issueDate", "24-JUL-2026")
), List.of(new FreightA4PdfDtos.CargoRow("N/M", "1028", "PACKAGES",
"AUTO SPARE PARTS\nHS CODE 870899\nACID: 4120661061019510011\nEGYPTIAN IMPORTER VAT NUMBER:412066106\nEXPORTER REGISTRATION\nNUMBER:CN-02-91330782MA2E62HK3Y\n申请目的港 14 天免用箱",
"28000.00 KGS", "28.000 CBM")));
}
private void assertA4(PDDocument document) {
assertThat(document.getNumberOfPages()).isEqualTo(1);
assertThat(document.getPage(0).getMediaBox().getWidth()).isCloseTo(PdfUnits.mmToPt(210),
org.assertj.core.data.Offset.offset(0.002f));
assertThat(document.getPage(0).getMediaBox().getHeight()).isCloseTo(PdfUnits.mmToPt(297),
org.assertj.core.data.Offset.offset(0.002f));
assertThat(document.getPage(0).getRotation()).isZero();
}
}
@@ -0,0 +1,64 @@
package com.baiyue.print.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
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.FreightA4PdfDtos;
import com.baiyue.print.renderer.FreightA4Renderer;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(classes = {FreightA4PdfService.class, PrintPdfProperties.class})
class FreightA4PdfServiceValidationTest {
@MockBean private PdfGenerationLimiter limiter;
@MockBean private FreightA4Renderer renderer;
@jakarta.annotation.Resource private FreightA4PdfService service;
@Test
void rejectsUnsupportedTemplateCode() {
assertValidationFailure(new FreightA4PdfDtos.Request("UNKNOWN",
pageSpec(), "a4.pdf", List.of(document())), "templateCode");
}
@Test
void rejectsNonA4PageSize() {
assertValidationFailure(new FreightA4PdfDtos.Request(
FreightA4PdfDtos.SHIPPING_INSTRUCTION_TEMPLATE,
new FreightA4PdfDtos.PageSpec(new BigDecimal("100"), new BigDecimal("100")),
"a4.pdf", List.of(document())), "pageSpec.widthMm");
}
@Test
void allowsMultilineDocumentFields() {
FreightA4PdfDtos.Request request = new FreightA4PdfDtos.Request(
FreightA4PdfDtos.SHIPPING_INSTRUCTION_TEMPLATE,
pageSpec(), "a4.pdf", List.of(document()));
assertThatCode(() -> service.generate(request)).doesNotThrowAnyException();
}
private void assertValidationFailure(FreightA4PdfDtos.Request request, String messagePart) {
assertThatThrownBy(() -> service.generate(request))
.isInstanceOfSatisfying(BizException.class, exception -> {
assertThat(exception.getResultCode()).isEqualTo(ResultCode.VALIDATION_FAILED);
assertThat(exception.getMessage()).contains(messagePart);
});
}
private FreightA4PdfDtos.PageSpec pageSpec() {
return new FreightA4PdfDtos.PageSpec(new BigDecimal("210"), new BigDecimal("297"));
}
private FreightA4PdfDtos.Document document() {
return new FreightA4PdfDtos.Document(Map.of("shipper", "A\nB"),
List.of(new FreightA4PdfDtos.CargoRow("N/M", "1", "PACKAGES", "GOODS", "1 KGS", "1 CBM")));
}
}