
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组件库表格、表单、弹窗、分页半小时就搭完。相比ReactVue的模板语法对新手更友好过度动画、组件通信的心智负担也小很多。整套架构是典型的前后端分离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.xVue3用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侧再用路由守卫控制页面入口的显示前端隐藏并不能真正防越权后端校验才是安全底线。我在项目里会用JWTJSON 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) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT竞赛报名表;uk_contest_user这个唯一索引是我强烈建议维护的字段设计——它保证同一个学生同一个竞赛只能报名一次即使并发请求同时进来数据库级别的唯一约束也能挡住重复数据应用层的判断只是辅助手段。3.3 状态流转设计让业务有序推进竞赛和报名记录都应该是状态机模型。竞赛的状态由后端一个定时任务或延迟判断来驱动当前时间小于start_time是未开始在start_time和end_time之间是报名中超过end_time自动关闭报名功能。报名记录的状态则靠管理员审核驱动待审核 - 通过/驳回驳回后学生可以修改信息重新提交这个操作要额外加上报名截止时间的校验。状态机的价值在于将模糊的业务规则显式化。你接手源码后第一步应该看每个实体类的状态字段和状态变更方法弄清楚每个合法迁移路径这比逐行读代码更能把握系统骨架。4. 核心功能模块实现细节4.1 登录鉴权JWT方案落地登录接口是系统最核心的入口也是安全隐患最多的地方。我用JWT实现无状态鉴权流程是这样的前端axios向/api/auth/login提交账号密码。后端UserService根据用户名查用户表BCrypt匹配密码。匹配成功后生成TokenToken里存三个关键信息用户ID、角色、过期时间。后续请求经过自定义拦截器从请求头Authorization里取Token校验签名和过期时间解析出用户ID和角色放入ThreadLocal供业务层使用。接口权限用自定义注解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; filenameregistrations.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 -versionMaven3.6命令行执行mvn -vMySQL5.7或8.0命令行执行mysql --versionNode.js14.x或16.x命令行执行node -vnpm6.x或7.x命令行执行npm -vIDEIntelliJ 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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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/serverTimezoneAsia/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托管前端静态文件、反向代理后端APIserver { 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 rootlocalhost数据库密码和yml配置不一致检查application.yml中密码Unknown database contest_db数据库没建成功重新执行建库SQL并刷新连接Port 8080 was already in use8080端口被占用netstat -ano查占用进程或改server.portTable xxx doesnt 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里写着详细的环境要求、数据库导入顺序、常见问题汇总比你盲猜省一个小时。这个习惯在我看来比代码能力本身更重要——真正的工程开发读文档永远是第一步。