首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >📚开源考试答题源码app/在线考试答题小程序源码系统开发🆓基于SpringBoot+Vue前后端分离

📚开源考试答题源码app/在线考试答题小程序源码系统开发🆓基于SpringBoot+Vue前后端分离

原创
作者头像
用户11722177
发布2025-06-29 16:29:35
发布2025-06-29 16:29:35
46800
代码可运行
举报
运行总次数:0
代码可运行

  一、引言

  在教育信息化和在线学习需求激增的背景下,开发一套开源、跨平台的考试答题系统具有重要价值。基于SpringBoot和Vue的前后端分离架构,结合uni-app跨端框架,可实现一套代码同时编译为App、微信小程序、H5等多端应用,降低开发成本的同时提升用户覆盖范围。本文将详细介绍该系统的技术架构、核心功能实现及开源生态设计,附完整代码示例。

  源码:zs.xcxyms.top

  二、技术架构选型

  2.1后端技术栈

  核心框架:SpringBoot 3.2.0(简化配置,快速构建微服务)

  Web框架:Spring MVC(处理RESTful接口)

  ORM工具:MyBatis-Plus 3.5.3(简化数据库操作,支持代码生成)

  安全认证:Spring Security+JWT(基于令牌的认证机制)

  缓存服务:Redis 6.0(缓存题库、考试状态等高频数据)

  接口文档:Knife4j(基于OpenAPI 3.0,替代Swagger)

  2.2前端技术栈

添加描述

添加描述

  主框架:Vue 3.3.4(响应式组件化开发)

  状态管理:Pinia(轻量级状态管理库)

  UI组件库:

  移动端:Vant 4.0(App端UI)

  小程序:uni-app组件(跨端兼容)

  跨端框架:uni-app 3.8.1(一套代码编译为App、小程序、H5)

  路由管理:uni-simple-router(小程序路由适配)

  2.3数据库设计

  采用MySQL 8.0存储核心数据,主要表结构如下:

代码语言:javascript
代码运行次数:0
运行
复制
  |表名|核心字段|功能说明|
  |---------------------|--------------------------------------------------------------------------|--------------------------|
  |`user`|`id`,`username`,`password`,`role`,`create_time`|用户账号与权限管理|
  |`question_bank`|`id`,`question_type`,`content`,`options`,`answer`,`difficulty`|题库管理(支持单选、多选、判断、主观题)|
  |`exam_paper`|`id`,`paper_name`,`total_score`,`duration`,`question_ids`|试卷定义(关联题库ID)|
  |`exam_record`|`id`,`user_id`,`paper_id`,`score`,`submit_time`,`answer_details`|考试记录与答卷详情|
  |`exam_room`|`id`,`paper_id`,`status`,`start_time`,`end_time`|考试场次管理(支持定时考试)|
  三、核心功能模块实现

复制

复制

  3.1用户认证与权限管理

  3.1.1后端认证接口(SpringBoot)

代码语言:javascript
代码运行次数:0
运行
复制
  java
  //UserController.java用户登录接口
  RestController
  RequestMapping("/api/auth")
  public class UserController{
  Autowired
  private UserService userService;
  Autowired
  private JwtTokenUtil jwtTokenUtil;
  PostMapping("/login")
  public Result<LoginVO>login(RequestBody LoginDTO loginDTO){
  //1.验证账号密码
  User user=userService.login(loginDTO.getUsername(),loginDTO.getPassword());
  if(user==null){
  return Result.error("账号或密码错误");
  }
  //2.生成JWT令牌
  String token=jwtTokenUtil.generateToken(user);
  //3.封装返回结果
  LoginVO loginVO=new LoginVO();
  loginVO.setToken(token);
  loginVO.setUserInfo(UserInfoVO.convertFrom(user));
  return Result.success(loginVO);
  }
  //其他接口如注册、修改密码等...
  }
  //JWT工具类核心代码
  Component
  public class JwtTokenUtil{
  private static final String SECRET="exam_system_secret_key_2025";
  private static final long EXPIRATION=24*60*60*1000;//24小时有效期
  public String generateToken(User user){
  Claims claims=Jwts.claims().setSubject(user.getUsername());
  claims.put("userId",user.getId());
  claims.put("role",user.getRole());
  return Jwts.builder()
  .setClaims(claims)
  .setExpiration(new Date(System.currentTimeMillis()+EXPIRATION))
  .signWith(SignatureAlgorithm.HS256,SECRET)
  .compact();
  }
  public boolean validateToken(String token){
  try{
  Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
  return true;
  }catch(Exception e){
  return false;
  }
  }
  }

复制

