Selaa lähdekoodia

Merge branch 'plan/exam-sprint-outlook-report-complex-toggle' of jyx/dcjxb.microservice into master

金逸霄 1 viikko sitten
vanhempi
commit
a65fb118f6

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

@@ -60,7 +60,7 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
                     .replace("{{highFrequencyVocabularySection}}", renderHighFrequencyVocabularyChart(reportPayload.highFrequencyVocabularySection()))
                     .replace("{{frequencyBandSection}}", renderVocabularyFrequencyBandChart(reportPayload.frequencyBandSection()))
                     .replace("{{studySuggestionSection}}", renderStudySuggestionSection(reportPayload.studyPlan()))
-                    .replace("{{caseStudySection}}", renderScoreImprovementCaseStudy(reportPayload.caseStudy()));
+                    .replace("{{moduleThreeSection}}", renderModuleThreeSection(payloadContract.complex(), reportPayload.caseStudy()));
         } catch (IOException exception) {
             throw new UncheckedIOException("Failed to load outlook exam sprint report template", exception);
         } catch (Exception exception) {
@@ -510,6 +510,18 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
                 .toString();
     }
 
+    private String renderModuleThreeSection(boolean complex, ScoreImprovementCaseStudy caseStudy) {
+        if (!complex) {
+            return "";
+        }
+        return new StringBuilder()
+                .append("<div class='section'>")
+                .append("<h2 class='section-title'>模块三:上届学员提分案例</h2>")
+                .append(renderScoreImprovementCaseStudy(caseStudy))
+                .append("</div>")
+                .toString();
+    }
+
     private String renderScoreImprovementCaseStudy(ScoreImprovementCaseStudy caseStudy) {
         double progressPercent = Math.max(0d, Math.min(100d, caseStudy.hitRatePercent()));
         double endAngle = -90 + (progressPercent * 3.6);

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

@@ -372,10 +372,7 @@
         {{studySuggestionSection}}
     </div>
 
-    <div class="section">
-        <h2 class="section-title">模块三:上届学员提分案例</h2>
-        {{caseStudySection}}
-    </div>
+    {{moduleThreeSection}}
 </div>
 </body>
 </html>

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

@@ -7,6 +7,7 @@ import cn.yunzhixue.ability.center.examsprint.infrastructure.report.rendering.ac
 import cn.yunzhixue.ability.center.examsprint.infrastructure.report.rendering.outlook.ClasspathOutlookExamSprintReportRenderer;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.pdfbox.pdmodel.PDDocument;
 import org.apache.pdfbox.text.PDFTextStripper;
 import org.junit.jupiter.api.Test;
@@ -34,7 +35,7 @@ class OpenHtmlToPdfExamSprintReportPdfGeneratorTest {
         ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
         OpenHtmlToPdfExamSprintReportPdfGenerator pdfGenerator = new OpenHtmlToPdfExamSprintReportPdfGenerator();
 
-        String html = renderer.render(unmodeledOutlookContent(samplePayload()), Instant.parse("2026-01-03T08:00:00Z"));
+        String html = renderer.render(unmodeledOutlookContent(samplePayloadWithComplex(true)), Instant.parse("2026-01-03T08:00:00Z"));
         byte[] pdfBytes = pdfGenerator.generate(html);
 
         assertThat(pdfBytes).isNotEmpty();
@@ -65,6 +66,34 @@ class OpenHtmlToPdfExamSprintReportPdfGeneratorTest {
         }
     }
 
+    /**
+     * 覆盖官方上游词汇 payload 在 Complex=false 时生成 Outlook PDF,应隐藏模块三案例文本。
+     */
+    @Test
+    void generateCreatesPdfSmokeWithoutOutlookModuleThreeWhenComplexIsFalse() throws Exception {
+        ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
+        OpenHtmlToPdfExamSprintReportPdfGenerator pdfGenerator = new OpenHtmlToPdfExamSprintReportPdfGenerator();
+
+        String html = renderer.render(unmodeledOutlookContent(samplePayloadWithComplex(false)), Instant.parse("2026-01-03T08:00:00Z"));
+        byte[] pdfBytes = pdfGenerator.generate(html);
+
+        assertThat(pdfBytes).isNotEmpty();
+        assertThat(new String(pdfBytes, 0, 4, StandardCharsets.ISO_8859_1)).isEqualTo("%PDF");
+
+        try (PDDocument document = PDDocument.load(pdfBytes)) {
+            assertThat(document.getNumberOfPages()).isGreaterThanOrEqualTo(1);
+            String normalizedText = new PDFTextStripper().getText(document).replaceAll("\\s+", "");
+            assertThat(normalizedText)
+                    .contains("高考英语临考词汇突击潜力展望报告")
+                    .contains("模块一:个人学情分析")
+                    .contains("模块二:科学备考建议")
+                    .contains("词频区间掌握度")
+                    .doesNotContain("模块三:上届学员提分案例")
+                    .doesNotContain("王雷宇")
+                    .doesNotContain("+19分");
+        }
+    }
+
     /**
      * 覆盖学习成果报告 HTML 生成 PDF 时,应产出可解析 PDF 并包含成果报告关键文本。
      */
@@ -298,4 +327,10 @@ class OpenHtmlToPdfExamSprintReportPdfGeneratorTest {
                 """);
     }
 
+    private JsonNode samplePayloadWithComplex(boolean complex) throws Exception {
+        ObjectNode payload = (ObjectNode) samplePayload();
+        payload.put("Complex", complex);
+        return payload;
+    }
+
 }

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

@@ -32,7 +32,7 @@ class ClasspathOutlookExamSprintReportRendererTest {
     void renderBuildsOutlookHtmlAlignedWithDesignDraftDynamicStructure() throws Exception {
         ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
 
-        String html = renderer.render(unmodeledOutlookContent(callerVocabularyPayload()), Instant.parse("2026-01-03T08:00:00Z"));
+        String html = renderer.render(unmodeledOutlookContent(callerVocabularyPayloadWithComplex(true)), Instant.parse("2026-01-03T08:00:00Z"));
 
         assertThat(html)
                 .contains("class=\"analysis-table\"")
@@ -138,9 +138,46 @@ class ClasspathOutlookExamSprintReportRendererTest {
                 .contains("王雷宇")
                 .doesNotContain("{{syllabusMasterySection}}")
                 .doesNotContain("{{pastPaperVocabularySection}}")
+                .doesNotContain("{{moduleThreeSection}}")
                 .doesNotContain("{{studySuggestionSection}}");
     }
 
+    /**
+     * 覆盖展望报告模块三按 Complex 开关展示的场景,当 Complex=false 时,模块三标题和案例内容都不应出现在 HTML 中。
+     */
+    @Test
+    void renderHidesModuleThreeWhenComplexIsFalse() throws Exception {
+        ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
+
+        String html = renderer.render(unmodeledOutlookContent(callerVocabularyPayloadWithComplex(false)), Instant.parse("2026-01-03T08:00:00Z"));
+
+        assertThat(html)
+                .contains("模块一:个人学情分析")
+                .contains("模块二:科学备考建议")
+                .doesNotContain("模块三:上届学员提分案例")
+                .doesNotContain("class='student-case'")
+                .doesNotContain("王雷宇")
+                .doesNotContain("{{moduleThreeSection}}")
+                .doesNotContain("+19分");
+    }
+
+    /**
+     * 覆盖展望报告模块三按 Complex 开关展示的场景,当 Complex=true 时,模块三标题和案例内容应正常渲染。
+     */
+    @Test
+    void renderShowsModuleThreeWhenComplexIsTrue() throws Exception {
+        ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
+
+        String html = renderer.render(unmodeledOutlookContent(callerVocabularyPayloadWithComplex(true)), Instant.parse("2026-01-03T08:00:00Z"));
+
+        assertThat(html)
+                .contains("模块三:上届学员提分案例")
+                .contains("class='student-case'")
+                .contains("王雷宇")
+                .doesNotContain("{{moduleThreeSection}}")
+                .contains("+19分");
+    }
+
     /**
      * 覆盖官方词汇 payload 计算出 0 值区间时,对应柱状图高度应保持为 0 且不被最小高度兜底抬高。
      */
@@ -233,7 +270,7 @@ class ClasspathOutlookExamSprintReportRendererTest {
     void renderDeclaresBatikCjkFontFamilyOnEveryInlineSvg() throws Exception {
         ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
 
-        String html = renderer.render(unmodeledOutlookContent(callerVocabularyPayload()), Instant.parse("2026-01-03T08:00:00Z"));
+        String html = renderer.render(unmodeledOutlookContent(callerVocabularyPayloadWithComplex(true)), Instant.parse("2026-01-03T08:00:00Z"));
 
         assertThat(html)
                 .contains("<svg class='syllabus-donut-chart' font-family=\"'MiSans VF', MiSans, ReportFont, sans-serif\"")
@@ -383,7 +420,7 @@ class ClasspathOutlookExamSprintReportRendererTest {
     void renderCaseStudyRingAnchorsCenterCopyAsCircleCenteredLabelGroup() throws Exception {
         ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
 
-        String html = renderer.render(unmodeledOutlookContent(callerVocabularyPayload()), Instant.parse("2026-01-03T08:00:00Z"));
+        String html = renderer.render(unmodeledOutlookContent(callerVocabularyPayloadWithComplex(true)), Instant.parse("2026-01-03T08:00:00Z"));
 
         String caseStudySvg = extractSvgByAriaLabel(html, "上届学员提分案例图示");
 
@@ -471,6 +508,12 @@ class ClasspathOutlookExamSprintReportRendererTest {
                 """);
     }
 
