fix(enlish-service): 优化单词数据处理及模板配置更新

- 修改开发和生产环境配置中的数据库连接及模板文件路径
- ExamWordsController新增单词列表拆分为两部分返回
- ExamWordsServiceImpl增加单词数量不足时补充逻辑,确保单词数量满足要求
- LessonPlansServiceImpl优化教案数据组装,增加班级信息及单词列表拆分功能
- PngUtil调整图像二值化阈值,完善轮廓检测及未背熟单词识别逻辑,移除冗余代码
- SaTokenConfigure更新路由权限配置,添加对tts接口的不拦截支持
- 删除StudentExamWordsDOMapper中is_completed条件,调整查询方式
- UserController修正接口日志注释,准确描述修改用户信息功能
- VocabularyBankDOMapper新增根据年级与排除ID查询单词接口及SQL映射
- WordExportUtil更新导出配置,支持拆分单词列表绑定两个集合以适应新结构
This commit is contained in:
lbw
2026-01-05 18:06:34 +08:00
parent 09b326c07a
commit 7182371c92
17 changed files with 83 additions and 66 deletions

View File

@@ -25,11 +25,13 @@ public class SaTokenConfigure implements WebMvcConfigurer {
SaRouter.match("/**") SaRouter.match("/**")
.notMatch("/login/**") .notMatch("/login/**")
.notMatch("plan/word/voice") .notMatch("/plan/word/voice")
.notMatch("/plan/word/voice/tts")
.check(r -> StpUtil.checkLogin()); .check(r -> StpUtil.checkLogin());
SaRouter.match("/admin/**") SaRouter.match("/admin/**")
.notMatch("plan/word/voice") .notMatch("/plan/word/voice")
.notMatch("/plan/word/voice/tts")
.check(r -> StpUtil.checkRole("root")); .check(r -> StpUtil.checkRole("root"));
})) }))

View File

@@ -72,6 +72,12 @@ public class ExamWordsController {
data.put("examStr", examWordsDO.getTitle()); data.put("examStr", examWordsDO.getTitle());
data.put("words", assessmentWords); data.put("words", assessmentWords);
data.put("answer", assessmentWords); data.put("answer", assessmentWords);
List<Word> words1 = assessmentWords.subList(0, assessmentWords.size() / 2);
List<Word> words2 = assessmentWords.subList(assessmentWords.size() / 2, assessmentWords.size());
data.put("words1", words1);
data.put("words2", words2);
return data; return data;
}).toList(); }).toList();

View File

