refactor(exam): 优化考试单词生成逻辑并新增期中期末类型

- 调整考试类型选择,增加“期中”和“期末”选项
- 删除旧的gradeId和level参数,简化接口参数为studentId和type
- 新增考试类型常量:期中(2)、期末(3)
- 实现期中考试和期末考试生成逻辑,分别根据年级及单元名称筛选词汇
- 调整服务层方法签名及调用,支持新考试类型生成流程
- 扩展Mapper接口,支持按单元名称和单元ID查询词汇
- 优化导出逻辑,导出文件名和压缩包名称根据考试标题动态生成
- 调整测试代码,适配新的方法参数和实现细节
This commit is contained in:
lbw
2025-12-25 18:05:43 +08:00
parent 7b68184787
commit 0b0311d2d9
12 changed files with 92 additions and 31 deletions

View File

@@ -32,6 +32,8 @@ public class ExamWordsConstant {
public static final int EXAM_TYPE_BASELINE = 1;
public static final int EXAM_TYPE_MIDTERM = 2;
public static final int EXAM_TYPE_FINAL = 3;
public static int getZoneA(int gradeId) {
return switch (gradeId) {

View File

@@ -42,15 +42,13 @@ public class ExamWordsController {
@PostMapping("generate")
public void generateFeltExamWords(@RequestBody GenerateExamWordsReqVO generateExamWordsReqVO, HttpServletResponse response) {
Integer gradeId = generateExamWordsReqVO.getGradeId();
Integer level = generateExamWordsReqVO.getLevel();
Integer type = generateExamWordsReqVO.getType();
Integer studentId = generateExamWordsReqVO.getStudentId();
if (studentId == null || gradeId == null || level == null) {
if (studentId == null) {
throw new RuntimeException("参数错误");
}
try {
ExamWordsDO examWordsDO = examWordsService.generateExamWords(gradeId, level, studentId, type);
ExamWordsDO examWordsDO = examWordsService.generateExamWords(studentId, type);
if (examWordsDO == null || examWordsDO.getWordIds().isEmpty()) {
throw new RuntimeException("没有单词");
}
@@ -77,7 +75,7 @@ public class ExamWordsController {
return data;
}).toList();
WordExportUtil.generateExamWords(maps, response, templateWordPath);
WordExportUtil.generateExamWords(maps, examWordsDO, response, templateWordPath);
} catch (Exception e) {

View File

@@ -25,4 +25,6 @@ public interface UnitDOMapper {
List<UnitDO> selectUnitDOList(@Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
Integer selectUnitDOListCount();
List<UnitDO> selectByUnitName(@Param("unitName") String unitName);
}

View File

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

View File

@@ -14,8 +14,6 @@ import java.util.List;
@Builder
public class GenerateExamWordsReqVO {
private Integer gradeId;
private Integer level;
private Integer type;
private Integer studentId;
}

View File

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

View File

@@ -1,14 +1,12 @@
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.ExamWordsJudgeResultDO;
import com.yinlihupo.enlish.service.domain.dataobject.StudentDO;
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
import com.yinlihupo.enlish.service.domain.dataobject.*;
import com.yinlihupo.enlish.service.domain.mapper.*;
import com.yinlihupo.enlish.service.service.ExamWordsService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -16,6 +14,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
@@ -26,8 +25,6 @@ import java.util.UUID;
@Slf4j
public class ExamWordsServiceImpl implements ExamWordsService {
@Resource
private GradeUnitDOMapper gradeUnitDOMapper;
@Resource
private VocabularyBankDOMapper vocabularyBankDOMapper;
@Resource
@@ -38,6 +35,8 @@ public class ExamWordsServiceImpl implements ExamWordsService {
private ExamWordsJudgeResultDOMapper examWordsJudgeResultDOMapper;
@Resource
private StudentDOMapper studentDOMapper;
@Resource
private UnitDOMapper unitDOMapper;
@Value("${templates.count}")
private Integer wordCount;
@@ -46,7 +45,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 studentId, Integer type) {
ExamWordsDO examWordsDO;
@@ -54,9 +53,10 @@ public class ExamWordsServiceImpl implements ExamWordsService {
if (type == ExamWordsConstant.EXAM_TYPE_BASELINE) {
log.info("生成摸底测试");
examWordsDO = generateBaselineExamWords(studentId);
} else {
// todo 生成期中考试待实现
examWordsDO = null;
} else if (type == ExamWordsConstant.EXAM_TYPE_MIDTERM) {
examWordsDO = generateMidtermExamWords(studentId);
} else {
examWordsDO = generateFinalExamWords(studentId);
}
return examWordsDO;
@@ -95,11 +95,54 @@ public class ExamWordsServiceImpl implements ExamWordsService {
.gradeId(gradeId)
.level(1)
.type(ExamWordsConstant.EXAM_TYPE_BASELINE)
.title("摸低测试测试" + studentDO.getName())
.title("摸低测试" + studentDO.getName())
.createdAt(LocalDateTime.now())
.wordIds(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList())
.build();
return getExamWordsDO(studentId, examWordsDO);
}
private ExamWordsDO generateMidtermExamWords(Integer studentId) {
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
Integer gradeId = studentDO.getGradeId();
List<UnitDO> unitDOS = unitDOMapper.selectByUnitName(ExamWordsConstant.getGradeName(gradeId) + "");
ExamWordsDO examWordsDO = getExamWordsDO(studentId, studentDO, gradeId, unitDOS);
examWordsDO.setTitle("期中测试" + studentDO.getName());
return getExamWordsDO(studentId, examWordsDO);
}
private ExamWordsDO generateFinalExamWords(Integer studentId) {
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
Integer gradeId = studentDO.getGradeId();
List<UnitDO> unitDOS = unitDOMapper.selectByUnitName(ExamWordsConstant.getGradeName(gradeId));
ExamWordsDO examWordsDO = getExamWordsDO(studentId, studentDO, gradeId, unitDOS);
examWordsDO.setTitle("期末测试" + studentDO.getName());
return getExamWordsDO(studentId, examWordsDO);
}
@NonNull
private ExamWordsDO getExamWordsDO(Integer studentId, StudentDO studentDO, Integer gradeId, List<UnitDO> unitDOS) {
if (unitDOS.isEmpty()) {
throw new RuntimeException("没有找到对应的单元");
}
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectByUnitIds(unitDOS.stream().map(UnitDO::getId).toList());
ExamWordsDO examWordsDO = ExamWordsDO.builder()
.gradeId(gradeId)
.level(1)
.type(ExamWordsConstant.EXAM_TYPE_BASELINE)
.title(studentDO.getName())
.createdAt(LocalDateTime.now())
.wordIds(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList())
.build();
return getExamWordsDO(studentId, examWordsDO);
}
@NonNull
private ExamWordsDO getExamWordsDO(Integer studentId, ExamWordsDO examWordsDO) {
int insert = examWordsDOMapper.insert(examWordsDO);
if (insert <= 0) {
throw new RuntimeException("插入考试失败");

View File

@@ -3,6 +3,7 @@ package com.yinlihupo.enlish.service.utils;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsDO;
import jakarta.servlet.http.HttpServletResponse;
import java.io.*;
@@ -48,7 +49,7 @@ public class WordExportUtil {
/**
* 公共入口:根据数据量决定是导出单文件还是压缩包
*/
public static void generateExamWords(List<Map<String, Object>> data, HttpServletResponse response, String templateWordPath) {
public static void generateExamWords(List<Map<String, Object>> data, ExamWordsDO examWordsDO, HttpServletResponse response, String templateWordPath) {
if (data == null || data.isEmpty()) {
return;
}
@@ -56,10 +57,10 @@ public class WordExportUtil {
try {
if (data.size() == 1) {
// 如果只有一份数据,直接导出 docx用户体验更好
generateExamWordsDocx(data.get(0), response, templateWordPath);
generateExamWordsDocx(data.get(0), examWordsDO, response, templateWordPath);
} else {
// 如果有多份数据,打包导出
generateExamWordsZip(data, response, templateWordPath);
generateExamWordsZip(data, examWordsDO, response, templateWordPath);
}
} catch (IOException e) {
e.printStackTrace();
@@ -92,9 +93,9 @@ public class WordExportUtil {
/**
* 核心补充:批量渲染并打包为 ZIP
*/
private static void generateExamWordsZip(List<Map<String, Object>> data, HttpServletResponse response, String templateWordPath) throws IOException {
private static void generateExamWordsZip(List<Map<String, Object>> data, ExamWordsDO examWordsDO, HttpServletResponse response, String templateWordPath) throws IOException {
// 1. 设置响应头为 ZIP
String zipName = URLEncoder.encode("批量导出_摸底测试.zip", StandardCharsets.UTF_8).replaceAll("\\+", "%20");
String zipName = URLEncoder.encode("批量导出_" + examWordsDO.getTitle() + ".zip", StandardCharsets.UTF_8).replaceAll("\\+", "%20");
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + zipName);
@@ -106,7 +107,7 @@ public class WordExportUtil {
for (Map<String, Object> itemData : data) {
// 3. 确定压缩包内的文件名
// 优先从 map 中获取 'fileName' 字段,否则使用默认编号
String entryName = (String) itemData.getOrDefault("fileName", "摸底测试_" + index);
String entryName = (String) itemData.getOrDefault("fileName", + index);
// 确保文件名后缀正确
if (!entryName.endsWith(".docx")) {
entryName += ".docx";
@@ -143,9 +144,9 @@ public class WordExportUtil {
/**
* 现有的单文件导出逻辑
*/
private static void generateExamWordsDocx(Map<String, Object> data, HttpServletResponse response, String templateWordPath) throws IOException {
private static void generateExamWordsDocx(Map<String, Object> data, ExamWordsDO examWordsDO, HttpServletResponse response, String templateWordPath) throws IOException {
String fileName = URLEncoder.encode("摸底测试" + ".docx", StandardCharsets.UTF_8).replaceAll("\\+", "%20");
String fileName = URLEncoder.encode(examWordsDO.getTitle() + ".docx", StandardCharsets.UTF_8).replaceAll("\\+", "%20");
// 3. 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");

View File

@@ -101,5 +101,10 @@
select count(*)
from unit
</select>
<select id="selectByUnitName" resultType="com.yinlihupo.enlish.service.domain.dataobject.UnitDO">
select *
from unit
where title like concat(#{unitName}, '%')
</select>
</mapper>

View File

@@ -135,4 +135,15 @@
limit #{wordCount}
</select>
<select id="selectByUnitIds" resultMap="BaseResultMap">
select *
from vocabulary_bank
where unit_id in
<foreach item="unitId" collection="unitIds" separator="," open="(" close=")">
#{unitId}
</foreach>
order by rand()
limit 100
</select>
</mapper>

View File

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