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

资讯详情

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

SpringBoot+Vue校园竞赛管理系统:从需求建模到答辩演示全解析

SpringBoot+Vue校园竞赛管理系统:从需求建模到答辩演示全解析 简介基于Spring Boot和Vue的校园竞赛管理系统毕业设计项目面向计算机相关专业学生与Java初学者适用于毕业设计、课程设计和课设大作业。系统覆盖用户管理、竞赛发布、参赛报名、成绩管理等核心模块学校可便捷发布竞赛信息、管理参赛学生及成绩学生可随时查看竞赛、在线报名并查询个人成绩同时支持竞赛信息的分类检索与成绩的自动统计。压缩包约27.36MB内含前端Vue源码、后端Spring Boot源码、MySQL数据库SQL脚本及毕业论文文档代码结构清晰、数据库设计合理便于二次扩展与维护。系统采用前后端分离架构开发环境配置简便运行测试通过稳定性有保障。项目附带论文参考为撰写毕业设计文档提供理论支撑开发者可在现有代码基础上按需增加功能模块。已有54人学习/下载适合用作实战项目练习或毕业设计成果展示。1. 基于SpringBootVue的校园竞赛管理系统这套毕业设计到底能解决什么基于SpringBootVue的校园竞赛管理系统面向的是高校里竞赛管理全流程线上化这个高频需求竞赛发布、学生在线报名、作品上传、评委打分、成绩公示和证书发放如果还靠Excel和微信群来回传递到评奖周基本是一片混乱。这类项目也是毕业设计里常青选题资源包一般会打包后端源码、前端源码、数据库SQL脚本和毕业论文看起来像个“全家桶”但拆开看核心不复杂权限模型加一套状态流转。适合两类人——准备交毕业设计或课程设计的学生以及想快速给学校搭一个竞赛报名后台的开发者。接下来按做这类项目的实际顺序把从需求拆分到能演示、能答辩的完整过程讲清楚。2. 需求建模和技术选型别急着写代码先定角色和状态2.1 为什么是SpringBootVue技术选型的真实理由这类系统选SpringBootVue不是因为它“新”而是因为交付效率高、答辩好讲。SpringBoot内置Tomcat起步依赖把SpringMVC、Jackson、校验框架都带了写一个能跑通的REST接口只需要很少的样板代码。对于校园竞赛管理这种标准CRUD加状态流转的系统SpringBoot的资源配置和拦截器机制都足够直接答辩时被问到“SpringBoot配置加载顺序”“自动配置原理”这类问题也容易讲清楚。Vue这边负责管理后台特别顺手组件化写表单、列表、弹窗比JSP那套前后端混编改起来快很多。Vue Route管页面跳转Axios封装接口请求配合UI组件库能快速搭出管理员、评委、学生三个角色的操作界面。做毕设时最怕的不是功能难而是时间不够SpringBootVue恰好是上手曲线平缓、资料齐全、遇到报错能搜得到解决方案的组合。另外要提一句环境问题。这类毕业设计资源包多数按JDK8、Node 14、MySQL 5.7/8.0的版本写好拿到手先确认本机环境匹配别一上来用JDK17跑老工程各种依赖报错会浪费半天。2.2 三个角色的用例划分管理员、评委、学生先不考虑“超级管理员”这种花哨设计把角色收敛成三个用例图会干净很多代码也容易写。角色核心用例关键操作管理员竞赛管理、用户管理、成绩审核创建竞赛、设置报名截止时间、分配评委、审核成绩、发布公告评委教师作品评审、评分维护查看已分配作品、打分、填写评语、退回不符合赛制的作品学生报名、作品提交、成绩查询浏览竞赛列表、在线报名、上传作品、查看自己的分数和获奖状态这版设计里最重要的是“管理员分配评委”这一步。如果不做分配所有评委能看到所有作品评审环节就没法控制公平性。数据库里用一张评委分配表或直接在竞赛表里加多个评委字段都能实现优先用单独的关系表后面要扩展“一个竞赛多个评委、一个评委多个竞赛”会方便很多。用例定清楚之后才能动手画时序图和E-R图。论文里的架构图、功能模块图都要以这个阶段的产出为基础后面代码和论文对不上答辩被问“这个功能在哪个类里实现的”会很尴尬。2.3 核心流程的状态闭环从发赛到成绩公示校园竞赛管理系统的业务本质是一个状态机。下面这个链路基本覆盖所有模块创建竞赛草稿→ 发布竞赛报名中→ 报名截止评审中→ 评委打分评审中→ 管理员审核并公示已结束。学生报名记录的状态单独再走一条已报名 → 已提交作品 → 已评分 → 已获奖/未获奖。状态字段不要用String随便存建议用TINYINT或INT配合明确的注释0草稿、1报名中、2评审中、3已结束这样写SQL查询条件时干净写前端下拉框也好映射。常见做法是在一个枚举类里统一管理Java端用常量类或枚举前端用字典数组两边的数字含义必须一致。数据库表设计就是对这个状态机的落库。先画好状态流转图再定表结构你会发现主表不过五六张用户、竞赛、报名、作品、评分、公告。很多人一上来先堆了二十张表后面改需求时光改外键关系就崩溃了。按“先跑通主流程再补扩展字段”的顺序建表才是这类毕设项目的正确打开方式。3. 数据库与SQL设计表结构定好了后端少写一半3.1 六张核心表的结构清单数据库设计是这个系统里最值得花时间的部分表结构稳定了实体类、Mapper、前端页面都能顺下来。我通常按下面这六张表起步。表名用途关键字段user用户表存三种角色id、username、password、role、real_namecompetition竞赛主表id、title、description、type、status、start_time、end_timeregistration报名表关联学生与竞赛id、competition_id、student_id、status、create_timework作品表学生提交的成果id、registration_id、title、file_url、submit_timescore评分表评委打分记录id、work_id、judge_id、score、comment、create_timenotice公告表id、title、content、publish_time用户表里role用TINYINT存数字1管理员、2评委、3学生方便权限拦截器做判断。competition表里的status就是上一节说的竞赛状态机。score表的主键建议用自增id不用work_id加judge_id做联合主键原因后面会讲。理论上还应该有证书表或获奖记录表但毕设阶段把获奖状态挂在registration表里就够用了少一张表少一摊事。3.2 初始化SQL脚本与关键参数说明拿到资源包里的SQL文件先别急着在Navicat里整个导入打开看一眼字符集和表结构。下面是按生产习惯写的一个最小建表脚本表结构精简过方便讲解。CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT BCrypt密文, role TINYINT NOT NULL DEFAULT 3 COMMENT 1管理员 2评委 3学生, real_name VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;几处参数需要特别说明。密码字段长度设成100是因为BCrypt加密结果有60位MD5只有32位如果你打算用BCryptVARCHAR(64)够用但你设100更保险如果用MD5VARCHAR(32)就行。但答辩时建议说BCryptMD5加盐已经不算安全实践了。create_time用DATETIME而不是TIMESTAMP是避免2038年问题和时区换算带来的坑。DEFAULT CURRENT_TIMESTAMP是MySQL 5.6以上支持的写法老版本会语法报错SQL导不进去先查一下MySQL版本。CREATE TABLE registration ( id INT NOT NULL AUTO_INCREMENT, competition_id INT NOT NULL, student_id INT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0已报名 1已提交作品 2已评分 3未获奖 4已获奖, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_comp_student (competition_id, student_id), KEY idx_student (student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT竞赛报名表;报名表的联合唯一索引uk_comp_student是防重复报名的第一道关卡。就算前端按钮做了置灰两个学生同时提交或者同一学生开了两个页面操作数据库层面依然能拦住重复记录。这条索引在答辩时很加分讲出来就是“我在数据库层面做了幂等控制”比只说前端判断高级一个档次。单列KEY索引idx_student是给“查某学生报名了哪些竞赛”这个高频查询准备的否则每次都要扫全表。3.3 SQL注入与慢SQL优化答辩老师最爱问的两个点先说SQL注入。为什么MyBatis里建议用#{}而不是${}因为#{}走预编译传参被当作字符串处理${}是直接拼接SQL把用户输入原样塞进语句一旦参数里带OR 11这类内容就出问题。毕业设计里常见的翻车点就是有人图省事在ORDER BY或动态表名处用了${}却没校验参数。如果确实要动态排序字段必须做白名单校验只允许传入预设好的字段名。再说慢SQL优化。竞赛列表页按状态查询、报名记录按学生查询这两条是系统里最高频的SQL。遇到响应变慢别瞎猜先用EXPLAIN看执行计划。EXPLAIN SELECT r.id, r.status, c.title FROM registration r LEFT JOIN competition c ON r.competition_id c.id WHERE r.student_id 17 ORDER BY r.id DESC LIMIT 20;EXPLAIN返回的type列如果出现ALL说明当前查询在走全表扫描。解决方式就是给WHERE条件里的student_id加索引就是上一节建表时加的idx_student。type从ALL变成ref查询成本立刻降下来。还有一个容易被忽略的点SELECT后面只查需要的字段不要SELECT *。student表里有密码字段列表页根本用不到查出来还平白增加数据传输量。3.4 SQL脚本在毕业设计材料里的作用SQL文件不只是让项目能跑起来它还是论文里数据库设计的直接证据。论文第一章要写“数据库设计”一般会要求给出E-R图和核心表的结构说明。答辩时老师很可能抽查某个字段的含义或某条查询语句的设计理由这时候能指着SQL文件讲清楚就行。评分验收时有一个常见现象老师会直接打开源码里的SQL文件看表设计表过大、命名混乱、没有注释会影响印象分。建议每个字段都写COMMENT每张表都用utf8mb4字符集主键统一叫id业务表的时间字段统一叫create_time或update_time。风格一致的表结构比多写几个非业务功能更容易拿到好评。4. SpringBoot与Vue联调从登录到报名评分的主链路实现4.1 SpringBoot工程骨架与关键配置工程结构建议按controller、service、mapper、entity、config、common这几层拆不要图省事把逻辑全写在Controller里。校园竞赛系统的逻辑虽然不重但“登录→鉴权→报名→评分→公示”这条链路很长分层清楚后面论文画架构图也方便。核心配置集中在application.yml里地方不大但坑不少。数据源、MyBatis映射路径、文件上传大小、JWT密钥全在这一份配置里调。下面是常见配置片段。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/competition?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true # 自定义参数 jwt: secret: 替换成你自己的长随机字符串 expire-hours: 8MySQL 8以后驱动类要从com.mysql.jdbc.Driver换成com.mysql.cj.jdbc.Driver连接串不带serverTimezone会直接报时区错误这是每年毕设掉坑的重灾区。map-underscore-to-camel-case这个配置打开后数据库字段user_name能自动映射成Java属性userName不用在结果映射里一个个写。JWT密钥不要写“abc123”这种被老师在源码里看到会问你怎么保证安全性。实际做法是随便生成一段64位随机字符串用的时候从配置里读不要把密钥硬编码在代码里。expire-hours设8小时是照顾白天连续开发对于生产场景偏短但毕业设计够用。4.2 JWT登录、路由守卫与接口拦截登录接口本身没什么技术含量重点是登录后怎么让前端记住状态、后续请求怎么带凭证、后端怎么拦截非法请求。最顺手的一套组合是JWT加Axios请求拦截器加后端HandlerInterceptor三个部分配起来整个系统的鉴权就闭环了。RestController RequestMapping(/api/auth) public class AuthController { Autowired private UserMapper userMapper; PostMapping(/login) public Result login(RequestBody LoginDTO dto) { User user userMapper.selectByUsername(dto.getUsername()); if (user null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) { return Result.error(用户名或密码错误); } MapString, Object claims new HashMap(); claims.put(userId, user.getId()); claims.put(role, user.getRole()); String token JwtUtil.createToken(claims, jwtProperties.getExpireHours()); return Result.success(token); } }逻辑说明passwordEncoder是SpringSecurity里BCryptPasswordEncoder的实例matches方法校验明文密码和数据库密文是否一致返回true才放行。JwtUtil是手写的工具类createToken第一个参数是token里要携带的用户信息这里只放userId和role不放密码。过期时间从配置类读取统一管理。token返回给前端之后就出现一个新问题后端怎么知道每个接口要不要鉴权HandlerInterceptor可以解决。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 登录接口、验证码等白名单路径放行 if (request.getRequestURI().startsWith(/api/auth/)) { return true; } String token request.getHeader(Authorization); if (token ! null JwtUtil.parseToken(token) ! null) { return true; } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; } }注意这段代码里白名单路径的判断比较粗实际项目会把白名单路径收集成一个Set统一检查。这里最容易被问倒的是“JWT无状态服务端怎么让token失效”答案是JWT天然带过期时间token过期后parseToken返回null自然被拦截。如果要求用户注销后token立即失效就得引入黑名单或Redis属于加分项毕设阶段不强制。前端Vue这一侧重点在Axios请求拦截器和Vue Router的路由守卫。// src/utils/request.js axios.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization token; } return config; }, error Promise.reject(error)); // src/router/index.js router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (!token to.path ! /login) { next({ path: /login, query: { redirect: to.fullPath } }); } else { next(); } });这里有个细节值得说localStorage存token在XSS攻击面前不安全理论上应该用httpOnly Cookie或者内存存储但毕业设计里localStorage仍然是主流因为简单。如果你在论文里写了“安全性考虑”却用localStorage存token答辩会被挑刺。不想深入安全话题就统一说“采用token鉴权前端本地存储”别把安全拔太高。路由守卫配合后端401拦截就实现了“前端跳登录页、后端拒绝访问”的双重防线。带redirect参数是为了登录后能跳回用户本来要访问的页面这个细节很显工程师思维。4.3 竞赛报名与作品提交的数据流报名和作品提交看似两个功能实际是一条数据流。学生看竞赛列表点击报名registration表插入一条记录竞赛状态没到报名截止这条记录status是0。然后学生进入“我的报名”上传作品work表插入记录同时registration表的status改成1。评委端看到的就是状态为1的报名记录对应去作品表拿文件来评。// 前端报名调用 async submitRegistration(competitionId) { const { data } await axios.post(/api/registration, { competitionId: competitionId }); if (data.code 200) { this.$message.success(报名成功); this.loadCompetitionList(); } else { this.$message.error(data.message); } }// 后端报名接口 PostMapping(/api/registration) public Result register(RequestBody RegisterDTO dto, RequestAttribute(userId) Integer userId) { // 判断竞赛是否处于报名中状态 Competition comp competitionMapper.selectById(dto.getCompetitionId()); if (comp null || comp.getStatus() ! 1) { return Result.error(当前不在报名时间范围内); } // 数据库索引保证同一学生不能重复报名 Registration reg new Registration(); reg.setCompetitionId(dto.getCompetitionId()); reg.setStudentId(userId); reg.setStatus(0); registrationMapper.insert(reg); return Result.success(); }后端接口里的userId不是前端传的是从JWT解析后通过RequestAttribute注入的这是一种典型的信任链设计用户身份信息统一从token取不用前端再传一遍user_id防止有人改参数报名到别人名下。这个点能讲清楚至少在“不信任前端输入”这个维度上站得住。4.4 联调时的黑匣子排查习惯前后端联调时前端页面只显示一个“接口报错”看不到后端哪里挂了这时候别瞎猜按“请求有没有到后端→后端有没有报异常→返回的数据前端有没有解析对”的顺序排查。先看浏览器Network面板里接口的HTTP状态码再看后端控制台日志最后看Response里的数据结构。这个习惯能解决大多数所谓“联调不通”的玄学问题。常规做法是后端提前把统一返回结构固定下来比如Result类里code、message、data三个字段所有接口都走这一个壳。这样前端Axios响应拦截器只要判断code是否等于200就行不用每个页面单独处理异常数据。联调时如果发现前端拿不到数据先打印完整响应十有八九是data字段缺少一个属性。5. 常见问题与避坑毕业设计包最容易翻车的五个点5.1 压缩包代码跑不起来JDK、Node、MySQL版本对不上现象解压按README执行前端npm install报错后端启动直接抛UnsupportedClassVersionErrorSQL脚本导入也报语法错误。原因旧毕业设计多数基于JDK8、Node 14或更老版本生成包里的依赖版本和2025年的新环境不兼容。MySQL从5.7升到8.0后驱动类名和时区配置都变了老SQL脚本里如果用了ENGINEMyISAM或旧语法导入过程也会中断。解决先看pom.xml里的java.version再看package.json里的vue版本按对应的环境安装。不要追求最新版按包内锁定的版本走。如果包内没有说明把JDK装到8、Node装到14或16、MySQL保持8.0是兼容性最高的组合。MySQL连接串记得加serverTimezoneAsia/Shanghai。5.2 跨域报错前端能打开、接口全被拦现象Vue项目npm run dev起的页面能正常显示但调用后端接口时浏览器报blocked by CORS。原因前端运行在localhost:5173后端运行在localhost:8080端口不同就跨域了。Vite或Webpack开发服务器没配代理时浏览器直接向8080发请求后端没允许跨域来源请求被浏览器拦下。解决开发环境优先用Vite代理把/api开头的请求转发到8080这样浏览器看到的还是同源请求。// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } });项目上线走Nginx时也要配一份同样的反向代理前后端同源跨域问题就不存在了。后端加CorsFilter是兜底方案不过毕业设计里用代理更干净论文里也更好解释。5.3 时间字段显示异常返回一串数字或格式不对现象接口返回的create_time是一串毫秒值或者前端显示成“2025-05-01T10:30:00”和页面风格不搭。原因Java 8的LocalDateTime经过Jackson默认序列化规则后可能变成数组或ISO字符串前端如果直接渲染它就出现这种不直观的显示。解决在application.yml里统一指定Jackson的日期格式。spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8注意这个配置只对java.util.Date生效对LocalDateTime需要配合jackson-datatype-jsr310模块并设置write-dates-as-timestamps为false。很多新项目直接全局配置就够用但LocalDateTime场景需要在实体字段上再加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)。两个都写上最稳妥答辩被问也能说明白区别。5.4 作品上传失败文件超限和PDF的XSS风险现象学生上传竞赛作品时文件稍微大一点就报MultipartException直接返回500。还有另一种情况上传的PDF文件名里包含特殊字符列表页展示时页面结构被破坏。原因SpringBoot默认单文件上传上限是1MB竞赛作品的PDF、打包作品压缩包动辄几十兆必然超限。文件名问题在于后端未对原始文件名做清洗特殊字符被前端渲染成HTML。解决调大上传限制同时在后端保存文件时对文件名做处理。spring: servlet: multipart: max-file-size: 50MB max-request-size: 60MBString originalFilename file.getOriginalFilename(); String safeName originalFilename.replaceAll([\()], _); String storePath uploadDir / System.currentTimeMillis() _ safeName;常见做法是保存文件时只在数据库存相对路径展示时用专用的下载接口让浏览器强制下载而不是直接把文件路径当链接拼到页面上。文件名里的特殊字符用replaceAll清洗成下划线既防止路径穿越也防XSS。这类安全细节写在论文里比空谈“系统安全”更有说服力。5.5 重复报名与状态乱掉前端接口被绕过怎么办现象同一个学生报名同一竞赛报了两条记录或者竞赛还没发布学生就能通过直接调接口报名成功。原因前端只做了按钮置灰和提示后端没有校验。任何人只要会用Postman就能绕过前端直接调接口把数据写进去。解决后端报名接口里做两件事。第一检查竞赛status必须等于报名中第二靠数据库联合唯一索引兜底重复报名。另外在用户表初始化时预置一条管理员账号密码用BCrypt加密不要在SQL脚本里写明文密码。答辩现场老师很可能现场开Postman模拟一个越权请求这两个校验做了他挑不出毛病。6. 答辩演示前这样验证部署、演示脚本与二次开发加分点6.1 答辩必演示的主链路脚本答辩演示最怕临场点开一个模块发现数据是空的或者流程走不通。提前准备一套完整演示数据按照下面脚本连走一遍。步骤操作预期结果1管理员登录创建竞赛并发布竞赛出现在公开列表状态变为报名中2学生注册登录报名该竞赛报名成功重复报名被拦截3学生上传作品文件作品列表出现记录报名状态变为已提交4管理员分配评委评委登录打分分数落库可填写评语5管理员审核并公示成绩学生端能看到分数和获奖状态这套脚本跑通核心功能就演示完了。建议提前录一份备用视频防止现场网络或环境出问题。6.2 三个不超纲的加分功能第一个是数据导出用EasyExcel把报名名单导出成Excel老师经常手动收集报名信息这个功能很实用。第二个是通知模块竞赛状态变化时给相关学生发站内消息用WebSocket或轮询都能实现。第三个是公告置顶和竞赛分类筛选代码量不大但能让系统看起来更像生产项目而不只是课程作业。6.3 用jar包加Nginx把项目跑起来构建部署这块答辩不一定要求但配置过和没配置过对项目的理解深度完全不同。前后端分离的项目最终要能在服务器上跑起来。# 前端打包 npm run build # 后端打包跳过测试减时间 mvn clean package -DskipTests # 启动后端jar包 java -jar target/competition-server.jar --spring.profiles.activeprod # 前端dist目录放到Nginx html目录后配置反向代理location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }部署的关键点是Nginx托管前端静态文件、反向代理后端接口、前端通过相对路径访问服务这样浏览器不跨域。我自己带毕业设计有个习惯项目能跑、能演示、能部署只是及格线答辩前一定要把核心模块的代码从头翻一遍确保每个接口都讲得出“为什么这么设计”。很多学生栽在“代码是网上找的自己没通读过”这一关。做这类系统先跑通主链路再谈附加功能顺序别颠倒。希望帮到你。本文还有配套的精品资源点击获取
返回列表