作者主页:编程指南针
作者简介:Java领域优质创作者、CSDN博客专家 、掘金特邀作者、多年架构师设计经验、腾讯课堂常驻讲师
主要内容:Java项目、毕业设计、简历模板、学习资料、面试题库、技术互助
收藏点赞不迷路 关注作者有好处
文末获取源码
项目编号:
一,项目简介
随着计算机技术的不断发展,我们的日常生活和工作都与计算机技术的关系越来越密切。计算机技术的发展改变了我们日常的生活和工作习惯,也改变了社会的发展速度,使得我们的生活更加便利和高效。伴随着计算机技术发展起来的互联网技术将我们的生活带领进信息化时代,改变了我们的学习和工作环境,例如我们经常面对的考试也随着互联网技术的发展产生了改变,伴随着信息技术的发展,在线无纸化的考试系统应运而生,不仅彻底改变了传统纸质考试的习惯和环境,更是提高了考试效率,保证了考试效果,达到了考试目的[1]。传统的纸质考试具有很多局限性和不足,主要包括以下几点:
1.传统纸质考试需要较多的人力资源和时间资源进行题目的设定,同时题目的难易程度和考核价值水平很难达到基本的要求;
2.传统纸质考试的阅卷采用人工的方式,人工阅卷难免会出现阅卷差错或者分数合算差错,这也会对考试的效果造成影响;
3.传统纸质考试的人工阅卷模式也会浪费大量的人力资源和时间资源,不能保证工作效率和工作质量;
4.传统纸质考试对考试的总结能力较差,不能够全面具体的分析考试结果,教师也很难得到基本的考试结果分析的数据信息,而这些数据信息是提高教学质量和教学效果的关键因素;
5.传统纸质考试对考试时间以及考试纪律的要求不能达到统一,这也会影响到考试的公平性。根据以上分析的传统纸质考试的不足之处,新型的结合计算机技术以及互联网技术的在线考试系统应运而生,不仅通过一种新的技术解决了传统纸质考试的基本问题,还提供了一种新的考试思路和考试理念,纠正了传统纸质考试的弊端,提供更加合理有效的考试过程。
二,环境介绍
三,技术说明
后端系统 | 前端系统 | 微信小程序 |
---|---|---|
spring-boot 2.1.6.RELEASE | vue 采用新版,使用了vue-cli4搭建的系统 | iView 主题样式 |
spring-boot-security 用户登录验证 | element-ui 最流行的vue UI框架 | |
undertow web容器 | vue-element-admin 模版 | |
mybatis/mybatis_plus | echarts 图表统计 | |
hikari 速度最快的数据库连接池 | ueditor 题目编辑器 |
四,功能列表
4.1 学生系统功能
模块 | 介绍 |
---|---|
登录 | 用户名、密码 |
注册 | 年级、用户名、密码 |
任务中心 | 管理员发布的年级任务,每个学生只能做一次 |
考试 | 题干支持文本、图片、数学公式、表格等,学生答题支持:文本 |
固定试卷 | 可重复练习、自行批改的试卷 |
时段试卷 | 在时间限制内,可重复练习、自行批改的试卷 |
考试记录 | 查看答卷记录和试卷信息 |
错题本 | 答错题目会自动进入错题本,显示题目基本信息 |
个人信息 | 显示学生个人资料 |
更新信息 | 修改个人资料、头像 |
个人动态 | 显示用户最近的个人动态 |
消息中心 | 用于接收管理员发送的消息 |
4.2 管理系统功能
模块 | 介绍 |
---|---|
登录 | 用户名、密码 |
主页 | 试卷总数、题目总数、用户活跃度、题目月数量 |
学生列表 | 显示系统所有的学生,新增、修改、删除、禁用 |
管理员列表 | 显示系统所有的管理员,新增、修改、删除、禁用 |
学科列表 | 学科查询、修改、删除 |
学科创编 | 创建学科 |
试卷列表 | 试卷查询、修改、删除 |
试卷创编 | 创建的试卷为时段试卷、固定试卷、任务试卷 |
题目列表 | 题目查询、修改、删除 |
题目创建 | 题目支持单选题、多选题、判断题、填空题、简答题,题干支持文本、图片、表格、数学公式 |
任务列表 | 任务查询、修改、删除 |
消息列表 | 显示已发送的消息,消息已读人数等信息 |
消息发送 | 发送消息给多个用户 |
用户日志 | 显示所有用户日志 |
个人资料 | 显示管理员用户名、真实姓名 |
时间线 | 显示管理员创建时间 |
修改资料 | 修改姓名、手机号 |
4.3 小程序功能
模块 | 介绍 |
---|---|
登录 | 用户登录登出功能,登录会自动绑定微信账号,登出会解绑 |
注册 | 年级、用户名、密码 |
任务中心 | 管理员发布的年级任务,每个学生只能做一次 |
考试 | 题干支持文本、图片、数学公式、表格等,学生答题支持:文本 |
固定试卷 | 可重复练习、自行批改的试卷 |
时段试卷 | 在时间限制内,可重复练习、自行批改的试卷 |
考试记录 | 查看答卷记录和试卷信息 |
错题本 | 答错题目会自动进入错题本,显示题目基本信息 |
个人信息 | 显示学生个人资料 |
更新信息 | 修改个人资料、头像 |
个人动态 | 显示用户最近的个人动态 |
消息中心 | 用于接收管理员发送的消息 |
五,数据库设计
仅展示部分数据库字段设计
5.1 试卷表
字段名 | 类型 | 注释 |
---|---|---|
id | int | |
name | varchar | 试卷名称 |
subject_id | int | 学科 |
paper_type | int | 试卷类型( 1.固定试卷 4.时段试卷 6.任务试卷 ) |
grade_level | int | 年级 |
score | int | 试卷总分(千分制) |
question_count | int | 题目数量 |
suggest_time | int | 建议时长(分钟) |
limit_start_time | datetime | 时段试卷 开始时间 |
limit_end_time | datetime | 时段试卷 结束时间 |
frame_text_content_id | int | 试卷框架 内容为JSON |
create_user | int | |
create_time | datetime | |
deleted | bit | |
task_exam_id | int |
5.2 试卷答案表
字段名 | 类型 | 注释 |
---|---|---|
id | int | |
exam_paper_id | int | |
paper_name | varchar | 试卷名称 |
paper_type | int | 试卷类型( 1.固定试卷 4.时段试卷 6.任务试卷 ) |
subject_id | int | 学科 |
system_score | int | 系统判定得分 |
user_score | int | 最终得分(千分制) |
paper_score | int | 试卷总分 |
question_correct | int | 做对题目数量 |
question_count | int | 题目总数量 |
do_time | int | 做题时间(秒) |
status | int | 试卷状态(1待判分 2完成) |
create_user | int | 学生 |
create_time | datetime | 提交时间 |
task_exam_id | int |
5.3 题目表
字段名 | 类型 | 注释 |
---|---|---|
id | int | |
question_type | int | 1.单选题 2.多选题 3.判断题 4.填空题 5.简答题 |
subject_id | int | 学科 |
score | int | 题目总分(千分制) |
grade_level | int | 级别 |
difficult | int | 题目难度 |
correct | text | 正确答案 |
info_text_content_id | int | 题目 填空、 题干、解析、答案等信息 |
create_user | int | 创建人 |
status | int | 1.正常 |
create_time | datetime | 创建时间 |
deleted | bit |
5.4 学科表
字段名 | 类型 | 注释 |
---|---|---|
id | int | |
name | varchar | 语文 数学 英语 等 |
level | int | 年级 (1-12) 小学 初中 高中 大学 |
level_name | varchar | 一年级、二年级等 |
item_order | int | 排序 |
deleted | bit |
5.5 用户表
字段名 | 类型 | 注释 |
---|---|---|
id | int | |
user_uuid | varchar | |
user_name | varchar | 用户名 |
password | varchar | |
real_name | varchar | 真实姓名 |
age | int | |
sex | int | 1.男 2女 |
birth_day | datetime | |
user_level | int | 学生年级(1-12) |
phone | varchar | |
role | int | 1.学生 3.管理员 |
status | int | 1.启用 2禁用 |
image_path | varchar | 头像地址 |
create_time | datetime | |
modify_time | datetime | |
last_active_time | datetime | |
deleted | bit | 是否删除 |
wx_open_id | varchar | 微信openId |
5.6 用户日志表
字段名 | 类型 | 注释 |
---|---|---|
id | int | |
user_id | int | 用户id |
user_name | varchar | 用户名 |
real_name | varchar | 真实姓名 |
content | text | 内容 |
create_time | datetime | 时间 |
其他表的设计省略............
六,系统展示
6.1 后台管理
主页
用户管理
试卷管理
题目管理
添加题目
添加试卷
任务管理
教育管理
成绩管理
6.2 学生端
首页登录与注册
学生端首页
试卷中心
考试记录
错题本
个人中心
6.3 小程序端
登录与注册
首页
试卷考试
考试记录
我的
七,核心代码展示
@Servicepublic class TaskExamServiceImpl extends BaseServiceImpl<TaskExam> implements TaskExamService { protected final static ModelMapper modelMapper = ModelMapperSingle.Instance(); private final TaskExamMapper taskExamMapper; private final TextContentService textContentService; private final ExamPaperMapper examPaperMapper; @Autowired public TaskExamServiceImpl(TaskExamMapper taskExamMapper, TextContentService textContentService, ExamPaperMapper examPaperMapper) { super(taskExamMapper); this.taskExamMapper = taskExamMapper; this.textContentService = textContentService; this.examPaperMapper = examPaperMapper; } @Override public PageInfo<TaskExam> page(TaskPageRequestVM requestVM) { return PageHelper.startPage(requestVM.getPageIndex(), requestVM.getPageSize(), "id desc").doSelectPageInfo(() -> taskExamMapper.page(requestVM) ); } @Override @Transactional public void edit(TaskRequestVM model, User user) { ActionEnum actionEnum = (model.getId() == null) ? ActionEnum.ADD : ActionEnum.UPDATE; TaskExam taskExam = null; if (actionEnum == ActionEnum.ADD) { Date now = new Date(); taskExam = modelMapper.map(model, TaskExam.class); taskExam.setCreateUser(user.getId()); taskExam.setCreateUserName(user.getUserName()); taskExam.setCreateTime(now); taskExam.setDeleted(false); //保存任务结构 TextContent textContent = textContentService.jsonConvertInsert(model.getPaperItems(), now, p -> { TaskItemObject taskItemObject = new TaskItemObject(); taskItemObject.setExamPaperId(p.getId()); taskItemObject.setExamPaperName(p.getName()); return taskItemObject; }); textContentService.insertByFilter(textContent); taskExam.setFrameTextContentId(textContent.getId()); taskExamMapper.insertSelective(taskExam); } else { taskExam = taskExamMapper.selectByPrimaryKey(model.getId()); modelMapper.map(model, taskExam); TextContent textContent = textContentService.selectById(taskExam.getFrameTextContentId()); //清空试卷任务的试卷Id,后面会统一设置 List<Integer> paperIds = JsonUtil.toJsonListObject(textContent.getContent(), TaskItemObject.class) .stream() .map(d -> d.getExamPaperId()) .collect(Collectors.toList()); examPaperMapper.clearTaskPaper(paperIds); //更新任务结构 textContentService.jsonConvertUpdate(textContent, model.getPaperItems(), p -> { TaskItemObject taskItemObject = new TaskItemObject(); taskItemObject.setExamPaperId(p.getId()); taskItemObject.setExamPaperName(p.getName()); return taskItemObject; }); textContentService.updateByIdFilter(textContent); taskExamMapper.updateByPrimaryKeySelective(taskExam); } //更新试卷的taskId List<Integer> paperIds = model.getPaperItems().stream().map(d -> d.getId()).collect(Collectors.toList()); examPaperMapper.updateTaskPaper(taskExam.getId(), paperIds); model.setId(taskExam.getId()); } @Override public TaskRequestVM taskExamToVM(Integer id) { TaskExam taskExam = taskExamMapper.selectByPrimaryKey(id); TaskRequestVM vm = modelMapper.map(taskExam, TaskRequestVM.class); TextContent textContent = textContentService.selectById(taskExam.getFrameTextContentId()); List<ExamResponseVM> examResponseVMS = JsonUtil.toJsonListObject(textContent.getContent(), TaskItemObject.class).stream().map(tk -> { ExamPaper examPaper = examPaperMapper.selectByPrimaryKey(tk.getExamPaperId()); ExamResponseVM examResponseVM = modelMapper.map(examPaper, ExamResponseVM.class); examResponseVM.setCreateTime(DateTimeUtil.dateFormat(examPaper.getCreateTime())); return examResponseVM; }).collect(Collectors.toList()); vm.setPaperItems(examResponseVMS); return vm; } @Override public List<TaskExam> getByGradeLevel(Integer gradeLevel) { return taskExamMapper.getByGradeLevel(gradeLevel); }}
@Servicepublic class AuthenticationServiceImpl implements AuthenticationService { private final UserService userService; private final SystemConfig systemConfig; @Autowired public AuthenticationServiceImpl(UserService userService, SystemConfig systemConfig) { this.userService = userService; this.systemConfig = systemConfig; } /** * @param username username * @param password password * @return boolean */ @Override public boolean authUser(String username, String password) { User user = userService.getUserByUserName(username); return authUser(user, username, password); } @Override public boolean authUser(User user, String username, String password) { if (user == null) { return false; } String encodePwd = user.getPassword(); if (null == encodePwd || encodePwd.length() == 0) { return false; } String pwd = pwdDecode(encodePwd); return pwd.equals(password); } @Override public String pwdEncode(String password) { return RsaUtil.rsaEncode(systemConfig.getPwdKey().getPublicKey(), password); } @Override public String pwdDecode(String encodePwd) { return RsaUtil.rsaDecode(systemConfig.getPwdKey().getPrivateKey(), encodePwd); }}
<view class="exam-page"> <view class="view-wrap"> <view class="exam-count-down">{{remainTimeStr}}</view> </view> <view class="view-wrap-hidden"> </view> <view> <view class="exam-name-title"> <h1>{{form.name}}</h1> </view> <form bindsubmit='formSubmit'> <i-panel title="{{titleItem.name}}" wx:for="{{form.titleItems}}" wx:for-item="titleItem" wx:key="{{titleItem.name}}" i-class="exam-panel-title"> <i-cell-group i-class="exam-cell"> <i-cell wx:for="{{titleItem.questionItems}}" wx:key="{{titleItem.id}}" wx:for-item="questionItem"> <view wx:if="{{questionItem.questionType === 1}}"> <rich-text nodes="{{questionItem.itemOrder}}. {{questionItem.title}}" /> <radio-group class="radio-group" name="{{questionItem.itemOrder}}_{{questionItem.id}}_{{questionItem.questionType}}"> <label class="radio" wx:for="{{questionItem.items}}" wx:key="{{questionItem.prefix}}" wx:for-item="radioItem" class="exam-radio-item-label"> <radio color="#2d8cf0" value="{{radioItem.prefix}}" checked="{{radioItem.checked}}" class="exam-item-left" /> <rich-text nodes="{{radioItem.prefix}}. {{radioItem.content}}" class="exam-item-left" /> </label> </radio-group> </view> <view wx:elif="{{questionItem.questionType === 2}}"> <rich-text nodes="{{questionItem.itemOrder}}. {{questionItem.title}}" class="exam-item-left" style="line-height:35px" /> <checkbox-group class="exam-item-left" style="margin-left:10px" name="{{questionItem.itemOrder}}_{{questionItem.id}}_{{questionItem.questionType}}"> <label wx:for="{{questionItem.items}}" wx:key="{{questionItem.prefix}}" wx:for-item="radioItem" class="exam-radio-item-label"> <checkbox color="#2d8cf0" value="{{radioItem.prefix}}" checked="{{radioItem.checked}}" class="exam-item-left" /> <rich-text nodes="{{radioItem.prefix}}. {{radioItem.content}}" class="exam-item-left" /> </label> </checkbox-group> </view> <view wx:elif="{{questionItem.questionType === 3}}"> <rich-text nodes="{{questionItem.itemOrder}}. {{questionItem.title}}" class="exam-item-left" style="line-height:35px" /> <radio-group class="radio-group" class="exam-item-left" style="margin-left:10px" name="{{questionItem.itemOrder}}_{{questionItem.id}}_{{questionItem.questionType}}"> <label class="radio" wx:for="{{questionItem.items}}" wx:key="{{questionItem.prefix}}" wx:for-item="radioItem" class="exam-radio-item-label"> <radio color="#2d8cf0" value="{{radioItem.prefix}}" checked="{{radioItem.checked}}" class="exam-item-left" /> <rich-text nodes="{{radioItem.content}}" class="exam-item-left" /> </label> </radio-group> </view> <view wx:elif="{{questionItem.questionType === 4}}"> <rich-text nodes="{{questionItem.itemOrder}}. {{questionItem.title}}" /> <view class="exam-input-contain" wx:for="{{questionItem.items}}" wx:key="{{questionItem.prefix}}" wx:for-item="inputItem" wx:for-index="idx"> <view class="exam-input-contain-label">{{inputItem.prefix}}</view> <input class="exam-input-contain-content" maxlength="-1" name="{{questionItem.itemOrder}}_{{questionItem.id}}_{{questionItem.questionType}}_{{idx}}" /> </view> </view> <view wx:else> <rich-text nodes="{{questionItem.itemOrder}}. {{questionItem.title}}" /> <view class="exam-textarea-contain"> <textarea placeholder="答案" maxlength="-1" name="{{questionItem.itemOrder}}_{{questionItem.id}}_{{questionItem.questionType}}"></textarea> </view> </view> </i-cell> </i-cell-group> </i-panel> <view> <button class="i-btn i-btn-primary i-btn-square" form-type='submit'>提交</button> </view> <i-action-sheet visible="true" visible="{{timeOutShow}}" mask-closable="{{ false }}"> <view slot="header" style="padding: 16px"> <view class="exam-timeout-title">考试试卷结束,请提交试卷!</view> <button class="i-btn i-btn-primary i-btn-square" form-type='submit'>提交</button> </view> </i-action-sheet> </form> <i-modal title="考试结果" visible="{{modalShow}}" bind:ok="returnRecord" bind:cancel="returnRecord"> <view>得分:{{result}}</view> </i-modal> <i-spin size="large" fix wx:if="{{ spinShow }}"></i-spin> <i-message id="message" /> </view></view>
八,项目部署
8.1 目录结构
获取到源码进行解压后,文件列表如下:
8.2 导入数据库
打开Navicat(或者其他数据库连接工具也可),导入资料中的sql文件。
8.3 后端源码部署
8.3.1 导入源码
打开idea,新建工程【可以任意目录】。把解压后的xzs目录复制到创建好的idea工程中。进行导入。
导入进来之后,idea会进行编译。编译之后没有出现错误说明导入成功。
8.3.2 修改配置文件
修改数据库连接配置,在application-dev.yml配置文件中修改成自己的数据库名和自己的密码。
8.3.3 启动项目
SpringBoot的程序启动类,相信大部分小伙伴们都知道该怎么启动。不过在这里还是要写一下,以防个别小伙伴不知道。在src下找到XzsApplication启动类,在该类中进行右键运行即可。最后查看控制台有没有报错信息。没有报错信息,启动成功。
上面说明启动成功
8.4 管理员端和学生端部署
管理员端和学生端部署操作都是一样的,在这里以管理员端为例进行演示。
打开vscode【用其他前端开发工具打开也可】,导入资料中的vue目录下的xzs-admin工程,学生端是xzs-student。打开vscode的终端,进行安装依赖和启动项目。
8.4.1 安装依赖
命令:npm install
8.4.2 启动项目
命令:npm run serve
运行成功,端口号8002
8.5 小程序端部署
8.5.1 微信小程序开发工具下载与安装
开发工具的官方下载地址为:https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/download.html。
不支持Windows XP和Windows 7系统,建议使用WIN10。我这里选择Windows 64版本的安装包。
安装过程比较简单,不用设置什么,按照提示来就行了。下面是每一步详细截图
8.5.2 项目部署
打开微信开发工具---> 选择导入--->找到资料中的wx目录,导入xzs_student
到这一步会提示,输入appId,如果自己有就写自己的,没有的话,点击测试,使用测试号
导入进来之后,会自动提示是否运行,选择【信任并运行】
运行成功 。到此整个项目部署就已经完成了
九,项目总结
该项目是PC端+小程序端。Java做为后端支持。代码结构规整,源码容易阅读,功能完善,非常适合做为毕设来使用。