Explorar el Código

Merge branch 'fix/outlook-report-student-vocabulary' of jyx/dcjxb.microservice into master

金逸霄 hace 1 semana
padre
commit
4479ac919f

+ 2 - 0
abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java

@@ -550,6 +550,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
         requireOutlookIntegralField(payload, "StudentStage");
         requireOutlookTextualField(payload, "StageName");
         requireOutlookIntegralField(payload, "StageVocabulary");
+        requireOutlookIntegralField(payload, "StudentVocabulary");
         requireOutlookTextualField(payload, "StageExaminName");
         requireOutlookIntegralField(payload, "StageImportant");
         requireOutlookArrayField(payload, "StudentWordsLatest");
@@ -833,6 +834,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
             case "studentStage" -> "StudentStage";
             case "stageName" -> "StageName";
             case "stageVocabulary" -> "StageVocabulary";
+            case "studentVocabulary" -> "StudentVocabulary";
             case "stageExaminName" -> "StageExaminName";
             case "stageImportant" -> "StageImportant";
             case "studentWordsLatest" -> "StudentWordsLatest";

+ 1 - 0
abilities/exam-sprint/application/src/main/resources/warmup/outlook-exam-sprint-report.json

@@ -3,6 +3,7 @@
   "StudentStage": 2,
   "StageName": "初中",
   "StageVocabulary": 2400,
