基于 Spring Boot 的在线考试系统设计与实现——学生课程实践记录
基于 Spring Boot 框架构建在线考试系统,涵盖学生、教师及管理员多角色权限管理。系统支持多种题型(单选、多选、判断、简答),具备定时交卷、防作弊检测及断线续考功能。后端采用 MyBatis-Plus 简化数据库操作,前端通过 Bootstrap 实现响应式界面。开发过程结合 AI 辅助生成基础代码,重点实现了客观题自动判分与主观题人工批改逻辑,完成了从需求分析到系统部署的全流程实践。

基于 Spring Boot 框架构建在线考试系统,涵盖学生、教师及管理员多角色权限管理。系统支持多种题型(单选、多选、判断、简答),具备定时交卷、防作弊检测及断线续考功能。后端采用 MyBatis-Plus 简化数据库操作,前端通过 Bootstrap 实现响应式界面。开发过程结合 AI 辅助生成基础代码,重点实现了客观题自动判分与主观题人工批改逻辑,完成了从需求分析到系统部署的全流程实践。

明确了系统需要实现的核心功能:
选择了容易上手且资料丰富的技术栈:
选择 AI 辅助工具是因为它能根据需求生成基础代码框架,对于编码经验不足的学生来说,能节省大量重复劳动,让我专注于业务逻辑实现。
结合常用的 Windows 环境,完成环境搭建:
作为学生,免费且功能完善的IDEA 社区版是首选。在 JetBrains 官网找到'IntelliJ IDEA',选择'Community'版本下载安装包。
安装时注意两个关键设置:一是勾选'Add launchers dir to the PATH'(添加到环境变量),方便后续通过命令行启动;二是勾选'Create Desktop Shortcut'(创建桌面快捷方式),避免后续找不到启动图标。全程点击'下一步'即可。
打开 IDEA 后,点击顶部菜单栏'File → Settings → Plugins',在右侧搜索框输入相关插件名称,找到带有官方标识的插件,点击'Install'。
重启 IDEA 后,点击面板中的'立即登录',用学生邮箱注册账号,完成手机验证后登录。登录成功后,面板会显示'需求分析→软件设计→工程代码生成'的全流程引导,贴合从想法到落地的完整开发需求。
在 AI 辅助工具的协助下,按'需求描述→拆解分析→设计→编码'的流程系统化开发。以下是系统的完整开发过程:
AI 工具支持口语化需求描述——不需要专业术语,用日常表达就能精准生成代码。在插件面板的'需求编辑器'中输入需求描述,例如生成在线考试系统基础模块,包含 3 类核心角色及核心实体,实现核心功能如学生注册/登录、在线考试、成绩查询等。
提交需求后,AI 自动解析需求,将描述拆解成多个可执行的核心模块,标注了'必填功能'和'可选优化':
确认需求后,AI 自动进入'接口设计→表结构设计→处理逻辑→生成源码'环节。最终生成的项目结构清晰,核心包与类已完整创建,省去了手动建包、写基础类的麻烦。
com.student.exam
├─ entity // 实体类(映射数据库表)
│ ├─ User.java // 用户实体(区分学生/教师/管理员)
│ ├─ Question.java // 试题实体(含题型、选项、答案等)
│ ├─ Paper.java // 试卷实体(含考试时长、总分等)
│ ├─ PaperQuestion.java // 试卷 - 试题关联实体(多对多关系)
│ ├─ ExamRecord.java // 考试记录实体(含得分、考试状态)
│ └─ AnswerSheet.java // 答题记录实体(含学生答案、批改结果)
├─ dto // 数据传输对象(接收前端请求参数)
│ ├─ UserRegisterDTO.java // 用户注册请求 DTO
│ ├─ QuestionAddDTO.java // 试题添加请求 DTO
│ ├─ PaperCreateDTO.java // 试卷创建请求 DTO
│ └─ ExamSubmitDTO.java // 考试提交请求 DTO
├─ vo // 视图对象(向前端返回数据)
│ ├─ QuestionVO.java // 试题展示 VO(隐藏正确答案)
│ ├─ PaperDetailVO.java // 试卷详情 VO(含试题列表)
│ └─ ScoreVO.java // 成绩展示 VO(含答题详情与批改意见)
├─ mapper // 数据访问接口(MyBatis-Plus)
│ ├─ 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
AI 生成的代码不仅结构规范,还自带参数校验、事务控制和详细注释,仅需根据考试场景补充少量业务逻辑。以下是关键模块的代码示例:
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;
/**
* <p>
* 试题实体类:映射 question 表,支持单选、多选、判断、简答 4 种题型
* </p>
* @author Student Developer
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("question")
public class Question {
/**
* 试题 ID:主键,自增
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 题干:试题内容,如'下列关于 Java 继承的描述正确的是?'
*/
private String content;
/**
* 题型:0-单选 1-多选 2-判断 3-简答(使用枚举约束)
*/
private QuestionTypeEnum type;
/**
* 选项 A:单选/多选题有效
*/
private String optionA;
/**
* 选项 B:单选/多选题有效
*/
private String optionB;
/**
* 选项 C:单选/多选题有效
*/
private String optionC;
/**
* 选项 D:单选/多选题有效
*/
private String optionD;
/**
* 正确答案:单选(A/B/C/D)、多选(A,B,C)、判断(对/错)、简答(文本答案)
*/
private String correctAnswer;
/**
* 分值:试题满分,如 2 分、5 分
*/
private Integer score;
/**
* 难度:0-简单 1-中等 2-困难
*/
private Integer difficulty;
String subject;
Long createBy;
LocalDateTime createTime;
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;
/**
* <p>
* 考试记录实体类:映射 exam_record 表,记录学生考试全流程信息
* </p>
* @author Student Developer
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("exam_record")
public class ExamRecord {
/**
* 记录 ID:主键,自增
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 学生 ID:关联 user 表(学生角色)
*/
private Long studentId;
/**
* 试卷 ID:关联 paper 表
*/
private Long paperId;
/**
* 考试状态:0-未开始 1-进行中 2-已提交 3-已批改 4-已逾期(未提交)
*/
private ExamStatusEnum status;
/**
* 开始时间:学生进入考试的时间
*/
private LocalDateTime startTime;
/**
* 结束时间:学生提交考试的时间(未提交则为 null)
*/
private LocalDateTime endTime;
/**
* 得分:考试最终分数(客观题自动打分 + 主观题手动打分)
*/
private BigDecimal score;
/**
* 总分:试卷满分(与 paper 表总分一致)
*/
private Integer totalScore;
/**
* 切屏次数:防作弊统计(超过 5 次提醒教师)
*/
private Integer screenChangeCount;
/**
* 最后操作时间:用于异常退出后续考判断
*/
LocalDateTime lastOperateTime;
}
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;
/**
* <p>
* 试卷创建请求 DTO:接收教师创建试卷的参数,含参数校验
* </p>
* @author Student Developer
*/
@Data
public class PaperCreateDTO {
/**
* 试卷名称:必填,如'Java 编程基础期末测试'
*/
@NotBlank(message = "试卷名称不能为空")
private String paperName;
/**
* 学科:必填,如'Java 编程'
*/
@NotBlank(message = "学科不能为空")
private String subject;
/**
* 考试时长:必填,单位分钟(如 60、90)
*/
@NotNull(message = "考试时长不能为空")
@Positive(message = "考试时长必须大于 0")
private Integer examDuration;
/**
* 总分:必填,试卷满分(如 100)
*/
@NotNull(message = "试卷总分不能为空")
@Positive(message = "试卷总分必须大于 0")
private Integer totalScore;
/**
* 组卷方式:0-手动组卷 1-随机组卷
*/
@NotNull(message = "组卷方式不能为空")
private Integer paperType;
/**
* 手动组卷 - 试题 ID 列表:paperType=0 时必填
*/
private List<Long> questionIds;
/**
* 随机组卷 - 参数:paperType=1 时必填
*/
private RandomPaperParam randomParam;
/**
* 考试开始时间:可选,不填则学生可立即开始
*/
private String start_time;
/**
* 考试截止时间:可选,不填则无截止时间
*/
private String end_time;
/**
* 随机组卷参数(内部类)
*/
@Data
{
String subject;
Integer singleCount;
Integer singleScore;
Integer multipleCount;
Integer multipleScore;
Integer judgeCount;
Integer judgeScore;
Integer essayCount;
Integer essayScore;
String difficulty;
}
}
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;
/**
* <p>
* 考试服务实现类:处理考试开始、提交、自动批改、续考等核心业务
* </p>
* @author Student Developer
*/
@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 AnswerSheetMapper answerSheetMapper;
UserMapper userMapper;
PaperDetailVO {
log.info(, studentId, paperId);
userMapper.selectById(studentId);
(student == || !.equals(student.getRole())) {
();
}
paperMapper.selectById(paperId);
(paper == ) {
();
}
(paper.getStatus() != ) {
();
}
LambdaQueryWrapper<ExamRecord> recordWrapper = <>();
recordWrapper.eq(ExamRecord::getStudentId, studentId).eq(ExamRecord::getPaperId, paperId)
.in(ExamRecord::getStatus, ExamStatusEnum.IN_PROGRESS, ExamStatusEnum.SUBMITTED, ExamStatusEnum.GRADED);
(examRecordMapper.exists(recordWrapper)) {
();
}
LocalDateTime.now();
(paper.getStartTime() != && now.isBefore(paper.getStartTime())) {
( + paper.getStartTime());
}
(paper.getEndTime() != && now.isAfter(paper.getEndTime())) {
();
}
();
examRecord.setStudentId(studentId);
examRecord.setPaperId(paperId);
examRecord.setStatus(ExamStatusEnum.IN_PROGRESS);
examRecord.setStartTime(now);
examRecord.setLastOperateTime(now);
examRecord.setTotalScore(paper.getTotalScore());
examRecord.setScreenChangeCount();
examRecordMapper.insert(examRecord);
List<PaperQuestion> paperQuestions = paperQuestionMapper.selectList( <PaperQuestion>().eq(PaperQuestion::getPaperId, paperId));
List<Long> questionIds = paperQuestions.stream().map(PaperQuestion::getQuestionId).collect(Collectors.toList());
List<Question> questions = questionMapper.selectBatchIds(questionIds);
();
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.setId(question.getId());
questionVO.setContent(question.getContent());
questionVO.setType(question.getType());
questionVO.setScore(question.getScore());
(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());
}
questionVO;
}).collect(Collectors.toList());
paperDetailVO.setQuestions(questionVOList);
log.info(, examRecord.getId());
paperDetailVO;
}
{
log.info(, submitDTO.getExamRecordId(), submitDTO.getStudentId());
examRecordMapper.selectById(submitDTO.getExamRecordId());
(examRecord == ) {
();
}
(!ExamStatusEnum.IN_PROGRESS.equals(examRecord.getStatus())) {
();
}
(!examRecord.getStudentId().equals(submitDTO.getStudentId())) {
();
}
paperMapper.selectById(examRecord.getPaperId());
LocalDateTime.now();
(paper.getEndTime() != && now.isAfter(paper.getEndTime())) {
examRecord.setStatus(ExamStatusEnum.OVERDUE);
examRecordMapper.updateById(examRecord);
();
}
BigDecimal.ZERO;
List<AnswerSheet> answerSheets = submitDTO.getAnswers().stream().map(answer -> {
();
answerSheet.setExamRecordId(submitDTO.getExamRecordId());
answerSheet.setQuestionId(answer.getQuestionId());
answerSheet.setStudentAnswer(answer.getStudentAnswer());
answerSheet.setCreateTime(now);
questionMapper.selectById(answer.getQuestionId());
(question == ) {
( + answer.getQuestionId());
}
(QuestionTypeEnum.SINGLE_CHOICE.equals(question.getType()) || QuestionTypeEnum.JUDGE.equals(question.getType())) {
question.getCorrectAnswer().equals(answer.getStudentAnswer());
answerSheet.setIsCorrect(isCorrect ? : );
answerSheet.setScore(isCorrect ? question.getScore() : );
totalScore = totalScore.add(BigDecimal.valueOf(isCorrect ? question.getScore() : ));
} (QuestionTypeEnum.MULTIPLE_CHOICE.equals(question.getType())) {
sortAnswer(question.getCorrectAnswer());
sortAnswer(answer.getStudentAnswer());
correctAnswer.equals(studentAnswer);
answerSheet.setIsCorrect(isCorrect ? : );
answerSheet.setScore(isCorrect ? question.getScore() : );
totalScore = totalScore.add(BigDecimal.valueOf(isCorrect ? question.getScore() : ));
} {
answerSheet.setIsCorrect();
answerSheet.setScore();
answerSheet.setTeacherComment();
}
answerSheet;
}).collect(Collectors.toList());
answerSheetMapper.batchInsert(answerSheets);
examRecord.setStatus(ExamStatusEnum.SUBMITTED);
examRecord.setEndTime(now);
examRecord.setScore(totalScore);
examRecordMapper.updateById(examRecord);
log.info(, submitDTO.getExamRecordId(), totalScore, examRecord.getTotalScore());
}
{
log.info(, teacherId, examRecordId, gradeList.size());
userMapper.selectById(teacherId);
(teacher == || !.equals(teacher.getRole())) {
();
}
examRecordMapper.selectById(examRecordId);
(examRecord == ) {
();
}
(!ExamStatusEnum.SUBMITTED.equals(examRecord.getStatus())) {
();
}
BigDecimal.ZERO;
(GradeDTO grade : gradeList) {
questionMapper.selectById(grade.getQuestionId());
(question == ) {
( + grade.getQuestionId());
}
(!QuestionTypeEnum.ESSAY.equals(question.getType())) {
( + grade.getQuestionId());
}
LambdaQueryWrapper<AnswerSheet> answerWrapper = <>();
answerWrapper.eq(AnswerSheet::getExamRecordId, examRecordId).eq(AnswerSheet::getQuestionId, grade.getQuestionId());
answerSheetMapper.selectOne(answerWrapper);
(answerSheet == ) {
( + grade.getQuestionId());
}
(grade.getScore() < || grade.getScore() > question.getScore()) {
( + question.getScore() + + grade.getScore());
}
answerSheet.setScore(grade.getScore());
answerSheet.setTeacherComment(grade.getComment());
answerSheet.setIsCorrect(grade.getScore() >= question.getScore() * ? : );
answerSheet.setGradeBy(teacherId);
answerSheet.setGradeTime(LocalDateTime.now());
answerSheetMapper.updateById(answerSheet);
essayTotalScore = essayTotalScore.add(BigDecimal.valueOf(grade.getScore()));
}
examRecord.getScore().add(essayTotalScore);
examRecord.setScore(finalScore);
examRecord.setStatus(ExamStatusEnum.GRADED);
examRecordMapper.updateById(examRecord);
log.info(, examRecordId, finalScore, examRecord.getTotalScore());
}
PaperDetailVO {
log.info(, studentId, examRecordId);
examRecordMapper.selectById(examRecordId);
(examRecord == ) {
();
}
(!ExamStatusEnum.IN_PROGRESS.equals(examRecord.getStatus())) {
();
}
(!examRecord.getStudentId().equals(studentId)) {
();
}
paperMapper.selectById(examRecord.getPaperId());
LocalDateTime.now();
(paper.getEndTime() != && now.isAfter(paper.getEndTime())) {
examRecord.setStatus(ExamStatusEnum.OVERDUE);
examRecordMapper.updateById(examRecord);
();
}
ChronoUnit.MINUTES.between(examRecord.getStartTime(), now);
paper.getExamDuration() - () usedMinutes;
(remainingMinutes <= ) {
examRecord.setStatus(ExamStatusEnum.OVERDUE);
examRecordMapper.updateById(examRecord);
();
}
examRecord.setLastOperateTime(now);
examRecordMapper.updateById(examRecord);
();
paperDetailVO.setPaperId(paper.getId());
paperDetailVO.setPaperName(paper.getPaperName());
paperDetailVO.setExamDuration(remainingMinutes);
paperDetailVO.setTotalScore(paper.getTotalScore());
paperDetailVO.setExamRecordId(examRecordId);
List<AnswerSheet> answerSheets = answerSheetMapper.selectList( <AnswerSheet>().eq(AnswerSheet::getExamRecordId, examRecordId));
paperDetailVO.setAnsweredQuestions(answerSheets);
List<PaperQuestion> paperQuestions = paperQuestionMapper.selectList( <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.setId(question.getId());
questionVO.setContent(question.getContent());
questionVO.setType(question.getType());
questionVO.setScore(question.getScore());
(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());
}
questionVO;
}).collect(Collectors.toList());
paperDetailVO.setQuestions(questionVOList);
log.info(, examRecordId, remainingMinutes);
paperDetailVO;
}
String {
(answer == || answer.isEmpty()) {
;
}
java.util.Arrays.stream(answer.split()).sorted().collect(Collectors.joining());
}
}
为贴合学生在教室、实验室使用电脑考试的场景,前端采用 Bootstrap 实现响应式布局,界面简洁无冗余,突出'考试'核心功能。以下是核心页面的设计与功能:
<!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,,); : (-); }
{ : solid ; }
{ : ; : ; : ; }
{ : ; : ; }
{ : ; : ; }
{ : ; : ; }
{ : absolute; : ; : ; : ; : ; : ; : ; : ; }
在线考试系统
我的考试
我的成绩
个人中心
学号:2024001(张三)
个人信息
修改密码
退出登录
我的考试
可参加
Java 编程基础期末测试
学科:Java 编程
考试时长:90 分钟
总分:100 分
时间:2024-06-20 09:00 - 2024-06-20 11:00
开始考试
即将开始(10 分钟后)
未开始
计算机网络期中测试
学科:计算机基础
考试时长:60 分钟
总分:80 分
时间:2024-06-20 14:30 - 2024-06-20 15:30
未到开始时间
已截止
高等数学(上)单元测试
学科:高等数学
考试时长:120 分钟
总分:150 分
时间:2024-06-15 09:00 - 2024-06-15 11:00
已截止
上一页
1
2
3
下一页
<!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: ; : solid ; : ; : ; : ( - ); : auto; }
{ : ; : ; : ; : ; : flex; : center; : center; : solid ; : ; : pointer; : all ; }
{ : ; : ; : ; }
{ : ; : ; : ; : bold; }
{ : ; : ; : ; }
{ : ; : solid ; : ; : ; : ( - ); }
{ : ; : ; : solid ; : ; : pointer; : all ; }
{ : ; }
{ : ; : ; }
{ : ; : ; : solid ; : ; : ; : vertical; }
{ : fixed; : ; : ; : ; : none; }
试卷名称:Java 编程基础期末测试
切屏次数:3(次数过多,请注意!)
剩余时间:01:25:30
警告! 检测到切屏行为,切屏次数过多将影响考试成绩。
试题导航
1
2
3
4
5
6
7
8
9
10
当前题
已答题
未答题
单选题(2 分)
1. 下列关于 Java 中'继承'的描述,正确的是?
A. Java 支持多继承
B. 子类可以继承父类的所有成员
C. 子类可以重写父类的方法
D. 父类可以访问子类的成员
上一题
下一题
提交考试
确认提交考试?
提交后将无法修改答案,请确认是否完成所有试题?
剩余时间:01:25: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>
.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: ; : ; : ; }
{ : ; : ; }
{ : ; : ; }
{ : ; : ; : ; : ; }
{ : ; : nowrap; : hidden; : ellipsis; }
在线考试系统
试卷管理
试题管理
考试管理
批改管理
工号:T202401(李老师)
个人中心
修改密码
退出登录
试题管理
批量导入
添加试题
全部题型
单选题
多选题
判断题
简答题
全部难度
简单
中等
困难
全部状态
启用
禁用
搜索
重置
试题 ID
题干
题型
学科
难度
分值
状态
创建时间
操作
1
下列关于 Java 中'继承'的描述,正确的是?
单选题
Java 编程
中等
2
启用
2024-06-01
编辑禁用
2
下列属于 Java 集合框架中的接口的是?(多选)
多选题
Java 编程
困难
4
启用
2024-06-02
编辑禁用
3
Java 中的'=='既可以比较基本数据类型,也可以比较引用数据类型的地址。(判断)
判断题
Java 编程
简单
1
禁用
2024-06-03
编辑启用
4
请简述 Java 中 synchronized 关键字的作用及使用场景。
简答题
Java 编程
困难
10
启用
2024-06-05
编辑禁用
已选中 0 道试题
批量启用
批量禁用
批量删除
上一页
1
2
3
下一页
批量导入试题
下载模板
试题导入模板.xlsx(含使用说明)
选择 Excel 文件
支持.xlsx/.xls 格式,单次最多导入 100 道试题
取消
开始导入
这次开发在线考试系统,让我跳出'单纯写代码'的局限,真正体会到'软件开发是解决实际问题'的本质,收获远超课程设计本身:
最初开发时,只关注'如何实现试题添加功能',却忽略了教师'批量导入试题''按难度筛选'等实际需求——这些细节不是技术决定的,而是教学场景驱动的。AI 工具在生成代码时,会通过注释提示'需支持 Excel 批量导入(教师常用)''需添加试题难度字段(组卷时筛选)',帮助跳出'纯技术思维'。比如客观题自动批改功能,原本只简单比对答案,后来根据提示添加'多选题选项排序比对'(避免学生因选项顺序不同被判错),这正是教师批改时的真实痛点。
以前开发时,要花 1-2 天写 Entity、Mapper 层的重复代码(比如每个实体的 get/set、每个 Mapper 的 CRUD 方法),现在 AI 工具能快速生成规范的基础代码,还自带参数校验、事务控制和防重复提交逻辑。这让有更多精力优化'用户体验':比如为考试页面添加'自动保存答案'(每 30 秒保存一次,避免浏览器崩溃丢失答案)、为教师端添加'试题批量导入模板下载'(附带详细使用说明),这些小功能虽简单,却让系统更贴合师生使用习惯。终于明白,AI 工具不是'替代开发者',而是帮我们把时间花在更有价值的'需求落地'上。
开发中遇到的'Excel 批量导入失败''考试计时偏差'等问题,对技术的理解从'会用'变成'能用好'。比如批量导入时,Excel 中'题型'字段学生可能填'单选'而非'0',参考生成的 Excel 解析代码,添加'中文转枚举'逻辑('单选'→0、'多选'→1);再比如考试计时偏差,最初用前端定时器计时,切换标签页时会暂停,后来结合后端记录的'开始时间'计算剩余时间,从根本上解决问题。这些方法不是课本上的理论,而是实际开发中的'踩坑经验'。
通过这个在线考试系统的开发,有以下收获:
(1)技术能力提升:掌握了 Spring Boot+MyBatis-Plus 的开发流程,学会了使用 Bootstrap 实现响应式布局,对数据库设计和性能优化有了更深入的理解。
(2)解决问题能力:面对实际开发中的问题,学会了查阅文档、搜索解决方案,并通过调试逐步定位问题根源。AI 工具生成的代码注释和优化建议节省了很多时间。
(3)项目管理意识:学会了将一个复杂项目分解为多个模块,按优先级逐步实现,每周制定开发计划并检查进度,这种方法在有限时间内完成了所有核心功能。
由于时间和技术水平限制,系统还有一些可以优化的地方:
(1)防作弊功能可以更完善:目前只实现了简单的切屏检测,未来可以添加摄像头监控、禁止复制粘贴、随机调整题目顺序等功能。
(2)添加智能组卷算法:根据知识点分布和难度系数自动生成更科学的试卷,而不仅仅是随机抽取。
(3)实现大数据分析:对学生答题数据进行分析,找出易错知识点,为教学提供参考。
(4)支持更多题型:如填空题、编程题(可在线编译运行)等。
(1)善用开发工具:AI 工具能帮我们生成基础代码,避免重复劳动,但核心逻辑还是要自己思考和编写,不能完全依赖工具。
(2)多动手实践:看教程和文档只能掌握理论,真正的进步来自于实际开发,遇到问题不要怕,解决问题的过程就是成长的过程。
(3)学会拆解问题:复杂系统往往让人望而却步,但只要分解成一个个小模块,逐个实现,就能逐步构建出完整的系统。
(4)重视代码规范:即使是课程设计,也要养成良好的编码习惯,写注释、用有意义的变量名、遵循设计模式,这些都会让后续维护变得轻松。
这次在线考试系统的开发深刻体会到,从 0 到 1 构建一个实用的系统虽然有挑战,但也充满乐趣和成就感。作为学生,不必追求完美,重要的是在实践中学习和成长。希望我的开发记录能给其他同学带来一些启发,祝大家都能顺利完成课程设计!

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online