@ -52,6 +52,7 @@ public class ExamService {
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 > 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 Set < String > 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 long MAX_ATTACHMENT_SIZE = 20L * 1024 * 1024 ;
private static final DateTimeFormatter TIME = DateTimeFormatter . ofPattern ( "yyyy-MM-dd HH:mm:ss" ) ;
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*(.+)$" ) ;
private static final Pattern OPTION_LINE = Pattern . compile ( "^([A-Ha-h])\\s*[..、:: ]\\s*(.+)$" ) ;
@ -133,7 +134,7 @@ public class ExamService {
public Map < String , Object > uploadQuestionAttachment ( JwtUser teacher , MultipartFile file ) {
public Map < String , Object > uploadQuestionAttachment ( JwtUser teacher , MultipartFile file ) {
requireTeacher ( teacher ) ;
requireTeacher ( teacher ) ;
validate Attachment ( file ) ;
validate Material ( file ) ;
String path = fileUtil . upload ( file ) ;
String path = fileUtil . upload ( file ) ;
return map ( "name" , safeFilename ( file . getOriginalFilename ( ) ) , "url" , "/" + path . replace ( '\\' , '/' ) ) ;
return map ( "name" , safeFilename ( file . getOriginalFilename ( ) ) , "url" , "/" + path . replace ( '\\' , '/' ) ) ;
}
}
@ -310,7 +311,7 @@ public class ExamService {
int order = 1 ;
int order = 1 ;
for ( Map < String , Object > item : paper ) {
for ( Map < String , Object > item : paper ) {
long questionId = number ( item . get ( "questionId" ) ) . longValue ( ) ;
long questionId = number ( item . get ( "questionId" ) ) . longValue ( ) ;
Map < String , Object > question = get Owned Question( teacher , questionId ) ;
Map < String , Object > question = get Usable Question( 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(?,?,?,?,?,?,?,?,?,?,?)" ,
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" ) ) ,
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" ) ) ;
json ( question . get ( "answer" ) ) , question . get ( "analysis" ) , question . get ( "attachmentName" ) , question . get ( "attachmentUrl" ) ) ;
@ -676,6 +677,13 @@ public class ExamService {
return rows . get ( 0 ) ;
return rows . get ( 0 ) ;
}
}
private Map < String , Object > getUsableQuestion ( JwtUser teacher , long id ) {
String schoolId = teacher . getSchoolId ( ) ;
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=? 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 < String , Object > ownedExam ( JwtUser teacher , long id ) {
private Map < String , Object > ownedExam ( JwtUser teacher , long id ) {
requireTeacher ( teacher ) ;
requireTeacher ( teacher ) ;
List < Map < String , Object > > 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=?" ,
List < Map < String , Object > > 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 ) {
static void validateDelete ( String status , Object exportedAt ) {
requireState ( status , "ENDED" , "只有已结束考试可以删除" ) ;
if ( "DRAFT" . equals ( status ) ) return ;
requireState ( status , "ENDED" , "只有未发布或已结束的考试可以删除" ) ;
if ( exportedAt = = null ) throw new IllegalArgumentException ( "请先导出全部试卷后再删除考试" ) ;
if ( exportedAt = = null ) throw new IllegalArgumentException ( "请先导出全部试卷后再删除考试" ) ;
}
}
@ -800,7 +809,7 @@ public class ExamService {
for ( Map < String , Object > item : paper ) {
for ( Map < String , Object > item : paper ) {
long id = number ( item . get ( "questionId" ) ) . longValue ( ) ;
long id = number ( item . get ( "questionId" ) ) . longValue ( ) ;
if ( ! seen . add ( id ) ) throw new IllegalArgumentException ( "试卷中存在重复题目" ) ;
if ( ! seen . add ( id ) ) throw new IllegalArgumentException ( "试卷中存在重复题目" ) ;
get Owned Question( teacher , id ) ;
get Usable Question( teacher , id ) ;
if ( decimal ( item . get ( "score" ) ) . signum ( ) < = 0 ) throw new IllegalArgumentException ( "每题分值必须大于0" ) ;
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格式" ) ;
if ( ! Arrays . asList ( "docx" , "xls" , "xlsx" ) . contains ( extension ) ) throw new IllegalArgumentException ( "仅支持docx、xls或xlsx格式" ) ;
return extension ;
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 < String > 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 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 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 ( "仅学生可以操作" ) ; }
private static void requireStudent ( JwtUser user ) { if ( user = = null ) throw new AccessDeniedException ( "请先登录" ) ; if ( user . getRoleId ( ) ! = 4 ) throw new AccessDeniedException ( "仅学生可以操作" ) ; }