Explorar o código

Merge branch 'fix/展望报告渲染别名反序列化' of jyx/dcjxb.microservice into master

金逸霄 hai 2 semanas
pai
achega
6daf5d0246

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

@@ -7,6 +7,7 @@ import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
 import cn.yunzhixue.ability.center.examsprint.domain.report.UnmodeledReportContent;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.springframework.core.io.ClassPathResource;
 import org.springframework.stereotype.Component;
 
@@ -51,7 +52,7 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
             throw new IllegalArgumentException("Outlook renderer requires unmodeled OUTLOOK JsonNode content");
         }
         try {
-            OutlookExamSprintReportPayload payloadContract = objectMapper.treeToValue(payload, OutlookExamSprintReportPayload.class);
+            OutlookExamSprintReportPayload payloadContract = objectMapper.treeToValue(payloadForDeserialization(payload), OutlookExamSprintReportPayload.class);
             OutlookReportViewModel reportPayload = adaptPayload(payloadContract);
             return loadTemplate()
                     .replace("{{syllabusMasterySection}}", renderSyllabusMasteryChart(reportPayload.syllabusMasterySection()))
@@ -67,6 +68,15 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
         }
     }
 
+    private JsonNode payloadForDeserialization(JsonNode payload) {
+        if (!payload.has("StudentName") || !payload.has("studentName")) {
+            return payload;
+        }
+        ObjectNode deserializationPayload = ((ObjectNode) payload).deepCopy();
+        deserializationPayload.remove("studentName");
+        return deserializationPayload;
+    }
+
     private OutlookReportViewModel adaptPayload(OutlookExamSprintReportPayload payload) {
         int stageVocabulary = payload.stageVocabulary();
         int masteredWordCount = payload.mastedWordCount();

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

@@ -115,6 +115,27 @@ class ClasspathOutlookExamSprintReportRendererTest {
                 .contains("+19分");
     }
 
+    /**
+     * 覆盖应用层归一化后的官方词汇 payload 同时携带 StudentName/studentName 时,真实 renderer 应继续完成反序列化和 HTML 渲染。
+     */
+    @Test
+    void renderAcceptsNormalizedOutlookPayloadWithCanonicalAndLowercaseStudentNameAliases() throws Exception {
+        ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
+
+        String html = renderer.render(
+                unmodeledOutlookContent(normalizedCallerVocabularyPayloadWithStudentNameAliases()),
+                Instant.parse("2026-01-03T08:00:00Z"));
+
+        assertThat(html)
+                .contains("考纲总量:<span class='highlight'>10词</span>")
+                .contains("真题总词:5词 | 生词量:3词(60.00%)")
+                .contains("7 天提分冲刺是首选节奏")
+                .contains("王雷宇")
+                .doesNotContain("{{syllabusMasterySection}}")
+                .doesNotContain("{{pastPaperVocabularySection}}")
+                .doesNotContain("{{studySuggestionSection}}");
+    }
+
     /**
      * 覆盖官方词汇 payload 计算出 0 值区间时,对应柱状图高度应保持为 0 且不被最小高度兜底抬高。
      */
@@ -347,6 +368,13 @@ class ClasspathOutlookExamSprintReportRendererTest {
         return payload;
     }
 
+    private JsonNode normalizedCallerVocabularyPayloadWithStudentNameAliases() throws Exception {
+        ObjectNode payload = (ObjectNode) callerVocabularyPayload();
+        payload.put("StudentName", "20260318测试");
+        payload.put("studentName", "20260318测试");
+        return payload;
+    }
+
     private UnmodeledReportContent unmodeledOutlookContent(JsonNode payload) {
         return new UnmodeledReportContent(ReportType.OUTLOOK, payload);
     }