跳到主要内容
基于 Spring Boot 的在线考试系统设计与实现——学生课程实践记录 | 极客日志
Java AI java
基于 Spring Boot 的在线考试系统设计与实现——学生课程实践记录 综述由AI生成 基于 Spring Boot 框架构建在线考试系统,涵盖学生、教师及管理员多角色权限管理。系统支持多种题型(单选、多选、判断、简答),具备定时交卷、防作弊检测及断线续考功能。后端采用 MyBatis-Plus 简化数据库操作,前端通过 Bootstrap 实现响应式界面。开发过程结合 AI 辅助生成基础代码,重点实现了客观题自动判分与主观题人工批改逻辑,完成了从需求分析到系统部署的全流程实践。
古灵精怪 发布于 2026/3/21 更新于 2026/6/6 17 浏览一、需求分析与技术选型
1. 核心需求梳理
明确了系统需要实现的核心功能:
多角色管理:学生(参加考试、查询成绩)、教师(创建题库、组卷、阅卷)、管理员(用户管理、系统设置)
题库管理:支持单选题、多选题、判断题和简答题,可批量导入试题
在线考试:定时交卷、防作弊(禁止切屏)、异常断线后可续考
自动阅卷:客观题自动评分,主观题教师手动评分
成绩统计:按班级、科目统计平均分、及格率等数据
2. 技术选型考量
选择了容易上手且资料丰富的技术栈:
后端:Spring Boot 2.7.x(开发效率高,适合快速迭代)
前端:Bootstrap 5 + jQuery(响应式设计,适配电脑和手机)
数据库:MySQL 8.0(关系型数据库,适合存储结构化考试数据)
开发工具:IntelliJ IDEA + AI 辅助插件
选择 AI 辅助工具是因为它能根据需求生成基础代码框架,对于编码经验不足的学生来说,能节省大量重复劳动,让我专注于业务逻辑实现。
二、环境准备
结合常用的 Windows 环境,完成环境搭建:
1. 下载并安装 IntelliJ IDEA
作为学生,免费且功能完善的IDEA 社区版 是首选。在 JetBrains 官网找到'IntelliJ IDEA',选择'Community'版本下载安装包。
安装时注意两个关键设置:一是勾选'Add launchers dir to the PATH'(添加到环境变量),方便后续通过命令行启动;二是勾选'Create Desktop Shortcut'(创建桌面快捷方式),避免后续找不到启动图标。全程点击'下一步'即可。
2. 安装 AI 辅助插件
打开 IDEA 后,点击顶部菜单栏'File → Settings → Plugins',在右侧搜索框输入相关插件名称,找到带有官方标识的插件,点击'Install'。
3. 登录 AI 辅助工具
重启 IDEA 后,点击面板中的'立即登录',用学生邮箱注册账号,完成手机验证后登录。登录成功后,面板会显示'需求分析→软件设计→工程代码生成'的全流程引导,贴合从想法到落地的完整开发需求。
三、模块设计与编码
在 AI 辅助工具的协助下,按'需求描述→拆解分析→设计→编码'的流程系统化开发。以下是系统的完整开发过程:
1. AI 辅助生成基础模块
AI 工具支持口语化需求描述——不需要专业术语,用日常表达就能精准生成代码。在插件面板的'需求编辑器'中输入需求描述,例如生成在线考试系统基础模块,包含 3 类核心角色及核心实体,实现核心功能如学生注册/登录、在线考试、成绩查询等。
提交需求后,AI 自动解析需求,将描述拆解成多个可执行的核心模块,标注了'必填功能'和'可选优化':
☑ 用户模块:支持学生/教师/管理员三类角色注册登录,基于角色控制权限
☑ 试题模块:教师添加/编辑/删除试题,支持按题型/难度/学科筛选,支持批量导入试题
☑ 试卷模块:教师手动选择试题组卷或按'题型 + 分值 + 难度'随机组卷,设置考试时长与总分
☑ 考试模块:学生进入考试后倒计时提醒,禁止重复提交,异常退出后可续考
☑ 阅卷模块:客观题自动比对答案评分,主观题教师手动打分并写评语
☑ 成绩模块:学生查询个人考试成绩与答题详情,教师查看班级成绩统计
确认需求后,AI 自动进入'接口设计→表结构设计→处理逻辑→生成源码'环节。最终生成的项目结构清晰,核心包与类已完整创建,省去了手动建包、写基础类的麻烦。
com.student.exam
├─ entity
│ ├─ User.java
│ ├─ Question.java
│ ├─ Paper.java
│ ├─ PaperQuestion.java
│ ├─ ExamRecord.java
│ └─ AnswerSheet.java
├─ dto
│ ├─ UserRegisterDTO.java
│ ├─ QuestionAddDTO.java
│ ├─ PaperCreateDTO.java
│ └─ ExamSubmitDTO.java
├─ vo
│ ├─ QuestionVO.java
│ ├─ PaperDetailVO.java
│ └─ ScoreVO.java
├─ mapper
│ ├─ UserMapper.java
│ ├─ QuestionMapper.java
│ ├─ PaperMapper.java
│ └─ ExamRecordMapper.java
├─ service
│ ├─ UserService.java
│ ├─ QuestionService.java
│ ├─ PaperService.java
│ └─ ExamService.java
├─ controller
│ ├─ UserController.java
│ ├─ QuestionController.java
│ ├─ PaperController.java
│ └─ ExamController.java
└─ config
├─ WebConfig.java
└─ SecurityConfig.java
2. 核心代码展示 AI 生成的代码不仅结构规范,还自带参数校验、事务控制和详细注释,仅需根据考试场景补充少量业务逻辑。以下是关键模块的代码示例:
(1)entity 包:核心实体类 Question.java(试题实体,支持多题型)
package com.student.exam.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.student.exam.enums.QuestionTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("question")
public class Question {
@TableId(type = IdType.AUTO)
private Long id;
private String content;
private QuestionTypeEnum type;
private String optionA;
private String optionB;
private String optionC;
private String optionD;
private String correctAnswer;
private Integer score;
private Integer difficulty;
private String subject;
private Long createBy;
private LocalDateTime createTime;
private Integer status;
}
ExamRecord.java(考试记录实体,含考试状态与得分)
package com.student.exam.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.student.exam.enums.ExamStatusEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("exam_record")
public class ExamRecord {
@TableId(type = IdType.AUTO)
private Long id;
private Long studentId;
private Long paperId;
private ExamStatusEnum status;
private LocalDateTime startTime;
private LocalDateTime endTime;
private BigDecimal score;
private Integer totalScore;
private Integer screenChangeCount;
private LocalDateTime lastOperateTime;
}
(2)dto 包:数据传输对象(带参数校验) PaperCreateDTO.java(教师创建试卷的请求 DTO)
package com.student.exam.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Data;
import java.util.List;
@Data
public class PaperCreateDTO {
@NotBlank(message = "试卷名称不能为空")
private String paperName;
@NotBlank(message = "学科不能为空")
private String subject;
@NotNull(message = "考试时长不能为空")
@Positive(message = "考试时长必须大于 0")
private Integer examDuration;
@NotNull(message = "试卷总分不能为空")
@Positive(message = "试卷总分必须大于 0")
private Integer totalScore;
@NotNull(message = "组卷方式不能为空")
private Integer paperType;
private List<Long> questionIds;
private RandomPaperParam randomParam;
private String start_time;
private String end_time;
@Data
public static class RandomPaperParam {
@NotBlank(message = "随机组卷需指定学科")
private String subject;
@NotNull(message = "随机组卷需指定单选题数")
@Positive(message = "单选题数必须大于 0")
private Integer singleCount;
@NotNull(message = "随机组卷需指定单选题分值")
@Positive(message = "单选题分值必须大于 0")
private Integer singleScore;
@NotNull(message = "随机组卷需指定多选题数")
@Positive(message = "多选题数必须大于 0")
private Integer multipleCount;
@NotNull(message = "随机组卷需指定多选题分值")
@Positive(message = "多选题分值必须大于 0")
private Integer multipleScore;
@NotNull(message = "随机组卷需指定判断题数")
@Positive(message = "判断题数必须大于 0")
private Integer judgeCount;
@NotNull(message = "随机组卷需指定判断题分值")
@Positive(message = "判断题分值必须大于 0")
private Integer judgeScore;
@NotNull(message = "随机组卷需指定简答题数")
@Positive(message = "简答题数必须大于 0")
private Integer essayCount;
@NotNull(message = "随机组卷需指定简答题分值")
@Positive(message = "简答题分值必须大于 0")
private Integer essayScore;
@NotBlank(message = "随机组卷需指定难度分布")
private String difficulty;
}
}
(3)service 包:业务逻辑实现(含核心考试流程) ExamServiceImpl.java(考试服务实现类,含自动批改与续考逻辑)
package com.student.exam.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.student.exam.dto.ExamSubmitDTO;
import com.student.exam.entity.*;
import com.student.exam.enums.ExamStatusEnum;
import com.student.exam.enums.QuestionTypeEnum;
import com.student.exam.mapper.*;
import com.student.exam.service.ExamService;
import com.student.exam.vo.ScoreVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class ExamServiceImpl extends ServiceImpl <ExamRecordMapper, ExamRecord> implements ExamService {
private final ExamRecordMapper examRecordMapper;
private final PaperMapper paperMapper;
private final PaperQuestionMapper paperQuestionMapper;
private final QuestionMapper questionMapper;
private final AnswerSheetMapper answerSheetMapper;
private final UserMapper userMapper;
@Override
@Transactional
public PaperDetailVO startExam (Long studentId, Long paperId) {
log.info("学生开始考试:学生 ID={}, 试卷 ID={}" , studentId, paperId);
User student = userMapper.selectById(studentId);
if (student == null || !"student" .equals(student.getRole())) {
throw new RuntimeException ("无效的学生账号,无法开始考试" );
}
Paper paper = paperMapper.selectById(paperId);
if (paper == null ) {
throw new RuntimeException ("试卷不存在" );
}
if (paper.getStatus() != 1 ) {
throw new RuntimeException ("该试卷已禁用,无法进行考试" );
}
LambdaQueryWrapper<ExamRecord> recordWrapper = new LambdaQueryWrapper <>();
recordWrapper.eq(ExamRecord::getStudentId, studentId).eq(ExamRecord::getPaperId, paperId)
.in(ExamRecord::getStatus, ExamStatusEnum.IN_PROGRESS, ExamStatusEnum.SUBMITTED, ExamStatusEnum.GRADED);
if (examRecordMapper.exists(recordWrapper)) {
throw new RuntimeException ("您已参与过该考试,无法重复考试" );
}
LocalDateTime now = LocalDateTime.now();
if (paper.getStartTime() != null && now.isBefore(paper.getStartTime())) {
throw new RuntimeException ("考试尚未开始,开始时间:" + paper.getStartTime());
}
if (paper.getEndTime() != null && now.isAfter(paper.getEndTime())) {
throw new RuntimeException ("考试已截止,无法开始" );
}
ExamRecord examRecord = new ExamRecord ();
examRecord.setStudentId(studentId);
examRecord.setPaperId(paperId);
examRecord.setStatus(ExamStatusEnum.IN_PROGRESS);
examRecord.setStartTime(now);
examRecord.setLastOperateTime(now);
examRecord.setTotalScore(paper.getTotalScore());
examRecord.setScreenChangeCount(0 );
examRecordMapper.insert(examRecord);
List<PaperQuestion> paperQuestions = paperQuestionMapper.selectList(new LambdaQueryWrapper <PaperQuestion>().eq(PaperQuestion::getPaperId, paperId));
List<Long> questionIds = paperQuestions.stream().map(PaperQuestion::getQuestionId).collect(Collectors.toList());
List<Question> questions = questionMapper.selectBatchIds(questionIds);
PaperDetailVO paperDetailVO = new PaperDetailVO ();
paperDetailVO.setPaperId(paperId);
paperDetailVO.setPaperName(paper.getPaperName());
paperDetailVO.setExamDuration(paper.getExamDuration());
paperDetailVO.setTotalScore(paper.getTotalScore());
paperDetailVO.setExamRecordId(examRecord.getId());
List<QuestionVO> questionVOList = questions.stream().map(question -> {
QuestionVO questionVO = new QuestionVO ();
questionVO.setId(question.getId());
questionVO.setContent(question.getContent());
questionVO.setType(question.getType());
questionVO.setScore(question.getScore());
if (QuestionTypeEnum.SINGLE_CHOICE.equals(question.getType()) || QuestionTypeEnum.MULTIPLE_CHOICE.equals(question.getType()) || QuestionTypeEnum.JUDGE.equals(question.getType())) {
questionVO.setOptionA(question.getOptionA());
questionVO.setOptionB(question.getOptionB());
questionVO.setOptionC(question.getOptionC());
questionVO.setOptionD(question.getOptionD());
}
return questionVO;
}).collect(Collectors.toList());
paperDetailVO.setQuestions(questionVOList);
log.info("学生考试初始化完成:考试记录 ID={}" , examRecord.getId());
return paperDetailVO;
}
@Override
@Transactional
public void submitExam (ExamSubmitDTO submitDTO) {
log.info("学生提交考试:考试记录 ID={}, 学生 ID={}" , submitDTO.getExamRecordId(), submitDTO.getStudentId());
ExamRecord examRecord = examRecordMapper.selectById(submitDTO.getExamRecordId());
if (examRecord == null ) {
throw new RuntimeException ("考试记录不存在" );
}
if (!ExamStatusEnum.IN_PROGRESS.equals(examRecord.getStatus())) {
throw new RuntimeException ("考试状态异常,无法提交" );
}
if (!examRecord.getStudentId().equals(submitDTO.getStudentId())) {
throw new RuntimeException ("无权提交他人考试" );
}
Paper paper = paperMapper.selectById(examRecord.getPaperId());
LocalDateTime now = LocalDateTime.now();
if (paper.getEndTime() != null && now.isAfter(paper.getEndTime())) {
examRecord.setStatus(ExamStatusEnum.OVERDUE);
examRecordMapper.updateById(examRecord);
throw new RuntimeException ("考试已逾期,提交失败" );
}
BigDecimal totalScore = BigDecimal.ZERO;
List<AnswerSheet> answerSheets = submitDTO.getAnswers().stream().map(answer -> {
AnswerSheet answerSheet = new AnswerSheet ();
answerSheet.setExamRecordId(submitDTO.getExamRecordId());
answerSheet.setQuestionId(answer.getQuestionId());
answerSheet.setStudentAnswer(answer.getStudentAnswer());
answerSheet.setCreateTime(now);
Question question = questionMapper.selectById(answer.getQuestionId());
if (question == null ) {
throw new RuntimeException ("试题不存在:ID=" + answer.getQuestionId());
}
if (QuestionTypeEnum.SINGLE_CHOICE.equals(question.getType()) || QuestionTypeEnum.JUDGE.equals(question.getType())) {
boolean isCorrect = question.getCorrectAnswer().equals(answer.getStudentAnswer());
answerSheet.setIsCorrect(isCorrect ? 1 : 0 );
answerSheet.setScore(isCorrect ? question.getScore() : 0 );
totalScore = totalScore.add(BigDecimal.valueOf(isCorrect ? question.getScore() : 0 ));
} else if (QuestionTypeEnum.MULTIPLE_CHOICE.equals(question.getType())) {
String correctAnswer = sortAnswer(question.getCorrectAnswer());
String studentAnswer = sortAnswer(answer.getStudentAnswer());
boolean isCorrect = correctAnswer.equals(studentAnswer);
answerSheet.setIsCorrect(isCorrect ? 1 : 0 );
answerSheet.setScore(isCorrect ? question.getScore() : 0 );
totalScore = totalScore.add(BigDecimal.valueOf(isCorrect ? question.getScore() : 0 ));
} else {
answerSheet.setIsCorrect(0 );
answerSheet.setScore(0 );
answerSheet.setTeacherComment("" );
}
return answerSheet;
}).collect(Collectors.toList());
answerSheetMapper.batchInsert(answerSheets);
examRecord.setStatus(ExamStatusEnum.SUBMITTED);
examRecord.setEndTime(now);
examRecord.setScore(totalScore);
examRecordMapper.updateById(examRecord);
log.info("学生考试提交成功:考试记录 ID={}, 客观题得分={}/{}" , submitDTO.getExamRecordId(), totalScore, examRecord.getTotalScore());
}
@Override
@Transactional
public void gradeEssayQuestion (Long teacherId, Long examRecordId, List<GradeDTO> gradeList) {
log.info("教师批改主观题:教师 ID={}, 考试记录 ID={}, 待批改试题数={}" , teacherId, examRecordId, gradeList.size());
User teacher = userMapper.selectById(teacherId);
if (teacher == null || !"teacher" .equals(teacher.getRole())) {
throw new RuntimeException ("无效的教师账号,无法批改作业" );
}
ExamRecord examRecord = examRecordMapper.selectById(examRecordId);
if (examRecord == null ) {
throw new RuntimeException ("考试记录不存在" );
}
if (!ExamStatusEnum.SUBMITTED.equals(examRecord.getStatus())) {
throw new RuntimeException ("考试未提交,无法批改" );
}
BigDecimal essayTotalScore = BigDecimal.ZERO;
for (GradeDTO grade : gradeList) {
Question question = questionMapper.selectById(grade.getQuestionId());
if (question == null ) {
throw new RuntimeException ("试题不存在:ID=" + grade.getQuestionId());
}
if (!QuestionTypeEnum.ESSAY.equals(question.getType())) {
throw new RuntimeException ("试题类型错误,仅支持批改主观题:ID=" + grade.getQuestionId());
}
LambdaQueryWrapper<AnswerSheet> answerWrapper = new LambdaQueryWrapper <>();
answerWrapper.eq(AnswerSheet::getExamRecordId, examRecordId).eq(AnswerSheet::getQuestionId, grade.getQuestionId());
AnswerSheet answerSheet = answerSheetMapper.selectOne(answerWrapper);
if (answerSheet == null ) {
throw new RuntimeException ("该考试无此试题答题记录:试题 ID=" + grade.getQuestionId());
}
if (grade.getScore() < 0 || grade.getScore() > question.getScore()) {
throw new RuntimeException ("得分无效,试题满分:" + question.getScore() + ",提交得分:" + grade.getScore());
}
answerSheet.setScore(grade.getScore());
answerSheet.setTeacherComment(grade.getComment());
answerSheet.setIsCorrect(grade.getScore() >= question.getScore() * 0.6 ? 1 : 0 );
answerSheet.setGradeBy(teacherId);
answerSheet.setGradeTime(LocalDateTime.now());
answerSheetMapper.updateById(answerSheet);
essayTotalScore = essayTotalScore.add(BigDecimal.valueOf(grade.getScore()));
}
BigDecimal finalScore = examRecord.getScore().add(essayTotalScore);
examRecord.setScore(finalScore);
examRecord.setStatus(ExamStatusEnum.GRADED);
examRecordMapper.updateById(examRecord);
log.info("主观题批改完成:考试记录 ID={}, 最终得分={}/{}" , examRecordId, finalScore, examRecord.getTotalScore());
}
@Override
public PaperDetailVO resumeExam (Long studentId, Long examRecordId) {
log.info("学生续考:学生 ID={}, 考试记录 ID={}" , studentId, examRecordId);
ExamRecord examRecord = examRecordMapper.selectById(examRecordId);
if (examRecord == null ) {
throw new RuntimeException ("考试记录不存在" );
}
if (!ExamStatusEnum.IN_PROGRESS.equals(examRecord.getStatus())) {
throw new RuntimeException ("考试状态异常,无法续考" );
}
if (!examRecord.getStudentId().equals(studentId)) {
throw new RuntimeException ("无权续考他人考试" );
}
Paper paper = paperMapper.selectById(examRecord.getPaperId());
LocalDateTime now = LocalDateTime.now();
if (paper.getEndTime() != null && now.isAfter(paper.getEndTime())) {
examRecord.setStatus(ExamStatusEnum.OVERDUE);
examRecordMapper.updateById(examRecord);
throw new RuntimeException ("考试已截止,无法续考" );
}
long usedMinutes = ChronoUnit.MINUTES.between(examRecord.getStartTime(), now);
int remainingMinutes = paper.getExamDuration() - (int ) usedMinutes;
if (remainingMinutes <= 0 ) {
examRecord.setStatus(ExamStatusEnum.OVERDUE);
examRecordMapper.updateById(examRecord);
throw new RuntimeException ("考试时间已用完,无法续考" );
}
examRecord.setLastOperateTime(now);
examRecordMapper.updateById(examRecord);
PaperDetailVO paperDetailVO = new PaperDetailVO ();
paperDetailVO.setPaperId(paper.getId());
paperDetailVO.setPaperName(paper.getPaperName());
paperDetailVO.setExamDuration(remainingMinutes);
paperDetailVO.setTotalScore(paper.getTotalScore());
paperDetailVO.setExamRecordId(examRecordId);
List<AnswerSheet> answerSheets = answerSheetMapper.selectList(new LambdaQueryWrapper <AnswerSheet>().eq(AnswerSheet::getExamRecordId, examRecordId));
paperDetailVO.setAnsweredQuestions(answerSheets);
List<PaperQuestion> paperQuestions = paperQuestionMapper.selectList(new LambdaQueryWrapper <PaperQuestion>().eq(PaperQuestion::getPaperId, paper.getId()));
List<Long> questionIds = paperQuestions.stream().map(PaperQuestion::getQuestionId).collect(Collectors.toList());
List<Question> questions = questionMapper.selectBatchIds(questionIds);
List<QuestionVO> questionVOList = questions.stream().map(question -> {
QuestionVO questionVO = new QuestionVO ();
questionVO.setId(question.getId());
questionVO.setContent(question.getContent());
questionVO.setType(question.getType());
questionVO.setScore(question.getScore());
if (QuestionTypeEnum.SINGLE_CHOICE.equals(question.getType()) || QuestionTypeEnum.MULTIPLE_CHOICE.equals(question.getType()) || QuestionTypeEnum.JUDGE.equals(question.getType())) {
questionVO.setOptionA(question.getOptionA());
questionVO.setOptionB(question.getOptionB());
questionVO.setOptionC(question.getOptionC());
questionVO.setOptionD(question.getOptionD());
}
return questionVO;
}).collect(Collectors.toList());
paperDetailVO.setQuestions(questionVOList);
log.info("学生续考成功:考试记录 ID={}, 剩余时间={}分钟" , examRecordId, remainingMinutes);
return paperDetailVO;
}
private String sortAnswer (String answer) {
if (answer == null || answer.isEmpty()) {
return "" ;
}
return java.util.Arrays.stream(answer.split("," )).sorted().collect(Collectors.joining("," ));
}
}
四、网页展示 为贴合学生在教室、实验室使用电脑考试的场景,前端采用 Bootstrap 实现响应式布局,界面简洁无冗余,突出'考试'核心功能。以下是核心页面的设计与功能:
1. 学生端 - 考试列表页
布局 :顶部是'我的考试'标题与搜索栏(按试卷名称/学科搜索),中间是考试列表(卡片式展示),底部是分页控件;
核心功能 :卡片显示试卷名称、学科、考试时长、总分、开始/截止时间,状态标签区分'未开始''可参加''已截止';点击'开始考试'按钮,若未到开始时间则提示'考试未开始',若已截止则提示'无法参加',否则跳转至考试页面;
细节设计 :'可参加'状态的卡片添加浅蓝色边框,突出可操作项;考试开始前 10 分钟,卡片右上角显示'即将开始'橙色标签,提醒学生及时准备。
<!DOCTYPE html >
<html lang ="zh-CN" >
<head >
<meta charset ="UTF-8" >
<meta name ="viewport" content ="width=device-width, initial-scale=1.0" >
<title > 我的考试 - 在线考试系统</title >
<link href ="https://cdn.jsdelivr.net/npm/[email protected] /dist/css/bootstrap.min.css" rel ="stylesheet" >
<link href ="https://cdn.jsdelivr.net/npm/[email protected] /css/font-awesome.min.css" rel ="stylesheet" >
<style >
.exam-card { border : 1px solid #e9ecef ; border-radius : 8px ; padding : 20px ; margin-bottom : 20px ; transition : all 0.3s ; }
.exam-card :hover { box-shadow : 0 4px 12px rgba (0 ,0 ,0 ,0.05 ); transform : translateY (-2px ); }
.exam-card .available { border-left : 4px solid #409eff ; }
.status-badge { padding : 4px 8px ; border-radius : 4px ; font-size : 0.8rem ; }
.status-not-start { background-color : #e6f7ff ; color : #1890ff ; }
.status-available { background-color : #f0f9eb ; color : #52c41a ; }
.status-overdue { background-color : #fff2e8 ; color : #fa8c16 ; }
.countdown-tag { position : absolute; top : 15px ; right : 15px ; background-color : #fff3cd ; color : #856404 ; padding : 2px 8px ; border-radius : 4px ; font-size : 0.75rem ; }
</style >
</head >
<body >
<nav class ="navbar navbar-expand-lg navbar-light bg-white border-bottom" >
<div class ="container" >
<a class ="navbar-brand text-primary" href ="/student/index" > <i class ="fa fa-pencil-square-o mr-2" > </i > 在线考试系统</a >
<button class ="navbar-toggler" type ="button" data-bs-toggle ="collapse" data-bs-target ="#navbarNav" > <span class ="navbar-toggler-icon" > </span > </button >
<div class ="collapse navbar-collapse" id ="navbarNav" >
<ul class ="navbar-nav me-auto" >
<li class ="nav-item" > <a class ="nav-link active" href ="/student/exam-list" > 我的考试</a > </li >
<li class ="nav-item" > <a class ="nav-link" href ="/student/score-list" > 我的成绩</a > </li >
<li class ="nav-item" > <a class ="nav-link" href ="/student/profile" > 个人中心</a > </li >
</ul >
<div class ="dropdown" >
<button class ="btn btn-outline-primary dropdown-toggle" type ="button" data-bs-toggle ="dropdown" > <i class ="fa fa-user mr-1" > </i > 学号:2024001(张三)</button >
<ul class ="dropdown-menu dropdown-menu-end" >
<li > <a class ="dropdown-item" href ="/student/profile" > 个人信息</a > </li >
<li > <a class ="dropdown-item" href ="/student/change-pwd" > 修改密码</a > </li >
<li > <hr class ="dropdown-divider" > </li >
<li > <a class ="dropdown-item" href ="/login" > 退出登录</a > </li >
</ul >
</div >
</div >
</div >
</nav >
<div class ="container mt-4" >
<div class ="d-flex justify-content-between align-items-center mb-4" >
<h4 > 我的考试</h4 >
<div class ="search-box" >
<div class ="input-group" >
<input type ="text" class ="form-control" placeholder ="搜索试卷名称/学科" id ="searchInput" >
<button class ="btn btn-primary" type ="button" id ="searchBtn" > <i class ="fa fa-search" > </i > </button >
</div >
</div >
</div >
<div class ="row" id ="examList" >
<div class ="col-md-6 col-lg-4" >
<div class ="exam-card available position-relative" >
<span class ="status-badge status-available" > 可参加</span >
<h5 class ="mt-2 mb-1" > Java 编程基础期末测试</h5 >
<p class ="text-muted mb-1" > <i class ="fa fa-book mr-1" > </i > 学科:Java 编程</p >
<p class ="text-muted mb-1" > <i class ="fa fa-clock-o mr-1" > </i > 考试时长:90 分钟</p >
<p class ="text-muted mb-1" > <i class ="fa fa-score mr-1" > </i > 总分:100 分</p >
<p class ="text-muted mb-3" > <i class ="fa fa-calendar mr-1" > </i > 时间:2024-06-20 09:00 - 2024-06-20 11:00</p >
<button class ="btn btn-primary w-100 start-exam-btn" data-paper-id ="1" > <i class ="fa fa-play-circle mr-1" > </i > 开始考试</button >
</div >
</div >
<div class ="col-md-6 col-lg-4" >
<div class ="exam-card position-relative" >
<span class ="countdown-tag" > 即将开始(10 分钟后)</span >
<span class ="status-badge status-not-start" > 未开始</span >
<h5 class ="mt-2 mb-1" > 计算机网络期中测试</h5 >
<p class ="text-muted mb-1" > <i class ="fa fa-book mr-1" > </i > 学科:计算机基础</p >
<p class ="text-muted mb-1" > <i class ="fa fa-clock-o mr-1" > </i > 考试时长:60 分钟</p >
<p class ="text-muted mb-1" > <i class ="fa fa-score mr-1" > </i > 总分:80 分</p >
<p class ="text-muted mb-3" > <i class ="fa fa-calendar mr-1" > </i > 时间:2024-06-20 14:30 - 2024-06-20 15:30</p >
<button class ="btn btn-secondary w-100" disabled > <i class ="fa fa-lock mr-1" > </i > 未到开始时间</button >
</div >
</div >
<div class ="col-md-6 col-lg-4" >
<div class ="exam-card position-relative" >
<span class ="status-badge status-overdue" > 已截止</span >
<h5 class ="mt-2 mb-1" > 高等数学(上)单元测试</h5 >
<p class ="text-muted mb-1" > <i class ="fa fa-book mr-1" > </i > 学科:高等数学</p >
<p class ="text-muted mb-1" > <i class ="fa fa-clock-o mr-1" > </i > 考试时长:120 分钟</p >
<p class ="text-muted mb-1" > <i class ="fa fa-score mr-1" > </i > 总分:150 分</p >
<p class ="text-muted mb-3" > <i class ="fa fa-calendar mr-1" > </i > 时间:2024-06-15 09:00 - 2024-06-15 11:00</p >
<button class ="btn btn-secondary w-100" disabled > <i class ="fa fa-times-circle mr-1" > </i > 已截止</button >
</div >
</div >
</div >
<nav aria-label ="Page navigation" class ="mt-5" >
<ul class ="pagination justify-content-center" >
<li class ="page-item disabled" > <a class ="page-link" href ="#" tabindex ="-1" > 上一页</a > </li >
<li class ="page-item active" > <a class ="page-link" href ="#" > 1</a > </li >
<li class ="page-item" > <a class ="page-link" href ="#" > 2</a > </li >
<li class ="page-item" > <a class ="page-link" href ="#" > 3</a > </li >
<li class ="page-item" > <a class ="page-link" href ="#" > 下一页</a > </li >
</ul >
</nav >
</div >
<script src ="https://cdn.jsdelivr.net/npm/[email protected] /dist/jquery.min.js" > </script >
<script src ="https://cdn.jsdelivr.net/npm/[email protected] /dist/js/bootstrap.bundle.min.js" > </script >
<script >
$(".start-exam-btn" ).click (function ( ){
const paperId = $(this ).data ("paper-id" );
$.ajax ({
url :"/api/student/exam/start" ,
type :"POST" ,
data :JSON .stringify ({ studentId :1 ,
contentType :"application/json" ,
success :function (res ){
if (res.code ===200 ){
window .location .href =`/student/exam/do?examRecordId=${res.data.examRecordId} ` ;
}else {
alert (res.msg );
}},
error :function ( ){alert ("请求失败,请重试" );}});
});
$("#searchBtn" ).click (function ( ){
const keyword = $("#searchInput" ).val ().trim ();
(keyword === ){ ( ); ;}
( );
});
</script >
</body >
</html >
2. 学生端 - 考试答题页
布局 :顶部是考试信息栏(试卷名称、剩余时间、切屏次数提醒),左侧是试题导航栏(按题号排列,区分'未答''已答'),中间是试题答题区(根据题型显示选项/文本框),底部是'上一题''下一题''提交考试'按钮;
核心功能 :剩余时间实时倒计时,时间不足 10 分钟时变红提醒;学生选择答案后自动标记为'已答',切换试题时自动保存答案;切屏超过 5 次时,弹出'切屏次数过多,将影响成绩'提示;点击'提交考试'时弹出确认框,确认后提交所有答案并跳转至'考试提交成功'页面;
细节设计 :试题导航栏的题号按钮用不同颜色区分状态(未答=灰色、已答=蓝色、当前题=绿色);主观题(简答)提供富文本编辑器,支持换行、列表等格式;自动保存答案(每 30 秒保存一次),避免浏览器崩溃导致答案丢失。
<!DOCTYPE html >
<html lang ="zh-CN" >
<head >
<meta charset ="UTF-8" >
<meta name ="viewport" content ="width=device-width, initial-scale=1.0" >
<title > 考试答题 - 在线考试系统</title >
<link href ="https://cdn.jsdelivr.net/npm/[email protected] /dist/css/bootstrap.min.css" rel ="stylesheet" >
<link href ="https://cdn.jsdelivr.net/npm/[email protected] /css/font-awesome.min.css" rel ="stylesheet" >
<style >
.exam-header { background-color : #fff ; border-bottom : 1px solid #e9ecef ; padding : 10px 20px ; margin-bottom : 20px ; }
.countdown { font-size : 1.2rem ; font-weight : bold; color : #dc3545 ; }
.question-nav { background-color : #fff ; border : 1px solid #e9ecef ; border-radius : 8px ; padding : 15px ; height : calc (100vh - 180px ); overflow-y : auto; }
.question-btn { width : 35px ; height : 35px ; margin : 5px ; border-radius : 50% ; display : flex; align-items : center; justify-content : center; border : 1px solid #e9ecef ; background-color : #f8f9fa ; cursor : pointer; transition : all 0.2s ; }
.question-btn .answered { background-color : #e6f7ff ; border-color : #91d5ff ; color : #1890ff ; }
.question-btn .current { background-color : #f0f9eb ; border-color : #73d13d ; color : #52c41a ; font-weight : bold; }
.question-btn .unanswered { background-color : #f8f9fa ; border-color : #e9ecef ; color : #495057 ; }
.question-content { background-color : #fff ; border : 1px solid #e9ecef ; border-radius : 8px ; padding : 20px ; min-height : calc (100vh - 180px ); }
.option-item { margin-bottom : 10px ; padding : 10px ; border : 1px solid #e9ecef ; border-radius : 4px ; cursor : pointer; transition : all 0.2s ; }
.option-item :hover { background-color : #f8f9fa ; }
.option-item .selected { background-color : #e6f7ff ; border-color : #91d5ff ; }
.essay-input { width : 100% ; min-height : 200px ; border : 1px solid #e9ecef ; border-radius : 4px ; padding : 10px ; resize : vertical; }
.screen-change-warning { position : fixed; top : 20px ; right : 20px ; z-index : 9999 ; display : none; }
</style >
</head >
<body >
<div class ="exam-header d-flex justify-content-between align-items-center" >
<div > <h5 class ="mb-0" > <i class ="fa fa-file-text-o mr-2" > </i > 试卷名称:Java 编程基础期末测试</h5 > </div >
<div class ="d-flex align-items-center gap-4" >
<div > <span class ="text-muted" > 切屏次数:</span > <span id ="screenChangeCount" > 3</span > <span class ="text-danger" id ="screenWarn" style ="display: none;" > (次数过多,请注意!)</span > </div >
<div class ="countdown" > <i class ="fa fa-clock-o mr-1" > </i > 剩余时间:<span id ="remainingTime" > 01:25:30</span > </div >
</div >
</div >
<div class ="alert alert-warning alert-dismissible fade show screen-change-warning" id ="screenChangeAlert" >
<strong > 警告!</strong > 检测到切屏行为,切屏次数过多将影响考试成绩。
<button type ="button" class ="btn-close" data-bs-dismiss ="alert" aria-label ="Close" > </button >
</div >
<div class ="container" >
<div class ="row" >
<div class ="col-md-2" >
<div class ="question-nav" >
<h6 class ="mb-3" > 试题导航</h6 >
<div class ="d-flex flex-wrap" id ="questionNav" >
<div class ="question-btn current" data-question-id ="1" > 1</div >
<div class ="question-btn unanswered" data-question-id ="2" > 2</div >
<div class ="question-btn answered" data-question-id ="3" > 3</div >
<div class ="question-btn unanswered" data-question-id ="4" > 4</div >
<div class ="question-btn unanswered" data-question-id ="5" > 5</div >
<div class ="question-btn unanswered" data-question-id ="6" > 6</div >
<div class ="question-btn unanswered" data-question-id ="7" > 7</div >
<div class ="question-btn unanswered" data-question-id ="8" > 8</div >
<div class ="question-btn unanswered" data-question-id ="9" > 9</div >
<div class ="question-btn unanswered" data-question-id ="10" > 10</div >
</div >
<div class ="mt-4" >
<div class ="d-flex align-items-center mb-2" >
<div class ="question-btn current mr-2" > </div >
<span class ="text-sm" > 当前题</span >
</div >
<div class ="d-flex align-items-center mb-2" >
<div class ="question-btn answered mr-2" > </div >
<span class ="text-sm" > 已答题</span >
</div >
<div class ="d-flex align-items-center" >
<div class ="question-btn unanswered mr-2" > </div >
<span class ="text-sm" > 未答题</span >
</div >
</div >
</div >
</div >
<div class ="col-md-10" >
<div class ="question-content" id ="questionContent" >
<div class ="question-item" data-question-id ="1" >
<div class ="d-flex align-items-center mb-3" >
<span class ="badge bg-primary mr-2" > 单选题(2 分)</span >
<h5 class ="mb-0" > 1. 下列关于 Java 中'继承'的描述,正确的是?</h5 >
</div >
<div class ="options" >
<div class ="option-item" data-option ="A" >
<input type ="radio" name ="question1" id ="q1A" value ="A" checked >
<label for ="q1A" > A. Java 支持多继承</label >
</div >
<div class ="option-item" data-option ="B" >
<input type ="radio" name ="question1" id ="q1B" value ="B" >
<label for ="q1B" > B. 子类可以继承父类的所有成员</label >
</div >
<div class ="option-item selected" data-option ="C" >
<input type ="radio" name ="question1" id ="q1C" value ="C" >
<label for ="q1C" > C. 子类可以重写父类的方法</label >
</div >
<div class ="option-item" data-option ="D" >
<input type ="radio" name ="question1" id ="q1D" value ="D" >
<label for ="q1D" > D. 父类可以访问子类的成员</label >
</div >
</div >
</div >
</div >
<div class ="d-flex justify-content-between mt-4" >
<button class ="btn btn-secondary" id ="prevBtn" > <i class ="fa fa-angle-left mr-1" > </i > 上一题</button >
<button class ="btn btn-secondary" id ="nextBtn" > 下一题<i class ="fa fa-angle-right ml-1" > </i > </button >
<button class ="btn btn-danger" id ="submitBtn" > <i class ="fa fa-paper-plane mr-1" > </i > 提交考试</button >
</div >
</div >
</div >
</div >
<div class ="modal fade" id ="submitConfirmModal" tabindex ="-1" aria-labelledby ="submitConfirmModalLabel" aria-hidden ="true" >
<div class ="modal-dialog modal-dialog-centered" >
<div class ="modal-content" >
<div class ="modal-header" >
<h5 class ="modal-title" id ="submitConfirmModalLabel" > 确认提交考试?</h5 >
<button type ="button" class ="btn-close" data-bs-dismiss ="modal" aria-label ="Close" > </button >
</div >
<div class ="modal-body" >
<p > 提交后将无法修改答案,请确认是否完成所有试题?</p >
<p class ="text-danger" > 剩余时间:<span id ="modalRemainingTime" > 01:25:30</span > </p >
</div >
<div class ="modal-footer" >
<button type ="button" class ="btn btn-secondary" data-bs-dismiss ="modal" > 取消</button >
<button type ="button" class ="btn btn-danger" id ="confirmSubmitBtn" > 确认提交</button >
</div >
</div >
</div >
</div >
<script src ="https://cdn.jsdelivr.net/npm/[email protected] /dist/jquery.min.js" > </script >
<script src ="https://cdn.jsdelivr.net/npm/[email protected] /dist/js/bootstrap.bundle.min.js" > </script >
<script >
function startCountdown ( ){
let totalSeconds = 5130 ;
const countdownElement = document .getElementById ('remainingTime' );
const modalCountdownElement = document .getElementById ('modalRemainingTime' );
const interval = setInterval (()=> {
totalSeconds--;
if (totalSeconds <=0 ){
clearInterval (interval);
autoSubmit ();
return ;
}
const hours = Math .floor (totalSeconds /3600 );
const minutes = Math .floor ((totalSeconds %3600 )/60 );
const seconds = totalSeconds %60 ;
const timeStr =`${hours.toString().padStart(2 ,'0' )} :${minutes.toString().padStart(2 ,'0' )} :${seconds.toString().padStart(2 ,'0' )} ` ;
countdownElement.textContent = timeStr;
modalCountdownElement.textContent = timeStr;
(totalSeconds <= ){
countdownElement. . ( );
modalCountdownElement. . ( );
}
}, );
interval;
}
screenChangeCount = ;
lastVisibilityState = . ;
. ( , {
( . === && lastVisibilityState === ){
screenChangeCount++;
. ( ). = screenChangeCount;
alertElement = . ( );
alertElement. . = ;
( {
alertElement. . = ;
}, );
(screenChangeCount >= ){
. ( ). . = ;
}
}
lastVisibilityState = . ;
});
. ( ). ( , {
( );
});
. ( ). ( , {
( );
();
});
. ( ). ( , {
modal = bootstrap. ( . ( ));
modal. ();
});
. ( ). ( , {
modal = bootstrap. . ( . ( ));
modal. ();
();
});
( ){
currentQuestionId = ;
selectedOption = . ( ). ;
. ( );
}
(saveCurrentAnswer, );
( ){
$. ({
: ,
: ,
: . ({ : ,
: ,
: ( ){
(res. === ){
. . = ;
} {
(res. );
}},
: ( ){ ( );}});
}
( ){
( );
();
}
. = ( ){
();
();
};
( ){
. ( ). ( {
btn. ( , {
questionId = btn. ( );
();
( );
. ( ). ( {
b. . ( );
});
btn. . ( );
});
});
}
</script >
</body >
</html >
3. 教师端 - 试题管理页
布局 :顶部是'试题管理'标题、搜索栏(按题干/学科/题型搜索)与'添加试题''批量导入'按钮,中间是试题列表(表格形式),底部是分页控件;
核心功能 :表格显示试题 ID、题干、题型、学科、难度、分值、状态、创建时间,操作列有'编辑''禁用/启用''删除'按钮;点击'添加试题'跳转至试题添加页面(根据题型动态显示选项输入框:单选/多选/判断显示 A-D 选项,简答隐藏选项);点击'批量导入'弹出文件上传框,支持 Excel 批量导入试题(含模板下载);
细节设计 :状态列用标签区分'启用'(绿色)/'禁用'(灰色);支持按题型/难度/状态筛选试题(顶部筛选下拉框);表格支持'全选'批量操作(批量启用/禁用/删除),提升教师管理效率。
<!DOCTYPE html >
<html lang ="zh-CN" >
<head >
<meta charset ="UTF-8" >
<meta name ="viewport" content ="width=device-width, initial-scale=1.0" >
<title > 试题管理 - 在线考试系统</title >
<link href ="https://cdn.jsdelivr.net/npm/[email protected] /dist/css/bootstrap.min.css" rel ="stylesheet" >
<link href ="https://cdn.jsdelivr.net/npm/[email protected] /css/font-awesome.min.css" rel ="stylesheet" >
<style >
.navbar { box-shadow : 0 2px 5px rgba (0 ,0 ,0 ,0.05 ); }
.action-btn { padding : 0.25rem 0.5rem ; font-size : 0.875rem ; margin : 0 2px ; }
.status-badge { padding : 0.25rem 0.5rem ; border-radius : 4px ; font-size : 0.875rem ; }
.status-enabled { background-color : #d1e7dd ; color : #0f5132 ; }
.status-disabled { background-color : #e9ecef ; color : #495057 ; }
.filter-bar { background-color : #f8f9fa ; border-radius : 8px ; padding : 15px ; margin-bottom : 20px ; }
.question-content { max-width : 400px ; white-space : nowrap; overflow : hidden; text-overflow : ellipsis; }
</style >
</head >
<body >
<nav class ="navbar navbar-expand-lg navbar-light bg-white" >
<div class ="container" >
<a class ="navbar-brand text-primary" href ="/teacher/index" > <i class ="fa fa-pencil-square-o mr-2" > </i > 在线考试系统</a >
<button class ="navbar-toggler" type ="button" data-bs-toggle ="collapse" data-bs-target ="#navbarNav" > <span class ="navbar-toggler-icon" > </span > </button >
<div class ="collapse navbar-collapse" id ="navbarNav" >
<ul class ="navbar-nav me-auto" >
<li class ="nav-item" > <a class ="nav-link" href ="/teacher/paper-manage" > 试卷管理</a > </li >
<li class ="nav-item" > <a class ="nav-link active" href ="/teacher/question-manage" > 试题管理</a > </li >
<li class ="nav-item" > <a class ="nav-link" href ="/teacher/exam-manage" > 考试管理</a > </li >
<li class ="nav-item" > <a class ="nav-link" href ="/teacher/grade-manage" > 批改管理</a > </li >
</ul >
<div class ="dropdown" >
<button class ="btn btn-outline-primary dropdown-toggle" type ="button" data-bs-toggle ="dropdown" > <i class ="fa fa-user mr-1" > </i > 工号:T202401(李老师)</button >
<ul class ="dropdown-menu dropdown-menu-end" >
<li > <a class ="dropdown-item" href ="/teacher/profile" > 个人中心</a > </li >
<li > <a class ="dropdown-item" href ="/teacher/change-pwd" > 修改密码</a > </li >
<li > <hr class ="dropdown-divider" > </li >
<li > <a class ="dropdown-item" href ="/login" > 退出登录</a > </li >
</ul >
</div >
</div >
</div >
</nav >
<div class ="container mt-4" >
<div class ="d-flex justify-content-between align-items-center mb-4" >
<h4 > 试题管理</h4 >
<div >
<button class ="btn btn-outline-primary me-2" id ="batchImportBtn" > <i class ="fa fa-upload mr-1" > </i > 批量导入</button >
<button class ="btn btn-primary" id ="addQuestionBtn" > <i class ="fa fa-plus mr-1" > </i > 添加试题</button >
</div >
</div >
<div class ="filter-bar" >
<div class ="row g-3" >
<div class ="col-md-3" >
<input type ="text" class ="form-control" placeholder ="搜索题干/学科" id ="searchInput" >
</div >
<div class ="col-md-2" >
<select class ="form-select" id ="typeFilter" >
<option value ="" > 全部题型</option >
<option value ="0" > 单选题</option >
<option value ="1" > 多选题</option >
<option value ="2" > 判断题</option >
<option value ="3" > 简答题</option >
</select >
</div >
<div class ="col-md-2" >
<select class ="form-select" id ="difficultyFilter" >
<option value ="" > 全部难度</option >
<option value ="0" > 简单</option >
<option value ="1" > 中等</option >
<option value ="2" > 困难</option >
</select >
</div >
<div class ="col-md-2" >
<select class ="form-select" id ="statusFilter" >
<option value ="" > 全部状态</option >
<option value ="1" > 启用</option >
<option value ="0" > 禁用</option >
</select >
</div >
<div class ="col-md-3" >
<div class ="d-flex gap-2" >
<button class ="btn btn-primary flex-grow-1" id ="searchBtn" > <i class ="fa fa-search mr-1" > </i > 搜索</button >
<button class ="btn btn-outline-secondary" id ="resetBtn" > <i class ="fa fa-refresh mr-1" > </i > 重置</button >
</div >
</div >
</div >
</div >
<div class ="card" >
<div class ="card-body" >
<div class ="table-responsive" >
<table class ="table table-hover" >
<thead >
<tr >
<th style ="width: 50px;" > <input type ="checkbox" id ="selectAll" > </th >
<th > 试题 ID</th >
<th > 题干</th >
<th > 题型</th >
<th > 学科</th >
<th > 难度</th >
<th > 分值</th >
<th > 状态</th >
<th > 创建时间</th >
<th > 操作</th >
</tr >
</thead >
<tbody >
<tr >
<td > <input type ="checkbox" class ="question-check" data-question-id ="1" > </td >
<td > 1</td >
<td class ="question-content" > 下列关于 Java 中'继承'的描述,正确的是?</td >
<td > 单选题</td >
<td > Java 编程</td >
<td > 中等</td >
<td > 2</td >
<td > <span class ="status-badge status-enabled" > 启用</span > </td >
<td > 2024-06-01</td >
<td > <button class ="btn btn-outline-primary action-btn edit-btn" data-question-id ="1" > 编辑</button > <button class ="btn btn-outline-secondary action-btn disable-btn" data-question-id ="1" > 禁用</button > </td >
</tr >
<tr >
<td > <input type ="checkbox" class ="question-check" data-question-id ="2" > </td >
<td > 2</td >
<td class ="question-content" > 下列属于 Java 集合框架中的接口的是?(多选)</td >
<td > 多选题</td >
<td > Java 编程</td >
<td > 困难</td >
<td > 4</td >
<td > <span class ="status-badge status-enabled" > 启用</span > </td >
<td > 2024-06-02</td >
<td > <button class ="btn btn-outline-primary action-btn edit-btn" data-question-id ="2" > 编辑</button > <button class ="btn btn-outline-secondary action-btn disable-btn" data-question-id ="2" > 禁用</button > </td >
</tr >
<tr >
<td > <input type ="checkbox" class ="question-check" data-question-id ="3" > </td >
<td > 3</td >
<td class ="question-content" > Java 中的'=='既可以比较基本数据类型,也可以比较引用数据类型的地址。(判断)</td >
<td > 判断题</td >
<td > Java 编程</td >
<td > 简单</td >
<td > 1</td >
<td > <span class ="status-badge status-disabled" > 禁用</span > </td >
<td > 2024-06-03</td >
<td > <button class ="btn btn-outline-primary action-btn edit-btn" data-question-id ="3" > 编辑</button > <button class ="btn btn-outline-success action-btn enable-btn" data-question-id ="3" > 启用</button > </td >
</tr >
<tr >
<td > <input type ="checkbox" class ="question-check" data-question-id ="4" > </td >
<td > 4</td >
<td class ="question-content" > 请简述 Java 中 synchronized 关键字的作用及使用场景。</td >
<td > 简答题</td >
<td > Java 编程</td >
<td > 困难</td >
<td > 10</td >
<td > <span class ="status-badge status-enabled" > 启用</span > </td >
<td > 2024-06-05</td >
<td > <button class ="btn btn-outline-primary action-btn edit-btn" data-question-id ="4" > 编辑</button > <button class ="btn btn-outline-secondary action-btn disable-btn" data-question-id ="4" > 禁用</button > </td >
</tr >
</tbody >
</table >
</div >
</div >
</div >
<div class ="d-flex justify-content-between align-items-center mt-3" id ="batchActionBar" style ="display: none;" >
<div > <span > 已选中 <span id ="selectedCount" > 0</span > 道试题</span > </div >
<div >
<button class ="btn btn-outline-secondary me-2" id ="batchEnableBtn" > 批量启用</button >
<button class ="btn btn-outline-secondary me-2" id ="batchDisableBtn" > 批量禁用</button >
<button class ="btn btn-danger" id ="batchDeleteBtn" > 批量删除</button >
</div >
</div >
<nav aria-label ="Page navigation" class ="mt-4" >
<ul class ="pagination justify-content-center" >
<li class ="page-item disabled" > <a class ="page-link" href ="#" tabindex ="-1" > 上一页</a > </li >
<li class ="page-item active" > <a class ="page-link" href ="#" > 1</a > </li >
<li class ="page-item" > <a class ="page-link" href ="#" > 2</a > </li >
<li class ="page-item" > <a class ="page-link" href ="#" > 3</a > </li >
<li class ="page-item" > <a class ="page-link" href ="#" > 下一页</a > </li >
</ul >
</nav >
</div >
<div class ="modal fade" id ="batchImportModal" tabindex ="-1" aria-labelledby ="batchImportModalLabel" aria-hidden ="true" >
<div class ="modal-dialog" >
<div class ="modal-content" >
<div class ="modal-header" >
<h5 class ="modal-title" id ="batchImportModalLabel" > 批量导入试题</h5 >
<button type ="button" class ="btn-close" data-bs-dismiss ="modal" aria-label ="Close" > </button >
</div >
<div class ="modal-body" >
<div class ="mb-3" >
<label class ="form-label" > 下载模板</label >
<a href ="/static/template/question-template.xlsx" class ="d-block mt-1 text-primary" > <i class ="fa fa-download mr-1" > </i > 试题导入模板.xlsx(含使用说明)</a >
</div >
<div class ="mb-3" >
<label for ="fileUpload" class ="form-label" > 选择 Excel 文件</label >
<input class ="form-control" type ="file" id ="fileUpload" accept =".xlsx,.xls" >
<div class ="form-text mt-1" > 支持.xlsx/.xls 格式,单次最多导入 100 道试题</div >
</div >
</div >
<div class ="modal-footer" >
<button type ="button" class ="btn btn-secondary" data-bs-dismiss ="modal" > 取消</button >
<button type ="button" class ="btn btn-primary" id ="confirmImportBtn" > 开始导入</button >
</div >
</div >
</div >
</div >
<script src ="https://cdn.jsdelivr.net/npm/[email protected] /dist/jquery.min.js" > </script >
<script src ="https://cdn.jsdelivr.net/npm/[email protected] /dist/js/bootstrap.bundle.min.js" > </script >
<script >
$('#selectAll' ).click (function ( ){
const isChecked = $(this ).prop ('checked' );
$('.question-check' ).prop ('checked' , isChecked);
updateBatchActionBar ();
});
$('.question-check' ).click (function ( ){
updateBatchActionBar ();
});
function updateBatchActionBar ( ){
const selectedCount = $('.question-check:checked' ).length ;
$('#selectedCount' ).text (selectedCount);
if (selectedCount >0 ){
$('#batchActionBar' ).show ();
}else {
$('#batchActionBar' ).hide ();
$('#selectAll' ).prop ('checked' ,false );
}
}
$('#addQuestionBtn' ).click (function ( ){
window .location .href ='/teacher/question-add' ;
});
$('.edit-btn' ).click ( ( ){
questionId = $( ). ( );
. . = ;
});
$( ). ( ( ){
questionId = $( ). ( );
$tr = $( ). ( );
$tr. ( ). ( ). ( ). ( );
$( ). ( ). ( ). ( );
( );
});
$( ). ( ( ){
questionId = $( ). ( );
$tr = $( ). ( );
( ( )){
$tr. ( ). ( ). ( ). ( );
$( ). ( ). ( ). ( );
( );
}
});
$( ). ( ( ){
selectedIds = ();
( );
();
});
$( ). ( ( ){
selectedIds = ();
( ( )){
( );
();
}
});
$( ). ( ( ){
selectedIds = ();
( ( )){
( );
();
}
});
( ){
ids =[];
$( ). ( ( ){
ids. ($( ). ( ));
});
ids;
}
$( ). ( ( ){
modal = bootstrap. ($( ));
modal. ();
});
$( ). ( ( ){
fileName = $( ). ();
(!fileName){ ( ); ;}
( );
modal = bootstrap. . ($( ));
modal. ();
$( ). ( );
});
$( ). ( ( ){
keyword = $( ). (). ();
type = $( ). ();
difficulty = $( ). ();
status = $( ). ();
filterText =[];
(keyword) filterText. ( );
(type) filterText. ( );
(difficulty) filterText. ( );
(status) filterText. ( );
( );
});
$( ). ( ( ){
$( ). ( );
$( ). ( );
$( ). ( );
$( ). ( );
});
</script >
</body >
</html >
五、自我感想 这次开发在线考试系统,让我跳出'单纯写代码'的局限,真正体会到'软件开发是解决实际问题'的本质,收获远超课程设计本身:
1. 从'代码搬运工'到'需求解决者'的转变 最初开发时,只关注'如何实现试题添加功能',却忽略了教师'批量导入试题''按难度筛选'等实际需求——这些细节不是技术决定的,而是教学场景驱动的。AI 工具在生成代码时,会通过注释提示'需支持 Excel 批量导入(教师常用)''需添加试题难度字段(组卷时筛选)',帮助跳出'纯技术思维'。比如客观题自动批改功能,原本只简单比对答案,后来根据提示添加'多选题选项排序比对'(避免学生因选项顺序不同被判错),这正是教师批改时的真实痛点。
2. 聚焦'核心价值' 以前开发时,要花 1-2 天写 Entity、Mapper 层的重复代码(比如每个实体的 get/set、每个 Mapper 的 CRUD 方法),现在 AI 工具能快速生成规范的基础代码,还自带参数校验、事务控制和防重复提交逻辑。这让有更多精力优化'用户体验':比如为考试页面添加'自动保存答案'(每 30 秒保存一次,避免浏览器崩溃丢失答案)、为教师端添加'试题批量导入模板下载'(附带详细使用说明),这些小功能虽简单,却让系统更贴合师生使用习惯。终于明白,AI 工具不是'替代开发者',而是帮我们把时间花在更有价值的'需求落地'上。
3. 解决问题的能力在实践中飞速提升 开发中遇到的'Excel 批量导入失败''考试计时偏差'等问题,对技术的理解从'会用'变成'能用好'。比如批量导入时,Excel 中'题型'字段学生可能填'单选'而非'0',参考生成的 Excel 解析代码,添加'中文转枚举'逻辑('单选'→0、'多选'→1);再比如考试计时偏差,最初用前端定时器计时,切换标签页时会暂停,后来结合后端记录的'开始时间'计算剩余时间,从根本上解决问题。这些方法不是课本上的理论,而是实际开发中的'踩坑经验'。
六、开发总结与展望
1. 开发收获 (1)技术能力提升:掌握了 Spring Boot+MyBatis-Plus 的开发流程,学会了使用 Bootstrap 实现响应式布局,对数据库设计和性能优化有了更深入的理解。
(2)解决问题能力:面对实际开发中的问题,学会了查阅文档、搜索解决方案,并通过调试逐步定位问题根源。AI 工具生成的代码注释和优化建议节省了很多时间。
(3)项目管理意识:学会了将一个复杂项目分解为多个模块,按优先级逐步实现,每周制定开发计划并检查进度,这种方法在有限时间内完成了所有核心功能。
2. 系统不足与优化方向 由于时间和技术水平限制,系统还有一些可以优化的地方:
(1)防作弊功能可以更完善:目前只实现了简单的切屏检测,未来可以添加摄像头监控、禁止复制粘贴、随机调整题目顺序等功能。
(2)添加智能组卷算法:根据知识点分布和难度系数自动生成更科学的试卷,而不仅仅是随机抽取。
(3)实现大数据分析:对学生答题数据进行分析,找出易错知识点,为教学提供参考。
(4)支持更多题型:如填空题、编程题(可在线编译运行)等。
3. 给其他学生开发者的建议 (1)善用开发工具:AI 工具能帮我们生成基础代码,避免重复劳动,但核心逻辑还是要自己思考和编写,不能完全依赖工具。
(2)多动手实践:看教程和文档只能掌握理论,真正的进步来自于实际开发,遇到问题不要怕,解决问题的过程就是成长的过程。
(3)学会拆解问题:复杂系统往往让人望而却步,但只要分解成一个个小模块,逐个实现,就能逐步构建出完整的系统。
(4)重视代码规范:即使是课程设计,也要养成良好的编码习惯,写注释、用有意义的变量名、遵循设计模式,这些都会让后续维护变得轻松。
这次在线考试系统的开发深刻体会到,从 0 到 1 构建一个实用的系统虽然有挑战,但也充满乐趣和成就感。作为学生,不必追求完美,重要的是在实践中学习和成长。希望我的开发记录能给其他同学带来一些启发,祝大家都能顺利完成课程设计!
if
""
alert
"请输入搜索关键词"
return
alert
`搜索关键词:${keyword} ,共找到 3 条结果`
if
600
classList
add
'text-danger'
classList
add
'text-danger'
1000
return
let
3
let
document
visibilityState
document
addEventListener
'visibilitychange'
()=>
if
document
visibilityState
'hidden'
'visible'
document
getElementById
'screenChangeCount'
textContent
const
document
getElementById
'screenChangeAlert'
style
display
'block'
setTimeout
()=>
style
display
'none'
3000
if
5
document
getElementById
'screenWarn'
style
display
'inline'
document
visibilityState
document
getElementById
'prevBtn'
addEventListener
'click'
()=>
alert
'切换到上一题'
document
getElementById
'nextBtn'
addEventListener
'click'
()=>
alert
'切换到下一题'
saveCurrentAnswer
document
getElementById
'submitBtn'
addEventListener
'click'
()=>
const
new
Modal
document
getElementById
'submitConfirmModal'
show
document
getElementById
'confirmSubmitBtn'
addEventListener
'click'
()=>
const
Modal
getInstance
document
getElementById
'submitConfirmModal'
hide
submitAllAnswers
function
saveCurrentAnswer
const
1
const
document
querySelector
`input[name="question${currentQuestionId} "]:checked`
value
console
log
`自动保存试题${currentQuestionId} 答案:${selectedOption} `
setInterval
30000
function
submitAllAnswers
ajax
url
"/api/student/exam/submit"
type
"POST"
data
JSON
stringify
examRecordId
1001
contentType
"application/json"
success
function
res
if
code
200
window
location
href
"/student/exam/submit-success?examRecordId=1001"
else
alert
msg
error
function
alert
"提交失败,请重试"
function
autoSubmit
alert
"考试时间已到,自动提交试卷"
submitAllAnswers
window
onload
function
startCountdown
initQuestionNav
function
initQuestionNav
document
querySelectorAll
'.question-btn'
forEach
btn =>
addEventListener
'click'
()=>
const
getAttribute
'data-question-id'
saveCurrentAnswer
alert
`切换到试题${questionId} `
document
querySelectorAll
'.question-btn'
forEach
b =>
classList
remove
'current'
classList
add
'current'
function
const
this
data
'question-id'
window
location
href
`/teacher/question-edit?questionId=${questionId} `
'.enable-btn'
click
function
const
this
data
'question-id'
const
this
closest
'tr'
find
'.status-badge'
removeClass
'status-disabled'
addClass
'status-enabled'
text
'启用'
this
removeClass
'btn-outline-success enable-btn'
addClass
'btn-outline-secondary disable-btn'
text
'禁用'
alert
`试题 ID ${questionId} 已启用,可参与组卷`
'.disable-btn'
click
function
const
this
data
'question-id'
const
this
closest
'tr'
if
confirm
`确定要禁用试题 ID ${questionId} 吗?禁用后将无法参与组卷`
find
'.status-badge'
removeClass
'status-enabled'
addClass
'status-disabled'
text
'禁用'
this
removeClass
'btn-outline-secondary disable-btn'
addClass
'btn-outline-success enable-btn'
text
'启用'
alert
`试题 ID ${questionId} 已禁用`
'#batchEnableBtn'
click
function
const
getSelectedQuestionIds
alert
`批量启用试题:${selectedIds.join(', ' )} `
updateBatchActionBar
'#batchDisableBtn'
click
function
const
getSelectedQuestionIds
if
confirm
`确定要批量禁用以下试题吗?\n${selectedIds.join(', ' )} `
alert
`批量禁用试题:${selectedIds.join(', ' )} `
updateBatchActionBar
'#batchDeleteBtn'
click
function
const
getSelectedQuestionIds
if
confirm
`确定要删除以下试题吗?删除后不可恢复!\n${selectedIds.join(', ' )} `
alert
`批量删除试题:${selectedIds.join(', ' )} `
updateBatchActionBar
function
getSelectedQuestionIds
const
'.question-check:checked'
each
function
push
this
data
'question-id'
return
'#batchImportBtn'
click
function
const
new
Modal
'#batchImportModal'
show
'#confirmImportBtn'
click
function
const
'#fileUpload'
val
if
alert
'请选择 Excel 文件'
return
alert
`文件 ${fileName} 导入中...\n(实际开发中需解析 Excel 并调用接口批量添加试题)`
const
Modal
getInstance
'#batchImportModal'
hide
'#fileUpload'
val
''
'#searchBtn'
click
function
const
'#searchInput'
val
trim
const
'#typeFilter'
val
const
'#difficultyFilter'
val
const
'#statusFilter'
val
let
if
push
`题干/学科:${keyword} `
if
push
`题型:${['单选题' ,'多选题' ,'判断题' ,'简答题' ][type]} `
if
push
`难度:${['简单' ,'中等' ,'困难' ][difficulty]} `
if
push
`状态:${status ==='1' ?'启用' :'禁用' } `
alert
`筛选条件:\n${filterText.length ? filterText.join('\n' ):'全部试题' } \n共找到 4 条结果`
'#resetBtn'
click
function
'#searchInput'
val
''
'#typeFilter'
val
''
'#difficultyFilter'
val
''
'#statusFilter'
val
''
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online