复制

  3.1.2前端登录组件(Vue 3+uni-app)

代码语言:javascript
代码运行次数:0
运行
复制
  vue
  <template>
  <view class="login-container">
  <input v-model="username"placeholder="账号"class="input"/>
  <input v-model="password"placeholder="密码"type="password"class="input"/>
  <button click="handleLogin"class="login-btn">登录</button>
  </view>
  </template>
  <script setup>
  import{ref,onMounted}from'vue';
  import{useRouter}from'uni-simple-router';
  import{request}from'/utils/request';
  const username=ref('');
  const password=ref('');
  const router=useRouter();
  const handleLogin=async()=>{
  if(!username.value||!password.value){
  uni.showToast({title:'请输入账号和密码',icon:'none'});
  return;
  }
  try{
  const res=await request({
  url:'/api/auth/login',
  method:'POST',
  data:{
  username:username.value,
  password:password.value
  }
  });
  if(res.code===200){
  //存储令牌和用户信息
  uni.setStorageSync('token',res.data.token);
  uni.setStorageSync('userInfo',res.data.userInfo);
  //跳转到首页
  router.push('/pages/index/index');
  }else{
  uni.showToast({title:res.message,icon:'none'});
  }
  }catch(error){
  console.error('登录失败',error);
  uni.showToast({title:'登录失败,请重试',icon:'none'});
  }
  };
  </script>

复制

复制

  3.2题库与试卷管理

  3.2.1题库CRUD接口(SpringBoot)

代码语言:javascript
代码运行次数:0
运行
复制
  java
  //QuestionBankController.java题库管理接口
  RestController
  RequestMapping("/api/question")
  PreAuthorize("hasAnyRole('ADMIN','TEACHER')")//仅管理员和教师可访问
  public class QuestionBankController{
  Autowired
  private QuestionBankService questionBankService;
  //添加题目
  PostMapping
  public Result<String>addQuestion(RequestBody QuestionBank question){
  boolean result=questionBankService.save(question);
  return result?Result.success("添加成功"):Result.error("添加失败");
  }
  //查询题目列表(支持分页和条件查询)
  GetMapping("/list")
  public Result<IPage<QuestionBank>>list(
  RequestParam(defaultValue="1")int page,
  RequestParam(defaultValue="10")int size,
  RequestParam(required=false)String keyword){
  IPage<QuestionBank>pageData=questionBankService.listByPage(page,size,keyword);
  return Result.success(pageData);
  }
  //批量导入题目(Excel导入)
  PostMapping("/import")
  public Result<String>importQuestions(RequestParam("file")MultipartFile file){
  try{
  int count=questionBankService.importFromExcel(file.getInputStream());
  return Result.success("成功导入"+count+"道题目");
  }catch(Exception e){
  return Result.error("导入失败:"+e.getMessage());
  }
  }
  }

复制

复制

  3.2.2试卷生成逻辑(后端核心代码)

代码语言:javascript
代码运行次数:0
运行
复制
 java
  //ExamPaperService.java试卷生成算法
  Service
  public class ExamPaperServiceImpl implements ExamPaperService{
  Autowired
  private QuestionBankMapper questionBankMapper;
  Autowired
  private RedisTemplate<String,Object>redisTemplate;
  Override
  public ExamPaper generateRandomPaper(String paperName,int totalScore,int duration,
  Map<String,Integer>difficultyCount){
  //1.从Redis缓存获取题库(若没有则查询数据库)
  List<QuestionBank>allQuestions=(List<QuestionBank>)redisTemplate.opsForValue()
  .get("question_bank_all");
  if(allQuestions==null){
  allQuestions=questionBankMapper.selectList(null);
  redisTemplate.opsForValue().set("question_bank_all",allQuestions,1,TimeUnit.HOURS);
  }
  //2.按难度和题型筛选题目(示例:随机抽取不同难度的题目)
  Map<String,List<QuestionBank>>difficultyMap=allQuestions.stream()
  .collect(Collectors.groupingBy(QuestionBank::getDifficulty));
  List<QuestionBank>selectedQuestions=new ArrayList<>();
  difficultyCount.forEach((difficulty,count)->{
  List<QuestionBank>questions=difficultyMap.getOrDefault(difficulty,new ArrayList<>());
  if(!questions.isEmpty()&&count>0){
  //随机抽取count道题
  Collections.shuffle(questions);
  selectedQuestions.addAll(questions.subList(0,Math.min(count,questions.size())));
  }
  });
  //3.计算总分(示例:单选题2分,多选题4分,主观题10分)
  int actualScore=selectedQuestions.stream()
  .mapToInt(q->"单选".equals(q.getQuestionType())?2:
  "多选".equals(q.getQuestionType())?4:10)
  .sum();
  //4.保存试卷到数据库
  ExamPaper paper=new ExamPaper();
  paper.setPaperName(paperName);
  paper.setTotalScore(actualScore);
  paper.setDuration(duration);
  //存储题目ID列表(用JSON格式)
  paper.setQuestionIds(JSON.toJSONString(selectedQuestions.stream()
  .map(QuestionBank::getId)
  .collect(Collectors.toList())));
  save(paper);
  return paper;
  }
  } 

