Compare commits

..

2 Commits

Author SHA1 Message Date
lbw
515bd8fae2 feat(exam): 实现考试阶段单词判卷与学生水平智能诊断
- 新增ActionType枚举定义系统动作类型
- 新增DiagnosisResult和ZoneStats数据模型支持诊断结果及区域统计
- 优化ExamWordsJudgeServiceImpl判卷逻辑,支持识别图片、更新考试判卷结果
- 基于分区词汇掌握情况,实现学生当前水平年级的智能判定
- 实现基于多分区准确率的升级、降级、复习和触发重测等动作建议
- 更新学生实际年级actualGradeId并展示在学生详情页面
- 修正ExamWordsConstant年级常量及年级名称映射方法
- 优化前端生成试题对年级和难度的校验逻辑,简化参数传递
- 修改服务端端口及API代理配置,保持一致性
- 调整相关数据库Mapper,支持批量查询和更新实际年级字段
- 修改错误信息字段命名,统一为msg
- 增删改代码注释与日志,提升容错性和可读性
2025-12-22 14:11:11 +08:00
lbw
065da854ee feat(exam): 支持按单个学生和考试类型生成考试试题
- 修改生成试题按钮仅在选中特定一个学生时可用,避免多选时误操作
- 在考试生成对话框新增“类型”选择项,支持“摸底”和“期中|期末”类型
- 调整后台接口,使用单个学生ID和考试类型替代学生ID列表参数
- 优化考试生成服务,新增摸底考试生成逻辑,按年级分区随机抽词汇
- 考试相关数据对象新增类型字段,保持数据完整性和一致性
- 修改考试判卷服务,将错误信息字段统一为msg,避免字段混淆
- 调整数据库操作,支持单个学生考试与词汇随机获取
- 同步更新测试用例和词汇库数据插入逻辑,确保环境一致性
- 修复界面生成按钮状态和对话框提交按钮的校验逻辑,提升用户体验
2025-12-18 17:21:37 +08:00
37 changed files with 588 additions and 149 deletions

View File

@@ -1,6 +1,132 @@
package com.yinlihupo.enlish.service.constant; package com.yinlihupo.enlish.service.constant;
public interface ExamWordsConstant { public class ExamWordsConstant {
int PGN_COL = 53; public static final int PGN_COL = 53;
public static final int GRADE_1 = 1;
public static final int GRADE_2 = 2;
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 = 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;
public static final int ZONE_C_SIZE = 28;
public static final int ZONE_D_SIZE = 21;
public static final int ZONE_E_SIZE = 14;
public static final int ZONE_F_SIZE = 7;
public static final int EXAM_TYPE_BASELINE = 1;
public static int getZoneA(int gradeId) {
return switch (gradeId) {
case GRADE_1 -> GRADE_2;
case GRADE_2 -> GRADE_3;
case GRADE_3 -> GRADE_4;
case GRADE_4 -> GRADE_5;
case GRADE_5 -> GRADE_6;
case GRADE_6 -> GRADE_7;
case GRADE_7 -> GRADE_8;
default -> 0;
};
}
public static int getZoneB(int gradeId) {
return switch (gradeId) {
case GRADE_1 -> GRADE_1;
case GRADE_2 -> GRADE_2;
case GRADE_3 -> GRADE_3;
case GRADE_4 -> GRADE_4;
case GRADE_5 -> GRADE_5;
case GRADE_6 -> GRADE_6;
case GRADE_7 -> GRADE_7;
case GRADE_8 -> GRADE_8;
default -> 0;
};
}
public static int getZoneC(int gradeId) {
return switch (gradeId) {
case GRADE_1 -> GRADE_1;
case GRADE_2 -> GRADE_1;
case GRADE_3 -> GRADE_2;
case GRADE_4 -> GRADE_3;
case GRADE_5 -> GRADE_4;
case GRADE_6 -> GRADE_5;
case GRADE_7 -> GRADE_6;
case GRADE_8 -> GRADE_7;
default -> 0;
};
}
public static int getZoneD(int gradeId) {
return switch (gradeId) {
case GRADE_1 -> GRADE_1;
case GRADE_2 -> GRADE_1;
case GRADE_3 -> GRADE_1;
case GRADE_4 -> GRADE_2;
case GRADE_5 -> GRADE_3;
case GRADE_6 -> GRADE_4;
case GRADE_7 -> GRADE_5;
case GRADE_8 -> GRADE_6;
default -> 0;
};
}
public static int getZoneE(int gradeId) {
return switch (gradeId) {
case GRADE_1 -> GRADE_1;
case GRADE_2 -> GRADE_1;
case GRADE_3 -> GRADE_1;
case GRADE_4 -> GRADE_1;
case GRADE_5 -> GRADE_2;
case GRADE_6 -> GRADE_3;
case GRADE_7 -> GRADE_4;
case GRADE_8 -> GRADE_5;
default -> 0;
};
}
public static int getZoneF(int gradeId) {
return switch (gradeId) {
case GRADE_1 -> GRADE_1;
case GRADE_2 -> GRADE_1;
case GRADE_3 -> GRADE_1;
case GRADE_4 -> GRADE_1;
case GRADE_5 -> GRADE_1;
case GRADE_6 -> GRADE_2;
case GRADE_7 -> GRADE_3;
case GRADE_8 -> GRADE_4;
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 -> "";
};
}
} }

View File

