ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于SpringBoot+Vue的毕业选题系统架构设计与实战解析

2026/9/14 7:15:46 拓冰建站 浏览量
基于SpringBoot+Vue的毕业选题系统架构设计与实战解析 简介这是一份面向高校毕业设计或课程设计的Java Web选题系统完整源码包包含管理员、老师、学生三类角色覆盖系主任信息维护、系统维护、题目录入、选题审核、学生网上选题等闭环流程适合正在搭建同类型管理系统的Java/JSP学习者参考。压缩包共547个文件大小约8.65MB其中以353张gif动态演示图和JPG截图为主便于对照操作界面核心代码由61个jsp页面、23个js脚本、38个css样式及7个jar依赖组成另附docx/doc说明文档、caj参考论文和MySQL数据库文件部署所需的JDK1.8、MySQL5.7、Tomcat等环境说明齐全。已有52人学习。资源整合了完整前后端源码、数据库脚本、配置文件和设计文档从环境搭建到功能实现均有据可查既有助于理解选题业务的权限划分与页面交互也可作为毕业设计写作和系统开发的双重参考。1. 毕业选题系统这类项目为什么值得把前后端和 MySQL 完整跑通一遍每年到大四下学期高校教务处的选题环节都要经历一轮“抢课式”访问高峰。一个基于 Java 的毕业选题系统本质上是把学生选题、教师出题、教研室审核这三方流程搬上线听起来不复杂但真正动手做就会发现登录鉴权、角色权限、选题冲突、时间窗口控制、批量导入导出每一块都是细节。市面上能下到的源代码包不少但多数要么前端是 Bootstrap 拼页面要么后端把业务逻辑全堆在 Servlet 里真正用 Spring Boot Vue 前后端分离、配 MySQL 做持久化的完整项目反而值得仔细拆一遍。这篇文章不替你做选择题只把一套可复现的方案讲清楚数据库表怎么设计才不会被并发选题打穿JWT 登录在前后端分离里怎么落地前端页面如何感知选题状态变化以及最后交付时说明文档和 LW论文里哪些坑是答辩现场高频翻车点。适合正在做毕设、或者接手这类老系统做改造的开发者。2. 毕业选题系统架构剖析与 Spring Boot Vue 选型依据2.1 功能拆解学生选题、教师出题、管理员审核的三方闭环毕业选题系统的业务模型可以压缩成三条主线教师创建课题并设定人数上限、学生在规定时间窗口内选择课题、教研室或管理员负责审核与调整。除此之外还有一个旁路管理员维护专业、班级、教师账号等基础数据。从代码角度讲这三条线映射到后端就是三个核心模块topic课题、selection选题记录、user用户。如果你看到的源代码包里还有review、notice这类表那属于附加功能——公告发布和审核记录。判断一个源码包质量的高低第一眼就看它在selection表上有没有做唯一约束这决定了两个学生同时选最后一个名额时系统是报错还是静默覆盖。2.2 前后端分离架构下的模块划分与请求链路现在的毕业选题系统源代码主流形态是 Spring Boot 做后端 REST API、Vue 或 Vue Element UI 做管理端页面。一个典型请求的链路是浏览器发起登录 → Vue 路由守卫检查本地 token → axios 携带 token 请求后端接口 → Spring Security 或拦截器校验 JWT → Controller 调 Service → MyBatis 或 JPA 操作 MySQL → 数据返回前端渲染。这里有一个容易被忽略的边界前后端分离不等于把页面文件丢进static目录。真正的分离是前端通过 HTTP 接口与后端交互后端不返回任何 HTML 片段。判断你手里的源码包是不是真分离看pom.xml里有没有spring-boot-starter-thymeleaf——如果有大概率是服务端渲染改了个皮。2.2.1 后端模块Spring Boot 三层结构与统一响应体后端代码一般按controller → service → mapper三层划分实体类放在entity或domain包下。一个容易忽略的细节是统一响应体所有接口返回{ code, message, data }这个结构前端 axios 拦截器才能统一做错误提示否则每个页面都要单独判断返回格式代码会膨胀得很难维护。前后端分离项目里CORS 配置是必写的。Spring Boot 里常见的做法是实现WebMvcConfigurer的addCorsMappings方法允许本机开发时的localhost:8080访问后端8081端口。实际部署到服务器后更稳妥的方案是用 Nginx 反向代理让前后端同源绕开跨域问题。2.2.2 前端模块Vue Router 路由守卫与状态管理Vue 前端这边的核心不是页面长什么样而是两个机制路由守卫和状态持久化。路由守卫写在src/router/index.js里每次跳转前检查localStorage.getItem(token)是否存在不存在就重定向到登录页。状态管理用 Vuex 或 Pinia 都行但像用户角色这种数据刷新页面后会丢失得从 token 解析或在App.vue的created钩子里重新拉取用户信息。2.3 为什么 MySQL 是这类系统最稳妥的存储方案选题系统的数据量撑死几千条课题、几万条选题记录用 MySQL 完全够用而且生态成熟JDBC 驱动稳定、MyBatis 的 XML 映射有大量现成模板、Navicat 等可视化工具找资料方便。相比 PostgreSQL 或 OracleMySQL 在这类教学项目里的优势是“出了问题搜得到答案”。但 MySQL 也不是没坑。最典型的是字符集建库时没指定utf8mb4插入中文姓名或课题名称时直接报Incorrect string value。这类问题在源码包里极其常见拿到项目第一步不是启动而是先检查application.yml里的数据库连接参数和后端实体类的字段命名是否对得上。3. MySQL 数据库设计与核心表结构实现3.1 用户、课题、选题记录三张核心表的设计要点数据库设计决定了选题系统后期加功能时是轻松还是痛苦。以最常见的三张表为例——sys_user用户表、topic_info课题表、selection_record选题记录表——每张表都要回答三个问题主键怎么生成、状态字段怎么取值、哪些字段需要唯一约束。用户表里必须有role字段区分学生、教师、管理员三种角色用tinyint存1 学生、2 教师、3 管理员。课题表必须有selected_count当前已选人数和max_count人数上限这两个字段是后端校验选题冲突的依据。选题记录表是核心学生 ID 和课题 ID 联合起来必须唯一这是防重复选题的底线。3.2 建表 SQL字段类型、主键策略与注释规范拿到源码包后先看sql目录下的建表脚本没有完整脚本的项目基本可以直接放弃。下面这份建表 SQL 是这类系统最常见的设计可以直接对照你手里的源码检查出入-- 用户表存放学生、教师、管理员账号 CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT 密码BCrypt加密, real_name varchar(50) NOT NULL COMMENT 真实姓名, role tinyint(4) NOT NULL DEFAULT 1 COMMENT 角色1学生2教师3管理员, student_no varchar(20) DEFAULT NULL COMMENT 学号仅学生角色有值, teacher_title varchar(20) DEFAULT NULL COMMENT 职称仅教师角色有值, major varchar(50) DEFAULT NULL COMMENT 专业方向, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 课题表教师发布选题信息 CREATE TABLE topic_info ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, topic_name varchar(200) NOT NULL COMMENT 课题名称, topic_desc text COMMENT 课题详细描述与要求, teacher_id bigint(20) NOT NULL COMMENT 发布教师ID关联sys_user.id, max_count int(11) NOT NULL DEFAULT 1 COMMENT 可选人数上限, selected_count int(11) NOT NULL DEFAULT 0 COMMENT 当前已选人数, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1开放选题2已截止3已关闭, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_teacher_id (teacher_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课题表; -- 选题记录表学生选题的核心关联表 CREATE TABLE selection_record ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, student_id bigint(20) NOT NULL COMMENT 学生ID关联sys_user.id, topic_id bigint(20) NOT NULL COMMENT 课题ID关联topic_info.id, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0待审核1已通过2已拒绝, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 选题时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 审核时间, PRIMARY KEY (id), UNIQUE KEY uk_student_topic (student_id, topic_id), KEY idx_topic_id (topic_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT选题记录表;这段 SQL 里最关键的是uk_student_topic这个联合唯一索引。它保证同一个学生对同一个课题只能有一条选题记录即便后端代码漏了判断数据库层面也能兜住重复插入。selected_count用int而不是bigint因为一个课题人数上限不会超过几百省空间是次要的主要是不需要为了不存在的并发量过度设计。字段注释直接写在COMMENT里这不是给数据库看的是给接手源码的人看的。很多源码包的表结构没有注释字段名写a、b、c这种项目后续调试的时间成本极高建议直接弃用。3.3 索引设计防并发选同题唯一索引 事务更新的配合并发选题是这类系统最容易出 Bug 的地方。两个学生同时点“选题”按钮后端先查selected_count max_count再插入记录、把selected_count 1。这个流程不加锁的话MySQL 的默认隔离级别下可能两个请求都通过了检查最后实际选中的多了一人。常见的做法是在插入选题记录后执行一条带条件更新的 SQL用受影响行数判断是否超员UPDATE topic_info SET selected_count selected_count 1 WHERE id #{topicId} AND selected_count max_count这条语句的巧妙之处在于把“检查更新”合并成一条原子操作selected_count max_count是条件MySQL 的行锁保证同一时刻只有一个事务能成功更新。MyBatis 的update方法返回int等于 1 说明更新成功等于 0 说明名额已被抢完不用再插入selection_record。配合事务使用时要注意 Mapper 接口的返回值类型必须写对Transactional(rollbackFor Exception.class) public boolean selectTopic(Long studentId, Long topicId) { // 先插入选题记录捕获唯一键冲突 try { selectionRecordMapper.insert(studentId, topicId); } catch (DuplicateKeyException e) { return false; // 该学生已选过这个课题 } // 再执行条件更新影响行数为0说明名额已满 int rows topicInfoMapper.increaseSelectedCount(topicId); if (rows 0) { throw new BusinessException(该课题可选名额已满); } return true; }这段逻辑里事务注解Transactional必须加因为插入记录和更新人数是两步操作要么都成功要么都回滚。DuplicateKeyException是 Spring 对 MySQL 唯一键冲突的封装捕获它比先查再插更安全——查和插之间存在时间窗口极端并发下还是会撞。4. 前后端分离下的选题核心链路从登录鉴权到选题提交4.1 后端 JWT 登录鉴权与角色权限校验的实现前后端分离项目里Session 方案已经不适用了因为前端和后端可能部署在不同的域名或端口下跨域携带 Cookie 的配置很麻烦。常见做法是基于 JWT 的无状态鉴权登录成功后后端返回一个 token前端存在localStorage里每次请求在 Header 中带上Authorization: Bearer token。JWT 的生成用jjwt库一个简单的工具类通常包含以下逻辑// JwtUtil.java - 基于io.jsonwebtoken的JWT生成与解析 public class JwtUtil { // 密钥要放在配置文件中这里仅为示例 private static final String SECRET_KEY your-secret-key-at-least-32-bytes-long; private static final long EXPIRE_TIME 24 * 60 * 60 * 1000; // 24小时 // 生成tokenclaims里放用户ID和角色 public static String generateToken(Long userId, Integer role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } // 解析token校验签名和有效期 public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); } }拦截器或过滤器中校验 token并在HandlerInterceptor里把用户信息写入ThreadLocal后续 Controller 直接取用public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { try { Claims claims JwtUtil.parseToken(token.substring(7)); // 将userId和role存入request attribute供后续使用 request.setAttribute(userId, Long.valueOf(claims.getSubject())); request.setAttribute(role, claims.get(role, Integer.class)); return true; } catch (Exception e) { response.setStatus(401); return false; } } response.setStatus(401); return false; } }这个拦截器要注册进 Spring MVC 的拦截器注册表并配置放行路径。常见的放行清单是/api/auth/login、/api/auth/register和静态资源路径其他接口全部拦截。权限校验属于第二个维度。比如只有教师角色能创建课题管理员能审核学生只能选题。如果每个接口都靠if (role ! 2)判断代码会非常啰嗦。用 Spring AOP 或拦截器统一校验会好很多但大部分毕业设计源码为了省事写在校验注解里——这里有一个针对性方案// RequireRole.java - 自定义角色注解加在Controller方法上 Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { int[] value(); // 允许访问的角色列表 } // 配合拦截器preHandle中解析注解 HandlerMethod handlerMethod (HandlerMethod) handler; RequireRole requireRole handlerMethod.getMethodAnnotation(RequireRole.class); if (requireRole ! null) { int role (Integer) request.getAttribute(role); int[] allowedRoles requireRole.value(); if (!Arrays.stream(allowedRoles).anyMatch(r - r role)) { response.setStatus(403); return false; } }代码逻辑很简单先解析注解若标注了RequireRole({2})则校验当前用户角色是否为 2不是就返回 403。相比在 Service 里写判断这种方法能让权限控制一目了然且后端源码的可读性明显提升。4.2 Vue 前端路由守卫与选题页面的状态联动前端的核心任务是两件事登录态保持和选题状态同步。路由守卫使用 Vue Router 的beforeEach// src/router/index.js - 全局前置守卫 router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path /login) { next(); // 登录页放行 } else if (!token) { next(/login); // 未登录跳转登录页 } else { // 根据token里的角色信息控制可访问页面 const role JSON.parse(localStorage.getItem(userInfo)).role; if (to.meta.roles !to.meta.roles.includes(role)) { next(/403); // 角色不匹配跳转无权限页 } else { next(); } } });选题页面的状态联动是另一个高频考点。学生打开课题列表时接口返回课题详情和当前学生的选题记录前端用两个变量控制按钮状态避免学生重复提交。常见的实现是每行数据里比对mySelectionTopicIds数组命中就禁用按钮。4.3 前后端交互的 3 个隐蔽坑代码层面怎么绕过第一个坑是时间格式不一致。后端LocalDateTime序列化后默认是2025-01-15T10:30:00这种 ISO 格式前端new Date()能解析但想要2025-01-15 10:30:00就得在application.yml里配置 Jacksonspring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8第二个坑是 Long 类型精度丢失。后端主键用bigintJava 的 Long前端 JavaScript 的 Number 最大安全整数是 9007199254740991而雪花算法生成的 ID 有 19 位超过这个范围会丢精度。这是真正隐蔽的 Bug。解决办法是序列化时把 Long 转成 String——在实体类的 ID 字段上加JsonSerialize(using ToStringSerializer.class)前端拿到的就是字符串传给后端时也能被正常解析。第三个坑是跨域配置里allowCredentials跟allowedOrigins冲突。后端如果同时设置了这两个请求会被浏览器拦截报The value of the Access-Control-Allow-Origin header ... must not be the wildcard *。代码层面建议这样写Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) // 允许所有来源 .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }allowedOriginPatterns配合allowCredentials(true)是合法的allowedOrigins(*)不行。这里最容易踩坑的坑在于开发时用http://localhost:8080访问后端http://localhost:8081没报错部署到 Nginx 后却出现跨域报错因为代理环境下来源变成了服务器的外网地址。5. LW论文与说明文档里最该有的 5 张图和 3 张表毕业设计交付物里论文 LW 的质量往往比代码更影响最终成绩。一份合格的毕设论文围绕选题系统至少要包含 5 张图和 3 张表。5 张图分别是系统功能结构图、总体架构图、数据库 ER 图、选题流程图、部署架构图。3 张表是核心接口表、数据库表清单、测试用例表。功能结构图直接决定评委第一印象。画图时按角色分模块学生端选题、查看审核状态、修改个人信息、教师端发布课题、查看选题列表、审核学生、管理员端用户管理、课题管理、系统公告。这张图不需要多精美但层级和连线要清楚。数据库 ER 图要和 SQL 脚本严格对应评委会对照源码里的建表语句检查 ER 图是否真实。最容易出问题的地方是被动图——论文里画了 8 张表代码里只建了 5 张这种不对应会在答辩现场引发质疑。建议画 ER 图时直接从建表脚本回推不存在的表不要画。测试用例表是很多人忽略的加分项。一个最小可用的测试表结构如下用例编号测试场景输入数据预期结果实际结果TC-001学生重复选题同一学生ID同一课题ID提示“已选过该课题”与预期一致TC-002课题超出人数上限提交时selected_count max_count提示“名额已满”与预期一致TC-003教师登录访问学生接口教师角色访问选题接口返回403与预期一致这张表写 8 到 10 条就够覆盖登录、选题、审核、用户管理四块即可。每一条的“实际结果”必须真跑过一遍再填不要直接复制“预期结果”。部署图是实操中容易被忽视的。很多源码包默认数据库连接是localhost:3306部署到服务器后要把application.yml里的连接地址改成服务器 IP 或内网地址。MySQL 5.7 以上还需要确认user表里 root 用户的host字段是否允许远程连接-- 允许root从任何主机连接慎用仅限开发环境 UPDATE mysql.user SET host % WHERE user root; FLUSH PRIVILEGES;这里最容易被卡住的是 MySQL 8.0 版本默认认证插件是caching_sha2_password老项目用的 MySQL Connector/J 5.x 驱动连不上报错内容类似Public Key Retrieval is not allowed。解决办法是把连接参数加上allowPublicKeyRetrievaltrueuseSSLfalse或者把认证方式改回mysql_native_password再或者把驱动升级到 8.0.x 并保持 jar 和数据库大版本匹配。验证系统是否可交付建议按这个顺序走一遍先看说明文档里的启动步骤能否一字不差地跑通再检查application.yml里的数据库账号密码是不是写死的怎么改都行最后测试每个角色登录后能否看到自己的菜单。源码包里的 LW 里没有写清楚开发环境版本那就自己补一份运行环境说明JDK 1.8、Maven 3.6、Node 14、MySQL 5.7四个版本号全部列明这页纸在答辩时起到的作用经常比正文还大。本文还有配套的精品资源点击获取