复制

复制

  3.3在线考试核心模块

  3.3.1考试接口设计(前后端交互)

代码语言:javascript
代码运行次数:0
运行
复制
  java
  //ExamRoomController.java考试场次管理
  RestController
  RequestMapping("/api/exam/room")
  public class ExamRoomController{
  Autowired
  private ExamRoomService examRoomService;
  Autowired
  private ExamRecordService examRecordService;
  //创建考试场次
  PostMapping
  public Result<ExamRoom>createExamRoom(RequestBody ExamRoom examRoom){
  examRoomService.save(examRoom);
  return Result.success(examRoom);
  }
  //进入考试(获取试卷)
  GetMapping("/{roomId}/enter")
  public Result<ExamPaperVO>enterExam(PathVariable Long roomId,
  RequestParam Long userId){
  //1.验证考试场次状态
  ExamRoom room=examRoomService.getById(roomId);
  if(room==null||room.getStatus()!=1){//1表示进行中
  return Result.error("考试场次不存在或已结束");
  }
  //2.验证用户是否已参加过考试
  ExamRecord existedRecord=examRecordService.getOne(
  new QueryWrapper<ExamRecord>()
  .eq("user_id",userId)
  .eq("room_id",roomId));
  if(existedRecord!=null){
  return Result.error("您已参加过该考试");
  }
  //3.获取试卷信息(含题目)
  ExamPaper paper=examRoom.getPaper();
  ExamPaperVO paperVO=ExamPaperVO.convertFrom(paper);
  //4.记录考试开始(创建考试记录)
  ExamRecord record=new ExamRecord();
  record.setUserId(userId);
  record.setRoomId(roomId);
  record.setPaperId(paper.getId());
  record.setStatus(1);//1表示进行中
  examRecordService.save(record);
  paperVO.setRecordId(record.getId());
  return Result.success(paperVO);
  }
  //提交答卷
  PostMapping("/submit")
  public Result<String>submitExam(RequestBody ExamSubmitDTO submitDTO){
  //1.计算得分(对比用户答案与正确答案)
  int score=examRecordService.calculateScore(
  submitDTO.getRecordId(),submitDTO.getAnswerDetails());
  //2.更新考试记录状态
  ExamRecord record=examRecordService.getById(submitDTO.getRecordId());
  record.setScore(score);
  record.setStatus(2);//2表示已提交
  record.setSubmitTime(new Date());
  record.setAnswerDetails(JSON.toJSONString(submitDTO.getAnswerDetails()));
  examRecordService.updateById(record);
  return Result.success("提交成功,得分:"+score);
  }
  }

复制

复制

  3.3.2考试页面组件(Vue 3+uni-app)

