基于Springboot+Vue的在线答题闯关系统
前言:随着在线教育的快速发展,传统的教育模式逐渐向互联网+教育模式转型。在线答题系统作为其中的一个重要组成部分,能够帮助用户通过互动式的学习方式提升知识掌握度。本文基于Spring Boot和Vue.js框架,设计并实现了一个在线答题闯关系统,旨在为用户提供顺序出题、随机出题、错题本、收藏题目、答题统计等多种功能,以增强用户的学习体验。
目录
- 前言
- 项目功能及技术
- 用户端
- 管理端
- API
- SpringBoot框架搭建
- 实体映射创建Mapper
- 接口封装
- 整合Swagger
- 常用字段类型
- 参考代码块
前言
本系统采用前后端分离架构,前端使用Vue.js框架实现,后端则通过Spring Boot进行构建,数据存储使用MySQL数据库。前端使用Vue.js进行数据渲染,而后端提供RESTful API接口来实现前后端的有效数据交互。
项目功能及技术
功能模块设计
-
顺序出题模块:该模块允许用户按顺序答题,系统根据预设的题目顺序逐一展示给用户。用户完成每一道题后可以进入下一题,适合需要系统化学习的用户。
-
体型练习模块:用户可以根据自己的需求选择特定的练习模式,比如选择某个类别或某个难度的题目进行练习。该模块支持用户自定义练习内容,帮助用户强化薄弱的知识点。
-
随机出题模块:系统可以随机从题库中抽取题目,进行答题闯关,用户在有限的时间内答题,提升学习的趣味性和挑战性。
-
错题本模块:该模块记录用户做错的题目,用户可以随时查看并重新进行练习,帮助用户集中攻克自己的薄弱环节,提升记忆与掌握度。
-
我的收藏模块:用户可以将自己喜欢或难度较高的题目收藏到个人收藏夹,方便以后再次复习或挑战。
-
答题统计模块:系统自动统计用户的答题情况,包括正确率、答题速度、错题数量等,帮助用户了解自己的学习进度和成效,并能根据数据调整学习策略。
技术:
-
Spring Boot:后端框架,利用Spring Boot的快速开发特性。同时,通过Mybatis简化数据库操作,提高数据访问效率。
-
Vue.js:前端框架,使用Vuex进行全局状态管理,提升数据的一致性与可维护性。
-
MySQL:数据库存储引擎,负责存储题目数据、用户答题记录、错题与收藏等信息。
-
Layui:前端UI组件库,用于搭建美观且响应式的用户界面,提升用户交互体验。
用户端
管理端
API
SpringBoot框架搭建
1.创建maven project,先创建一个名为SpringBootDemo的项目,选择【New Project】
然后在弹出的下图窗口中,选择左侧菜单的【New Project】
在project下创建module,点击右键选择【new】—【Module…】
左侧选择【Spring initializr】,通过idea中集成的Spring initializr工具进行spring boot项目的快速创建。窗口右侧:name可根据自己喜好设置,group和artifact和上面一样的规则,其他选项保持默认值即可,【next】
Developer Tools模块勾选【Spring Boot DevTools】,web模块勾选【Spring Web】,此时,一个Springboot项目已经搭建完成,可开发后续功能
实体映射创建Mapper
创建一个entity实体类文件夹,并在该文件夹下创建项目用到的实体类
package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Data
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String account;
private String pwd;
private String userDesc;
private String userHead;
private LocalDateTime createTime;
private Long role;
private String nickname;
private String email;
private String tags;
}
接口封装
由于我们使用mybatis-plus,所以简单的增删改查不用自己写,框架自带了,只需要实现或者继承他的Mapper、Service
创建控制器Controller
整合Swagger
添加依赖
先导入spring boot的web包
<!--swagger依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
配置Swagger
创建一个swagger的配置类,命名为SwaggerConfig.java
/*
*用于定义API主界面的信息,比如可以声明所有的API的总标题、描述、版本
*/
private ApiInfo apiDemo() {
return new ApiInfoBuilder()
//用来自定义API的标题
.title("SpringBoot项目SwaggerAPIAPI标题测试")
//用来描述整体的API
.description("SpringBoot项目SwaggerAPI描述测试")
//创建人信息
.contact(new Contact("测试员张三","http://localhost:8080/springboot/swagger-ui.html","xxxxxxxx@163.com"))
//用于定义服务的域名
//.termsOfServiceUrl("")
.version("1.0") //可以用来定义版本
.build();
}
接口测试
运行Spring Boot项目,默认端口8080,通过地址栏访问url
接口组定义
根据不同的业务区分不同的接口组,使用@API来划分
@Api(tags = "用户管理") // tags:组名称
@RestController
public class RoleController {
}
接口定义
使用@ApiModel来标注实体类,同时在接口中定义入参为实体类作为参数。
-
@ApiModel:用来标类
-
常用配置项:value:实体类简称;description:实体类说明
-
@ApiModelProperty:用来描述类的字段的含义。
常用字段类型
字段类型 | 所占字节 | 存储范围 | 最大存储值 | 使用场景 |
---|---|---|---|---|
TINYINT | 1 | -128~127 | 127 | 存储小整数 |
INT | 4 | -2147483648~2147483647 | 2147483647 | 存储大整数 |
BIGINT | 8 | -9223372036854775808~9223372036854775807 | 9223372036854775807 | 存储极大整数 |
DECIMAL | 可变长度 | 存储精度要求高的数值 | ||
CHAR | 固定长度 | 最多255字节 | 255个字符 | 存储长度固定的字符串 |
VARCHAR | 可变长度 | 最多65535字节 | 65535个字符 | 存储长度不固定的字符串 |
DATETIME | 8 | ‘1000-01-01 00:00:00’~‘9999-12-31 23:59:59’ | ‘9999-12-31 23:59:59’ | 存储日期和时间 |
参考代码块
<body style="background-color: #f7f7f7;">
<div class="headerBox">
<div class="logoBox">
<img src="img/logo1.png" />
<div class="logoTitle">在线答题闯关</div>
</div>
<div class="menuBox">
<div class="menuItem activeMenu ">
<a href="index.html">练习模式</a>
</div>
<div class="menuItem blackColor">
<a href="challengeLevels.html?param=primary">闯关模式</a>
</div>
<div class="menuItem blackColor">
<a href="wrongQuestion.html">我的错题</a>
</div>
<div class="menuItem blackColor">
<a href="myCollection.html">我的收藏</a>
</div>
<div class="menuItem blackColor">
<a href="statistics.html">答题统计</a>
</div>
<div class="menuItem blackColor">
<a href="center.html">个人中心</a>
</div>
<div class="menuItem blackColor">
<a href="./login/login.html">退出登录</a>
</div>
</div>
</div>
<div class="container-fluid" id="content-page">
<div class="row">
<div class="col-md-2"> </div>
<div class="col-md-8">
<div class="searchBox">
<div class="leftTitle">
{{pageTitle}}
</div>
</div>
</div>
<div class="col-md-2"> </div>
</div>
<div class="row">
<div class="col-md-2"> </div>
<div class="col-md-8">
<div v-for="(item,index) in questionList">
<div class="radioItemBox" v-if="item.questionType == '单选题'">
<div class="BtnBox radioItem">
<div>{{index+1}}.</div>
<textarea rows="18" cols="90" v-model="item.title" disabled></textarea>
<img src="img/collecQues.png" class="radioItemTypeImg"
v-on:click="collection(item.id)" />
</div>
<div class="BtnBox radioLineV2" v-if="item.checkA!=''">
<div class="radioIconBox">
<input type="radio" class="radioInputCheck" :name="index"
v-on:change="checkOne(index,'A')" />
</div>
<div>A.</div>
<input v-model="item.checkA" disabled class="radioLineV2Input" />
</div>
<div class="BtnBox radioLineV2" v-if="item.checkB!=''">
<div class="radioIconBox">
<input type="radio" class="radioInputCheck" :name="index"
v-on:change="checkOne(index,'B')" />
</div>
<div>B.</div>
<input v-model="item.checkB" disabled class="radioLineV2Input" />
</div>
<div class="BtnBox radioLineV2" v-if="item.checkC!=''">
<div class="radioIconBox">
<input type="radio" class="radioInputCheck" :name="index"
v-on:change="checkOne(index,'C')" />
</div>
<div>C.</div>
<input v-model="item.checkC" disabled class="radioLineV2Input" />
</div>
<div class="BtnBox radioLineV2" v-if="item.checkD!=''">
<div class="radioIconBox">
<input type="radio" class="radioInputCheck" :name="index"
v-on:change="checkOne(index,'D')" />
</div>
<div>D.</div>
<input v-model="item.checkD" disabled class="radioLineV2Input" />
</div>
<div class="BtnBox radioLineV2" v-if="item.checkE!=''">
<div class="radioIconBox">
<input type="radio" class="radioInputCheck" :name="index"
v-on:change="checkOne(index,'E')" />
</div>
<div>E.</div>
<input v-model="item.checkE" disabled class="radioLineV2Input" />
</div>
</div>
<div class="radioItemBox" v-if="item.questionType == '多选题'">
<div class="BtnBox radioItem">
<div>{{index+1}}.</div>
<textarea rows="18" cols="90" v-model="item.title" disabled></textarea>
<img src="img/collecQues.png" class="radioItemTypeImg"
v-on:click="collection(item.id)" />
</div>
<div class="BtnBox radioLineV2" v-if="item.checkA!=''">
<div class="radioIconBox">
<input type="checkbox" class="radioInputCheck" :name="index"
v-on:change="checkTwo($event,index,'A')" />
</div>
<div>A.</div>
<input v-model="item.checkA" disabled class="radioLineV2Input" />
</div>
<div class="BtnBox radioLineV2" v-if="item.checkB!=''">
<div class="radioIconBox">
<input type="checkbox" class="radioInputCheck" :name="index"
v-on:change="checkTwo($event,index,'B')" />
</div>
<div>B.</div>
<input v-model="item.checkB" disabled class="radioLineV2Input" />
</div>
<div class="BtnBox radioLineV2" v-if="item.checkC!=''">
<div class="radioIconBox">
<input type="checkbox" class="radioInputCheck" :name="index"
v-on:change="checkTwo($event,index,'C')" />
</div>
<div>C.</div>
<input v-model="item.checkC" disabled class="radioLineV2Input" />
</div>
<div class="BtnBox radioLineV2" v-if="item.checkD!=''">
<div class="radioIconBox">
<input type="checkbox" class="radioInputCheck" :name="index"
v-on:change="checkTwo($event,index,'D')" />
</div>
<div>D.</div>
<input v-model="item.checkD" disabled class="radioLineV2Input" />
</div>
<div class="BtnBox radioLineV2" v-if="item.checkE!=''">
<div class="radioIconBox">
<input type="checkbox" class="radioInputCheck" :name="index"
v-on:change="checkTwo($event,index,'E')" />
</div>
<div>E.</div>
<input v-model="item.checkE" disabled class="radioLineV2Input" />
</div>
</div>
<div class="radioItemBox" v-if="item.questionType == '判断题'">
<div class="BtnBox radioItem">
<div>{{index+1}}.</div>
<textarea rows="18" cols="90" v-model="item.title" disabled></textarea>
<img src="img/collecQues.png" class="radioItemTypeImg"
v-on:click="collection(item.id)" />
</div>
<div class="BtnBox radioLineV2" v-if="item.checkA!=''">
<div class="radioIconBox">
<input type="radio" class="radioInputCheck" :name="index"
v-on:change="checkOne(index,'A')" />
</div>
<div>A.</div>
<input v-model="item.checkA" disabled class="radioLineV2Input" />
</div>
<div class="BtnBox radioLineV2" v-if="item.checkB!=''">
<div class="radioIconBox">
<input type="radio" class="radioInputCheck" :name="index"
v-on:change="checkOne(index,'B')" />
</div>
<div>B.</div>
<input v-model="item.checkB" disabled class="radioLineV2Input" />
</div>
</div>
</div>
<div class="BtnBox margin-sm">
<div class="AddQuesBtnItem" v-on:click="SaveChange">
<img src="img/submit.png" />
提交
</div>
<div style="height:100px;"></div>
</div>
</div>
<div class="col-md-2"> </div>
</div>
</div>
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript" src="login/layui/layui.js"></script>
<script>
//轻量级框架
var dataInfo = new Vue({
el: "#content-page",
//Vue的数据对象
data: {
questionList: [],
pageTitle: ''
}, //数据对象结束
//方法
methods: {
GetAll: function() {
let vm = this;
let param = GetQueryString("param");
let type = GetQueryString("type");
let quesType = '';
if (param == 'practice') {
if (type == '0') {
quesType = '单选题';
} else if (type == '1') {
quesType = '多选题';
} else {
quesType = '判断题';
}
vm.pageTitle = '练习模式:' + quesType;
} else if (param == 'order') {
vm.pageTitle = '顺序出题';
} else {
vm.pageTitle = '随机练习';
}
var user = JSON.parse(sessionStorage.getItem('user'));
$.ajax({
url: "http://127.0.0.1:8081/common/answer-list?userId=" + user.id + "¶m=" +
param + "&type=" + quesType,
async: false,
type: "POST",
contentType: 'application/json',
dataType: 'json',
success: function(json) {
vm.questionList = json.list
}
});
},
//单选框选择事件
checkOne(index, check) {
let vm = this;
vm.questionList[index].isChecked = check;
},
//多选框选择事件
checkTwo(event, index, check) {
let vm = this;
const checked = event.target.checked;
let info = vm.questionList[index];
let checkedArray = info.isChecked.split(',');
if (checkedArray[0] == '') {
checkedArray.splice(0, 1);
}
if (checked) {
checkedArray.push(check);
info.isChecked = checkedArray.join(',');
} else {
let ind = checkedArray.indexOf(check);
if (ind !== -1) {
checkedArray.splice(ind, 1);
}
info.isChecked = checkedArray.join(',');
}
},
//点击提交
SaveChange() {
let vm = this;
//得分
let number = 0;
let list = vm.questionList;
for (let i = 0; i < list.length; i++) {
if (list[i].isChecked != '') {
let right = list[i].rightKey.split(',');
let check = list[i].isChecked.split(',');
if (right.length === check.length && right.sort().toString() === check.sort()
.toString()) {
list[i].correct = 1;
number++;
}
}
}
var user = JSON.parse(sessionStorage.getItem('user'));
var vo = {};
vo.answerList = list;
vo.number = number;
vo.userId = user.id;
vo.type = '练习';
$.ajax({
url: "http://127.0.0.1:8081/common/get-answer",
async: false,
type: "POST",
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify(vo),
success: function(json) {
layui.use('layer', function() {
var layer = layui.layer;
// 弹出提示框
layer.msg('您的分数为:' + number + '分', {
icon: 6, // 图标样式,默认为信息图标
time: 2000, // 显示时间,默认为2秒
shade: 0.5, // 遮罩层透明度,默认为0.3
shadeClose: true // 是否点击遮罩关闭弹框,默认为true
});
});
}
});
},
//点击收藏
collection(id) {
var vo = {};
var user = JSON.parse(sessionStorage.getItem('user'));
vo.userId = user.id;
vo.answerId = id;
$.ajax({
url: "http://127.0.0.1:8081/common/addCollect",
async: false,
type: "POST",
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify(vo),
success: function(json) {
layui.use('layer', function() {
var layer = layui.layer;
// 弹出提示框
layer.msg(json.returnMsg, {
icon: json.returnMsg == '您已收藏过' ? 5 :
6, // 图标样式,默认为信息图标
time: 2000, // 显示时间,默认为2秒
shade: 0.5, // 遮罩层透明度,默认为0.3
shadeClose: true // 是否点击遮罩关闭弹框,默认为true
});
});
}
});
},
}, //方法结束
created: function() {
var vm = this;
vm.GetAll();
}, //初始加载方法结束
}); //vue结束
function GetQueryString(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]);
return null;
}
</script>
</body>
</html>
<head>
<meta charset="utf-8" />
<title>答题统计</title>
</head>
<link href="css/index.css" rel="stylesheet" />
<link href="css/bootstrap.min.css" rel="stylesheet" />
<body style="background-color: #f7f7f7;">
<div class="headerBox">
<div class="logoBox">
<img src="img/logo1.png" />
<div class="logoTitle">在线答题闯关</div>
</div>
<div class="menuBox">
<div class="menuItem blackColor">
<a href="index.html">练习模式</a>
</div>
<div class="menuItem blackColor">
<a href="challengeLevels.html?param=primary">闯关模式</a>
</div>
<div class="menuItem blackColor">
<a href="wrongQuestion.html">我的错题</a>
</div>
<div class="menuItem blackColor">
<a href="myCollection.html">我的收藏</a>
</div>
<div class="menuItem activeMenu">
<a href="statistics.html">答题统计</a>
</div>
<div class="menuItem blackColor">
<a href="center.html">个人中心</a>
</div>
<div class="menuItem blackColor">
<a href="./login/login.html">退出登录</a>
</div>
</div>
</div>
<div class="container-fluid" id="content-page">
<div class="row">
<div class="col-md-2"> </div>
<div class="col-md-8">
<div class="searchBox">
<div class="leftTitle">
答题统计
</div>
<div>
</div>
</div>
</div>
<div class="col-md-2"> </div>
</div>
<div class="row">
<div class="col-md-2"> </div>
<div class="col-md-4">
<div id="main"></div>
</div>
<div class="col-md-4">
<div id="main2"></div>
</div>
<div class="col-md-2"> </div>
</div>
</div>
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="js/echarts.min.js"></script>
<script>
function GetQueryString(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]);
return null;
}
var user = JSON.parse(sessionStorage.getItem('user'));
$.ajax({
url: "http://127.0.0.1:8081/common/total?userId=" + user.id,
async: false,
type: "POST",
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify({}),
success: function(json) {
initCharts1(json.data);
var inputArray = json.data;
// 定义对应的题目类型名称数组
var typeNameArray = ['单选题', '多选题', '判断题'];
// 结果数组
var resultArray = [];
// 遍历输入数组
for (var i = 0; i < inputArray.length; i++) {
// 构造对象,并添加到结果数组中
var item = {
value: inputArray[i],
name: typeNameArray[i]
};
resultArray.push(item);
}
initCharts(resultArray);
}
});
function initCharts(data1) {
var chartDom = document.getElementById('main');
var myChart = echarts.init(chartDom);
var option;
option = {
title: {
text: '答题数统计',
subtext: '',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [{
name: 'Access From',
type: 'pie',
radius: '50%',
data: data1,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
};
myChart.setOption(option);
};
function initCharts1(data2) {
var chartDom = document.getElementById('main2');
var myChart = echarts.init(chartDom);
var option;
option = {
xAxis: {
type: 'category',
data: ['单选题', '多选题', '判断题']
},
yAxis: {
type: 'value'
},
series: [{
data: data2,
type: 'bar',
showBackground: true,
backgroundStyle: {
color: 'rgba(180, 180, 180, 0.2)'
}
}]
};
myChart.setOption(option);
};
</script>
</body>
</html>