每日一句
愿你是一只燕子,
衔着春光飞来;
愿你是一只雄鹰,
箭一般射向蓝天。
目录
六.Controller 层设计:RESTful 接口与统一响应
一.引言:当代码自动生成成为现实
作为计算机专业学生,我曾以为 "一天开发一个系统" 只是天方夜谭。直到使用飞算 JavaAI 开发在线考试系统时,这个想法被彻底颠覆 —— 系统不仅自动生成了规范代码,还处理了事务管理、异常处理等高级逻辑。本文将深入剖析 AI 生成的核心代码,展示从需求到可运行系统的完整技术路径。
二.数据库设计:自动生成的表结构与关系映射
飞算 JavaAI 根据需求自动生成了 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 具有以下特点:
1.完整的字段注释和表注释,符合企业开发规范
2.合理的索引设计,尤其是idx_user_paper唯一索引有效防止重复考试
3.包含时间戳字段create_time和update_time,便于数据追踪
4.状态字段使用 tinyint 类型,节省存储空间
5.针对考试业务特点设计了cheat_count等特色字段
三.实体类设计:注解驱动的对象映射
基于数据库表结构,飞算 JavaAI 自动生成了对应的实体类,采用 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; // 科目名称
}
代码解析:实体类体现了 AI 的细致处理:
1.使用@Data注解简化 getter/setter 等模板代码
2.通过@TableName和@TableField实现与数据库表的映射
3.集成javax.validation注解实现参数校验
4.合理使用@TableField(exist = false)定义 DTO 扩展字段
5.使用LocalDateTime处理时间,符合 Java 8 + 规范
6.添加序列化接口,支持分布式场景
四.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 层代码展现了高级查询能力:
1.继承BaseMapper获得基础 CRUD 操作,减少重复代码
2.使用 MyBatis 动态 SQL 实现多条件查询
3.专门设计selectRandomQuestions方法支持智能组卷
4.分页查询包含关联表信息,减少 N+1 查询问题
5.参数命名规范,与业务逻辑保持一致
五.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 层代码体现了企业级应用的核心特性:
1.使用@Transactional注解保证事务一致性
2.完善的参数校验和异常处理(自定义BusinessException)
3.复杂业务逻辑实现,如多选题的按比例计分规则
4.结合 Redis 实现考试状态缓存和过期控制
5.批量操作优化(batchInsert)提升性能
6.详细的日志记录便于问题排查
7.面向接口编程,通过 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 设计:
1.采用 RESTful 风格的 URL 设计,使用合适的 HTTP 方法
2.统一的响应格式Result,包含状态码、消息和数据
3.使用@Valid注解进行请求参数校验
4.完善的异常处理机制,区分业务异常和系统异常
5.日志分级记录(warn/error)便于问题定位
6.安全控制(SecurityUtils.getCurrentUserId())
7.清晰的接口命名和职责划分
七.前端代码:Vue 组件与实时交互
飞算 JavaAI 生成的前端代码基于 Vue 和 Element UI,实现了丰富的交互功能:
<template>
<div class="exam-container">
<!-- 考试头部信息 -->
<el-card class="exam-header">
<div class="header-content">
<div class="title">
<h2>{{ paperName }}</h2>
<p>考试时长:{{ duration }}分钟</p>
</div>
<div class="timer" :class="{ warning: remainingTime < 5 * 60, danger: remainingTime < 60 }">
<i class="el-icon-clock"></i>
<span>剩余时间:{{ formatTime(remainingTime) }}</span>
</div>
<el-button
type="primary"
@click="handleSubmit"
:loading="submitting"
>
交卷
</el-button>
</div>
</el-card>
<!-- 题目区域 -->
<el-card class="questions-container">
<div class="question-nav">
<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 class="question-content">
<div v-if="currentQuestion" class="question-item">
<div class="question-header">
<span class="question-type">
{{ getQuestionTypeName(currentQuestion.questionTypeId) }}
({{ currentQuestion.score }}分)
</span>
<h3>{{ currentQuestionIndex + 1 }}. {{ currentQuestion.content }}</h3>
</div>
<div class="question-options" v-if="currentQuestion.questionTypeId !== 3">
<!-- 单选题/多选题选项 -->
<el-radio-group
v-if="currentQuestion.questionTypeId === 1"
v-model="currentAnswer"
@change="handleAnswerChange"
>
<el-radio :label="'A'" class="option-item">A. {{ currentQuestion.optionA }}</el-radio>
<el-radio :label="'B'" class="option-item">B. {{ currentQuestion.optionB }}</el-radio>
<el-radio :label="'C'" class="option-item">C. {{ currentQuestion.optionC }}</el-radio>
<el-radio :label="'D'" class="option-item">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'" class="option-item">A. {{ currentQuestion.optionA }}</el-checkbox>
<el-checkbox label="'B'" class="option-item">B. {{ currentQuestion.optionB }}</el-checkbox>
<el-checkbox label="'C'" class="option-item">C. {{ currentQuestion.optionC }}</el-checkbox>
<el-checkbox label="'D'" class="option-item">D. {{ currentQuestion.optionD }}</el-checkbox>
</el-checkbox-group>
</div>
<div class="question-options" v-if="currentQuestion.questionTypeId === 3">
<!-- 判断题 -->
<el-radio-group
v-model="currentAnswer"
@change="handleAnswerChange"
>
<el-radio :label="'正确'" class="option-item">正确</el-radio>
<el-radio :label="'错误'" class="option-item">错误</el-radio>
</el-radio-group>
</div>
</div>
<div class="question-navigation">
<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>
代码解析:前端代码实现了丰富的交互功能:
1.完整的考试流程:题目导航、答题、交卷
2.实时倒计时功能,支持自动交卷
3.防作弊机制:监控页面切换、窗口焦点变化
4.答案自动保存,避免意外丢失
5.响应式布局,适配不同屏幕尺寸
6.清晰的视觉反馈:已答题标记、当前题标记
7.不同题型的差异化展示
八.开发效率对比:AI 生成代码带来的质变
通过飞算 JavaAI 开发在线考试系统,我深刻体会到了智能开发工具的革命性影响:
开发环节
传统开发(预计)
飞算 JavaAI 开发(实际)
效率提升
数据库设计
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 驱动的开发新范式
飞算 JavaAI 彻底改变了我的开发认知 —— 它不仅是一个代码生成工具,更是一个全流程的开发助手。通过分析本次生成的在线考试系统代码,可以发现几个显著特点:
1.架构完整性:严格遵循三层架构设计,各层职责清晰,依赖关系合理
2.业务深度:针对考试场景设计了完整的业务逻辑,包括复杂的计分规则和组卷算法
3.技术先进性:整合了 Spring Boot、MyBatis-Plus、Redis 等主流技术栈
4.安全性考虑:包含权限控制、防作弊机制、数据校验等安全措施
5.可扩展性:代码结构松耦合,便于后续功能扩展和维护
对于学生开发者而言,飞算 JavaAI 是一个绝佳的学习工具 —— 通过研究 AI 生成的高质量代码,我掌握了许多企业级开发的最佳实践。对于企业来说,这种智能开发工具将大幅降低开发成本,缩短项目周期。
未来的软件开发,必将是 "人类定义需求,AI 实现细节" 的协作模式。飞算 JavaAI 让我看到了这种未来的可能性,也让我对自己的编程之路充满了新的期待。在 AI 的助力下,我们终于可以从重复劳动中解放出来,将更多精力投入到创意和创新中去。