跳到主要内容 基于 AI 生成的在线考试系统全流程技术实现 | 极客日志
Java AI java 算法
基于 AI 生成的在线考试系统全流程技术实现 一个基于 AI 辅助开发的在线考试系统的全流程技术实现。系统采用 Spring Boot、MyBatis-Plus、Redis 和 Vue 技术栈,涵盖了数据库设计、实体类映射、DAO 层封装、Service 层业务逻辑、Controller 层接口设计及前端交互。核心功能包括智能组卷、防作弊监控、自动判分及实时倒计时。相比传统开发,AI 辅助显著提升了开发效率,同时保证了代码质量和功能完整性,展示了 AI 驱动开发的新范式。
活在当下 发布于 2026/4/6 更新于 2026/4/13 1 浏览引言:当代码自动生成成为现实
在传统的软件开发中,开发一个完整的在线考试系统通常需要数周时间。通过引入 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 ,
(`id`),
KEY `idx_username` (`username`),
KEY `idx_role_id` (`role_id`),
KEY `idx_status` (`status`)
) ENGINE InnoDB CHARSET utf8mb4 COMMENT ;
`t_question` (
`id` AUTO_INCREMENT COMMENT ,
`question_type_id` COMMENT ,
`subject_id` COMMENT ,
`content` text COMMENT ,
`option_a` ( ) COMMENT ,
`option_b` ( ) COMMENT ,
`option_c` ( ) COMMENT ,
`option_d` ( ) COMMENT ,
`answer` ( ) COMMENT ,
`score` COMMENT ,
`difficulty` tinyint COMMENT ,
`analysis` text COMMENT ,
`create_by` COMMENT ,
`create_time` datetime ,
`update_time` datetime ,
(`id`),
KEY `idx_question_type` (`question_type_id`),
KEY `idx_subject` (`subject_id`),
KEY `idx_difficulty` (`difficulty`)
) ENGINE InnoDB CHARSET utf8mb4 COMMENT ;
`t_exam_record` (
`id` AUTO_INCREMENT COMMENT ,
`paper_id` COMMENT ,
`user_id` COMMENT ,
`start_time` datetime COMMENT ,
`end_time` datetime COMMENT ,
`status` tinyint COMMENT ,
`score` ( , ) COMMENT ,
`cheat_count` COMMENT ,
`ip_address` ( ) COMMENT ,
(`id`),
KEY `idx_user_paper` (`user_id`,`paper_id`,`status`) COMMENT ,
KEY `idx_status` (`status`),
KEY `idx_end_time` (`end_time`)
) ENGINE InnoDB CHARSET utf8mb4 COMMENT ;
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 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
PRIMARY KEY
UNIQUE
=
DEFAULT
=
=
'用户信息表'
CREATE TABLE
bigint
NOT NULL
'题目 ID'
bigint
NOT NULL
'题目类型 ID(1:单选,2:多选,3:判断)'
bigint
NOT NULL
'科目 ID'
NOT NULL
'题目内容'
varchar
500
DEFAULT
NULL
'选项 A'
varchar
500
DEFAULT
NULL
'选项 B'
varchar
500
DEFAULT
NULL
'选项 C'
varchar
500
DEFAULT
NULL
'选项 D'
varchar
100
NOT NULL
'正确答案'
int
NOT NULL
'分值'
NOT NULL
'难度 (1:易,2:中,3:难)'
'答案解析'
bigint
NOT NULL
'创建人'
NOT NULL
DEFAULT
CURRENT_TIMESTAMP
NOT NULL
DEFAULT
CURRENT_TIMESTAMP
ON
UPDATE
CURRENT_TIMESTAMP
PRIMARY KEY
=
DEFAULT
=
=
'题库表'
CREATE TABLE
bigint
NOT NULL
'考试记录 ID'
bigint
NOT NULL
'试卷 ID'
bigint
NOT NULL
'考生 ID'
NOT NULL
'开始时间'
DEFAULT
NULL
'结束时间'
NOT NULL
'状态 (1:进行中,2:已完成,3:超时)'
decimal
5
1
DEFAULT
NULL
'总分'
int
NOT NULL
DEFAULT
'0'
'作弊次数'
varchar
50
DEFAULT
NULL
'登录 IP'
PRIMARY KEY
UNIQUE
'防止重复考试'
=
DEFAULT
=
=
'考试记录表'
完整的字段注释和表注释,符合企业开发规范
合理的索引设计,尤其是 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 ;
@TableId(type = IdType.AUTO)
private Long id;
@TableField("question_type_id")
@NotNull(message = "题目类型不能为空")
private Long questionTypeId;
@TableField("subject_id")
@NotNull(message = "科目不能为空")
private Long subjectId;
@TableField("content")
@NotBlank(message = "题目内容不能为空")
private String content;
@TableField("option_a")
private String optionA;
@TableField("option_b")
private String optionB;
@TableField("option_c")
private String optionC;
@TableField("option_d")
private String optionD;
@TableField("answer")
@NotBlank(message = "正确答案不能为空")
private String answer;
@TableField("score")
@Min(value = 1, message = "分值不能小于 1")
private Integer score;
@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 注解简化 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) ;
}
继承 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) {
Paper paper = paperMapper.selectById(paperId);
if (paper == null ) {
throw new BusinessException ("试卷不存在" );
}
if (paper.getStatus() != 1 ) {
throw new BusinessException ("试卷未发布或已过期" );
}
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 ("您已参加过该考试,不能重复考试" );
}
}
ExamRecord examRecord = new ExamRecord ();
examRecord.setPaperId(paperId);
examRecord.setUserId(userId);
examRecord.setStartTime(LocalDateTime.now());
examRecord.setStatus(1 );
examRecord.setIpAddress(IpUtils.getIpAddr());
examRecordMapper.insert(examRecord);
List<Question> questions = questionMapper.selectByPaperId(paperId);
if (CollectionUtils.isEmpty(questions)) {
throw new BusinessException ("试卷未配置题目,请联系管理员" );
}
String examKey = "exam:record:" + examRecord.getId();
redisTemplate.opsForValue().set(examKey, examRecord, paper.getDuration() + 5 , TimeUnit.MINUTES);
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) {
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 ("考试已结束或已提交" );
}
Paper paper = paperMapper.selectById(examRecord.getPaperId());
if (paper == null ) {
throw new BusinessException ("试卷信息不存在" );
}
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);
}
if (!answers.isEmpty()) {
answerMapper.batchInsert(answers);
}
examRecord.setEndTime(LocalDateTime.now());
examRecord.setStatus(2 );
examRecord.setScore(totalScore);
examRecordMapper.updateById(examRecord);
String examKey = "exam:record:" + examId;
redisTemplate.delete(examKey);
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) {
if (dto.getSubjectId() == null ) {
throw new BusinessException ("请选择考试科目" );
}
if (CollectionUtils.isEmpty(dto.getQuestionTypeList())) {
throw new BusinessException ("请选择题目类型" );
}
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();
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);
}
}
}
if (!paperQuestions.isEmpty()) {
paperQuestionMapper.batchInsert(paperQuestions);
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 实现考试状态缓存和过期控制
批量操作优化(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 {
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 注解进行请求参数校验
完善的异常处理机制,区分业务异常和系统异常
日志分级记录(warn/error)便于问题定位
安全控制(SecurityUtils.getCurrentUserId())
清晰的接口命名和职责划分
前端代码:Vue 组件与实时交互 AI 辅助工具生成的前端代码基于 Vue 和 Element UI,实现了丰富的交互功能:
<template>
<div>
<!-- 考试头部信息 -->
<el-card>
<div>
<h2>{{ paperName }}</h2>
<p>考试时长:{{ duration }}分钟</p>
<div :class="{ warning: remainingTime < 5 * 60, danger: remainingTime < 60 }">
<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">
<span>{{ getQuestionTypeName(currentQuestion.questionTypeId) }} ({{ currentQuestion.score }}分)</span>
<h3>{{ currentQuestionIndex + 1 }}. {{ currentQuestion.content }}</h3>
<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: {},
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() {
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()
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; }
.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; }
</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 辅助开发彻底改变了开发认知 —— 它不仅是一个代码生成工具,更是一个全流程的开发助手。通过分析本次生成的在线考试系统代码,可以发现几个显著特点:
架构完整性 :严格遵循三层架构设计,各层职责清晰,依赖关系合理
业务深度 :针对考试场景设计了完整的业务逻辑,包括复杂的计分规则和组卷算法
技术先进性 :整合了 Spring Boot、MyBatis-Plus、Redis 等主流技术栈
安全性考虑 :包含权限控制、防作弊机制、数据校验等安全措施
可扩展性 :代码结构松耦合,便于后续功能扩展和维护
对于开发者而言,AI 辅助开发是一个绝佳的学习工具 —— 通过研究 AI 生成的高质量代码,可以掌握许多企业级开发的最佳实践。未来的软件开发,必将是 "人类定义需求,AI 实现细节" 的协作模式。在 AI 的助力下,我们终于可以从重复劳动中解放出来,将更多精力投入到创意和创新中去。