Jelajahi Sumber

fix(临考突击报告): 使用系统 MiSans 字体生成 PDF

金逸霄 6 hari lalu
induk
melakukan
4574b489a5

+ 1 - 0
abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/pdf/BundledOutlookReportFonts.java

@@ -135,4 +135,5 @@ public final class BundledOutlookReportFonts {
             Objects.requireNonNull(file, "file");
         }
     }
+
 }

+ 7 - 59
abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/pdf/PlaywrightExamSprintReportPdfGenerator.java

@@ -12,25 +12,18 @@ import com.microsoft.playwright.options.WaitUntilState;
 import org.springframework.beans.factory.DisposableBean;
 import org.springframework.stereotype.Component;
 
-import java.io.File;
-import java.util.List;
-import java.util.Locale;
 import java.util.Objects;
 import java.util.function.Supplier;
-import java.util.stream.Collectors;
 
 @Component
 public class PlaywrightExamSprintReportPdfGenerator implements ExamSprintReportPdfGenerator, DisposableBean, AutoCloseable {
 
     private static final double DEFAULT_LAUNCH_TIMEOUT_MILLIS = 30_000;
     private static final double DEFAULT_RENDER_TIMEOUT_MILLIS = 30_000;
-    private static final List<String> REPORT_FONT_FAMILIES = List.of("MiSans", "MiSans VF", "ReportFont");
-    private static final String FONT_STYLE_MARKER = "data-playwright-pdf-fonts";
 
     private final Object playwrightLock = new Object();
     private final double launchTimeoutMillis;
     private final double renderTimeoutMillis;
-    private final String bundledFontStyle;
     private Playwright playwright;
     private Browser browser;
     private boolean closed;
@@ -43,9 +36,9 @@ public class PlaywrightExamSprintReportPdfGenerator implements ExamSprintReportP
             Supplier<BundledOutlookReportFonts> bundledFontsSupplier,
             double launchTimeoutMillis,
             double renderTimeoutMillis) {
+        Objects.requireNonNull(bundledFontsSupplier, "bundledFontsSupplier");
         this.launchTimeoutMillis = launchTimeoutMillis;
         this.renderTimeoutMillis = renderTimeoutMillis;
-        this.bundledFontStyle = resolveBundledFontStyle(Objects.requireNonNull(bundledFontsSupplier, "bundledFontsSupplier"));
     }
 
     @Override
@@ -64,7 +57,7 @@ public class PlaywrightExamSprintReportPdfGenerator implements ExamSprintReportP
                 context.setDefaultNavigationTimeout(renderTimeoutMillis);
                 Page page = context.newPage();
                 page.emulateMedia(new Page.EmulateMediaOptions().setMedia(Media.PRINT));
-                page.setContent(withBundledFonts(htmlContent), new Page.SetContentOptions()
+                page.setContent(htmlContent, new Page.SetContentOptions()
                         .setWaitUntil(WaitUntilState.LOAD)
                         .setTimeout(renderTimeoutMillis));
                 page.evaluate("() => document.fonts ? document.fonts.ready.then(() => true) : true");
@@ -137,9 +130,7 @@ public class PlaywrightExamSprintReportPdfGenerator implements ExamSprintReportP
         Browser initializedBrowser = null;
         try {
             initializedPlaywright = Playwright.create();
-            initializedBrowser = initializedPlaywright.chromium().launch(new BrowserType.LaunchOptions()
-                    .setHeadless(true)
-                    .setTimeout(launchTimeoutMillis));
+            initializedBrowser = initializedPlaywright.chromium().launch(createLaunchOptions(launchTimeoutMillis));
             playwright = initializedPlaywright;
             browser = initializedBrowser;
         } catch (RuntimeException exception) {
@@ -149,53 +140,10 @@ public class PlaywrightExamSprintReportPdfGenerator implements ExamSprintReportP
         }
     }
 
