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_BASELINE = 1;
public static final int EXAM_TYPE_MIDTERM = 2;
public static final int EXAM_TYPE_FINAL = 3;
public static int getZoneA(int gradeId) { public static int getZoneA(int gradeId) {
return switch (gradeId) { return switch (gradeId) {

View File

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

View File

@@ -25,4 +25,6 @@ public interface UnitDOMapper {
List<UnitDO> selectUnitDOList(@Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize); List<UnitDO> selectUnitDOList(@Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
Integer selectUnitDOListCount(); 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); List<VocabularyBankDO> selectVocabularyBankListByGradeIdRandom(@Param("gradeId") Integer gradeId, @Param("wordCount") Integer wordCount);
Integer selectWordTotal(); Integer selectWordTotal();
List<VocabularyBankDO> selectByUnitIds(@Param("unitIds") List<Integer> unitIds);
} }

View File

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

View File

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

View File

@@ -1,14 +1,12 @@
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.constant.ExamWordsConstant;
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsDO; import com.yinlihupo.enlish.service.domain.dataobject.*;
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.mapper.*; import com.yinlihupo.enlish.service.domain.mapper.*;
import com.yinlihupo.enlish.service.service.ExamWordsService; import com.yinlihupo.enlish.service.service.ExamWordsService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -16,6 +14,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@@ -26,8 +25,6 @@ import java.util.UUID;
@Slf4j @Slf4j
public class ExamWordsServiceImpl implements ExamWordsService { public class ExamWordsServiceImpl implements ExamWordsService {
@Resource
private GradeUnitDOMapper gradeUnitDOMapper;
@Resource @Resource
private VocabularyBankDOMapper vocabularyBankDOMapper; private VocabularyBankDOMapper vocabularyBankDOMapper;
@Resource @Resource
@@ -38,6 +35,8 @@ public class ExamWordsServiceImpl implements ExamWordsService {
private ExamWordsJudgeResultDOMapper examWordsJudgeResultDOMapper; private ExamWordsJudgeResultDOMapper examWordsJudgeResultDOMapper;
@Resource @Resource
private StudentDOMapper studentDOMapper; private StudentDOMapper studentDOMapper;
@Resource
private UnitDOMapper unitDOMapper;
@Value("${templates.count}") @Value("${templates.count}")
private Integer wordCount; private Integer wordCount;
@@ -46,7 +45,7 @@ public class ExamWordsServiceImpl implements ExamWordsService {
@Override @Override
@Transactional(rollbackFor = RuntimeException.class) @Transactional(rollbackFor = RuntimeException.class)
public ExamWordsDO generateExamWords(Integer gradeId, Integer level, Integer studentId, Integer type) { public ExamWordsDO generateExamWords(Integer studentId, Integer type) {
ExamWordsDO examWordsDO; ExamWordsDO examWordsDO;
@@ -54,9 +53,10 @@ public class ExamWordsServiceImpl implements ExamWordsService {
if (type == ExamWordsConstant.EXAM_TYPE_BASELINE) { if (type == ExamWordsConstant.EXAM_TYPE_BASELINE) {
log.info("生成摸底测试"); log.info("生成摸底测试");
examWordsDO = generateBaselineExamWords(studentId); examWordsDO = generateBaselineExamWords(studentId);
} else if (type == ExamWordsConstant.EXAM_TYPE_MIDTERM) {
examWordsDO = generateMidtermExamWords(studentId);
} else { } else {
// todo 生成期中考试待实现 examWordsDO = generateFinalExamWords(studentId);
examWordsDO = null;
} }
return examWordsDO; return examWordsDO;
@@ -95,11 +95,54 @@ public class ExamWordsServiceImpl implements ExamWordsService {
.gradeId(gradeId) .gradeId(gradeId)
.level(1) .level(1)
.type(ExamWordsConstant.EXAM_TYPE_BASELINE) .type(ExamWordsConstant.EXAM_TYPE_BASELINE)
.title("摸低测试测试" + studentDO.getName()) .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();
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); int insert = examWordsDOMapper.insert(examWordsDO);
if (insert <= 0) { if (insert <= 0) {
throw new RuntimeException("插入考试失败"); throw new RuntimeException("插入考试失败");

View File

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

View File

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

View File

@@ -135,4 +135,15 @@
limit #{wordCount} limit #{wordCount}
</select> </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> </mapper>

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, 1, 0); ExamWordsDO examWordsDO = examWordsService.generateExamWords(5, 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

@@ -5,7 +5,8 @@
<el-form-item label="类型"> <el-form-item label="类型">
<el-select v-model="type" placeholder="请选择类型" style="width: 240px"> <el-select v-model="type" placeholder="请选择类型" style="width: 240px">
<el-option :label="'摸底'" :value="1" /> <el-option :label="'摸底'" :value="1" />
<el-option :label="'期中|期末'" :value="2" /> <el-option :label="'期中'" :value="2" />
<el-option :label="'期末'" :value="3" />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -61,8 +62,6 @@ async function fetchGrades() {
async function handleGenerate() { async function handleGenerate() {
if (!type.value) return if (!type.value) return
await generateExamWords({ await generateExamWords({
gradeId: Number(gradeId.value),
level: Number(level.value),
type: Number(type.value), type: Number(type.value),
studentId: props.studentIds[0] studentId: props.studentIds[0]
}) })