代码语言:javascript
代码运行次数:0
运行
复制
 vue
  <template>
  <view class="exam-container">
  <!--考试标题与倒计时-->
  <view class="exam-header">
  <text class="title">{{paperInfo.paperName}}</text>
  <text class="countdown":class="{'expired':isExpired}">
  剩余时间:{{formatTime(remainingTime)}}
  </text>
  </view>
  <!--题目列表-->
  <view class="question-list">
  <view v-for="(question,index)in questions":key="question.id"class="question-item">
  <view class="question-number">{{index+1}}.</view>
  <view class="question-content"v-html="question.content"></view>
  <view class="question-options">
  <view v-for="(option,optIndex)in question.options":key="optIndex"
  class="option-item"
  :class="{'selected':selectedAnswers[question.id]===option.value}"
  click="selectAnswer(question.id,option.value)">
  {{getOptionLetter(optIndex)}}.{{option.label}}
  </view>
  </view>
  </view>
  </view>
  <!--提交按钮-->
  <button class="submit-btn"click="submitExam":disabled="isSubmitting">
  提交答卷
  </button>
  </view>
  </template>
  <script setup>
  import{ref,onMounted,onUnmounted,watch}from'vue';
  import{useRouter}from'uni-simple-router';
  import{request}from'/utils/request';
  import{useStore}from'pinia';
  import examStore from'/stores/exam';
  const router=useRouter();
  const store=useStore(examStore);
  //试卷数据
  const paperInfo=ref({});
  const questions=ref([]);
  const recordId=ref('');
  const remainingTime=ref(0);//剩余时间(秒)
  const isExpired=ref(false);
  const isSubmitting=ref(false);
  const selectedAnswers=ref({});//存储用户选择的答案{questionId:'A',...}
  //进入考试时获取试卷
  const enterExam=async(roomId)=>{
  try{
  const userInfo=uni.getStorageSync('userInfo');
  if(!userInfo){
  router.push('/pages/login/login');
  return;
  }
  const res=await request({
  url:`/api/exam/room/${roomId}/enter`,
  method:'GET',
  data:{userId:userInfo.id}
  });
  if(res.code===200){
  paperInfo.value=res.data;
  recordId.value=res.data.recordId;
  //解析题目列表(假设题目ID存储为JSON字符串)
  const questionIds=JSON.parse(res.data.questionIds);
  //从题库中获取题目详情(实际项目中可能通过接口批量获取)
  const questionRes=await request({
  url:'/api/question/list',
  method:'GET',
  params:{ids:questionIds.join(',')}
  });
  questions.value=questionRes.data.records;
  //初始化倒计时
  initCountdown(res.data.duration*60);//duration单位为分钟,转换为秒
  }else{
  uni.showToast({title:res.message,icon:'none'});
  setTimeout(()=>{
  router.back();
  },1500);
  }
  }catch(error){
  console.error('进入考试失败',error);
  uni.showToast({title:'进入考试失败,请重试',icon:'none'});
  setTimeout(()=>{
  router.back();
  },1500);
  }
  };
  //初始化倒计时
  const initCountdown=(totalSeconds)=>{
  remainingTime.value=totalSeconds;
  const timer=setInterval(()=>{
  remainingTime.value--;
  if(remainingTime.value<=0){
  clearInterval(timer);
  isExpired.value=true;
  submitExam();//自动提交
  }
  },1000);
  onUnmounted(()=>{
  clearInterval(timer);
  });
  };
  //选择答案
  const selectAnswer=(questionId,answer)=>{
  selectedAnswers.value[questionId]=answer;
  };
  //提交答卷
  const submitExam=async()=>{
  if(isSubmitting.value)return;
  isSubmitting.value=true;
  try{
  const res=await request({
  url:'/api/exam/room/submit',
  method:'POST',
  data:{
  recordId:recordId.value,
  answerDetails:selectedAnswers.value
  }
  });
  uni.showToast({title:res.message,icon:'success'});
  //跳转到成绩页面
  setTimeout(()=>{
  router.push(`/pages/exam/result?recordId=${recordId.value}`);
  },1500);
  }catch(error){
  console.error('提交答卷失败',error);
  uni.showToast({title:'提交失败,请重试',icon:'none'});
  }finally{
  isSubmitting.value=false;
  }
  };
  //其他辅助函数(如时间格式化、选项字母转换等)
  const formatTime=(seconds)=>{
  const minutes=Math.floor(seconds/60);
  const secs=seconds%60;
  return`${minutes.toString().padStart(2,'0')}:${secs.toString().padStart(2,'0')}`;
  };
  const getOptionLetter=(index)=>{
  return String.fromCharCode(65+index);//A,B,C,D...
  };
  //页面加载时获取参数并进入考试
  const roomId=uni.getStorageSync('currentRoomId');
  if(roomId){
  enterExam(roomId);
  }
  </script> 

复制

复制

  四、跨端适配与性能优化

  4.1跨端开发方案(uni-app实现)

  通过uni-app的条件编译实现多端适配,示例如下:

代码语言:javascript
代码运行次数:0
运行
复制
  vue
  <template>
  <view>
  <!--App端特有功能:离线缓存考试数据-->
  ifdef APP-PLUS
  <view class="app-only">
  <button click="clearLocalCache">清除本地考试缓存</button>
  </view>
  endif
  <!--微信小程序特有功能:分享试卷-->
  ifdef MP-WEIXIN
  <view class="wechat-only">
  <button open-type="share">分享试卷</button>
  </view>
  endif
  <!--通用考试界面-->
  <view class="common-exam-content">
  <!--通用考试组件-->
  </view>
  </view>
  </template>
  <script>
  //不同端的JS逻辑适配
  export default{
  methods:{
  //App端本地存储操作
  ifdef APP-PLUS
  clearLocalCache(){
  uni.clearStorage();
  uni.showToast({title:'缓存已清除'});
  },
  endif
  //小程序端分享逻辑
  ifdef MP-WEIXIN
  onShareAppMessage(){
  return{
  title:'邀请好友参加考试',
  path:'/pages/exam/index'
  };
  }
  endif
  }
  };
  </script>

