feat: add exam settings and platform question bank APIs

fix/exam-question-access-material-excel-delete-draft
chenyuan 3 weeks ago
parent c74c803618
commit d69efbd865

@ -0,0 +1,7 @@
-- 考试设置扩展。执行前请确认目标数据库与备份策略。
ALTER TABLE exam_assessment
ADD COLUMN question_order_mode varchar(16) NOT NULL DEFAULT 'FIXED' AFTER allow_rollback,
ADD COLUMN option_order_mode varchar(16) NOT NULL DEFAULT 'FIXED' AFTER question_order_mode,
ADD COLUMN allow_copy_question bit NOT NULL DEFAULT 0 AFTER option_order_mode,
ADD COLUMN allow_paste_answer bit NOT NULL DEFAULT 0 AFTER allow_copy_question,
ADD COLUMN multiple_score_mode varchar(32) NOT NULL DEFAULT 'ALL_OR_NOTHING' AFTER allow_paste_answer;

@ -0,0 +1,11 @@
-- Platform question bank extension. Execute after 2026-09-15-exam.sql.
-- Existing teacher questions remain usable and are classified as TEACHER.
ALTER TABLE exam_question
ADD COLUMN source_type varchar(16) NOT NULL DEFAULT 'TEACHER' COMMENT '题目来源:PLATFORM/TEACHER',
ADD COLUMN task_key varchar(128) NULL COMMENT '归属实训任务,关联 training_task.task_key';
UPDATE exam_question SET source_type='TEACHER' WHERE source_type IS NULL OR source_type='';
ALTER TABLE exam_question
ADD KEY idx_exam_question_source_task (source_type, task_key),
ADD KEY idx_exam_question_task (task_key);

