学科竞赛这件事,真正折磨人的往往不是比赛本身,而是赛前报名、赛中材料流转、赛后成绩归档这一整套流程。我见过太多团队用共享表格加聊天群硬扛,报名信息重复提交、作品版本对不上、评委打分靠邮件来回传,等到要统计获奖情况时,几个人对着十几份表格熬夜核对。这套基于 Vue 和 SpringBoot 的学科竞赛管理系统,就是冲着这些具体麻烦去的:它把竞赛发布、报名审核、作品提交、评审打分、成绩公示串成一条线,让参赛者、指导教师、管理员各看各的界面、各干各的活。开源免费意味着你可以直接拿来改,附带的论文则给了完整的选题背景和设计论证,适合课程设计、毕业设计参考,也适合想练手一个完整前后端分离项目的中级开发者。下面我按实际落地时会遇到的顺序,把这套系统拆开讲透。
1. 这套系统到底在解决哪些具体麻烦
1.1 从一张报名表说起:传统流程的四个断点
先别急着看代码,想清楚它替代的是什么。假设某学院要办一场程序设计竞赛,传统做法是发通知、收报名表、建群、收作品、找评委、汇总分数。这条链上有四个典型断点。
第一个断点是信息重复采集。报名表里填过的学号姓名,到了作品提交环节又要填一遍,到了获奖登记再填一遍,每次都可能填错。第二个断点是状态不透明,参赛者不知道自己的报名审没审过,管理员不知道哪些人还没交作品,只能挨个问。第三个断点是版本混乱,作品通过群文件提交,最终版和修改版混在一起,评委打开的可能不是最新那份。第四个断点是统计靠手工,评委打完分,管理员要把分散的评分表合并、算平均分、排名次,人一多就容易出错。
这套系统的价值就在于把四个断点接上:一次录入、全程复用;每个环节有明确状态;作品提交有版本记录;评分自动汇总排名。理解了这个目标,后面看它的表结构和接口设计就不会觉得是在堆功能。
1.2 三类角色,三套完全不同的界面诉求
系统里通常划分三类角色,每类人打开系统想看到的东西完全不同,这也是前端路由和权限设计的基本依据。
| 角色 | 核心诉求 | 高频操作 | 界面重点 |
|---|---|---|---|
| 参赛学生 | 我能不能报、报上了没、作品交没交、得了多少分 | 浏览竞赛、提交报名、上传作品、查成绩 | 流程进度清晰,操作入口少而明确 |
| 指导教师 | 我带的队伍报名情况、作品质量、评审进度 | 审核报名、查看作品、参与评审 | 列表筛选和批量查看 |
| 管理员 | 全局掌控:竞赛、人员、评审、统计 | 发布竞赛、分配评委、汇总成绩、导出 | 数据看板与配置项 |
很多初学者做这类系统,容易把三类人塞进同一个界面靠按钮显隐来区分,结果越做越乱。正确做法是登录后按角色跳转到不同的布局容器,菜单项也按角色动态生成。这一点在 Vue 里用动态路由配合路由守卫实现最自然,后面会细说。
1.3 为什么选前后端分离而不是服务端渲染
有人会问,一个学院内部系统,用模板引擎一把梭不就行了,何必上 Vue 加 SpringBoot 两套。这里的选择逻辑值得说清楚。
前后端分离的核心好处是职责边界清晰和可复用。后端只负责数据和业务规则,输出 JSON;前端只负责展示和交互。带来的实际收益有三点:一是前端可以独立开发调试,不用等后端页面渲染;二是同一套接口将来可以喂给小程序或移动端,不用重写业务逻辑;三是排错时边界明确,页面白屏查前端,接口报错查后端,不会互相甩锅。
代价也有:需要处理跨域、需要设计统一的响应格式、部署时多一个静态资源服务。对于想练手完整工程能力的开发者,这些"代价"恰恰是学习价值所在。所以这套技术选型不是炫技,而是把真实项目里最常见的协作模式搬进来练一遍。
2. 技术栈拆解:每个选择背后的理由
2.1 前端为什么是 Vue 而不是别的框架
Vue 在这个场景下的优势是上手曲线平缓加生态完整。学科竞赛系统的界面以表单、列表、详情页为主,属于典型的"数据驱动视图",Vue 的响应式机制让"数据变了页面自动更新"这件事几乎零成本。比如报名状态从"待审核"变成"已通过",只要改一下绑定的数据,标签颜色和文字自动跟着变,不用手动操作 DOM。
配套的 Element Plus 组件库覆盖了表格、分页、表单校验、弹窗、上传这些本系统高频用到的组件,省掉大量重复造轮子的时间。状态管理用 Pinia 存登录用户信息和权限,路由用 Vue Router 做动态菜单。这套组合是目前国内中小型管理系统的常见配置,资料多、遇到问题好搜。
需要提醒的是版本匹配。Vue 3 配 Element Plus,Vue 2 配 Element UI,两者 API 差异不小,网上搜到的代码片段要先看清是哪个版本,直接混用会报一堆莫名其妙的错。这是新手最容易踩的第一个坑。
2.2 后端 SpringBoot 的分层与职责
后端采用经典的分层结构,理解每层干什么,改代码时才不会乱放。
- Controller 层:接收请求、校验参数、调用 Service、包装返回结果。它不该写业务逻辑,只做"门面"。
- Service 层:业务规则的核心,比如"报名截止后不允许再提交""一个学生同一竞赛只能报一次""评委不能给自己的队伍打分",这些判断都放这里。
- Mapper/DAO 层:与数据库打交道,用 MyBatis 或 MyBatis-Plus 写 SQL 或借助条件构造器。
- Entity/DTO/VO:实体对应数据库表,DTO 接收前端入参,VO 返回给前端展示。三者分开是为了避免"数据库字段直接暴露给前端"这种耦合。
分层不是形式主义。举个实际例子:竞赛表里有个"状态"字段,数据库存的是数字 0/1/2,但前端要显示"未开始/进行中/已结束"。这个转换如果放在 Controller 里,每个接口都要写一遍;放在 VO 里统一处理,一处改处处生效。这就是分层的实际收益。
2.3 数据库表设计的关键几张表
表设计是这类系统的地基,地基歪了后面全是补丁。核心表大致如下:
| 表名 | 作用 | 关键字段 | 设计要点 |
|---|---|---|---|
| 用户表 | 存学生、教师、管理员 | 角色标识、学号/工号、密码 | 用角色字段区分,不建三张表 |
| 竞赛表 | 竞赛基本信息 | 名称、报名起止、竞赛时间、状态 | 时间字段用 datetime,状态可派生 |
| 报名表 | 学生与竞赛的关联 | 竞赛ID、用户ID、审核状态 | 竞赛ID+用户ID 建唯一索引防重复报名 |
| 作品表 | 提交的作品记录 | 报名ID、文件路径、版本号、提交时间 | 保留历史版本,不覆盖 |
| 评审表 | 评委打分记录 | 作品ID、评委ID、各项分数、评语 | 作品ID+评委ID 唯一,防重复打分 |
| 奖项表 | 获奖结果 | 竞赛ID、作品ID、奖项等级 | 与评审结果关联但独立存储 |
这里有两个设计细节值得展开。第一,报名表加唯一索引是防重复报名最可靠的手段,比在代码里先查再插更稳,因为并发情况下"先查再插"仍可能插重。第二,作品表保留版本号而不是覆盖,是为了应对"评委看的是旧版"这类纠纷,每次提交新增一条记录,最新版本用时间或版本号标记,历史可追溯。
提示:密码字段务必存加密后的值(如 BCrypt),不要存明文。这是底线,不是可选项。
3. 从零跑起来:环境搭建与首次启动
3.1 环境清单与版本对齐
拿到一个开源项目,第一步不是急着运行,而是把环境版本对齐。版本错配导致的报错往往信息晦涩,新手容易卡在这一步就放弃。
| 组件 | 建议版本 | 说明 |
|---|---|---|
| JDK | 8 或 17 | 看项目 pom 里的编译版本,别硬上高版本 |
| Maven | 3.6+ | 用于后端依赖管理和打包 |
| MySQL | 5.7 或 8.0 | 注意驱动类名和连接串参数差异 |
| Node.js | 16 或 18 | 与前端构建工具匹配,过高可能不兼容 |
| npm | 随 Node 自带 | 也可用 pnpm/yarn,看项目锁文件 |
判断 JDK 版本最直接的办法是打开后端pom.xml,看<java.version>或<maven.compiler.source>的值。如果写的是 1.8,就用 JDK 8 或 11 跑,硬用 17 可能遇到模块化相关的报错。MySQL 8 要注意连接串里加时区和是否用 SSL 的参数,否则启动时报连接失败。
3.2 数据库初始化:导入脚本的三个注意点
项目一般会附带一个.sql文件,导入时有三点要留意。
第一,先建库再导表。脚本里可能只有建表语句没有建库语句,需要手动CREATE DATABASE并指定字符集为utf8mb4,否则中文和特殊符号会乱码。第二,检查脚本里的初始账号。多数项目会预置一个管理员账号,密码可能是加密串,登录不上时看脚本里的注释或说明文档。第三,外键与导入顺序。如果脚本开启了外键约束,导入顺序错了会报错,稳妥做法是导入前临时关闭外键检查,导完再打开。
CREATE DATABASE competition_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 导入前可临时关闭外键检查 SET FOREIGN_KEY_CHECKS = 0; -- 执行建表与数据脚本 SET FOREIGN_KEY_CHECKS = 1;3.3 后端启动:配置文件里必须改的几处
后端启动前,application.yml(或.properties)里有几处必须按自己的环境改,否则一定连不上数据库。
- 数据库连接串:地址、端口、库名、用户名、密码。
- 文件上传路径:作品文件存本地哪个目录,路径要真实存在且有写权限。
- 端口号:默认 8080,若被占用需改。
- 跨域配置:前后端分离开发时前端端口不同,需允许跨域,或在前端配代理。
改完用mvn spring-boot:run或直接跑主类启动。看到控制台打印出启动成功的横幅和端口号,说明后端起来了。此时可以用浏览器或接口工具访问一个简单接口验证,比如登录接口,能返回 JSON 就说明链路通了。
3.4 前端启动与代理配置
前端进入目录后先装依赖再启动。装依赖时如果卡在某个包下载慢,可以换国内镜像源。启动后默认在某个端口(常见 8080 或 5173),如果和后端端口冲突要改。
开发阶段最关键的是代理配置。前端请求后端接口时,如果直接写后端地址会触发跨域,标准做法是在前端配置里把/api开头的请求代理到后端地址,这样浏览器看来是同源请求,绕开跨域限制。
// vite.config.js 中的代理示例 server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }配好代理后,前端代码里所有请求都写相对路径/api/xxx,由代理转发。这样将来部署到正式环境时,只需改代理目标或由网关统一转发,前端代码不用动。这是工程化的一个小习惯,但能省掉后期大量改地址的麻烦。
4. 核心业务流程的实现逻辑
4.1 登录与权限:动态路由是怎么跑起来的
登录流程表面简单,背后是权限体系的入口。用户提交账号密码,后端校验通过后返回一个令牌(token)和用户角色信息。前端把令牌存起来,之后每次请求都在请求头里带上它,后端据此识别身份。
真正的难点在动态路由。不同角色看到的菜单不同,实现思路是:前端只定义一份完整的路由表,每条路由标注允许哪些角色访问;登录后根据当前角色过滤出可访问的路由,用router.addRoute动态注册。这样学生登录看不到"竞赛管理"菜单,管理员登录能看到全部。
这里有个常见坑:刷新页面后动态路由丢失。因为动态注册的路由存在内存里,刷新就没了。解决办法是在路由守卫里判断,如果已登录但路由未注册,先重新拉取用户信息、注册路由,再放行。这个细节不处理,用户一刷新就白屏或跳回登录页,体验很差。
4.2 竞赛发布到报名审核的状态流转
竞赛从创建到结束是一条状态链,把状态定义清楚,整个系统的逻辑就顺了。
- 草稿/未发布:管理员创建了但还没开放,学生看不到。
- 报名中:在报名起止时间内,学生可提交报名。
- 报名截止/进行中:不再接受新报名,进入作品提交或比赛阶段。
- 评审中:作品提交截止,评委开始打分。
- 已结束:成绩公示,流程归档。
状态可以由时间自动派生,也可以由管理员手动切换。两种方式各有取舍:自动派生省事但不够灵活(比如提前截止报名就做不到),手动切换灵活但依赖人操作。实际项目里常见做法是"时间做默认判断,管理员可手动覆盖",兼顾两者。
报名审核环节,学生提交后状态为"待审核",教师或管理员审核通过或驳回,驳回要填原因。这里要注意审核动作的幂等性:重复点击审核按钮不应产生多条记录或状态错乱,后端在处理时要判断当前状态是否允许该操作。
4.3 作品提交:文件存储与版本管理
作品提交是这类系统里最容易出问题的环节,因为它涉及文件。几个关键决策点。
文件存哪里:小规模用本地磁盘最简单,配一个上传目录,数据库只存相对路径。规模大了再考虑对象存储。本地存储要注意目录按日期或竞赛分文件夹,避免单目录文件过多影响性能。
文件名怎么处理:绝对不能用用户上传的原始文件名直接存盘,重名会覆盖,特殊字符可能引发路径问题。标准做法是生成唯一文件名(如 UUID 加扩展名),原始文件名单独存数据库用于展示和下载时还原。
版本怎么管:前面说过作品表保留版本号。每次提交新增一条记录,标记为最新版,旧版保留。评审时默认看最新版,但允许查看历史。这样既避免覆盖丢失,又能追溯。
// 生成唯一存储文件名的思路 String originalName = file.getOriginalFilename(); String ext = originalName.substring(originalName.lastIndexOf(".")); String storedName = UUID.randomUUID().toString().replace("-", "") + ext; // 存盘用 storedName,数据库同时记录 originalName 和 storedName注意:上传要做大小和类型校验。不限制大小,一个大文件就能拖垮服务;不校验类型,可能上传可执行文件带来风险。这些校验在后端做,前端校验只是提升体验,不能作为安全依据。
4.4 评审打分与成绩自动汇总
评审环节的设计要点是防重复、可追溯、能汇总。
防重复靠数据库唯一约束(作品ID加评委ID),一个评委对同一作品只能有一条评分记录。可追溯靠保留评分时间和评语,出问题能查。汇总则是把所有评委对某作品的分数按规则计算,常见是去掉最高最低取平均,或直接平均。
计算逻辑放在 Service 层,用一个方法统一处理。这里有个容易忽略的点:评委可能没打完所有作品的分数,汇总时要判断是否所有评委都已完成,否则算出来的平均分不完整。系统里可以给评审加"完成"标记,全部完成才允许汇总公示。
排名时还要处理并列情况。分数相同怎么排名次,是并列同名次还是按其他规则区分,这个规则要提前定好并在代码里体现,否则公示后容易引发争议。
5. 实操中容易踩的坑与排查思路
5.1 跨域报错:先分清是开发还是部署阶段
跨域是前后端分离项目最高频的报错,浏览器控制台出现 CORS 相关提示。排查第一步是分清场景。
开发阶段,前端和后端跑在不同端口,属于跨域。解决办法是前端配代理(前面讲过),让请求走同源。如果配了代理还报错,检查代理路径前缀是否和请求路径一致,/api对/api,多一个斜杠少一个斜杠都可能不匹配。
部署阶段,如果前端打包后和后端分开部署,同样跨域。这时要么后端开跨域配置,要么用反向代理把前后端放到同一域名下不同路径。后端开跨域要注意允许的源、方法、请求头都配对,只配一半仍会失败。
5.2 中文乱码:字符集要全链路一致
乱码问题看着玄学,本质是字符集在某一段不一致。全链路包括:数据库字符集、表字符集、连接串字符集、后端响应编码、前端页面编码。任何一环是 latin1 或 GBK,中文就可能出问题。
排查顺序建议从数据库往上查。先确认库和表是utf8mb4,再看连接串有没有指定编码,然后看后端返回的响应头Content-Type是否带charset=UTF-8。多数乱码在这一串里能找到源头。utf8mb4比utf8多支持 emoji 等四字节字符,新项目直接用utf8mb4。
5.3 文件上传失败:从大小限制到路径权限
上传失败的原因排起来有一条清晰的链路,按顺序查效率最高。
- 前端是否限制:有些组件默认限制文件大小,超了直接拦下。
- 后端上传大小限制:SpringBoot 默认有单文件和总请求大小限制,超了报错,需在配置里调大。
- 服务器/容器限制:反向代理如 Nginx 也有请求体大小限制,默认值可能偏小。
- 存储路径权限:目录不存在或进程无写权限,写入失败。
- 磁盘空间:极端情况,磁盘满了。
按这个顺序逐层排查,基本能定位。经验上,第二和第三步最容易被忽略,因为报错信息不一定直白。
5.4 动态路由刷新丢失与菜单不更新
前面提过刷新丢路由的问题,这里给完整思路。核心是在全局路由守卫里做判断:目标路由需要登录但当前没有用户信息时,先请求用户信息接口,拿到角色后注册动态路由,再用next({ ...to, replace: true })重新进入,确保新注册的路由生效。直接next()有时不生效,因为路由表在本次导航中已确定。
菜单不更新则多半是状态管理没接好。角色变化后,菜单数据源要跟着变,如果菜单是登录时算一次存起来,切换角色(比如测试时改数据库角色)不刷新就不更新。开发时遇到这种情况,清一下本地存储重新登录即可,不是 bug。
6. 二次开发与功能扩展的切入点
6.1 想加消息通知,从哪下手
开源项目往往缺消息通知,而这在实际使用中很需要:报名审核结果、作品提交成功、成绩公示,都该通知到人。扩展思路是加一张通知表,记录接收人、内容、已读状态、时间。触发点在各个业务动作完成后插入一条通知记录,前端加一个消息中心页面轮询或拉取。
如果想做得更实时,可以引入 WebSocket 推送,但对学院内部系统,轮询(比如每分钟查一次未读数)已经够用,实现成本低得多。技术选型要匹配实际需求,不是越新越好。
6.2 统计报表:从导出 Excel 到可视化
管理员最想要的是统计:每个竞赛多少人报名、获奖分布、各学院参与情况。最简做法是后端用工具库(如 EasyExcel)生成 Excel 供下载。进一步可以做可视化,前端用图表库把后端聚合好的数据画成柱状图、饼图。
关键在后端聚合而不是前端算。把原始明细全丢给前端让前端统计,数据量大时又慢又容易算错。正确做法是后端用 SQL 的GROUP BY聚合好,前端只负责展示。这个分工要一开始就定好。
6.3 部署上线:打包与反向代理
开发跑通只是第一步,部署是另一回事。标准流程是:前端npm run build打包出静态文件,后端mvn package打成 jar。部署时 jar 用java -jar启动,静态文件交给 Nginx 托管,Nginx 同时把/api请求反向代理到后端端口。
这样前后端在同一域名下,跨域问题自然消失,也便于统一做访问控制。Nginx 配置里要注意client_max_body_size调大以支持文件上传,以及代理时正确传递请求头。这套部署方式是目前中小型项目的主流,值得完整走一遍。
7. 关于附带的论文怎么用
7.1 论文不是装饰,是设计依据
很多人拿到带论文的项目,只看代码不看论文,其实亏了。论文里的需求分析、用例图、E-R 图、系统架构图,正是代码背后的"为什么"。比如为什么报名表要加唯一索引,论文的数据库设计章节往往有说明;为什么分三类角色,需求分析里有依据。做课程设计或毕业设计答辩时,这些论证比代码本身更能体现你的理解深度。
7.2 借鉴论文结构而不是照抄
论文的价值在于提供一种"把工程讲清楚"的范式:背景与意义、需求分析、总体设计、详细设计、数据库设计、系统实现、测试、总结。你可以借鉴这个结构组织自己的文档,但内容必须换成你实际做的和理解的。照抄的风险不只是查重,更在于答辩时被问到细节答不上来,反而暴露没真懂。
我在实际带人做这类项目时的体会是,先把系统跑起来、把一条完整业务链走通(比如从发布竞赛到公示成绩),再回头读论文,理解会快很多。纸上读十遍设计,不如亲手点一遍流程。跑通之后再去改一个小功能,比如给报名加一个"备注"字段,从数据库到前端完整走一遍,这套系统的脉络就真正长在你脑子里了。