@@ -35,7 +35,7 @@ public class UserController {
} }
@PostMapping("update-user-info") @PostMapping("update-user-info")
@ApiOperationLog(description = "修改密码") @ApiOperationLog(description = "修改用户信息")
public Response<String> updatePassword(@RequestBody UpdateUserInfoReqVO updateUserInfoReqVO) { public Response<String> updatePassword(@RequestBody UpdateUserInfoReqVO updateUserInfoReqVO) {
try { try {
String code = updateUserInfoReqVO.getCode(); String code = updateUserInfoReqVO.getCode();

View File

@@ -26,4 +26,6 @@ public interface VocabularyBankDOMapper {
Integer selectWordTotal(); Integer selectWordTotal();
List<VocabularyBankDO> selectByUnitIds(@Param("unitIds") List<Integer> unitIds); List<VocabularyBankDO> selectByUnitIds(@Param("unitIds") List<Integer> unitIds);
List<VocabularyBankDO> selectByGradeIdAndNotMatchIds(@Param("gradeId") Integer gradeId, @Param("ids") List<Integer> ids, @Param("wordCount") Integer wordCount);
} }

View File

@@ -47,7 +47,6 @@ public class ExamWordsServiceImpl implements ExamWordsService {
@Transactional(rollbackFor = RuntimeException.class) @Transactional(rollbackFor = RuntimeException.class)
public ExamWordsDO generateExamWords(Integer studentId, Integer type) { public ExamWordsDO generateExamWords(Integer studentId, Integer type) {
ExamWordsDO examWordsDO; ExamWordsDO examWordsDO;
if (type == ExamWordsConstant.EXAM_TYPE_BASELINE) { if (type == ExamWordsConstant.EXAM_TYPE_BASELINE) {
@@ -61,6 +60,16 @@ public class ExamWordsServiceImpl implements ExamWordsService {
examWordsDO = generateFinalExamWords(studentId); examWordsDO = generateFinalExamWords(studentId);
} }
List<Integer> wordIds = new ArrayList<>(examWordsDO.getWordIds());
if (wordIds.size() < wordCount) {
log.info("单词数量不足,补充单词");
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectByGradeIdAndNotMatchIds(studentDO.getGradeId(), wordIds, wordCount - wordIds.size());
List<Integer> list = new ArrayList<>(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList());
wordIds.addAll(list);
examWordsDO.setWordIds(wordIds);
}
return examWordsDO; return examWordsDO;
} }

View File

@@ -50,7 +50,8 @@ public class LessonPlansServiceImpl implements LessonPlansService {
private PlanExamDOMapper planExamDOMapper; private PlanExamDOMapper planExamDOMapper;
@Resource @Resource
private RedisTemplate<String, Object> redisTemplate; private RedisTemplate<String, Object> redisTemplate;
@Resource
private ClassDOMapper classDOMapper;
@Override @Override
public void generateLessonPlans(Integer studentId, Integer unitId, Integer wordSize) { public void generateLessonPlans(Integer studentId, Integer unitId, Integer wordSize) {
@@ -227,11 +228,16 @@ public class LessonPlansServiceImpl implements LessonPlansService {
examWordsDOMapper.insert(examWordsDO); examWordsDOMapper.insert(examWordsDO);
studentExamWordsDOMapper.insertStudentsExam(studentId, examWordsDO.getId()); studentExamWordsDOMapper.insertStudentsExam(studentId, examWordsDO.getId());
ClassDO classDO = classDOMapper.selectClassDOById(studentDOMapper.selectStudentById(studentId).getClassId());
data.put("examId", examWordsDO.getId()); data.put("examId", examWordsDO.getId());
data.put("studentId", studentId); data.put("studentId", studentId);
data.put("studentStr", studentDO.getName()); data.put("studentStr", gradeDO.getTitle() + " " + classDO.getTitle() + " " + studentDO.getName());
data.put("examStr", ExamTitle); data.put("examStr", ExamTitle);
data.put("checkList", words);
List<VocabularyBankDO> words1 = words.subList(0, words.size() / 2);
List<VocabularyBankDO> words2 = words.subList(words.size() / 2, words.size());
data.put("words1", words1);
data.put("words2", words2);
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy(); // LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
// Configure config = Configure.builder() // Configure config = Configure.builder()
// .bind("checkList", policy) // .bind("checkList", policy)
@@ -250,6 +256,7 @@ public class LessonPlansServiceImpl implements LessonPlansService {
List<VocabularyBankDO> checkList, List<VocabularyBankDO> checkList,
int day, int day,
GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws Exception { GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws Exception {
String title = gradeDO.getTitle() + " " + unitDO.getTitle() + " " + "" + day + ""; String title = gradeDO.getTitle() + " " + unitDO.getTitle() + " " + "" + day + "";
Map<String, Object> data = new HashMap<>(); Map<String, Object> data = new HashMap<>();
data.put("title", title); data.put("title", title);
@@ -258,7 +265,6 @@ public class LessonPlansServiceImpl implements LessonPlansService {
data.put("reviewVocabList", reviewVocabList); data.put("reviewVocabList", reviewVocabList);
data.put("checkList", checkList); data.put("checkList", checkList);
data.put("checkListAns", checkList); data.put("checkListAns", checkList);
// 中译英 // 中译英
List<Word> list = syncVocabList.stream().map(vocabularyBankDO -> Word.builder().title(vocabularyBankDO.getWord()).definition(vocabularyBankDO.getDefinition()).build()).toList(); List<Word> list = syncVocabList.stream().map(vocabularyBankDO -> Word.builder().title(vocabularyBankDO.getWord()).definition(vocabularyBankDO.getDefinition()).build()).toList();
list.forEach(word -> word.setTitle(" ")); list.forEach(word -> word.setTitle(" "));
@@ -344,7 +350,12 @@ public class LessonPlansServiceImpl implements LessonPlansService {
data.put("studentId", studentId); data.put("studentId", studentId);
data.put("studentStr", studentDO.getName()); data.put("studentStr", studentDO.getName());
data.put("examStr", ExamTitle); data.put("examStr", ExamTitle);
data.put("words", words);
List<VocabularyBankDO> words1 = words.subList(0, wordIds.size() / 2);
List<VocabularyBankDO> words2 = words.subList(wordIds.size() / 2, wordIds.size());
data.put("words1", words1);
data.put("words2", words2);
log.info("生成教案小测成功"); log.info("生成教案小测成功");
// LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy(); // LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();

View File

@@ -48,14 +48,14 @@ public class PngUtil {
// 反转后,黑色块变成白色(255),背景变成黑色(0),方便 findContours 查找。 // 反转后,黑色块变成白色(255),背景变成黑色(0),方便 findContours 查找。
Mat binary = new Mat(); Mat binary = new Mat();
// 阈值设为 50 左右即可,因为块是纯黑的 // 阈值设为 50 左右即可,因为块是纯黑的
Imgproc.threshold(gray, binary, 50, 255, Imgproc.THRESH_BINARY_INV); Imgproc.threshold(gray, binary, 80, 255, Imgproc.THRESH_BINARY_INV);
// 4. 查找轮廓 // 4. 查找轮廓
List<MatOfPoint> contours = new ArrayList<>(); List<MatOfPoint> contours = new ArrayList<>();
Mat hierarchy = new Mat(); Mat hierarchy = new Mat();
// RETR_EXTERNAL 只检测最外层轮廓,忽略块内部可能存在的噪点 // RETR_EXTERNAL 只检测最外层轮廓,忽略块内部可能存在的噪点
Imgproc.findContours(binary, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE); Imgproc.findContours(binary, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
//Imgcodecs.imwrite("output_red___v1.png", binary);
System.out.println("检测到的轮廓总数: " + contours.size()); System.out.println("检测到的轮廓总数: " + contours.size());
System.out.println("------------------------------------------------"); System.out.println("------------------------------------------------");
@@ -89,32 +89,18 @@ public class PngUtil {
System.out.println("------------------------------------------------"); System.out.println("------------------------------------------------");
list.add(CoordinatesXY.builder().x(rect.x).y(rect.y).width(rect.width).height(rect.height).build()); list.add(CoordinatesXY.builder().x(rect.x).y(rect.y).width(rect.width).height(rect.height).build());
// 可选:在原图上画出框,用于调试验证 // 可选:在原图上画出框,用于调试验证
// Imgproc.rectangle(src, rect, new Scalar(0, 0, 255), 2); // 红色框 Imgproc.rectangle(src, rect, new Scalar(0, 0, 255), 2); // 红色框
// Imgproc.putText(src, "#" + blockCount, new Point(rect.x, rect.y - 5), Imgproc.FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 0, 255), 1); Imgproc.putText(src, "#" + blockCount, new Point(rect.x, rect.y - 5), Imgproc.FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 0, 255), 1);
// Imgcodecs.imwrite("output_red.png", src); //Imgcodecs.imwrite("output_red.png", src);
} }
} }
System.out.println("找到 " + blockCount + " 个黑色块。"); System.out.println("找到 " + blockCount + " 个黑色块。");
// 获取每一列的宽度
list.sort(Comparator.comparingInt(CoordinatesXY::getHeight));
int height = list.get(list.size() - 1).getHeight() / ExamWordsConstant.PGN_COL;
// 删除两列答题卡区块
list.sort(Comparator.comparingInt(CoordinatesXY::getWidth));
list.remove(list.size() - 1);
list.remove(list.size() - 1);
list.sort(Comparator.comparingInt(CoordinatesXY::getX));
// 计算起始坐标 // 计算起始坐标
List<CoordinatesXY> ans = getCoordinatesXIES(list, height); list.sort(Comparator.comparingInt(CoordinatesXY::getX));
src.release(); return list;
binary.release();
hierarchy.release();
binary.release();
return ans;
} }
// 获取(未背熟)单词的 id // 获取(未背熟)单词的 id
@@ -134,22 +120,18 @@ public class PngUtil {
// 建议:如果光照不均匀,考虑使用 THRESH_OTSU 自动阈值,或者自适应阈值 // 建议:如果光照不均匀,考虑使用 THRESH_OTSU 自动阈值,或者自适应阈值
Imgproc.threshold(gray, binary, 150, 255, Imgproc.THRESH_BINARY_INV); Imgproc.threshold(gray, binary, 150, 255, Imgproc.THRESH_BINARY_INV);
// 调试时打印 // 调试时打印
// Imgcodecs.imwrite("output_binary.png", binary); //Imgcodecs.imwrite("output_binary.png", binary);
List<Integer> answer = new ArrayList<>(); List<Integer> answer = new ArrayList<>();
int words_index = 0; int words_index = 0;
for (int i = 0; i < coordinatesXYList.size(); i++) { for (CoordinatesXY coordinatesXY : coordinatesXYList) {
CoordinatesXY coordinatesXY = coordinatesXYList.get(i);
int width = coordinatesXY.getWidth(); int width = coordinatesXY.getWidth();
int height = coordinatesXY.getHeight(); int height = coordinatesXY.getHeight();
int currentX = coordinatesXY.getX(); int currentX = coordinatesXY.getX();
int currentY = coordinatesXY.getY(); int currentY = coordinatesXY.getY() + height;
int count = i == 0 ? ExamWordsConstant.PGN_COL - 1 : ExamWordsConstant.PGN_COL;
// 内层循环:遍历这一列的每一行 // 内层循环:遍历这一列的每一行
for (int j = 0; j < count; j++) { for (int j = 0; j < 50; j++) {
// 安全检查:防止单词列表比格子少导致越界 // 安全检查:防止单词列表比格子少导致越界
if (words_index >= wordIds.size()) { if (words_index >= wordIds.size()) {
log.warn("单词列表耗尽,停止检测。格子数多于单词数。"); log.warn("单词列表耗尽,停止检测。格子数多于单词数。");
@@ -169,11 +151,14 @@ public class PngUtil {
Mat region = binary.submat(rect); Mat region = binary.submat(rect);
int countNonZero = Core.countNonZero(region); int countNonZero = Core.countNonZero(region);
if (countNonZero > 500) { if (countNonZero > 370) {
Integer id = wordIds.get(words_index); Integer id = wordIds.get(words_index);
answer.add(id); answer.add(id);
log.info("检测到标记未背熟ID={}, 当前坐标 x = {} y = {} ", id, currentX + 1, currentY + 1); log.info("检测到标记未背熟ID={}, 当前坐标 x = {} y = {} ", id, currentX + 1, currentY + 1);
} }
if (countNonZero == 0) {
break;
}
region.release(); region.release();
words_index++; words_index++;
@@ -217,7 +202,7 @@ public class PngUtil {
Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU); Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
// 可选:保存预处理后的图片查看效果 // 可选:保存预处理后的图片查看效果
// Imgcodecs.imwrite("debug_roi.jpg", binary); //Imgcodecs.imwrite("debug_roi.jpg", binary);
// 4. 将 OpenCV Mat 转换为 BufferedImage (供 Tess4J 使用) // 4. 将 OpenCV Mat 转换为 BufferedImage (供 Tess4J 使用)
BufferedImage processedImage = matToBufferedImage(binary); BufferedImage processedImage = matToBufferedImage(binary);
@@ -276,21 +261,4 @@ public class PngUtil {
return image; return image;
} }
private static @NonNull List<CoordinatesXY> getCoordinatesXIES(List<CoordinatesXY> list, int height) {
List<CoordinatesXY> ans = new ArrayList<>();
CoordinatesXY left = new CoordinatesXY();
left.setX(list.get(1).getX());
left.setWidth(list.get(1).getWidth());
left.setHeight(height);
left.setY(list.get(0).getY() + left.getHeight());
ans.add(left);
CoordinatesXY right = new CoordinatesXY();
right.setX(list.get(2).getX());
right.setY(list.get(0).getY());
right.setWidth(list.get(1).getWidth());
right.setHeight(height);
ans.add(right);
return ans;
}
} }

View File

@@ -36,6 +36,8 @@ public class WordExportUtil {
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy(); LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
config = Configure.builder() config = Configure.builder()
.bind("words", policy) .bind("words", policy)
.bind("words1", policy)
.bind("words2", policy)
.bind("answer", policy) .bind("answer", policy)
.build(); .build();
@@ -52,12 +54,14 @@ public class WordExportUtil {
.bind("checkListAns", policyLessonPlanWeekday) .bind("checkListAns", policyLessonPlanWeekday)
.bind("sentences", policyLessonPlanWeekday) .bind("sentences", policyLessonPlanWeekday)
.bind("sentencesAns", policyLessonPlanWeekday) .bind("sentencesAns", policyLessonPlanWeekday)
.bind("words", policyLessonPlanWeekday) .bind("words1", policy)
.bind("words2", policy)
.build(); .build();
LoopRowTableRenderPolicy policyLessonPlan = new LoopRowTableRenderPolicy(); LoopRowTableRenderPolicy policyLessonPlan = new LoopRowTableRenderPolicy();
configLessonPlanWeekend = Configure.builder() configLessonPlanWeekend = Configure.builder()
.bind("checkList", policyLessonPlan) .bind("words1", policy)
.bind("words2", policy)
.build(); .build();
} }

View File

@@ -2,7 +2,7 @@ spring:
datasource: datasource:
driver-class-name: com.mysql.cj.jdbc.Driver # 指定数据库驱动类 driver-class-name: com.mysql.cj.jdbc.Driver # 指定数据库驱动类
# 数据库连接信息 # 数据库连接信息
url: jdbc:mysql://124.220.58.5:3306/enlish?allowMultiQueries=true url: jdbc:mysql://124.220.58.5:3306/dev_english?allowMultiQueries=true
username: root # 数据库用户名 username: root # 数据库用户名
password: YLHP@admin123 # 数据库密码 password: YLHP@admin123 # 数据库密码
data: data:
@@ -31,12 +31,12 @@ spring:
templates: templates:
word: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\assessment_v5.docx word: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\assessment_v9.docx
count: 100 count: 100
data: C:\project\tess data: C:\project\tess
plan: plan:
weekday: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\tem_study_plan_v6.docx weekday: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\tem_study_plan_v7.docx
weekend: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\study_plan_review_v2.docx weekend: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\study_plan_review_v3.docx
plan_day: 7 plan_day: 7
tmp: tmp:
png: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\tmp\png\ png: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\tmp\png\

View File

@@ -31,11 +31,11 @@ spring:
templates: templates:
word: assessment_v5.docx word: assessment_v7.docx
count: 100 count: 100
data: eng.traineddata data: eng.traineddata
plan: plan:
weekday: tem_study_plan_v5.docx weekday: tem_study_plan_v6.docx
weekend: study_plan_review_v2.docx weekend: study_plan_review_v2.docx
plan_day: 7 plan_day: 7
tmp: tmp:

View File

@@ -21,7 +21,6 @@
from student_exam_words from student_exam_words
where student_id = #{studentId} where student_id = #{studentId}
and exam_words_id = #{examWordsId} and exam_words_id = #{examWordsId}
and is_completed = 0
</select> </select>
<update id="updateStudentExamWordsFinished"> <update id="updateStudentExamWordsFinished">

View File

@@ -145,5 +145,21 @@
order by rand() order by rand()
limit 100 limit 100
</select> </select>
<select id="selectByGradeIdAndNotMatchIds"
resultType="com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO">
select *
from vocabulary_bank
where unit_id in (
select unit_id
from grade_unit
where grade_id = #{gradeId}
)
and id not in
<foreach item="id" collection="ids" separator="," open="(" close=")">
#{id}
</foreach>
order by rand()
limit #{wordCount}
</select>
</mapper> </mapper>