@ -10,6 +10,8 @@ import com.sztzjy.linkCommerce.service.PlatformAdminService;
import com.sztzjy.linkCommerce.service.SchoolDefaultTaskService; import com.sztzjy.linkCommerce.service.SchoolDefaultTaskService;
import com.sztzjy.linkCommerce.service.SchoolProductConfigService; import com.sztzjy.linkCommerce.service.SchoolProductConfigService;
import com.sztzjy.linkCommerce.service.UserInfoService; import com.sztzjy.linkCommerce.service.UserInfoService;
import com.sztzjy.linkCommerce.service.PlatformQuestionBankService;
import com.sztzjy.linkCommerce.service.ExamService;
import com.sztzjy.linkCommerce.util.ResultEntity; import com.sztzjy.linkCommerce.util.ResultEntity;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
@ -22,6 +24,8 @@ import javax.servlet.http.HttpServletRequest;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.Map;
import org.springframework.web.multipart.MultipartFile;
@Api(tags = "平台超管管理") @Api(tags = "平台超管管理")
@RequestMapping("api/platform-admin") @RequestMapping("api/platform-admin")
@ -47,6 +51,71 @@ public class PlatformAdminController {
private UserinfoMapper userinfoMapper; private UserinfoMapper userinfoMapper;
@Autowired @Autowired
private TaskAllocationMapper taskAllocationMapper; private TaskAllocationMapper taskAllocationMapper;
@Autowired
private PlatformQuestionBankService platformQuestionBankService;
@Autowired
private ExamService examService;
@GetMapping("/question-bank/projects")
public ResultEntity<List<Map<String, Object>>> questionBankProjects(HttpServletRequest request) {
return new ResultEntity<>(HttpStatus.OK, "查询成功", platformQuestionBankService.projects(currentUser(request)));
}
@GetMapping("/question-bank")
public ResultEntity<List<Map<String, Object>>> questionBank(@RequestParam(required = false) String type,
@RequestParam(required = false) String taskKey,
@RequestParam(required = false) String keyword,
HttpServletRequest request) {
return new ResultEntity<>(HttpStatus.OK, "查询成功", platformQuestionBankService.list(currentUser(request), type, taskKey, keyword));
}
@GetMapping("/question-bank/{id}")
public ResultEntity<Map<String, Object>> questionBankDetail(@PathVariable long id, HttpServletRequest request) {
return new ResultEntity<>(HttpStatus.OK, "查询成功", platformQuestionBankService.get(currentUser(request), id));
}
@PostMapping("/question-bank")
public ResultEntity<Map<String, Object>> addQuestionBank(@RequestBody Map<String, Object> payload, HttpServletRequest request) {
return new ResultEntity<>(HttpStatus.OK, "保存成功", platformQuestionBankService.save(currentUser(request), null, payload));
}
@PutMapping("/question-bank/{id}")
public ResultEntity<Map<String, Object>> updateQuestionBank(@PathVariable long id, @RequestBody Map<String, Object> payload, HttpServletRequest request) {
return new ResultEntity<>(HttpStatus.OK, "保存成功", platformQuestionBankService.save(currentUser(request), id, payload));
}
@DeleteMapping("/question-bank/{id}")
public ResultEntity<Void> deleteQuestionBank(@PathVariable long id, HttpServletRequest request) {
platformQuestionBankService.delete(currentUser(request), id); return new ResultEntity<>(HttpStatus.OK, "删除成功");
}
@PostMapping("/question-bank/import")
public ResultEntity<List<Map<String, Object>>> importQuestionBank(@RequestPart("file") MultipartFile file,
@RequestParam String taskKey, HttpServletRequest request) {
return new ResultEntity<>(HttpStatus.OK, "导入成功", platformQuestionBankService.importExcel(currentUser(request), file, taskKey));
}
@GetMapping("/question-bank/template")
public void questionBankTemplate(@RequestParam(required = false, defaultValue = "word") String format,
HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws java.io.IOException {
platformAdminService.requirePlatformAdmin(currentUser(request));
examService.questionTemplate(format, response);
}
@PostMapping("/question-bank/attachment")
public ResultEntity<Map<String, Object>> questionBankAttachment(@RequestPart("file") MultipartFile file, HttpServletRequest request) {
return new ResultEntity<>(HttpStatus.OK, "上传成功", platformQuestionBankService.uploadAttachment(currentUser(request), file));
}
@PostMapping("/question-bank/ai/preview")
public ResultEntity<Map<String, Object>> questionBankAiPreview(@RequestBody Map<String, Object> payload, HttpServletRequest request) {
return new ResultEntity<>(HttpStatus.OK, "生成成功", platformQuestionBankService.aiPreview(currentUser(request), payload));
}
@PostMapping("/question-bank/ai/confirm")
public ResultEntity<List<Map<String, Object>>> questionBankAiConfirm(@RequestBody Map<String, Object> payload, HttpServletRequest request) {
return new ResultEntity<>(HttpStatus.OK, "保存成功", platformQuestionBankService.aiConfirm(currentUser(request), payload));
}
@GetMapping("/schools") @GetMapping("/schools")
@ApiOperation("平台超管-学校列表") @ApiOperation("平台超管-学校列表")
@ -224,6 +293,10 @@ public class PlatformAdminController {
platformAdminService.requirePlatformAdmin(user); platformAdminService.requirePlatformAdmin(user);
} }
private JwtUser currentUser(HttpServletRequest request) {
return TokenProvider.getJWTUser(request);
}
private School requireSchool(String schoolId) { private School requireSchool(String schoolId) {
if (StringUtils.isBlank(schoolId)) { if (StringUtils.isBlank(schoolId)) {
throw new IllegalArgumentException("学校ID不能为空"); throw new IllegalArgumentException("学校ID不能为空");

@ -32,9 +32,15 @@ public class ExamController {
@GetMapping("/teacher/questions") @GetMapping("/teacher/questions")
public ResultEntity<List<Map<String, Object>>> questions(@RequestParam(required = false) String type, public ResultEntity<List<Map<String, Object>>> questions(@RequestParam(required = false) String type,
@RequestParam(required = false) String taskKey,
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
HttpServletRequest request) { HttpServletRequest request) {
return ok(examService.questions(user(request), type, keyword)); return ok(examService.questions(user(request), type, taskKey, keyword));
}
@GetMapping("/teacher/question-projects")
public ResultEntity<List<Map<String, Object>>> questionProjects(HttpServletRequest request) {
return ok(examService.questionProjects(user(request)));
} }
@PostMapping("/teacher/questions") @PostMapping("/teacher/questions")

@ -43,6 +43,13 @@ public class ExamService {
public static final String MULTIPLE = "MULTIPLE"; public static final String MULTIPLE = "MULTIPLE";
public static final String TRUE_FALSE = "TRUE_FALSE"; public static final String TRUE_FALSE = "TRUE_FALSE";
public static final String SHORT_ANSWER = "SHORT_ANSWER"; public static final String SHORT_ANSWER = "SHORT_ANSWER";
private static final String FIXED_ORDER = "FIXED";
private static final String RANDOM_ORDER = "RANDOM";
private static final String ALL_OR_NOTHING = "ALL_OR_NOTHING";
private static final String PARTIAL_BY_COUNT = "PARTIAL_BY_COUNT";
private static final String HALF_FOR_PARTIAL = "HALF_FOR_PARTIAL";
private static final Set<String> ORDER_MODES = new HashSet<>(Arrays.asList(FIXED_ORDER, RANDOM_ORDER));
private static final Set<String> MULTIPLE_SCORE_MODES = new HashSet<>(Arrays.asList(ALL_OR_NOTHING, PARTIAL_BY_COUNT, HALF_FOR_PARTIAL));
private static final Set<String> TYPES = new LinkedHashSet<>(Arrays.asList(SINGLE, MULTIPLE, TRUE_FALSE, SHORT_ANSWER)); private static final Set<String> TYPES = new LinkedHashSet<>(Arrays.asList(SINGLE, MULTIPLE, TRUE_FALSE, SHORT_ANSWER));
private static final Set<String> ALLOWED_ATTACHMENTS = new HashSet<>(Arrays.asList("pdf", "doc", "docx", "png", "jpg", "jpeg")); private static final Set<String> ALLOWED_ATTACHMENTS = new HashSet<>(Arrays.asList("pdf", "doc", "docx", "png", "jpg", "jpeg"));
private static final long MAX_ATTACHMENT_SIZE = 20L * 1024 * 1024; private static final long MAX_ATTACHMENT_SIZE = 20L * 1024 * 1024;
@ -67,22 +74,33 @@ public class ExamService {
(rs, n) -> map("id", rs.getString("id"), "name", rs.getString("name")), teacher.getUserId()); (rs, n) -> map("id", rs.getString("id"), "name", rs.getString("name")), teacher.getUserId());
} }
public List<Map<String, Object>> questions(JwtUser teacher, String type, String keyword) { public List<Map<String, Object>> questions(JwtUser teacher, String type, String taskKey, String keyword) {
requireTeacher(teacher); requireTeacher(teacher);
StringBuilder sql = new StringBuilder("select * from exam_question where teacher_id=?"); StringBuilder sql = new StringBuilder("select q.*,t.project_name,t.task_name from exam_question q left join training_task t on t.task_key=q.task_key where (q.source_type='PLATFORM' or (q.source_type='TEACHER' and q.teacher_id=?))");
List<Object> args = new ArrayList<>(); List<Object> args = new ArrayList<>();
args.add(teacher.getUserId()); args.add(teacher.getUserId());
if (StringUtils.isNotBlank(type)) { validateType(type); sql.append(" and question_type=?"); args.add(type); } if (StringUtils.isNotBlank(type)) { validateType(type); sql.append(" and q.question_type=?"); args.add(type); }
if (StringUtils.isNotBlank(keyword)) { sql.append(" and stem like ?"); args.add("%" + keyword.trim() + "%"); } if (StringUtils.isNotBlank(taskKey)) { sql.append(" and q.task_key=?"); args.add(taskKey); }
sql.append(" order by update_time desc, id desc"); if (StringUtils.isNotBlank(keyword)) { sql.append(" and q.stem like ?"); args.add("%" + keyword.trim() + "%"); }
sql.append(" order by q.update_time desc, q.id desc");
return jdbc.query(sql.toString(), (rs, n) -> questionRow(rs, true), args.toArray()); return jdbc.query(sql.toString(), (rs, n) -> questionRow(rs, true), args.toArray());
} }
public List<Map<String, Object>> questions(JwtUser teacher, String type, String keyword) {
return questions(teacher, type, null, keyword);
}
public List<Map<String, Object>> questionProjects(JwtUser teacher) {
requireTeacher(teacher);
return jdbc.query("select task_key,project_name,task_name from training_task where enabled is null or enabled=1 order by sort,project_name,task_name",
(rs, n) -> map("taskKey", rs.getString("task_key"), "projectName", rs.getString("project_name"), "taskName", rs.getString("task_name")));
}
public Map<String, Object> createQuestion(JwtUser teacher, Map<String, Object> payload) { public Map<String, Object> createQuestion(JwtUser teacher, Map<String, Object> payload) {
requireTeacher(teacher); requireTeacher(teacher);
QuestionData q = validateQuestion(payload); QuestionData q = validateQuestion(payload);
jdbc.update("insert into exam_question(school_id,teacher_id,question_type,stem,options_json,answer_json,answer_analysis,attachment_name,attachment_url) values(?,?,?,?,?,?,?,?,?)", jdbc.update("insert into exam_question(school_id,teacher_id,source_type,task_key,question_type,stem,options_json,answer_json,answer_analysis,attachment_name,attachment_url) values(?,?,?,?,?,?,?,?,?,?,?)",
teacher.getSchoolId(), teacher.getUserId(), q.type, q.stem, json(q.options), json(q.answer), q.analysis, teacher.getSchoolId(), teacher.getUserId(), "TEACHER", text(payload.get("taskKey")), q.type, q.stem, json(q.options), json(q.answer), q.analysis,
text(payload.get("attachmentName")), text(payload.get("attachmentUrl"))); text(payload.get("attachmentName")), text(payload.get("attachmentUrl")));
Long id = jdbc.queryForObject("select last_insert_id()", Long.class); Long id = jdbc.queryForObject("select last_insert_id()", Long.class);
return getOwnedQuestion(teacher, id); return getOwnedQuestion(teacher, id);
@ -90,18 +108,20 @@ public class ExamService {
public Map<String, Object> updateQuestion(JwtUser teacher, long id, Map<String, Object> payload) { public Map<String, Object> updateQuestion(JwtUser teacher, long id, Map<String, Object> payload) {
requireTeacher(teacher); requireTeacher(teacher);
getOwnedQuestion(teacher, id); Map<String, Object> existing = getOwnedQuestion(teacher, id);
if ("PLATFORM".equals(existing.get("sourceType"))) throw new AccessDeniedException("平台题库题目只读");
QuestionData q = validateQuestion(payload); QuestionData q = validateQuestion(payload);
jdbc.update("update exam_question set question_type=?,stem=?,options_json=?,answer_json=?,answer_analysis=?,attachment_name=?,attachment_url=? where id=? and teacher_id=?", jdbc.update("update exam_question set task_key=?,question_type=?,stem=?,options_json=?,answer_json=?,answer_analysis=?,attachment_name=?,attachment_url=? where id=? and teacher_id=? and source_type='TEACHER'",
q.type, q.stem, json(q.options), json(q.answer), q.analysis, text(payload.get("taskKey")), q.type, q.stem, json(q.options), json(q.answer), q.analysis,
text(payload.get("attachmentName")), text(payload.get("attachmentUrl")), id, teacher.getUserId()); text(payload.get("attachmentName")), text(payload.get("attachmentUrl")), id, teacher.getUserId());
return getOwnedQuestion(teacher, id); return getOwnedQuestion(teacher, id);
} }
public void deleteQuestion(JwtUser teacher, long id) { public void deleteQuestion(JwtUser teacher, long id) {
requireTeacher(teacher); requireTeacher(teacher);
getOwnedQuestion(teacher, id); Map<String, Object> existing = getOwnedQuestion(teacher, id);
jdbc.update("delete from exam_question where id=? and teacher_id=?", id, teacher.getUserId()); if ("PLATFORM".equals(existing.get("sourceType"))) throw new AccessDeniedException("平台题库题目只读");
jdbc.update("delete from exam_question where id=? and teacher_id=? and source_type='TEACHER'", id, teacher.getUserId());
} }
public Map<String, Object> uploadQuestionAttachment(JwtUser teacher, MultipartFile file) { public Map<String, Object> uploadQuestionAttachment(JwtUser teacher, MultipartFile file) {
@ -263,13 +283,18 @@ public class ExamService {
id == null ? new Object[]{teacher.getSchoolId(), name} : new Object[]{teacher.getSchoolId(), name, id}); id == null ? new Object[]{teacher.getSchoolId(), name} : new Object[]{teacher.getSchoolId(), name, id});
if (duplicate > 0) throw new IllegalArgumentException("考试名称已存在"); if (duplicate > 0) throw new IllegalArgumentException("考试名称已存在");
boolean allowRollback = bool(payload.get("allowRollback"), true); boolean allowRollback = bool(payload.get("allowRollback"), true);
String questionOrderMode = enumValue(payload.get("questionOrderMode"), FIXED_ORDER, ORDER_MODES, "题目乱序方式无效");
String optionOrderMode = enumValue(payload.get("optionOrderMode"), FIXED_ORDER, ORDER_MODES, "选项乱序方式无效");
boolean allowCopyQuestion = bool(payload.get("allowCopyQuestion"), false);
boolean allowPasteAnswer = bool(payload.get("allowPasteAnswer"), false);
String multipleScoreMode = enumValue(payload.get("multipleScoreMode"), ALL_OR_NOTHING, MULTIPLE_SCORE_MODES, "多选题计分方式无效");
long examId; long examId;
if (id == null) { if (id == null) {
jdbc.update("insert into exam_assessment(school_id,teacher_id,exam_name,start_time,end_time,allow_rollback) values(?,?,?,?,?,?)", jdbc.update("insert into exam_assessment(school_id,teacher_id,exam_name,start_time,end_time,allow_rollback,question_order_mode,option_order_mode,allow_copy_question,allow_paste_answer,multiple_score_mode) values(?,?,?,?,?,?,?,?,?,?,?)",
teacher.getSchoolId(), teacher.getUserId(), name, start, end, allowRollback); teacher.getSchoolId(), teacher.getUserId(), name, start, end, allowRollback, questionOrderMode, optionOrderMode, allowCopyQuestion, allowPasteAnswer, multipleScoreMode);
examId = jdbc.queryForObject("select last_insert_id()", Long.class); examId = jdbc.queryForObject("select last_insert_id()", Long.class);
} else { } else {
jdbc.update("update exam_assessment set exam_name=?,start_time=?,end_time=?,allow_rollback=? where id=?", name, start, end, allowRollback, id); jdbc.update("update exam_assessment set exam_name=?,start_time=?,end_time=?,allow_rollback=?,question_order_mode=?,option_order_mode=?,allow_copy_question=?,allow_paste_answer=?,multiple_score_mode=? where id=?", name, start, end, allowRollback, questionOrderMode, optionOrderMode, allowCopyQuestion, allowPasteAnswer, multipleScoreMode, id);
jdbc.update("delete from exam_assessment_class where exam_id=?", id); jdbc.update("delete from exam_assessment_class where exam_id=?", id);
jdbc.update("delete from exam_assessment_question where exam_id=?", id); jdbc.update("delete from exam_assessment_question where exam_id=?", id);
examId = id; examId = id;
@ -440,7 +465,10 @@ public class ExamService {
public List<Map<String, Object>> studentExams(JwtUser student) { public List<Map<String, Object>> studentExams(JwtUser student) {
requireStudent(student); autoSubmitExpired(); requireStudent(student); autoSubmitExpired();
return jdbc.query("select distinct e.*,a.id attempt_id,a.attempt_status,a.total_score from exam_assessment e " + return jdbc.query("select distinct e.*,a.id attempt_id,a.attempt_status,a.total_score," +
"(select u.name from userinfo u where u.user_id=e.teacher_id collate utf8mb4_general_ci limit 1) teacher_name," +
"(select group_concat(distinct s.class_name order by s.class_name separator '、') from exam_assessment_class ec join school_class s on s.school_class_id=ec.teaching_class_id collate utf8mb4_0900_ai_ci where ec.exam_id=e.id) class_names " +
"from exam_assessment e " +
"join exam_assessment_class c on c.exam_id=e.id join teaching_class_student t on t.teaching_class_id=c.teaching_class_id and t.status='IN_PROGRESS' " + "join exam_assessment_class c on c.exam_id=e.id join teaching_class_student t on t.teaching_class_id=c.teaching_class_id and t.status='IN_PROGRESS' " +
"left join exam_attempt a on a.exam_id=e.id and a.student_id=? where t.student_user_id=? and e.publication_status='PUBLISHED' order by e.start_time desc", "left join exam_attempt a on a.exam_id=e.id and a.student_id=? where t.student_user_id=? and e.publication_status='PUBLISHED' order by e.start_time desc",
(rs, n) -> { Map<String, Object> m = examSummary(rs); m.put("attemptId", rs.getObject("attempt_id")); m.put("attemptStatus", rs.getString("attempt_status")); (rs, n) -> { Map<String, Object> m = examSummary(rs); m.put("attemptId", rs.getObject("attempt_id")); m.put("attemptStatus", rs.getString("attempt_status"));
@ -480,13 +508,27 @@ public class ExamService {
if (!"IN_PROGRESS".equals(exam.get("status"))) throw new IllegalArgumentException("考试已结束,不能继续作答"); if (!"IN_PROGRESS".equals(exam.get("status"))) throw new IllegalArgumentException("考试已结束,不能继续作答");
long attemptId = requireAttempt(student, examId, deviceToken, true); long attemptId = requireAttempt(student, examId, deviceToken, true);
Map<String, Object> question = examQuestion(examId, questionId, true); Map<String, Object> question = examQuestion(examId, questionId, true);
Boolean correct = SHORT_ANSWER.equals(question.get("type")) ? null : answersEqual(question.get("answer"), answer); String type = String.valueOf(question.get("type"));
BigDecimal score = correct == null ? null : (correct ? decimal(question.get("score")) : BigDecimal.ZERO); Boolean correct = SHORT_ANSWER.equals(type) ? null : answersEqual(question.get("answer"), answer);
BigDecimal score = SHORT_ANSWER.equals(type) ? null : objectiveScore(exam, question, answer);
jdbc.update("insert into exam_attempt_answer(attempt_id,question_id,answer_json,is_correct,awarded_score) values(?,?,?,?,?) " + jdbc.update("insert into exam_attempt_answer(attempt_id,question_id,answer_json,is_correct,awarded_score) values(?,?,?,?,?) " +
"on duplicate key update answer_json=values(answer_json),is_correct=values(is_correct),awarded_score=values(awarded_score)", "on duplicate key update answer_json=values(answer_json),is_correct=values(is_correct),awarded_score=values(awarded_score)",
attemptId, questionId, json(answer), correct, score); attemptId, questionId, json(answer), correct, score);
} }
private BigDecimal objectiveScore(Map<String, Object> exam, Map<String, Object> question, Object answer) {
BigDecimal full = decimal(question.get("score"));
if (!MULTIPLE.equals(question.get("type"))) return answersEqual(question.get("answer"), answer) ? full : BigDecimal.ZERO;
Set<String> expected = toSet(question.get("answer"));
Set<String> actual = toSet(answer);
if (expected.isEmpty() || actual.isEmpty() || !expected.containsAll(actual)) return BigDecimal.ZERO;
if (expected.equals(actual)) return full;
String mode = String.valueOf(exam.getOrDefault("multipleScoreMode", ALL_OR_NOTHING));
if (PARTIAL_BY_COUNT.equals(mode)) return full.multiply(BigDecimal.valueOf(actual.size())).divide(BigDecimal.valueOf(expected.size()), 2, BigDecimal.ROUND_HALF_UP);
if (HALF_FOR_PARTIAL.equals(mode)) return full.divide(BigDecimal.valueOf(2), 2, BigDecimal.ROUND_HALF_UP);
return BigDecimal.ZERO;
}
public Map<String, Object> uploadAnswerFile(JwtUser student, long examId, long questionId, String deviceToken, MultipartFile file) { public Map<String, Object> uploadAnswerFile(JwtUser student, long examId, long questionId, String deviceToken, MultipartFile file) {
requireStudent(student); validateAttachment(file); requireStudent(student); validateAttachment(file);
Map<String, Object> exam = accessibleStudentExam(student, examId); Map<String, Object> exam = accessibleStudentExam(student, examId);
@ -561,6 +603,7 @@ public class ExamService {
private List<Map<String, Object>> answeredQuestions(long examId, long attemptId, boolean revealAnswer) { private List<Map<String, Object>> answeredQuestions(long examId, long attemptId, boolean revealAnswer) {
List<Map<String, Object>> questions = examQuestions(examId, revealAnswer); List<Map<String, Object>> questions = examQuestions(examId, revealAnswer);
if (!revealAnswer) applyStudentOrdering(examId, attemptId, questions);
Map<Long, Map<String, Object>> answers = jdbc.query("select * from exam_attempt_answer where attempt_id=?", (rs, n) -> { Map<Long, Map<String, Object>> answers = jdbc.query("select * from exam_attempt_answer where attempt_id=?", (rs, n) -> {
Map<String, Object> m = map("questionId", rs.getLong("question_id"), "studentAnswer", parse(rs.getString("answer_json")), Map<String, Object> m = map("questionId", rs.getLong("question_id"), "studentAnswer", parse(rs.getString("answer_json")),
"correct", rs.getObject("is_correct") == null ? null : rs.getBoolean("is_correct"), "awardedScore", rs.getBigDecimal("awarded_score"), "correct", rs.getObject("is_correct") == null ? null : rs.getBoolean("is_correct"), "awardedScore", rs.getBigDecimal("awarded_score"),
@ -570,6 +613,32 @@ public class ExamService {
return questions; return questions;
} }
private void applyStudentOrdering(long examId, long attemptId, List<Map<String, Object>> questions) {
Map<String, Object> exam = jdbc.queryForMap("select question_order_mode,option_order_mode from exam_assessment where id=?", examId);
String questionMode = String.valueOf(exam.getOrDefault("question_order_mode", FIXED_ORDER));
String optionMode = String.valueOf(exam.getOrDefault("option_order_mode", FIXED_ORDER));
if (RANDOM_ORDER.equals(questionMode)) {
Map<String, List<Map<String, Object>>> groups = new LinkedHashMap<>();
for (Map<String, Object> question : questions) groups.computeIfAbsent(String.valueOf(question.get("type")), key -> new ArrayList<>()).add(question);
List<Map<String, Object>> reordered = new ArrayList<>();
for (List<Map<String, Object>> group : groups.values()) {
Collections.shuffle(group, new Random(attemptId * 31L + group.hashCode()));
reordered.addAll(group);
}
questions.clear(); questions.addAll(reordered);
for (int i = 0; i < questions.size(); i++) questions.get(i).put("order", i + 1);
}
if (RANDOM_ORDER.equals(optionMode)) {
for (Map<String, Object> question : questions) {
List<Map<String, Object>> options = mapList(question.get("options"));
if (options.size() > 1) {
Collections.shuffle(options, new Random(attemptId * 131L + ((Number) question.get("id")).longValue()));
question.put("options", options);
}
}
}
}
private List<Map<String, Object>> examQuestions(long examId, boolean revealAnswer) { private List<Map<String, Object>> examQuestions(long examId, boolean revealAnswer) {
return jdbc.query("select p.question_id id,p.question_type,p.stem,p.options_json,p.answer_json,p.answer_analysis,p.attachment_name,p.attachment_url,p.create_time,p.update_time,p.question_score,p.question_order " + return jdbc.query("select p.question_id id,p.question_type,p.stem,p.options_json,p.answer_json,p.answer_analysis,p.attachment_name,p.attachment_url,p.create_time,p.update_time,p.question_score,p.question_order " +
"from exam_assessment_question p where p.exam_id=? order by p.question_order", "from exam_assessment_question p where p.exam_id=? order by p.question_order",
@ -583,7 +652,7 @@ public class ExamService {
} }
private Map<String, Object> getOwnedQuestion(JwtUser teacher, long id) { private Map<String, Object> getOwnedQuestion(JwtUser teacher, long id) {
List<Map<String, Object>> rows = jdbc.query("select * from exam_question where id=? and teacher_id=?", (rs, n) -> questionRow(rs, true), id, teacher.getUserId()); List<Map<String, Object>> rows = jdbc.query("select q.*,t.project_name,t.task_name from exam_question q left join training_task t on t.task_key=q.task_key where q.id=? and (q.source_type='PLATFORM' or (q.source_type='TEACHER' and q.teacher_id=?))", (rs, n) -> questionRow(rs, true), id, teacher.getUserId());
if (rows.isEmpty()) throw new IllegalArgumentException("题目不存在或无权访问"); if (rows.isEmpty()) throw new IllegalArgumentException("题目不存在或无权访问");
return rows.get(0); return rows.get(0);
} }
@ -630,6 +699,14 @@ public class ExamService {
"startTime", start, "endTime", end, "durationMinutes", Math.max(0, (end.getTime() - start.getTime()) / 60000), "startTime", start, "endTime", end, "durationMinutes", Math.max(0, (end.getTime() - start.getTime()) / 60000),
"publicationStatus", publication, "status", status(publication, start, end), "allowRollback", rs.getBoolean("allow_rollback"), "publicationStatus", publication, "status", status(publication, start, end), "allowRollback", rs.getBoolean("allow_rollback"),
"exportedAt", getQuiet(rs, "exported_at"), "totalScore", getQuiet(rs, "total_score")); "exportedAt", getQuiet(rs, "exported_at"), "totalScore", getQuiet(rs, "total_score"));
putQuiet(rs, m, "question_order_mode", "questionOrderMode");
putQuiet(rs, m, "option_order_mode", "optionOrderMode");
Object copyQuestion = getQuiet(rs, "allow_copy_question"); if (copyQuestion != null) m.put("allowCopyQuestion", copyQuestion instanceof Boolean ? copyQuestion : rsBoolean(copyQuestion));
Object pasteAnswer = getQuiet(rs, "allow_paste_answer"); if (pasteAnswer != null) m.put("allowPasteAnswer", pasteAnswer instanceof Boolean ? pasteAnswer : rsBoolean(pasteAnswer));
putQuiet(rs, m, "multiple_score_mode", "multipleScoreMode");
m.putIfAbsent("questionOrderMode", FIXED_ORDER); m.putIfAbsent("optionOrderMode", FIXED_ORDER);
m.putIfAbsent("allowCopyQuestion", false); m.putIfAbsent("allowPasteAnswer", false); m.putIfAbsent("multipleScoreMode", ALL_OR_NOTHING);
putQuiet(rs, m, "teacher_name", "teacherName"); putQuiet(rs, m, "class_names", "classNames");
putQuiet(rs, m, "class_count", "classCount"); putQuiet(rs, m, "question_count", "questionCount"); putQuiet(rs, m, "class_count", "classCount"); putQuiet(rs, m, "question_count", "questionCount");
return m; return m;
} }
@ -666,6 +743,8 @@ public class ExamService {
"options", parse(rs.getString("options_json")), "analysis", revealAnswer ? rs.getString("answer_analysis") : null, "options", parse(rs.getString("options_json")), "analysis", revealAnswer ? rs.getString("answer_analysis") : null,
"attachmentName", rs.getString("attachment_name"), "attachmentUrl", rs.getString("attachment_url"), "attachmentName", rs.getString("attachment_name"), "attachmentUrl", rs.getString("attachment_url"),
"createdAt", rs.getTimestamp("create_time"), "updatedAt", rs.getTimestamp("update_time")); "createdAt", rs.getTimestamp("create_time"), "updatedAt", rs.getTimestamp("update_time"));
putQuiet(rs, q, "source_type", "sourceType"); putQuiet(rs, q, "task_key", "taskKey");
putQuiet(rs, q, "project_name", "projectName"); putQuiet(rs, q, "task_name", "taskName");
if (revealAnswer) q.put("answer", parse(rs.getString("answer_json"))); if (revealAnswer) q.put("answer", parse(rs.getString("answer_json")));
return q; return q;
} }
@ -1099,12 +1178,22 @@ public class ExamService {
private static Number number(Object value) { if (value instanceof Number) return (Number) value; try { return Long.parseLong(text(value)); } catch (Exception e) { throw new IllegalArgumentException("数字参数格式错误"); } } private static Number number(Object value) { if (value instanceof Number) return (Number) value; try { return Long.parseLong(text(value)); } catch (Exception e) { throw new IllegalArgumentException("数字参数格式错误"); } }
private static BigDecimal decimal(Object value) { if (value == null || text(value).isEmpty()) return BigDecimal.ZERO; return value instanceof BigDecimal ? (BigDecimal) value : new BigDecimal(text(value)); } private static BigDecimal decimal(Object value) { if (value == null || text(value).isEmpty()) return BigDecimal.ZERO; return value instanceof BigDecimal ? (BigDecimal) value : new BigDecimal(text(value)); }
private static boolean bool(Object value, boolean fallback) { return value == null ? fallback : Boolean.parseBoolean(text(value)); } private static boolean bool(Object value, boolean fallback) { return value == null ? fallback : Boolean.parseBoolean(text(value)); }
private static String enumValue(Object value, String fallback, Set<String> allowed, String message) {
String normalized = StringUtils.defaultIfBlank(text(value), fallback).trim().toUpperCase(Locale.ROOT);
if (!allowed.contains(normalized)) throw new IllegalArgumentException(message);
return normalized;
}
private static Timestamp parseTime(Object value) { try { String s = text(value).replace('T', ' '); if (s.length() == 16) s += ":00"; return Timestamp.valueOf(LocalDateTime.parse(s, TIME)); } catch (Exception e) { throw new IllegalArgumentException("考试时间格式不正确"); } } private static Timestamp parseTime(Object value) { try { String s = text(value).replace('T', ' '); if (s.length() == 16) s += ":00"; return Timestamp.valueOf(LocalDateTime.parse(s, TIME)); } catch (Exception e) { throw new IllegalArgumentException("考试时间格式不正确"); } }
private String json(Object value) { try { return objectMapper.writeValueAsString(value); } catch (Exception e) { throw new IllegalArgumentException("数据格式错误"); } } private String json(Object value) { try { return objectMapper.writeValueAsString(value); } catch (Exception e) { throw new IllegalArgumentException("数据格式错误"); } }
private Object parse(String value) { if (StringUtils.isBlank(value)) return null; try { return objectMapper.readValue(value, Object.class); } catch (Exception e) { return value; } } private Object parse(String value) { if (StringUtils.isBlank(value)) return null; try { return objectMapper.readValue(value, Object.class); } catch (Exception e) { return value; } }
private List<Map<String, Object>> mapList(Object value) { if (value == null) return new ArrayList<>(); return objectMapper.convertValue(value, new TypeReference<List<Map<String, Object>>>(){}); } private List<Map<String, Object>> mapList(Object value) { if (value == null) return new ArrayList<>(); return objectMapper.convertValue(value, new TypeReference<List<Map<String, Object>>>(){}); }
private List<String> stringList(Object value) { if (value == null) return new ArrayList<>(); if (value instanceof Collection) return ((Collection<?>) value).stream().map(String::valueOf).filter(StringUtils::isNotBlank).collect(Collectors.toList()); String s = text(value); return StringUtils.isBlank(s) ? new ArrayList<>() : Collections.singletonList(s); } private List<String> stringList(Object value) { if (value == null) return new ArrayList<>(); if (value instanceof Collection) return ((Collection<?>) value).stream().map(String::valueOf).filter(StringUtils::isNotBlank).collect(Collectors.toList()); String s = text(value); return StringUtils.isBlank(s) ? new ArrayList<>() : Collections.singletonList(s); }
private static Object getQuiet(ResultSet rs, String column) { try { return rs.getObject(column); } catch (SQLException e) { return null; } } private static Object getQuiet(ResultSet rs, String column) { try { return rs.getObject(column); } catch (SQLException e) { return null; } }
private static boolean rsBoolean(Object value) {
if (value instanceof byte[]) return ((byte[]) value).length > 0 && ((byte[]) value)[0] != 0;
if (value instanceof Number) return ((Number) value).intValue() != 0;
return Boolean.parseBoolean(String.valueOf(value));
}
private static void putQuiet(ResultSet rs, Map<String, Object> map, String column, String key) { Object value = getQuiet(rs, column); if (value != null) map.put(key, value); } private static void putQuiet(ResultSet rs, Map<String, Object> map, String column, String key) { Object value = getQuiet(rs, column); if (value != null) map.put(key, value); }
private static Map<String, Object> map(Object... values) { Map<String, Object> m = new LinkedHashMap<>(); for (int i = 0; i < values.length; i += 2) m.put(String.valueOf(values[i]), values[i + 1]); return m; } private static Map<String, Object> map(Object... values) { Map<String, Object> m = new LinkedHashMap<>(); for (int i = 0; i < values.length; i += 2) m.put(String.valueOf(values[i]), values[i + 1]); return m; }
private static boolean answersEqual(Object expected, Object actual) { if (expected instanceof Collection || actual instanceof Collection) { Set<String> a = toSet(expected), b = toSet(actual); return a.equals(b); } return String.valueOf(expected).equalsIgnoreCase(String.valueOf(actual)); } private static boolean answersEqual(Object expected, Object actual) { if (expected instanceof Collection || actual instanceof Collection) { Set<String> a = toSet(expected), b = toSet(actual); return a.equals(b); } return String.valueOf(expected).equalsIgnoreCase(String.valueOf(actual)); }

@ -0,0 +1,179 @@
package com.sztzjy.linkCommerce.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sztzjy.linkCommerce.ai.QwenChatClient;
import com.sztzjy.linkCommerce.config.security.JwtUser;
import com.sztzjy.linkCommerce.util.file.IFileUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class PlatformQuestionBankService {
private static final String PLATFORM = "PLATFORM";
private final JdbcTemplate jdbc;
private final ObjectMapper objectMapper;
private final PlatformAdminService platformAdminService;
private final QwenChatClient qwen;
private final IFileUtil fileUtil;
public PlatformQuestionBankService(JdbcTemplate jdbc, ObjectMapper objectMapper,
PlatformAdminService platformAdminService, QwenChatClient qwen, IFileUtil fileUtil) {
this.jdbc = jdbc; this.objectMapper = objectMapper; this.platformAdminService = platformAdminService; this.qwen = qwen; this.fileUtil = fileUtil;
}
public Map<String, Object> uploadAttachment(JwtUser user, MultipartFile file) {
platformAdminService.requirePlatformAdmin(user);
if (file == null || file.isEmpty()) throw new IllegalArgumentException("请选择资料文件");
if (file.getSize() > 20L * 1024 * 1024) throw new IllegalArgumentException("资料不能超过20MB");
String name = StringUtils.defaultString(file.getOriginalFilename()).replace("\\", "_").replace("/", "_");
int dot = name.lastIndexOf('.'); String ext = dot < 0 ? "" : name.substring(dot + 1).toLowerCase(Locale.ROOT);
if (!Arrays.asList("pdf", "doc", "docx", "png", "jpg", "jpeg").contains(ext)) throw new IllegalArgumentException("资料仅支持PDF、Word和图片格式");
String path = fileUtil.upload(file);
return map("name", name, "url", "/" + path.replace('\\', '/'));
}
public List<Map<String, Object>> projects(JwtUser user) {
platformAdminService.requirePlatformAdmin(user);
return jdbc.query("select task_key,project_name,task_name from training_task where enabled is null or enabled=1 order by sort,project_name,task_name",
(rs, n) -> map("taskKey", rs.getString("task_key"), "projectName", rs.getString("project_name"), "taskName", rs.getString("task_name")));
}
public List<Map<String, Object>> list(JwtUser user, String type, String taskKey, String keyword) {
platformAdminService.requirePlatformAdmin(user);
StringBuilder sql = new StringBuilder("select q.*,t.project_name,t.task_name from exam_question q left join training_task t on t.task_key=q.task_key where q.source_type='PLATFORM'");
List<Object> args = new ArrayList<>();
if (StringUtils.isNotBlank(type)) { validateType(type); sql.append(" and q.question_type=?"); args.add(type); }
if (StringUtils.isNotBlank(taskKey)) { sql.append(" and q.task_key=?"); args.add(taskKey); }
if (StringUtils.isNotBlank(keyword)) { sql.append(" and q.stem like ?"); args.add("%" + keyword.trim() + "%"); }
sql.append(" order by q.update_time desc,q.id desc");
return jdbc.query(sql.toString(), (rs, n) -> row(rs), args.toArray());
}
public Map<String, Object> get(JwtUser user, long id) {
platformAdminService.requirePlatformAdmin(user);
List<Map<String, Object>> rows = jdbc.query("select q.*,t.project_name,t.task_name from exam_question q left join training_task t on t.task_key=q.task_key where q.id=? and q.source_type='PLATFORM'", (rs, n) -> row(rs), id);
if (rows.isEmpty()) throw new IllegalArgumentException("题目不存在");
return rows.get(0);
}
public Map<String, Object> save(JwtUser user, Long id, Map<String, Object> payload) {
platformAdminService.requirePlatformAdmin(user);
ValidQuestion q = validate(payload);
requireTask(q.taskKey);
if (id == null) {
jdbc.update("insert into exam_question(school_id,teacher_id,source_type,task_key,question_type,stem,options_json,answer_json,answer_analysis,attachment_name,attachment_url) values(?,?,?,?,?,?,?,?,?,?,?)",
PLATFORM, PLATFORM, PLATFORM, q.taskKey, q.type, q.stem, json(q.options), json(q.answer), q.analysis, text(payload.get("attachmentName")), text(payload.get("attachmentUrl")));
id = jdbc.queryForObject("select last_insert_id()", Long.class);
} else {
get(user, id);
jdbc.update("update exam_question set task_key=?,question_type=?,stem=?,options_json=?,answer_json=?,answer_analysis=?,attachment_name=?,attachment_url=? where id=? and source_type='PLATFORM'",
q.taskKey, q.type, q.stem, json(q.options), json(q.answer), q.analysis, text(payload.get("attachmentName")), text(payload.get("attachmentUrl")), id);
}
return get(user, id);
}
public void delete(JwtUser user, long id) {
platformAdminService.requirePlatformAdmin(user); get(user, id);
jdbc.update("delete from exam_question where id=? and source_type='PLATFORM'", id);
}
public Map<String, Object> aiPreview(JwtUser user, Map<String, Object> request) {
platformAdminService.requirePlatformAdmin(user);
String taskKey = required(request, "taskKey", "请选择归属项目");
requireTask(taskKey);
String type = required(request, "type", "请选择题型"); validateType(type);
int count;
try { count = Integer.parseInt(text(request.get("count"))); } catch (Exception e) { throw new IllegalArgumentException("题目数量必须是数字"); }
if (count < 1 || count > 50) throw new IllegalArgumentException("题目数量需在1到50之间");
String requirement = StringUtils.defaultString(text(request.get("requirement")), "围绕项目任务设计,题干清晰,答案准确,避免重复。");
String prompt = "请生成" + count + "道" + typeLabel(type) + "。每道题包含 type,stem,options,answer,analysis。" +
"选择题 options 为 [{label,content}],判断题 answer 为 true/false,简答题 answer 为参考答案字符串。" + requirement;
String raw = qwen.completeJson("你是严谨的考试题库出题助手,只输出JSON对象,格式为{\"questions\":[...]}", prompt);
try {
JsonNode root = objectMapper.readTree(raw);
JsonNode items = root.isArray() ? root : root.path("questions");
if (!items.isArray()) throw new IllegalArgumentException("AI返回格式不正确");
List<Map<String, Object>> questions = objectMapper.convertValue(items, new TypeReference<List<Map<String, Object>>>(){});
if (questions.size() != count) throw new IllegalArgumentException("AI生成题目数量不符合要求");
List<Map<String, Object>> normalized = questions.stream().map(item -> { item.put("taskKey", taskKey); item.put("type", StringUtils.defaultIfBlank(text(item.get("type")), type)); return normalize(item); }).collect(Collectors.toList());
return map("taskKey", taskKey, "questions", normalized);
} catch (IllegalArgumentException e) { throw e; } catch (Exception e) { throw new IllegalArgumentException("AI返回内容无法解析"); }
}
@Transactional
public List<Map<String, Object>> aiConfirm(JwtUser user, Map<String, Object> request) {
platformAdminService.requirePlatformAdmin(user);
List<Map<String, Object>> questions = objectMapper.convertValue(request.get("questions"), new TypeReference<List<Map<String, Object>>>(){});
if (questions.isEmpty() || questions.size() > 50) throw new IllegalArgumentException("确认入库的题目数量不正确");
List<Map<String, Object>> saved = new ArrayList<>();
for (Map<String, Object> q : questions) saved.add(save(user, null, q));
return saved;
}
@Transactional
public List<Map<String, Object>> importExcel(JwtUser user, MultipartFile file, String taskKey) {
platformAdminService.requirePlatformAdmin(user); requireTask(taskKey);
if (file == null || file.isEmpty()) throw new IllegalArgumentException("请选择Word或Excel文件");
String name = StringUtils.defaultString(file.getOriginalFilename()).toLowerCase(Locale.ROOT);
if (name.endsWith(".docx")) return importWord(user, file, taskKey);
try (Workbook workbook = WorkbookFactory.create(file.getInputStream())) {
Sheet sheet = workbook.getSheetAt(0); DataFormatter formatter = new DataFormatter(); Row header = sheet.getRow(0);
if (header == null) throw new IllegalArgumentException("Excel缺少表头");
Map<String, Integer> columns = new HashMap<>(); for (int i=0;i<header.getLastCellNum();i++) columns.put(formatter.formatCellValue(header.getCell(i)).trim(), i);
List<Map<String,Object>> result = new ArrayList<>();
for (int r=1;r<=sheet.getLastRowNum();r++) { Row row=sheet.getRow(r); if (row==null) continue; Map<String,Object> q=new LinkedHashMap<>(); q.put("taskKey",taskKey); q.put("type", excelType(value(row, columns.get("题型"), formatter))); q.put("stem", value(row, columns.get("题干"), formatter)); q.put("answer", value(row, columns.get("正确答案"), formatter)); q.put("analysis", value(row, columns.get("答案解析"), formatter)); List<Map<String,Object>> options=new ArrayList<>(); for(char c='A';c<='H';c++){String v=value(row,columns.get("选项"+c),formatter); if(StringUtils.isNotBlank(v)) options.add(map("label",String.valueOf(c),"content",v));} q.put("options",options); if(StringUtils.isBlank(text(q.get("stem")))) continue; result.add(save(user,null,q)); }
return result;
} catch (IllegalArgumentException e) { throw e; } catch (Exception e) { throw new IllegalArgumentException("Excel导入失败,请检查模板"); }
}
private List<Map<String, Object>> importWord(JwtUser user, MultipartFile file, String taskKey) {
try (XWPFDocument doc = new XWPFDocument(file.getInputStream())) {
List<Map<String,Object>> result = new ArrayList<>(); Map<String,Object> current = null; List<Map<String,Object>> options = new ArrayList<>(); String type = "SHORT_ANSWER";
for (org.apache.poi.xwpf.usermodel.XWPFParagraph p : doc.getParagraphs()) {
String line = StringUtils.trimToEmpty(p.getText()); if (line.isEmpty()) continue;
String marker = line.replace("[", "").replace("]", "").replace("【", "").replace("】", "").trim();
if (Arrays.asList("单选题","多选题","判断题","简答题").contains(marker)) { if (current != null) { current.put("options", options); result.add(save(user,null,current)); } type = excelType(marker); current = null; options = new ArrayList<>(); continue; }
if (line.startsWith("题干") || line.startsWith("题目")) { if (current != null) { current.put("options", options); result.add(save(user,null,current)); } current = new LinkedHashMap<>(); current.put("taskKey",taskKey); current.put("type",type); current.put("stem",after(line)); continue; }
if (current == null) continue;
if (line.matches("^[A-H][..、].*")) { options.add(map("label",line.substring(0,1),"content",line.substring(2).trim())); }
else if (line.startsWith("答案")) current.put("answer",after(line));
else if (line.startsWith("解析")) current.put("analysis",after(line));
}
if (current != null) { current.put("options", options); result.add(save(user,null,current)); }
if (result.isEmpty()) throw new IllegalArgumentException("Word未读取到题目"); return result;
} catch (IllegalArgumentException e) { throw e; } catch (Exception e) { throw new IllegalArgumentException("Word导入失败,请检查模板"); }
}
private ValidQuestion validate(Map<String,Object> p) { String type=required(p,"type","请选择题型"); validateType(type); String stem=required(p,"stem","题干不能为空"); if(StringUtils.isBlank(plain(stem))) throw new IllegalArgumentException("题干不能为空"); List<Map<String,Object>> options=objectMapper.convertValue(p.getOrDefault("options",Collections.emptyList()),new TypeReference<List<Map<String,Object>>>(){}); Object answer=p.get("answer"); if(type.equals("SINGLE")||type.equals("MULTIPLE")){ if(options.size()<2) throw new IllegalArgumentException("选择题至少需要两个选项"); Set<String> labels=new LinkedHashSet<>(); List<Map<String,Object>> normalized=new ArrayList<>(); for(Map<String,Object> o:options){String label=text(o.get("label")).trim().toUpperCase();String content=text(o.get("content")).trim();if(!label.matches("[A-H]")||StringUtils.isBlank(plain(content))||!labels.add(label)) throw new IllegalArgumentException("选项格式不正确"); normalized.add(map("label",label,"content",content));} List<String> answers=list(answer); if(type.equals("SINGLE")&&answers.size()!=1) throw new IllegalArgumentException("单选题只能设置一个正确答案"); if(answers.isEmpty()||!labels.containsAll(answers)) throw new IllegalArgumentException("正确答案必须来自题目选项"); return new ValidQuestion(type,stem,normalized,answers,text(p.get("analysis")),required(p,"taskKey","请选择归属项目")); } if(type.equals("TRUE_FALSE")){String v=text(answer);if(!"true".equalsIgnoreCase(v)&&!"false".equalsIgnoreCase(v)&&!"正确".equals(v)&&!"错误".equals(v))throw new IllegalArgumentException("判断题答案格式不正确");return new ValidQuestion(type,stem,Collections.emptyList(),"正确".equals(v)||"true".equalsIgnoreCase(v),text(p.get("analysis")),required(p,"taskKey","请选择归属项目"));} if(StringUtils.isBlank(plain(text(answer)))) throw new IllegalArgumentException("参考答案不能为空"); return new ValidQuestion(type,stem,Collections.emptyList(),text(answer),text(p.get("analysis")),required(p,"taskKey","请选择归属项目")); }
private Map<String,Object> normalize(Map<String,Object> p){ValidQuestion q=validate(p);return map("type",q.type,"stem",q.stem,"options",q.options,"answer",q.answer,"analysis",q.analysis,"taskKey",q.taskKey);}
private void requireTask(String key){if(jdbc.queryForObject("select count(*) from training_task where task_key=?",Integer.class,key)==0)throw new IllegalArgumentException("归属项目不存在");}
private Map<String,Object> row(java.sql.ResultSet rs)throws java.sql.SQLException{return map("id",rs.getLong("id"),"type",rs.getString("question_type"),"stem",rs.getString("stem"),"options",parse(rs.getString("options_json")),"answer",parse(rs.getString("answer_json")),"analysis",rs.getString("answer_analysis"),"attachmentName",rs.getString("attachment_name"),"attachmentUrl",rs.getString("attachment_url"),"sourceType",rs.getString("source_type"),"taskKey",rs.getString("task_key"),"projectName",rs.getString("project_name"),"taskName",rs.getString("task_name"),"updatedAt",rs.getTimestamp("update_time"));}
private Object parse(String s){try{return StringUtils.isBlank(s)?null:objectMapper.readValue(s,Object.class);}catch(Exception e){return s;}}
private String json(Object o){try{return objectMapper.writeValueAsString(o);}catch(Exception e){throw new IllegalArgumentException("数据格式错误");}}
private static String required(Map<String,Object> p,String k,String m){String v=text(p.get(k));if(StringUtils.isBlank(v))throw new IllegalArgumentException(m);return v.trim();}
private static String text(Object v){return v==null?"":String.valueOf(v);}
private static String plain(String value){return value == null ? "" : value.replaceAll("<[^>]+>", "").replace("&nbsp;", "").trim();}
private static List<String> list(Object v){if(v instanceof Collection)return ((Collection<?>)v).stream().map(String::valueOf).map(s->s.trim().toUpperCase()).filter(StringUtils::isNotBlank).distinct().collect(Collectors.toList());String s=text(v);return StringUtils.isBlank(s)?Collections.emptyList():Arrays.stream(s.split("[,,、 ]")).map(String::trim).map(String::toUpperCase).filter(StringUtils::isNotBlank).distinct().collect(Collectors.toList());}
private static void validateType(String t){if(!Arrays.asList("SINGLE","MULTIPLE","TRUE_FALSE","SHORT_ANSWER").contains(t))throw new IllegalArgumentException("不支持的题型");}
private static String typeLabel(String t){return mapOf("SINGLE","单选题","MULTIPLE","多选题","TRUE_FALSE","判断题","SHORT_ANSWER","简答题").get(t);}
private static String excelType(String t){if("单选题".equals(t))return "SINGLE";if("多选题".equals(t))return "MULTIPLE";if("判断题".equals(t))return "TRUE_FALSE";return "SHORT_ANSWER";}
private static String after(String s){int i=Math.max(s.indexOf(':'),s.indexOf(':'));return i<0?"":s.substring(i+1).trim();}
private static String value(Row r,Integer i,DataFormatter f){return i==null?"":f.formatCellValue(r.getCell(i)).trim();}
private static Map<String,Object> map(Object...v){Map<String,Object>m=new LinkedHashMap<>();for(int i=0;i<v.length;i+=2)m.put(String.valueOf(v[i]),v[i+1]);return m;}
private static Map<String,String> mapOf(String...v){Map<String,String>m=new HashMap<>();for(int i=0;i<v.length;i+=2)m.put(v[i],v[i+1]);return m;}
private static final class ValidQuestion{final String type,stem,analysis,taskKey;final List<Map<String,Object>> options;final Object answer;ValidQuestion(String t,String s,List<Map<String,Object>>o,Object a,String an,String k){type=t;stem=s;options=o;answer=a;analysis=an;taskKey=k;}}
}
Loading…
Cancel
Save