拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

大学竞赛报名管理系统:Spring Boot + Vue前后端分离实战

大学竞赛报名管理系统:Spring Boot + Vue前后端分离实战

1. 项目核心思路与价值定位

1.1 这个系统到底解决了什么问题

大学里的竞赛报名,常年处于一种“原始社会”的状态。教务处发一个红头文件到各学院,学院教学秘书转发到班级群,班长用Excel手工统计,最后汇总出一个几百行的表。这个过程里,学生填错学号、漏报项目、错过截止日期的情况比比皆是;管理员收到几十个版本的Excel,光合并格式就要忙活一下午。

这套基于Java + Spring Boot + Vue的大学竞赛报名管理系统,目标就是把这条手工链路搬到线上:学生在线查看竞赛列表、一键报名、上传作品附件;管理员在后台发布竞赛、审核报名、导出统计报表;系统自动处理报名截止时间、重复报名校验、参赛资格筛选这些容易出错的环节。本质上是把一套传统的信息收集工作,改造成一个带状态流转、权限隔离和数据闭环的Web应用。

1.2 适合谁来参考这套源码

三类人最值得研究这份源码。第一类是计算机相关专业的在校生,毕业设计或课程设计直接可以用这个业务场景,麻雀虽小五脏俱全——前后端分离、RBAC权限模型、文件上传、Excel导入导出,全是面试高频考点。第二类是刚入门Spring Boot + Vue的开发者,想找一个结构清晰、注释到位、能跑起来的完整项目作为学习样板。第三类是高校信息中心的老师或学生团队,希望在最短时间内上线一个够用、能改、不复杂的竞赛管理工具。

这套代码的定位非常精准:不追求大而全的微服务架构,而是把Spring Boot + Vue这套组合在一个单体项目里用到极致,对个人开发者和小团队来说,这是性价比最高的路线。

2. 技术栈选型与架构拆解

2.1 为什么是Spring Boot + Vue

选择技术栈不是拍脑袋,核心考量是“学习成本可控 + 生态成熟 + 招人好招”。

后端选Spring Boot,因为它把Spring那套复杂的XML配置彻底干掉了,内嵌Tomcat,一个jar包直接启动。做竞赛报名这种CRUD密集的系统,Spring Boot提供的spring-boot-starter-web、spring-boot-starter-data-jpa或MyBatis-Plus几乎是开箱即用。更关键的是,Spring生态的拦截器、过滤器、事务管理机制,能很自然地处理登录校验、接口鉴权、事务回滚这些非功能需求。

前端选Vue,因为它是目前国内中小型管理系统的事实标准。Vue 2的Options API足够简单,Vue 3的Composition API更灵活,配Element UI或Element Plus组件库,表格、表单、弹窗、分页半小时就搭完。相比React,Vue的模板语法对新手更友好,过度动画、组件通信的心智负担也小很多。

整套架构是典型的前后端分离:Vue通过axios调Spring Boot的RESTful API,数据格式统一为JSON,后端不关心页面渲染,前端不关心SQL拼接。部署时可以打包Spring Boot jar和Vue静态资源分开放,也可以像我后面要讲的,把Vue打成静态文件扔进Spring Boot的static目录合并部署,避免跨域麻烦。

2.2 关键依赖选型建议

我在这个项目里用到的后端核心依赖大致是:

依赖版本建议用途
spring-boot-starter-parent2.7.x统一依赖管理,不建议直接上3.x,部分组件兼容性麻烦
mybatis-plus-boot-starter3.5.x简化单表CRUD,内置分页插件
mysql-connector-java8.0.xMySQL 8连接驱动
lombok最新稳定版省掉getter/setter的样板代码
hutool-all5.8.x工具类,处理Excel导入、ID生成、日期转换非常方便
jjwt0.9.1生成和校验JWT Token

前端的关键依赖对应如下:

依赖版本建议用途
vue2.6.x(如用Vue3则3.2.x)核心框架
vue-router3.x(Vue3用4.x)前端路由,控制页面跳转
axios1.xHTTP请求库,统一封装请求拦截器
element-ui2.15.x桌面端UI组件库
echarts5.x可选,做报名数据可视化报表

