当前位置: 首页 > news >正文

飞算Java的在线考试系统的设计与实现——学生开发者的课程实践记录

目录

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

作为视觉传达专业大一学生,网页制作课程设计要求我们开发一个在线考试系统。起初我对这个任务感到压力很大,毕竟要兼顾用户管理、题库设计、在线考试和自动阅卷等多个模块。好在计算机专业的学长推荐了飞算JavaAI辅助开发,让我在三周时间内顺利完成了系统开发。现将开发过程中的思路与实践分享给大家。

一、需求分析与技术选型

1. 核心需求梳理

通过与课程老师沟通,我明确了系统需要实现的核心功能:

  • 多角色管理:学生(参加考试、查询成绩)、教师(创建题库、组卷、阅卷)、管理员(用户管理、系统设置)
  • 题库管理:支持单选题、多选题、判断题和简答题,可批量导入试题
  • 在线考试:定时交卷、防作弊(禁止切屏)、异常断线后可续考
  • 自动阅卷:客观题自动评分,主观题教师手动评分
  • 成绩统计:按班级、科目统计平均分、及格率等数据

2. 技术选型考量

作为学生开发者,我选择了容易上手且资料丰富的技术栈:

  • 后端:Spring Boot 2.7.x(开发效率高,适合快速迭代)
  • 前端:Bootstrap 5 + jQuery(响应式设计,适配电脑和手机)
  • 数据库:MySQL 8.0(关系型数据库,适合存储结构化考试数据)
  • 开发工具:IntelliJ IDEA(学生免费版)+ 飞算JavaAI插件

选择飞算JavaAI是因为它能根据需求生成基础代码框架,对于我这种编码经验不足的学生来说,能节省大量重复劳动,让我专注于业务逻辑实现。

二、环境准备

参考学长分享的开发经验,结合学生常用的Windows环境,我用3步完成环境搭建,全程未遇到复杂配置,新手也能快速上手:

1. 下载并安装IntelliJ IDEA

作为学生,免费且功能完善的IDEA社区版是首选。打开JetBrains中文官网(www.jetbrains.com.cn),在“开发者工具”中找到“IntelliJ IDEA”,选择“Community”版本下载Windows安装包。

安装时注意两个关键设置:一是勾选“Add launchers dir to the PATH”(添加到环境变量),方便后续通过命令行启动;二是勾选“Create Desktop Shortcut”(创建桌面快捷方式),避免后续找不到启动图标。全程点击“下一步”即可,约8分钟完成安装,比安装Office套件更简单。
在这里插入图片描述

2. 安装飞算JavaAI插件

打开IDEA后,点击顶部菜单栏“File → Settings → Plugins”,在右侧搜索框输入“飞算JavaAI”,找到带有“feisuanyz”官方标识的插件(评分4.6+,下载量超50K),点击“Install”。

3. 登录飞算JavaAI

重启IDEA后,点击右侧“飞算JavaAI”面板中的“立即登录”,用学生邮箱(QQ邮箱、校园邮箱均可)注册账号,完成手机验证后登录。登录成功后,面板会显示“需求分析→软件设计→工程代码生成”的全流程引导,与我之前用过的“仅生成代码片段”的工具不同,它更贴合学生“从想法到落地”的完整开发需求。

三、模块设计与编码

在飞算JavaAI的辅助下,我不再像以前那样“想到哪写到哪”,而是按“需求描述→拆解分析→设计→编码”的流程系统化开发。以下是在线考试系统的完整开发过程:

1. 飞算JavaAI生成基础模块

飞算JavaAI最让我惊喜的是“支持口语化需求描述”——不需要专业术语,用学生的日常表达就能精准生成代码。我在插件面板的“需求编辑器”中输入:

“生成在线考试系统基础模块,包含3类核心角色:学生(学号、班级、联系方式)、教师(工号、学科、职称)、管理员(工号、权限范围);实现核心实体:试题(支持单选/多选/判断/简答4种题型,含题干、选项、答案、分值、难度)、试卷(含考试时间、总分、关联试题列表)、考试记录(含学生ID、试卷ID、开始时间、结束时间、得分、答题详情);核心功能:学生注册/登录、在线考试(定时交卷、防切屏提醒)、成绩查询;教师创建试题/组卷(手动组卷+随机组卷)、批改作业(客观题自动批改、主观题手动批改);管理员管理用户(增删改查学生/教师账号)、系统参数设置(考试时长范围、成绩等级划分);技术栈:Spring Boot 3.x + MyBatis-Plus + MySQL 8.0,前端用Thymeleaf+Bootstrap(适配电脑端,方便学生在教室/实验室使用)。”

