格式修改

This commit is contained in:
2026-08-05 10:35:07 +08:00
parent 81c9c92f5d
commit 496fc58a90
241 changed files with 5600 additions and 2088 deletions
@@ -1,5 +1,8 @@
package com.baiyue.print.config;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.io.InputStream;
import org.apache.fontbox.ttf.TTFParser;
import org.apache.fontbox.ttf.TrueTypeFont;
@@ -11,11 +14,11 @@ import org.springframework.stereotype.Component;
/** Exposes font packaging/parsing failures through readiness health. */
@Component("printFont")
public class PrintFontHealthIndicator implements HealthIndicator {
private final Health validationResult;
@Resource private PrintPdfProperties properties;
private Health validationResult;
public PrintFontHealthIndicator(PrintPdfProperties properties) {
this.validationResult = validate(properties.getFontResource());
}
@PostConstruct
void initializeHealth() { validationResult = validate(properties.getFontResource()); }
@Override
public Health health() {
@@ -1,5 +1,7 @@
package com.baiyue.print.controller;
import jakarta.annotation.Resource;
import com.baiyue.common.web.CorrelationContext;
import com.baiyue.print.dto.InboundMarkPdfDtos;
import com.baiyue.print.service.InboundMarkPdfService;
@@ -24,13 +26,8 @@ public class InternalPdfController {
static final String INTERNAL_TOKEN_HEADER = "X-Internal-Service-Token";
static final String PAGE_COUNT_HEADER = "X-Print-Page-Count";
private final InboundMarkPdfService pdfService;
private final InternalServiceTokenGuard tokenGuard;
public InternalPdfController(InboundMarkPdfService pdfService, InternalServiceTokenGuard tokenGuard) {
this.pdfService = pdfService;
this.tokenGuard = tokenGuard;
}
@Resource private InboundMarkPdfService pdfService;
@Resource private InternalServiceTokenGuard tokenGuard;
@PostMapping(value = "/inbound-marks", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<byte[]> generateInboundMarks(
@@ -1,5 +1,7 @@
package com.baiyue.print.controller;
import jakarta.annotation.PostConstruct;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import org.springframework.beans.factory.annotation.Value;
@@ -11,9 +13,11 @@ import java.security.MessageDigest;
/** Verifies the shared credential used by internal service callers. */
@Component
public class InternalServiceTokenGuard {
private final byte[] expectedToken;
@Value("${baiyue.security.internal-service-token}") private String token;
private byte[] expectedToken;
public InternalServiceTokenGuard(@Value("${baiyue.security.internal-service-token}") String token) {
@PostConstruct
void initializeToken() {
if (token == null || token.getBytes(StandardCharsets.UTF_8).length < 32) {
throw new IllegalStateException("INTERNAL_SERVICE_TOKEN must contain at least 32 bytes");
}
@@ -1,5 +1,7 @@
package com.baiyue.print.controller;
import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.web.CorrelationContext;
@@ -28,13 +30,8 @@ import org.springframework.web.filter.OncePerRequestFilter;
public class PrintRequestSizeFilter extends OncePerRequestFilter {
private static final String PATH = "/internal/print/v1/pdfs/inbound-marks";
private final PrintPdfProperties properties;
private final ObjectMapper objectMapper;
public PrintRequestSizeFilter(PrintPdfProperties properties, ObjectMapper objectMapper) {
this.properties = properties;
this.objectMapper = objectMapper;
}
@Resource private PrintPdfProperties properties;
@Resource private ObjectMapper objectMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
@@ -1,5 +1,7 @@
package com.baiyue.print.renderer;
import jakarta.annotation.Resource;
import com.baiyue.print.dto.InboundMarkPdfDtos;
import com.google.zxing.common.BitMatrix;
import java.io.IOException;
@@ -24,11 +26,7 @@ public class InboundMarkV1Renderer {
private static final float QR_LEFT_PT = (PAGE_WIDTH_PT - QR_SIZE_PT) / 2f;
private static final float QR_BOTTOM_PT = PAGE_HEIGHT_PT - PdfUnits.mmToPt(84);
private final PdfQrRenderer qrRenderer;
public InboundMarkV1Renderer(PdfQrRenderer qrRenderer) {
this.qrRenderer = qrRenderer;
}
@Resource private PdfQrRenderer qrRenderer;
public void renderPage(PDDocument document, PDFont font, InboundMarkPdfDtos.Label label,
BitMatrix qrMatrix) throws IOException {
@@ -1,5 +1,7 @@
package com.baiyue.print.service;
import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.print.config.PrintPdfProperties;
@@ -30,18 +32,10 @@ public class InboundMarkPdfService {
private static final Set<Character> FORBIDDEN_FILE_NAME_CHARACTERS =
Set.of('/', '\\', ':', '*', '?', '"', '<', '>', '|');
private final PrintPdfProperties properties;
private final PdfGenerationLimiter limiter;
private final InboundMarkV1Renderer renderer;
private final PdfQrRenderer qrRenderer;
public InboundMarkPdfService(PrintPdfProperties properties, PdfGenerationLimiter limiter,
InboundMarkV1Renderer renderer, PdfQrRenderer qrRenderer) {
this.properties = properties;
this.limiter = limiter;
this.renderer = renderer;
this.qrRenderer = qrRenderer;
}
@Resource private PrintPdfProperties properties;
@Resource private PdfGenerationLimiter limiter;
@Resource private InboundMarkV1Renderer renderer;
@Resource private PdfQrRenderer qrRenderer;
public GeneratedPdf generate(InboundMarkPdfDtos.Request request) {
NormalizedRequest normalized = validateAndNormalize(request);
@@ -1,5 +1,8 @@
package com.baiyue.print.service;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.print.config.PrintPdfProperties;
@@ -13,10 +16,12 @@ import org.springframework.stereotype.Component;
*/
@Component
public class PdfGenerationLimiter {
private final Semaphore semaphore;
private final Duration acquireTimeout;
@Resource private PrintPdfProperties properties;
private Semaphore semaphore;
private Duration acquireTimeout;
public PdfGenerationLimiter(PrintPdfProperties properties) {
@PostConstruct
void initializeLimiter() {
if (properties.getMaxConcurrentGenerations() < 1) {
throw new IllegalArgumentException("print.pdf.max-concurrent-generations must be positive");
}
+1 -1
View File
@@ -44,6 +44,6 @@ logging:
rollingpolicy:
file-name-pattern: "${LOGGING_ROLLING_FILE_NAME_PATTERN:${LOG_PATH:./logs}/${spring.application.name}.%d{yyyy-MM-dd}.%i.log.gz}"
max-file-size: "${LOGGING_ROLLING_MAX_FILE_SIZE:100MB}"
max-history: "${LOGGING_ROLLING_MAX_HISTORY:30}"
max-history: "${LOGGING_ROLLING_MAX_HISTORY:7}"
total-size-cap: "${LOGGING_ROLLING_TOTAL_SIZE_CAP:5GB}"
clean-history-on-start: "${LOGGING_ROLLING_CLEAN_HISTORY_ON_START:false}"
@@ -9,6 +9,7 @@ import com.baiyue.print.renderer.InboundMarkV1Renderer;
import com.baiyue.print.renderer.PdfQrRenderer;
import com.baiyue.print.service.InboundMarkPdfService;
import com.baiyue.print.service.PdfGenerationLimiter;
import jakarta.annotation.Resource;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.RGBLuminanceSource;
@@ -29,16 +30,20 @@ import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.text.PDFTextStripper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/** End-to-end renderer acceptance checks. These intentionally exercise the real service, not mocks. */
@SpringJUnitConfig(classes = {InboundMarkPdfService.class, PrintPdfProperties.class,
PdfGenerationLimiter.class, InboundMarkV1Renderer.class, PdfQrRenderer.class})
class InboundMarkPdfAcceptanceTest {
private static final String TEMPLATE = "INBOUND_MARK_V1";
private static final BigDecimal MM_100 = new BigDecimal("100");
@Resource
private InboundMarkPdfService service;
@Test
void generatesOnePageWithExactSizeTextOrderEmbeddedChineseFontAndNoDiskArtifact(@TempDir Path tempDir)
throws Exception {
InboundMarkPdfService service = service();
InboundMarkPdfDtos.Label label = label(1);
String originalTmp = System.getProperty("java.io.tmpdir");
System.setProperty("java.io.tmpdir", tempDir.toString());
@@ -62,13 +67,13 @@ class InboundMarkPdfAcceptanceTest {
@Test
void preservesPageOrderAndDecodableQrOnFirstMiddleAndLastPages() throws Exception {
List<InboundMarkPdfDtos.Label> labels = labels(19);
assertPdf(service().generate(request(labels)).content(), labels);
assertPdf(service.generate(request(labels)).content(), labels);
}
@Test
void generatesTwoHundredEighteenPagesWithExactDimensions() throws Exception {
List<InboundMarkPdfDtos.Label> labels = labels(218);
byte[] pdf = service().generate(request(labels)).content();
byte[] pdf = service.generate(request(labels)).content();
try (PDDocument document = Loader.loadPDF(pdf)) {
assertThat(document.getNumberOfPages()).isEqualTo(218);
for (int i : List.of(0, 108, 217)) {
@@ -82,7 +87,7 @@ class InboundMarkPdfAcceptanceTest {
assumeTrue(Boolean.getBoolean("print.capacity.tests"),
"Enable with -Dprint.capacity.tests=true for the 2000-page capacity acceptance test");
List<InboundMarkPdfDtos.Label> labels = labels(2000);
byte[] pdf = service().generate(request(labels)).content();
byte[] pdf = service.generate(request(labels)).content();
assertThat(pdf).startsWith("%PDF".getBytes(StandardCharsets.US_ASCII));
assertThat(pdf.length).isLessThan(32 * 1024 * 1024);
try (PDDocument document = Loader.loadPDF(pdf)) {
@@ -152,12 +157,6 @@ class InboundMarkPdfAcceptanceTest {
return List.of(0, size / 2, size - 1);
}
private InboundMarkPdfService service() {
PrintPdfProperties properties = new PrintPdfProperties();
return new InboundMarkPdfService(properties, new PdfGenerationLimiter(properties),
new InboundMarkV1Renderer(new PdfQrRenderer()), new PdfQrRenderer());
}
private InboundMarkPdfDtos.Request request(List<InboundMarkPdfDtos.Label> labels) {
return new InboundMarkPdfDtos.Request(TEMPLATE,
new InboundMarkPdfDtos.PageSpec(MM_100, MM_100), "acceptance.pdf", labels);
@@ -4,20 +4,28 @@ import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
class PrintFontHealthIndicatorTest {
@Test
void reportsUpForPackagedParseableFont() {
PrintPdfProperties properties = new PrintPdfProperties();
assertThat(new PrintFontHealthIndicator(properties).health().getStatus()).isEqualTo(Status.UP);
context("fonts/NotoSansSC-Bold.ttf").run(context -> assertThat(
context.getBean(PrintFontHealthIndicator.class).health().getStatus()).isEqualTo(Status.UP));
}
@Test
void reportsDownForMissingFont() {
PrintPdfProperties properties = new PrintPdfProperties();
properties.setFontResource("fonts/missing.ttf");
context("fonts/missing.ttf").run(context -> assertThat(
context.getBean(PrintFontHealthIndicator.class).health().getStatus()).isEqualTo(Status.DOWN));
}
assertThat(new PrintFontHealthIndicator(properties).health().getStatus()).isEqualTo(Status.DOWN);
private ApplicationContextRunner context(String font) {
return new ApplicationContextRunner()
.withBean(PrintPdfProperties.class, () -> {
PrintPdfProperties properties = new PrintPdfProperties();
properties.setFontResource(font);
return properties;
})
.withBean(PrintFontHealthIndicator.class);
}
}
@@ -5,6 +5,7 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.web.GlobalExceptionHandler;
import com.baiyue.print.dto.InboundMarkPdfDtos;
import com.baiyue.print.service.InboundMarkPdfService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
@@ -12,6 +13,9 @@ import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.nio.charset.StandardCharsets;
@@ -28,16 +32,18 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringJUnitConfig(classes = {InternalPdfController.class, InternalServiceTokenGuard.class})
@TestPropertySource(properties = "baiyue.security.internal-service-token=0123456789abcdef0123456789abcdef")
class InternalPdfControllerTest {
private static final String TOKEN = "0123456789abcdef0123456789abcdef";
@MockBean
private InboundMarkPdfService service;
@Resource
private InternalPdfController controller;
private MockMvc mockMvc;
@BeforeEach
void setUp() {
service = mock(InboundMarkPdfService.class);
InternalPdfController controller = new InternalPdfController(
service, new InternalServiceTokenGuard(TOKEN));
mockMvc = MockMvcBuilders.standaloneSetup(controller)
.setControllerAdvice(new GlobalExceptionHandler())
.build();
@@ -3,17 +3,23 @@ package com.baiyue.print.controller;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import org.junit.jupiter.api.Test;
import jakarta.annotation.Resource;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@SpringJUnitConfig(InternalServiceTokenGuard.class)
@TestPropertySource(properties = "baiyue.security.internal-service-token=0123456789abcdef0123456789abcdef")
class InternalServiceTokenGuardTest {
private static final String TOKEN = "0123456789abcdef0123456789abcdef";
@Resource private InternalServiceTokenGuard guard;
@Test
void acceptsOnlyConfiguredToken() {
InternalServiceTokenGuard guard = new InternalServiceTokenGuard(TOKEN);
guard.verify(TOKEN);
assertThatThrownBy(() -> guard.verify(null))
.isInstanceOf(BizException.class)
@@ -27,10 +33,14 @@ class InternalServiceTokenGuardTest {
@Test
void refusesUnsafeConfiguration() {
assertThatThrownBy(() -> new InternalServiceTokenGuard("too-short"))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("at least 32 bytes");
assertThatThrownBy(() -> new InternalServiceTokenGuard(null))
.isInstanceOf(IllegalStateException.class);
new ApplicationContextRunner()
.withBean(PropertySourcesPlaceholderConfigurer.class)
.withBean(InternalServiceTokenGuard.class)
.withPropertyValues("baiyue.security.internal-service-token=too-short")
.run(context -> assertThat(context).hasFailed());
new ApplicationContextRunner()
.withBean(PropertySourcesPlaceholderConfigurer.class)
.withBean(InternalServiceTokenGuard.class)
.run(context -> assertThat(context).hasFailed());
}
}
@@ -10,15 +10,20 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(classes = {PrintRequestSizeFilter.class, PrintPdfProperties.class,
PrintRequestSizeFilterTest.TestConfig.class})
class PrintRequestSizeFilterTest {
private PrintRequestSizeFilter filter;
@Resource private PrintRequestSizeFilter filter;
@Resource private PrintPdfProperties properties;
@BeforeEach
void setUp() {
PrintPdfProperties properties = new PrintPdfProperties();
properties.setMaxRequestBytes(10);
filter = new PrintRequestSizeFilter(properties, new ObjectMapper().findAndRegisterModules());
}
@Test
@@ -86,4 +91,12 @@ class PrintRequestSizeFilterTest {
.contains("Request body exceeds 10 bytes")
.contains("\"requestId\":\"request-123\"");
}
@Configuration
static class TestConfig {
@Bean
ObjectMapper objectMapper() {
return new ObjectMapper().findAndRegisterModules();
}
}
}
@@ -7,6 +7,7 @@ import com.baiyue.print.dto.InboundMarkPdfDtos;
import com.baiyue.print.renderer.InboundMarkV1Renderer;
import com.baiyue.print.renderer.PdfQrRenderer;
import com.baiyue.print.renderer.PdfUnits;
import jakarta.annotation.Resource;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
@@ -14,17 +15,15 @@ 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 = {InboundMarkPdfService.class, PrintPdfProperties.class,
PdfGenerationLimiter.class, InboundMarkV1Renderer.class, PdfQrRenderer.class})
class InboundMarkPdfServiceGenerationTest {
@Resource
private InboundMarkPdfService service;
@Test
void generatesOneStrictHundredMillimeterPagePerLabelInRequestOrder() throws IOException {
PrintPdfProperties properties = new PrintPdfProperties();
PdfQrRenderer qrRenderer = new PdfQrRenderer();
InboundMarkPdfService service = new InboundMarkPdfService(
properties,
new PdfGenerationLimiter(properties),
new InboundMarkV1Renderer(qrRenderer),
qrRenderer);
InboundMarkPdfDtos.Request request = new InboundMarkPdfDtos.Request(
"INBOUND_MARK_V1",
new InboundMarkPdfDtos.PageSpec(new BigDecimal("100"), new BigDecimal("100.0")),
@@ -10,23 +10,20 @@ import com.baiyue.print.config.PrintPdfProperties;
import com.baiyue.print.dto.InboundMarkPdfDtos;
import com.baiyue.print.renderer.InboundMarkV1Renderer;
import com.baiyue.print.renderer.PdfQrRenderer;
import jakarta.annotation.Resource;
import java.math.BigDecimal;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(classes = {InboundMarkPdfService.class, PrintPdfProperties.class})
class InboundMarkPdfServiceValidationTest {
private InboundMarkPdfService service;
@BeforeEach
void setUp() {
PrintPdfProperties properties = new PrintPdfProperties();
service = new InboundMarkPdfService(
properties,
mock(PdfGenerationLimiter.class),
mock(InboundMarkV1Renderer.class),
mock(PdfQrRenderer.class));
}
@MockBean private PdfGenerationLimiter limiter;
@MockBean private InboundMarkV1Renderer renderer;
@MockBean private PdfQrRenderer qrRenderer;
@Resource private InboundMarkPdfService service;
@Test
void rejectsUnsupportedPageSizeBeforeRendering() {
@@ -7,15 +7,16 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.print.config.PrintPdfProperties;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(classes = {PdfGenerationLimiter.class, PdfGenerationLimiterTest.TestConfig.class})
class PdfGenerationLimiterTest {
@Resource private PdfGenerationLimiter limiter;
@Test
void rejectsWhenAllPermitsAreInUseAndReleasesPermitOnClose() {
PrintPdfProperties properties = new PrintPdfProperties();
properties.setMaxConcurrentGenerations(1);
properties.setAcquireTimeout(Duration.ZERO);
PdfGenerationLimiter limiter = new PdfGenerationLimiter(properties);
PdfGenerationLimiter.Permit permit = limiter.acquire();
assertThatThrownBy(limiter::acquire)
.isInstanceOfSatisfying(BizException.class,
@@ -27,4 +28,15 @@ class PdfGenerationLimiterTest {
// A second acquisition proves that close returned the only permit.
}
}
@Configuration
static class TestConfig {
@Bean
PrintPdfProperties properties() {
PrintPdfProperties properties = new PrintPdfProperties();
properties.setMaxConcurrentGenerations(1);
properties.setAcquireTimeout(Duration.ZERO);
return properties;
}
}
}