跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaAIjava

基于 Spring Boot 的在线考试系统设计与实现——学生课程实践记录

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

古灵精怪发布于 2026/3/21更新于 2026/10/1596 浏览
基于 Spring Boot 的在线考试系统设计与实现——学生课程实践记录

一、需求分析与技术选型

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 // 用户注册请求 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
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;

/**
 * <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;
    /**
     * 学科:如'计算机基础''Java 编程''高等数学'
     */
    private String subject;
    /**
     * 创建人 ID:关联 user 表(教师/管理员角色)
     */
    private Long createBy;
    /**
     * 创建时间:试题添加时间
     */
    private LocalDateTime createTime;
    /**
     * 状态:0-禁用(不参与组卷)1-启用(可组卷)
     */
    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;

/**
 * <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;
    /**
     * 最后操作时间:用于异常退出后续考判断
     */
    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;

/**
 * <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
    public static class RandomPaperParam {
        /**
         * 学科:随机组卷的试题学科范围
         */
        @NotBlank(message = "随机组卷需指定学科")
        private String subject;
        /**
         * 单选 - 题数:如 10
         */
        @NotNull(message = "随机组卷需指定单选题数")
        @Positive(message = "单选题数必须大于 0")
        private Integer singleCount;
        /**
         * 单选 - 每题分值:如 2
         */
        @NotNull(message = "随机组卷需指定单选题分值")
        @Positive(message = "单选题分值必须大于 0")
        private Integer singleScore;
        /**
         * 多选 - 题数:如 5
         */
        @NotNull(message = "随机组卷需指定多选题数")
        @Positive(message = "多选题数必须大于 0")
        private Integer multipleCount;
        /**
         * 多选 - 每题分值:如 4
         */
        @NotNull(message = "随机组卷需指定多选题分值")
        @Positive(message = "多选题分值必须大于 0")
        private Integer multipleScore;
        /**
         * 判断 - 题数:如 10
         */
        @NotNull(message = "随机组卷需指定判断题数")
        @Positive(message = "判断题数必须大于 0")
        private Integer judgeCount;
        /**
         * 判断 - 每题分值:如 1
         */
        @NotNull(message = "随机组卷需指定判断题分值")
        @Positive(message = "判断题分值必须大于 0")
        private Integer judgeScore;
        /**
         * 简答 - 题数:如 3
         */
        @NotNull(message = "随机组卷需指定简答题数")
        @Positive(message = "简答题数必须大于 0")
        private Integer essayCount;
        /**
         * 简答 - 每题分值:如 10
         */
        @NotNull(message = "随机组卷需指定简答题分值")
        @Positive(message = "简答题分值必须大于 0")
        private Integer essayScore;
        /**
         * 难度分布:0-简单 1-中等 2-困难(多个用逗号分隔,如 0,1)
         */
        @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;

