Просмотр исходного кода

feat(临考突击报告): 支持展望报告动态标题

金逸霄 3 дней назад
Родитель
Сommit
8064fa905a

+ 6 - 0
abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRenderer.java

@@ -75,8 +75,10 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
         try {
             OutlookExamSprintReportPayload payloadContract = objectMapper.treeToValue(payloadForDeserialization(payload), OutlookExamSprintReportPayload.class);
             OutlookReportViewModel reportPayload = adaptPayload(payloadContract);
+            String reportTitle = reportTitle(payloadContract.stageExaminName());
             return template
                     .replace("{{reportLogoDataUri}}", escape(reportLogoDataUri))
+                    .replace("{{reportTitle}}", escape(reportTitle))
                     .replace("{{headerReportType}}", "潜力展望报告")
                     .replace("{{studentName}}", escape(payloadContract.studentName()))
                     .replace("{{generatedAtText}}", escape(formatGeneratedAt(generatedAt)))
@@ -92,6 +94,10 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
         }
     }
 
+    private String reportTitle(String stageExaminName) {
+        return stageExaminName.trim() + "英语临考词汇突击潜力展望报告";
+    }
+
     private JsonNode payloadForDeserialization(JsonNode payload) {
         if (!payload.has("StudentName") || !payload.has("studentName")) {
             return payload;

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

@@ -2,7 +2,7 @@
 <html lang="zh-CN">
 <head>
     <meta charset="UTF-8"/>
-    <title>高考英语临考词汇突击潜力展望报告</title>
+    <title>{{reportTitle}}</title>
     <style>
         @page {
             size: A4;
@@ -409,7 +409,7 @@
         <div class="header-generated-at">{{generatedAtText}}</div>
     </header>
 
-    <h1 class="report-title">高考英语临考词汇突击潜力展望报告</h1>
+    <h1 class="report-title">{{reportTitle}}</h1>
     <p class="report-subtitle">科学规划 · 精准提分 · 短期见效</p>
 
     <div class="section">

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

@@ -266,7 +266,7 @@ class PlaywrightExamSprintReportPdfGeneratorTest {
         try (PDDocument document = PDDocument.load(pdfBytes)) {
             assertThat(document.getNumberOfPages()).isGreaterThanOrEqualTo(1);
             assertThat(normalizePdfText(new PDFTextStripper().getText(document)))
-                    .contains("考英语临考词汇突击潜力展望报告")
+                    .contains("考英语临考词汇突击潜力展望报告")
                     .contains("模块一:个人学情分析")
                     .contains("模块二:科学备考建议");
         }

+ 36 - 0
abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRendererTest.java

@@ -149,6 +149,42 @@ class ClasspathOutlookExamSprintReportRendererTest {
         assertCssRuleContains(html, ".header-generated-at", "width: 33.3333%;", "text-align: right;");
     }
 
+    /**
+     * 覆盖展望报告主标题动态展示场景,当调用方提供 StageExaminName 时,应使用该考试名称生成页面标题。
+     */
+    @Test
+    void renderUsesStageExaminNameForDynamicReportTitle() throws Exception {
+        ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
+        ObjectNode payload = (ObjectNode) callerVocabularyPayload();
+        payload.put("StageExaminName", "中考");
+
+        String html = renderer.render(unmodeledOutlookContent(payload), Instant.parse("2026-05-13T02:08:54.657335Z"));
+
+        assertThat(html)
+                .contains("<title>中考英语临考词汇突击潜力展望报告</title>")
+                .contains("<h1 class=\"report-title\">中考英语临考词汇突击潜力展望报告</h1>")
+                .doesNotContain("<title>高考英语临考词汇突击潜力展望报告</title>")
+                .doesNotContain("<h1 class=\"report-title\">高考英语临考词汇突击潜力展望报告</h1>");
+    }
+
+    /**
+     * 覆盖展望报告主标题安全渲染场景,当 StageExaminName 包含 HTML 片段时,应转义后写入标题。
+     */
+    @Test
+    void renderEscapesStageExaminNameInDynamicReportTitle() throws Exception {
+        ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
+        ObjectNode payload = (ObjectNode) callerVocabularyPayload();
+        payload.put("StageExaminName", "注入考试<script>alert(1)</script>");
+
+        String html = renderer.render(unmodeledOutlookContent(payload), Instant.parse("2026-05-13T02:08:54.657335Z"));
+
+        assertThat(html)
+                .contains("<title>注入考试&lt;script&gt;alert(1)&lt;/script&gt;英语临考词汇突击潜力展望报告</title>")
+                .contains("<h1 class=\"report-title\">注入考试&lt;script&gt;alert(1)&lt;/script&gt;英语临考词汇突击潜力展望报告</h1>")
+                .doesNotContain("<title>注入考试<script>alert(1)</script>英语临考词汇突击潜力展望报告</title>")
+                .doesNotContain("<h1 class=\"report-title\">注入考试<script>alert(1)</script>英语临考词汇突击潜力展望报告</h1>");
+    }
+
     @Test
     void renderUsesCompactModuleOneCardLayout() throws Exception {
         ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);