diff --git a/src/main/java/com/sztzjy/linkCommerce/service/ExamService.java b/src/main/java/com/sztzjy/linkCommerce/service/ExamService.java index 6cfb995..e5398d1 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/ExamService.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/ExamService.java @@ -52,6 +52,7 @@ public class ExamService { private static final Set MULTIPLE_SCORE_MODES = new HashSet<>(Arrays.asList(ALL_OR_NOTHING, PARTIAL_BY_COUNT, HALF_FOR_PARTIAL)); private static final Set TYPES = new LinkedHashSet<>(Arrays.asList(SINGLE, MULTIPLE, TRUE_FALSE, SHORT_ANSWER)); private static final Set ALLOWED_ATTACHMENTS = new HashSet<>(Arrays.asList("pdf", "doc", "docx", "png", "jpg", "jpeg")); + private static final Set ALLOWED_MATERIALS = new HashSet<>(Arrays.asList("pdf", "doc", "docx", "xls", "xlsx", "png", "jpg", "jpeg")); private static final long MAX_ATTACHMENT_SIZE = 20L * 1024 * 1024; private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final Pattern OPTION_LINE = Pattern.compile("^([A-Ha-h])\\s*[..、::]\\s*(.+)$"); @@ -133,7 +134,7 @@ public class ExamService { public Map uploadQuestionAttachment(JwtUser teacher, MultipartFile file) { requireTeacher(teacher); - validateAttachment(file); + validateMaterial(file); String path = fileUtil.upload(file); return map("name", safeFilename(file.getOriginalFilename()), "url", "/" + path.replace('\\', '/')); } @@ -310,7 +311,7 @@ public class ExamService { int order = 1; for (Map item : paper) { long questionId = number(item.get("questionId")).longValue(); - Map question = getOwnedQuestion(teacher, questionId); + Map question = getUsableQuestion(teacher, questionId); jdbc.update("insert into exam_assessment_question(exam_id,question_id,question_order,question_score,question_type,stem,options_json,answer_json,answer_analysis,attachment_name,attachment_url) values(?,?,?,?,?,?,?,?,?,?,?)", examId, questionId, order++, decimal(item.get("score")), question.get("type"), question.get("stem"), json(question.get("options")), json(question.get("answer")), question.get("analysis"), question.get("attachmentName"), question.get("attachmentUrl")); @@ -676,6 +677,13 @@ public class ExamService { return rows.get(0); } + private Map getUsableQuestion(JwtUser teacher, long id) { + String schoolId = teacher.getSchoolId(); + List> 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=? or (? is not null and q.school_id=?))))", (rs, n) -> questionRow(rs, true), id, teacher.getUserId(), schoolId, schoolId); + if (rows.isEmpty()) throw new IllegalArgumentException("题目不存在或无权访问"); + return rows.get(0); + } + private Map ownedExam(JwtUser teacher, long id) { requireTeacher(teacher); List> rows = jdbc.query("select e.*,(select coalesce(sum(question_score),0) from exam_assessment_question q where q.exam_id=e.id) total_score from exam_assessment e where e.id=? and e.teacher_id=?", @@ -747,7 +755,8 @@ public class ExamService { } static void validateDelete(String status, Object exportedAt) { - requireState(status, "ENDED", "只有已结束考试可以删除"); + if ("DRAFT".equals(status)) return; + requireState(status, "ENDED", "只有未发布或已结束的考试可以删除"); if (exportedAt == null) throw new IllegalArgumentException("请先导出全部试卷后再删除考试"); } @@ -800,7 +809,7 @@ public class ExamService { for (Map item : paper) { long id = number(item.get("questionId")).longValue(); if (!seen.add(id)) throw new IllegalArgumentException("试卷中存在重复题目"); - getOwnedQuestion(teacher, id); + getUsableQuestion(teacher, id); if (decimal(item.get("score")).signum() <= 0) throw new IllegalArgumentException("每题分值必须大于0"); } } @@ -1188,7 +1197,9 @@ public class ExamService { if (!Arrays.asList("docx", "xls", "xlsx").contains(extension)) throw new IllegalArgumentException("仅支持docx、xls或xlsx格式"); return extension; } - private static void validateAttachment(MultipartFile file) { if (file == null || file.isEmpty()) throw new IllegalArgumentException("请选择附件"); if (file.getSize() > MAX_ATTACHMENT_SIZE) throw new IllegalArgumentException("附件不能超过20MB"); String n = safeFilename(file.getOriginalFilename()); int i = n.lastIndexOf('.'); if (i < 0 || !ALLOWED_ATTACHMENTS.contains(n.substring(i + 1).toLowerCase())) throw new IllegalArgumentException("附件仅支持PDF、Word和图片格式"); } + private static void validateAttachment(MultipartFile file) { validateAttachment(file, ALLOWED_ATTACHMENTS, "附件仅支持PDF、Word和图片格式"); } + private static void validateMaterial(MultipartFile file) { validateAttachment(file, ALLOWED_MATERIALS, "题目资料仅支持PDF、Word、Excel和图片格式"); } + private static void validateAttachment(MultipartFile file, Set allowed, String unsupportedMessage) { if (file == null || file.isEmpty()) throw new IllegalArgumentException("请选择附件"); if (file.getSize() > MAX_ATTACHMENT_SIZE) throw new IllegalArgumentException("附件不能超过20MB"); String n = safeFilename(file.getOriginalFilename()); int i = n.lastIndexOf('.'); if (i < 0 || !allowed.contains(n.substring(i + 1).toLowerCase())) throw new IllegalArgumentException(unsupportedMessage); } private static String safeFilename(String name) { return StringUtils.defaultString(name).replace("\\", "_").replace("/", "_"); } private static void requireTeacher(JwtUser user) { if (user == null) throw new AccessDeniedException("请先登录"); if (user.getRoleId() != 3) throw new AccessDeniedException("仅教师可以操作"); } private static void requireStudent(JwtUser user) { if (user == null) throw new AccessDeniedException("请先登录"); if (user.getRoleId() != 4) throw new AccessDeniedException("仅学生可以操作"); } diff --git a/src/main/java/com/sztzjy/linkCommerce/service/PlatformQuestionBankService.java b/src/main/java/com/sztzjy/linkCommerce/service/PlatformQuestionBankService.java index 40ecfd9..5d32ec5 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/PlatformQuestionBankService.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/PlatformQuestionBankService.java @@ -41,7 +41,7 @@ public class PlatformQuestionBankService { 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和图片格式"); + if (!Arrays.asList("pdf", "doc", "docx", "xls", "xlsx", "png", "jpg", "jpeg").contains(ext)) throw new IllegalArgumentException("资料仅支持PDF、Word、Excel和图片格式"); String path = fileUtil.upload(file); return map("name", name, "url", "/" + path.replace('\\', '/')); } diff --git a/src/test/java/com/sztzjy/linkCommerce/service/ExamServiceTest.java b/src/test/java/com/sztzjy/linkCommerce/service/ExamServiceTest.java index d567c36..c929c07 100644 --- a/src/test/java/com/sztzjy/linkCommerce/service/ExamServiceTest.java +++ b/src/test/java/com/sztzjy/linkCommerce/service/ExamServiceTest.java @@ -41,6 +41,30 @@ class ExamServiceTest { private final JdbcTemplate jdbc = mock(JdbcTemplate.class); private final ExamService service = new ExamService(jdbc, new ObjectMapper(), mock(IFileUtil.class)); + @Test + void questionMaterialAcceptsWordAndExcelButRejectsOtherTypes() { + IFileUtil fileUtil = mock(IFileUtil.class); + when(fileUtil.upload(any(org.springframework.web.multipart.MultipartFile.class))).thenReturn("exam/material"); + ExamService uploadService = new ExamService(jdbc, new ObjectMapper(), fileUtil); + + for (String name : Arrays.asList("资料.doc", "资料.docx", "数据.xls", "数据.XLSX", "说明.pdf", "图.png")) { + Map result = uploadService.uploadQuestionAttachment(teacher(), new MockMultipartFile("file", name, "application/octet-stream", new byte[]{1})); + assertEquals(name, result.get("name")); + } + for (String name : Arrays.asList("脚本.exe", "数据.csv", "无扩展名")) { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> uploadService.uploadQuestionAttachment(teacher(), new MockMultipartFile("file", name, "application/octet-stream", new byte[]{1}))); + assertEquals("题目资料仅支持PDF、Word、Excel和图片格式", error.getMessage()); + } + } + + @Test + void studentAnswerFilesStillRejectExcel() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> service.uploadAnswerFile(student(), 1L, 1L, "device", new MockMultipartFile("file", "答案.xlsx", "application/octet-stream", new byte[]{1}))); + assertEquals("附件仅支持PDF、Word和图片格式", error.getMessage()); + } + @Test void recognizesOnlyCompletePlainOrBracketedQuestionTypeMarkers() { assertEquals(ExamService.SINGLE, ExamService.typeMarker("[单选题]")); @@ -202,6 +226,9 @@ class ExamServiceTest { assertThrows(IllegalArgumentException.class, () -> ExamService.requireState("IN_PROGRESS", "NOT_STARTED", "error")); assertThrows(IllegalArgumentException.class, () -> ExamService.validateDelete("IN_PROGRESS", new Object())); assertThrows(IllegalArgumentException.class, () -> ExamService.validateDelete("ENDED", null)); + assertThrows(IllegalArgumentException.class, () -> ExamService.validateDelete("NOT_STARTED", new Object())); + ExamService.validateDelete("DRAFT", null); + ExamService.validateDelete("ENDED", new Object()); } @Test