-    private String withBundledFonts(String htmlContent) {
-        if (bundledFontStyle.isBlank() || htmlContent.contains(FONT_STYLE_MARKER)) {
-            return htmlContent;
-        }
-
-        String styleElement = "<style " + FONT_STYLE_MARKER + ">\n" + bundledFontStyle + "\n</style>";
-        String lowerCaseHtml = htmlContent.toLowerCase(Locale.ROOT);
-        int headEndIndex = lowerCaseHtml.indexOf("</head>");
-        if (headEndIndex < 0) {
-            return styleElement + htmlContent;
-        }
-        return htmlContent.substring(0, headEndIndex) + styleElement + htmlContent.substring(headEndIndex);
-    }
-
-    private static String resolveBundledFontStyle(Supplier<BundledOutlookReportFonts> bundledFontsSupplier) {
-        BundledOutlookReportFonts bundledFonts;
-        try {
-            bundledFonts = Objects.requireNonNull(bundledFontsSupplier.get(), "bundledFonts");
-        } catch (BundledOutlookReportFonts.BundledFontMissingException exception) {
-            return "";
-        }
-
-        if (bundledFonts.registrations().isEmpty()) {
-            return "";
-        }
-
-        File fontFile = bundledFonts.registrations().get(0).file();
-        if (!fontFile.exists() || !fontFile.isFile() || !fontFile.canRead()) {
-            throw new IllegalStateException("Bundled font file is not readable: " + fontFile);
-        }
-
-        String fontUri = fontFile.toURI().toString();
-        return REPORT_FONT_FAMILIES.stream()
-                .map(family -> fontFace(family, fontUri))
-                .collect(Collectors.joining("\n"));
-    }
-
-    private static String fontFace(String family, String fontUri) {
-        return """
-                @font-face {
-                    font-family: '%s';
-                    src: url('%s') format('truetype');
-                    font-weight: 100 900;
-                    font-style: normal;
-                    font-display: swap;
-                }
-                """.formatted(family, fontUri);
+    static BrowserType.LaunchOptions createLaunchOptions(double launchTimeoutMillis) {
+        return new BrowserType.LaunchOptions()
+                .setHeadless(true)
+                .setTimeout(launchTimeoutMillis);
     }
 
     private static void closeQuietly(AutoCloseable closeable) {

+ 12 - 12
abilities/exam-sprint/infrastructure/src/main/resources/templates/achievement-exam-sprint-report-template.html

@@ -14,7 +14,7 @@
             padding: 0;
             background: #f5f7fa;
             color: #263241;
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             font-size: 14px;
             line-height: 1.72;
         }
@@ -31,7 +31,7 @@
         .report-title {
             margin: 0 0 8px;
             color: #233f76;
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             font-size: 28px;
             font-weight: 700;
             text-align: center;
@@ -40,7 +40,7 @@
         .report-subtitle {
             margin: 0 0 24px;
             color: #68768a;
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             text-align: center;
         }
 
@@ -57,7 +57,7 @@
         .completion-title {
             margin: 0 0 6px;
             color: #9a4f00;
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             font-size: 22px;
             font-weight: 700;
         }
@@ -88,7 +88,7 @@
 
         .result-value {
             color: #ff7d00;
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             font-size: 24px;
             font-weight: 700;
             line-height: 1.2;
@@ -110,7 +110,7 @@
             border-left: 6px solid #ff7d00;
             padding-left: 12px;
             color: #233f76;
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             font-size: 20px;
             font-weight: 700;
         }
@@ -142,7 +142,7 @@
 
         .data-text {
             color: #444;
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             font-size: 14px;
             line-height: 1.8;
         }
@@ -155,27 +155,27 @@
 
         .chart-value {
             fill: #233f76;
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             font-size: 13px;
             font-weight: 700;
         }
 
         .chart-label {
             fill: #56657a;
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             font-size: 12px;
         }
 
         .chart-tick-label {
             fill: #68768a;
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             font-size: 11px;
         }
 
         .detail-text {
             margin: 8px 0;
             color: #3d4a5d;
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
         }
 
         .highlight {
@@ -230,7 +230,7 @@
             border-radius: 999px;
             background: #fff1e7;
             color: #9a4f00;
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             font-weight: 600;
         }
 

+ 9 - 9
abilities/exam-sprint/infrastructure/src/main/resources/templates/outlook-exam-sprint-report-template.html

@@ -14,7 +14,7 @@
             background-color: #f5f7fa;
             padding: 0;
             color: #333;
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             font-size: 14px;
             line-height: 1.72;
         }
@@ -29,7 +29,7 @@
         }
 
         h1.report-title {
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             text-align: center;
             font-size: 28px;
             font-weight: 600;
@@ -38,7 +38,7 @@
         }
 
         p.report-subtitle {
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             text-align: center;
             margin: 0 0 24px;
             color: #68768a;
@@ -50,7 +50,7 @@
         }
 
         .section-title {
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             font-size: 20px;
             color: #2b4c8a;
             border-left: 6px solid #ff7d00;
@@ -94,7 +94,7 @@
         }
 
         .card-title {
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             font-size: 16px;
             color: #2b4c8a;
             font-weight: 600;
@@ -116,7 +116,7 @@
         }
 
         .data-text {
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             margin: 6px 0;
             color: #3d4a5d;
             line-height: 1.72;
@@ -172,7 +172,7 @@
         }
 
         .freq-header {
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             font-size: 18px;
             font-weight: 600;
             color: #303a49;
@@ -237,14 +237,14 @@
         }
 
         .freq-data {
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             color: #4a5568;
             font-size: 14px;
             line-height: 1.72;
         }
 
         .text-desc {
-            font-family: MiSans, ReportFont, sans-serif;
+            font-family: 'MiSans VF', MiSans, ReportFont, sans-serif;
             font-size: 14px;
             color: #4a5568;
             margin: 10px 0;

+ 8 - 0
abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/pdf/PlaywrightExamSprintReportPdfGeneratorTest.java

@@ -8,6 +8,7 @@ import cn.yunzhixue.ability.center.examsprint.infrastructure.report.rendering.ou
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.microsoft.playwright.BrowserType;
 import org.apache.pdfbox.pdmodel.PDDocument;
 import org.apache.pdfbox.text.PDFTextStripper;
 import org.junit.jupiter.api.AfterAll;
@@ -56,6 +57,13 @@ class PlaywrightExamSprintReportPdfGeneratorTest {
         }).doesNotThrowAnyException();
     }
 