在这里插入图片描述
点击“提交需求”后,飞算JavaAI自动解析需求,10秒内将我的口语化描述拆解成9个可执行的核心模块,还标注了“必填功能”和“可选优化”(学生可根据课程设计要求调整):

  • ☑ 用户模块:支持学生/教师/管理员三类角色注册登录,基于角色控制权限(学生不能组卷,教师不能删除用户)
  • ☑ 试题模块:教师添加/编辑/删除试题,支持按题型/难度/学科筛选,支持批量导入试题(Excel)
  • ☑ 试卷模块:教师手动选择试题组卷或按“题型+分值+难度”随机组卷,设置考试时长与总分
  • ☑ 考试模块:学生进入考试后倒计时提醒,禁止重复提交,异常退出后可续考
  • ☑ 阅卷模块:客观题(单选/多选/判断)自动比对答案评分,主观题(简答)教师手动打分并写评语
  • ☑ 成绩模块:学生查询个人考试成绩与答题详情,教师查看班级成绩统计(平均分、及格率)
  • △ 防作弊模块:添加摄像头监控、切屏次数限制(可选,后续课程设计优化时补充)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
确认需求后,飞算JavaAI自动进入“接口设计→表结构设计→处理逻辑→生成源码”环节,全程无需手动干预。最终生成的项目结构清晰,核心包与类已完整创建,省去了我手动建包、写基础类的麻烦:
在这里插入图片描述

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. 核心代码展示

飞算JavaAI生成的代码不仅结构规范,还自带参数校验、事务控制和详细注释,我仅需根据考试场景补充少量业务逻辑(比如客观题自动批改、考试倒计时),以下是关键模块的代码示例:

(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 feisuan-javaai(学生开发者二次优化)*/
@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 feisuan-javaai(学生开发者二次优化)*/
@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 feisuan-javaai*/
@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;/*** 随机组卷参数(内部类)*/@Datapublic 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 feisuan-javaai(学生开发者补充自动批改与续考逻辑)*/
@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@Transactionalpublic 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); // 初始切屏次数为0examRecordMapper.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@Transactionalpublic 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@Transactionalpublic 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());}/*** 学生续考(异常退出后恢复考试)*/@Overridepublic 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/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"><link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/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/jquery@3.6.4/dist/jquery.min.js"></script><script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/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) {// 跳转至考试页面,携带考试记录IDwindow.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/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"><link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/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/jquery@3.6.4/dist/jquery.min.js"></script><script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/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; // 实际开发中获取当前题IDconst 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/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"><link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/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/jquery@3.6.4/dist/jquery.min.js"></script><script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/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();}});// 获取选中的试题IDfunction 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>

在这里插入图片描述

五、自我感想

作为计算机专业学生,这次用飞算JavaAI开发在线考试系统,让我跳出“单纯写代码”的局限,真正体会到“软件开发是解决实际问题”的本质,收获远超课程设计本身:

1. 从“代码搬运工”到“需求解决者”的转变

最初开发时,我只关注“如何实现试题添加功能”,却忽略了教师“批量导入试题”“按难度筛选”等实际需求——这些细节不是技术决定的,而是教学场景驱动的。飞算JavaAI在生成代码时,会通过注释提示“需支持Excel批量导入(教师常用)”“需添加试题难度字段(组卷时筛选)”,帮我跳出“纯技术思维”。比如客观题自动批改功能,我原本只简单比对答案,后来根据飞算的提示添加“多选题选项排序比对”(避免学生因选项顺序不同被判错),这正是教师批改时的真实痛点。

2. 飞算JavaAI让我聚焦“核心价值”

以前开发时,我要花1-2天写Entity、Mapper层的重复代码(比如每个实体的get/set、每个Mapper的CRUD方法),现在飞算20分钟就能生成规范的基础代码,还自带参数校验、事务控制和防重复提交逻辑。这让我有更多精力优化“用户体验”:比如为考试页面添加“自动保存答案”(每30秒保存一次,避免浏览器崩溃丢失答案)、为教师端添加“试题批量导入模板下载”(附带详细使用说明),这些小功能虽简单,却让系统更贴合师生使用习惯。我终于明白,AI工具不是“替代开发者”,而是帮我们把时间花在更有价值的“需求落地”上。

3. 解决问题的能力在实践中飞速提升

开发中遇到的“Excel批量导入失败”“考试计时偏差”等问题,让我对技术的理解从“会用”变成“能用好”。比如批量导入时,Excel中“题型”字段学生可能填“单选”而非“0”,我参考飞算生成的Excel解析代码,添加“中文转枚举”逻辑(“单选”→0、“多选”→1);再比如考试计时偏差,最初用前端定时器计时,切换标签页时会暂停,后来结合后端记录的“开始时间”计算剩余时间,从根本上解决问题。这些方法不是课本上的理论,而是实际开发中的“踩坑经验”。