复制

复制

  4.2性能优化策略

  1.题库缓存:

  后端使用Redis缓存高频访问的题库数据,有效期1小时

  前端通过uni-storage缓存已加载的试卷,避免重复请求

代码语言:javascript
代码运行次数:0
运行
复制
  java
  //后端Redis缓存示例
  Service
  public class QuestionBankServiceImpl implements QuestionBankService{
  Autowired
  private RedisTemplate<String,Object>redisTemplate;
  Autowired
  private QuestionBankMapper questionBankMapper;
  Override
  public List<QuestionBank>getHotQuestions(int count){
  //从Redis获取热门题目
  List<QuestionBank>hotQuestions=(List<QuestionBank>)redisTemplate.opsForValue()
  .get("hot_questions_"+count);
  if(hotQuestions==null){
  //数据库查询后缓存
  hotQuestions=questionBankMapper.selectHotQuestions(count);
  redisTemplate.opsForValue().set(
  "hot_questions_"+count,hotQuestions,30,TimeUnit.MINUTES);
  }
  return hotQuestions;
  }
  }

复制

复制

  2.考试防作弊:

  前端监听页面切换事件,切出考试页面超过10秒自动交卷

  后端记录考试IP地址,防止同一账号多端同时考试

代码语言:javascript
代码运行次数:0
运行
复制
  vue
  //前端防切屏逻辑
  onMounted(){
  //监听页面切换(App端和小程序端适配)
  ifdef APP-PLUS
  plus.webview.currentWebview().addEventListener('pause',()=>{
  this.switchOutCount++;
  if(this.switchOutCount>=2){//切出超过2次
  uni.showToast({title:'考试已自动交卷',icon:'none'});
  this.submitExam();
  }
  });
  endif
  ifdef MP-WEIXIN
  wx.onAppHide(()=>{
  this.hideTime=new Date().getTime();
  });
  wx.onAppShow(()=>{
  if(this.hideTime){
  const diff=new Date().getTime()this.hideTime;
  if(diff>10000){//切出超过10秒
  uni.showToast({title:'考试已自动交卷',icon:'none'});
  this.submitExam();
  }
  }
  });
  endif
  }

复制

复制

  五、开源生态与部署指南

  5.1开源项目结构

代码语言:javascript
代码运行次数:0
运行
复制
  exam-system/
  ├──backend/后端项目(SpringBoot)
  │├──src/
  ││├──main/
  │││├──java/com/exam/
  ││││├──config/配置类(安全、数据库等)
  ││││├──controller/接口控制器
  ││││├──service/服务层
  ││││├──mapper/MyBatis映射
  ││││├──entity/实体类
  ││││├──dto/数据传输对象
  ││││├──vo/视图对象
  ││││├──utils/工具类
  ││││├──ExamSystemApplication.java启动类
  │││└──resources/
  │││├──mapper/MyBatis XML映射文件
  │││├──application.yml配置文件
  ││└──test/测试类
  ├──frontend/前端项目(Vue 3+uni-app)
  │├──src/
  ││├──pages/页面组件
  ││├──components/公共组件
  ││├──stores/Pinia状态管理
  ││├──utils/工具函数
  ││├──App.vue应用入口
  ││└──main.js主程序
  │├──manifest.json应用配置
  │└──pages.json页面路由配置
  ├──doc/文档
  │├──database/数据库设计文档
  │├──api/接口文档
  │└──deploy.md部署指南
  └──pom.xml后端Maven依赖

复制

复制

  5.2部署流程

  1.后端部署:

  bash

  编译后端项目

  cd backend

  mvn clean package-DskipTests

  启动服务(默认端口8080)

  java-jar target/exam-system-1.0.0.jar

  2.前端部署:

  bash

  安装依赖

  cd frontend

  npm install

  编译为App(Android/iOS)

  npm run build:app-plus

  编译为微信小程序

  npm run build:mp-weixin

  3.数据库初始化:

  执行`doc/database/exam_system.sql`创建数据库和表结构

  初始化管理员账号:`admin/123456`

  六、总结与展望

  本开源考试答题系统基于SpringBoot+Vue前后端分离架构,结合uni-app跨端框架,实现了一套代码多端运行的能力,涵盖用户管理、题库管理、试卷生成、在线考试、成绩统计等核心功能。系统采用JWT认证、Redis缓存、MyBatis-Plus等技术提升性能和开发效率,通过条件编译解决多端适配问题。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
作者已关闭评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档