ARTICLE DETAIL

资讯详情

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

基于Spring Boot的竞赛管理系统开发全解:从数据库建模到权限控制

基于Spring Boot的竞赛管理系统开发全解:从数据库建模到权限控制 1. 项目核心需求拆解与整体设计思路1.1 竞赛管理究竟在解决什么问题做了这么多年开发我见过太多高校里的竞赛组织方式还停留在“Excel表格 微信群 人工统计”的阶段。报名信息靠接龙作品提交靠邮箱评审打分靠纸质表格汇总成绩公示靠贴公告栏。大学生竞赛管理系统的核心价值就是把这个链条上的每一个环节搬到线上让管理员、指导教师、参赛学生三方都在同一个平台上协作避免“通知靠转发、统计靠熬夜”的尴尬局面。从我拿到这个项目需求的第一反应来看它至少应该覆盖以下几条核心业务线管理员端创建竞赛、设置报名时间窗口、分配评审专家、审核报名资格、管理作品归档、发布最终成绩与公示文件。教师/评审端查看被分配的作品、在线打分与填写评语、查看历史评审记录。学生端浏览竞赛公告、在线组队报名、上传参赛作品、查看审核状态与最终排名。这个定位决定了系统不是一个简单的 CRUD 练习而是要处理“多角色权限 竞赛全生命周期 评审打分流程”这些真实业务逻辑。毕业设计选这个题目有一个天然优势——业务场景足够具体需求文档好写数据库表结构能设计出水平代码也能展示出一定的复杂度不会像“图书管理系统”那样千篇一律。1.2 技术选型为什么锁死 Spring Boot 全家桶这个项目标题直接点名了 Spring Boot这既是毕业设计的主流选择也是企业级 Java 开发的事实标准。选择 Spring Boot 而不是 Servlet JSP 或者 SSM 手写配置核心原因有三个第一开发效率极高。Spring Boot 的自动配置机制帮我们省掉了大量 XML 配置。以前 SSM 项目要写数据源配置、MyBatis 工厂、事务管理器、视图解析器Spring Boot 只需要在application.yml里写几行参数其余交给自动配置完成。这就是热词里大家反复搜“springboot自动装配原理”的原因——它确实是这个框架最核心、也最能体现设计水平的机制。第二生态极其成熟。权限控制可以用 Spring Security 或 Sa-Token持久层可以用 MyBatis-Plus接口文档可以用 Knife4j文件存储可以用 MinIO 或本地磁盘。这个项目所有需要的能力在 Spring Boot 生态里都有现成的成熟方案我们不需要去造轮子。第三前后端分离是当下主流。毕设如果想要亮点建议不要做传统的服务端渲染页面而是用 Spring Boot 做纯后端接口搭配 Vue 3 Element Plus 做管理后台再配一个移动端适配的 H5 页面给学生使用。这也是热词里“基于springboot vue购物管理系统”这类项目非常多的原因——前后端分离已经是标准姿态。我实际做这个项目时采用的技术栈清单如下供直接参考层次选型说明后端框架Spring Boot 2.7.x稳定且资料多避免直接用 3.x 踩坑持久层MyBatis-Plus 3.5.x自带分页插件和条件构造器省时省力权限认证Sa-Token 1.34比 Spring Security 易上手适合毕设展示数据库MySQL 5.7 / 8.05.7 兼容性更好8.0 功能更强文件存储本地磁盘 Nginx 映射毕设阶段不需要上 MinIO简单可靠接口文档Knife4j 4.x自动生成 Swagger 文档答辩加分前端Vue 3 Element Plus Vite管理后台开发效率高组件丰富提示如果你的毕业设计时间特别紧张后端直接用 Spring Boot 2.7.x 而不是 3.x因为网上能搜到的资料和踩坑记录绝大多数都是基于 2.x 的。等你的项目稳定跑通了再去折腾升级不迟。1.3 角色权限体系的顶层设计竞赛管理系统的用户角色比一般的管理系统多一层复杂性。我把它分为三类共四种角色数据库层面用role字段区分权限拦截在接口层面做系统管理员admin用户管理、竞赛创建、全局配置、评审分配。竞赛负责人manager可以理解为二级管理员负责某个具体竞赛的报名审核、作品归档、成绩发布。评审专家reviewer只能看到分配给自己的待评审作品打分提交后不可随意修改。参赛学生student注册登录、浏览竞赛、报名参赛、上传作品、查看成绩。有一个设计细节需要注意如果初期不想把角色分得太细可以先用admin和student两种角色把主流程跑通后面再扩展manager和reviewer。但数据库表设计时一定要预留角色字段的扩展空间不要用布尔类型的is_admin字段否则改造成本很高。2. 数据库表结构设计与关联建模2.1 核心数据表清单数据库设计是这个项目的灵魂。我见过太多毕设项目代码写得还行但数据库表设计一塌糊涂——该拆的表不拆该建的索引不建关联查询全是笛卡尔积。竞赛管理系统的数据模型其实非常典型可以归纳为“用户-竞赛-报名-作品-评审”五张核心表外加公告、字典、日志等辅助表。下面是我实际抽出来的表结构核心字段按业务重要性排列用户表sys_userCREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, student_no varchar(30) DEFAULT NULL COMMENT 学号, teacher_no varchar(30) DEFAULT NULL COMMENT 工号, role varchar(20) NOT NULL DEFAULT student COMMENT 角色admin/manager/reviewer/student, email varchar(100) DEFAULT NULL, phone varchar(20) DEFAULT NULL, status tinyint(1) DEFAULT 1 COMMENT 1启用 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;用户名必须唯一索引这个不用多解释。角色字段用字符串而不是数字枚举是为了代码里可读性更强this.role.equals(student)一眼就能看懂。竞赛表comp_competition竞赛表是整个系统的核心主数据它需要记录竞赛的基础信息并支撑状态流转。关键的字段设计如下competition_name竞赛名称比如“第十七届全国大学生智能汽车竞赛校内选拔赛”。level竞赛级别包括校级、市级、省级、国家级这个字段后面做统计报表时很有用。type竞赛类型比如学科竞赛、创新创业、文体比赛方便前台分类展示。signup_start_time和signup_end_time报名时间窗口系统要在这个时间段内开放报名按钮过期自动关闭。contest_start_time和contest_end_time竞赛时间。max_team_members最大组队人数单人参赛设为 1。cover_image封面图 URL。status状态字段对应草稿、报名中、评审中、已结束用0/1/2/3四个数字表示。核心建表 SQL 骨架CREATE TABLE comp_competition ( id bigint(20) NOT NULL AUTO_INCREMENT, competition_name varchar(200) NOT NULL, level varchar(20) DEFAULT school, type varchar(50) DEFAULT NULL, description text COMMENT 竞赛简介, cover_image varchar(255) DEFAULT NULL, signup_start_time datetime DEFAULT NULL, signup_end_time datetime DEFAULT NULL, contest_start_time datetime DEFAULT NULL, contest_end_time datetime DEFAULT NULL, max_team_members int(11) DEFAULT 1, status tinyint(1) DEFAULT 0 COMMENT 0草稿 1报名中 2评审中 3已结束, create_by bigint(20) DEFAULT NULL COMMENT 创建人ID, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status (status), KEY idx_signup_end (signup_end_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status和signup_end_time建索引是因为系统高频查询场景是“找出所有报名中且未截止的竞赛列表”这两个字段联合起来走索引扫描效率极高。2.2 报名、作品与评审的关联建模报名表comp_signup报名表解决的是“谁参加了哪个竞赛”的问题。设计时有一个关键决策点一个学生报名一个竞赛是创建一条记录还是团队所有成员各自创建一条记录我推荐一个团队一条主报名记录团队成员用子表存储这样后期审批操作针对的是“队伍”而不是个人。主表字段id、competition_id、captain_id队长用户ID、team_name团队名称、status待审核/已通过/已驳回、signup_time。子表comp_signup_member字段id、signup_id、user_id、role_in_team。主表和子表通过signup_id关联一对一报名单人参赛时子表只插入一条记录。这个设计的好处是查询“某个竞赛有多少队伍报名”只需要count主表查询“某个学生参加了哪些竞赛”需要 join 子表反查SQL 也能写得很干净。作品表comp_work作品表和报名表是 1:1 的关系。一个通过审核的报名记录对应一份作品提交记录。字段大致如下CREATE TABLE comp_work ( id bigint(20) NOT NULL AUTO_INCREMENT, competition_id bigint(20) NOT NULL, signup_id bigint(20) NOT NULL, work_title varchar(200) DEFAULT NULL, work_type varchar(50) DEFAULT NULL COMMENT 论文/实物/软件/视频, summary text COMMENT 作品摘要, file_url varchar(500) DEFAULT NULL COMMENT 文件下载地址, submit_status tinyint(1) DEFAULT 0 COMMENT 0未提交 1已提交 2已撤回, submit_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_signup (signup_id), KEY idx_competition (competition_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里注意signup_id上加了唯一索引防止一份报名记录提交多份作品。业务层面还要再加一层校验只有报名状态为“已通过”的队伍才能上传作品。评审表comp_review评审表的设计最能体现这个项目的专业性。它的关联关系是一个评审专家被分配到某个竞赛的多份作品每份作品可以由多个专家打分最终成绩取平均分。因此设计两张表comp_review_task评审任务分配表和comp_review_score打分记录表。任务分配表字段id、competition_id、reviewer_id、work_id、assign_time、status待评审/已完成。打分表字段id、task_id、reviewer_id、work_id、score百分制、comment评语、review_time。一张任务记录只能对应一条打分记录通过task_id加唯一索引控制。2.3 状态流转的设计心得状态管理看起来简单但实际踩坑最多。我强烈建议竞赛状态、报名状态、作品提交状态全部用显式数字枚举并且在代码里写成常量类或枚举类而不是散落各处的魔法数字。比如public class CompetitionStatus { public static final int DRAFT 0; public static final int SIGNING 1; public static final int REVIEWING 2; public static final int FINISHED 3; }竞赛的完整状态流转是草稿(DRAFT) - 报名中(SIGNING) - 评审中(REVIEWING) - 已结束(FINISHED)。报名结束后系统自动进入评审中管理员分配评审任务后评审专家才能看到待评审列表。这个流转可以在管理员的“结束报名”按钮里触发也可以用 Spring 的Scheduled定时任务扫描时间自动触发。我建议毕设阶段用按钮触发更直观也方便演示时控制节奏。3. 核心功能模块的代码实现与要点解析3.1 登录认证与权限控制认证方案我选择了 Sa-Token理由很简单对于前后端分离项目Sa-Token 的登录鉴权代码量比 Spring Security 少得多而且中文文档友好答辩时你能把每个方法的原理说清楚。登录接口核心逻辑RestController RequestMapping(/api/auth) public class AuthController { Autowired private SysUserService userService; PostMapping(/login) public Result login(RequestBody LoginDTO dto) { SysUser user userService.getByUsername(dto.getUsername()); if (user null) { return Result.error(用户名不存在); } // BCrypt 密码校验 if (!BCrypt.checkpw(dto.getPassword(), user.getPassword())) { return Result.error(密码错误); } if (user.getStatus() 0) { return Result.error(账号已被禁用请联系管理员); } StpUtil.login(user.getId()); // 返回 token 和用户基本信息 return Result.ok(StpUtil.getTokenInfo()); } }Sa-Token 的StpUtil.login()会在服务端创建一个会话并把 token 返回给前端。前端把 token 存在 localStorage 中之后每个请求在请求头里带上satoken这个字段即可。权限拦截器配置Configuration public class SaTokenConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle - { StpUtil.checkLogin(); })).addPathPatterns(/**) .excludePathPatterns(/api/auth/login, /api/auth/register, /api/competition/list, /api/competition/detail/**); } }这里有一个容易被忽略的细节如果使用 Sa-Token 默认的拦截器所有请求都会强制登录所以必须把“竞赛列表查询”和“竞赛详情查询”这两个前台页面不需要登录就能访问的接口放进白名单。否则游客连竞赛公告都看不了体验很差。3.2 竞赛发布与报名模块竞赛发布是管理员功能。核心代码并不复杂就是往comp_competition表插入一条记录同时做几个参数校验报名结束时间要晚于当前时间竞赛开始时间要晚于报名结束时间。这里要注意的是时间字段的格式化处理前端传过来的是2025-05-20 18:00:00这样的字符串后端用DateTimeFormat注解接收后转成LocalDateTime。报名模块的核心难点在于防重复提交和状态校验。一个学生不能对同一竞赛重复报名这个不能只靠前端按钮置灰后端必须做兜底校验。我当时的实现逻辑是public Result signUp(SignUpDTO dto) { // 1. 校验竞赛是否存在且在报名时间内 CompCompetition comp competitionMapper.selectById(dto.getCompetitionId()); if (comp null || comp.getStatus() ! 1) { return Result.error(当前不在报名时间范围内); } LocalDateTime now LocalDateTime.now(); if (now.isBefore(comp.getSignupStartTime()) || now.isAfter(comp.getSignupEndTime())) { return Result.error(报名时间已截止); } // 2. 队长查重防止重复报名 Long userId StpUtil.getLoginIdAsLong(); long count signupMapper.selectCount(new LambdaQueryWrapperCompSignup() .eq(CompSignup::getCompetitionId, dto.getCompetitionId()) .eq(CompSignup::getCaptainId, userId)); if (count 0) { return Result.error(你已报名该竞赛请勿重复操作); } // 3. 成员查重防止一个学生被多个团队拉入同一竞赛 if (dto.getMemberIds() ! null !dto.getMemberIds().isEmpty()) { long memberCount signupMemberMapper.selectCount(...); if (memberCount 0) { return Result.error(团队中存在已报名该竞赛的成员); } } // 4. 插入主记录和成员子记录事务由Service加Transactional保证 }这段代码里面步骤 2 和步骤 3 是两个很容易遗漏的校验点。很多毕设项目只做了步骤 2结果出现“一个学生被 A 队和 B 队同时拉进同一竞赛”的数据脏问题。答辩时把这个坑点主动讲出来反而是加分项。3.3 作品上传与文件存储文件上传这个功能看起来简单但真要做好有几个细节值得注意。首先是存储路径的规划。我在application.yml中做了如下配置file: upload-path: /data/contest-files/ access-path: /files/**后端把文件写到/data/contest-files/目录文件名用 UUID 重新命名保留原始文件扩展名然后通过一个文件访问的WebMvcConfigurer把/files/**映射到物理路径Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceLocations(file: fileUploadPath); }这样前台上传成功后拿到的是/files/xxx.pdf这样的相对路径拼接上后端地址就能访问。为什么要用 UUID 重命名因为学生上传的作品文件名可能是“最终版(1)(2)(3).zip”这种千奇百怪的名字直接落盘可能触发操作系统文件名特殊字符问题UUID 统一命名可以彻底规避。文件类型和大小校验也必须做。毕设阶段至少要限制扩展名为zip/rar/pdf/doc/docx/jpg/png/mp4单文件大小上限建议100MB可以在application.yml里配置 Spring MVC 的上传限制spring: servlet: multipart: max-file-size: 100MB max-request-size: 100MB3.4 评审打分模块与成绩统计评审模块是竞赛管理系统区别于普通报名系统的最核心功能也是我写代码时最花心思的地方。业务流程如下管理员在竞赛进入“评审中”状态后进入评审分配页面选择评审专家勾选需要分配的作品点击“批量分配”。系统为每对“专家-作品”生成一条comp_review_task记录。评审专家用自己的账号登录只能看到分配给自己的、状态为“待评审”的任务列表。点击“评审”按钮进入打分页面填写分数和评语提交后任务状态变为“已完成”。分配任务的代码逻辑里有一个注意点——重复分配问题。如果同一作品已经分配给同一专家再次点击分配需要给出友好提示而不是直接报错。我当时的做法是插入前先查重存在则跳过并在返回结果里提示“3条任务已创建2条跳过重复分配”。成绩统计的核心 SQL 是分组聚合查询SELECT work_id, ROUND(AVG(score), 2) AS avg_score, COUNT(*) AS review_count FROM comp_review_score WHERE competition_id #{competitionId} GROUP BY work_id ORDER BY avg_score DESC这里有一个业务约定需要跟用户确认最终成绩是取平均分还是去掉最高最低分后的均值如果竞赛规模较大专家打分尺度可能不一致去掉最高最低分更稳妥。我实现时在系统参数表里加了一个review_mode配置项默认取平均分管理员可以切换模式。这个设计体现了你对业务的理解深度面试或答辩时能讲出这个东西就是亮点。3.5 前台展示页面的接口设计学生端前台页面虽然技术上不复杂但接口设计要尽量贴合使用习惯。首页轮播图下方是“进行中的竞赛”列表每张卡片展示竞赛封面、名称、级别、报名截止时间倒计时。这个列表的查询接口需要返回每个竞赛的已报名队伍数如果用 Java 代码遍历查询每个竞赛的报名数会出现 N1 查询问题性能很差。更好的做法是写一条关联子查询SELECT c.*, (SELECT COUNT(*) FROM comp_signup s WHERE s.competition_id c.id) AS signup_count FROM comp_competition c WHERE c.status 1 ORDER BY c.signup_end_time ASCMyBatis-Plus 的QueryWrapper不支持这种子查询但可以结合Select注解写在 Mapper 方法里。对于毕设项目数据量几百条这种 SQL 的性能完全没问题而且语义清晰。4. 开发与部署中的常见问题排查实录4.1 Spring Boot 版本与依赖冲突我在开发过程中遇到的第一个大坑就是版本问题。如果直接用 Spring Initializr 生成项目默认可能是 Spring Boot 3.x而 3.x 把javax.servlet换成了jakarta.servlet很多网上搜到的老代码片段直接拷贝会报程序包javax.servlet不存在的错误。我的建议是毕设项目统一用 Spring Boot 2.7.x等以后工作了再慢慢拥抱 3.x。另一个高频问题是 MyBatis-Plus 和 Spring Boot 的兼容性。MyBatis-Plus 3.5.x 需要对应的mybatis-plus-boot-starter版本不要装错成mybatis-plus那是非 Spring Boot 版本。如果启动时报Error creating bean with name xxxMapper优先检查 Mapper 接口上是否加了Mapper注解或者在启动类上加了MapperScan。4.2 分页查询的坑MyBatis-Plus 的分页需要先配置MybatisPlusInterceptorBean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }忘记配这个 Bean 会导致page方法返回的total恒为 0但数据能查出来——这是个非常隐蔽的 bug。排查方式是看控制台 SQL 日志如果 SQL 后面没有LIMIT ?基本就是拦截器没生效。4.3 定时任务与状态自动流转我用Scheduled实现了一个每 30 秒执行一次的定时任务扫描所有状态为“报名中”但当前时间已超过signup_end_time的竞赛把状态改为“评审中”。这里要特别注意定时任务所在的类上要加EnableScheduling注解才能生效。另外定时任务方法里要加日志输出方便排查“时间到了但状态没变”的问题。4.4 文件上传跨域与路径问题前后端分离模式下前端把文件 POST 到后端的/api/file/upload接口最容易遇到跨域问题。如果前端是 Vite 开发服务器默认端口 5173后端是 8080 端口必须解决跨域。我用的是全局 CORS 配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }还要注意一个问题allowCredentials(true)和allowedOriginPatterns(*)必须搭配使用如果用了allowedOrigins(*)会与allowCredentials(true)冲突导致浏览器拦截响应。4.5 打包部署和演示环境的坑毕设最终需要提交可运行的项目包。我建议用 Maven 打包成 jar 包在服务器上用java -jar方式运行不要用 IDEA 直接运行来演示——答辩现场网络和数据库环境不可控提前在服务器上打好包、连好数据库演示时用nohup java -jar contest-system.jar 启动再把前端dist目录放到 Nginx 下并配置反向代理location /api/ { proxy_pass http://localhost:8080/api/; } location /files/ { proxy_pass http://localhost:8080/files/; }注意 Nginx 配置里的proxy_pass路径末尾是否有斜杠这个是老生常谈的坑了。带斜杠表示替换匹配到的前缀不带斜杠则保留原路径搞错了接口会 404。5. 答辩演示技巧与源码学习方法5.1 演示流程如何设计才出彩我见过很多同学做毕设项目功能做得挺全但演示时东点一下西点一下老师看得一头雾水。结合我做这类项目的经验演示流程建议按“一条完整业务线”来走效果最好第一步用管理员账号登录创建一个“2025年度大学生创新大赛”设置报名起止时间系统进入“报名中”状态。第二步切换到学生账号浏览到刚创建的竞赛组队报名现场添加两个成员上传一份作品文件。第三步切回管理员账号审核报名通过结束报名系统进入“评审中”状态。第四步给评审专家分配任务换个账号登录评审端在线打分提交。第五步管理员查看成绩排名发布最终结果学生端能看到自己的排名。这条流程走下来五个核心模块用户管理、竞赛管理、报名管理、评审管理、成绩管理全部展示到了评委能很直观地看到系统的完整性。5.2 拿到源码之后怎么快速上手标题里提到“附源码”很多同学拿到源码第一个动作是急着改代码。我的经验是先别急着写任何代码花一下午把这三件事做完第一看数据库脚本把表结构和字段含义弄明白这是理解业务最快的方式第二看启动类和相关配置文件搞清楚端口号、数据库连接、文件路径都配在什么地方第三跑通一次完整业务流程从前台注册到后台配置再到学生报名亲手点一遍远比读代码高效。然后才是“按功能点去读代码”从 Controller 层看接口从 Service 层看业务逻辑从 Mapper 层看 SQL。不急着全看懂先把登录认证这一条线Aspect/Interceptor - Controller - Service - Mapper理清楚后面就顺了。5.3 从毕设到真实项目的差距在哪里最后说几句实在话。竞赛管理系统作为毕业设计难度和工作量都恰到好处但如果你以后想走开发这条路要知道它和企业级项目的差距主要在三块分布式事务报名和扣减库存这种跨库操作、高并发削峰报名瞬间大量写入、容器化交付Docker K8s。这些在毕设阶段不一定需要做但可以在论文的“不足与展望”里提一下体现你有技术视野。我在开发这个系统的过程中踩过的坑已经整理在上面的“问题排查实录”中了——版本冲突、分页拦截器丢失、跨域与凭证冲突、Nginx 反代路径斜杠每个都是实际项目中高频出现的问题。把这些经验消化成自己的东西无论答辩还是将来面试聊起来都会比别人多一分底气。
返回列表