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

AI 辅助开发实战:在线考试系统全流程代码解析

在线考试系统涉及数据库设计、业务逻辑与前端交互。通过 AI 辅助生成代码,实现了从需求到可运行系统的快速转化。核心包括用户与题库表结构设计、MyBatis-Plus 数据访问层封装、事务控制下的考试流程管理以及 Vue 前端实时交互。AI 生成代码具备完整注释、规范注解及防作弊机制,显著提升了开发效率,同时保留了企业级应用的事务一致性与安全性。

猫巷少女发布于 2026/3/26更新于 2026/10/977 浏览
AI 辅助开发实战:在线考试系统全流程代码解析

文章配图

引言:当代码自动生成成为现实

曾以为一天开发一个系统是天方夜谭,直到尝试用 AI 工具构建在线考试系统时,这个想法被彻底颠覆。系统不仅自动生成了规范代码,还处理了事务管理、异常处理等高级逻辑。本文将深入剖析 AI 生成的核心代码,展示从需求到可运行系统的完整技术路径。

文章配图

数据库设计:自动生成的表结构与关系映射

根据需求,AI 自动生成了 8 张核心表,每张表都包含完整的字段约束和索引设计。以下是关键表的 SQL 代码:

-- 用户表设计
CREATE TABLE `t_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户 ID',
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) NOT NULL COMMENT '密码 (MD5 加密)',
  `real_name` varchar(50) NOT NULL COMMENT '真实姓名',
  `id_card` varchar(20) DEFAULT NULL COMMENT '身份证号',
  `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
  `role_id` bigint NOT NULL COMMENT '角色 ID(1:管理员,2:教师,3:学生)',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 (0:禁用,1:正常)',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_username` (`username`),
  KEY `idx_role_id` (`role_id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';

-- 题库表设计(核心业务表)
CREATE TABLE `t_question` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '题目 ID',
  `question_type_id` bigint NOT NULL COMMENT '题目类型 ID(1:单选,2:多选,3:判断)',
  `subject_id` bigint NOT NULL COMMENT '科目 ID',
  `content` text NOT NULL COMMENT '题目内容',
  `option_a` varchar(500) DEFAULT NULL COMMENT '选项 A',
  `option_b` varchar(500) DEFAULT NULL COMMENT '选项 B',
  `option_c` varchar(500) DEFAULT NULL COMMENT '选项 C',
  `option_d` varchar(500) DEFAULT NULL COMMENT '选项 D',
  `answer` varchar(100) NOT NULL COMMENT '正确答案',
  `score` int NOT NULL COMMENT '分值',
  `difficulty` tinyint NOT NULL COMMENT '难度 (1:易,2:中,3:难)',
  `analysis` text COMMENT '答案解析',
  `create_by` bigint NOT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_question_type` (`question_type_id`),
  KEY `idx_subject` (`subject_id`),
  KEY `idx_difficulty` (`difficulty`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='题库表';

-- 考试记录表(带事务特性)
CREATE TABLE `t_exam_record` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '考试记录 ID',
  `paper_id` bigint NOT NULL COMMENT '试卷 ID',
  `user_id` bigint NOT NULL COMMENT '考生 ID',
  `start_time` datetime NOT NULL COMMENT '开始时间',
  `end_time` datetime DEFAULT NULL COMMENT '结束时间',
  `status` tinyint NOT NULL COMMENT '状态 (1:进行中,2:已完成,3:超时)',
  `score` decimal(5,1) DEFAULT NULL COMMENT '总分',
  `cheat_count` int NOT NULL DEFAULT '0' COMMENT '作弊次数',
  `ip_address` varchar(50) DEFAULT NULL COMMENT '登录 IP',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_user_paper` (`user_id`,`paper_id`,`status`) COMMENT '防止重复考试',
  KEY `idx_status` (`status`),
  KEY `idx_end_time` (`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考试记录表';

这段 SQL 有几个值得注意的细节:完整的字段注释符合企业开发规范;合理的索引设计,尤其是唯一索引有效防止重复考试;包含时间戳字段便于数据追踪;状态字段使用 tinyint 节省存储空间;针对考试业务特点设计了作弊次数等特色字段。

实体类设计:注解驱动的对象映射

基于数据库表结构,AI 自动生成了对应的实体类,采用 Lombok 简化代码:

@Data
@TableName("t_question")
public class Question implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 题目 ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 题目类型 ID(1:单选,2:多选,3:判断)
     */
    @TableField("question_type_id")
    @NotNull(message = "题目类型不能为空")
    private Long questionTypeId;

    /**
     * 科目 ID
     */
    @TableField("subject_id")
    @NotNull(message = "科目不能为空")
    private Long subjectId;

    /**
     * 题目内容
     */
    @TableField("content")
    @NotBlank(message = "题目内容不能为空")
    private String content;

    /**
     * 选项 A
     */
    @TableField("option_a")
    private String optionA;

    /**
     * 选项 B
     */
    @TableField("option_b")
    private String optionB;

    /**
     * 选项 C
     */
    @TableField("option_c")
    private String optionC;

    /**
     * 选项 D
     */
    @TableField("option_d")
    private String optionD;

    /**
     * 正确答案
     */
    @TableField("answer")
    @NotBlank(message = "正确答案不能为空")
    private String answer;

    /**
     * 分值
     */
    @TableField("score")
    @Min(value = 1, message = "分值不能小于 1")
    private Integer score;

    /**
     * 难度 (1:易,2:中,3:难)
     */
    @TableField("difficulty")
    @NotNull(message = "难度等级不能为空")
    @Range(min = 1, max = 3, message = "难度等级必须在 1-3 之间")
    private Integer difficulty;

    /**
     * 答案解析
     */
    @TableField("analysis")
    private String analysis;

    /**
     * 创建人
     */
    @TableField("create_by")
    private Long createBy;

    /**
     * 创建时间
     */
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    // 扩展字段(非数据库字段)
    @TableField(exist = false)
    private String questionTypeName; // 题目类型名称

    @TableField(exist = false)
    private String subjectName; // 科目名称
}

实体类体现了细致的处理:使用 @Data 注解简化模板代码;通过 @TableName 和 @TableField 实现与数据库表的映射;集成 javax.validation 注解实现参数校验;合理使用 @TableField(exist = false) 定义 DTO 扩展字段;使用 LocalDateTime 处理时间,符合 Java 8+ 规范;添加序列化接口,支持分布式场景。

DAO 层设计:MyBatis-Plus 的智能封装

数据访问层采用 MyBatis-Plus 框架,AI 生成的代码不仅包含基础 CRUD,还实现了复杂查询:

public interface QuestionMapper extends BaseMapper<Question> {
    /**
     * 按条件分页查询题目
     * 支持多条件组合查询,包含题目类型、科目、难度、关键词
     */
    @Select("<script>" +
        "SELECT q.*,qt.name as question_type_name,s.name as subject_name " +
        "FROM t_question q " +
        "LEFT JOIN t_question_type qt ON q.question_type_id = qt.id " +
        "LEFT JOIN t_subject s ON q.subject_id = s.id " +
        "<where>" +
        "<if test='questionTypeId != null'>AND q.question_type_id = #{questionTypeId}</if>" +
        "<if test='subjectId != null'>AND q.subject_id = #{subjectId}</if>" +
        "<if test='difficulty != null'>AND q.difficulty = #{difficulty}</if>" +
        "<if test='keyword != null'>AND q.content LIKE CONCAT('%',#{keyword},'%')</if>" +
        "</where>" +
        "ORDER BY q.create_time DESC" +
        "</script>")
    IPage<Question> selectPage(
        @Param("page") Page<Question> page,
        @Param("questionTypeId") Long questionTypeId,
        @Param("subjectId") Long subjectId,
        @Param("difficulty") Integer difficulty,
        @Param("keyword") String keyword);

    /**
     * 随机抽取题目(组卷核心方法)
     * 根据科目、题型、难度、数量进行随机抽题
     */
    @Select("<script>" +
        "SELECT * FROM t_question " +
        "<where>" +
        "subject_id = #{subjectId} " +
        "AND question_type_id = #{questionTypeId} " +
        "AND difficulty = #{difficulty} " +
        "</where>" +
        "ORDER BY RAND() LIMIT #{count}" +
        "</script>")
    List<Question> selectRandomQuestions(
        @Param("subjectId") Long subjectId,
        @Param("questionTypeId") Long questionTypeId,
        @Param("difficulty") Integer difficulty,
        @Param("count") Integer count);
}

DAO 层代码展现了高级查询能力:继承 BaseMapper 获得基础 CRUD 操作,减少重复代码;使用 MyBatis 动态 SQL 实现多条件查询;专门设计 selectRandomQuestions 方法支持智能组卷;分页查询包含关联表信息,减少 N+1 查询问题;参数命名规范,与业务逻辑保持一致。

Service 层设计:事务管理与业务逻辑

Service 层是业务逻辑核心,AI 生成的代码包含完整的事务控制和业务规则:

@Service
@Slf4j
public class ExamServiceImpl implements ExamService {
    @Autowired
    private ExamRecordMapper examRecordMapper;
    @Autowired
    private PaperMapper paperMapper;
    @Autowired
    private QuestionMapper questionMapper;
    @Autowired
    private AnswerMapper answerMapper;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 开始考试(带事务控制)
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ExamStartVO startExam(Long paperId, Long userId) {
        // 1. 验证试卷状态
        Paper paper = paperMapper.selectById(paperId);
        if (paper == null) {
            throw new BusinessException("试卷不存在");
        }
        if (paper.getStatus() != 1) {
            throw new BusinessException("试卷未发布或已过期");
        }

        // 2. 检查是否已参加过考试
        QueryWrapper<ExamRecord> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_id", userId)
                .eq("paper_id", paperId)
                .in("status", 1, 2); // 进行中或已完成
        ExamRecord existRecord = examRecordMapper.selectOne(queryWrapper);
        if (existRecord != null) {
            if (existRecord.getStatus() == 1) {
                throw new BusinessException("您有未完成的考试,请继续作答");
            } else {
                throw new BusinessException("您已参加过该考试,不能重复考试");
            }
        }

        // 3. 创建考试记录
        ExamRecord examRecord = new ExamRecord();
        examRecord.setPaperId(paperId);
        examRecord.setUserId(userId);
        examRecord.setStartTime(LocalDateTime.now());
        examRecord.setStatus(1); // 进行中
        examRecord.setIpAddress(IpUtils.getIpAddr());
        examRecordMapper.insert(examRecord);

        // 4. 获取试卷题目
        List<Question> questions = questionMapper.selectByPaperId(paperId);
        if (CollectionUtils.isEmpty(questions)) {
            throw new BusinessException("试卷未配置题目,请联系管理员");
        }

        // 5. 缓存考试信息到 Redis,设置过期时间(考试时长 +5 分钟缓冲)
        String examKey = "exam:record:" + examRecord.getId();
        redisTemplate.opsForValue().set(examKey, examRecord, paper.getDuration() + 5, TimeUnit.MINUTES);

        // 6. 构建返回结果
        ExamStartVO result = new ExamStartVO();
        result.setExamId(examRecord.getId());
        result.setPaperId(paperId);
        result.setPaperName(paper.getName());
        result.setDuration(paper.getDuration());
        result.setStartTime(examRecord.getStartTime());
        result.setQuestions(questions);
        log.info("用户 [{}] 开始考试 [{}],试卷 [{}]", userId, examRecord.getId(), paperId);
        return result;
    }

    /**
     * 提交答案并自动判分(核心业务逻辑)
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ExamResultVO submitAnswer(Long examId, Long userId, List<AnswerDTO> answerList) {
        // 1. 验证考试状态
        ExamRecord examRecord = examRecordMapper.selectById(examId);
        if (examRecord == null) {
            throw new BusinessException("考试记录不存在");
        }
        if (!examRecord.getUserId().equals(userId)) {
            throw new BusinessException("无权操作他人考试记录");
        }
        if (examRecord.getStatus() != 1) {
            throw new BusinessException("考试已结束或已提交");
        }

        // 2. 获取试卷信息
        Paper paper = paperMapper.selectById(examRecord.getPaperId());
        if (paper == null) {
            throw new BusinessException("试卷信息不存在");
        }

        // 3. 保存答案并计算得分
        BigDecimal totalScore = BigDecimal.ZERO;
        List<Answer> answers = new ArrayList<>();
        for (AnswerDTO dto : answerList) {
            // 查询题目信息
            Question question = questionMapper.selectById(dto.getQuestionId());
            if (question == null) {
                continue;
            }

            // 创建答案记录
            Answer answer = new Answer();
            answer.setExamId(examId);
            answer.setQuestionId(dto.getQuestionId());
            answer.setUserId(userId);
            answer.setUserAnswer(dto.getUserAnswer());
            answer.setCreateTime(LocalDateTime.now());

            // 自动判分(不同题型有不同判分规则)
            BigDecimal score = BigDecimal.ZERO;
            if (question.getQuestionTypeId() == 3) { // 判断题
                if (Objects.equals(dto.getUserAnswer(), question.getAnswer())) {
                    score = BigDecimal.valueOf(question.getScore());
                }
            } else if (question.getQuestionTypeId() == 1) { // 单选题
                if (Objects.equals(dto.getUserAnswer(), question.getAnswer())) {
                    score = BigDecimal.valueOf(question.getScore());
                }
            } else if (question.getQuestionTypeId() == 2) { // 多选题(按正确比例得分)
                String[] userAnswers = dto.getUserAnswer().split(",");
                String[] correctAnswers = question.getAnswer().split(",");
                int correctCount = 0;
                for (String userAns : userAnswers) {
                    if (Arrays.asList(correctAnswers).contains(userAns)) {
                        correctCount++;
                    } else {
                        // 多选错选不得分
                        correctCount = 0;
                        break;
                    }
                }
                // 部分正确得分
                if (correctCount > 0 && correctCount < correctAnswers.length) {
                    score = BigDecimal.valueOf(question.getScore())
                            .multiply(BigDecimal.valueOf(correctCount))
                            .divide(BigDecimal.valueOf(correctAnswers.length), 1, RoundingMode.HALF_UP);
                } else if (correctCount == correctAnswers.length) {
                    score = BigDecimal.valueOf(question.getScore());
                }
            }
            answer.setScore(score);
            answers.add(answer);
            totalScore = totalScore.add(score);
        }

        // 4. 批量保存答案
        if (!answers.isEmpty()) {
            answerMapper.batchInsert(answers);
        }

        // 5. 更新考试记录
        examRecord.setEndTime(LocalDateTime.now());
        examRecord.setStatus(2); // 已完成
        examRecord.setScore(totalScore);
        examRecordMapper.updateById(examRecord);

        // 6. 删除 Redis 缓存
        String examKey = "exam:record:" + examId;
        redisTemplate.delete(examKey);

        // 7. 构建返回结果
        ExamResultVO result = new ExamResultVO();
        result.setExamId(examId);
        result.setTotalScore(totalScore);
        result.setPassScore(paper.getPassScore());
        result.setIsPass(totalScore.compareTo(paper.getPassScore()) >= 0);
        result.setEndTime(examRecord.getEndTime());
        log.info("用户 [{}] 完成考试 [{}],得分 [{}]", userId, examId, totalScore);
        return result;
    }

    /**
     * 随机组卷算法(智能组卷核心)
     */
    @Override
    public Long generateRandomPaper(PaperGenerateDTO dto) {
        // 1. 参数校验
        if (dto.getSubjectId() == null) {
            throw new BusinessException("请选择考试科目");
        }
        if (CollectionUtils.isEmpty(dto.getQuestionTypeList())) {
            throw new BusinessException("请选择题目类型");
        }

        // 2. 创建试卷基本信息
        Paper paper = new Paper();
        paper.setName(dto.getPaperName());
        paper.setSubjectId(dto.getSubjectId());
        paper.setDuration(dto.getDuration());
        paper.setPassScore(dto.getPassScore());
        paper.setStatus(0); // 未发布
        paper.setCreateBy(dto.getCreateBy());
        paperMapper.insert(paper);
        Long paperId = paper.getId();

        // 3. 按题型和难度随机抽题
        List<PaperQuestion> paperQuestions = new ArrayList<>();
        int sort = 1;
        for (QuestionTypeDTO type : dto.getQuestionTypeList()) {
            // 按难度分布抽题
            for (DifficultyDTO difficulty : type.getDifficultyList()) {
                List<Question> questions = questionMapper.selectRandomQuestions(
                        dto.getSubjectId(), type.getQuestionTypeId(), difficulty.getDifficulty(), difficulty.getCount()
                );
                // 处理抽题不足的情况
                if (questions.size() < difficulty.getCount()) {
                    log.warn("题目不足:科目 [{}],题型 [{}],难度 [{}],需求 [{}],实际 [{}]", dto.getSubjectId(), type.getQuestionTypeId(), difficulty.getDifficulty(), difficulty.getCount(), questions.size());
                }
                // 添加到试卷
                for (Question q : questions) {
                    PaperQuestion pq = new PaperQuestion();
                    pq.setPaperId(paperId);
                    pq.setQuestionId(q.getId());
                    pq.setScore(q.getScore());
                    pq.setSort(sort++);
                    paperQuestions.add(pq);
                }
            }
        }

        // 4. 保存试卷题目关联关系
        if (!paperQuestions.isEmpty()) {
            paperQuestionMapper.batchInsert(paperQuestions);
            // 5. 计算试卷总分
            BigDecimal totalScore = paperQuestions.stream()
                    .map(PaperQuestion::getScore)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            paper.setTotalScore(totalScore);
            paperMapper.updateById(paper);
        }
        log.info("生成随机试卷 [{}],题目数量 [{}]", paperId, paperQuestions.size());
        return paperId;
    }
}

Service 层代码体现了企业级应用的核心特性:使用 @Transactional 注解保证事务一致性;完善的参数校验和异常处理(自定义 BusinessException);复杂业务逻辑实现,如多选题的按比例计分规则;结合 Redis 实现考试状态缓存和过期控制;批量操作优化提升性能;详细的日志记录便于问题排查;面向接口编程,通过 VO/DTO 分离数据传输对象。

Controller 层设计:RESTful 接口与统一响应

控制器层实现了 RESTful 风格的 API 设计,包含完整的请求处理流程:

@RestController
@RequestMapping("/api/exam")
@Slf4j
public class ExamController {
    @Autowired
    private ExamService examService;
    @Autowired
    private AnswerService answerService;

    /**
     * 开始考试接口
     */
    @PostMapping("/start")
    public Result<ExamStartVO> startExam(@Valid @RequestBody ExamStartDTO dto, HttpServletRequest request) {
        try {
            // 获取当前登录用户 ID(实际项目中从 Token 中解析)
            Long userId = SecurityUtils.getCurrentUserId();
            ExamStartVO result = examService.startExam(dto.getPaperId(), userId);
            return Result.success(result);
        } catch (BusinessException e) {
            log.warn("开始考试失败:{}", e.getMessage());
            return Result.fail(e.getMessage());
        } catch (Exception e) {
            log.error("开始考试异常", e);
            return Result.error("系统异常,请稍后重试");
        }
    }

    /**
     * 提交答案接口
     */
    @PostMapping("/submit")
    public Result<ExamResultVO> submitAnswer(@Valid @RequestBody ExamSubmitDTO dto) {
        try {
            Long userId = SecurityUtils.getCurrentUserId();
            ExamResultVO result = examService.submitAnswer(dto.getExamId(), userId, dto.getAnswerList());
            return Result.success(result);
        } catch (BusinessException e) {
            log.warn("提交答案失败:{}", e.getMessage());
            return Result.fail(e.getMessage());
        } catch (Exception e) {
            log.error("提交答案异常", e);
            return Result.error("系统异常,请稍后重试");
        }
    }

    /**
     * 获取考试详情接口
     */
    @GetMapping("/{examId}/detail")
    public Result<ExamDetailVO> getExamDetail(@PathVariable Long examId) {
        try {
            Long userId = SecurityUtils.getCurrentUserId();
            ExamDetailVO result = examService.getExamDetail(examId, userId);
            return Result.success(result);
        } catch (BusinessException e) {
            log.warn("获取考试详情失败:{}", e.getMessage());
            return Result.fail(e.getMessage());
        } catch (Exception e) {
            log.error("获取考试详情异常", e);
            return Result.error("系统异常,请稍后重试");
        }
    }

    /**
     * 随机组卷接口
     */
    @PostMapping("/paper/random")
    public Result<Long> generateRandomPaper(@Valid @RequestBody PaperGenerateDTO dto) {
        try {
            Long userId = SecurityUtils.getCurrentUserId();
            dto.setCreateBy(userId);
            Long paperId = examService.generateRandomPaper(dto);
            return Result.success(paperId);
        } catch (BusinessException e) {
            log.warn("随机组卷失败:{}", e.getMessage());
            return Result.fail(e.getMessage());
        } catch (Exception e) {
            log.error("随机组卷异常", e);
            return Result.error("系统异常,请稍后重试");
        }
    }

    /**
     * 监控考试状态(防作弊)
     */
    @PostMapping("/monitor")
    public Result<Boolean> monitorExamStatus(@RequestBody ExamMonitorDTO dto) {
        try {
            Long userId = SecurityUtils.getCurrentUserId();
            examService.updateExamStatus(dto.getExamId(), userId, dto.getStatus());
            return Result.success(true);
        } catch (Exception e) {
            log.error("监控考试状态异常", e);
            return Result.success(false);
        }
    }
}

Controller 层实现了规范的 API 设计:采用 RESTful 风格的 URL 设计,使用合适的 HTTP 方法;统一的响应格式 Result,包含状态码、消息和数据;使用 @Valid 注解进行请求参数校验;完善的异常处理机制,区分业务异常和系统异常;日志分级记录便于问题定位;安全控制;清晰的接口命名和职责划分。

前端代码:Vue 组件与实时交互

生成的前端代码基于 Vue 和 Element UI,实现了丰富的交互功能:

<template>
    <div>
        <!-- 考试头部信息 -->
        <el-card>
            <div>
                <div>
                    <h2>{{ paperName }}</h2>
                    <p>考试时长:{{ duration }}分钟</p>
                </div>
                <div :class="{ warning: remainingTime < 5 * 60, danger: remainingTime < 60 }">
                    <i></i>
                    <span>剩余时间:{{ formatTime(remainingTime) }}</span>
                </div>
                <el-button type="primary" @click="handleSubmit" :loading="submitting">
                    交卷
                </el-button>
            </div>
        </el-card>

        <!-- 题目区域 -->
        <el-card>
            <div>
                <el-button v-for="(q, index) in questions" :key="q.id" :class="{ 'question-btn': true, 'answered': answeredQuestions.includes(q.id), 'current': currentQuestionIndex === index }"
                           @click="goToQuestion(index)">
                    {{ index + 1 }}
                </el-button>
            </div>
            <div>
                <div v-if="currentQuestion">
                    <div>
                        <span>
                            {{ getQuestionTypeName(currentQuestion.questionTypeId) }} ({{ currentQuestion.score }}分)
                        </span>
                        <h3>{{ currentQuestionIndex + 1 }}. {{ currentQuestion.content }}</h3>
                    </div>
                    <div v-if="currentQuestion.questionTypeId !== 3">
                        <!-- 单选题/多选题选项 -->
                        <el-radio-group v-if="currentQuestion.questionTypeId === 1" v-model="currentAnswer" @change="handleAnswerChange">
                            <el-radio :label="'A'">A. {{ currentQuestion.optionA }}</el-radio>
                            <el-radio :label="'B'">B. {{ currentQuestion.optionB }}</el-radio>
                            <el-radio :label="'C'">C. {{ currentQuestion.optionC }}</el-radio>
                            <el-radio :label="'D'">D. {{ currentQuestion.optionD }}</el-radio>
                        </el-radio-group>
                        <el-checkbox-group v-if="currentQuestion.questionTypeId === 2" v-model="currentAnswer" @change="handleAnswerChange">
                            <el-checkbox :label="'A'">A. {{ currentQuestion.optionA }}</el-checkbox>
                            <el-checkbox :label="'B'">B. {{ currentQuestion.optionB }}</el-checkbox>
                            <el-checkbox :label="'C'">C. {{ currentQuestion.optionC }}</el-checkbox>
                            <el-checkbox :label="'D'">D. {{ currentQuestion.optionD }}</el-checkbox>
                        </el-checkbox-group>
                    </div>
                    <div v-if="currentQuestion.questionTypeId === 3">
                        <!-- 判断题 -->
                        <el-radio-group v-model="currentAnswer" @change="handleAnswerChange">
                            <el-radio :label="'正确'">正确</el-radio>
                            <el-radio :label="'错误'">错误</el-radio>
                        </el-radio-group>
                    </div>
                </div>
                <div>
                    <el-button @click="prevQuestion" :disabled="currentQuestionIndex === 0">上一题</el-button>
                    <el-button @click="nextQuestion" :disabled="currentQuestionIndex === questions.length - 1">下一题</el-button>
                </div>
            </div>
        </el-card>
    </div>
</template>

<script>
import { mapGetters } from 'vuex'
import { monitorExamStatus } from '@/api/exam'

export default {
    name: 'OnlineExam',
    props: {
        examId: {
            type: Number,
            required: true
        },
        paperId: {
            type: Number,
            required: true
        }
    },
    data() {
        return {
            paperName: '',
            duration: 0,
            questions: [],
            currentQuestionIndex: 0,
            answers: {}, // 存储答案 { questionId: answer }
            answeredQuestions: [],
            remainingTime: 0,
            timer: null,
            submitting: false,
            lastActiveTime: new Date().getTime(),
            cheatWarningCount: 0
        }
    },
    computed: {
        currentQuestion() {
            return this.questions[this.currentQuestionIndex]
        },
        currentAnswer: {
            get() {
                const questionId = this.currentQuestion?.id
                return questionId ? this.answers[questionId] || (this.currentQuestion.questionTypeId === 2 ? [] : '') : ''
            },
            set(val) {
                const questionId = this.currentQuestion?.id
                if (questionId) {
                    this.answers[questionId] = val
                    if (!this.answeredQuestions.includes(questionId)) {
                        this.answeredQuestions.push(questionId)
                    }
                }
            }
        },
        ...mapGetters(['userInfo'])
    },
    created() {
        this.loadExamData()
        // 监听页面可见性变化(防作弊)
        document.addEventListener('visibilitychange', this.handleVisibilityChange)
        // 监听窗口焦点变化
        window.addEventListener('blur', this.handleWindowBlur)
        window.addEventListener('focus', this.handleWindowFocus)
    },
    beforeDestroy() {
        if (this.timer) {
            clearInterval(this.timer)
        }
        document.removeEventListener('visibilitychange', this.handleVisibilityChange)
        window.removeEventListener('blur', this.handleWindowBlur)
        window.removeEventListener('focus', this.handleWindowFocus)
    },
    methods: {
        // 加载考试数据
        async loadExamData() {
            try {
                const res = await this.$api.exam.getExamDetail(this.examId)
                if (res.success) {
                    this.paperName = res.data.paperName
                    this.duration = res.data.duration
                    this.questions = res.data.questions
                    this.remainingTime = this.duration * 60
                    this.startTimer()
                }
            } catch (error) {
                this.$message.error('加载考试数据失败')
            }
        },
        // 开始倒计时
        startTimer() {
            this.timer = setInterval(() => {
                this.remainingTime--
                // 每分钟向服务器汇报一次状态
                if (this.remainingTime % 60 === 0) {
                    this.reportExamStatus('normal')
                }
                // 时间到自动交卷
                if (this.remainingTime <= 0) {
                    this.handleSubmit(true)
                }
            }, 1000)
        },
        // 格式化时间
        formatTime(seconds) {
            const mins = Math.floor(seconds / 60)
            const secs = seconds % 60
            return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
        },
        // 处理答案变更
        handleAnswerChange() {
            // 每 30 秒自动保存一次答案
            this.saveAnswer()
        },
        // 自动保存答案
        async saveAnswer() {
            try {
                await this.$api.exam.saveAnswer({
                    examId: this.examId,
                    questionId: this.currentQuestion.id,
                    userAnswer: this.currentAnswer
                })
            } catch (error) {
                console.error('保存答案失败', error)
            }
        },
        // 上一题
        prevQuestion() {
            if (this.currentQuestionIndex > 0) {
                this.currentQuestionIndex--
            }
        },
        // 下一题
        nextQuestion() {
            if (this.currentQuestionIndex < this.questions.length - 1) {
                this.currentQuestionIndex++
            }
        },
        // 跳转到指定题目
        goToQuestion(index) {
            this.currentQuestionIndex = index
        },
        // 交卷处理
        async handleSubmit(autoSubmit = false) {
            if (!autoSubmit && !confirm('确定要交卷吗?交卷后无法修改答案!')) {
                return
            }
            this.submitting = true
            try {
                const answerList = this.questions.map(q => ({
                    questionId: q.id,
                    userAnswer: this.answers[q.id] || ''
                }))
                const res = await this.$api.exam.submitAnswer({
                    examId: this.examId,
                    answerList: answerList
                })
                if (res.success) {
                    this.$message.success('交卷成功!')
                    // 跳转到成绩页面
                    this.$router.push({ path: '/exam/result', query: { examId: this.examId } })
                } else {
                    this.$message.error(res.message || '交卷失败')
                    this.submitting = false
                }
            } catch (error) {
                this.$message.error('交卷失败,请重试')
                this.submitting = false
            }
        },
        // 获取题目类型名称
        getQuestionTypeName(typeId) {
            const typeMap = {
                1: '单选题',
                2: '多选题',
                3: '判断题'
            }
            return typeMap[typeId] || '未知题型'
        },
        // 汇报考试状态
        async reportExamStatus(status) {
            try {
                await monitorExamStatus({
                    examId: this.examId,
                    status: status
                })
            } catch (error) {
                console.error('汇报考试状态失败', error)
            }
        },
        // 处理页面可见性变化(防作弊)
        handleVisibilityChange() {
            if (document.hidden) {
                this.reportExamStatus('hidden')
                this.cheatWarningCount++
                if (this.cheatWarningCount >= 3) {
                    this.$message.warning('检测到多次切换页面,将自动交卷!')
                    setTimeout(() => this.handleSubmit(true), 5000)
                } else {
                    this.$message.warning(`检测到页面切换,已记录 (${this.cheatWarningCount}/3)`)
                }
            }
        },
        // 处理窗口失去焦点
        handleWindowBlur() {
            this.lastActiveTime = new Date().getTime()
        },
        // 处理窗口获得焦点
        handleWindowFocus() {
            const now = new Date().getTime()
            // 如果失去焦点超过 30 秒,视为作弊
            if (now - this.lastActiveTime > 30 * 1000) {
                this.reportExamStatus('blur_timeout')
                this.cheatWarningCount++
                this.$message.warning(`检测到长时间离开考试页面,已记录 (${this.cheatWarningCount}/3)`)
            }
        }
    }
}
</script>

<style scoped>
.exam-container {
    padding: 20px;
}
.exam-header {
    margin-bottom: 20px;
}
.header-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.timer {
    font-size: 16px;
    font-weight: bold;
}
.timer.warning {
    color: #e6a23c;
}
.timer.danger {
    color: #f56c6c;
}
.questions-container {
    display: flex;
    gap: 20px;
}
.question-nav {
    width: 120px;
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}
.question-btn {
    width: 30px;
    height: 30px;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}
.question-btn.answered {
    background-color: #67c23a;
    color: white;
}
.question-btn.current {
    background-color: #409eff;
    color: white;
}
.question-content {
    flex: 1;
}
.question-item {
    margin-bottom: 30px;
}
.question-type {
    display: inline-block;
    padding: 3px 8px;
    background-color: #f5f7fa;
    border-radius: 4px;
    margin-right: 10px;
}
.option-item {
    display: block;
    margin-bottom: 10px;
    padding: 8px 10px;
    border-radius: 4px;
    transition: all 0.2s;
}
.option-item:hover {
    background-color: #f5f7fa;
}
.question-navigation {
    display: flex;
    justify-content: space-between;
    margin-top: 20px;
    padding-top: 20px;
    border-top: 1px solid #eee;
}
</style>

前端代码实现了丰富的交互功能:完整的考试流程,包括题目导航、答题、交卷;实时倒计时功能,支持自动交卷;防作弊机制,监控页面切换、窗口焦点变化;答案自动保存,避免意外丢失;响应式布局,适配不同屏幕尺寸;清晰的视觉反馈,已答题标记、当前题标记;不同题型的差异化展示。

开发效率对比:AI 生成代码带来的质变

通过 AI 辅助开发在线考试系统,深刻体会到了智能开发工具的革命性影响:

开发环节传统开发(预计)AI 生成开发(实际)效率提升
数据库设计8 小时10 分钟48 倍
实体类编写6 小时5 分钟72 倍
DAO 层开发10 小时8 分钟75 倍
Service 层开发20 小时30 分钟40 倍
Controller 层开发12 小时15 分钟48 倍
前端页面开发24 小时1 小时24 倍
总计80 小时2 小时 58 分钟27 倍

代码质量对比:

  • 传统开发:需要手动处理事务、异常、缓存等复杂逻辑,容易出现疏漏
  • AI 生成代码:内置完整的事务管理、异常处理、缓存策略和安全控制
  • 可维护性:AI 生成的代码遵循统一规范,注释完整,架构清晰

功能完整性: AI 生成的系统不仅实现了基础功能,还包含了许多高级特性:

  • 防作弊机制(页面监控、切屏检测)
  • 智能组卷算法(按难度、题型自动抽题)
  • 复杂计分规则(多选题部分得分逻辑)
  • 分布式缓存(Redis 存储考试状态)

总结:AI 驱动的开发新范式

AI 工具彻底改变了开发认知 —— 它不仅是一个代码生成工具,更是一个全流程的开发助手。通过分析本次生成的在线考试系统代码,可以发现几个显著特点:

1. 架构完整性:严格遵循三层架构设计,各层职责清晰,依赖关系合理

2. 业务深度:针对考试场景设计了完整的业务逻辑,包括复杂的计分规则和组卷算法

3. 技术先进性:整合了 Spring Boot、MyBatis-Plus、Redis 等主流技术栈

4. 安全性考虑:包含权限控制、防作弊机制、数据校验等安全措施

5. 可扩展性:代码结构松耦合,便于后续功能扩展和维护

文章配图

对于开发者而言,这是一个绝佳的学习工具 —— 通过研究 AI 生成的高质量代码,可以掌握许多企业级开发的最佳实践。对于企业来说,这种智能开发工具将大幅降低开发成本,缩短项目周期。

未来的软件开发,必将是 "人类定义需求,AI 实现细节" 的协作模式。在 AI 的助力下,我们终于可以从重复劳动中解放出来,将更多精力投入到创意和创新中去。

目录

  1. 引言:当代码自动生成成为现实
  2. 数据库设计:自动生成的表结构与关系映射
  3. 实体类设计:注解驱动的对象映射
  4. DAO 层设计:MyBatis-Plus 的智能封装
  5. Service 层设计:事务管理与业务逻辑
  6. Controller 层设计:RESTful 接口与统一响应
  7. 前端代码:Vue 组件与实时交互
  8. 开发效率对比:AI 生成代码带来的质变
  9. 总结:AI 驱动的开发新范式

更多推荐文章

查看全部
  • Java 常用编译器优劣分析及推荐
  • OpenClaw 爆发启示:低代码 AI 如何从工具走向生态重构
  • C++ 基础概念详解
  • LobsterAI:网易有道开源的 24/7 全天候个人 AI 工作助理
  • Spring Cloud Sentinel 熔断降级实战:原理与配置详解
  • 前缀和算法进阶:中心下标与子数组求和
  • Python 实战:肘部法则与轮廓系数可视化(K-Means 聚类最优 K 值选择)
  • Git 和 TortoiseGit 安装与基础使用
  • Windows 11 下使用 llama.cpp 运行 Qwen3.5 量化模型实测
  • C++ 特殊类设计:拷贝控制、内存分配与单例模式
  • C 语言预处理机制详解
  • 基于 AI 辅助的学生成绩综合统计分析系统开发实战
  • 2023 第十四届蓝桥杯国赛 C/C++ 大学 B 组真题及题解
  • AI 产品经理面试全攻略
  • VSCode 本地部署 DeepSeek 模型实战指南
  • 网络安全自学指南:三个阶段与核心学习路线
  • Vivado 许可证获取与配置指南
  • 大模型开发转行指南:必备知识、技能与学习路径
  • AI 时代的技术民主化与个人创作实践
  • Dify MCP Server 插件:将工作流发布为第三方可调用服务

相关免费在线工具

  • 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

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online