package jnpf.tmsService.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import jnpf.audit.sdk.annotation.AuditLog; import jnpf.exception.DataException; import jnpf.permission.UserApi; import jnpf.permission.entity.UserEntity; import jnpf.tmsEntity.TmsQuestionBankEntity; import jnpf.tmsEntity.TmsQuestionEntity; import jnpf.tmsEntity.TmsQuestionOptionEntity; import jnpf.tmsEntity.question.TmsQuestionBankOption; import jnpf.tmsEntity.question.TmsQuestionExportForm; import jnpf.tmsEntity.question.TmsQuestionForm; import jnpf.tmsEntity.question.TmsQuestionImportResult; import jnpf.tmsEntity.question.TmsQuestionOptionForm; import jnpf.tmsEntity.question.TmsQuestionQuery; import jnpf.tmsEntity.enums.TmsOpenClosedInvalid; import jnpf.tmsEntity.enums.TmsQuestionLifeType; import jnpf.tmsEntity.enums.TmsQuestionType; import jnpf.tmsEntity.enums.TmsYesNo; import jnpf.tmsMapper.TmsQuestionBankMapper; import jnpf.tmsMapper.TmsQuestionMapper; import jnpf.tmsMapper.TmsQuestionOptionMapper; import jnpf.tmsService.TmsQuestionService; import jnpf.tmsUtil.TmsExcelExportHelper; import jnpf.tmsUtil.TmsExcelImportHelper; import jnpf.util.DateUtil; import jnpf.util.RandomUtil; import jnpf.util.StringUtil; import jnpf.util.UserProvider; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class TmsQuestionServiceImpl implements TmsQuestionService { /** 导入/导出表头(顺序固定) */ private static final List EXCEL_HEADERS = Arrays.asList( "试题编号", "题库名称", "题型", "难度", "题干", "解析", "选项A", "选项B", "选项C", "选项D", "选项E", "选项F", "正确答案", "状态"); private static final List EXCEL_WIDTHS = Arrays.asList( 80, 140, 70, 60, 280, 160, 100, 100, 100, 100, 100, 100, 100, 70); private static final String SOURCE_IMPORT = "import"; private static final int EXPORT_LIMIT = 5000; private final TmsQuestionMapper questionMapper; private final TmsQuestionOptionMapper optionMapper; private final TmsQuestionBankMapper bankMapper; private final UserApi userApi; /** * 查询题库列表 */ @Override public List listBanks() { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.orderByDesc(TmsQuestionBankEntity::getCreatorTime); List list = bankMapper.selectList(qw); List result = new ArrayList<>(list.size()); for (TmsQuestionBankEntity bank : list) { TmsQuestionBankOption opt = new TmsQuestionBankOption(); opt.setId(bank.getId()); opt.setFullName(bank.getBankName()); result.add(opt); } return result; } /** * 查询管理员列表 */ @Override public List> listAdmins() { QueryWrapper qw = new QueryWrapper<>(); qw.select("DISTINCT admin_user_id"); qw.isNotNull("admin_user_id"); qw.ne("admin_user_id", ""); List rows = questionMapper.selectList(qw); List userIds = rows.stream() .map(TmsQuestionEntity::getAdminUserId) .filter(StringUtil::isNotEmpty) .distinct() .collect(Collectors.toList()); if (userIds.isEmpty()) { return Collections.emptyList(); } Map nameMap = loadUserNames(userIds); List> result = new ArrayList<>(); for (String userId : userIds) { Map item = new HashMap<>(2); item.put("id", userId); item.put("fullName", nameMap.getOrDefault(userId, userId)); result.add(item); } return result; } /** * 试题:条件分页查询列表 */ @Override public List getList(TmsQuestionQuery query) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); if (StringUtil.isNotEmpty(query.getBankId())) { qw.eq(TmsQuestionEntity::getBankId, query.getBankId()); } if (StringUtil.isNotEmpty(query.getQuestionType())) { qw.eq(TmsQuestionEntity::getQuestionType, query.getQuestionType()); } if (StringUtil.isNotEmpty(query.getBizStatus())) { qw.eq(TmsQuestionEntity::getBizStatus, query.getBizStatus()); } if (StringUtil.isNotEmpty(query.getLifeType())) { qw.eq(TmsQuestionEntity::getLifeType, query.getLifeType()); } if (StringUtil.isNotEmpty(query.getAdminUserId())) { qw.eq(TmsQuestionEntity::getAdminUserId, query.getAdminUserId()); } if (StringUtil.isNotEmpty(query.getKeyword()) && !"null".equalsIgnoreCase(query.getKeyword())) { String kw = query.getKeyword().trim(); qw.like(TmsQuestionEntity::getStem, kw); } qw.orderByDesc(TmsQuestionEntity::getCreatorTime); Page page = new Page<>(query.getCurrentPage(), query.getPageSize()); Page result = questionMapper.selectPage(page, qw); List records = result.getRecords(); Map bankNames = loadBankNames( records.stream().map(TmsQuestionEntity::getBankId).filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList())); Map userNames = loadUserNames( records.stream().map(TmsQuestionEntity::getAdminUserId).filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList())); List list = new ArrayList<>(records.size()); for (TmsQuestionEntity entity : records) { list.add(toForm(entity, bankNames, userNames, false)); } query.setData(list, result.getTotal()); return list; } /** * 试题:按主键查询详情 */ @Override public TmsQuestionForm getInfo(String id) { TmsQuestionEntity entity = questionMapper.selectById(id); if (entity == null) { throw new DataException("试题不存在"); } Map bankNames = loadBankNames(Collections.singletonList(entity.getBankId())); Map userNames = loadUserNames( StringUtil.isEmpty(entity.getAdminUserId()) ? Collections.emptyList() : Collections.singletonList(entity.getAdminUserId())); return toForm(entity, bankNames, userNames, true); } /** * 试题:新增 */ @Override @Transactional(rollbackFor = Exception.class) public String create(TmsQuestionForm form) { validateForm(form); String id = RandomUtil.uuId(); Date now = new Date(); String userId = UserProvider.getLoginUserId(); TmsQuestionEntity entity = new TmsQuestionEntity(); entity.setId(id); entity.setQuestionNo(nextQuestionNo()); entity.setBankId(form.getBankId()); entity.setQuestionType(form.getQuestionType()); entity.setDifficulty(form.getDifficulty()); entity.setSourceType(form.getSourceType()); applyLifeFields(entity, form); entity.setStem(form.getStem()); entity.setAnalysis(form.getAnalysis()); entity.setAdminUserId(StringUtil.isNotEmpty(form.getAdminUserId()) ? form.getAdminUserId() : userId); entity.setBizStatus(StringUtil.isNotEmpty(form.getBizStatus()) ? form.getBizStatus() : TmsOpenClosedInvalid.OPEN.getCode()); entity.setCreatorUserId(userId); entity.setCreatorTime(now); questionMapper.insert(entity); saveOptions(id, form.getOptions()); refreshBankCount(form.getBankId()); return id; } /** * 试题:更新 */ @Override @Transactional(rollbackFor = Exception.class) public void update(String id, TmsQuestionForm form) { TmsQuestionEntity entity = questionMapper.selectById(id); if (entity == null) { throw new DataException("试题不存在"); } if (TmsOpenClosedInvalid.INVALID.equalsCode(entity.getBizStatus())) { throw new DataException("已废弃试题不可修改"); } validateForm(form); String oldBankId = entity.getBankId(); entity.setBankId(form.getBankId()); entity.setQuestionType(form.getQuestionType()); entity.setDifficulty(form.getDifficulty()); entity.setSourceType(form.getSourceType()); applyLifeFields(entity, form); entity.setStem(form.getStem()); entity.setAnalysis(form.getAnalysis()); if (StringUtil.isNotEmpty(form.getAdminUserId())) { entity.setAdminUserId(form.getAdminUserId()); } if (StringUtil.isNotEmpty(form.getBizStatus())) { entity.setBizStatus(form.getBizStatus()); } entity.setLastModifyUserId(UserProvider.getLoginUserId()); entity.setLastModifyTime(new Date()); questionMapper.updateById(entity); // 长期题清空过期时间(updateById 默认忽略 null) if (TmsQuestionLifeType.LONG.equalsCode(entity.getLifeType())) { questionMapper.update(null, new LambdaUpdateWrapper() .eq(TmsQuestionEntity::getId, id) .set(TmsQuestionEntity::getExpireTime, null)); } // 选项全量替换 LambdaQueryWrapper delQw = new LambdaQueryWrapper<>(); delQw.eq(TmsQuestionOptionEntity::getForeignId, id); optionMapper.delete(delQw); saveOptions(id, form.getOptions()); refreshBankCount(oldBankId); if (!Objects.equals(oldBankId, form.getBankId())) { refreshBankCount(form.getBankId()); } } @Override @Transactional(rollbackFor = Exception.class) @AuditLog(action = "QUESTION_INVALIDATE", label = "废弃试题", bizType = "TMS_QUESTION", bizCodeExpr = "#id", targetTable = "tms_question", targetIdExpr = "#id") /** * 试题:作废 */ public void invalidate(String id) { TmsQuestionEntity entity = questionMapper.selectById(id); if (entity == null) { return; } if (TmsOpenClosedInvalid.INVALID.equalsCode(entity.getBizStatus())) { return; } if (TmsOpenClosedInvalid.OPEN.equalsCode(entity.getBizStatus())) { throw new DataException("请先设为不开放后再废弃"); } // 可见弃用态:保留记录,列表可按状态筛选区分在用/弃用 entity.setBizStatus(TmsOpenClosedInvalid.INVALID.getCode()); entity.setLastModifyUserId(UserProvider.getLoginUserId()); entity.setLastModifyTime(new Date()); questionMapper.updateById(entity); refreshBankCount(entity.getBankId()); } @Override @Transactional(rollbackFor = Exception.class) @AuditLog(action = "QUESTION_STATUS", label = "试题开放状态变更", bizType = "TMS_QUESTION", bizCodeExpr = "#id", targetTable = "tms_question", targetIdExpr = "#id", extraExpr = "{'bizStatus': #bizStatus}") /** * 设置业务状态 */ public void setBizStatus(String id, String bizStatus) { if (!TmsOpenClosedInvalid.OPEN.equalsCode(bizStatus) && !TmsOpenClosedInvalid.CLOSED.equalsCode(bizStatus)) { throw new DataException("状态无效,仅支持开放/不开放"); } TmsQuestionEntity entity = questionMapper.selectById(id); if (entity == null) { throw new DataException("试题不存在或已删除"); } if (TmsOpenClosedInvalid.INVALID.equalsCode(entity.getBizStatus())) { throw new DataException("已废弃试题不可开放或不开放"); } entity.setBizStatus(bizStatus); entity.setLastModifyUserId(UserProvider.getLoginUserId()); entity.setLastModifyTime(new Date()); questionMapper.updateById(entity); } /** * 校验表单 */ private void validateForm(TmsQuestionForm form) { if (form == null) { throw new DataException("参数不能为空"); } if (StringUtil.isEmpty(form.getBankId())) { throw new DataException("请选择所属题库"); } if (StringUtil.isEmpty(form.getQuestionType())) { throw new DataException("请选择题型"); } if (StringUtil.isEmpty(form.getStem())) { throw new DataException("请填写题干"); } TmsQuestionBankEntity bank = bankMapper.selectById(form.getBankId()); if (bank == null) { throw new DataException("题库不存在或已删除"); } String lifeType = StringUtil.isNotEmpty(form.getLifeType()) ? form.getLifeType() : TmsQuestionLifeType.LONG.getCode(); if (TmsQuestionLifeType.of(lifeType) == null) { throw new DataException("时效类型无效,仅支持长期/临时"); } if (TmsQuestionLifeType.TEMP.equalsCode(lifeType)) { if (StringUtil.isEmpty(form.getExpireTime())) { throw new DataException("临时试题请填写过期时间"); } if (parseDate(form.getExpireTime()) == null) { throw new DataException("过期时间格式无效"); } } } /** 写入时效类型与过期时间;长期题清空过期时间 */ private void applyLifeFields(TmsQuestionEntity entity, TmsQuestionForm form) { String lifeType = StringUtil.isNotEmpty(form.getLifeType()) ? form.getLifeType() : TmsQuestionLifeType.LONG.getCode(); entity.setLifeType(lifeType); if (TmsQuestionLifeType.TEMP.equalsCode(lifeType)) { entity.setExpireTime(parseDate(form.getExpireTime())); } else { entity.setExpireTime(null); } } private static Date parseDate(String raw) { if (StringUtil.isEmpty(raw)) { return null; } try { return DateUtil.stringToDate(raw.trim()); } catch (Exception ignored) { return null; } } /** * 保存试题选项 */ private void saveOptions(String questionId, List options) { if (options == null || options.isEmpty()) { return; } Date now = new Date(); String userId = UserProvider.getLoginUserId(); int i = 0; for (TmsQuestionOptionForm opt : options) { TmsQuestionOptionEntity row = new TmsQuestionOptionEntity(); row.setId(RandomUtil.uuId()); row.setForeignId(questionId); row.setSortNo(opt.getSortNo() != null ? opt.getSortNo() : ++i); row.setOptionLabel(opt.getOptionLabel()); row.setOptionContent(opt.getOptionContent()); row.setIsCorrect(StringUtil.isNotEmpty(opt.getIsCorrect()) ? opt.getIsCorrect() : "0"); row.setCreatorUserId(userId); row.setCreatorTime(now); optionMapper.insert(row); } } /** 按题库下未删除且未废弃试题实计回写 question_count */ private void refreshBankCount(String bankId) { if (StringUtil.isEmpty(bankId)) { return; } TmsQuestionBankEntity bank = bankMapper.selectById(bankId); if (bank == null) { return; } LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(TmsQuestionEntity::getBankId, bankId) .ne(TmsQuestionEntity::getBizStatus, TmsOpenClosedInvalid.INVALID.getCode()); Long count = questionMapper.selectCount(qw); bank.setQuestionCount(count == null ? 0 : count.intValue()); bank.setLastModifyUserId(UserProvider.getLoginUserId()); bank.setLastModifyTime(new Date()); bankMapper.updateById(bank); } /** * 试题编号:年份后两位 + 4 位流水,如 260001。 * 按当年最大号 +1,含已删记录,避免流水回退。 */ private synchronized String nextQuestionNo() { String prefix = String.format("%02d", LocalDate.now().getYear() % 100); String maxNo = questionMapper.selectMaxQuestionNo(prefix); int next = 1; if (StringUtil.isNotEmpty(maxNo) && maxNo.length() >= 6 && maxNo.startsWith(prefix)) { try { next = Integer.parseInt(maxNo.substring(prefix.length())) + 1; } catch (NumberFormatException ignored) { next = 1; } } if (next > 9999) { throw new DataException("当年试题编号已用尽(" + prefix + "9999)"); } return prefix + String.format("%04d", next); } /** * 实体转为表单/VO */ private TmsQuestionForm toForm( TmsQuestionEntity entity, Map bankNames, Map userNames, boolean withOptions) { TmsQuestionForm form = new TmsQuestionForm(); form.setId(entity.getId()); form.setQuestionNo(entity.getQuestionNo()); form.setBankId(entity.getBankId()); form.setBankName(bankNames.get(entity.getBankId())); form.setQuestionType(entity.getQuestionType()); form.setDifficulty(entity.getDifficulty()); form.setSourceType(entity.getSourceType()); form.setLifeType(StringUtil.isNotEmpty(entity.getLifeType()) ? entity.getLifeType() : TmsQuestionLifeType.LONG.getCode()); form.setExpireTime(entity.getExpireTime() == null ? null : DateUtil.dateToString(entity.getExpireTime(), "yyyy-MM-dd HH:mm:ss")); form.setStem(entity.getStem()); form.setAnalysis(entity.getAnalysis()); form.setAdminUserId(entity.getAdminUserId()); form.setAdminUserName(userNames.get(entity.getAdminUserId())); form.setBizStatus(entity.getBizStatus()); form.setCreatorTime(entity.getCreatorTime() == null ? null : DateUtil.dateToString(entity.getCreatorTime(), "yyyy-MM-dd HH:mm:ss")); if (withOptions) { form.setOptions(loadOptions(entity.getId())); } return form; } /** * 加载题目选项 */ private List loadOptions(String questionId) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.eq(TmsQuestionOptionEntity::getForeignId, questionId); qw.orderByAsc(TmsQuestionOptionEntity::getSortNo); List rows = optionMapper.selectList(qw); List list = new ArrayList<>(rows.size()); for (TmsQuestionOptionEntity row : rows) { TmsQuestionOptionForm opt = new TmsQuestionOptionForm(); opt.setId(row.getId()); opt.setSortNo(row.getSortNo()); opt.setOptionLabel(row.getOptionLabel()); opt.setOptionContent(row.getOptionContent()); opt.setIsCorrect(row.getIsCorrect()); list.add(opt); } return list; } /** * 加载题库名称 */ private Map loadBankNames(List bankIds) { if (bankIds == null || bankIds.isEmpty()) { return Collections.emptyMap(); } List banks = bankMapper.selectBatchIds(bankIds); Map map = new HashMap<>(); for (TmsQuestionBankEntity bank : banks) { map.put(bank.getId(), bank.getBankName()); } return map; } /** * 批量加载用户姓名 */ private Map loadUserNames(List userIds) { if (userIds == null || userIds.isEmpty()) { return Collections.emptyMap(); } try { List users = userApi.getUserName(userIds); if (users == null || users.isEmpty()) { return Collections.emptyMap(); } return users.stream() .filter(Objects::nonNull) .collect(Collectors.toMap(UserEntity::getId, UserEntity::getRealName, (a, b) -> a)); } catch (Exception ex) { return Collections.emptyMap(); } } /** * 导出 Excel */ @Override public void exportExcel(TmsQuestionExportForm form, HttpServletResponse response) { List entities = loadForExport(form); if (entities.isEmpty()) { throw new DataException("没有可导出的试题"); } Map bankNames = loadBankNames( entities.stream().map(TmsQuestionEntity::getBankId).filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList())); Map> optionMap = loadOptionsBatch( entities.stream().map(TmsQuestionEntity::getId).collect(Collectors.toList())); List> rows = new ArrayList<>(entities.size()); for (TmsQuestionEntity entity : entities) { List options = optionMap.getOrDefault(entity.getId(), Collections.emptyList()); rows.add(toExportRow(entity, bankNames.get(entity.getBankId()), options)); } try { TmsExcelExportHelper.writeSpreadsheet(response, "试题导出", "试题", EXCEL_HEADERS, EXCEL_WIDTHS, rows); } catch (IOException e) { throw new DataException("导出试题失败"); } } /** * 下载导入模板 */ @Override public void downloadImportTemplate(HttpServletResponse response) { List> samples = new ArrayList<>(); samples.add(Arrays.asList( "", "示例题库", "单选", "一般", "GMP要求中,质量管理部门的职责是?", "示例解析", "批准产品质量", "生产排产", "设备维保", "销售推广", "", "", "A", "开放")); samples.add(Arrays.asList( "", "示例题库", "判断", "简单", "培训记录应长期保存。", "", "", "", "", "", "", "", "正确", "开放")); samples.add(Arrays.asList( "", "示例题库", "多选", "困难", "下列属于人员卫生要求的有?", "", "洗手", "更衣", "佩戴饰品", "进入洁净区前更衣", "", "", "A,B,D", "开放")); try { TmsExcelExportHelper.writeSpreadsheet(response, "试题导入模板", "试题", EXCEL_HEADERS, EXCEL_WIDTHS, samples); } catch (IOException e) { throw new DataException("下载导入模板失败"); } } @Override @AuditLog(action = "QUESTION_IMPORT", label = "试题批量导入", bizType = "TMS_QUESTION", bizCodeExpr = "#fileName != null && !#fileName.isEmpty() ? #fileName : 'IMPORT'", targetTable = "tms_question", bizCodeRequired = false, extraExpr = "{'successCount': #result?.successCount, 'failCount': #result?.failCount}") /** * Excel 导入 */ public TmsQuestionImportResult importExcel(byte[] fileBytes, String fileName) { List> rawRows = TmsExcelImportHelper.readRows(fileBytes, fileName); if (rawRows == null || rawRows.isEmpty()) { throw new DataException("导入文件无数据"); } Map headerIndex = buildHeaderIndex(rawRows.get(0)); requireHeader(headerIndex, "题库名称"); requireHeader(headerIndex, "题型"); requireHeader(headerIndex, "题干"); Map bankIdByName = loadBankIdByName(); TmsQuestionImportResult result = new TmsQuestionImportResult(); for (int i = 1; i < rawRows.size(); i++) { List row = rawRows.get(i); int excelRow = i + 1; try { TmsQuestionForm form = parseImportRow(row, headerIndex, bankIdByName); if (form == null) { continue; } form.setSourceType(SOURCE_IMPORT); if (StringUtil.isEmpty(form.getBizStatus())) { form.setBizStatus(TmsOpenClosedInvalid.OPEN.getCode()); } create(form); result.addSuccess(); } catch (Exception ex) { String msg = ex.getMessage() == null ? "未知错误" : ex.getMessage(); result.addError("第" + excelRow + "行:" + msg); } } if (result.getSuccessCount() == 0 && result.getFailCount() == 0) { throw new DataException("未识别到有效数据行,请检查模板表头与内容"); } return result; } /** * 加载导出数据 */ private List loadForExport(TmsQuestionExportForm form) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); if (form != null && form.getIds() != null && !form.getIds().isEmpty()) { List ids = form.getIds().stream().filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList()); if (ids.isEmpty()) { return Collections.emptyList(); } qw.in(TmsQuestionEntity::getId, ids); } else { if (form != null) { if (StringUtil.isNotEmpty(form.getBankId())) { qw.eq(TmsQuestionEntity::getBankId, form.getBankId()); } if (StringUtil.isNotEmpty(form.getQuestionType())) { qw.eq(TmsQuestionEntity::getQuestionType, form.getQuestionType()); } if (StringUtil.isNotEmpty(form.getBizStatus())) { qw.eq(TmsQuestionEntity::getBizStatus, form.getBizStatus()); } if (StringUtil.isNotEmpty(form.getLifeType())) { qw.eq(TmsQuestionEntity::getLifeType, form.getLifeType()); } if (StringUtil.isNotEmpty(form.getKeyword()) && !"null".equalsIgnoreCase(form.getKeyword())) { String kw = form.getKeyword().trim(); qw.like(TmsQuestionEntity::getStem, kw); } } qw.last("LIMIT " + EXPORT_LIMIT); } qw.orderByDesc(TmsQuestionEntity::getCreatorTime); return questionMapper.selectList(qw); } /** * 批量加载题目选项 */ private Map> loadOptionsBatch(List questionIds) { if (questionIds == null || questionIds.isEmpty()) { return Collections.emptyMap(); } LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); qw.in(TmsQuestionOptionEntity::getForeignId, questionIds); qw.orderByAsc(TmsQuestionOptionEntity::getSortNo); List rows = optionMapper.selectList(qw); Map> map = new HashMap<>(); for (TmsQuestionOptionEntity row : rows) { TmsQuestionOptionForm opt = new TmsQuestionOptionForm(); opt.setId(row.getId()); opt.setSortNo(row.getSortNo()); opt.setOptionLabel(row.getOptionLabel()); opt.setOptionContent(row.getOptionContent()); opt.setIsCorrect(row.getIsCorrect()); map.computeIfAbsent(row.getForeignId(), k -> new ArrayList<>()).add(opt); } return map; } /** * 转为导出行 */ private List toExportRow(TmsQuestionEntity entity, String bankName, List options) { String[] optContents = new String[6]; Arrays.fill(optContents, ""); List correctLabels = new ArrayList<>(); if (options != null) { for (TmsQuestionOptionForm opt : options) { String label = opt.getOptionLabel() == null ? "" : opt.getOptionLabel().trim().toUpperCase(Locale.ROOT); int idx = label.length() == 1 && label.charAt(0) >= 'A' && label.charAt(0) <= 'F' ? label.charAt(0) - 'A' : -1; if (idx >= 0) { optContents[idx] = nvl(opt.getOptionContent()); } if (TmsYesNo.isYesCode(opt.getIsCorrect())) { if (TmsQuestionType.JUDGE.equalsCode(entity.getQuestionType())) { correctLabels.add(labelJudgeAnswer(opt)); } else if (TmsQuestionType.BLANK.equalsCode(entity.getQuestionType()) || TmsQuestionType.ESSAY.equalsCode(entity.getQuestionType())) { correctLabels.add(nvl(opt.getOptionContent())); } else if (StringUtil.isNotEmpty(label)) { correctLabels.add(label); } } } // 判断题未标 A/B 时,按内容导出 if (TmsQuestionType.JUDGE.equalsCode(entity.getQuestionType()) && correctLabels.isEmpty()) { for (TmsQuestionOptionForm opt : options) { if (TmsYesNo.isYesCode(opt.getIsCorrect())) { correctLabels.add(labelJudgeAnswer(opt)); } } } // 填空:全部选项内容按顺序作为答案 if (TmsQuestionType.BLANK.equalsCode(entity.getQuestionType()) && correctLabels.isEmpty()) { for (TmsQuestionOptionForm opt : options) { if (StringUtil.isNotEmpty(opt.getOptionContent())) { correctLabels.add(opt.getOptionContent().trim()); } } } } String answer = String.join( TmsQuestionType.BLANK.equalsCode(entity.getQuestionType()) ? "|" : ",", correctLabels); return Arrays.asList( nvl(entity.getQuestionNo()), nvl(bankName), labelQuestionType(entity.getQuestionType()), labelDifficulty(entity.getDifficulty()), stripHtml(entity.getStem()), stripHtml(entity.getAnalysis()), optContents[0], optContents[1], optContents[2], optContents[3], optContents[4], optContents[5], answer, labelBizStatus(entity.getBizStatus())); } /** * 判断题答案标签 */ private static String labelJudgeAnswer(TmsQuestionOptionForm opt) { String content = nvl(opt.getOptionContent()); String label = opt.getOptionLabel() == null ? "" : opt.getOptionLabel().trim().toUpperCase(Locale.ROOT); if (content.contains("正确") || "T".equals(label) || "A".equals(label) || "对".equals(content)) { return "正确"; } if (content.contains("错误") || "F".equals(label) || "B".equals(label) || "错".equals(content)) { return "错误"; } return StringUtil.isNotEmpty(content) ? content : label; } /** * 解析导入行 */ private TmsQuestionForm parseImportRow(List row, Map headerIndex, Map bankIdByName) { String bankName = cell(row, headerIndex, "题库名称"); String typeLabel = cell(row, headerIndex, "题型"); String stem = cell(row, headerIndex, "题干"); if (StringUtil.isEmpty(bankName) && StringUtil.isEmpty(typeLabel) && StringUtil.isEmpty(stem)) { return null; } if (StringUtil.isEmpty(bankName)) { throw new DataException("题库名称不能为空"); } if (StringUtil.isEmpty(stem)) { throw new DataException("题干不能为空"); } String bankId = bankIdByName.get(bankName.trim()); if (StringUtil.isEmpty(bankId)) { throw new DataException("题库不存在:" + bankName); } String questionType = parseQuestionType(typeLabel); if (StringUtil.isEmpty(questionType)) { throw new DataException("题型无效:" + typeLabel); } TmsQuestionForm form = new TmsQuestionForm(); form.setBankId(bankId); form.setQuestionType(questionType); form.setDifficulty(parseDifficulty(cell(row, headerIndex, "难度"))); form.setStem(stem); form.setAnalysis(cell(row, headerIndex, "解析")); form.setBizStatus(parseBizStatus(cell(row, headerIndex, "状态"))); form.setOptions(buildOptions( questionType, Arrays.asList( cell(row, headerIndex, "选项A"), cell(row, headerIndex, "选项B"), cell(row, headerIndex, "选项C"), cell(row, headerIndex, "选项D"), cell(row, headerIndex, "选项E"), cell(row, headerIndex, "选项F")), cell(row, headerIndex, "正确答案"))); return form; } /** * 构建选项列表 */ private List buildOptions(String questionType, List optionTexts, String answerRaw) { List options = new ArrayList<>(); String answer = answerRaw == null ? "" : answerRaw.trim(); if (TmsQuestionType.JUDGE.equalsCode(questionType)) { boolean trueCorrect = isTrueAnswer(answer); options.add(option("A", "正确", trueCorrect)); options.add(option("B", "错误", !trueCorrect && StringUtil.isNotEmpty(answer))); if (StringUtil.isEmpty(answer)) { throw new DataException("判断题请填写正确答案(正确/错误)"); } return options; } if (TmsQuestionType.BLANK.equalsCode(questionType)) { String[] parts = answer.split("[||;;]"); int i = 0; for (String part : parts) { if (StringUtil.isEmpty(part)) { continue; } i++; options.add(option(String.valueOf(i), part.trim(), true)); } if (options.isEmpty()) { throw new DataException("填空题请在正确答案中填写答案,多个用|分隔"); } return options; } if (TmsQuestionType.ESSAY.equalsCode(questionType)) { if (StringUtil.isNotEmpty(answer)) { options.add(option("1", answer, true)); } return options; } // 单选 / 多选 char label = 'A'; for (String text : optionTexts) { if (StringUtil.isEmpty(text)) { label++; continue; } String lab = String.valueOf(label); boolean correct = isChoiceCorrect(answer, lab); options.add(option(lab, text.trim(), correct)); label++; } if (options.size() < 2) { throw new DataException("选择题至少需要两个选项"); } boolean anyCorrect = options.stream().anyMatch(o -> TmsYesNo.isYesCode(o.getIsCorrect())); if (!anyCorrect) { throw new DataException("请填写正确答案,如 A 或 A,C"); } return options; } /** * 构建选项 */ private static TmsQuestionOptionForm option(String label, String content, boolean correct) { TmsQuestionOptionForm opt = new TmsQuestionOptionForm(); opt.setOptionLabel(label); opt.setOptionContent(content); opt.setIsCorrect(correct ? TmsYesNo.YES.getCode() : TmsYesNo.NO.getCode()); return opt; } /** * 判断选择题正误 */ private static boolean isChoiceCorrect(String answer, String label) { if (StringUtil.isEmpty(answer) || StringUtil.isEmpty(label)) { return false; } String normalized = answer.toUpperCase(Locale.ROOT).replace(",", ",").replace(" ", ""); for (String part : normalized.split("[,|/、]")) { if (label.equalsIgnoreCase(part.trim())) { return true; } } return false; } /** * 判断是否为正确答案 */ private static boolean isTrueAnswer(String answer) { if (StringUtil.isEmpty(answer)) { return false; } String v = answer.trim().toLowerCase(Locale.ROOT); return "正确".equals(answer.trim()) || "对".equals(answer.trim()) || "true".equals(v) || "t".equals(v) || "y".equals(v) || "yes".equals(v) || "a".equals(v) || "1".equals(v); } /** * 按题库名解析 ID */ private Map loadBankIdByName() { List banks = bankMapper.selectList(new LambdaQueryWrapper<>()); Map map = new LinkedHashMap<>(); for (TmsQuestionBankEntity bank : banks) { if (bank != null && StringUtil.isNotEmpty(bank.getBankName())) { map.put(bank.getBankName().trim(), bank.getId()); } } return map; } /** * 构建表头列索引 */ private static Map buildHeaderIndex(List headerRow) { Map map = new HashMap<>(); if (headerRow == null) { return map; } for (int i = 0; i < headerRow.size(); i++) { String h = headerRow.get(i); if (StringUtil.isNotEmpty(h)) { map.put(h.trim(), i); } } return map; } /** * 校验并获取导入表头 */ private static void requireHeader(Map headerIndex, String name) { if (!headerIndex.containsKey(name)) { throw new DataException("导入模板缺少列:" + name); } } /** * 导出单元格文本 */ private static String cell(List row, Map headerIndex, String name) { Integer idx = headerIndex.get(name); if (idx == null || row == null || idx < 0 || idx >= row.size()) { return ""; } String v = row.get(idx); return v == null ? "" : v.trim(); } /** * 解析题型 */ private static String parseQuestionType(String raw) { if (StringUtil.isEmpty(raw)) { return null; } String v = raw.trim(); TmsQuestionType byCode = TmsQuestionType.of(v); if (byCode != null) { return byCode.getCode(); } switch (v) { case "单选": case "单选题": return TmsQuestionType.SINGLE.getCode(); case "多选": case "多选题": return TmsQuestionType.MULTI.getCode(); case "判断": case "判断题": return TmsQuestionType.JUDGE.getCode(); case "填空": case "填空题": return TmsQuestionType.BLANK.getCode(); case "问答": case "问答题": case "简答": return TmsQuestionType.ESSAY.getCode(); default: return null; } } /** * 解析难度 */ private static String parseDifficulty(String raw) { if (StringUtil.isEmpty(raw)) { return "normal"; } String v = raw.trim(); if ("easy".equalsIgnoreCase(v) || "简单".equals(v)) { return "easy"; } if ("hard".equalsIgnoreCase(v) || "困难".equals(v) || "难".equals(v)) { return "hard"; } return "normal"; } /** * 解析业务状态 */ private static String parseBizStatus(String raw) { if (StringUtil.isEmpty(raw)) { return TmsOpenClosedInvalid.OPEN.getCode(); } String v = raw.trim(); if (TmsOpenClosedInvalid.CLOSED.equalsCode(v) || "关闭".equals(v) || "不开放".equals(v) || "停用".equals(v)) { return TmsOpenClosedInvalid.CLOSED.getCode(); } if (TmsOpenClosedInvalid.INVALID.equalsCode(v) || "作废".equals(v) || "废弃".equals(v)) { return TmsOpenClosedInvalid.INVALID.getCode(); } return TmsOpenClosedInvalid.OPEN.getCode(); } /** * 题型字典标签 */ private static String labelQuestionType(String code) { TmsQuestionType t = TmsQuestionType.of(code); return t == null ? nvl(code) : t.getLabel(); } /** * 难度字典标签 */ private static String labelDifficulty(String code) { if ("easy".equalsIgnoreCase(code)) { return "简单"; } if ("hard".equalsIgnoreCase(code)) { return "困难"; } if ("normal".equalsIgnoreCase(code) || StringUtil.isEmpty(code)) { return "一般"; } return code; } /** * 业务状态标签 */ private static String labelBizStatus(String code) { TmsOpenClosedInvalid s = TmsOpenClosedInvalid.of(code); return s == null ? nvl(code) : s.getLabel(); } /** * 去除 HTML 标签 */ private static String stripHtml(String html) { if (StringUtil.isEmpty(html)) { return ""; } return html.replaceAll("<[^>]+>", " ") .replace(" ", " ") .replaceAll("\\s+", " ") .trim(); } /** * 空值兜底 */ private static String nvl(String v) { return v == null ? "" : v; } }