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='考试记录表';
代码解析:AI 生成的 SQL 具有以下特点:
- 完整的字段注释和表注释,符合企业开发规范
- 合理的索引设计,尤其是
idx_user_paper唯一索引有效防止重复考试 - 包含时间戳字段
create_time和update_time,便于数据追踪 - 状态字段使用 tinyint 类型,节省存储空间
- 针对考试业务特点设计了
cheat_count等特色字段
三、实体类设计:注解驱动的对象映射
基于数据库表结构,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;
Integer score;
Integer difficulty;
String analysis;
Long createBy;
LocalDateTime createTime;
LocalDateTime updateTime;
String questionTypeName;
String subjectName;
}
代码解析:实体类体现了 AI 的细致处理:
- 使用
@Data注解简化 getter/setter 等模板代码 - 通过
@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, );
examRecordMapper.selectOne(queryWrapper);
(existRecord != ) {
(existRecord.getStatus() == ) {
();
} {
();
}
}
();
examRecord.setPaperId(paperId);
examRecord.setUserId(userId);
examRecord.setStartTime(LocalDateTime.now());
examRecord.setStatus();
examRecord.setIpAddress(IpUtils.getIpAddr());
examRecordMapper.insert(examRecord);
List<Question> questions = questionMapper.selectByPaperId(paperId);
(CollectionUtils.isEmpty(questions)) {
();
}
+ examRecord.getId();
redisTemplate.opsForValue().set(examKey, examRecord, paper.getDuration() + , TimeUnit.MINUTES);
();
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);
result;
}
ExamResultVO {
examRecordMapper.selectById(examId);
(examRecord == ) {
();
}
(!examRecord.getUserId().equals(userId)) {
();
}
(examRecord.getStatus() != ) {
();
}
paperMapper.selectById(examRecord.getPaperId());
(paper == ) {
();
}
BigDecimal.ZERO;
List<Answer> answers = <>();
(AnswerDTO dto : answerList) {
questionMapper.selectById(dto.getQuestionId());
(question == ) {
;
}
();
answer.setExamId(examId);
answer.setQuestionId(dto.getQuestionId());
answer.setUserId(userId);
answer.setUserAnswer(dto.getUserAnswer());
answer.setCreateTime(LocalDateTime.now());
BigDecimal.ZERO;
(question.getQuestionTypeId() == ) {
(Objects.equals(dto.getUserAnswer(), question.getAnswer())) {
score = BigDecimal.valueOf(question.getScore());
}
} (question.getQuestionTypeId() == ) {
(Objects.equals(dto.getUserAnswer(), question.getAnswer())) {
score = BigDecimal.valueOf(question.getScore());
}
} (question.getQuestionTypeId() == ) {
String[] userAnswers = dto.getUserAnswer().split();
String[] correctAnswers = question.getAnswer().split();
;
(String userAns : userAnswers) {
(Arrays.asList(correctAnswers).contains(userAns)) {
correctCount++;
} {
correctCount = ;
;
}
}
(correctCount > && correctCount < correctAnswers.length) {
score = BigDecimal.valueOf(question.getScore())
.multiply(BigDecimal.valueOf(correctCount))
.divide(BigDecimal.valueOf(correctAnswers.length), , RoundingMode.HALF_UP);
} (correctCount == correctAnswers.length) {
score = BigDecimal.valueOf(question.getScore());
}
}
answer.setScore(score);
answers.add(answer);
totalScore = totalScore.add(score);
}
(!answers.isEmpty()) {
answerMapper.batchInsert(answers);
}
examRecord.setEndTime(LocalDateTime.now());
examRecord.setStatus();
examRecord.setScore(totalScore);
examRecordMapper.updateById(examRecord);
+ examId;
redisTemplate.delete(examKey);
();
result.setExamId(examId);
result.setTotalScore(totalScore);
result.setPassScore(paper.getPassScore());
result.setIsPass(totalScore.compareTo(paper.getPassScore()) >= );
result.setEndTime(examRecord.getEndTime());
log.info(, userId, examId, totalScore);
result;
}
Long {
(dto.getSubjectId() == ) {
();
}
(CollectionUtils.isEmpty(dto.getQuestionTypeList())) {
();
}
();
paper.setName(dto.getPaperName());
paper.setSubjectId(dto.getSubjectId());
paper.setDuration(dto.getDuration());
paper.setPassScore(dto.getPassScore());
paper.setStatus();
paper.setCreateBy(dto.getCreateBy());
paperMapper.insert(paper);
paper.getId();
List<PaperQuestion> paperQuestions = <>();
;
(QuestionTypeDTO type : dto.getQuestionTypeList()) {
(DifficultyDTO difficulty : type.getDifficultyList()) {
List<Question> questions = questionMapper.selectRandomQuestions(
dto.getSubjectId(), type.getQuestionTypeId(), difficulty.getDifficulty(), difficulty.getCount()
);
(questions.size() < difficulty.getCount()) {
log.warn(, dto.getSubjectId(), type.getQuestionTypeId(), difficulty.getDifficulty(), difficulty.getCount(), questions.size());
}
(Question q : questions) {
();
pq.setPaperId(paperId);
pq.setQuestionId(q.getId());
pq.setScore(q.getScore());
pq.setSort(sort++);
paperQuestions.add(pq);
}
}
}
(!paperQuestions.isEmpty()) {
paperQuestionMapper.batchInsert(paperQuestions);
paperQuestions.stream()
.map(PaperQuestion::getScore)
.reduce(BigDecimal.ZERO, BigDecimal::add);
paper.setTotalScore(totalScore);
paperMapper.updateById(paper);
}
log.info(, paperId, paperQuestions.size());
paperId;
}
}
代码解析:Service 层代码体现了企业级应用的核心特性:
- 使用
@Transactional注解保证事务一致性 - 完善的参数校验和异常处理(自定义
BusinessException) - 复杂业务逻辑实现,如多选题的按比例计分规则
- 结合 Redis 实现考试状态缓存和过期控制
- 批量操作优化(
batchInsert)提升性能 - 详细的日志记录便于问题排查
- 面向接口编程,通过 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 examService.submitAnswer(dto.getExamId(), userId, dto.getAnswerList());
Result.success(result);
} (BusinessException e) {
log.warn(, e.getMessage());
Result.fail(e.getMessage());
} (Exception e) {
log.error(, e);
Result.error();
}
}
Result<ExamDetailVO> {
{
SecurityUtils.getCurrentUserId();
examService.getExamDetail(examId, userId);
Result.success(result);
} (BusinessException e) {
log.warn(, e.getMessage());
Result.fail(e.getMessage());
} (Exception e) {
log.error(, e);
Result.error();
}
}
Result<Long> {
{
SecurityUtils.getCurrentUserId();
dto.setCreateBy(userId);
examService.generateRandomPaper(dto);
Result.success(paperId);
} (BusinessException e) {
log.warn(, e.getMessage());
Result.fail(e.getMessage());
} (Exception e) {
log.error(, e);
Result.error();
}
}
Result<Boolean> {
{
SecurityUtils.getCurrentUserId();
examService.updateExamStatus(dto.getExamId(), userId, dto.getStatus());
Result.success();
} (Exception e) {
log.error(, e);
Result.success();
}
}
}
代码解析:Controller 层实现了规范的 API 设计:
- 采用 RESTful 风格的 URL 设计,使用合适的 HTTP 方法
- 统一的响应格式
Result,包含状态码、消息和数据 - 使用
@Valid注解进行请求参数校验 - 完善的异常处理机制,区分业务异常和系统异常
- 日志分级记录(warn/error)便于问题定位
- 安全控制(
SecurityUtils.getCurrentUserId()) - 清晰的接口命名和职责划分
七、前端代码:Vue 组件与实时交互
AI 辅助开发工具生成的前端代码基于 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 实现细节" 的协作模式。AI 辅助开发工具让我看到了这种未来的可能性,也让我对自己的编程之路充满了新的期待。在 AI 的助力下,我们终于可以从重复劳动中解放出来,将更多精力投入到创意和创新中去。