+  "StudentVocabulary": 997,
   "StageExaminName": "中考",
   "StageImportant": 300,
   "StudentWordsLatest": [

+ 35 - 3
abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java

@@ -704,6 +704,24 @@ class ExamSprintReportApplicationServiceTest {
         assertCreateOutlookReportSyncRejectsInvalidPayload(OBJECT_MAPPER.nullNode());
     }
 
+    /** 覆盖展望报告新增必填字段场景,当 StudentVocabulary 缺失时,应在保存前校验失败。 */
+    @Test
+    void createOutlookReportSyncRejectsMissingStudentVocabularyBeforeSaving() throws Exception {
+        ObjectNode invalidPayload = callerVocabularyPayload();
+        invalidPayload.remove("StudentVocabulary");
+
+        assertCreateOutlookReportSyncRejectsInvalidPayload(invalidPayload, "StudentVocabulary", "studentVocabulary");
+    }
+
+    /** 覆盖展望报告新增必填字段类型场景,当 StudentVocabulary 不是整数时,应在保存前校验失败。 */
+    @Test
+    void createOutlookReportSyncRejectsNonIntegralStudentVocabularyBeforeSaving() throws Exception {
+        ObjectNode invalidPayload = callerVocabularyPayload();
+        invalidPayload.put("StudentVocabulary", "7");
+
+        assertCreateOutlookReportSyncRejectsInvalidPayload(invalidPayload, "StudentVocabulary", "studentVocabulary");
+    }
+
     /** 覆盖同步展望报告渲染失败场景,当 pipeline 生成失败时,应抛出下载不可用并保留失败报告。 */
     @Test
     void createOutlookReportSyncThrowsDownloadUnavailableAndKeepsFailedReportWhenGenerationFails() {
@@ -1153,6 +1171,7 @@ class ExamSprintReportApplicationServiceTest {
                   "StudentStage": 2,
                   "StageName": "初中",
                   "StageVocabulary": 10,
+                  "StudentVocabulary": 4,
                   "StageExaminName": "中考",
                   "StageImportant": 3,
                   "StudentWordsLatest": [
@@ -1347,6 +1366,13 @@ class ExamSprintReportApplicationServiceTest {
     }
 
     private void assertCreateOutlookReportSyncRejectsInvalidPayload(JsonNode payload) {
+        assertCreateOutlookReportSyncRejectsInvalidPayload(payload, null, null);
+    }
+
+    private void assertCreateOutlookReportSyncRejectsInvalidPayload(
+            JsonNode payload,
+            String expectedMessageField,
+            String forbiddenMessageField) {
         TestRepository repository = new TestRepository();
         boolean[] dispatched = {false};
         DefaultExamSprintReportApplicationService service = service(
@@ -1354,9 +1380,15 @@ class ExamSprintReportApplicationServiceTest {
                 reportId -> dispatched[0] = true,
                 new TestStorage());
 
-        assertThatThrownBy(() -> service.createOutlookReportSync(payload))
-                .isInstanceOf(BusinessException.class)
-                .extracting(exception -> ((BusinessException) exception).getErrorCode())
+        var throwableAssert = assertThatThrownBy(() -> service.createOutlookReportSync(payload))
+                .isInstanceOf(BusinessException.class);
+        if (expectedMessageField != null) {
+            throwableAssert.hasMessageContaining(expectedMessageField);
+        }
+        if (forbiddenMessageField != null) {
+            throwableAssert.hasMessageNotContaining(forbiddenMessageField);
+        }
+        throwableAssert.extracting(exception -> ((BusinessException) exception).getErrorCode())
                 .isEqualTo(ErrorCode.VALIDATION_ERROR);
 
         assertThat(repository.storage).isEmpty();

+ 1 - 0
abilities/exam-sprint/contracts/src/main/java/cn/yunzhixue/ability/center/examsprint/contracts/report/OutlookExamSprintReportPayload.java

@@ -17,6 +17,7 @@ public record OutlookExamSprintReportPayload(
         @JsonProperty("StudentStage") @NotNull @Min(0) Integer studentStage,
         @JsonProperty("StageName") @NotBlank String stageName,
         @JsonProperty("StageVocabulary") @NotNull @Min(0) Integer stageVocabulary,
+        @JsonProperty("StudentVocabulary") @NotNull @Min(0) Integer studentVocabulary,
         @JsonProperty("StageExaminName") @NotBlank String stageExaminName,
         @JsonProperty("StageImportant") @NotNull @Min(0) Integer stageImportant,
         @JsonProperty("StudentWordsLatest") @NotEmpty List<@NotNull @Valid StudentWordLatest> studentWordsLatest,

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

@@ -79,9 +79,10 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
 
     private OutlookReportViewModel adaptPayload(OutlookExamSprintReportPayload payload) {
         int stageVocabulary = payload.stageVocabulary();
-        int masteredWordCount = payload.mastedWordCount();
-        int unmasteredWordCount = payload.unMastedWordCount();
+        int masteredWordCount = payload.studentVocabulary();
+        int unmasteredWordCount = Math.max(0, stageVocabulary - masteredWordCount);
         int masteryPercent = (int) Math.round(percentage(masteredWordCount, stageVocabulary));
+        int pastPaperUnknownWordCount = Math.max(0, payload.testPaperWordCount() - payload.testPaperMastedWordCount());
         List<OutlookExamSprintReportPayload.StudentWordLatest> words = participatingWords(payload);
 
         int basicUpper = (int) Math.ceil(stageVocabulary * 0.2d);
@@ -99,7 +100,7 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
                         "优先补齐高频和核心常考词。"),
                 new PastPaperVocabularyChart(
                         payload.testPaperWordCount(),
-                        payload.testPaperUnMasterWordCount(),
+                        pastPaperUnknownWordCount,
                         null),
                 new HighFrequencyVocabularyChart(
                         roundedMasteryPercent(words, 0, basicUpper),
@@ -650,7 +651,8 @@ public class ClasspathOutlookExamSprintReportRenderer implements ExamSprintRepor
         if (denominator <= 0) {
             return 0d;
         }
-        return (numerator * 100d) / denominator;
+        double calculatedPercent = (numerator * 100d) / denominator;
+        return Math.max(0d, Math.min(100d, calculatedPercent));
     }
 
     private String describeArc(double centerX, double centerY, double radius, double startAngle, double endAngle) {

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

@@ -299,6 +299,7 @@ class OpenHtmlToPdfExamSprintReportPdfGeneratorTest {
                   "StudentStage": 2,
                   "StageName": "初中",
                   "StageVocabulary": 10,
+                  "StudentVocabulary": 4,
                   "StageExaminName": "中考",
                   "StageImportant": 3,
                   "StudentWordsLatest": [

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

@@ -101,9 +101,11 @@ class ClasspathOutlookExamSprintReportRendererTest {
 
         assertThat(html)
                 .contains("考纲总量:<span class='highlight'>10词</span>")
-                .contains("已掌握:<span class='highlight'>4词(40.00%)</span>")
-                .contains("未掌握:<span class='highlight'>6词(60.00%)</span>")
-                .contains("真题总词:5词 | 生词量:3词(60.00%)")
+                .contains("已掌握:<span class='highlight'>7词(70.00%)</span>")
+                .contains("未掌握:<span class='highlight'>3词(30.00%)</span>")
+                .contains("真题总词:5词 | 生词量:1词(20.00%)")
+                .doesNotContain("已掌握:<span class='highlight'>4词(40.00%)</span>")
+                .doesNotContain("真题总词:5词 | 生词量:3词(60.00%)")
                 .doesNotContain("冲刺后生词:")
                 .doesNotContain("先压降真题生词占比。")
                 .doesNotContain("拉分词是提分核心突破项")
@@ -480,6 +482,7 @@ class ClasspathOutlookExamSprintReportRendererTest {
                   "StudentStage": 2,
                   "StageName": "初中",
                   "StageVocabulary": 10,
+                  "StudentVocabulary": 4,
                   "StageExaminName": "中考",
                   "StageImportant": 3,
                   "StudentWordsLatest": [
@@ -526,6 +529,9 @@ class ClasspathOutlookExamSprintReportRendererTest {
 
     private JsonNode callerVocabularyPayloadWithMismatchedPastPaperCounts() throws Exception {
         ObjectNode payload = (ObjectNode) callerVocabularyPayloadWithComplex(true);
+        payload.put("StudentVocabulary", 7);
+        payload.put("MastedWordCount", 4);
+        payload.put("UnMastedWordCount", 6);
         payload.put("TestPaperMastedWordCount", 4);
         payload.put("TestPaperUnMasterWordCount", 3);
         return payload;
@@ -593,6 +599,7 @@ class ClasspathOutlookExamSprintReportRendererTest {
         ObjectNode payload = (ObjectNode) callerVocabularyPayload();
         int boundedMasteryPercent = Math.max(0, Math.min(100, masteryPercent));
         payload.put("StageVocabulary", 100);
+        payload.put("StudentVocabulary", boundedMasteryPercent);
         payload.put("MastedWordCount", boundedMasteryPercent);
         payload.put("UnMastedWordCount", 100 - boundedMasteryPercent);
         return payload;

+ 1 - 0
ability-center-runtime/scripts/outlook-report-demo.sh

@@ -25,6 +25,7 @@ http_code="$({
   "StudentStage": 2,
   "StageName": "初中",
   "StageVocabulary": 10,
+  "StudentVocabulary": 4,
   "StageExaminName": "中考",
   "StageImportant": 3,
   "StudentWordsLatest": [

+ 1 - 0
ability-center-runtime/src/test/resources/requests/exam-sprint-outlook-report-request.json

@@ -3,6 +3,7 @@
   "StudentStage": 2,
   "StageName": "初中",
   "StageVocabulary": 10,
+  "StudentVocabulary": 4,
   "StageExaminName": "中考",
   "StageImportant": 3,
   "StudentWordsLatest": [