/**
 * <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 final AnswerSheetMapper answerSheetMapper;
    private final UserMapper userMapper;

    /**
     * 学生开始考试(创建考试记录,返回试卷与试题)
     */
    @Override
    @Transactional
    public PaperDetailVO startExam(Long studentId, Long paperId) {
        log.info("学生开始考试:学生 ID={}, 试卷 ID={}", studentId, paperId);
        // 1. 校验学生身份(必须是学生角色)
        User student = userMapper.selectById(studentId);
        if (student == null || !"student".equals(student.getRole())) {
            throw new RuntimeException("无效的学生账号,无法开始考试");
        }
        // 2. 校验试卷状态(必须存在且启用)
        Paper paper = paperMapper.selectById(paperId);
        if (paper == null) {
            throw new RuntimeException("试卷不存在");
        }
        if (paper.getStatus() != 1) {
            throw new RuntimeException("该试卷已禁用,无法进行考试");
        }
        // 3. 校验是否已参与过该考试(避免重复考试)
        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("您已参与过该考试,无法重复考试");
        }
        // 4. 校验考试时间范围(若设置了开始/截止时间)
        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("考试已截止,无法开始");
        }
        // 5. 创建考试记录(状态设为'进行中')
        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); // 初始切屏次数为 0
        examRecordMapper.insert(examRecord);
        // 6. 查询试卷关联的试题(按题型排序)
        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);
        // 7. 转换为 PaperDetailVO(隐藏正确答案,仅返回题干与选项)
        PaperDetailVO paperDetailVO = new PaperDetailVO();
        paperDetailVO.setPaperId(paperId);
        paperDetailVO.setPaperName(paper.getPaperName());
        paperDetailVO.setExamDuration(paper.getExamDuration());
        paperDetailVO.setTotalScore(paper.getTotalScore());
        paperDetailVO.setExamRecordId(examRecord.getId()); // 关联考试记录 ID
        // 组装试题 VO(隐藏正确答案)
        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());
        // 1. 校验考试记录状态(必须是'进行中')
        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("无权提交他人考试");
        }
        // 2. 校验试卷截止时间(若已逾期,状态设为'已逾期')
        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("考试已逾期,提交失败");
        }
        // 3. 保存学生答题记录,自动批改客观题
        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());
        // 4. 批量保存答题记录
        answerSheetMapper.batchInsert(answerSheets);
        // 5. 更新考试记录(状态设为'已提交',填充得分与结束时间)
        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());
        // 1. 校验教师身份
        User teacher = userMapper.selectById(teacherId);
        if (teacher == null || !"teacher".equals(teacher.getRole())) {
            throw new RuntimeException("无效的教师账号,无法批改作业");
        }
        // 2. 校验考试记录状态(必须是'已提交')
        ExamRecord examRecord = examRecordMapper.selectById(examRecordId);
        if (examRecord == null) {
            throw new RuntimeException("考试记录不存在");
        }
        if (!ExamStatusEnum.SUBMITTED.equals(examRecord.getStatus())) {
            throw new RuntimeException("考试未提交,无法批改");
        }
        // 3. 校验待批改试题(必须是主观题,且属于该考试)
        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); // 60% 以上视为正确
            answerSheet.setGradeBy(teacherId);
            answerSheet.setGradeTime(LocalDateTime.now());
            answerSheetMapper.updateById(answerSheet);
            // 累计主观题总分
            essayTotalScore = essayTotalScore.add(BigDecimal.valueOf(grade.getScore()));
        }
        // 4. 更新考试记录总分与状态(客观题得分 + 主观题得分)
        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);
        // 1. 校验考试记录(必须存在且状态为'进行中')
        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("无权续考他人考试");
        }
        // 2. 校验是否已逾期(若试卷设置了截止时间)
        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("考试已截止,无法续考");
        }
        // 3. 计算剩余考试时间(原考试时长 - 已消耗时间)
        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("考试时间已用完,无法续考");
        }
        // 4. 更新最后操作时间(避免被判定为异常)
        examRecord.setLastOperateTime(now);
        examRecordMapper.updateById(examRecord);
        // 5. 返回试卷详情与已答题记录(用于前端恢复考试)
        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">
        <!-- 考试卡片 1:可参加 -->
        <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>
        <!-- 考试卡片 2:未开始(即将开始) -->
        <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>
        <!-- 考试卡片 3:已截止 -->
        <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,// 从登录态获取 paperId: paperId }),
            contentType:"application/json",
            success:function(res){
                if(res.code ===200){// 跳转至考试页面,携带考试记录 ID
                    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();
        if(keyword ===""){alert("请输入搜索关键词");return;}
        // 模拟搜索请求
        alert(`搜索关键词:${keyword},共找到 3 条结果`);
        // 实际开发中需刷新列表
    });
