尧图精选

Spring Boot题库管理系统毕设实战:数据模型、组卷算法与权限设计

🕒 发布时间:2026/9/10 2:38:53 📁 来源:尧图网络
简介一套基于Spring Boot的题库管理系统毕业设计项目包面向计算机相关专业正在做毕设的学生及需要实战练习的Java学习者也可直接作为课程设计或期末大作业使用。系统完整涵盖在线考试、题库管理、讨论区、个人中心与后台管理五大模块在线考试支持倒计时、考试安排、答题卡、自动批改及成绩查看题库支持课程分类、题目难度与标签管理讨论区具备发帖、回帖、评论、点赞和浏览统计个人中心可查看考试记录与统计分析后台则包含用户权限、考试、题目、课程、班级、成绩等全面管理功能。资源包共1087个文件以Java源码、前端页面和配置文件为主涵盖java、js、xml、css、html等类型压缩包仅8.21MB便于快速下载部署。已有2655人学习适合需要直接落地或二次开发的场景。资源中包含完整数据库脚本和项目说明代码经过严格调试结构清晰可直接支撑毕业设计答辩及后续功能扩展。1. 用 Spring Boot 拆一个题库管理系统毕设选题前先想清楚这几个点大多数同学拿到「基于 Spring Boot 的题库管理系统源码 数据库脚本」这类压缩包时第一反应都是导入数据库、启动项目、截几张图然后丢进论文附录。但答辩现场往往会被问住「组卷时怎么保证难度分布均匀」「多选的答案为什么用字符串存而不是 JSON」「JWT 过期了怎么办」。这套系统看起来是普通 CRUD真正能体现工作量的是题目数据模型、组卷策略、判分逻辑和权限边界这几个点。本文从这些可以被追问的细节入手先讲清楚表结构怎么设计再逐步落到 Spring Boot 的代码实现最后给出导入数据库脚本和接口自测的完整步骤。目标是让你不光能跑起来还能在被质疑时讲出设计理由。2. 题库管理系统的数据模型设计从题目表到试卷模板2.1 题目类型决定表结构单选、多选、判断、简答怎么落库题库系统的核心是exam_question这张表。题目类型一般用 type 字段区分1 单选、2 多选、3 判断、4 简答。单选和判断的答案可以直接用字符串多选如果选 ABC答案就存 ABC简答则需要单独存参考答案要点。这样设计的优点是统一查询和索引简单不需要为每种题型建子表。CREATE TABLE exam_question ( id bigint(20) NOT NULL AUTO_INCREMENT, type tinyint(4) NOT NULL COMMENT 1单选 2多选 3判断 4简答, subject varchar(500) NOT NULL COMMENT 题干, options text COMMENT 选项JSON数组字符串, answer varchar(1000) DEFAULT NULL COMMENT 标准答案, analysis varchar(2000) DEFAULT NULL COMMENT 解析, difficulty tinyint(4) DEFAULT 1 COMMENT 1易 2中 3难, category_id bigint(20) DEFAULT NULL COMMENT 所属分类, create_by varchar(64) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, del_flag tinyint(1) DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT题目表;options字段用文本存储 JSON 数组例如单选存[A,B,C,D]。为什么不直接做四个列option_a、option_b因为判断题、简答题没有选项简答题如果后续要扩展填空、综合题全部用列会越加越多。JSON 字符串在读取后用JSONArray.parseArray转成ListString即可MySQL 5.7 也支持json类型但考虑到兼容性毕设和中小型管理项目用text更省事。2.2 课程、章节与知识点的树形组织题目分类不能只用一个扁平字段真实场景里题目属于某门课程课程下有章节章节下再挂知识点。分类表用父子关系实现一棵树CREATE TABLE exam_category ( id bigint(20) NOT NULL AUTO_INCREMENT, parent_id bigint(20) DEFAULT 0 COMMENT 父分类id0为根, name varchar(100) NOT NULL COMMENT 分类名称, level tinyint(4) NOT NULL COMMENT 层级1课程 2章节 3知识点, sort int(11) DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT题目分类表;查询时常见的做法是先一次性查出全部分类再在内存里组装成树。SQL 只需要按parent_id排序然后通过 Map 合并子节点。如果数据量在几千条以内完全不需要递归查询数据库。组卷时用户选择某个课程节点系统需要把该节点及其所有子节点下的题目都算作候选范围这一步在 Service 层先查出一串子孙分类 id再作为IN条件传进去。2.3 数据库脚本直接可用MySQL 版本建表 SQL 整理压缩包里的数据库脚本一般包含建库、建表和初始化数据三部分。最稳的导入方式是先用命令行或客户端创建数据库再指定字符集导入mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS exam_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -uroot -p exam_db sql/exam_db.sql脚本里除了exam_question、exam_category还应有这几张表表名作用关键字段sys_user用户表id, username, password, real_name, role_idsys_role角色表id, role_code, role_nameexam_paper试卷表id, title, total_score, duration_minutes, creator_id, statusexam_paper_question试卷题目关联表id, paper_id, question_id, score, sort_noexam_record考试记录表id, paper_id, user_id, start_time, submit_time, scoreexam_record_answer学生答题明细id, record_id, question_id, user_answer, is_correct, scoreexam_paper_question是试卷与题目的多对多关联除了关联两个 id还应该有score字段因为同一个题目出现在不同试卷中可以有不同的分值。exam_record_answer保存学生每次作业的作答结果这样错题本功能就可以直接从这张表统计。2.4 导入数据库脚本的 3 个常见坑第一个坑是字符集不一致。Windows 下用 Navicat 打开脚本时文件本身是 UTF-8但客户端连接用的可能是 gbk导致中文乱码。解决方法是导入前先执行SET NAMES utf8mb4;或者直接在连接参数里指定characterEncodingutf8mb4。第二个坑是时间字段默认值。在 MySQL 5.7 之前DEFAULT CURRENT_TIMESTAMP只能用在timestamp类型上如果脚本里的datetime带了这个默认值导入会报版本错误。可以手动改成DEFAULT CURRENT_TIMESTAMP加版本兼容或者直接去掉默认值由 Java 代码在插入时统一填充LocalDateTime.now()。第三个坑是外键和删除顺序。题库系统的表之间最好不要物理外键而是靠逻辑关联。否则清表数据时必须先删exam_record_answer、exam_record、exam_paper_question再删exam_paper最后才能删exam_question。很多毕业设计项目跑不起来就是因为在测试时直接 truncate 主表被外键约束卡住了。3. Spring Boot 项目骨架与题库核心模块的实现3.1 项目分层与关键依赖题库管理系统用 Spring Boot MyBatis-Plus 是最常见的选择。MyBatis-Plus 提供BaseMapper单表 CRUD 不用写 XML能省下大量时间对于答辩时展示项目进度很有帮助。Maven 依赖的核心部分如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.14/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.alibaba/groupId artifactIdfastjson/artifactId version2.0.32/version /dependency dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.3.2/version /dependency /dependencies项目结构按 controller、service、mapper、entity、config 分包。控制器只做参数接收和结果封装业务逻辑全部放在 Service 层。题目管理接口关心的是分页查询、按题型过滤、按分类过滤、保存时校验答案格式。以下是一个典型的题目 ControllerRestController RequestMapping(/api/question) public class QuestionController { Autowired private QuestionService questionService; GetMapping(/page) public ResultIPageQuestion page(RequestParam(defaultValue 1) long page, RequestParam(defaultValue 10) long size, RequestParam(required false) Integer type, RequestParam(required false) Long categoryId) { return Result.ok(questionService.pageQuery(page, size, type, categoryId)); } PostMapping public Result? save(RequestBody Question question) { questionService.checkAndSave(question); return Result.ok(); } DeleteMapping(/{id}) public Result? delete(PathVariable Long id) { questionService.deleteById(id); return Result.ok(); } }pageQuery内部使用 MyBatis-Plus 的LambdaQueryWrapper动态拼接条件type和categoryId为空时不参与过滤这是最常见的查询写法。参数说明page和size控制分页type传入 1 到 4对应四种题型categoryId如果不传就查出该分类及其子分类下所有题目。3.2 题目批量导入用 EasyExcel 读 Excel 省去手输手工录入几百道题效率太低批量导入是题库系统里被问得最多的话题。EasyExcel 监听器是最稳妥的方案public class QuestionExcelListener extends AnalysisEventListenerQuestionExcelRow { private final IQuestionService questionService; private final ListQuestion cache new ArrayList(); private static final int BATCH_COUNT 100; public QuestionExcelListener(IQuestionService questionService) { this.questionService questionService; } Override public void invoke(QuestionExcelRow row, AnalysisContext context) { Question question new Question(); question.setType(row.getType()); question.setSubject(row.getSubject()); question.setOptions(row.getOptions()); question.setAnswer(row.getAnswer()); question.setAnalysis(row.getAnalysis()); question.setDifficulty(row.getDifficulty()); cache.add(question); if (cache.size() BATCH_COUNT) { questionService.saveBatch(cache); cache.clear(); } } Override public void doAfterAllAnalysed(AnalysisContext context) { if (!cache.isEmpty()) { questionService.saveBatch(cache); } } }监听器的invoke每解析一行就会调用一次但入库操作不应当每行都做所以用cache攒到 100 条再批量保存减少数据库连接损耗。doAfterAllAnalysed负责处理最后不足 100 条的数据。Excel 里的列顺序需要和QuestionExcelRow的字段顺序保持一致否则会出现某个字段串位。3.3 随机组卷算法按题型和难度分布取题手工选题适合出小测验正式模拟考试通常要随机组卷。随机组卷的核心参数是试卷满分、各题型题量、各难度比例。我常用的做法是拆成两步第一步按题型分组第二步在每个分组里按权重取题目。public ListExamPaperQuestion generatePaper(PaperConfig config) { ListExamPaperQuestion paperQuestions new ArrayList(); for (QuestionTypeConfig typeConfig : config.getTypeConfigs()) { // 按难度分段每段独立抽样避免直接用 ORDER BY RAND() 导致分布失控 for (DifficultyConfig diff : typeConfig.getDifficultyConfigs()) { int needCount diff.getCount(); if (needCount 0) { continue; } LambdaQueryWrapperQuestion wrapper new LambdaQueryWrapper(); wrapper.eq(Question::getType, typeConfig.getType()) .eq(Question::getDifficulty, diff.getLevel()) .last(ORDER BY RAND() LIMIT needCount); ListQuestion selected questionMapper.selectList(wrapper); if (selected.size() needCount) { throw new BizException(题型 typeConfig.getType() 难度 diff.getLevel() 的题目不足请补充题库); } for (Question q : selected) { ExamPaperQuestion pq new ExamPaperQuestion(); pq.setQuestionId(q.getId()); pq.setScore(typeConfig.getScorePerQuestion()); paperQuestions.add(pq); } } } return paperQuestions; }ORDER BY RAND()在小数据量单次查询几百条下是可接受的毕设项目不需要引入复杂洗牌算法。但必须注意题目不足时不要静默跳过直接抛异常否则生成的试卷总分达不到设定值。参数上typeConfig指单选题 10 题、多选题 5 题这样的组合difficultyConfigs指「易 40%、中 40%、难 20%」的比例拆分。每个难度段独立随机才能保证一张试卷不是全简单题。3.4 在线答题与自动判分学生提交答案后系统要逐题判断对错。单选、判断直接比较字符串多选需要把用户的选项拆成字符集合后再比较顺序不能影响结果public boolean checkAnswer(Question question, String userAnswer) { if (question.getType() 2) { SetString standard new HashSet(Arrays.asList( question.getAnswer().split())); SetString user new HashSet(Arrays.asList( userAnswer.replace(,, ).split())); return standard.equals(user); } // 单选、判断直接用 trim 后比较 return question.getAnswer().trim().equalsIgnoreCase(userAnswer.trim()); }多选判分有个规则要想清楚漏选、错选是否给分。如果计分规则是「多选、少选、错选均不得分」那么standard.equals(user)就够了。如果「少选得一半分」就需要在分数计算时单独处理。业务上建议把判分逻辑收敛到一个ScoreCalculator类中而不是散落在 RecordService 里方便答辩时说明可扩展性。4. 用户权限与考试流程Spring Security JWT4.1 三角色权限模型题库系统至少要有三种角色管理员负责基础配置和用户管理教师负责题目维护、组卷、阅卷学生只能参加考试和查看成绩。数据库里sys_user通过role_id关联sys_role没有做成 user_role 中间表因为一个用户在当前系统里只需要一种角色。若后续要支持「既是教师又是管理员」再扩展中间表即可这是常见的过度设计取舍。接口权限用 Spring Security 的PreAuthorize注解控制例如PreAuthorize(hasRole(ADMIN) or hasRole(TEACHER)) PostMapping(/api/question) public Result? save(RequestBody Question question) { ... } PreAuthorize(hasRole(STUDENT)) PostMapping(/api/exam/submit) public Result? submit(RequestBody ExamSubmitVO submitVO) { ... }角色代码存的是ROLE_ADMIN、ROLE_TEACHER、ROLE_STUDENT在 JWT 的 claims 里放入role过滤器解析后交给SecurityContextHolder。4.2 JWT 登录与过滤器的配置方式JWT 可以避免每次请求都去数据库查 session在管理系统中很流行。生成 token 的代码集中在JwtUtil中public String generateToken(Long userId, String username, String role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 12)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }过滤器里只做三件事从Authorization头取出Bearer token、校验签名、把用户身份放进SecurityContext。放行路径要明确/api/auth/login、/doc.html、静态资源不需要 token。某一次被拦截的常见原因是前端传的是token头而后端过滤器读的是Authorization两者不一致就 401。4.3 考试记录与自动算分闭环学生提交试卷时后端接收recordId和answers列表。事务方法内需要完成更新exam_record的submit_time和end_time遍历answers调用判分逻辑批量插入exam_record_answer最后累加成绩写回exam_record。这里的核心是避免提交两次导致重复计算所以exam_record里加一个status字段提交后状态从 1考试中改为 2已提交再次提交直接拒绝。Transactional(rollbackFor Exception.class) public ExamResult submit(ExamSubmitVO submitVO) { ExamRecord record recordMapper.selectById(submitVO.getRecordId()); if (record null || record.getUserId().longValue() ! currentUserId()) { throw new BizException(考试记录不存在); } if (record.getStatus() 2) { throw new BizException(试卷已提交请勿重复操作); } ListRecordAnswer answers buildAnswers(submitVO.getAnswers()); answerMapper.insertBatch(answers); int totalScore sumScore(answers); record.setSubmitTime(LocalDateTime.now()); record.setStatus(2); record.setScore(totalScore); recordMapper.updateById(record); return new ExamResult(totalScore, answers.size(), countCorrect(answers)); }事务注解保证提交、写明细、更新分数要么全部成功要么全部回滚。答辩时遇到追问「如果判分中间出错了怎么办」可以直接回答利用了Transactional的回滚机制并在catch中记录日志。5. 启动校验与答辩准备把项目跑通并自测5.1 启动前检查数据源和配置解压源码后先改application.yml确认数据源、端口、文件上传路径与本地环境一致server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/exam_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: delFlag logic-delete-value: 1 logic-not-delete-value: 0serverTimezoneAsia/Shanghai能规避数据库时区和 Java 本地时间相差 8 小时的问题。如果项目中用了 Redis 缓存还需要在application.yml里增加spring.redis.host、spring.redis.port没有安装 Redis 的机器上要先把缓存相关代码临时注释掉。5.2 接口自测清单答辩前至少跑通这 6 个用例接口功能请求方式请求路径关键参数预期结果管理员登录POST/api/auth/loginusername, password返回 token 和角色分页查询题目GET/api/question/pagepage1size10type1返回单选题目列表新增题目POST/api/question题目 JSON状态 200数据入库手动组卷POST/api/paper/manualpaperId, questionIds试卷题目数量正确学生开始考试POST/api/exam/startpaperId创建考试记录提交试卷POST/api/exam/submitrecordId, answers返回成绩和正确数建议用 Postman 导出环境变量把 token 放在全局请求头的Authorization中。对于学生考试接口需要先换一个学生账号登录再执行考试流程否则会因权限不足被 Spring Security 拦截。5.3 高频报错定位方法启动时报Failed to configure a DataSource说明数据源配置没读到优先检查application.yml位置是否在src/main/resources下以及SpringBootApplication扫描的包路径是否覆盖了 mapper。Invalid bound statement (not found)是 MyBatis 常见问题检查MapperScan是否指向正确包名XML 文件的 namespace 是否和接口全限定名一致。用 MyBatis-Plus 的BaseMapper时不会遇到但自己写的自定义 SQL 很容易在 XML 路径上出错。跨域报错时在WebMvcConfigurer中加一个全局CorsFilter允许来源为前端地址。注意spring.mvc.cors配置无法覆盖 Spring Security 的过滤器链安全配置里要显式放行 OPTIONS 请求。5.4 可现场演示的加分功能题库系统最容易被低估的是统计能力。基于exam_record和exam_record_answer可以增加一个「答题统计」页面按知识点维度显示正确率。这不需要额外写复杂聚合用一条 SQL 就能实现SELECT c.name AS category_name, COUNT(a.id) AS total_count, SUM(CASE WHEN a.is_correct 1 THEN 1 ELSE 0 END) AS correct_count FROM exam_record_answer a LEFT JOIN exam_question q ON a.question_id q.id LEFT JOIN exam_category c ON q.category_id c.id WHERE a.record_id IN (SELECT id FROM exam_record WHERE user_id #{userId}) GROUP BY c.id ORDER BY correct_count / COUNT(a.id) ASC;把正确率升序排列排在前面就是该学生的薄弱知识点。答辩现场演示这个统计比单纯展示增删改查更能说明你理解了「管理系统」不只是数据维护。另外导出成绩单 PDF、导出错题 Excel 这类功能只需要引入一个开源工具类加上表格模板即可实现也属于性价比极高的演示项。整个项目跑通后建议把数据库脚本、导入步骤、测试账号写进 README压缩包里的内容是否完整、能否让下一个同学无痛复现本身就是题管理系统开发流程的一部分。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →