diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/constant/ExamWordsConstant.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/constant/ExamWordsConstant.java index ed6260d..c941e73 100644 --- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/constant/ExamWordsConstant.java +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/constant/ExamWordsConstant.java @@ -9,9 +9,19 @@ public class ExamWordsConstant { public static final int GRADE_3 = 3; public static final int GRADE_4 = 4; public static final int GRADE_5 = 5; - public static final int GRADE_6 = 7; - public static final int GRADE_7 = 8; - public static final int GRADE_8 = 9; + public static final int GRADE_6 = 6; + public static final int GRADE_7 = 7; + public static final int GRADE_8 = 8; + + public static final String GRADE_1_NAME = "一年级"; + public static final String GRADE_2_NAME = "二年级"; + public static final String GRADE_3_NAME = "三年级"; + public static final String GRADE_4_NAME = "四年级"; + public static final String GRADE_5_NAME = "五年级"; + public static final String GRADE_6_NAME = "六年级"; + public static final String GRADE_7_NAME = "初一"; + public static final String GRADE_8_NAME = "初二"; + public static final int ZONE_A_SIZE = 10; public static final int ZONE_B_SIZE = 20; @@ -105,4 +115,18 @@ public class ExamWordsConstant { default -> 0; }; } + + public static String getGradeName(int gradeId) { + return switch (gradeId) { + case GRADE_1 -> GRADE_1_NAME; + case GRADE_2 -> GRADE_2_NAME; + case GRADE_3 -> GRADE_3_NAME; + case GRADE_4 -> GRADE_4_NAME; + case GRADE_5 -> GRADE_5_NAME; + case GRADE_6 -> GRADE_6_NAME; + case GRADE_7 -> GRADE_7_NAME; + case GRADE_8 -> GRADE_8_NAME; + default -> ""; + }; + } } diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/controller/ExamWordsController.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/controller/ExamWordsController.java index 074d177..2192dd6 100644 --- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/controller/ExamWordsController.java +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/controller/ExamWordsController.java @@ -116,7 +116,7 @@ public class ExamWordsController { .correctWordCount(examWordsJudgeResultDO.getCorrectWordCount()) .wrongWordCount(examWordsJudgeResultDO.getWrongWordCount()) .isFinished(examWordsJudgeResultDO.getIsFinished()) - .errorMsg(examWordsJudgeResultDO.getMsg()) + .msg(examWordsJudgeResultDO.getMsg()) .build() ).toList(); return PageResponse.success(list, page, total, size); diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/controller/StudentController.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/controller/StudentController.java index c5e222d..9c1c9d7 100644 --- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/controller/StudentController.java +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/controller/StudentController.java @@ -60,12 +60,13 @@ public class StudentController { StudentDO studentById = studentService.getStudentById(studentId); ClassDO classById = classService.findClassById(studentById.getClassId()); GradeDO byClassId = gradeService.findByClassId(studentById.getGradeId()); - + GradeDO actualGradeById = gradeService.findByClassId(studentById.getActualGradeId()); FindStudentDetailRspVO findStudentDetailRspVO = FindStudentDetailRspVO.builder() .id(studentById.getId()) .name(studentById.getName()) .className(classById.getTitle()) .gradeName(byClassId.getTitle()) + .actualGrade(actualGradeById != null ? actualGradeById.getTitle() : "") .build(); return Response.success(findStudentDetailRspVO); diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/dataobject/ExamWordsJudgeResultDO.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/dataobject/ExamWordsJudgeResultDO.java index 015f30f..730182c 100644 --- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/dataobject/ExamWordsJudgeResultDO.java +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/dataobject/ExamWordsJudgeResultDO.java @@ -13,6 +13,7 @@ import java.util.List; @Data @Builder public class ExamWordsJudgeResultDO { + private Integer id; private String ansSheetPath; @@ -27,8 +28,6 @@ public class ExamWordsJudgeResultDO { private Integer isFinished; - private Integer ansGradeId; - private LocalDateTime startDate; private List correctWordIds; diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/dataobject/StudentDO.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/dataobject/StudentDO.java index a570de8..d971c31 100644 --- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/dataobject/StudentDO.java +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/dataobject/StudentDO.java @@ -21,6 +21,8 @@ public class StudentDO { private Integer gradeId; + private Integer actualGradeId; + private Integer isDeleted; private LocalDateTime startTime; diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/mapper/GradeUnitDOMapper.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/mapper/GradeUnitDOMapper.java index 6334095..3042d2a 100644 --- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/mapper/GradeUnitDOMapper.java +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/mapper/GradeUnitDOMapper.java @@ -11,6 +11,8 @@ public interface GradeUnitDOMapper { GradeUnitDO selectByUnitId(@Param("unitId") Integer unitId); + List selectByUnitIds(@Param("unitIds") List unitIds); + int insert(GradeUnitDO record); int deleteByUnitId(@Param("unitId") Integer unitId); diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/mapper/StudentDOMapper.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/mapper/StudentDOMapper.java index ce85bcc..a99c689 100644 --- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/mapper/StudentDOMapper.java +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/domain/mapper/StudentDOMapper.java @@ -21,4 +21,6 @@ public interface StudentDOMapper { void deleteById(Integer id); int selectStudentCountByClassId(@Param("classId") Integer classId); + + int updateStudentActualGradeId(@Param("studentId") Integer studentId, @Param("gradeId") Integer gradeId); } \ No newline at end of file diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/bo/exam/ActionType.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/bo/exam/ActionType.java new file mode 100644 index 0000000..eb1df65 --- /dev/null +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/bo/exam/ActionType.java @@ -0,0 +1,9 @@ +package com.yinlihupo.enlish.service.model.bo.exam; + +public enum ActionType { + PASS, // 保持当前进度 + UPGRADE, // 升级 + DOWNGRADE, // 降级回填 + STAY_AND_REVIEW, // 保持年级但进入复习模式 + TRIGGER_RETEST // 触发熔断二测 +} \ No newline at end of file diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/bo/exam/DiagnosisResult.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/bo/exam/DiagnosisResult.java new file mode 100644 index 0000000..689cf38 --- /dev/null +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/bo/exam/DiagnosisResult.java @@ -0,0 +1,17 @@ +package com.yinlihupo.enlish.service.model.bo.exam; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class DiagnosisResult { + + int determinedLevel; // 系统判定的真实等级 + ActionType actionType; // 建议的系统动作 + String message; // 展示给用户的文案 +} diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/bo/exam/ZoneStats.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/bo/exam/ZoneStats.java new file mode 100644 index 0000000..c4e2bab --- /dev/null +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/bo/exam/ZoneStats.java @@ -0,0 +1,19 @@ +package com.yinlihupo.enlish.service.model.bo.exam; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class ZoneStats { + + private int gradeId; + private int totalCount; + private int correctCount; + private double accuracy; + +} \ No newline at end of file diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/vo/exam/ExamWordsResultRspVO.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/vo/exam/ExamWordsResultRspVO.java index 420e59c..2852b2b 100644 --- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/vo/exam/ExamWordsResultRspVO.java +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/vo/exam/ExamWordsResultRspVO.java @@ -28,5 +28,5 @@ public class ExamWordsResultRspVO { private LocalDateTime startDate; - private String errorMsg; + private String msg; } diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/vo/student/FindStudentDetailRspVO.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/vo/student/FindStudentDetailRspVO.java index 21dd87a..e0b868b 100644 --- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/vo/student/FindStudentDetailRspVO.java +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/vo/student/FindStudentDetailRspVO.java @@ -15,4 +15,5 @@ public class FindStudentDetailRspVO { private String name; private String className; private String gradeName; + private String actualGrade; } diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/exam/ExamWordsServiceImpl.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/exam/ExamWordsServiceImpl.java index f49f84f..883823f 100644 --- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/exam/ExamWordsServiceImpl.java +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/exam/ExamWordsServiceImpl.java @@ -46,7 +46,7 @@ public class ExamWordsServiceImpl implements ExamWordsService { @Override @Transactional(rollbackFor = RuntimeException.class) - public ExamWordsDO generateExamWords(Integer gradeId, Integer level, Integer studentId, Integer type) { + public ExamWordsDO generateExamWords(Integer gradeId, Integer level, Integer studentId, Integer type) { ExamWordsDO examWordsDO; @@ -64,7 +64,8 @@ public class ExamWordsServiceImpl implements ExamWordsService { private ExamWordsDO generateBaselineExamWords(Integer studentId) { StudentDO studentDO = studentDOMapper.selectStudentById(studentId); - Integer gradeId = studentDO.getGradeId(); + + Integer gradeId = studentDO.getActualGradeId(); int zoneA = ExamWordsConstant.getZoneA(gradeId); int zoneASize = ExamWordsConstant.ZONE_A_SIZE; diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/judge/ExamWordsJudgeServiceImpl.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/judge/ExamWordsJudgeServiceImpl.java index 5dffabf..d75565f 100644 --- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/judge/ExamWordsJudgeServiceImpl.java +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/judge/ExamWordsJudgeServiceImpl.java @@ -1,15 +1,13 @@ package com.yinlihupo.enlish.service.service.judge; -import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsDO; -import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsJudgeResultDO; -import com.yinlihupo.enlish.service.domain.dataobject.StudentExamWordsDO; -import com.yinlihupo.enlish.service.domain.dataobject.WordMasteryLogDO; -import com.yinlihupo.enlish.service.domain.mapper.ExamWordsDOMapper; -import com.yinlihupo.enlish.service.domain.mapper.ExamWordsJudgeResultDOMapper; -import com.yinlihupo.enlish.service.domain.mapper.StudentExamWordsDOMapper; -import com.yinlihupo.enlish.service.domain.mapper.WordMasteryLogDOMapper; +import com.yinlihupo.enlish.service.constant.ExamWordsConstant; +import com.yinlihupo.enlish.service.domain.dataobject.*; +import com.yinlihupo.enlish.service.domain.mapper.*; import com.yinlihupo.enlish.service.model.bo.CoordinatesXY; import com.yinlihupo.enlish.service.model.bo.StudentExamId; +import com.yinlihupo.enlish.service.model.bo.exam.ActionType; +import com.yinlihupo.enlish.service.model.bo.exam.DiagnosisResult; +import com.yinlihupo.enlish.service.model.bo.exam.ZoneStats; import com.yinlihupo.enlish.service.service.ExamWordsJudgeService; import com.yinlihupo.enlish.service.utils.PngUtil; import jakarta.annotation.Resource; @@ -20,8 +18,8 @@ import org.springframework.transaction.annotation.Transactional; import java.io.File; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; @Service @Slf4j @@ -36,6 +34,12 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService { private ExamWordsDOMapper examWordsDOMapper; @Resource private WordMasteryLogDOMapper wordMasteryLogDOMapper; + @Resource + private VocabularyBankDOMapper vocabularyBankDOMapper; + @Resource + private GradeUnitDOMapper gradeUnitDOMapper; + @Resource + private StudentDOMapper studentDOMapper; @Value("${templates.data}") private String tessdataPath; @@ -45,91 +49,266 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService { public void judgeExamWords(int count) { List examWordsJudgeResultDOS = examWordsJudgeResultDOMapper.selectUnfinishedExamWordsJudgeResultDOList(count); for (ExamWordsJudgeResultDO examWordsJudgeResultDO : examWordsJudgeResultDOS) { - String ansSheetPath = examWordsJudgeResultDO.getAnsSheetPath(); - List coordinatesXIES = PngUtil.analysisXY(ansSheetPath); - // 从图片中获取学生 id 和考试 id - StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(ansSheetPath, tessdataPath, coordinatesXIES); - Integer examWordsJudgeResultDOId = examWordsJudgeResultDO.getId(); - if (studentExamId == null) { - examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未识别学生 id 和考试 id"); - continue; - } + try { + String ansSheetPath = examWordsJudgeResultDO.getAnsSheetPath(); + List coordinatesXIES = PngUtil.analysisXY(ansSheetPath); + // 从图片中获取学生 id 和考试 id + StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(ansSheetPath, tessdataPath, coordinatesXIES); + Integer examWordsJudgeResultDOId = examWordsJudgeResultDO.getId(); + if (studentExamId == null) { + examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未识别学生 id 和考试 id"); + continue; + } - Integer studentId = studentExamId.getStudentId(); - Integer examWordsId = studentExamId.getExamId(); - StudentExamWordsDO studentExamWordsDO = studentExamWordsDOMapper.selectByStudentIdAndExamWordsId(studentId, examWordsId); - if (studentExamWordsDO == null) { - examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未找到学生 id 和考试 id 对应的考试记录"); - continue; - } - log.info("studentId:{},examWordsId:{}", studentId, examWordsId); - if (studentExamWordsDO.getIsCompleted() == 1) { - examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "考试记录此前已识别"); - continue; - } + Integer studentId = studentExamId.getStudentId(); + Integer examWordsId = studentExamId.getExamId(); + StudentExamWordsDO studentExamWordsDO = studentExamWordsDOMapper.selectByStudentIdAndExamWordsId(studentId, examWordsId); + if (studentExamWordsDO == null) { + examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未找到学生 id 和考试 id 对应的考试记录"); + continue; + } + log.info("studentId:{},examWordsId:{}", studentId, examWordsId); + if (studentExamWordsDO.getIsCompleted() == 1) { + examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "考试记录此前已识别"); + continue; + } - ExamWordsDO examWordsDO = examWordsDOMapper.selectById(examWordsId); - if(examWordsDO == null) { - examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未找到考试"); - continue; - } + ExamWordsDO examWordsDO = examWordsDOMapper.selectById(examWordsId); + if(examWordsDO == null) { + examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未找到考试"); + continue; + } - List wordIds = examWordsDO.getWordIds(); - List unmemorizedWordIds = PngUtil.analyzePngForUnmemorizedWordIds(ansSheetPath, wordIds, coordinatesXIES); - List memorizedWordIds = wordIds.stream().filter(wordId -> !unmemorizedWordIds.contains(wordId)).toList(); + List wordIds = examWordsDO.getWordIds(); + List unmemorizedWordIds = PngUtil.analyzePngForUnmemorizedWordIds(ansSheetPath, wordIds, coordinatesXIES); + List memorizedWordIds = wordIds.stream().filter(wordId -> !unmemorizedWordIds.contains(wordId)).toList(); - ExamWordsJudgeResultDO wordsJudgeResultDO = ExamWordsJudgeResultDO.builder() - .id(examWordsJudgeResultDOId) - .studentId(studentId) - .examWordsId(examWordsId) - .correctWordIds(memorizedWordIds) - .wrongWordIds(unmemorizedWordIds) - .correctWordCount(memorizedWordIds.size()) - .wrongWordCount(unmemorizedWordIds.size()) - .isFinished(1) - .build(); - int updated = examWordsJudgeResultDOMapper.updateExamWordsJudgeResultDO(wordsJudgeResultDO); - if (updated != 1) { - examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "更新考试记录失败"); - continue; - } - log.info("更新考试记录成功"); + ExamWordsJudgeResultDO wordsJudgeResultDO = ExamWordsJudgeResultDO.builder() + .id(examWordsJudgeResultDOId) + .studentId(studentId) + .examWordsId(examWordsId) + .correctWordIds(memorizedWordIds) + .wrongWordIds(unmemorizedWordIds) + .correctWordCount(memorizedWordIds.size()) + .wrongWordCount(unmemorizedWordIds.size()) + .isFinished(1) + .build(); + // 判断考试等级 + judgeExamActualGrade(wordsJudgeResultDO, examWordsDO); - List wordMasteryLogDOS = new ArrayList<>(unmemorizedWordIds.stream().map(wordId -> WordMasteryLogDO.builder() - .wordId(wordId) - .studentId(studentId) - .reviewCount(1) - .memoryStrength(-0.5) - .update_time(LocalDateTime.now()) - .build() - ).toList()); + int updated = examWordsJudgeResultDOMapper.updateExamWordsJudgeResultDO(wordsJudgeResultDO); + if (updated != 1) { + examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "更新考试记录失败"); + continue; + } + log.info("更新考试记录成功"); - wordMasteryLogDOS.addAll(memorizedWordIds.stream().map(wordId -> WordMasteryLogDO.builder() - .wordId(wordId) - .studentId(studentId) - .reviewCount(1) - .memoryStrength(0.5) - .update_time(LocalDateTime.now()) - .build() - ).toList()); - int batched = wordMasteryLogDOMapper.batchUpdateStudentMastery(wordMasteryLogDOS); - if (batched == 0) { - examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "更新学生记忆力记录失败"); - continue; - } + List wordMasteryLogDOS = new ArrayList<>(unmemorizedWordIds.stream().map(wordId -> WordMasteryLogDO.builder() + .wordId(wordId) + .studentId(studentId) + .reviewCount(1) + .memoryStrength(-0.5) + .update_time(LocalDateTime.now()) + .build() + ).toList()); - int updateStudentExamWordsFinished = studentExamWordsDOMapper.updateStudentExamWordsFinished(studentId, examWordsId); - if (updateStudentExamWordsFinished != 1) { - examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "更新学生考试为结束时失败"); - } + wordMasteryLogDOS.addAll(memorizedWordIds.stream().map(wordId -> WordMasteryLogDO.builder() + .wordId(wordId) + .studentId(studentId) + .reviewCount(1) + .memoryStrength(0.5) + .update_time(LocalDateTime.now()) + .build() + ).toList()); + wordMasteryLogDOMapper.batchUpdateStudentMastery(wordMasteryLogDOS); + log.info("更新单词掌握记录成功"); - boolean delete = new File(ansSheetPath).delete(); - if (delete) { - log.info("删除文件成功:{}", ansSheetPath); + int updateStudentExamWordsFinished = studentExamWordsDOMapper.updateStudentExamWordsFinished(studentId, examWordsId); + if (updateStudentExamWordsFinished != 1) { + examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "更新学生考试为结束时失败"); + } + + boolean delete = new File(ansSheetPath).delete(); + if (delete) { + log.info("删除文件成功:{}", ansSheetPath); + } + } catch (Exception e) { + log.error("识别考试失败 {}", e.getMessage()); + examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDO.getId(), e.getMessage()); } } } + private void judgeExamActualGrade(ExamWordsJudgeResultDO wordsJudgeResultDO, ExamWordsDO examWordsDO) { + List vocabularyBankDOS = vocabularyBankDOMapper.selectVocabularyBankDOListByIds(examWordsDO.getWordIds()); + Map> unitId2Words = vocabularyBankDOS.stream().collect(Collectors.groupingBy(VocabularyBankDO::getUnitId)); + + List gradeUnitDOS = gradeUnitDOMapper.selectByUnitIds(unitId2Words.keySet().stream().toList()); + + // unitId -> gradeId + Map unitId2GradeId = gradeUnitDOS.stream() + .collect(Collectors.toMap( + GradeUnitDO::getUnitId, + GradeUnitDO::getGradeId, + (existing, replacement) -> existing // 如果重复,保留第一个(或根据业务决定) + )); + // gradeId -> List + Map> gradeId2Words = vocabularyBankDOS.stream() + .filter(vocab -> vocab.getUnitId() != null) + .filter(vocab -> unitId2GradeId.containsKey(vocab.getUnitId())) + .collect(Collectors.groupingBy( + vocab -> unitId2GradeId.get(vocab.getUnitId()) + )); + + // 核心数据结构:GradeId -> List (试卷里包含的每个年级的单词有哪些) + Map> gradeId2WordIdsMap = new HashMap<>(); + + for (VocabularyBankDO vocab : vocabularyBankDOS) { + if (vocab.getUnitId() != null && unitId2GradeId.containsKey(vocab.getUnitId())) { + Integer gradeId = unitId2GradeId.get(vocab.getUnitId()); + gradeId2WordIdsMap.computeIfAbsent(gradeId, k -> new HashSet<>()).add(vocab.getId()); + } + } + + // 统计各区域正确率 (Calculate Accuracy per Zone) + // 锚点年级 (G_test),即这张卷子是按哪个年级生成的 + Integer anchorGrade = examWordsDO.getGradeId(); + // 挑战区 + int zoneA = ExamWordsConstant.getZoneA(anchorGrade); + ZoneStats zoneAStats = calculateZoneStats(gradeId2WordIdsMap, wordsJudgeResultDO, zoneA); + // 当前核心 + int zoneB = ExamWordsConstant.getZoneB(anchorGrade); + ZoneStats zoneBStats = calculateZoneStats(gradeId2WordIdsMap, wordsJudgeResultDO, zoneB); + // 回溯 Level 1 + int zoneC = ExamWordsConstant.getZoneC(anchorGrade); + ZoneStats zoneCStats = calculateZoneStats(gradeId2WordIdsMap, wordsJudgeResultDO, zoneC); + // 回溯 Level 2 + int zoneD = ExamWordsConstant.getZoneD(anchorGrade); + ZoneStats zoneDStats = calculateZoneStats(gradeId2WordIdsMap, wordsJudgeResultDO, zoneD); + // 回溯 Level 3 + int zoneE = ExamWordsConstant.getZoneE(anchorGrade); + ZoneStats zoneEStats = calculateZoneStats(gradeId2WordIdsMap, wordsJudgeResultDO, zoneE); + // 地基兜底 + int zoneF = ExamWordsConstant.getZoneF(anchorGrade); + ZoneStats zoneFStats = calculateZoneStats(gradeId2WordIdsMap, wordsJudgeResultDO, zoneF); + // 地基区 + ZoneStats zoneFoundationStats = calculateFoundationStats(gradeId2WordIdsMap, wordsJudgeResultDO, zoneD); + + DiagnosisResult diagnosisResult = diagnoseStudentLevel(anchorGrade, zoneAStats, zoneBStats, zoneCStats, zoneDStats, zoneFoundationStats); + + studentDOMapper.updateStudentActualGradeId(wordsJudgeResultDO.getStudentId(), diagnosisResult.getDeterminedLevel()); + log.info("判断结果:{}", diagnosisResult); + wordsJudgeResultDO.setMsg(diagnosisResult.getMessage()); + } + + /** + * 计算特定年级的统计数据 + */ + private ZoneStats calculateZoneStats(Map> map, ExamWordsJudgeResultDO result, Integer targetGrade) { + Set totalWords = map.getOrDefault(targetGrade, Collections.emptySet()); + if (totalWords.isEmpty()) return new ZoneStats(targetGrade, 0, 0, 0.0); + + long correctCount = result.getCorrectWordIds().stream().filter(totalWords::contains).count(); + double accuracy = (double) correctCount / totalWords.size(); + + return new ZoneStats(targetGrade, totalWords.size(), (int)correctCount, accuracy); + } + + /** + * 计算地基区(所有低于某年级)的统计数据 + */ + private ZoneStats calculateFoundationStats(Map> map, ExamWordsJudgeResultDO result, Integer maxGradeThreshold) { + Set foundationWords = new HashSet<>(); + map.forEach((grade, words) -> { + if (grade <= maxGradeThreshold) { + foundationWords.addAll(words); + } + }); + + if (foundationWords.isEmpty()) return new ZoneStats(-1, 0, 0, 0.0); + + long correctCount = result.getCorrectWordIds().stream().filter(foundationWords::contains).count(); + double accuracy = (double) correctCount / foundationWords.size(); + + return new ZoneStats(-1, foundationWords.size(), (int)correctCount, accuracy); + } + + /** + * 核心诊断逻辑矩阵 + */ + private DiagnosisResult diagnoseStudentLevel(Integer anchorGrade, ZoneStats zoneA, ZoneStats zoneB, ZoneStats zoneC, ZoneStats zoneD, ZoneStats zoneFoundation) { + + // --- 场景 1: 熔断机制 (Meltdown) --- + // 针对 G8/G9 等高年级,如果地基区(G4/G5及以下)错误率极高 + // 阈值设为 20% (即 Zone F 正确率 < 20%) + if (zoneFoundation.getTotalCount() > 0 && zoneFoundation.getAccuracy() < 0.20 && anchorGrade >= 7) { + return new DiagnosisResult( + 4, // 强制下沉到 G4 (根据文档逻辑,熔断通常锚定 G4) + ActionType.TRIGGER_RETEST, + "严重预警!检测到您的基础词汇(" + ExamWordsConstant.getGradeName(anchorGrade - 3) + "及以下)存在大面积坍塌。系统已为您启动【基础词汇专项排查】,请勿担心,这是为了更好地起跳。" + ); + } + + // --- 场景 2: 进阶/跳级 (Upgrade) --- + // 必须满足:当前年级(B) > 80% 且 挑战年级(A) > 60% (如果有题的话) + boolean canUpgrade = zoneB.getAccuracy() >= 0.8 && (zoneA.getTotalCount() == 0 || zoneA.getAccuracy() >= 0.6); + if (canUpgrade) { + return new DiagnosisResult( + anchorGrade + 1, + ActionType.UPGRADE, + "恭喜!您对" + ExamWordsConstant.getGradeName(anchorGrade) + " 的掌握非常扎实,且具备挑战" + ExamWordsConstant.getGradeName(anchorGrade + 1) + " 的潜力。系统将为您解锁更高阶词库。" + ); + } + + // --- 场景 3: 正常回溯诊断 (Standard Diagnosis) --- + + // 3.1 当前年级崩盘 (Zone B < 60%) + if (zoneB.getAccuracy() < 0.6) { + + // 检查上一级 (Zone C) + if (zoneC.getAccuracy() >= 0.8) { + // "中考空心病" / "基础扎实但新课未动" + // 定级:保持在当前年级,但侧重复习 + return new DiagnosisResult( + anchorGrade, + ActionType.STAY_AND_REVIEW, + "基础尚可(" + ExamWordsConstant.getGradeName(zoneC.getGradeId()) + "掌握较好),但" + ExamWordsConstant.getGradeName(anchorGrade) + " 新词汇存在较大缺口。建议:重点攻克本年级核心动词。" + ); + } else if (zoneC.getAccuracy() >= 0.6) { + // Zone C 勉强及格,Zone B 不及格 -> 降一级 + return new DiagnosisResult( + anchorGrade - 1, + ActionType.DOWNGRADE, + "检测到" + ExamWordsConstant.getGradeName(anchorGrade) + " 学习吃力,且" + ExamWordsConstant.getGradeName(zoneC.getGradeId()) + " 存在模糊点。建议降级回溯,巩固 " + ExamWordsConstant.getGradeName(zoneC.getGradeId()) + "。" + ); + } else { + // Zone C 也崩了 (< 60%) -> 连降两级或更多 + // 检查 Zone D + if (zoneD.getTotalCount() > 0 && zoneD.getAccuracy() >= 0.6) { + return new DiagnosisResult( + anchorGrade - 2, + ActionType.DOWNGRADE, + "您的" + ExamWordsConstant.getGradeName(zoneC.getGradeId()) + " 和 " + ExamWordsConstant.getGradeName(anchorGrade) + " 均存在脱节。建议从 " + ExamWordsConstant.getGradeName(zoneD.getGradeId()) + " 开始系统补漏。" + ); + } else { + // 彻底崩盘,可能需要熔断,或者定级到更低 + return new DiagnosisResult( + Math.max(1, anchorGrade - 3), + ActionType.DOWNGRADE, + "基础薄弱,建议暂停当前进度,从低年级核心高频词重新开始。" + ); + } + } + } + + // 3.2 正常达标 (Zone B 60% - 80%) + return new DiagnosisResult( + anchorGrade, + ActionType.PASS, + "当前年级达标。建议继续保持,并尝试在阅读中增加长难句练习。" + ); + } + @Override public List getExamWordsJudgeResult(Integer page, Integer pageSize) { return examWordsJudgeResultDOMapper.selectByPage((page - 1) * pageSize, pageSize); diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/student/StudentServiceImpl.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/student/StudentServiceImpl.java index 21ca1e9..9948f9a 100644 --- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/student/StudentServiceImpl.java +++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/student/StudentServiceImpl.java @@ -81,6 +81,7 @@ public class StudentServiceImpl implements StudentService { .name(name) .classId(classId) .gradeId(gradeId) + .actualGradeId(gradeId) .startTime(createTime) .build(); studentDOMapper.insert(studentDO); diff --git a/enlish-service/src/main/resources/config/application.yml b/enlish-service/src/main/resources/config/application.yml index 3994f4a..0fce1b8 100644 --- a/enlish-service/src/main/resources/config/application.yml +++ b/enlish-service/src/main/resources/config/application.yml @@ -1,5 +1,5 @@ server: - port: 8080 # 项目启动的端口 + port: 8081 # 项目启动的端口 spring: profiles: diff --git a/enlish-service/src/main/resources/mapper/ExamWordsJudgeResultDOMapper.xml b/enlish-service/src/main/resources/mapper/ExamWordsJudgeResultDOMapper.xml index 632a0cc..1922adb 100644 --- a/enlish-service/src/main/resources/mapper/ExamWordsJudgeResultDOMapper.xml +++ b/enlish-service/src/main/resources/mapper/ExamWordsJudgeResultDOMapper.xml @@ -12,7 +12,6 @@ - @@ -34,8 +33,8 @@ update exam_words_judge_result - set msg = #{msg} - and is_finished = 2 + set msg = #{msg}, + is_finished = 2 where id = #{id} @@ -47,7 +46,8 @@ wrong_word_ids = #{examWordsJudgeResultDO.wrongWordIds, typeHandler=com.yinlihupo.enlish.service.config.ListWordIdTypeHandler}, correct_word_count = #{examWordsJudgeResultDO.correctWordCount}, wrong_word_count = #{examWordsJudgeResultDO.wrongWordCount}, - is_finished = 1 + is_finished = 1, + msg = #{examWordsJudgeResultDO.msg} where id = #{examWordsJudgeResultDO.id} diff --git a/enlish-service/src/main/resources/mapper/GradeUnitDOMapper.xml b/enlish-service/src/main/resources/mapper/GradeUnitDOMapper.xml index fe233ca..e47f3ec 100644 --- a/enlish-service/src/main/resources/mapper/GradeUnitDOMapper.xml +++ b/enlish-service/src/main/resources/mapper/GradeUnitDOMapper.xml @@ -19,6 +19,17 @@ where unit_id = #{unitId} + + insert into grade_unit (grade_id, unit_id) values (#{gradeId}, #{unitId}) diff --git a/enlish-service/src/main/resources/mapper/StudentDOMapper.xml b/enlish-service/src/main/resources/mapper/StudentDOMapper.xml index 8f323fd..e4391a6 100644 --- a/enlish-service/src/main/resources/mapper/StudentDOMapper.xml +++ b/enlish-service/src/main/resources/mapper/StudentDOMapper.xml @@ -10,6 +10,7 @@ + select count(1) from student diff --git a/enlish-service/src/main/resources/mapper/VocabularyBankDOMapper.xml b/enlish-service/src/main/resources/mapper/VocabularyBankDOMapper.xml index a29605c..d6579f6 100644 --- a/enlish-service/src/main/resources/mapper/VocabularyBankDOMapper.xml +++ b/enlish-service/src/main/resources/mapper/VocabularyBankDOMapper.xml @@ -117,6 +117,7 @@ limit #{wordCount} +