跳到主要内容AI 辅助开发实战:在线考试系统全流程代码解析 | 极客日志JavaAI大前端java算法
AI 辅助开发实战:在线考试系统全流程代码解析
在线考试系统涉及数据库设计、业务逻辑与前端交互。通过 AI 辅助生成代码,实现了从需求到可运行系统的快速转化。核心包括用户与题库表结构设计、MyBatis-Plus 数据访问层封装、事务控制下的考试流程管理以及 Vue 前端实时交互。AI 生成代码具备完整注释、规范注解及防作弊机制,显著提升了开发效率,同时保留了企业级应用的事务一致性与安全性。
猫巷少女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 ,
`create_time` datetime ,
`update_time` datetime ,
(`id`),
KEY `idx_username` (`username`),
KEY `idx_role_id` (`role_id`),
KEY `idx_status` (`status`)
) ENGINEInnoDB CHARSETutf8mb4 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`)
) ENGINEInnoDB CHARSETutf8mb4 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`)
) ENGINEInnoDB CHARSETutf8mb4 COMMENT;
'状态 (0:禁用,1:正常)'
NOT NULL
DEFAULT
CURRENT_TIMESTAMP
NOT NULL
DEFAULT
CURRENT_TIMESTAMP
ON
UPDATE
CURRENT_TIMESTAMP
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
=
=
'考试记录表'
这段 SQL 有几个值得注意的细节:完整的字段注释符合企业开发规范;合理的索引设计,尤其是唯一索引有效防止重复考试;包含时间戳字段便于数据追踪;状态字段使用 tinyint 节省存储空间;针对考试业务特点设计了作弊次数等特色字段。
实体类设计:注解驱动的对象映射
基于数据库表结构,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 注解简化模板代码;通过 @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) {
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 实现考试状态缓存和过期控制;批量操作优化提升性能;详细的日志记录便于问题排查;面向接口编程,通过 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 注解进行请求参数校验;完善的异常处理机制,区分业务异常和系统异常;日志分级记录便于问题定位;安全控制;清晰的接口命名和职责划分。
前端代码: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 的助力下,我们终于可以从重复劳动中解放出来,将更多精力投入到创意和创新中去。
相关免费在线工具
- 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