这里有一个经验:不要盲目追新版本。Spring Boot 3.x虽然已经发布很久,但它基于Jakarta EE规范,很多旧版MyBatis-Plus、旧版jwt库的兼容性都有坑。对于课程设计和毕设,稳定跑通比版本新更重要。我用的这套2.7.x组合,经历过大量生产环境验证,踩坑成本最低。

2.3 项目目录结构与职责划分

无论源码包怎么组织,建议你拿到手后先确认目录结构是否清晰。规范的Maven工程结构应该是这样的:

contest-registration-system/ ├── pom.xml # Maven父POM ├── src/main/java/ │ └── com/example/contest/ │ ├── ContestApplication.java # Spring Boot启动类 │ ├── config/ # 配置类(WebMvcConfig、CorsConfig、MybatisPlusConfig) │ ├── controller/ # 控制层(Web层入口) │ ├── service/ # 业务层(核心逻辑) │ │ └── impl/ │ ├── mapper/ # MyBatis-Plus数据访问层 │ ├── entity/ # 数据库实体类 │ ├── dto/ # 数据传输对象(接收前端参数) │ ├── vo/ # 视图对象(返回给前端的数据) │ ├── common/ # 通用类(Result统一返回、异常处理) │ ├── util/ # 工具类(JwtUtil、ExcelUtil) │ └── interceptor/ # 拦截器(登录校验、管理员权限校验) ├── src/main/resources/ │ ├── application.yml # 数据库、Redis、文件上传等核心配置 │ ├── mapper/ # MyBatis-Plus XML文件(复杂SQL用XML写) │ └── static/ # 打包后可放前端静态文件 │ └── admin/ # Vue打包后的dist内容复制到这里 └── web/ # 独立的前端项目(Vue工程源码) ├── src/ │ ├── api/ # 接口请求封装 │ ├── router/ # 路由配置 │ ├── store/ # Vuex状态管理 │ ├── views/ # 页面组件 │ └── main.js # 前端入口文件 └── package.json

这套结构的核心思想是按技术层次分包,而不是按业务模块分包。小项目这样分包逻辑清晰、找文件快;如果竞赛种类特别多、业务分支复杂,再考虑按模块分包也不迟。

3. 系统功能规划与数据库设计

3.1 角色权限:三种角色各管一摊

竞赛报名管理系统里,权限设计是第一个容易出错的地方。常见的角色划分有三种:

角色核心权限对应功能
学生查看竞赛、在线报名、上传作品、查看自己的报名记录和审核状态个人中心、竞赛广场、报名管理
教师/评委查看分配给自己的竞赛报名信息、评审打分、录入成绩评审管理、成绩录入
系统管理员发布竞赛、管理用户、审核报名、导出统计、维护系统配置全部后台功能

权限实现最朴素的方案是Spring Boot拦截器 + 注解。自定义一个角色注解,拦截器解析JWT Token里的角色字段,再判断当前请求的接口是否需要对应角色才能访问。Vue侧再用路由守卫控制页面入口的显示,前端隐藏并不能真正防越权,后端校验才是安全底线。

我在项目里会用JWT(JSON Web Token)做无状态登录,用户的角色信息直接编码在Token里,后端校验时不需要查库,性能好,也方便前端在本地保存登录状态。

3.2 核心数据表与关键字段设计

数据库设计是这套系统的地基。我给出一份经过实践调整的表结构设计,你在用源码的时候可以对照检查:

第一张是用户表t_user,注意角色字段建议用字符串枚举,不要用数字编码——因为在维护阶段,人眼能直接看懂的字段比什么省空间的技巧都重要:

CREATE TABLE `t_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` varchar(50) NOT NULL COMMENT '登录账号(学号/工号)', `password` varchar(255) NOT NULL COMMENT '加密后的密码(BCrypt)', `real_name` varchar(50) NOT NULL COMMENT '真实姓名', `role` varchar(20) NOT NULL COMMENT '角色:STUDENT/TEACHER/ADMIN', `department` varchar(100) DEFAULT NULL COMMENT '所属院系, `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `status` tinyint(1) DEFAULT '1' COMMENT '状态:1正常 0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';