+    private JsonNode callerVocabularyPayloadWithComplex(boolean complex) throws Exception {
+        ObjectNode payload = (ObjectNode) callerVocabularyPayload();
+        payload.put("Complex", complex);
+        return payload;
+    }
+
     private JsonNode callerVocabularyPayloadWithZeroValueBars() throws Exception {
         ObjectNode payload = (ObjectNode) callerVocabularyPayload();
         payload.withArray("StudentWordsLatest").forEach(node -> {
@@ -483,7 +526,7 @@ class ClasspathOutlookExamSprintReportRendererTest {
     }
 
     private JsonNode callerVocabularyPayloadWithMismatchedPastPaperCounts() throws Exception {
-        ObjectNode payload = (ObjectNode) callerVocabularyPayload();
+        ObjectNode payload = (ObjectNode) callerVocabularyPayloadWithComplex(true);
         payload.put("TestPaperMastedWordCount", 4);
         payload.putArray("TestPaperUnMasterWords")
                 .add("lot")
@@ -493,7 +536,7 @@ class ClasspathOutlookExamSprintReportRendererTest {
     }
 
     private JsonNode normalizedCallerVocabularyPayloadWithStudentNameAliases() throws Exception {
-        ObjectNode payload = (ObjectNode) callerVocabularyPayload();
+        ObjectNode payload = (ObjectNode) callerVocabularyPayloadWithComplex(true);
         payload.put("StudentName", "20260318测试");
         payload.put("studentName", "20260318测试");
         return payload;

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

@@ -88,13 +88,15 @@ class OutlookExamSprintReportTemplateCompatibilityTest {
     }
 
     @Test
-    void templateUsesH2ForAllModuleSectionTitles() throws Exception {
+    void templateUsesH2ForStaticModuleSectionTitlesAndDynamicModuleThreeSlot() throws Exception {
         String normalizedTemplate = normalizeWhitespace(loadTemplate());
 
         assertThat(normalizedTemplate)
                 .contains("<h2 class=\"section-title\">模块一:个人学情分析</h2>")
                 .contains("<h2 class=\"section-title\">模块二:科学备考建议</h2>")
-                .contains("<h2 class=\"section-title\">模块三:上届学员提分案例</h2>")
+                .contains("{{moduleThreeSection}}")
+                .containsPattern("模块二:科学备考建议</h2>\\s*\\{\\{studySuggestionSection}}\\s*</div>\\s*\\{\\{moduleThreeSection}}\\s*</div>")
+                .doesNotContain("<h2 class=\"section-title\">模块三:上届学员提分案例</h2>")
                 .doesNotContain("<div class=\"section-title\">");
     }
 
@@ -107,7 +109,7 @@ class OutlookExamSprintReportTemplateCompatibilityTest {
                 .contains("{{pastPaperVocabularySection}}")
                 .contains("{{highFrequencyVocabularySection}}")
                 .contains("{{frequencyBandSection}}")
-                .contains("{{caseStudySection}}")
+                .contains("{{moduleThreeSection}}")
                 .contains("{{studySuggestionSection}}")
                 .contains("<table class=\"analysis-table\" role=\"presentation\">")
                 .contains("<tr class=\"analysis-row\">")