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<String> EXCEL_HEADERS = Arrays.asList(
|
"试题编号", "题库名称", "题型", "难度", "题干", "解析",
|
"选项A", "选项B", "选项C", "选项D", "选项E", "选项F", "正确答案", "状态");
|
private static final List<Integer> 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<TmsQuestionBankOption> listBanks() {
|
LambdaQueryWrapper<TmsQuestionBankEntity> qw = new LambdaQueryWrapper<>();
|
qw.orderByDesc(TmsQuestionBankEntity::getCreatorTime);
|
List<TmsQuestionBankEntity> list = bankMapper.selectList(qw);
|
List<TmsQuestionBankOption> 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<Map<String, String>> listAdmins() {
|
QueryWrapper<TmsQuestionEntity> qw = new QueryWrapper<>();
|
qw.select("DISTINCT admin_user_id");
|
qw.isNotNull("admin_user_id");
|
qw.ne("admin_user_id", "");
|
List<TmsQuestionEntity> rows = questionMapper.selectList(qw);
|
List<String> userIds = rows.stream()
|
.map(TmsQuestionEntity::getAdminUserId)
|
.filter(StringUtil::isNotEmpty)
|
.distinct()
|
.collect(Collectors.toList());
|
if (userIds.isEmpty()) {
|
return Collections.emptyList();
|
}
|
Map<String, String> nameMap = loadUserNames(userIds);
|
List<Map<String, String>> result = new ArrayList<>();
|
for (String userId : userIds) {
|
Map<String, String> item = new HashMap<>(2);
|
item.put("id", userId);
|
item.put("fullName", nameMap.getOrDefault(userId, userId));
|
result.add(item);
|
}
|
return result;
|
}
|
/**
|
* 试题:条件分页查询列表
|
*/
|
@Override
|
public List<TmsQuestionForm> getList(TmsQuestionQuery query) {
|
LambdaQueryWrapper<TmsQuestionEntity> 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<TmsQuestionEntity> page = new Page<>(query.getCurrentPage(), query.getPageSize());
|
Page<TmsQuestionEntity> result = questionMapper.selectPage(page, qw);
|
List<TmsQuestionEntity> records = result.getRecords();
|
|
Map<String, String> bankNames = loadBankNames(
|
records.stream().map(TmsQuestionEntity::getBankId).filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList()));
|
Map<String, String> userNames = loadUserNames(
|
records.stream().map(TmsQuestionEntity::getAdminUserId).filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList()));
|
|
List<TmsQuestionForm> 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<String, String> bankNames = loadBankNames(Collections.singletonList(entity.getBankId()));
|
Map<String, String> 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<TmsQuestionEntity>()
|
.eq(TmsQuestionEntity::getId, id)
|
.set(TmsQuestionEntity::getExpireTime, null));
|
}
|
|
// 选项全量替换
|
LambdaQueryWrapper<TmsQuestionOptionEntity> 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<TmsQuestionOptionForm> 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<TmsQuestionEntity> 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<String, String> bankNames,
|
Map<String, String> 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<TmsQuestionOptionForm> loadOptions(String questionId) {
|
LambdaQueryWrapper<TmsQuestionOptionEntity> qw = new LambdaQueryWrapper<>();
|
qw.eq(TmsQuestionOptionEntity::getForeignId, questionId);
|
qw.orderByAsc(TmsQuestionOptionEntity::getSortNo);
|
List<TmsQuestionOptionEntity> rows = optionMapper.selectList(qw);
|
List<TmsQuestionOptionForm> 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<String, String> loadBankNames(List<String> bankIds) {
|
if (bankIds == null || bankIds.isEmpty()) {
|
return Collections.emptyMap();
|
}
|
List<TmsQuestionBankEntity> banks = bankMapper.selectBatchIds(bankIds);
|
Map<String, String> map = new HashMap<>();
|
for (TmsQuestionBankEntity bank : banks) {
|
map.put(bank.getId(), bank.getBankName());
|
}
|
return map;
|
}
|
|
/**
|
* 批量加载用户姓名
|
*/
|
private Map<String, String> loadUserNames(List<String> userIds) {
|
if (userIds == null || userIds.isEmpty()) {
|
return Collections.emptyMap();
|
}
|
try {
|
List<UserEntity> 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<TmsQuestionEntity> entities = loadForExport(form);
|
if (entities.isEmpty()) {
|
throw new DataException("没有可导出的试题");
|
}
|
Map<String, String> bankNames = loadBankNames(
|
entities.stream().map(TmsQuestionEntity::getBankId).filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList()));
|
Map<String, List<TmsQuestionOptionForm>> optionMap = loadOptionsBatch(
|
entities.stream().map(TmsQuestionEntity::getId).collect(Collectors.toList()));
|
|
List<List<String>> rows = new ArrayList<>(entities.size());
|
for (TmsQuestionEntity entity : entities) {
|
List<TmsQuestionOptionForm> 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<List<String>> 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<List<String>> rawRows = TmsExcelImportHelper.readRows(fileBytes, fileName);
|
if (rawRows == null || rawRows.isEmpty()) {
|
throw new DataException("导入文件无数据");
|
}
|
Map<String, Integer> headerIndex = buildHeaderIndex(rawRows.get(0));
|
requireHeader(headerIndex, "题库名称");
|
requireHeader(headerIndex, "题型");
|
requireHeader(headerIndex, "题干");
|
|
Map<String, String> bankIdByName = loadBankIdByName();
|
TmsQuestionImportResult result = new TmsQuestionImportResult();
|
for (int i = 1; i < rawRows.size(); i++) {
|
List<String> 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<TmsQuestionEntity> loadForExport(TmsQuestionExportForm form) {
|
LambdaQueryWrapper<TmsQuestionEntity> qw = new LambdaQueryWrapper<>();
|
if (form != null && form.getIds() != null && !form.getIds().isEmpty()) {
|
List<String> 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<String, List<TmsQuestionOptionForm>> loadOptionsBatch(List<String> questionIds) {
|
if (questionIds == null || questionIds.isEmpty()) {
|
return Collections.emptyMap();
|
}
|
LambdaQueryWrapper<TmsQuestionOptionEntity> qw = new LambdaQueryWrapper<>();
|
qw.in(TmsQuestionOptionEntity::getForeignId, questionIds);
|
qw.orderByAsc(TmsQuestionOptionEntity::getSortNo);
|
List<TmsQuestionOptionEntity> rows = optionMapper.selectList(qw);
|
Map<String, List<TmsQuestionOptionForm>> 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<String> toExportRow(TmsQuestionEntity entity, String bankName, List<TmsQuestionOptionForm> options) {
|
String[] optContents = new String[6];
|
Arrays.fill(optContents, "");
|
List<String> 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<String> row, Map<String, Integer> headerIndex,
|
Map<String, String> 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<TmsQuestionOptionForm> buildOptions(String questionType, List<String> optionTexts, String answerRaw) {
|
List<TmsQuestionOptionForm> 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<String, String> loadBankIdByName() {
|
List<TmsQuestionBankEntity> banks = bankMapper.selectList(new LambdaQueryWrapper<>());
|
Map<String, String> 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<String, Integer> buildHeaderIndex(List<String> headerRow) {
|
Map<String, Integer> 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<String, Integer> headerIndex, String name) {
|
if (!headerIndex.containsKey(name)) {
|
throw new DataException("导入模板缺少列:" + name);
|
}
|
}
|
|
/**
|
* 导出单元格文本
|
*/
|
private static String cell(List<String> row, Map<String, Integer> 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;
|
}
|
}
|