密码存储必须用BCrypt加密,这是很多新手容易忽视的点。明文密码一旦数据库泄露就是灾难,BCrypt每次加密结果不同、自带盐值,是目前最稳妥的密码散列方案之一;Spring Security的BCryptPasswordEncoder直接就能用,不用自己造轮子。

第二张是竞赛表t_contest,这里要特别注意报名开始时间和报名结束时间的校验,不能只在前端限制,后端接口也要判断,否则绕过页面直接调API就能在截止后报名:

CREATE TABLE `t_contest` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL COMMENT '竞赛名称, `type` varchar(50) DEFAULT NULL COMMENT '竞赛类别(学科竞赛/创新创业/文体比赛等)', `description` text COMMENT '竞赛详情描述', `start_time` datetime DEFAULT NULL COMMENT '报名开始时间', `end_time` datetime DEFAULT NULL COMMENT '报名截止时间', `max_team_size` int(11) DEFAULT '1' COMMENT '团队最大人数,默认个人赛', `status` varchar(20) DEFAULT 'PENDING' COMMENT '状态:PENDING报名中/CLOSED已截止/ONGOING进行中/FINISHED已结束', `creator_id` bigint(20) DEFAULT NULL COMMENT '创建人ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_status` (`status`), KEY `idx_end_time` (`end_time`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='竞赛信息表';

第三张是报名表t_registration,这是系统里数据量最大、并发最高的表,必须建好唯一索引来避免重复报名:

CREATE TABLE `t_registration` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `contest_id` bigint(20) NOT NULL COMMENT '竞赛ID', `user_id` bigint(20) NOT NULL COMMENT '报名的学生用户ID', `team_name` varchar(100) DEFAULT NULL COMMENT '团队名称(团队赛时填写)', `member_info` text COMMENT '队员信息JSON', `status` varchar(20) DEFAULT 'PENDING' COMMENT '状态:PENDING待审核/APPROVED已通过/REJECTED已驳回/CANCELLED已取消', `audit_comment` varchar(500) DEFAULT NULL COMMENT '审核意见', `work_file` varchar(255) DEFAULT NULL COMMENT '参赛作品文件路径', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_contest_user` (`contest_id`, `user_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='竞赛报名表';

uk_contest_user这个唯一索引是我强烈建议维护的字段设计——它保证同一个学生同一个竞赛只能报名一次,即使并发请求同时进来,数据库级别的唯一约束也能挡住重复数据,应用层的判断只是辅助手段。

3.3 状态流转设计:让业务有序推进

竞赛和报名记录都应该是状态机模型。竞赛的状态由后端一个定时任务或延迟判断来驱动:当前时间小于start_time是未开始,在start_time和end_time之间是报名中,超过end_time自动关闭报名功能。报名记录的状态则靠管理员审核驱动:待审核 -> 通过/驳回;驳回后学生可以修改信息重新提交,这个操作要额外加上报名截止时间的校验。

状态机的价值在于将模糊的业务规则显式化。你接手源码后,第一步应该看每个实体类的状态字段和状态变更方法,弄清楚每个合法迁移路径,这比逐行读代码更能把握系统骨架。

4. 核心功能模块实现细节

4.1 登录鉴权:JWT方案落地

登录接口是系统最核心的入口,也是安全隐患最多的地方。我用JWT实现无状态鉴权,流程是这样的:

  1. 前端axios向/api/auth/login提交账号密码。
  2. 后端UserService根据用户名查用户表,BCrypt匹配密码。
  3. 匹配成功后生成Token,Token里存三个关键信息:用户ID、角色、过期时间。
  4. 后续请求经过自定义拦截器,从请求头Authorization里取Token,校验签名和过期时间,解析出用户ID和角色,放入ThreadLocal供业务层使用。
  5. 接口权限用自定义注解@RequireRole("ADMIN")标记,拦截器比对角色。

核心代码结构大致是:

@Component public class JwtUtil { private SecretKey key = Keys.hmacShaKeyFor("your-256-bit-secret".getBytes()); public String generateToken(Long userId, String role) { Date now = new Date(); Date expire = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000L); return Jwts.builder() .claim("userId", userId) .claim("role", role) .setIssuedAt(now) .setExpiration(expire) .signWith(key) .compact(); } public Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); } }

这里有一个比较关键的实践点:JWT的签名密钥一定要放在配置文件中,不要硬编码在代码里,而且要用足够长的随机字符串,生产环境建议用RSA非对称签名的私钥。

4.2 报名流程:并发与校验的平衡

报名接口是学生用得最多的功能,处理不好容易出线上问题。我的处理逻辑分四步,每一步都不能省:

第一步,参数校验。解析出contestId和userId后,先查竞赛是否存在且状态为报名中。 第二步,时间校验。当前时间早于start_time或晚于end_time,直接拒绝,不能只在SQL判断里带条件,要明确给前端提示文案。 第三步,重复性校验。通过唯一索引uk_contest_user兜底,同时先查一次缓存或数据库给出友好提示:"您已报名该竞赛,请勿重复操作"。 第四步,业务落库。插入报名记录,状态为待审核,同时给参赛学生生成一条待办通知。

这里的核心是先校验后写入,校验和写入之间不跨事务,因为一个请求里多步骤操作必须保证原子性——要么全部成功,要么全部回滚。用Spring的@Transactional统一管理报名记录插入和竞赛报名人数更新的两个数据库操作,一个失败另一边不回滚的话,统计数字就会错乱。

而且这里不能用乐观锁版本号做过度设计,一张竞赛报名表的数据量级没有到那种需要高频更新版本的规模,反而会增加每次请求的SQL复杂度。

4.3 文件上传:实现作品提交功能

竞赛系统里,作品文件上传是关键功能。这个项目我采用本地文件存储方案,没有上MinIO或者OSS,原因是课程设计/毕设阶段不需要为文件存储单独维护一套服务,本地磁盘固定目录存储已经完全够用。

前端用el-upload组件,action地址指向后端/api/file/upload,支持jpg、png、pdf、zip、docx等格式,大小限制50MB。后端接收MultipartFile后做三件事:白名单校验文件扩展名、生成UUID重命名防止文件名冲突和路径穿越、根据日期创建子目录存储。

存储路径统一走配置项,避免把绝对路径写死在代码里:

file: upload-dir: /data/contest-upload/

这里必须提醒的是:文件名不能直接用用户上传的原名。如果直接用原文件名,第一个隐患是不同学生传了同名文件会互相覆盖;第二个隐患是路径拼接如果没处理好,可能遭遇路径穿越攻击,恶意用户构造../../前缀的文件名,就能往服务器的任意目录写文件。所以务必用UUID.randomUUID()或时间戳重命名,原始文件名只作为展示字段存数据库。

4.4 后台管理:审核、导出与统计

管理员最常用的三个功能是审核报名、导出名单、查看统计数据,这三个功能的质量直接决定系统的口碑。

审核功能本质就是修改报名记录的status字段,但要注意权限校验:普通学生不能把别人的审核状态改成通过。管理员审核时需要回写审核意见,被驳回的学生端能看到拒因,这样才能形成信息闭环。

导出功能我用的Hutool的ExcelWriter工具,把报名表数据 + 学生信息拼装成行写入Excel文件。这里有一个经验:导出接口不要直接在内存里生成几百兆的大文件,对于课程设计阶段的数据量完全没问题,但一定要设置响应头让前端触发下载而不是打开一个空白页:

response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=registrations.xlsx");

统计功能我只是用SQL做了简单的分组统计——按竞赛维度统计报名人数、审核通过率、院系分布。如果要做得更花哨,前端接一个ECharts饼图柱状图,视觉效果好很多,而且ECharts本身就很适合在Vue项目里接图表。

4.5 前端页面设计与路由守卫

前端页面我按三个端来组织:学生端、管理员端、公共端。学生端有竞赛广场、我的报名、个人信息三个主页面;管理员端有竞赛管理、报名审核、用户管理、数据统计四个主页面。

路由守卫是前端安全的第一道门,在vue-router的beforeEach里处理:未登录的用户跳转到登录页,已登录但角色不匹配的跳转到404或者公共页。这里的角色信息存在Vuex里,刷新页面时从localStorage重新加载。

需要特别注意:前端路由守卫只是用户体验层面的约束,不是安全机制。一个懂技术的用户完全可以通过开发者工具修改前端代码,直接向后端接口发admin角色的请求。真正拦住这种越权的,是4.1节讲到的后端拦截器。这种“前端控制展示,后端控制数据”的双层结构,是Web应用安全的基本修养。

5. 环境搭建与项目运行全流程

5.1 本地开发环境清单

拿到源码后,先对照环境清单检查自己电脑有没有装齐工具,这是很多新手卡壳的第一道坎。我把版本和建议用途列成表:

工具版本要求检查方式
JDK1.8或11(和Spring Boot 2.7兼容)命令行执行java -version
Maven3.6+命令行执行mvn -v
MySQL5.7或8.0命令行执行mysql --version
Node.js14.x或16.x命令行执行node -v
npm6.x或7.x命令行执行npm -v
IDEIntelliJ IDEA或VS Code无版本特殊要求

建议直接用IDEA打开后端工程,VS Code写前端Vue页面。IDEA强大的Spring Boot配置提示、快捷键和调试器能提升不少开发效率;前端用VS Code则有丰富的Vue插件,如Vetur和Volar。

5.2 从零到一跑起来:数据库导入

大部分源码包会附带一个sql目录,里面有建库建表的sql文件。如果没有,你得根据第3.2节的表结构手动建库。导入数据库的操作步骤如下:

第一步,创建数据库。用命令行或者Navicat执行:

CREATE DATABASE contest_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

注意这里我指定了utf8mb4而不是utf8。utf8mb4是完整的UTF-8编码,支持所有Unicode字符,包括表情符号和生僻字——竞赛名偶尔会有“创新”“π”这类特殊字符,用utf8就存不下了。

第二步,导入结构文件。命令行执行:

mysql -u root -p contest_db < /path/to/contest_system.sql

或者直接在Navicat里右键数据库运行SQL文件。

第三步,确认初始化数据。源码包里如果带了一个管理员账号,一般会有初始化SQL插入一个admin用户,默认账号密码类似admin / admin123,登录后第一件该做的事就是改密码。没有初始化数据的话就要手动插入:

INSERT INTO t_user (username, password, real_name, role) VALUES ('admin', '$2a$10$...BCrypt哈希值...', '系统管理员', 'ADMIN');

BCrypt哈希怎么生成?强烈建议写个临时main方法用Spring Security的BCryptPasswordEncoder生成,千万不要用网上随便找的在线工具生成的哈希,因为不同实现的盐值处理方式可能有差异。

5.3 后端配置与启动

数据库导入完成后,修改application.yml里的数据源配置。这一步是90%的“启动失败”问题的源头,常见错误是数据库名、用户名、密码没改成自己的本地环境:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/contest_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的数据库密码 servlet: multipart: max-file-size: 50MB max-request-size: 100MB mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl file: upload-dir: /data/contest-upload/

serverTimezone=Asia/Shanghai必须加上,否则新版MySQL驱动会因为时区问题报错。密码是空的就连一个空字符串,但我不建议任何环境用空密码。

然后直接启动ContestApplication.java的main方法。启动成功后在浏览器访问http://localhost:8080/api/health或者看控制台输出,确认Tomcat已启动在8080端口。

5.4 前端工程配置与启动

打开前端目录web,在终端执行依赖安装和启动:

cd web npm install npm run serve

依赖安装成功后Vue开发服务器默认跑在http://localhost:8081。这里前端端口和后端8080不同,就涉及开发环境跨域问题。常规解法是在vue.config.js里配置devServer代理:

module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }

这样前端的/api/xxx请求就会被代理转发到后端,绕过浏览器跨域限制。开发时用代理,部署时用合并部署或配置Nginx反向代理,生产环境尽量不要依赖后端CORS——CORS配置放开所有来源等于给所有人开了一扇大门,有安全隐患。

启动成功后访问http://localhost:8081,有登录页就说明前端环境正常,接下来用管理员账号登录,把系统核心功能都过一遍。

5.5 生产环境部署:前端打包合并方案

上线部署有两种常见方案。方案一,前端打包后直接复制到Spring Boot的static目录,这就是前面目录结构里写过的合并部署:

cd web npm run build # 将dist目录下的所有文件复制到后端的 src/main/resources/static/ 目录下 # 然后重新打包后端 mvn clean package -DskipTests java -jar target/contest-system-1.0.0.jar

这种方案的好处是一个进程搞定所有事情,不需要配置Nginx,内网部署、学生团队维护都非常省心。缺点是静态资源和API共用一个Tomcat线程池,并发量非常高的场景下不如Nginx独立处理静态文件高效——但竞赛报名系统的访问量根本打不到瓶颈。

方案二,前后端完全分离,用Nginx托管前端静态文件、反向代理后端API:

server { listen 80; server_name contest.example.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

try_files $uri $uri/ /index.html这行的作用是解决Vue Router的history模式刷新404问题——不配置的话,用户刷新一个子路由页面就会看到Nginx的404页。

我个人给毕设项目的建议是方案一,因为你们通常只有一台便宜云服务器,部署步骤少意味着你花在环境问题上的时间少,可以把精力放在功能打磨和论文写作上。

6. 常见问题排查与实操避坑实录

6.1 启动报错速查表

我在帮别人跑通这套项目时,汇总过一份出现频率最高的启动报错清单,按出现概率排序:

报错信息根本原因解决方案
Access denied for user 'root'@'localhost'数据库密码和yml配置不一致检查application.yml中密码
Unknown database 'contest_db'数据库没建成功重新执行建库SQL并刷新连接
Port 8080 was already in use8080端口被占用netstat -ano查占用进程,或改server.port
Table 'xxx' doesn't existSQL导入不完整或没指定正确数据库检查数据库连接URL和导入操作
java.sql.SQLSyntaxErrorException表结构字段和实体类不匹配排查是不是MySQL版本语法差异
Failed to configure a DataSource没有配置数据源检查yml文件是否读取到,配置是否完整
页面能打开但接口404后端Controller路径没对上用浏览器直接访问接口URL,对比前端api路径
前端页面空白控制台报错Vue依赖没装全或版本冲突删掉node_modules重新npm install

排查启动问题有个方法论:先看控制台最前面的报错,而不是最后的堆栈行。很多时候真正的错误信息被冗长的异常堆栈淹没了,Spring Boot在启动阶段遇到的致命错误会在日志前几行给出明确提示,比如“No active profile”或者“Application run failed”。

6.2 前端接口报错的三个典型场景

第一个典型场景是跨域报错。浏览器控制台出现CORS policy相关错误,多半是用了代理但proxy没生效,或者没配置代理直接请求了http://localhost:8080。用代理方案就记住:前端代码里的请求地址写成/api/xxx而不是http://localhost:8080/api/xxx,只有最后的网络地址才由代理转发。

第二个典型场景是Token失效。用户操作一段时间后报401,或者前端页面能打开但所有数据请求都失败。JWT的过期时间是我在前面设计里写了一天到一周不等,过期前端axios拦截器检测到401响应后,应该跳转到登录页并提示重新登录。这里有个“续期”方案就是每次请求成功后把新Token刷新到localStorage,但课程设计够用就行,不用做双Token刷新这种高级玩法。

第三个典型场景是参数格式不匹配。后端@RequestBody接收JSON对象,前端如果用application/x-www-form-urlencoded提交表单,参数就会解析为空。axios配置里注意设置Content-Type: application/json。

6.3 唯一索引冲突:重复报名问题

如果前端没做防抖,学生连点两次报名按钮,就会产生两个并发请求同时到达后端。后端第一步查数据库都发现“没有重复记录”,然后都开始执行插入——如果没有唯一索引,这两条记录就都进去了,这就重复报名了。

加了uk_contest_user唯一索引之后,第二条插入语句必然报Duplicate entry异常。后端捕获这个异常,转化为提示“请勿重复报名”,数据库层的兜底逻辑就生效了。这也是我在前面多次强调唯一索引的原因,它让系统的容错能力从“应用层努力防”升级为“数据库硬性保证”。

类似的并发场景还有管理员重复审核同一条报名记录,解决方案一致:在审核操作里加一个WHERE status = 'PENDING'的条件更新语句,靠Update返回影响行数为0来判断记录已经被处理过了。

6.4 文件上传失败的五个排查方向

文件上传失败是竞赛系统另一个高发问题。先看几个容易踩的坑:

第一,大小超限报错。Spring的max-file-size默认只有1MB,照片、PDF作品很容易超。后端yml要显式配置,前端el-upload的limit也要同步配置——两头不一致会产生诡异的报错:前端提示上传成功,后端返回413。

第二,路径非法报错。Windows开发时如果upload-dir: /data/contest-upload/这个目录不存在,会创建失败。要么手动创建,要么代码里用Files.createDirectories在启动时自动建目录,这个细节在Windows和Linux上表现还不一样。

第三,文件名中文乱码。浏览器端传文件名用的是UTF-8编码,后端读取时如果Tomcat的URIEncoding设置不对,中文文件名就变成乱码。最好的治理方案不解决问题,而是压根不用原名——代码里生成UUID作为存储文件名,把乱码问题从根源上消掉。

第四,磁盘占满。本地存储模式上线后要定期清理,作品文件堆积速度很快。运维层面加一个定时任务,清理已结束竞赛的过期临时文件,这类代码不算复杂但很体现工程素养。

第五,上传接口没做登录校验。不登录的人一直往服务器传垃圾文件,把磁盘打满就是一次低成本恶意攻击。拦截器对/api/file/upload要强制要求携带Token。

排查文件问题时,先看文件目录里有没有生成文件,再判断问题出在传输、存储还是配置层,比我这种东猜西猜高效得多。

7. 扩展方向与个人实践体会

7.1 如何从毕设项目进化成简历亮点

这套竞赛报名管理系统,接口和功能按当前状态已经完成了,但如果想让它真正成为面试官眼前一亮的作品,我建议在三个方向做增强。

第一个方向是通知提醒模块。学生报名成功后,对管理员推送消息;竞赛截止日期临近时,系统自动提醒未报名的学生。用Spring的定时任务@Scheduled扫描竞赛的end_time,再配合邮件或站内信接口就能实现。这个功能的业务价值非常直观,面试时能引出消息队列、任务调度的话题。

第二个方向是数据可视化增强。ECharts按年统计竞赛数量、按学院统计参与人数、按类型统计获奖分布,做出三张动态报表。管理员打开首页就能看到全校学科竞赛的参与热力图,这种可视化能力比纯表格更能体现你对业务的理解。

第三个方向是接入认证服务。比如对接学校统一身份认证(CAS/OAuth2),学生直接用学号免注册登录。虽然这套系统的本地账号密码登录可用,但真实高校场景普遍用统一认证对接,懂这个方案意味着你对企业级集成的套路有概念。

7.2 亲历的几个关键教训

第一,数据库字段命名不要用驼峰。Java实体类喜欢驼峰,数据库字段应该用下划线,靠MyBatis-Plus的mapUnderscoreToCamelCase自动转换。我曾经见过一个项目的数据库字段直接用userName这种驼峰命名,后期所有SQL都要写别名,维护体验极差。

第二,Vue打包后的路径问题。如果前端部署在根目录没问题,一旦放到/admin子路径下,Vue默认的静态资源绝对路径就全错了。解决方法是vue.config.js里设置publicPath: './',用相对路径加载资源。这个坑我记了很多年,每次打包部署都要提醒自己检查一遍。

第三,代码里保留清晰的TODO和注释。源码交付时,你写的每一段复杂逻辑都应该有一句注释说明“为什么这么写”,不仅仅是“写了什么”。面试官或老师看代码时最想知道的是这个分支为什么要这么设计,而不是这段代码在做什么。

最后分享一个小技巧,适合所有拿到源码想快速跑通的人:拿到包第一件事不是启动,而是先通读一遍README和配置文件。很多源码包的README里写着详细的环境要求、数据库导入顺序、常见问题汇总,比你盲猜省一个小时。这个习惯在我看来比代码能力本身更重要——真正的工程开发,读文档永远是第一步。

返回列表