六、开发总结与展望

1. 开发收获

通过这个在线考试系统的开发,我有以下几点收获:

(1)技术能力提升:掌握了Spring Boot+MyBatis-Plus的开发流程,学会了使用Bootstrap实现响应式布局,对数据库设计和性能优化有了更深入的理解。

(2)解决问题能力:面对实际开发中的问题,学会了查阅文档、搜索解决方案,并通过调试逐步定位问题根源。飞算JavaAI生成的代码注释和优化建议帮我节省了很多时间。

(3)项目管理意识:学会了将一个复杂项目分解为多个模块,按优先级逐步实现,每周制定开发计划并检查进度,这种方法让我在有限时间内完成了所有核心功能。

2. 系统不足与优化方向

由于时间和技术水平限制,系统还有一些可以优化的地方:

(1)防作弊功能可以更完善:目前只实现了简单的切屏检测,未来可以添加摄像头监控、禁止复制粘贴、随机调整题目顺序等功能。

(2)添加智能组卷算法:根据知识点分布和难度系数自动生成更科学的试卷,而不仅仅是随机抽取。

(3)实现大数据分析:对学生答题数据进行分析,找出易错知识点,为教学提供参考。

(4)支持更多题型:如填空题、编程题(可在线编译运行)等。

3. 给其他学生开发者的建议

(1)善用开发工具:飞算JavaAI这类工具能帮我们生成基础代码,避免重复劳动,但核心逻辑还是要自己思考和编写,不能完全依赖工具。

(2)多动手实践:看教程和文档只能掌握理论,真正的进步来自于实际开发,遇到问题不要怕,解决问题的过程就是成长的过程。

(3)学会拆解问题:复杂系统往往让人望而却步,但只要分解成一个个小模块,逐个实现,就能逐步构建出完整的系统。

(4)重视代码规范:即使是课程设计,也要养成良好的编码习惯,写注释、用有意义的变量名、遵循设计模式,这些都会让后续维护变得轻松。

这次在线考试系统的开发让我深刻体会到,从0到1构建一个实用的系统虽然有挑战,但也充满乐趣和成就感。作为学生,我们不必追求完美,重要的是在实践中学习和成长。希望我的开发记录能给其他同学带来一些启发,祝大家都能顺利完成课程设计!

http://www.dtcms.com/a/389511.html

相关文章:

  • Vue3 基础语法详解:从入门到实践
  • 大白话聊明白:同步刷盘、异步刷盘以及RocketMQ和RabbitMQ的刷盘策略
  • I0流学习
  • 摄影灯MCU方案开发,摄影灯单片机分析
  • Salesforce知识点: LWC 组件通信全解析
  • Lua语言程序设计3:闭包、模式匹配、日期和时间
  • Freertos系列教学(删除函数的使用)
  • DevOps平台建设 - 总体设计文档的核心架构与关键技术实践
  • 系统中间件与云虚拟化-云数据库与数据库访问中间件ORM框架-Sannic-非实验
  • DTC BluSDR™系列-满足您所有的无人机通信需求
  • 【猛犸AI科技】深度强化学习SCI/EI/CCF/中文核心一站式辅导
  • 美创科技闪耀亚洲教育装备博览会,以数据安全护航教育数字化
  • 1.css的几种定位方式
  • 【C#】对比两个坐标点是否相同的多种方法
  • Ubuntu之旅-03 InfluxDB
  • IEEE出版,稳定检索!|2025年智能制造、机器人与自动化国际学术会议 (IMRA 2025)
  • iOS 上架流程详细指南 苹果应用发布步骤、ipa 文件上传 打包上架实战经验
  • MessageBus 通信组件库
  • 性能测试-jmeter12-万能插件包管理器jmeter-plugins
  • 工地项目管理系统有什么强大功能?工程企业实现数字化的步骤
  • 【开题答辩全过程】以 “萌崽”宠物社交小程序为例,包含答辩的问题和答案
  • Spring Cloud Alibaba微服务架构深度解析:基于Nacos、Gateway、OpenFeign与Sentinel的现代化实践
  • 大模型-Attention面试
  • Hadoop3.3.5搭建指南(简约版)
  • Python运算符与表达式
  • “双碳”目标下,塔能科技如何用“物联网精准节能”重塑城市能源生态?
  • 格恩朗气体涡轮流量计:精准计量每一方气,守护能源高效利用
  • 从感知机到多层感知机:深度学习入门核心知识解析
  • 从Java ArrayList 学习泛型设计
  • 【Amber报错1】 Amber/Miniconda 与系统 Bash 的 libtinfo.so.6冲突