</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">
                    <!-- 题号按钮将通过 JS 动态生成 -->
                    <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">
                <!-- 第 1 题(单选题) -->
                <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>
                <!-- 其他题目将通过 JS 动态切换 -->
            </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(){
        // 从后端获取剩余时间(示例:1 小时 25 分 30 秒 = 5130 秒)
        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;
            // 剩余 10 分钟(600 秒)时变红提醒
            if(totalSeconds <=600){ 
                countdownElement.classList.add('text-danger'); 
                modalCountdownElement.classList.add('text-danger');
            }
        },1000);
        return interval;
    }
    // 切屏检测(简单实现)
    let screenChangeCount = 3;
    let lastVisibilityState = document.visibilityState;
    document.addEventListener('visibilitychange',()=>{
        if(document.visibilityState ==='hidden'&& lastVisibilityState ==='visible'){
            // 检测到切屏
            screenChangeCount++;
            document.getElementById('screenChangeCount').textContent = screenChangeCount;
            // 显示切屏提醒
            const alertElement = document.getElementById('screenChangeAlert');
            alertElement.style.display ='block';
            setTimeout(()=>{ 
                alertElement.style.display ='none';
            },3000);
            // 切屏超过 5 次显示警告
            if(screenChangeCount >=5){ 
                document.getElementById('screenWarn').style.display ='inline';
            }
        }
        lastVisibilityState = document.visibilityState;
    });
    // 上一题/下一题切换(模拟)
    document.getElementById('prevBtn').addEventListener('click',()=>{
        alert('切换到上一题');// 实际开发中需切换试题内容,更新题号导航状态
    }); 
    document.getElementById('nextBtn').addEventListener('click',()=>{
        alert('切换到下一题');// 实际开发中需切换试题内容,更新题号导航状态
        // 自动保存当前题答案
        saveCurrentAnswer();
    });
    // 提交考试
    document.getElementById('submitBtn').addEventListener('click',()=>{
        // 显示确认模态框
        const modal = new bootstrap.Modal(document.getElementById('submitConfirmModal'));
        modal.show();
    });
    // 确认提交
    document.getElementById('confirmSubmitBtn').addEventListener('click',()=>{
        // 关闭模态框
        const modal = bootstrap.Modal.getInstance(document.getElementById('submitConfirmModal'));
        modal.hide();
        // 提交所有答案
        submitAllAnswers();
    });
    // 自动保存答案(每 30 秒)
    function saveCurrentAnswer(){
        const currentQuestionId = 1;// 实际开发中获取当前题 ID
        const selectedOption = document.querySelector(`input[name="question${currentQuestionId}"]:checked`).value;
        // 模拟保存请求
        console.log(`自动保存试题${currentQuestionId}答案:${selectedOption}`);
    }
    // 定时自动保存(30 秒)
    setInterval(saveCurrentAnswer,30000);
    // 提交所有答案(模拟)
    function submitAllAnswers(){
        // 模拟提交请求
        $.ajax({
            url:"/api/student/exam/submit",
            type:"POST",
            data:JSON.stringify({ examRecordId:1001,// 从 URL 获取 answers:[{ questionId:1, studentAnswer:"C"},// 其他试题答案...]}),
            contentType:"application/json",
            success:function(res){
                if(res.code ===200){// 跳转至提交成功页面
                    window.location.href ="/student/exam/submit-success?examRecordId=1001";
                }else{
                    alert(res.msg);
                }},
            error:function(){alert("提交失败,请重试");}});
    }
    // 自动提交(时间到)
    function autoSubmit(){
        alert("考试时间已到,自动提交试卷");
        submitAllAnswers();
    }
    // 初始化
    window.onload=function(){
        // 启动倒计时
        startCountdown();
        // 初始化试题导航
        initQuestionNav();
    };
    // 初始化试题导航(模拟)
    function initQuestionNav(){
        // 为题号按钮添加点击事件
        document.querySelectorAll('.question-btn').forEach(btn=>{
            btn.addEventListener('click',()=>{
                const questionId = btn.getAttribute('data-question-id');
                // 保存当前题答案
                saveCurrentAnswer();
                // 切换到对应试题
                alert(`切换到试题${questionId}`);
                // 更新题号导航状态
                document.querySelectorAll('.question-btn').forEach(b=>{
                    b.classList.remove('current');
                });
                btn.classList.add('current');
            });
        });
    }
</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>
                        <!-- 试题 1:单选题(启用) -->
                        <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>
                        <!-- 试题 2:多选题(启用) -->
                        <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>
                        <!-- 试题 3:判断题(禁用) -->
                        <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>
                        <!-- 试题 4:简答题(启用) -->
                        <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(function(){
        const questionId = $(this).data('question-id'); 
        window.location.href =`/teacher/question-edit?questionId=${questionId}`;
    });
    // 启用/禁用试题(模拟)
    $('.enable-btn').click(function(){
        const questionId = $(this).data('question-id');
        const $tr = $(this).closest('tr');
        // 更新状态标签 
        $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 questionId = $(this).data('question-id');
        const $tr = $(this).closest('tr');
        if(confirm(`确定要禁用试题 ID ${questionId}吗?禁用后将无法参与组卷`)){
            // 更新状态标签 
            $tr.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 selectedIds = getSelectedQuestionIds();
        alert(`批量启用试题:${selectedIds.join(', ')}`);
        // 实际开发中需调用接口更新状态,并刷新表格
        updateBatchActionBar();
    });
    $('#batchDisableBtn').click(function(){
        const selectedIds = getSelectedQuestionIds();
        if(confirm(`确定要批量禁用以下试题吗?\n${selectedIds.join(', ')}`)){
            alert(`批量禁用试题:${selectedIds.join(', ')}`);
            // 实际开发中需调用接口更新状态,并刷新表格
            updateBatchActionBar();
        }
    });
    $('#batchDeleteBtn').click(function(){
        const selectedIds = getSelectedQuestionIds();
        if(confirm(`确定要删除以下试题吗?删除后不可恢复!\n${selectedIds.join(', ')}`)){
            alert(`批量删除试题:${selectedIds.join(', ')}`);
            // 实际开发中需调用接口删除试题,并刷新表格
            updateBatchActionBar();
        }
    });
    // 获取选中的试题 ID
    function getSelectedQuestionIds(){
        const ids =[];
        $('.question-check:checked').each(function(){ 
            ids.push($(this).data('question-id'));
        });
        return ids;
    }
    // 批量导入按钮(显示模态框)
    $('#batchImportBtn').click(function(){
        const modal = new bootstrap.Modal($('#batchImportModal')); 
        modal.show();
    });
    // 确认导入按钮(模拟)
    $('#confirmImportBtn').click(function(){
        const fileName = $('#fileUpload').val();
        if(!fileName){alert('请选择 Excel 文件');return;}
        // 模拟导入请求
        alert(`文件 ${fileName} 导入中...\n(实际开发中需解析 Excel 并调用接口批量添加试题)`);
        // 关闭模态框
        const modal = bootstrap.Modal.getInstance($('#batchImportModal')); 
        modal.hide();
        // 重置文件选择
        $('#fileUpload').val('');
    });
    // 搜索与筛选(模拟)
    $('#searchBtn').click(function(){
        const keyword = $('#searchInput').val().trim();
        const type = $('#typeFilter').val();
        const difficulty = $('#difficultyFilter').val();
        const status = $('#statusFilter').val();
        let filterText =[];
        if(keyword) filterText.push(`题干/学科:${keyword}`);
        if(type) filterText.push(`题型:${['单选题','多选题','判断题','简答题'][type]}`);
        if(difficulty) filterText.push(`难度:${['简单','中等','困难'][difficulty]}`);
        if(status) filterText.push(`状态:${status ==='1'?'启用':'禁用'}`);
        alert(`筛选条件:\n${filterText.length ? filterText.join('\n'):'全部试题'}\n共找到 4 条结果`);
    });
    // 重置筛选条件
    $('#resetBtn').click(function(){
        $('#searchInput').val('');
        $('#typeFilter').val('');
        $('#difficultyFilter').val('');
        $('#statusFilter').val('');
    });
</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 构建一个实用的系统虽然有挑战,但也充满乐趣和成就感。作为学生,不必追求完美,重要的是在实践中学习和成长。希望我的开发记录能给其他同学带来一些启发,祝大家都能顺利完成课程设计!

目录

  1. 一、需求分析与技术选型
  2. 1. 核心需求梳理
  3. 2. 技术选型考量
  4. 二、环境准备
  5. 1. 下载并安装 IntelliJ IDEA
  6. 2. 安装 AI 辅助插件
  7. 3. 登录 AI 辅助工具
  8. 三、模块设计与编码
  9. 1. AI 辅助生成基础模块
  10. 2. 核心代码展示
  11. (1)entity 包:核心实体类
  12. (2)dto 包:数据传输对象(带参数校验)
  13. (3)service 包:业务逻辑实现(含核心考试流程)
  14. 四、网页展示
  15. 1. 学生端 - 考试列表页
  16. 2. 学生端 - 考试答题页
  17. 3. 教师端 - 试题管理页
  18. 五、自我感想
  19. 1. 从“代码搬运工”到“需求解决者”的转变
  20. 2. 聚焦“核心价值”
  21. 3. 解决问题的能力在实践中飞速提升
  22. 六、开发总结与展望
  23. 1. 开发收获
  24. 2. 系统不足与优化方向
  25. 3. 给其他学生开发者的建议

更多推荐文章

查看全部
  • OpenClaw 框架 30+ 真实场景实战拆解
  • 前端开发实战:一天通常能完成多少个页面?
  • 二分查找实战:山脉数组的峰顶索引与寻找峰值
  • AI 产品经理入门指南:核心技能与机器学习基础
  • 基于 MCP Server - Figma AI Bridge 实现 Figma 设计稿前端代码自动化
  • GitHub Copilot SDK 与云原生多智能体系统构建
  • Linux 网络编程实战:HTTP 协议解析与 C++ 服务器实现
  • 大模型微调与 RAG 的区别是什么?
  • llamafile 使用指南:从下载到运行仅需 3 步
  • C++ 核心知识点解析
  • 基于深度学习的无人机航拍小目标检测算法研究
  • Gemini 3 免费上手:六个渠道实测与取舍
  • 将第三方 OpenAI 兼容模型接入 GitHub Copilot 的两种方案
  • 前端三年后,我辞职去韩国读了个CS硕士
  • 开源医疗大模型排行榜:健康领域基准测试与评估
  • AI 图像生成指南:从原理到实战
  • 默认安全治理实践:水平越权检测与前端安全防控
  • Stable Diffusion WebUI Forge 照片风格迁移实战指南
  • MCP 协议详解:与 Function Call 的区别及使用方式
  • AI+AR智能眼镜赛道年终观察

相关免费在线工具

  • 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