+    @Test
+    void chromiumLaunchUsesDefaultSandboxOptions() {
+        BrowserType.LaunchOptions launchOptions = PlaywrightExamSprintReportPdfGenerator.createLaunchOptions(123);
+
+        assertThat(launchOptions.args).isNull();
+    }
+
     @Test
     void generateCreatesPdfWithExtractableChineseText() throws Exception {
         byte[] pdfBytes = pdfGenerator.generate("""

+ 34 - 0
abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/AchievementExamSprintReportTemplateCompatibilityTest.java

@@ -0,0 +1,34 @@
+package cn.yunzhixue.ability.center.examsprint.infrastructure.report.rendering.achievement;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.core.io.ClassPathResource;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AchievementExamSprintReportTemplateCompatibilityTest {
+
+    private static final String REPORT_FONT_STACK = "'MiSans VF', MiSans, ReportFont, sans-serif";
+
+    @Test
+    void templateUsesRealBundledFontFamilyBeforeAliases() throws Exception {
+        String normalizedTemplate = normalizeWhitespace(loadTemplate());
+
+        assertThat(normalizedTemplate)
+                .contains("font-family: " + REPORT_FONT_STACK)
+                .doesNotContain("font-family: MiSans, ReportFont, sans-serif");
+    }
+
+    private String loadTemplate() throws Exception {
+        ClassPathResource resource = new ClassPathResource("templates/achievement-exam-sprint-report-template.html");
+        try (InputStream inputStream = resource.getInputStream()) {
+            return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
+        }
+    }
+
+    private String normalizeWhitespace(String value) {
+        return value.replaceAll("\\s+", " ").trim();
+    }
+}

+ 21 - 10
abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/OutlookExamSprintReportTemplateCompatibilityTest.java

@@ -10,6 +10,8 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 class OutlookExamSprintReportTemplateCompatibilityTest {
 
+    private static final String REPORT_FONT_STACK = "'MiSans VF', MiSans, ReportFont, sans-serif";
+
     @Test
     void templateMatchesDesignDraftWithPdfSafeLayoutStyles() throws Exception {
         String normalizedTemplate = normalizeWhitespace(loadTemplate());
@@ -19,7 +21,7 @@ class OutlookExamSprintReportTemplateCompatibilityTest {
                 .contains("background-color: #f5f7fa")
                 .contains("padding: 0")
                 .contains("color: #333")
-                .contains("font-family: MiSans, ReportFont, sans-serif")
+                .contains("font-family: " + REPORT_FONT_STACK)
                 .contains("font-size: 14px")
                 .contains("line-height: 1.72")
                 .doesNotContainPattern("(?i)-webkit-print-color-adjust\\s*:")
@@ -35,12 +37,12 @@ class OutlookExamSprintReportTemplateCompatibilityTest {
                 .doesNotContainPattern("(?i)(^|[\\s{;])box-shadow\\s*:")
                 .contains("h1.report-title")
                 .contains("font-size: 28px")
-                .containsPattern("h1\\.report-title\\s*\\{[^}]*font-family\\s*:\\s*MiSans, ReportFont, sans-serif\\s*;[^}]*font-size\\s*:\\s*28px\\s*;[^}]*font-weight\\s*:\\s*600\\s*;[^}]*}")
+                .containsPattern("h1\\.report-title\\s*\\{[^}]*font-family\\s*:\\s*'MiSans VF', MiSans, ReportFont, sans-serif\\s*;[^}]*font-size\\s*:\\s*28px\\s*;[^}]*font-weight\\s*:\\s*600\\s*;[^}]*}")
                 .contains("p.report-subtitle")
-                .containsPattern("p\\.report-subtitle\\s*\\{[^}]*font-family\\s*:\\s*MiSans, ReportFont, sans-serif\\s*;[^}]*line-height\\s*:\\s*1.72\\s*;[^}]*}")
+                .containsPattern("p\\.report-subtitle\\s*\\{[^}]*font-family\\s*:\\s*'MiSans VF', MiSans, ReportFont, sans-serif\\s*;[^}]*line-height\\s*:\\s*1.72\\s*;[^}]*}")
                 .contains(".section")
                 .contains(".section-title")
-                .containsPattern("\\.section-title\\s*\\{[^}]*font-family\\s*:\\s*MiSans, ReportFont, sans-serif\\s*;[^}]*font-size\\s*:\\s*20px\\s*;[^}]*font-weight\\s*:\\s*600\\s*;[^}]*}")
+                .containsPattern("\\.section-title\\s*\\{[^}]*font-family\\s*:\\s*'MiSans VF', MiSans, ReportFont, sans-serif\\s*;[^}]*font-size\\s*:\\s*20px\\s*;[^}]*font-weight\\s*:\\s*600\\s*;[^}]*}")
                 .contains("border-left: 6px solid #ff7d00")
                 .contains(".analysis-table")
                 .contains(".analysis-row")
@@ -51,7 +53,7 @@ class OutlookExamSprintReportTemplateCompatibilityTest {
                 .containsPattern("\\.card\\s*\\{[^}]*min-height\\s*:\\s*370px\\s*;[^}]*}")
                 .doesNotContainPattern("(?i)(^|[\\s{;])display\\s*:\\s*flex\\b")
                 .doesNotContainPattern("(?i)(^|[\\s{;])flex-direction\\s*:")
-                .containsPattern("\\.card-title\\s*\\{[^}]*font-family\\s*:\\s*MiSans, ReportFont, sans-serif\\s*;[^}]*font-size\\s*:\\s*16px\\s*;[^}]*font-weight\\s*:\\s*600\\s*;[^}]*}")
+                .containsPattern("\\.card-title\\s*\\{[^}]*font-family\\s*:\\s*'MiSans VF', MiSans, ReportFont, sans-serif\\s*;[^}]*font-size\\s*:\\s*16px\\s*;[^}]*font-weight\\s*:\\s*600\\s*;[^}]*}")
                 .contains(".frequency-table")
                 .contains(".frequency-row")
                 .contains(".frequency-cell")
@@ -59,7 +61,7 @@ class OutlookExamSprintReportTemplateCompatibilityTest {
                 .containsPattern("\\.frequency-row\\s*\\{[^}]*page-break-inside\\s*:\\s*avoid\\s*;[^}]*}")
                 .containsPattern("\\.freq-card\\s*\\{[^}]*page-break-inside\\s*:\\s*avoid\\s*;[^}]*}")
                 .contains(".freq-header")
-                .containsPattern("\\.freq-header\\s*\\{[^}]*font-family\\s*:\\s*MiSans, ReportFont, sans-serif\\s*;[^}]*font-size\\s*:\\s*18px\\s*;[^}]*font-weight\\s*:\\s*600\\s*;[^}]*}")
+                .containsPattern("\\.freq-header\\s*\\{[^}]*font-family\\s*:\\s*'MiSans VF', MiSans, ReportFont, sans-serif\\s*;[^}]*font-size\\s*:\\s*18px\\s*;[^}]*font-weight\\s*:\\s*600\\s*;[^}]*}")
                 .containsPattern("\\.badge\\s*\\{[^}]*position\\s*:\\s*relative\\s*;[^}]*top\\s*:\\s*7px\\s*;[^}]*min-width\\s*:\\s*40px\\s*;[^}]*height\\s*:\\s*20px\\s*;[^}]*padding\\s*:\\s*0\\s+8px\\s*;[^}]*line-height\\s*:\\s*20px\\s*;[^}]*text-align\\s*:\\s*center\\s*;[^}]*vertical-align\\s*:\\s*middle\\s*;[^}]*}")
                 .contains(".freq-header-table")
                 .containsPattern("\\.freq-header-table\\s*\\{[^}]*width\\s*:\\s*100%\\s*;[^}]*table-layout\\s*:\\s*fixed\\s*;[^}]*border-collapse\\s*:\\s*collapse\\s*;[^}]*}")
@@ -67,10 +69,10 @@ class OutlookExamSprintReportTemplateCompatibilityTest {
                 .containsPattern("\\.freq-star-cell\\s*\\{[^}]*width\\s*:\\s*28px\\s*;[^}]*text-align\\s*:\\s*right\\s*;[^}]*vertical-align\\s*:\\s*middle\\s*;[^}]*}")
                 .doesNotContainPattern("\\.crown\\s*\\{[^}]*position\\s*:\\s*absolute\\s*;")
                 .containsPattern("\\.crown\\s*\\{[^}]*position\\s*:\\s*relative\\s*;[^}]*top\\s*:\\s*-4px\\s*;[^}]*}")
-                .containsPattern("\\.data-text\\s*\\{[^}]*font-family\\s*:\\s*MiSans, ReportFont, sans-serif\\s*;[^}]*line-height\\s*:\\s*1.72\\s*;[^}]*}")
+                .containsPattern("\\.data-text\\s*\\{[^}]*font-family\\s*:\\s*'MiSans VF', MiSans, ReportFont, sans-serif\\s*;[^}]*line-height\\s*:\\s*1.72\\s*;[^}]*}")
                 .contains(".text-desc")
-                .containsPattern("\\.text-desc\\s*\\{[^}]*font-family\\s*:\\s*MiSans, ReportFont, sans-serif\\s*;[^}]*line-height\\s*:\\s*1.72\\s*;[^}]*}")
-                .containsPattern("\\.freq-data\\s*\\{[^}]*font-family\\s*:\\s*MiSans, ReportFont, sans-serif\\s*;[^}]*line-height\\s*:\\s*1.72\\s*;[^}]*}")
+                .containsPattern("\\.text-desc\\s*\\{[^}]*font-family\\s*:\\s*'MiSans VF', MiSans, ReportFont, sans-serif\\s*;[^}]*line-height\\s*:\\s*1.72\\s*;[^}]*}")
+                .containsPattern("\\.freq-data\\s*\\{[^}]*font-family\\s*:\\s*'MiSans VF', MiSans, ReportFont, sans-serif\\s*;[^}]*line-height\\s*:\\s*1.72\\s*;[^}]*}")
                 .contains(".student-case")
                 .contains(".student-case-table")
                 .contains(".student-case-row")
@@ -127,7 +129,7 @@ class OutlookExamSprintReportTemplateCompatibilityTest {
         assertThat(normalizedTemplate)
                 .containsPattern("@page\\s*\\{[^}]*size\\s*:\\s*A4\\s*;[^}]*margin\\s*:\\s*0\\s*;[^}]*}")
                 .contains("h1.report-title")
-                .contains("font-family: MiSans, ReportFont, sans-serif")
+                .contains("font-family: " + REPORT_FONT_STACK)
                 .contains("font-size: 28px")
                 .contains("font-weight: 600")
                 .contains("color: #2b4c8a")
@@ -136,6 +138,15 @@ class OutlookExamSprintReportTemplateCompatibilityTest {
                 .contains("text-align: center");
     }
 
+    @Test
+    void templateUsesRealBundledFontFamilyBeforeAliases() throws Exception {
+        String normalizedTemplate = normalizeWhitespace(loadTemplate());
+
+        assertThat(normalizedTemplate)
+                .contains("font-family: " + REPORT_FONT_STACK)
+                .doesNotContain("font-family: MiSans, ReportFont, sans-serif");
+    }
+
     private String loadTemplate() throws Exception {
         ClassPathResource resource = new ClassPathResource("templates/outlook-exam-sprint-report-template.html");
         try (InputStream inputStream = resource.getInputStream()) {

+ 13 - 3
deploy/ability-center/runtime/dockerfile

@@ -1,13 +1,23 @@
-# 使用官方 OpenJDK 17 镜像
-FROM openjdk:17-jdk
+# 使用官方 Playwright Java 镜像,内置 Chromium 与运行依赖
+FROM mcr.microsoft.com/playwright/java:v1.58.0-noble
 
 # 设置工作目录
 WORKDIR /app
 ENV TZ=Asia/Shanghai
+ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
+ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
+
 RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
 
+# 安装报告 PDF 使用的固定字体,避免 Playwright 每次通过 @font-face 加载随包字体
+RUN mkdir -p /usr/local/share/fonts/misans
+COPY fonts/MiSans-VF.ttf /usr/local/share/fonts/misans/MiSans-VF.ttf
+RUN fc-cache -fv \
+    && fc-match "MiSans VF" \
+    && fc-match "MiSans VF" | grep -q "MiSans-VF.ttf"
+
 # 暴露端口
 EXPOSE 8500
 
 # 启动前检查挂载文件,缺失时快速失败
-CMD ["sh", "-c", "PROFILE=${SPRING_PROFILES_ACTIVE:-test}; case \"$PROFILE\" in *,*) echo \"Unsupported comma-separated SPRING_PROFILES_ACTIVE: $PROFILE\"; exit 1 ;; esac; test -f /app/dcjxb-ability-center.jar || { echo 'Missing /app/dcjxb-ability-center.jar'; exit 1; }; test -f /app/application.yml || { echo 'Missing /app/application.yml'; exit 1; }; test -f \"/app/application-${PROFILE}.yml\" || { echo \"Missing /app/application-${PROFILE}.yml\"; exit 1; }; exec java -Xms256m -Xmx512m -Dspring.profiles.active=$PROFILE -jar /app/dcjxb-ability-center.jar --spring.config.location=/app/application.yml,/app/application-${PROFILE}.yml"]
+CMD ["sh", "-c", "PROFILE=${SPRING_PROFILES_ACTIVE:-test}; case \"$PROFILE\" in *,*) echo \"Unsupported comma-separated SPRING_PROFILES_ACTIVE: $PROFILE\"; exit 1 ;; esac; test -f /app/dcjxb-ability-center.jar || { echo 'Missing /app/dcjxb-ability-center.jar'; exit 1; }; test -f /app/application.yml || { echo 'Missing /app/application.yml'; exit 1; }; test -f \"/app/application-${PROFILE}.yml\" || { echo \"Missing /app/application-${PROFILE}.yml\"; exit 1; }; exec java -Xms256m -Xmx512m -Djava.net.preferIPv4Stack=true -Dspring.profiles.active=$PROFILE -jar /app/dcjxb-ability-center.jar --spring.config.location=/app/application.yml,/app/application-${PROFILE}.yml"]

TEMPAT SAMPAH
deploy/ability-center/runtime/fonts/MiSans-VF.ttf