又到了每年毕设选题和课程设计扎堆的时候,我经常在技术社区看到有人发帖:“求一个前后端分离的管理系统源码,要求能跑、有文档、适合答辩。”然后评论区一堆人发网盘链接,下载下来不是缺数据库脚本就是前端跑不起来。说实话,这类需求里最有代表性的一个方向就是“高校学科竞赛管理平台”。它既不像电商系统那样业务太杂,也不会像学生管理系统那样过于普通,功能上覆盖了用户管理、竞赛发布、报名审核、作品提交、评审打分、公告通知,刚好卡在一个“练手充分又不至于失控”的复杂度上。这个项目用 SpringBoot + Vue + MySQL 来做,是一个很标准的全栈实战场景。接下来我把这个项目从设计思路、技术选型、数据库建模、核心代码实现到本地启动部署,完整拆一遍,给准备拿它做毕设或者课设的同学一份能直接照着落地的参考。
1. 项目定位与核心功能拆解
1.1 竞赛管理平台的真实业务场景
先说清楚这个平台到底管什么。高校里学科竞赛的流程其实很固定:教务处或者学院发布竞赛通知,学生在规定时间内报名,指导老师或竞赛秘书初筛资格,然后参赛者提交作品或论文,评委线下或线上打分,最后公示成绩并归档。这个流程在线下操作最大的问题是信息分散:报名表靠Excel传来传去,通知靠QQ群刷屏,作品文件用网盘链接收集,成绩统计靠人工合分。一旦竞赛数量多起来,比如一个学院一年办十几个赛事,管理成本直接爆炸。
所以这个平台的核心价值就是把上面的流程数字化。用户在这里分角色做事:管理员维护基础数据和竞赛信息,教师可以参与审核和评审,学生查看竞赛公告、在线报名、上传作品、查询成绩。每一件事都在系统里留下记录,流程可追踪,数据可统计。
1.2 典型角色与权限边界
基于真实业务,系统至少要拆出三类角色,这也是大多数同类毕设项目的标准划分方式。
- 管理员:拥有全部权限,负责用户管理、竞赛创建、配置评委、处理异常数据。
- 教师/评委:负责审核学生报名、对入围作品打分、填写评审意见。
- 学生:注册登录后浏览竞赛、在线报名、提交作品附件、查看自己的成绩和状态。
在设计上要记住一点:权限控制不要做得过于复杂。很多同学一上来就搞RBAC加Spring Security的完整权限模型,结果角色还没划分清楚,代码先绕晕了。毕设和课设阶段用拦截器加角色枚举就足够,后面我会详细讲实现方案。
1.3 核心功能模块清单
| 模块 | 功能点 | 面向角色 |
|---|---|---|
| 用户模块 | 注册、登录、个人资料修改、头像上传 | 所有角色 |
| 竞赛模块 | 竞赛列表、详情、创建、编辑、状态管理 | 学生、管理员 |
| 报名模块 | 在线报名、取消报名、报名审核 | 学生、教师 |
| 作品模块 | 作品信息填报、附件上传、更新 | 学生 |
| 评审模块 | 评分、评语、评分汇总、获奖建议 | 教师 |
| 公告模块 | 公告发布、查看、置顶 | 所有角色 |
| 数据统计 | 参赛人数统计、竞赛类型分布 | 管理员 |
这个功能清单是“够用且不冗余”的版本。别急着加消息通知、在线支付、多人协同那些花活,先把主流程跑通,再考虑扩展。
2. 技术选型背后的逻辑与时下考量
2.1 为什么后端是SpringBoot而不是SSM
很多同学在选技术栈的时候会纠结:学校教的还是SSM,要不要用SSM来做。我的建议非常直接:毕设题目标明了SpringBoot,就用SpringBoot,这也是目前企业里的主流方向。SSM之所以被替代,最大的痛点是配置烦琐——web.xml、spring-mvc.xml、mybatis-config.xml一堆配置文件要手工维护,类与类之间还要自己接线。SpringBoot把自动配置做进来之后,一个内嵌Tomcat加一个application.yml就能启动项目,省下来的时间可以专注写业务代码而不是调配置文件。
而且SpringBoot对答辩非常友好。你可以这样讲:“项目采用前后端分离架构,后端有独立的Controller-Service-DAO三层结构,通过内嵌容器可以快速打包部署。”面试官或答辩老师听到这个描述基本不会再揪着配置问题问。
2.2 为什么前端选择Vue
前端框架现在有三大主流:Vue、React、Angular。毕设场景下Vue的优势非常明显:上手曲线低,中文文档完整,社区里现成组件库多,Element UI或者Element Plus一套组件就能把后台管理页面的表格、表单、弹窗、分页全部搞定。更重要的是,高校的课程设计和毕业设计中Vue占了绝对多数,你遇到问题时很容易搜到别人踩坑的记录。
这里提醒一个版本坑:Vue有2和3两个大版本,对应的组件库也不同。Vue2配Element UI,Vue3配Element Plus。如果拿到的源码是Vue2,不要强行用Element Plus;反之亦然。选型阶段定了就别中途换,换框架的代价比想象中大很多。
2.3 MySQL在竞赛管理中的建模思路
MySQL是这个项目的数据底座,竞赛平台的业务规模和高校一个院系的数据量匹配得很好。表结构一般不复杂,单表数据量也就是几千到几万行,连索引优化都不用刻意去做。但建表时有两个细节值得一开始就注意:一是字符集统一使用utf8mb4,否则存emoji或特殊符号会报错;二是MySQL 8.0默认的认证插件是caching_sha2_password,老版本的数据库连接工具或者驱动版本不匹配会导致连接失败。建议直接使用MySQL 8.0 + mysql-connector-java 8.x的组合,同时把useSSL设为false,serverTimezone设为Asia/Shanghai。
2.4 前后端分离架构的核心协作方式
这个项目里,前端和后端通过JSON格式的HTTP接口通信,典型的协作流程是:前端Vue应用启动后运行在开发服务器的8080端口,后端SpringBoot运行在8081端口,前端通过Axios发起请求时把接口地址代理到后端。开发环境用代理解决跨域,生产环境用Nginx把两个服务合并到同一个域名下。
接口设计方面,建议定义一个统一的响应体结构,比如:
{ "code": 200, "message": "操作成功", "data": {} }所有接口都返回这个格式,前端统一在Axios拦截器里判断code,code为200就正常处理,否则弹出message。这套约定在整个项目里是唯一的协作契约,写起来很简单,但是能让联调阶段少掉很多扯皮。
3. 核心模块设计与数据库建模实例
3.1 用户表与角色关联设计
用户表是最基础的一张表,也是登录鉴权的数据来源。参考结构如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| username | varchar(50) | 登录账号,唯一 |
| password | varchar(100) | BCrypt加密后的密码 |
| real_name | varchar(50) | 姓名 |
| role | varchar(20) | student / teacher / admin |
| avatar | varchar(255) | 头像URL |
| phone | varchar(20) | 手机号 |
| varchar(100) | 邮箱 | |
| create_time | datetime | 注册时间 |
密码加密这一步千万别省。用MD5加密早就过时了,BCrypt是最省事的选择。Spring Security的crypto库里有现成的BCryptPasswordEncoder,加盐逻辑内置在里面,同一个密码每次加密的结果都不一样。如果你不想引入Spring Security全家桶,可以单独引入spring-security-crypto依赖,只用来做密码加密和校验,不影响其他业务。
3.2 竞赛信息表与状态字段设计
竞赛表的设计重点是“状态”字段。现实中的竞赛生命周期非常明确:未开始报名、报名中、截止报名、评审中、已结束。这个状态不只是给前端展示用的,更重要的是控制后端业务规则,比如报名只能发生在“报名中”,作品上传只能发生在报名截止前。
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| title | varchar(100) | 竞赛名称 |
| type | varchar(50) | 竞赛类别,如算法、电子设计、数学建模 |
| organizer | varchar(100) | 主办单位 |
| description | text | 竞赛详情 |
| start_time | datetime | 报名开始时间 |
| end_time | datetime | 报名截止时间 |
| status | int | 0草稿,1报名中,2评审中,3已结束 |
这里有个常见问题:要不要把状态做成前端按钮手动切换。我见过很多项目是让学生报名前管理员手动点击“开启报名”,这种方式不仅体验奇怪,还容易出现状态与时间不一致的情况。更好的做法是在每次查询时根据当前时间和报名起止时间计算真实状态,同时允许管理员在特殊情况下手动调整。这个方案写起来只用SQL判断几个时间字段,但阅读体验会专业很多。
3.3 报名审核与作品提交的关联设计
报名表是连接用户和竞赛的桥梁,也是整个平台业务量最大的表。设计上要特别注意两件事:一是同一个学生对同一个竞赛只能报名一次,这个直接用唯一索引在数据库层面兜底,索引列是(competition_id, user_id);二是报名和作品提交的关系,建议合并成一张表,报名时先填写基本信息,提交作品时再补充附件URL和作品说明。
CREATE TABLE tb_registration ( id BIGINT PRIMARY KEY AUTO_INCREMENT, competition_id BIGINT NOT NULL, user_id BIGINT NOT NULL, status TINYINT DEFAULT 0 COMMENT '0待审核 1已通过 2已拒绝', team_name VARCHAR(100), work_title VARCHAR(200), work_file VARCHAR(255), submit_time DATETIME, UNIQUE KEY uk_comp_user (competition_id, user_id) );把报名和作品放在同一张表里,是为了简化查询。前端展示“我的报名列表”时,一条记录就能查出竞赛名称和作品提交状态,不用做多表关联。很多同学喜欢把作品单独建一张表,这样做逻辑上没问题,但查询链路会变长,答辩时解释起来也费劲。
3.4 评审打分表设计
评审模块是这个项目区别于普通管理系统的亮点。一个竞赛可以有多个评委,一个作品要被多个评委打分,最终成绩需要汇总计算。基础表结构如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| registration_id | bigint | 报名记录ID |
| judge_id | bigint | 评委用户ID |
| score | decimal(5,1) | 打分,如85.5 |
| comment | varchar(500) | 评语 |
| create_time | datetime | 评分时间 |
需要注意的是,评委和竞赛的关系最好也单独建一张关联表,管理员创建竞赛后从教师用户里选择一批评委,评委评分时只能看到自己负责的竞赛。如果没有这层关系,一个教师就能给所有竞赛打分,权限上就失控了。关联表字段很简单,竞赛ID加评委ID就够了,但加上之后系统的完整度立刻不一样。
4. 关键业务实现与代码级解析
4.1 后端分层结构与项目目录
一个标准的SpringBoot项目目录结构如下:
com.example.competition ├── controller # 接收前端请求,接口入口 ├── service # 业务逻辑处理 ├── mapper # MyBatis的Mapper接口 ├── entity # 数据库实体类 ├── dto # 前端入参、出参封装 ├── common # 统一响应体、异常处理、工具类 └── config # 拦截器、跨域配置等Controller层只做参数接收和结果返回,不写业务逻辑;Service层处理所有业务规则,比如报名时校验竞赛状态、评审时校验评委身份;Mapper层负责SQL操作。分层的价值在于:答辩时你能清晰说明每一层职责,出了问题也容易定位。我见过不少同学把所有代码写在Controller里,虽然能跑,但答辩老师问一句“如果我要增加一个通过邮件通知用户的逻辑,应该加在哪一层”就答不上来。
4.2 JWT登录鉴权的完整落地
登录鉴权是整个项目里最容易被问到的技术点。推荐方案是JWT加拦截器,流程如下:
- 用户提交用户名和密码,后端校验通过后生成一个JWT Token。
- Token中包含用户ID、用户名、角色,并设置过期时间(建议2小时)。
- 前端拿到Token后存到localStorage,每次请求在Axios拦截器里把Token放进Authorization请求头。
- 后端拦截器解析Token,校验通过后放行,并把用户信息写入ThreadLocal或者请求上下文对象中。
JWT生成的核心代码片段:
// 引入jjwt依赖后,生成Token的代码大致如下 String token = Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim("username", user.getUsername()) .claim("role", user.getRole()) .setExpiration(new Date(System.currentTimeMillis() + 2 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact();前端Axios拦截器的写法:
axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = token; } return config; });这个方案实现成本很低,却能回答关于Token认证和前端请求拦截的几乎所有问题。相比使用Spring Security配一套复杂的过滤器链,这个方案对课设和毕设来说更可控。
4.3 前端路由配置与登录守卫
既然前端用了Vue,路由自然选Vue Router。项目管理后台需要区分不同的页面,比如首页、竞赛列表、竞赛详情、报名管理、作品提交、评审打分、个人中心。路由配置看起来很简单,但有一个细节必须处理:未登录用户不能访问需要登录的页面,学生不能跳转到教师的管理页面。
实现方式是用路由守卫做前置拦截:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); const role = localStorage.getItem('role'); if (to.meta.requiresAuth && !token) { next('/login'); } else if (to.meta.roles && !to.meta.roles.includes(role)) { next('/403'); } else { next(); } });路由守卫的作用不只是前端体验,它还能在一定程度上挡住越权访问。真正的权限校验必须在后端做,但前端守卫能减少无意义的请求,也让用户界面更友好。配置meta信息时,把requiresAuth和roles字段写清楚,后面维护起来很轻松。
4.4 作品附件的上传方案
竞赛作品通常是一个压缩包、PDF或文档,上传接口用SpringBoot的MultipartFile实现。最简单的做法是保存到本地磁盘目录,然后把访问路径存到数据库的work_file字段。核心配置如下:
spring: servlet: multipart: max-file-size: 100MB max-request-size: 100MB上传接口的核心逻辑:
@PostMapping("/upload") public Result upload(@RequestParam("file") MultipartFile file) { String originalFilename = file.getOriginalFilename(); String extName = originalFilename.substring(originalFilename.lastIndexOf(".")); String serverName = UUID.randomUUID().toString().replaceAll("-", "") + extName; String savePath = uploadDir + "/" + serverName; file.transferTo(new File(savePath)); return Result.success("/files/" + serverName); }这里用UUID重命名文件,避免中文文件名和同名文件覆盖的问题。另一点是上传目录要加一个映射,可以用WebMvcConfigurer把本地目录映射为虚拟路径,这样前端通过/files/xxx就能直接访问文件内容。
5. 从零到一运行与部署实操记录
5.1 环境准备清单
动手之前先把环境数量理清楚,尽量一次到位,不要边写边装浪费时间。
| 软件 | 版本建议 | 说明 |
|---|---|---|
| JDK | 1.8 | 稳定兼容性好,不要追求JDK17 |
| Maven | 3.6.x | 从阿里云镜像拉依赖 |
| MySQL | 8.0 | 字符集utf8mb4 |
| Node.js | 14及以上 | 对应Vue CLI或Vite |
| IDEA | 2022+ | 使用社区版也可 |
| Navicat | 16 | 可视化建库导库 |
Maven安装后一定要把settings.xml里的镜像更换为阿里云地址,否则依赖下载速度会让你怀疑人生。
5.2 后端配置与启动步骤
拿到源码后,第一步是新建数据库competition_db,然后执行数据库脚本。注意脚本执行顺序:先建表后插入初始数据。管理员账号通常已经在脚本里写死了,比如admin/admin123。
application.yml的核心配置示例:
server: port: 8081 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/competition_db?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 你的密码执行完这些配置后,启动项目的入口类。看到“Started Application in xxx seconds”日志后,先用浏览器访问 http://localhost:8081 ,如果能返回统一的404或者错误JSON,说明后端已经正常启动。此时可以用接口测试工具请求登录接口验证账号密码是否能用。
5.3 前端环境安装与代理配置
前端项目通常是一个独立的目录,比如competition-web。进入目录后执行:
npm install这一步要用国内镜像,不然node_modules下载会很慢。临时用镜像可以执行:
npm install --registry=https://registry.npmmirror.com启动之前,检查vue.config.js里的代理配置:
devServer: { proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } }前端所有请求都以/api开头,代理转发到后端端口,这样开发阶段就绕开了跨域。执行npm run dev后访问localhost:8080,先用管理员账号登录试试,看首页数据和列表接口是否正常。这一步是验证前后端联调的最好时机,有任何问题都在这一阶段集中排查。
5.4 项目打包与生产部署
本地跑通之后,还需要能打包给答辩演示使用。后端的打包很简单:
mvn clean package -DskipTests打包后的jar文件在target目录下,服务器上执行:
nohup java -jar competition-api-0.0.1.jar > app.log 2>&1 &前端打包:
npm run build打包后的dist目录是纯静态文件,可以用Nginx托管。Nginx配置里除了server静态目录,还要加一个API反向代理:
server { listen 80; server_name your-domain; location / { root /opt/competition/dist; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files这个配置非常关键,Vue Router使用History模式时,刷新页面会404,加上它之后所有路由都回退到index.html,由前端路由自行接管。
6. 常见问题排查与避坑经验
6.1 MySQL连接报错类型与处理办法
每个用这个项目的人遇到的第一批坑几乎都在数据库连接上。常见的三种报错和解决方式如下:
- Communications link failure,通常是连接地址或密码错误,先检查url里的localhost和密码是否能对上。
- SSL连接错误,在url末尾追加useSSL=false即可。
- Public Key Retrieval is not allowed,如果是和MySQL连接相关的工具报这个错误,需要把allowPublicKeyRetrieval参数设为true。
另外还有一个很常见的问题是字符集不对导致中文乱码,建库时使用utf8mb4,连接字符串里加上characterEncoding=utf8mb4,基本就能解决。
6.2 后端端口冲突与启动失败
后端端口被占用是启动时最常见的异常。日志里会出现Address already in use之类的关键信息。排查命令:
netstat -ano | findstr 8081Windows系统看到进程PID后,到任务管理器结束对应进程,或者直接用命令杀掉。这个坑虽然小,但会浪费很多无意义的排查时间。
6.3 前端页面空白与接口404问题
前端启动后页面白屏,第一反应先看浏览器控制台。如果报错是API请求404,那大概率是代理配置没生效,检查vue.config.js里的target是否指向后端端口、是否改了配置后没有重启前端服务。如果报错是资源加载失败,则可能是node_modules没装全,删除后重新npm install。
6.4 适合课设和毕设的增量扩展建议
如果你的项目做完基础功能之后还有时间和精力,我强烈建议加了几个小而实用的功能:一是接入数据可视化组件ECharts,在首页做一个参赛人数趋势图和竞赛类型饼图,视觉冲击力强且实现简单;二是用Apache POI导出报名名单Excel,这是教务处最常见的需求;三是引入MinIO做文件对象存储,摆脱本地磁盘存储单点问题。这三个方向按顺序做,每一个都能在答辩时成为独立的亮点。
写在最后
这个项目我自己实际跑过不少遍,最大的感受是:它的难点不在于单个技术有多深,而在于模块之间的数据流能不能闭环。从竞赛发布,到学生报名,到教师审核,再到评委打分,每一步都有前置状态和后置动作,把这条链路理清楚,代码只是水到渠成的事情。最后再分享一个小建议:拿到任何一个毕设源码,不要急着打开IDE跑起来,先花半小时把数据库表看一遍,画出表之间的关联关系,再对照着功能清单看接口,最后才是看页面。这个习惯不仅能帮你快速理解项目逻辑,也会让你在答辩时面对老师的任何提问都能从容对应。