尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Java毕业选题系统:Spring Boot+Vue前后端分离与并发控制实战

基于Java毕业选题系统:Spring Boot+Vue前后端分离与并发控制实战 简介基于Java/JSP的毕业选题系统完整源码面向Java Web学习者、毕业设计或课程设计学生覆盖管理员、老师、学生三类角色的核心流程管理员可维护系主任信息与系统运行老师录入题目并审核学生选题学生在线完成选题操作体系完整且贴近真实教务场景。资源共547个文件以JSP动态页面、CSS样式、JS交互脚本为主并包含MySQL数据库文件MDF/LDF、多篇CAJ/DOCX论文资料及环境说明文档整体压缩包仅8.65MB导入Eclipse/IDEA配合Tomcat即可搭建运行目录结构也较为清晰。已有52人学习下载。包内提供完整前后端源码、数据库脚本、论文及说明文档既适合理解高校选题管理系统的模块设计与Web开发细节也便于二次开发与论文撰写参考是一份紧凑实用的Java Web项目样例。1. 毕业选题系统源码包里的前后端到底在解决什么每年三四月份高校教务办公室就会收到大量纸质选题表学生抢题靠手速老师统计靠复制粘贴。“基于java的毕业选题系统”这类课设和外包项目因此长盛不衰。你拿到的源码包里通常是一个完整前后端项目、MySQL初始化脚本、说明文档和一篇LW论文它本身是一条可复现的Spring Boot Vue MySQL工程主线。下面不打算逐行解读某个压缩包里的代码而是从一线工程师的视角把毕业选题系统拆成选型、数据模型、后端接口、前端联调和MySQL调优五个环节讲清楚每一步为什么这么做、参数怎么设、并发抢题时看哪里。适合正在做毕业设计的学生、需要给高校搭建选题系统的开发者以及想快速掌握前后端分离实战的Java程序员。2. 基于 Java 的毕业选题系统Spring Boot Vue 选型与 MySQL 表设计毕业选题系统不是一个复杂的业务但涉及三种角色、一个强并发场景抢题和一个明确的状态流待审核、已通过、被驳回所以技术选型要围绕“快速交付、容易演示、抗并发”来定。这一章先讲清楚为什么主流源码包都选 Spring Boot Vue再落到可以直接导入的 MySQL 表设计。2.1 前后端分离和单体 JSP为什么选前者先说原理。JSP 单体时代Java 代码和 HTML 混在一起后端改个字段必须把整个 war 包重新部署而毕业选题系统要同时给学生端、教师端和管理员端提供页面每个端的交互密度差异很大。采用前后端分离后后端只用 Spring Boot 提供 REST 接口前端用 Vue 构建单页应用部署时前端静态文件扔 Nginx后端独立跑在 Tomcat 或内嵌容器里。团队可以并行开发学生也更容易在答辩时演示点一下按钮axios 请求打到后端MySQL 返回结果整个过程链路清晰。这种结构对“源代码包”的整理也很友好通常一个目录放 backend一个目录放 frontend数据库脚本单独放在 doc 或 sql 文件夹。如果你下载的压缩包是 JSP 老架构建议迁移到前后端分离再开始改需求否则后续加接口、换皮肤的成本很高。需要注意前后端分离不等于无状态跨域、Token 刷新这些问题必须在一开始约定好这也是“前后端分离项目实战”里最常见的坑。另一点是快速开发平台的选择很多毕业设计会直接用若依框架生成前后端代码但自己从 Spring Boot 搭一次过滤器和事务才能理解抢题这种并发场景到底卡在哪里。2.2 核心实体学生、教师、选题与选题记录业务上至少需要四个实体用户包含学生和教师、选题、选题记录以及一个用于审计的操作日志。学生和教师都放在 sys_user 表中用 role 字段区分选题表保存题目标题、描述、所属教师和最大人数选题记录保存学生、题目、审定状态和版本号版本号用于并发控制。下面的 MySQL DDL 是常用做法很多 mysql 安装配置教程也会建议这样初始化库表导入后先别急着改业务看清楚唯一键和索引再动手。CREATE DATABASE IF NOT EXISTS graduation_topic DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE graduation_topic; CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL COMMENT 用户名/学号, password VARCHAR(100) NOT NULL COMMENT BCrypt 哈希值, role TINYINT NOT NULL COMMENT 1-学生 2-教师 0-管理员, department VARCHAR(100) DEFAULT NULL COMMENT 院系, real_name VARCHAR(50) NOT NULL COMMENT 姓名, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 0未删除 1已删除, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username), KEY idx_dept (department) ) ENGINEInnoDB COMMENT用户表; CREATE TABLE topic ( id BIGINT PRIMARY KEY AUTO_INCREMENT, teacher_id BIGINT NOT NULL COMMENT 发布选题的教师ID, title VARCHAR(200) NOT NULL, description TEXT, max_students INT NOT NULL DEFAULT 1 COMMENT 最多可选人数, selected_count INT NOT NULL DEFAULT 0 COMMENT 当前已选人数, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用, deleted TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_teacher (teacher_id) ) ENGINEInnoDB COMMENT选题表; CREATE TABLE selection_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, topic_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核 1通过 2驳回, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_student_topic (student_id, topic_id), KEY idx_topic_status (topic_id, status) ) ENGINEInnoDB COMMENT选题记录表;这段 SQL 有四个重点第一sys_user 表唯一键放在 username 上学号和工号统一走这个字段避免重复账号第二topic 表用 selected_count 缓存当前人数查询列表时不需要 count(*)但必须配合事务更新防止超卖第三selection_record 的唯一键 (student_id, topic_id) 保证一个学生对一个题目只能有一条申请第四version 字段留给乐观锁SELECT 时取出UPDATE 时带上如果更新影响行数为 0说明别人已经改过这条记录。索引上idx_topic_status 正好能覆盖“按题目查状态”的后台审核列表避免文件排序。2.3 字段设计中的 3 个易错点角色、状态和逻辑删除这块设计不好后面写接口会到处打补丁。第一角色不要用字符串 student 存字符串占用空间大且容易写错用 TINYINT 存数字接口层做枚举映射。第二状态字段预留扩展选题记录的状态常用“待审核、通过、驳回”但你加上“撤回”后修改枚举即可第三所有业务表都加 deleted 逻辑删除因为高校系统有审计要求物理删除会被学生投诉“成绩没了”。下面的表格是推荐字段组合。字段名类型默认值作用roleTINYINT10-管理员 1-学生 2-教师statusTINYINT00待审核 1通过 2驳回deletedTINYINT00正常 1删除versionINT0乐观锁判断create_timeDATETIMECURRENT_TIMESTAMP记录创建时间MySQL 创建索引时不要看到 WHERE 就加索引优先在唯一约束和外键字段上加。上面 selection_record 已经加了唯一键隐含了索引如果再单独加一个 student_id 的单列索引会重复占用空间。这里真正需要补的是 topic 表的 selected_count 更新条件例如在事务里先执行UPDATE topic SET selected_count selected_count 1 WHERE id ? AND selected_count max_students影响行数为 1 才允许插入记录。这条语句也是后面所有并发控制的核心。3. 用 Spring Boot 与 JWT 实现毕业选题后端接口与并发控制后端是整个系统的中枢。这里我以 Spring Boot MyBatis-Plus Spring Security 为例这套组合在“springboot vue前后端分离”项目里几乎成了默认配置。MyBatis-Plus 提供内置 CRUD但复杂的更新必须自己写 SQL尤其是乐观锁更新。这一章从登录鉴权讲到接口设计再看如何用一条 UPDATE 挡住多人抢同一个题。3.1 登录鉴权基于 RBAC 的 JWT 令牌流程原理是用户提交用户名密码后端调用 BCryptPasswordEncoder.matches 校验通过后生成一个包含 userId、role 的 JWT。后续每次请求前端都会在 Authorization 头带上这个令牌后端通过过滤器解析并放入 SecurityContext。这样接口里直接使用PreAuthorize(hasRole(TEACHER))就能限制角色。常见的代码组织是一个 SecurityConfig 注册过滤器链一个 JwtAuthenticationFilter 继承 OncePerRequestFilter一个 UserDetailsServiceImpl 查询 sys_user 表。JWT 的有效期参数建议设 2 小时刷新令牌另设 7 天不要把过期时间设成 24 小时学生改完页面挂一晚上很容易出安全漏洞。下面摘一段 JWT 工具的生成逻辑。public String generateToken(Long userId, Integer role) { long now System.currentTimeMillis(); long expire now jwtProperties.getExpireMinutes() * 60 * 1000L; return Jwts.builder() .claim(uid, userId) // 放入 uid供后续过滤器解析 .claim(role, role) // 放入角色控制接口权限 .setIssuedAt(new Date(now)) .setExpiration(new Date(expire)) .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecret()) .compact(); }这段代码的关键参数是 claim 里的 uid 和 role它们在后续 filter 里被取出并构造成 UsernamePasswordAuthenticationToken。HS256 是对称加密密钥必须放到 application.yml 里用环境变量注入不能硬编码在 java 文件中。expireMinutes 是从 properties 读取的整数值调整它可以直接控制会话时长例如写 120 表示 2 小时。3.2 选题与审核的接口设计路由、入参和返回体学生端接口至少包括分页查询可选题目、提交选题、查看我已选列表、取消待审核选题教师端有发布题目、修改题目、审核学生选题管理员端有用户管理和数据导出。接口风格统一为/api/v1/...前端通过 axios 访问。下面是一个精简的接口表具体路径可以在源码包的 controller 层找到。方法路径角色说明GET/api/v1/topic/page学生、教师分页查询选题带 keyword、status 参数POST/api/v1/selection学生提交选题申请PUT/api/v1/selection/{id}/audit教师审核通过或驳回GET/api/v1/selection/my学生当前学生已选列表GET/api/v1/export管理员导出选题名单 ExcelController 层要做的是参数校验和响应包装。Spring Boot 可以统一返回 ResultTcode 为 0 表示成功非 0 给前端弹出错误提示。不要返回裸对象否则前端拿到 null 字段很难排查。分页参数 pageNum 和 pageSize 要设置上限一般 pageSize 最大 100防止拖垮 MySQL。PostMapping(/selection) public ResultLong submit(RequestBody SelectionSubmitDTO dto) { // 校验学生身份取 JWT 中的 uid Long studentId SecurityUtils.getUserId(); // 乐观锁更新选题人数返回 boolean 表示是否抢到 boolean ok selectionService.submitSelection(studentId, dto.getTopicId()); return ok ? Result.success(0L) : Result.error(400, 人数已满或重复提交); }这段逻辑把校验和状态变更都收在 service 内部Controller 不写业务。submitSelection 里会执行两步先查 topic 当前 selected_count 是否小于 max_students再 UPDATE topic SET selected_count selected_count 1 ...最后 INSERT selection_record。如果 UPDATE 影响行数为 0直接回滚抛出异常让事务生效。3.3 防止两个学生选同一个题目事务和锁的顺序后端最怕的坑是两个请求同时通过第一步校验然后都去更新 selected_count数据库行锁会阻塞第二个但如果没有条件校验第二个仍可能成功。所以 update 必须绑定原有条件。下面这段是核心 SQL。-- 只有当前人数小于上限才更新返回影响行数 1 表示抢到名额 UPDATE topic SET selected_count selected_count 1, update_time NOW() WHERE id #{topicId} AND status 1 AND deleted 0 AND selected_count max_studentsMyBatis 里执行后如果返回 1说明当前请求抢到了名额返回 0说明题目已停用或人数已满。同时 selection_record 的唯一索引 uk_student_topic 会兜底重复申请即使两个请求并发插入也只有一个能成功。这里要注意事务隔离级别使用 MySQL 默认的 REPEATABLE_READ 即可不需要提高到 SERIALIZABLE否则高并发下性能下降明显。另一个建议是给 selection_record 的 status 字段一个索引方便审核列表分页。提示不要用 synchronized 做本地锁毕业选题系统如果部署多节点本地锁在另一台服务器上完全无效。用数据库条件更新才是跨实例安全的方式。4. 用 Vue3 axios 实现毕业选题系统的登录与选题页面前端我用 Vue3 Vite Element Plus这是当前毕业设计和外包项目里最常见的组合。前端不再关心业务规则只负责渲染后端字段真正难的反而是路由守卫、请求拦截和防止重复点击。这一章不贴完整页面代码只把三个最容易被扣分的点讲透。4.1 vite 初始化与路由守卫未登录跳转登录页先用npm create vitelatest graduation-frontend -- --template vue初始化项目再安装npm install vue-router4 element-plus axios。路由表用 createWebHistory 模式但部署到子路径时要设置 base否则刷新后会 404。下面是一个基础的路由守卫片段。const router createRouter({ history: createWebHistory(/admin/), routes }) // 每次跳转前检查 token没有 token 就回登录页 router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })这里的 createWebHistory 传入了/admin/作为 base部署到 Nginx 时 location/admin/内静态文件能得到正确路径。beforeEach 只做了 token 存在性检查没有校验过期更好的做法是把过期校验放到 axios 响应拦截器里处理 401。登录成功后的跳转路径取自 query.redirect这样用户被踢回登录页后还能回到原页面。4.2 封装 axios统一携带 JWT 并处理 401前后端分离项目里axios 不能直接用必须统一配置 baseURL、超时时间、请求拦截器和响应拦截器。请求拦截器从 localStorage 读取 token响应拦截器专捕 HTTP 401。如果后端返回 401就清 token 并跳回登录页。注意token 用 localStorage 存是为了演示方便生产环境更推荐 httpOnly cookie但那样还要处理 CSRF取舍要提前想清楚。const request axios.create({ baseURL: import.meta.env.VITE_API_BASE || /api/v1, timeout: 10000 }) // 请求拦截器自动加 Bearer Token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器捕获 401 并跳登录页 request.interceptors.response.use( response response.data, error { if (error.response?.status 401) { localStorage.removeItem(token) router.push({ path: /login }) } return Promise.reject(error) } )这里 baseURL 通过环境变量 VITE_API_BASE 传入例如开发环境指向http://localhost:8080/api/v1生产环境由 Nginx 把/api/v1反向代理到后端避免跨域。timeout 设 10000 毫秒是给慢 SQL 留余量如果你的毕业选题系统查询特别重可以调到 15000但别再高否则用户会以为页面卡死。4.3 选题页面倒计时、禁用按钮与状态刷新学生选题页最常见的交互是列表左侧显示题目右侧显示剩余名额点击“选题”按钮后按钮立刻变为 loading防止用户连点。下面是一个精简的 Vue 组件片段。script setup import { reactive, onMounted } from vue import { ElMessage } from element-plus const state reactive({ list: [], submittingId: null }) const submitSelection async (topic) { if (state.submittingId ! null) return state.submittingId topic.id try { const res await request.post(/selection, { topicId: topic.id }) ElMessage.success(res.message || 选题成功) await loadList() } catch (e) { ElMessage.error(e.response?.data?.message || 选题失败) } finally { state.submittingId null } } /script这里的 submittingId 是全局锁定标记只要有一个请求在途其他按钮全部 return。加载列表后用 selected_count 和 max_students 比较相等则禁用按钮并显示“已满”。Element Plus 的 el-table 自带 loading attribute可以绑在 state.loading 上不必再写额外遮罩。后端返回的 Result 结构要固定前端直接使用 res.code 判断成功而不是依赖 HTTP 状态码。5. 毕业选题系统上线前MySQL 索引与并发模拟验证这一章是验收阶段最实用的部分。代码写完了先别急着打包交报告用下面三组操作验证并发正确性再导一份截图放 LW 里会更有说服力。5.1 给 selection_record 加唯一索引和普通索引最核心的兜底是学生和题目之间不能重复申请所以必须先加唯一索引。然后用普通索引服务“查一个学生的所有申请”和“查一个题目的所有申请”。ALTER TABLE selection_record ADD UNIQUE KEY uk_student_topic (student_id, topic_id), ADD KEY idx_student (student_id), ADD KEY idx_topic (topic_id);注意唯一索引和普通索引不要重复建student_id 已经在联合索引的最左前缀里单独建 idx_student 只会在“只看学生维度”时命中如果联合索引已经经常使用其实可以省掉。这个要结合你实际的慢查询来定。5.2 模拟 50 个学生同时抢题验证逻辑靠人肉点击不可靠。可以用 bash 的 seq xargs 发送并发请求观察返回码。seq 1 50 | xargs -P 20 -I {} curl -s -X POST http://localhost:8080/api/v1/selection \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d {topicId:1} | grep -o code:[0-9]* | sort | uniq -c这里的 -P 20 控制并发数是 20每次请求使用同一个 token 或不同的 token 才有意义如果所有请求用同一个学生 token后端应该拦截重复记录。观察统计里 code400 的数量是否等于超出名额的数量并发下不能出现 200 的条数大于 max_students 的情况。5.3 用慢查询日志定位索引是否生效如果抢题高峰期接口变慢打开 MySQL 慢查询日志看有没有全表扫描。临时开启后执行 EXPLAIN检查 type 列是 ref 还是 ALL。SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 0.5; EXPLAIN SELECT * FROM selection_record WHERE student_id 1 ORDER BY create_time DESC;如果 possible_keys 里出现 uk_student_topic 或 idx_student并且 key 字段不是 NULL说明索引已经命中。如果看到 filesort说明排序字段没有索引可以增加组合索引 (student_id, create_time)。这些验证步骤做完再回头调整 topic 表的 selected_count 更新语句毕业选题系统的并发防护就完整了。本文还有配套的精品资源点击获取
返回列表