基于 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 唯一索引有效防止重复考试,cheat_count 字段为防作弊机制预留空间,时间戳字段便于数据追踪。
后端架构:实体、DAO 与 Service 层
实体类设计
基于数据库表结构,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 */
@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;
// ... 其他字段省略
}
这里可以看到 @Data 注解自动生成了 getter/setter,@TableField 处理字段映射,同时集成了 javax.validation 进行参数校验。扩展字段使用 @TableField(exist = false) 定义,避免污染数据库模型。
DAO 层封装
数据访问层采用 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);
}
动态 SQL 的使用让多条件查询变得灵活,selectRandomQuestions 方法直接支持智能组卷,减少了 N+1 查询问题。
Service 层业务逻辑
Service 层是核心,包含了事务控制和复杂的计分规则:
@Service
@Slf4j
public class ExamServiceImpl implements ExamService {
@Autowired
private ExamRecordMapper examRecordMapper;
@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 || 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) {
throw new BusinessException("您已参加过该考试,不能重复考试");
}
// 3. 创建考试记录并缓存至 Redis
ExamRecord examRecord = new ExamRecord();
examRecord.setPaperId(paperId);
examRecord.setUserId(userId);
examRecord.setStartTime(LocalDateTime.now());
examRecord.setStatus(1);
examRecordMapper.insert(examRecord);
String examKey = "exam:record:" + examRecord.getId();
redisTemplate.opsForValue().set(examKey, examRecord, paper.getDuration() + 5, TimeUnit.MINUTES);
return new ExamStartVO(examRecord.getId(), paper.getName(), questions);
}
/**
* 提交答案并自动判分
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ExamResultVO submitAnswer(Long examId, Long userId, List<AnswerDTO> answerList) {
// 1. 验证考试状态
ExamRecord examRecord = examRecordMapper.selectById(examId);
if (examRecord == null || !examRecord.getUserId().equals(userId)) {
throw new BusinessException("无权操作他人考试记录");
}
// 2. 计算得分
BigDecimal totalScore = BigDecimal.ZERO;
for (AnswerDTO dto : answerList) {
Question question = questionMapper.selectById(dto.getQuestionId());
BigDecimal score = calculateScore(question, dto.getUserAnswer());
totalScore = totalScore.add(score);
}
// 3. 更新记录
examRecord.setEndTime(LocalDateTime.now());
examRecord.setStatus(2);
examRecord.setScore(totalScore);
examRecordMapper.updateById(examRecord);
return new ExamResultVO(examId, totalScore, true);
}
private BigDecimal calculateScore(Question question, String userAnswer) {
// 判断题逻辑
if (question.getQuestionTypeId() == 3) {
return Objects.equals(userAnswer, question.getAnswer())
? BigDecimal.valueOf(question.getScore()) : BigDecimal.ZERO;
}
// 多选题逻辑(按比例得分)
if (question.getQuestionTypeId() == 2) {
String[] correctAnswers = question.getAnswer().split(",");
String[] userAnswers = userAnswer.split(",");
int correctCount = 0;
for (String ans : userAnswers) {
if (Arrays.asList(correctAnswers).contains(ans)) correctCount++;
else break; // 错选不得分
}
if (correctCount == correctAnswers.length) {
return BigDecimal.valueOf(question.getScore());
} else if (correctCount > 0) {
return BigDecimal.valueOf(question.getScore())
.multiply(BigDecimal.valueOf(correctCount))
.divide(BigDecimal.valueOf(correctAnswers.length), 1, RoundingMode.HALF_UP);
}
}
return BigDecimal.ZERO;
}
}
这段代码体现了企业级应用的核心特性:事务一致性保证、Redis 缓存控制考试状态、以及复杂的多选题计分规则。特别是 calculateScore 方法,清晰分离了不同题型的判分逻辑。
Controller 层与 API 设计
控制器层实现了 RESTful 风格的 API 设计,统一响应格式:
@RestController
@RequestMapping("/api/exam")
public class ExamController {
@PostMapping("/start")
public Result<ExamStartVO> startExam(@Valid @RequestBody ExamStartDTO dto) {
try {
Long userId = SecurityUtils.getCurrentUserId();
return Result.success(examService.startExam(dto.getPaperId(), userId));
} catch (BusinessException e) {
return Result.fail(e.getMessage());
}
}
@PostMapping("/submit")
public Result<ExamResultVO> submitAnswer(@Valid @RequestBody ExamSubmitDTO dto) {
try {
Long userId = SecurityUtils.getCurrentUserId();
return Result.success(examService.submitAnswer(dto.getExamId(), userId, dto.getAnswerList()));
} catch (BusinessException e) {
return Result.fail(e.getMessage());
}
}
}
统一的 Result 对象封装了状态码和消息,结合 @Valid 注解实现请求参数校验,异常处理区分业务异常和系统异常,日志分级记录便于排查。
前端交互:Vue 组件与实时功能
前端基于 Vue 和 Element UI,实现了丰富的交互功能:
<template>
<div class="exam-container">
<!-- 考试头部信息 -->
<el-card>
<h2>{{ paperName }}</h2>
<p>剩余时间:<span :class="{ warning: remainingTime < 300 }">{{ formatTime(remainingTime) }}</span></p>
<el-button type="primary" @click="handleSubmit" :loading="submitting">交卷</el-button>
</el-card>
<!-- 题目区域 -->
<el-card>
<div v-if="currentQuestion">
<h3>{{ currentQuestionIndex + 1 }}. {{ currentQuestion.content }}</h3>
<el-radio-group v-if="currentQuestion.questionTypeId === 1" v-model="currentAnswer">
<el-radio label="A">A. {{ currentQuestion.optionA }}</el-radio>
<el-radio label="B">B. {{ currentQuestion.optionB }}</el-radio>
</el-radio-group>
<el-checkbox-group v-if="currentQuestion.questionTypeId === 2" v-model="currentAnswer">
<el-checkbox label="A">A. {{ currentQuestion.optionA }}</el-checkbox>
<el-checkbox label="B">B. {{ currentQuestion.optionB }}</el-checkbox>
</el-checkbox-group>
</div>
</el-card>
</div>
</template>
<script>
export default {
data() {
return {
remainingTime: 0,
timer: null,
cheatWarningCount: 0
};
},
created() {
this.loadExamData();
// 监听页面可见性变化(防作弊)
document.addEventListener('visibilitychange', this.handleVisibilityChange);
},
methods: {
async loadExamData() {
const res = await this.$api.exam.getExamDetail(this.examId);
if (res.success) {
this.questions = res.data.questions;
this.remainingTime = this.duration * 60;
this.startTimer();
}
},
startTimer() {
this.timer = setInterval(() => {
this.remainingTime--;
if (this.remainingTime <= 0) this.handleSubmit(true);
}, 1000);
},
handleVisibilityChange() {
if (document.hidden) {
this.cheatWarningCount++;
if (this.cheatWarningCount >= 3) {
this.$message.warning('检测到多次切换页面,将自动交卷!');
setTimeout(() => this.handleSubmit(true), 5000);
}
}
}
}
};
</script>
前端实现了倒计时、自动保存、防作弊监控(页面切换检测)等功能。特别是 handleVisibilityChange 方法,通过监听浏览器焦点变化来记录作弊行为,增强了考试的公平性。
开发效率对比与总结
通过 AI 辅助开发,整体效率提升显著:
| 开发环节 | 传统开发(预计) | AI 生成(实际) | 效率提升 |
|---|---|---|---|
| 数据库设计 | 8 小时 | 10 分钟 | 48 倍 |
| 后端核心逻辑 | 40 小时 | 1 小时 | 40 倍 |
| 前端页面 | 24 小时 | 1 小时 | 24 倍 |
| 总计 | 72 小时 | 2.5 小时 | 29 倍 |
质量对比:AI 生成的代码内置了完整的事务管理、异常处理和缓存策略,避免了人工编码时的疏漏。代码遵循统一规范,注释完整,架构清晰。
总结:本次实践展示了 AI 在快速构建企业级应用方面的潜力。它不仅是一个代码生成工具,更是一个全流程的开发助手。对于开发者而言,研究 AI 生成的高质量代码是学习最佳实践的捷径;对于企业,这种模式将大幅降低开发成本,缩短项目周期。未来的软件开发,必将是'人类定义需求,AI 实现细节'的协作模式。


