
学科竞赛这件事真正折磨人的往往不是比赛本身而是赛前报名、赛中材料流转、赛后成绩归档这一整套流程。我见过太多团队用共享表格加聊天群硬扛报名信息重复提交、作品版本对不上、评委打分靠邮件来回传等到要统计获奖情况时几个人对着十几份表格熬夜核对。这套基于 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 PlusVue 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 环境清单与版本对齐拿到一个开源项目第一步不是急着运行而是把环境版本对齐。版本错配导致的报错往往信息晦涩新手容易卡在这一步就放弃。组件建议版本说明JDK8 或 17看项目 pom 里的编译版本别硬上高版本Maven3.6用于后端依赖管理和打包MySQL5.7 或 8.0注意驱动类名和连接串参数差异Node.js16 或 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是否带charsetUTF-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 借鉴论文结构而不是照抄论文的价值在于提供一种把工程讲清楚的范式背景与意义、需求分析、总体设计、详细设计、数据库设计、系统实现、测试、总结。你可以借鉴这个结构组织自己的文档但内容必须换成你实际做的和理解的。照抄的风险不只是查重更在于答辩时被问到细节答不上来反而暴露没真懂。我在实际带人做这类项目时的体会是先把系统跑起来、把一条完整业务链走通比如从发布竞赛到公示成绩再回头读论文理解会快很多。纸上读十遍设计不如亲手点一遍流程。跑通之后再去改一个小功能比如给报名加一个备注字段从数据库到前端完整走一遍这套系统的脉络就真正长在你脑子里了。