@@ -44,12 +44,13 @@ public class ExamWordsController {
public void generateFeltExamWords(@RequestBody GenerateExamWordsReqVO generateExamWordsReqVO, HttpServletResponse response) { public void generateFeltExamWords(@RequestBody GenerateExamWordsReqVO generateExamWordsReqVO, HttpServletResponse response) {
Integer gradeId = generateExamWordsReqVO.getGradeId(); Integer gradeId = generateExamWordsReqVO.getGradeId();
Integer level = generateExamWordsReqVO.getLevel(); Integer level = generateExamWordsReqVO.getLevel();
List<Integer> studentIds = generateExamWordsReqVO.getStudentIds(); Integer type = generateExamWordsReqVO.getType();
if (studentIds == null || studentIds.isEmpty() || gradeId == null || level == null) { Integer studentId = generateExamWordsReqVO.getStudentId();
if (studentId == null || gradeId == null || level == null) {
throw new RuntimeException("参数错误"); throw new RuntimeException("参数错误");
} }
try { try {
ExamWordsDO examWordsDO = examWordsService.generateExamWords(gradeId, level, studentIds); ExamWordsDO examWordsDO = examWordsService.generateExamWords(gradeId, level, studentId, type);
if (examWordsDO == null || examWordsDO.getWordIds().isEmpty()) { if (examWordsDO == null || examWordsDO.getWordIds().isEmpty()) {
throw new RuntimeException("没有单词"); throw new RuntimeException("没有单词");
} }
@@ -60,7 +61,7 @@ public class ExamWordsController {
.definition(vocabularyBankDO.getDefinition()) .definition(vocabularyBankDO.getDefinition())
.build()).toList(); .build()).toList();
List<StudentDetail> studentDetailList = studentService.getStudentDetailList(studentIds); List<StudentDetail> studentDetailList = studentService.getStudentDetailList(Collections.singletonList(studentId));
List<Map<String, Object>> maps = studentDetailList.stream().map(studentDetail -> { List<Map<String, Object>> maps = studentDetailList.stream().map(studentDetail -> {
Map<String, Object> data = new HashMap<>(); Map<String, Object> data = new HashMap<>();
data.put("examId", examWordsDO.getId()); data.put("examId", examWordsDO.getId());
@@ -115,7 +116,7 @@ public class ExamWordsController {
.correctWordCount(examWordsJudgeResultDO.getCorrectWordCount()) .correctWordCount(examWordsJudgeResultDO.getCorrectWordCount())
.wrongWordCount(examWordsJudgeResultDO.getWrongWordCount()) .wrongWordCount(examWordsJudgeResultDO.getWrongWordCount())
.isFinished(examWordsJudgeResultDO.getIsFinished()) .isFinished(examWordsJudgeResultDO.getIsFinished())
.errorMsg(examWordsJudgeResultDO.getErrorMsg()) .msg(examWordsJudgeResultDO.getMsg())
.build() .build()
).toList(); ).toList();
return PageResponse.success(list, page, total, size); return PageResponse.success(list, page, total, size);
@@ -134,7 +135,7 @@ public class ExamWordsController {
.correctWordCount(examWordsJudgeResultDO.getCorrectWordCount()) .correctWordCount(examWordsJudgeResultDO.getCorrectWordCount())
.wrongWordCount(examWordsJudgeResultDO.getWrongWordCount()) .wrongWordCount(examWordsJudgeResultDO.getWrongWordCount())
.isFinished(examWordsJudgeResultDO.getIsFinished()) .isFinished(examWordsJudgeResultDO.getIsFinished())
.errorMsg(examWordsJudgeResultDO.getErrorMsg()) .errorMsg(examWordsJudgeResultDO.getMsg())
.correctWordIds(examWordsJudgeResultDO.getCorrectWordIds()) .correctWordIds(examWordsJudgeResultDO.getCorrectWordIds())
.wrongWordIds(examWordsJudgeResultDO.getWrongWordIds()) .wrongWordIds(examWordsJudgeResultDO.getWrongWordIds())
.build(); .build();

View File

@@ -60,12 +60,13 @@ public class StudentController {
StudentDO studentById = studentService.getStudentById(studentId); StudentDO studentById = studentService.getStudentById(studentId);
ClassDO classById = classService.findClassById(studentById.getClassId()); ClassDO classById = classService.findClassById(studentById.getClassId());
GradeDO byClassId = gradeService.findByClassId(studentById.getGradeId()); GradeDO byClassId = gradeService.findByClassId(studentById.getGradeId());
GradeDO actualGradeById = gradeService.findByClassId(studentById.getActualGradeId());
FindStudentDetailRspVO findStudentDetailRspVO = FindStudentDetailRspVO.builder() FindStudentDetailRspVO findStudentDetailRspVO = FindStudentDetailRspVO.builder()
.id(studentById.getId()) .id(studentById.getId())
.name(studentById.getName()) .name(studentById.getName())
.className(classById.getTitle()) .className(classById.getTitle())
.gradeName(byClassId.getTitle()) .gradeName(byClassId.getTitle())
.actualGrade(actualGradeById != null ? actualGradeById.getTitle() : "")
.build(); .build();
return Response.success(findStudentDetailRspVO); return Response.success(findStudentDetailRspVO);

View File

@@ -19,6 +19,8 @@ public class ExamWordsDO {
private Integer level; private Integer level;
private Integer type;
private String title; private String title;
private LocalDateTime createdAt; private LocalDateTime createdAt;

View File

@@ -13,6 +13,7 @@ import java.util.List;
@Data @Data
@Builder @Builder
public class ExamWordsJudgeResultDO { public class ExamWordsJudgeResultDO {
private Integer id; private Integer id;
private String ansSheetPath; private String ansSheetPath;
@@ -33,6 +34,6 @@ public class ExamWordsJudgeResultDO {
private List<Integer> wrongWordIds; private List<Integer> wrongWordIds;
private String errorMsg; private String msg;
} }

View File

@@ -21,6 +21,8 @@ public class StudentDO {
private Integer gradeId; private Integer gradeId;
private Integer actualGradeId;
private Integer isDeleted; private Integer isDeleted;
private LocalDateTime startTime; private LocalDateTime startTime;

View File

@@ -11,7 +11,7 @@ public interface ExamWordsJudgeResultDOMapper {
List<ExamWordsJudgeResultDO> selectUnfinishedExamWordsJudgeResultDOList(int count); List<ExamWordsJudgeResultDO> selectUnfinishedExamWordsJudgeResultDOList(int count);
int updateErrorMsg(@Param("id") Integer id, @Param("errorMsg") String errorMsg); int updateMsg(@Param("id") Integer id, @Param("msg") String msg);
int updateExamWordsJudgeResultDO(@Param("examWordsJudgeResultDO") ExamWordsJudgeResultDO examWordsJudgeResultDO); int updateExamWordsJudgeResultDO(@Param("examWordsJudgeResultDO") ExamWordsJudgeResultDO examWordsJudgeResultDO);

View File

@@ -11,6 +11,8 @@ public interface GradeUnitDOMapper {
GradeUnitDO selectByUnitId(@Param("unitId") Integer unitId); GradeUnitDO selectByUnitId(@Param("unitId") Integer unitId);
List<GradeUnitDO> selectByUnitIds(@Param("unitIds") List<Integer> unitIds);
int insert(GradeUnitDO record); int insert(GradeUnitDO record);
int deleteByUnitId(@Param("unitId") Integer unitId); int deleteByUnitId(@Param("unitId") Integer unitId);

View File

@@ -21,4 +21,6 @@ public interface StudentDOMapper {
void deleteById(Integer id); void deleteById(Integer id);
int selectStudentCountByClassId(@Param("classId") Integer classId); int selectStudentCountByClassId(@Param("classId") Integer classId);
int updateStudentActualGradeId(@Param("studentId") Integer studentId, @Param("gradeId") Integer gradeId);
} }

View File

@@ -8,7 +8,7 @@ import java.util.List;
public interface StudentExamWordsDOMapper { public interface StudentExamWordsDOMapper {
int insertStudentsExam(@Param("studentIds") List<Integer> studentIds, @Param("examWordsId") Integer examWordsId); int insertStudentsExam(@Param("studentId") Integer studentId, @Param("examWordsId") Integer examWordsId);
StudentExamWordsDO selectByStudentIdAndExamWordsId(@Param("studentId") Integer studentId, @Param("examWordsId") Integer examWordsId); StudentExamWordsDO selectByStudentIdAndExamWordsId(@Param("studentId") Integer studentId, @Param("examWordsId") Integer examWordsId);

View File

@@ -21,5 +21,7 @@ public interface VocabularyBankDOMapper {
List<VocabularyBankDO> selectVocabularyBankListSelfCheck(@Param("gradeId") Integer gradeId, @Param("studentId") Integer studentId, @Param("ids") List<Integer> ids, @Param("wordCount") Integer wordCount); List<VocabularyBankDO> selectVocabularyBankListSelfCheck(@Param("gradeId") Integer gradeId, @Param("studentId") Integer studentId, @Param("ids") List<Integer> ids, @Param("wordCount") Integer wordCount);
List<VocabularyBankDO> selectVocabularyBankListByGradeIdRandom(@Param("gradeId") Integer gradeId, @Param("wordCount") Integer wordCount);
Integer selectWordTotal(); Integer selectWordTotal();
} }

View File

@@ -0,0 +1,9 @@
package com.yinlihupo.enlish.service.model.bo.exam;
public enum ActionType {
PASS, // 保持当前进度
UPGRADE, // 升级
DOWNGRADE, // 降级回填
STAY_AND_REVIEW, // 保持年级但进入复习模式
TRIGGER_RETEST // 触发熔断二测
}

View File

@@ -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; // 展示给用户的文案
}

View File

@@ -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;
}

View File

@@ -28,5 +28,5 @@ public class ExamWordsResultRspVO {
private LocalDateTime startDate; private LocalDateTime startDate;
private String errorMsg; private String msg;
} }

View File

@@ -16,5 +16,6 @@ public class GenerateExamWordsReqVO {
private Integer gradeId; private Integer gradeId;
private Integer level; private Integer level;
private List<Integer> studentIds; private Integer type;
private Integer studentId;
} }

View File

@@ -15,4 +15,5 @@ public class FindStudentDetailRspVO {
private String name; private String name;
private String className; private String className;
private String gradeName; private String gradeName;
private String actualGrade;
} }

View File

@@ -8,7 +8,7 @@ import java.util.List;
public interface ExamWordsService { public interface ExamWordsService {
ExamWordsDO generateExamWords(Integer gradeId, Integer level, List<Integer> studentIds); ExamWordsDO generateExamWords(Integer gradeId, Integer level, Integer studentId, Integer type);
int saveExamWordsPngToDbAndLocal(MultipartFile file); int saveExamWordsPngToDbAndLocal(MultipartFile file);
} }

View File

@@ -1,7 +1,9 @@
package com.yinlihupo.enlish.service.service.exam; package com.yinlihupo.enlish.service.service.exam;
import com.yinlihupo.enlish.service.constant.ExamWordsConstant;
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsDO; import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsDO;
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsJudgeResultDO; import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsJudgeResultDO;
import com.yinlihupo.enlish.service.domain.dataobject.StudentDO;
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO; import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
import com.yinlihupo.enlish.service.domain.mapper.*; import com.yinlihupo.enlish.service.domain.mapper.*;
import com.yinlihupo.enlish.service.service.ExamWordsService; import com.yinlihupo.enlish.service.service.ExamWordsService;
@@ -16,6 +18,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -33,6 +36,9 @@ public class ExamWordsServiceImpl implements ExamWordsService {
private StudentExamWordsDOMapper studentExamWordsDOMapper; private StudentExamWordsDOMapper studentExamWordsDOMapper;
@Resource @Resource
private ExamWordsJudgeResultDOMapper examWordsJudgeResultDOMapper; private ExamWordsJudgeResultDOMapper examWordsJudgeResultDOMapper;
@Resource
private StudentDOMapper studentDOMapper;
@Value("${templates.count}") @Value("${templates.count}")
private Integer wordCount; private Integer wordCount;
@Value("${tmp.png}") @Value("${tmp.png}")
@@ -40,23 +46,56 @@ public class ExamWordsServiceImpl implements ExamWordsService {
@Override @Override
@Transactional(rollbackFor = RuntimeException.class) @Transactional(rollbackFor = RuntimeException.class)
public ExamWordsDO generateExamWords(Integer gradeId, Integer level, List<Integer> studentIds) { public ExamWordsDO generateExamWords(Integer gradeId, Integer level, Integer studentId, Integer type) {
List<Integer> unitIds = gradeUnitDOMapper.selectUnitIdsByGradeId(gradeId);
List<VocabularyBankDO> vocabularyBankDOS = new ArrayList<>(); ExamWordsDO examWordsDO;
int count = wordCount;
for (Integer unitId : unitIds) { if (type == ExamWordsConstant.EXAM_TYPE_BASELINE) {
List<VocabularyBankDO> words = vocabularyBankDOMapper.selectVocabularyBankDOListByUnitId(unitId, 20); log.info("生成摸底测试");
vocabularyBankDOS.addAll(words); examWordsDO = generateBaselineExamWords(studentId);
count -= 20; } else {
if (count <= 0) { // todo 生成期中考试待实现
break; examWordsDO = null;
}
} }
return examWordsDO;
}
private ExamWordsDO generateBaselineExamWords(Integer studentId) {
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
Integer gradeId = studentDO.getActualGradeId();
int zoneA = ExamWordsConstant.getZoneA(gradeId);
int zoneASize = ExamWordsConstant.ZONE_A_SIZE;
List<VocabularyBankDO> vocabularyBankDOS = new ArrayList<>(vocabularyBankDOMapper.selectVocabularyBankListByGradeIdRandom(zoneA, zoneASize));
int zoneB = ExamWordsConstant.getZoneB(gradeId);
int zoneBSize = ExamWordsConstant.ZONE_B_SIZE;
vocabularyBankDOS.addAll(vocabularyBankDOMapper.selectVocabularyBankListByGradeIdRandom(zoneB, zoneBSize));
int zoneC = ExamWordsConstant.getZoneC(gradeId);
int zoneCSize = ExamWordsConstant.ZONE_C_SIZE;
vocabularyBankDOS.addAll(vocabularyBankDOMapper.selectVocabularyBankListByGradeIdRandom(zoneC, zoneCSize));
int zoneD = ExamWordsConstant.getZoneD(gradeId);
int zoneDSize = ExamWordsConstant.ZONE_D_SIZE;
vocabularyBankDOS.addAll(vocabularyBankDOMapper.selectVocabularyBankListByGradeIdRandom(zoneD, zoneDSize));
int zoneE = ExamWordsConstant.getZoneE(gradeId);
int zoneESize = ExamWordsConstant.ZONE_E_SIZE;
vocabularyBankDOS.addAll(vocabularyBankDOMapper.selectVocabularyBankListByGradeIdRandom(zoneE, zoneESize));
int zoneF = ExamWordsConstant.getZoneF(gradeId);
int zoneFSize = ExamWordsConstant.ZONE_F_SIZE;
vocabularyBankDOS.addAll(vocabularyBankDOMapper.selectVocabularyBankListByGradeIdRandom(zoneF, zoneFSize));
ExamWordsDO examWordsDO = ExamWordsDO.builder() ExamWordsDO examWordsDO = ExamWordsDO.builder()
.gradeId(gradeId) .gradeId(gradeId)
.level(level) .level(1)
.title(LocalDateTime.now() + "测试") .type(ExamWordsConstant.EXAM_TYPE_BASELINE)
.title("摸低测试测试" + studentDO.getName())
.createdAt(LocalDateTime.now()) .createdAt(LocalDateTime.now())
.wordIds(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList()) .wordIds(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList())
.build(); .build();
@@ -66,7 +105,7 @@ public class ExamWordsServiceImpl implements ExamWordsService {
throw new RuntimeException("插入考试失败"); throw new RuntimeException("插入考试失败");
} }
int insertStudentsExam = studentExamWordsDOMapper.insertStudentsExam(studentIds, examWordsDO.getId()); int insertStudentsExam = studentExamWordsDOMapper.insertStudentsExam(studentId, examWordsDO.getId());
if (insertStudentsExam <= 0) { if (insertStudentsExam <= 0) {
throw new RuntimeException("插入学生关联考试失败"); throw new RuntimeException("插入学生关联考试失败");
} }

View File

@@ -1,15 +1,13 @@
package com.yinlihupo.enlish.service.service.judge; package com.yinlihupo.enlish.service.service.judge;
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsDO; import com.yinlihupo.enlish.service.constant.ExamWordsConstant;
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsJudgeResultDO; import com.yinlihupo.enlish.service.domain.dataobject.*;
import com.yinlihupo.enlish.service.domain.dataobject.StudentExamWordsDO; import com.yinlihupo.enlish.service.domain.mapper.*;
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.model.bo.CoordinatesXY; import com.yinlihupo.enlish.service.model.bo.CoordinatesXY;
import com.yinlihupo.enlish.service.model.bo.StudentExamId; 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.service.ExamWordsJudgeService;
import com.yinlihupo.enlish.service.utils.PngUtil; import com.yinlihupo.enlish.service.utils.PngUtil;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@@ -20,8 +18,8 @@ import org.springframework.transaction.annotation.Transactional;
import java.io.File; import java.io.File;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.*;
import java.util.List; import java.util.stream.Collectors;
@Service @Service
@Slf4j @Slf4j
@@ -36,6 +34,12 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
private ExamWordsDOMapper examWordsDOMapper; private ExamWordsDOMapper examWordsDOMapper;
@Resource @Resource
private WordMasteryLogDOMapper wordMasteryLogDOMapper; private WordMasteryLogDOMapper wordMasteryLogDOMapper;
@Resource
private VocabularyBankDOMapper vocabularyBankDOMapper;
@Resource
private GradeUnitDOMapper gradeUnitDOMapper;
@Resource
private StudentDOMapper studentDOMapper;
@Value("${templates.data}") @Value("${templates.data}")
private String tessdataPath; private String tessdataPath;
@@ -45,91 +49,266 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
public void judgeExamWords(int count) { public void judgeExamWords(int count) {
List<ExamWordsJudgeResultDO> examWordsJudgeResultDOS = examWordsJudgeResultDOMapper.selectUnfinishedExamWordsJudgeResultDOList(count); List<ExamWordsJudgeResultDO> examWordsJudgeResultDOS = examWordsJudgeResultDOMapper.selectUnfinishedExamWordsJudgeResultDOList(count);
for (ExamWordsJudgeResultDO examWordsJudgeResultDO : examWordsJudgeResultDOS) { for (ExamWordsJudgeResultDO examWordsJudgeResultDO : examWordsJudgeResultDOS) {
String ansSheetPath = examWordsJudgeResultDO.getAnsSheetPath(); try {
List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(ansSheetPath); String ansSheetPath = examWordsJudgeResultDO.getAnsSheetPath();
// 从图片中获取学生 id 和考试 id List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(ansSheetPath);
StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(ansSheetPath, tessdataPath, coordinatesXIES); // 从图片中获取学生 id 和考试 id
Integer examWordsJudgeResultDOId = examWordsJudgeResultDO.getId(); StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(ansSheetPath, tessdataPath, coordinatesXIES);
if (studentExamId == null) { Integer examWordsJudgeResultDOId = examWordsJudgeResultDO.getId();
examWordsJudgeResultDOMapper.updateErrorMsg(examWordsJudgeResultDOId, "未识别学生 id 和考试 id"); if (studentExamId == null) {
continue; examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未识别学生 id 和考试 id");
} continue;
}
Integer studentId = studentExamId.getStudentId(); Integer studentId = studentExamId.getStudentId();
Integer examWordsId = studentExamId.getExamId(); Integer examWordsId = studentExamId.getExamId();
StudentExamWordsDO studentExamWordsDO = studentExamWordsDOMapper.selectByStudentIdAndExamWordsId(studentId, examWordsId); StudentExamWordsDO studentExamWordsDO = studentExamWordsDOMapper.selectByStudentIdAndExamWordsId(studentId, examWordsId);
if (studentExamWordsDO == null) { if (studentExamWordsDO == null) {
examWordsJudgeResultDOMapper.updateErrorMsg(examWordsJudgeResultDOId, "未找到学生 id 和考试 id 对应的考试记录"); examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未找到学生 id 和考试 id 对应的考试记录");
continue; continue;
} }
log.info("studentId:{},examWordsId:{}", studentId, examWordsId); log.info("studentId:{},examWordsId:{}", studentId, examWordsId);
if (studentExamWordsDO.getIsCompleted() == 1) { if (studentExamWordsDO.getIsCompleted() == 1) {
examWordsJudgeResultDOMapper.updateErrorMsg(examWordsJudgeResultDOId, "考试记录此前已识别"); examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "考试记录此前已识别");
continue; continue;
} }
ExamWordsDO examWordsDO = examWordsDOMapper.selectById(examWordsId); ExamWordsDO examWordsDO = examWordsDOMapper.selectById(examWordsId);
if(examWordsDO == null) { if(examWordsDO == null) {
examWordsJudgeResultDOMapper.updateErrorMsg(examWordsJudgeResultDOId, "未找到考试"); examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未找到考试");
continue; continue;
} }
List<Integer> wordIds = examWordsDO.getWordIds(); List<Integer> wordIds = examWordsDO.getWordIds();
List<Integer> unmemorizedWordIds = PngUtil.analyzePngForUnmemorizedWordIds(ansSheetPath, wordIds, coordinatesXIES); List<Integer> unmemorizedWordIds = PngUtil.analyzePngForUnmemorizedWordIds(ansSheetPath, wordIds, coordinatesXIES);
List<Integer> memorizedWordIds = wordIds.stream().filter(wordId -> !unmemorizedWordIds.contains(wordId)).toList(); List<Integer> memorizedWordIds = wordIds.stream().filter(wordId -> !unmemorizedWordIds.contains(wordId)).toList();
ExamWordsJudgeResultDO wordsJudgeResultDO = ExamWordsJudgeResultDO.builder() ExamWordsJudgeResultDO wordsJudgeResultDO = ExamWordsJudgeResultDO.builder()
.id(examWordsJudgeResultDOId) .id(examWordsJudgeResultDOId)
.studentId(studentId) .studentId(studentId)
.examWordsId(examWordsId) .examWordsId(examWordsId)
.correctWordIds(memorizedWordIds) .correctWordIds(memorizedWordIds)
.wrongWordIds(unmemorizedWordIds) .wrongWordIds(unmemorizedWordIds)
.correctWordCount(memorizedWordIds.size()) .correctWordCount(memorizedWordIds.size())
.wrongWordCount(unmemorizedWordIds.size()) .wrongWordCount(unmemorizedWordIds.size())
.isFinished(1) .isFinished(1)
.build(); .build();
int updated = examWordsJudgeResultDOMapper.updateExamWordsJudgeResultDO(wordsJudgeResultDO); // 判断考试等级
if (updated != 1) { judgeExamActualGrade(wordsJudgeResultDO, examWordsDO);
examWordsJudgeResultDOMapper.updateErrorMsg(examWordsJudgeResultDOId, "更新考试记录失败");
continue;
}
log.info("更新考试记录成功");
List<WordMasteryLogDO> wordMasteryLogDOS = new ArrayList<>(unmemorizedWordIds.stream().map(wordId -> WordMasteryLogDO.builder() int updated = examWordsJudgeResultDOMapper.updateExamWordsJudgeResultDO(wordsJudgeResultDO);
.wordId(wordId) if (updated != 1) {
.studentId(studentId) examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "更新考试记录失败");
.reviewCount(1) continue;
.memoryStrength(-0.5) }
.update_time(LocalDateTime.now()) log.info("更新考试记录成功");
.build()
).toList());
wordMasteryLogDOS.addAll(memorizedWordIds.stream().map(wordId -> WordMasteryLogDO.builder() List<WordMasteryLogDO> wordMasteryLogDOS = new ArrayList<>(unmemorizedWordIds.stream().map(wordId -> WordMasteryLogDO.builder()
.wordId(wordId) .wordId(wordId)
.studentId(studentId) .studentId(studentId)
.reviewCount(1) .reviewCount(1)
.memoryStrength(0.5) .memoryStrength(-0.5)
.update_time(LocalDateTime.now()) .update_time(LocalDateTime.now())
.build() .build()
).toList()); ).toList());
int batched = wordMasteryLogDOMapper.batchUpdateStudentMastery(wordMasteryLogDOS);
if (batched == 0) {
examWordsJudgeResultDOMapper.updateErrorMsg(examWordsJudgeResultDOId, "更新学生记忆力记录失败");
continue;
}
int updateStudentExamWordsFinished = studentExamWordsDOMapper.updateStudentExamWordsFinished(studentId, examWordsId); wordMasteryLogDOS.addAll(memorizedWordIds.stream().map(wordId -> WordMasteryLogDO.builder()
if (updateStudentExamWordsFinished != 1) { .wordId(wordId)
examWordsJudgeResultDOMapper.updateErrorMsg(examWordsJudgeResultDOId, "更新学生考试为结束时失败"); .studentId(studentId)
} .reviewCount(1)
.memoryStrength(0.5)
.update_time(LocalDateTime.now())
.build()
).toList());
wordMasteryLogDOMapper.batchUpdateStudentMastery(wordMasteryLogDOS);
log.info("更新单词掌握记录成功");
boolean delete = new File(ansSheetPath).delete(); int updateStudentExamWordsFinished = studentExamWordsDOMapper.updateStudentExamWordsFinished(studentId, examWordsId);
if (delete) { if (updateStudentExamWordsFinished != 1) {
log.info("删除文件成功:{}", ansSheetPath); 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<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectVocabularyBankDOListByIds(examWordsDO.getWordIds());
Map<Integer, List<VocabularyBankDO>> unitId2Words = vocabularyBankDOS.stream().collect(Collectors.groupingBy(VocabularyBankDO::getUnitId));
List<GradeUnitDO> gradeUnitDOS = gradeUnitDOMapper.selectByUnitIds(unitId2Words.keySet().stream().toList());
// unitId -> gradeId
Map<Integer, Integer> unitId2GradeId = gradeUnitDOS.stream()
.collect(Collectors.toMap(
GradeUnitDO::getUnitId,
GradeUnitDO::getGradeId,
(existing, replacement) -> existing // 如果重复,保留第一个(或根据业务决定)
));
// gradeId -> List<VocabularyBankDO>
Map<Integer, List<VocabularyBankDO>> gradeId2Words = vocabularyBankDOS.stream()
.filter(vocab -> vocab.getUnitId() != null)
.filter(vocab -> unitId2GradeId.containsKey(vocab.getUnitId()))
.collect(Collectors.groupingBy(
vocab -> unitId2GradeId.get(vocab.getUnitId())
));
// 核心数据结构GradeId -> List<WordId> (试卷里包含的每个年级的单词有哪些)
Map<Integer, Set<Integer>> 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<Integer, Set<Integer>> map, ExamWordsJudgeResultDO result, Integer targetGrade) {
Set<Integer> 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<Integer, Set<Integer>> map, ExamWordsJudgeResultDO result, Integer maxGradeThreshold) {
Set<Integer> 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 @Override
public List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize) { public List<ExamWordsJudgeResultDO> getExamWordsJudgeResult(Integer page, Integer pageSize) {
return examWordsJudgeResultDOMapper.selectByPage((page - 1) * pageSize, pageSize); return examWordsJudgeResultDOMapper.selectByPage((page - 1) * pageSize, pageSize);

View File

@@ -81,6 +81,7 @@ public class StudentServiceImpl implements StudentService {
.name(name) .name(name)
.classId(classId) .classId(classId)
.gradeId(gradeId) .gradeId(gradeId)
.actualGradeId(gradeId)
.startTime(createTime) .startTime(createTime)
.build(); .build();
studentDOMapper.insert(studentDO); studentDOMapper.insert(studentDO);

View File

@@ -3,7 +3,6 @@ package com.yinlihupo.enlish.service.utils;
import com.yinlihupo.enlish.service.constant.ExamWordsConstant; import com.yinlihupo.enlish.service.constant.ExamWordsConstant;
import com.yinlihupo.enlish.service.model.bo.CoordinatesXY; import com.yinlihupo.enlish.service.model.bo.CoordinatesXY;
import com.yinlihupo.enlish.service.model.bo.StudentExamId; import com.yinlihupo.enlish.service.model.bo.StudentExamId;
import com.yinlihupo.enlish.service.model.bo.Word;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.sourceforge.tess4j.ITesseract; import net.sourceforge.tess4j.ITesseract;
import net.sourceforge.tess4j.Tesseract; import net.sourceforge.tess4j.Tesseract;
@@ -13,7 +12,6 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import org.opencv.core.*; import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs; import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc; import org.opencv.imgproc.Imgproc;
import org.springframework.beans.factory.annotation.Value;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte; import java.awt.image.DataBufferByte;

View File

@@ -1,5 +1,5 @@
server: server:
port: 8080 # 项目启动的端口 port: 8081 # 项目启动的端口
spring: spring:
profiles: profiles:

View File

@@ -7,6 +7,7 @@
<result column="level" jdbcType="INTEGER" property="level" /> <result column="level" jdbcType="INTEGER" property="level" />
<result column="title" jdbcType="VARCHAR" property="title" /> <result column="title" jdbcType="VARCHAR" property="title" />
<result column="created_at" jdbcType="TIMESTAMP" property="createdAt" /> <result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
<result column="type" jdbcType="INTEGER" property="type" />
</resultMap> </resultMap>
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="com.yinlihupo.enlish.service.domain.dataobject.ExamWordsDO"> <resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="com.yinlihupo.enlish.service.domain.dataobject.ExamWordsDO">
<result column="word_ids" jdbcType="LONGVARCHAR" property="wordIds" typeHandler="com.yinlihupo.enlish.service.config.ListWordIdTypeHandler" /> <result column="word_ids" jdbcType="LONGVARCHAR" property="wordIds" typeHandler="com.yinlihupo.enlish.service.config.ListWordIdTypeHandler" />

View File

@@ -11,7 +11,7 @@
<result column="wrong_word_count" jdbcType="INTEGER" property="wrongWordCount" /> <result column="wrong_word_count" jdbcType="INTEGER" property="wrongWordCount" />
<result column="is_finished" jdbcType="INTEGER" property="isFinished" /> <result column="is_finished" jdbcType="INTEGER" property="isFinished" />
<result column="start_date" jdbcType="TIMESTAMP" property="startDate" /> <result column="start_date" jdbcType="TIMESTAMP" property="startDate" />
<result column="error_msg" jdbcType="VARCHAR" property="errorMsg" /> <result column="msg" jdbcType="VARCHAR" property="msg" />
</resultMap> </resultMap>
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="com.yinlihupo.enlish.service.domain.dataobject.ExamWordsJudgeResultDO"> <resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="com.yinlihupo.enlish.service.domain.dataobject.ExamWordsJudgeResultDO">
@@ -31,10 +31,10 @@
limit #{count} limit #{count}
</select> </select>
<update id="updateErrorMsg"> <update id="updateMsg">
update exam_words_judge_result update exam_words_judge_result
set error_msg = #{errorMsg} set msg = #{msg},
and is_finished = 2 is_finished = 2
where id = #{id} where id = #{id}
</update> </update>
@@ -46,7 +46,8 @@
wrong_word_ids = #{examWordsJudgeResultDO.wrongWordIds, typeHandler=com.yinlihupo.enlish.service.config.ListWordIdTypeHandler}, wrong_word_ids = #{examWordsJudgeResultDO.wrongWordIds, typeHandler=com.yinlihupo.enlish.service.config.ListWordIdTypeHandler},
correct_word_count = #{examWordsJudgeResultDO.correctWordCount}, correct_word_count = #{examWordsJudgeResultDO.correctWordCount},
wrong_word_count = #{examWordsJudgeResultDO.wrongWordCount}, wrong_word_count = #{examWordsJudgeResultDO.wrongWordCount},
is_finished = 1 is_finished = 1,
msg = #{examWordsJudgeResultDO.msg}
where id = #{examWordsJudgeResultDO.id} where id = #{examWordsJudgeResultDO.id}
</update> </update>

View File

@@ -19,6 +19,17 @@
where unit_id = #{unitId} where unit_id = #{unitId}
</select> </select>
<select id="selectByUnitIds" resultMap="BaseResultMap">
select *
from grade_unit
<if test="unitIds != null">
where unit_id in
<foreach collection="unitIds" item="unitId" separator="," open="(" close=")">
#{unitId}
</foreach>
</if>
</select>
<insert id="insert"> <insert id="insert">
insert into grade_unit (grade_id, unit_id) insert into grade_unit (grade_id, unit_id)
values (#{gradeId}, #{unitId}) values (#{gradeId}, #{unitId})

View File

@@ -10,6 +10,7 @@
<result column="current_vocabulary_size" jdbcType="INTEGER" property="currentVocabularySize" /> <result column="current_vocabulary_size" jdbcType="INTEGER" property="currentVocabularySize" />
<result column="is_deleted" jdbcType="INTEGER" property="isDeleted"/> <result column="is_deleted" jdbcType="INTEGER" property="isDeleted"/>
<result column="start_time" jdbcType="TIMESTAMP" property="startTime" /> <result column="start_time" jdbcType="TIMESTAMP" property="startTime" />
<result column="actual_grade_id" jdbcType="INTEGER" property="actualGradeId" />
</resultMap> </resultMap>
<select id="selectStudentDOListByClassIdAndGradeId" resultMap="BaseResultMap"> <select id="selectStudentDOListByClassIdAndGradeId" resultMap="BaseResultMap">
@@ -52,8 +53,8 @@
<insert id="insert" useGeneratedKeys="true" keyProperty="id"> <insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into student insert into student
(name, class_id, grade_id, is_deleted, start_time) (name, class_id, grade_id, is_deleted, start_time, actual_grade_id)
values (#{name}, #{classId}, #{gradeId}, 0, #{startTime}) values (#{name}, #{classId}, #{gradeId}, 0, #{startTime}, #{actualGradeId})
</insert> </insert>
<update id="deleteById"> <update id="deleteById">
@@ -62,6 +63,12 @@
where id = #{id} where id = #{id}
</update> </update>
<update id="updateStudentActualGradeId">
update student
set actual_grade_id = #{gradeId}
where id = #{studentId}
</update>
<select id="selectStudentCountByClassId" resultType="java.lang.Integer"> <select id="selectStudentCountByClassId" resultType="java.lang.Integer">
select count(1) select count(1)
from student from student

View File

@@ -13,9 +13,7 @@
insert into student_exam_words insert into student_exam_words
(student_id, exam_words_id, is_completed, start_data) (student_id, exam_words_id, is_completed, start_data)
values values
<foreach collection="studentIds" item="studentId" separator=",">
(#{studentId}, #{examWordsId}, 0, now()) (#{studentId}, #{examWordsId}, 0, now())
</foreach>
</insert> </insert>
<select id="selectByStudentIdAndExamWordsId" resultMap="BaseResultMap"> <select id="selectByStudentIdAndExamWordsId" resultMap="BaseResultMap">

View File

@@ -117,9 +117,22 @@
</if> </if>
limit #{wordCount} limit #{wordCount}
</select> </select>
<select id="selectWordTotal" resultType="java.lang.Integer"> <select id="selectWordTotal" resultType="java.lang.Integer">
select count(*) select count(*)
from vocabulary_bank from vocabulary_bank
</select> </select>
<select id="selectVocabularyBankListByGradeIdRandom" resultMap="BaseResultMap">
select *
from vocabulary_bank
where unit_id in (
select unit_id
from grade_unit
where grade_id = #{gradeId}
)
order by rand()
limit #{wordCount}
</select>
</mapper> </mapper>

View File

@@ -1,7 +1,9 @@
package com.yinlihupo.enlish.service.mapper; package com.yinlihupo.enlish.service.mapper;
import com.yinlihupo.enlish.service.domain.dataobject.GradeUnitDO;
import com.yinlihupo.enlish.service.domain.dataobject.UnitDO; import com.yinlihupo.enlish.service.domain.dataobject.UnitDO;
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO; import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
import com.yinlihupo.enlish.service.domain.mapper.GradeUnitDOMapper;
import com.yinlihupo.enlish.service.domain.mapper.UnitDOMapper; import com.yinlihupo.enlish.service.domain.mapper.UnitDOMapper;
import com.yinlihupo.enlish.service.domain.mapper.VocabularyBankDOMapper; import com.yinlihupo.enlish.service.domain.mapper.VocabularyBankDOMapper;
import com.yinlihupo.enlish.service.domain.mapper.WordMasteryLogDOMapper; import com.yinlihupo.enlish.service.domain.mapper.WordMasteryLogDOMapper;
@@ -29,11 +31,13 @@ public class TestVocabularyBankInsert {
private UnitDOMapper unitDOMapper; private UnitDOMapper unitDOMapper;
@Resource @Resource
private WordMasteryLogDOMapper wordMasteryLogDOMapper; private WordMasteryLogDOMapper wordMasteryLogDOMapper;
@Resource
private GradeUnitDOMapper gradeUnitDOMapper;
@Test @Test
void test() { void test() {
String file = "C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\test\\java\\com\\yinlihupo\\enlish\\service\\mapper\\min.xlsx"; String file = "C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\test\\java\\com\\yinlihupo\\enlish\\service\\mapper\\3上.xlsx";
HashMap<String, Integer> map = new HashMap<>(); HashMap<String, Integer> map = new HashMap<>();
int gradeId = 3;
try (FileInputStream fis = new FileInputStream(file); Workbook workbook = new XSSFWorkbook(fis)) { try (FileInputStream fis = new FileInputStream(file); Workbook workbook = new XSSFWorkbook(fis)) {
Sheet sheet = workbook.getSheetAt(0); Sheet sheet = workbook.getSheetAt(0);
@@ -44,10 +48,14 @@ public class TestVocabularyBankInsert {
continue; continue;
} }
String gradeUnit = row.getCell(0).getStringCellValue();
String word = row.getCell(1).getStringCellValue(); String word = row.getCell(0).getStringCellValue();
String phonetic = row.getCell(2) != null ? row.getCell(2).getStringCellValue() : ""; String meaning = row.getCell(1) != null ? row.getCell(1).getStringCellValue() : "";
String meaning = row.getCell(3) != null ? row.getCell(3).getStringCellValue() : ""; String gradeUnit = row.getCell(2) != null ? row.getCell(2).getStringCellValue() : "";
if (word.contains("Unit")) {
continue;
}
int gradeUnitId = 0; int gradeUnitId = 0;
if (map.containsKey(gradeUnit)) { if (map.containsKey(gradeUnit)) {
@@ -61,6 +69,7 @@ public class TestVocabularyBankInsert {
.createAt(LocalDateTime.now()) .createAt(LocalDateTime.now())
.build(); .build();
unitDOMapper.insert(unitDO); unitDOMapper.insert(unitDO);
gradeUnitDOMapper.insert(GradeUnitDO.builder().unitId(unitDO.getId()).gradeId(gradeId).build());
gradeUnitId = unitDO.getId(); gradeUnitId = unitDO.getId();
} else { } else {
gradeUnitId = unitDO.getId(); gradeUnitId = unitDO.getId();
@@ -71,7 +80,7 @@ public class TestVocabularyBankInsert {
VocabularyBankDO vocabularyBankDO = VocabularyBankDO.builder() VocabularyBankDO vocabularyBankDO = VocabularyBankDO.builder()
.word(word) .word(word)
.definition(meaning) .definition(meaning)
.pronunciation(phonetic) .pronunciation("")
.unitId(gradeUnitId) .unitId(gradeUnitId)
.build(); .build();
vocabularyBankMapper.insertSelective(vocabularyBankDO); vocabularyBankMapper.insertSelective(vocabularyBankDO);

View File

@@ -26,7 +26,7 @@ public class ExamTest {
private VocabularyService vocabularyService; private VocabularyService vocabularyService;
@Test @Test
public void test() { public void test() {
ExamWordsDO examWordsDO = examWordsService.generateExamWords(5, 0, List.of(1)); ExamWordsDO examWordsDO = examWordsService.generateExamWords(5, 0, 1, 0);
log.info("{}", examWordsDO); log.info("{}", examWordsDO);
List<VocabularyBankDO> vocabularyBankDOS = vocabularyService.findVocabularyBankDOListById(examWordsDO.getWordIds()); List<VocabularyBankDO> vocabularyBankDOS = vocabularyService.findVocabularyBankDOListById(examWordsDO.getWordIds());
List<Word> assessmentWords = vocabularyBankDOS.stream().map(vocabularyBankDO -> Word.builder() List<Word> assessmentWords = vocabularyBankDOS.stream().map(vocabularyBankDO -> Word.builder()

View File

@@ -2,16 +2,10 @@
<el-dialog v-model="visible" title="生成试题" width="520px" :close-on-click-modal="false"> <el-dialog v-model="visible" title="生成试题" width="520px" :close-on-click-modal="false">
<div class="space-y-4" v-loading="loading"> <div class="space-y-4" v-loading="loading">
<el-form label-width="80px"> <el-form label-width="80px">
<el-form-item label="年级"> <el-form-item label="类型">
<el-select v-model="gradeId" placeholder="请选择年级" style="width: 240px"> <el-select v-model="type" placeholder="请选择类型" style="width: 240px">
<el-option v-for="g in gradeOptions" :key="g.id" :label="g.title" :value="g.id" /> <el-option :label="'摸底'" :value="1" />
</el-select> <el-option :label="'期中|期末'" :value="2" />
</el-form-item>
<el-form-item label="难度">
<el-select v-model="level" placeholder="请选择难度" style="width: 240px">
<el-option :label="'一级'" :value="1" />
<el-option :label="'二级'" :value="2" />
<el-option :label="'三级'" :value="3" />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -22,7 +16,7 @@
<template #footer> <template #footer>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<el-button @click="visible = false">取消</el-button> <el-button @click="visible = false">取消</el-button>
<el-button type="primary" :disabled="!gradeId || !level" @click="handleGenerate">生成并下载</el-button> <el-button type="primary" :disabled="!type" @click="handleGenerate">生成并下载</el-button>
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
@@ -49,7 +43,7 @@ const loading = ref(false)
const gradeOptions = ref([]) const gradeOptions = ref([])
const gradeId = ref(null) const gradeId = ref(null)
const level = ref(null) const level = ref(null)
const type = ref(null)
async function fetchGrades() { async function fetchGrades() {
loading.value = true loading.value = true
try { try {
@@ -65,11 +59,12 @@ async function fetchGrades() {
} }
async function handleGenerate() { async function handleGenerate() {
if (!gradeId.value || !level.value || props.studentIds.length === 0) return if (!type.value) return
await generateExamWords({ await generateExamWords({
gradeId: Number(gradeId.value), gradeId: Number(gradeId.value),
level: Number(level.value), level: Number(level.value),
studentIds: props.studentIds type: Number(type.value),
studentId: props.studentIds[0]
}) })
ElMessage.success('生成任务已提交,正在下载') ElMessage.success('生成任务已提交,正在下载')
visible.value = false visible.value = false

View File

@@ -45,7 +45,7 @@
selectedGradeId }})</el-tag> selectedGradeId }})</el-tag>
<el-button type="primary" @click="fetchStudents">查询</el-button> <el-button type="primary" @click="fetchStudents">查询</el-button>
<el-button @click="resetStudentFilters">重置</el-button> <el-button @click="resetStudentFilters">重置</el-button>
<el-button type="success" :disabled="selectedStudentIds.length === 0" <el-button type="success" :disabled="selectedStudentIds.length !== 1"
@click="showGenerateDialog = true"> @click="showGenerateDialog = true">
生成试题 生成试题
</el-button> </el-button>

View File

@@ -16,6 +16,7 @@
<el-descriptions-item label="姓名">{{ detail.name }}</el-descriptions-item> <el-descriptions-item label="姓名">{{ detail.name }}</el-descriptions-item>
<el-descriptions-item label="班级">{{ detail.className }}</el-descriptions-item> <el-descriptions-item label="班级">{{ detail.className }}</el-descriptions-item>
<el-descriptions-item label="年级">{{ detail.gradeName }}</el-descriptions-item> <el-descriptions-item label="年级">{{ detail.gradeName }}</el-descriptions-item>
<el-descriptions-item label="学生实际水平年级">{{ detail.actualGrade }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</template> </template>
<template v-else> <template v-else>

View File

@@ -39,7 +39,7 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="startDate" label="开始时间" min-width="160" /> <el-table-column prop="startDate" label="开始时间" min-width="160" />
<el-table-column prop="errorMsg" label="错误信息" min-width="160" /> <el-table-column prop="msg" label="判卷结算" min-width="160" />
</el-table> </el-table>
<div class="mt-4 flex justify-end"> <div class="mt-4 flex justify-end">
<el-pagination <el-pagination

View File

@@ -26,7 +26,7 @@ export default defineConfig({
server: { server: {
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:8080', target: 'http://localhost:8081',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''), rewrite: (path) => path.replace(/^\/api/, ''),
}, },