这个选题我在带学生做毕设时见过太多次了——Spring Boot 学生竞赛管理系统几乎是每年毕业季的常青树。原因很简单它既有业务流程的复杂度报名、评审、成绩、证书又不需要对接支付、高并发这些容易失控的场景难度曲线对本科生非常友好。但正因为做的人多答辩老师对它的预期也高如果只是把CRUD拼起来翻车概率很高。这篇文章我不会给你贴整段整段的完整代码那是课程设计说明书该干的事。我要拆的是这个项目从选题到验收全过程中的核心决策点业务边界怎么划、表怎么设计、权限怎么控制、文件上传和评审打分这类功能怎么实现才不显得廉价以及我踩过的几个坑。你照着这个思路做不仅系统能立住答辩的时候也有话可说。1. 竞赛管理系统到底在管什么先把业务闭环画清楚很多同学拿到这个题目第一反应是“赛事表、报名表、成绩表完事”。真这么做下去做出来的东西就是一个带了几个按钮的增删改查答辩时老师随便追问一句“竞赛的评审流程你怎么建模的”就容易卡住。所以我建议你动手写代码之前先把业务闭环想透。1.1 三方角色学生、评委、管理员各管哪一段这类系统最常见的角色划分是三端学生、教师、管理员。学生端做的事情比较固定——看竞赛公告、报名、上传作品或者填写参赛信息、查成绩、下载证书。教师端则要分清两种身份一种是作为指导老师在赛前帮学生确认报名信息另一种是作为评审专家在赛后打分。同一个教师账号可能同时具备两个身份这在建表时很关键千万别给用户表直接写死一个“is_teacher”字段就完事。管理员端是整个系统的中枢负责竞赛发布、报名截止时间控制、评委分配、打分截止控制、成绩复核发布、证书生成。你会发现管理员要做的事情几乎贯穿所有表所以后台的权限粒度设计比前端页面要多花心思。1.2 从发布到归档七个环节缺一个都不完整一个竞赛的完整生命周期大概是这样的管理员发布赛事包含赛事名称、级别、类型、报名起止时间、作品提交截止时间→ 学生浏览赛事并进行报名 → 指导老师或管理员审核报名资格 → 学生上传作品/填写作品信息 → 管理员分配评委 → 评委在线评审打分 → 管理员核发成绩并归档。这七步对应到系统里就是竞赛表、报名表、作品表、评审表、成绩表、证书记录再加用户和角色权限相关表。建议你画一张简单的流程图贴在项目文档首页开发时对照着明确现状在哪一步答辩时也方便讲“我的系统覆盖了完整闭环”。1.3 定需求时要主动砍掉什么毕设项目最大的问题不是功能少而是需求泛。我看到不少同学想当然地加了一堆功能站内聊天、消息推送、人脸识别登录、在线支付报名费——结果核心业务没做扎实花大量时间在边缘功能上。竞赛管理系统里最该砍的东西第一是聊天无意义会增加复杂度和安全风险第二是花哨的权限设计先做到角色能区分即可第三是复杂的自动抽签分配评委逻辑这个难点可以提但初版用手工指定简单随机即可。我建议的需求定版是核心CRUD 文件的存储与下载 复杂查询统计比如竞赛报名人数趋势、学院参赛率、评委打分分布 成绩导出Excel。这几项每一项都能在答辩时讲出点技术含量而且开发工作量可控。2. 技术栈敲定Spring Boot组合拳怎么选才不被答辩追问到卡壳毕设的技术选型标准只有一个主流且够用并且你能解释清楚为什么选它。不要为了显得“高级”去选冷门框架也不要用已经过时的技术写进需求文档。2.1 为什么是Spring Boot而不是SSH或SSMSpring Boot的好处不需要我吹但你要在答辩时说得出实质嵌入式容器简化了部署自动装配解决了Spring配置地狱加上生态成熟度高。如果老师问“为什么不用经典的SSM结构”你可以答SSM需要大量手写XML配置Spring Boot用注解和自动配置把工程初始化时间从半天压缩到几分钟而底层依然是Spring IoC和MVC那一套学习成本是平滑过度的。Spring Boot版本我建议锁在2.7.x这一个大版本里。原因很现实3.x虽然新但它的Jakarta命名空间调整让很多教材和旧博客里的代码直接报错遇到问题你搜不到直接答案浪费大量时间。毕设求稳2.7.18就是很成熟的收尾版本。2.2 MyBatis Plus还是Spring Data JPA多数人最后的选择这俩我都用过竞赛管理系统这个场景我更推荐MyBatis Plus。理由有两条。第一MP的问世解决了MyBatis原生XML大量手写的问题自带的BaseMapper涵盖了单表CRUD让一张表对应一个Mapper几乎零成本。第二MP的分页插件和条件构造器在写后台多条件筛选时代码量比JPA少很多而且SQL对你不透明你随时能打开控制台看打印出来的语句这对毕设阶段的“可解释性”非常重要。JPA的优势是把关联关系玩得很花哨但你只用单表查询和少量连表查询时根本用不上反而会在懒加载和N1查询这几个坑里消耗大把时间。2.3 前端不建议背太多的锅简单优先如果你的毕设没有强制说必须前后端分离我建议用Thymeleaf Bootstrap jQuery或者这也是最稳妥的组合因为它省掉了跨域问题、Token维护、前端构建打包这些纯工程问题。但如果你们学院明确要求Vue或前后端分离那也可以做只是你要多学一点Vue Router和Axios的用法以及和Spring Boot对接时把CORS配置和JWT校验准备好。多说一句如果用了前后端分离条数最多的就是“接口鉴权”环节别图省事把接口全部放行否则答辩老师一测就知道你的系统连最基本的访问控制都没做。2.4 项目环境版本记录单照抄即可组件版本/选型JDK1.8JDK8仍然是最稳选择11也可Spring Boot2.7.18MyBatis Plus3.5.xMySQL5.7或8.0均可前端模板引擎Thymeleaf不分离/ Vue3分离文件存储本地磁盘目录 Nginx映射或纯Spring Boot静态映射权限控制Spring MVC拦截器 Session够用Excel导出EasyExcel比POI更省心这组组合的最大特点是每个组件都有大量中文资料和踩坑贴遇到问题你卡住的时间会很短。3. 数据库层面十二张表把业务闭环钉死表设计是竞赛管理系统最重要的一环因为几乎所有核心功能都依赖数据模型是否合理。很多同学上来就建表建到后面发现“我的报名表怎么越看越不对劲”那就是当初没有想清楚关系。我先把我建议的表清单列出来再重点拆几张容易出错的表。sys_user用户表sys_role角色表sys_user_role用户角色关联表race_info竞赛表race_signup报名表race_work作品表work_file作品文件表可选race_review评审表race_score成绩表或与评审合并race_certificate证书记录sys_announcement公告表sys_file统一文件主表可选3.1 用户和角色的关系别嫌多一张表用户表和角色表之间加一张关联表sys_user_role看起来比直接在user表里放role_id多一层但好处是将来一个用户可以有多重身份学生可以同时是某个低年级赛事的指导助理教师可以既是指导老师又是评委这种场景都能轻松覆盖。你的角色初始化数据可以由一个启动时执行的CommandLineRunner自动插入这样第一启动系统就有 admin 用户可用展示时也方便。3.2 报名表唯一约束才是最保险的防重复手段race_signup这张表的核心字段是CREATE TABLE race_signup ( id BIGINT PRIMARY KEY AUTO_INCREMENT, race_id BIGINT NOT NULL COMMENT 竞赛ID, user_id BIGINT NOT NULL COMMENT 报名学生ID, student_no VARCHAR(32) COMMENT 学号冗余存储方便列表展示, team_name VARCHAR(128) COMMENT 团队名称, status TINYINT COMMENT 0待审核 1通过 2驳回, create_time DATETIME, update_time DATETIME, UNIQUE KEY uk_race_user (race_id, user_id) ) COMMENT竞赛报名表;这里有个细节值得写进你的答辩词除了在业务代码里判断“用户是否已报名”数据库层面还加了唯一约束。因为并发情况下先查后插的逻辑是有间隔的只有数据库唯一索引才是最终防线。这种双保险的回答一旦说出来老师会明显觉得你是动过脑子的。3.3 评审打分表处理评委与作品的多对多评审是另一个容易翻车的区域。一个评委要评多个作品一个作品又会有多个评委打分所以要设计race_review表CREATE TABLE race_review ( id BIGINT PRIMARY KEY AUTO_INCREMENT, race_id BIGINT NOT NULL, work_id BIGINT NOT NULL, reviewer_id BIGINT NOT NULL COMMENT 评委用户ID, score DECIMAL(5,2) COMMENT 百分制评分允许小数, review_comment VARCHAR(512), status TINYINT DEFAULT 0 COMMENT 0未提交 1已提交, create_time DATETIME, update_time DATETIME, UNIQUE KEY uk_work_reviewer (work_id, reviewer_id) ) COMMENT评审打分表;这里要特别注意“没打完分”和“打完分”是两个状态。如果评委只填了50分但没点“提交”管理员看统计时不能把这个当成最终成绩否则很容易引发投诉。所以status字段很关键成绩统计时只汇总status1的评审数据。3.4 状态字段到底用Integer还是String我强烈建议所有状态字段都用TINYINT/Integer加注释而不要直接用中文字符串存储。原因很简单Integer状态可以配合MyBatis Plus的枚举做类型处理器写Java代码时用枚举常量存到库里是数字既保证编码期可读性又让数据库层面更紧凑而且你可以在代码里全局控制哪些状态是合法的不会有脏数据出现。具体的枚举类可以这样定义示意:public enum RaceStatus { NOT_STARTED(0, 未开始), SIGNING(1, 报名中), SUBMITTING(2, 提交作品中), REVIEWING(3, 评审中), PUBLISHED(4, 已公布), ARCHIVED(5, 已归档); private final int value; private final String desc; // 构造和getter略 }答辩的时候被问到“系统怎么控制竞赛状态流转”你就可以说每次操作前从数据库读race_info的status再结合当前时间来判断是否允许该操作而不是在前端隐藏按钮就算完。这体现的是后端状态机思维。4. 核心功能实现报名、作品提交、评审打分这些硬骨头怎么啃表设计完就到了编码阶段。这个项目的大部分CRUD页面没什么好讲的我带你把真正有含金量的几个环节过一遍。4.1 权限控制拦截器必配注解加分千万别只靠前端藏按钮系统分管理员、评委、学生三个角色最简单可靠的做法是用Spring MVC拦截器统一校验登录和角色。具体步骤是写一个WebConfig配置类注册拦截器设置拦截路径/**记录放行路径比如/login,/api/login,/static/**拦截器里从Session中取当前登录用户没有就重定向到登录页方法级权限用注解加拦截器二次校验比如自定义RequireRole(ADMIN)在拦截器中通过HandlerMethod判断方法是否标注了该注解再比对当前用户的角色这个方法不用引Shiro或Spring Security但对毕设来说已经完全够用而且你自己能讲明白每一行是干什么的。4.2 学生报名与防重复提交后端报名逻辑可以归纳为三步读取竞赛Id验证竞赛状态是否为“报名中”校验当前时间是否在报名起止时间范围内插入报名记录捕获数据库唯一键冲突异常一旦冲突直接返回“您已报名该竞赛”这里有不少同学只做逻辑判断不做数据库兜底然后演示的时候开两个浏览器同时报名同一个人偶遇冲突页面直接报错非常尴尬。建议你在Service方法里把唯一冲突的异常处理单独catch出来返回业务友好的提示。我当时实现时还加了一个很实用的小统计在竞赛列表页每行实时显示“已报名X人”这个数据不用每次count查询因为报名本身就是低频操作可以在报名成功时给对应的race_info表的signup_count字段做自增保持一个冗余统计字段。这是典型的以空间换时间的思路很好讲。4.3 作品上传文件名处理比存储逻辑更容易翻车文件上传这个点功能虽小坑是真多。存储方案我建议就用本地磁盘路径按日期和竞赛Id分目录存放// 示例代码路径示意 String dirPath uploadDir / raceId / LocalDate.now(); File dir new File(dirPath); if (!dir.exists()) { dir.mkdirs(); }文件名必须重命名不要直接存用户原始文件名。一个常见的方案是用UUID或System.currentTimeMillis()加随机数作为新文件名把原始文件名保存到数据库表里用于下载时还原展示。这个做法有三个实际理由第一处理好中文文件名乱码第二防止文件名里的非法字符或路径穿越第三避免同名覆盖。文件大小建议限制在50MB之内类型限制按竞赛要求走这些在Spring Boot里用spring.servlet.multipart.max-file-size即可配置。4.4 评审打分核心逻辑其实是“提交后不可再改”评委端的工作台页面是这样一个流程点击作品查看详情一般会有附件预览或下载链接→ 填写分数和评语 → 点击“提交”。后端在接收提交评审请求时最重要的校验逻辑是当前用户必须是该竞赛的评审之一该评审记录存在且status0如果status已经是1提示“该作品已提交评审不可修改”如果你是管理员要修改某个评审人的打分不能直接改race_review应该在成绩核定环节里做“重新开放评审”操作——把status改回0并且记录一次修改日志。这类“不能越级操作”的细节是后期评审老师比较爱看的安全意识。分数统计规则也要想好作品最终成绩多种方案可选比如“平均分”“去掉最高最低后的平均分”“加权平均分按评委权重”不要每种都做做一种并在答辩时说出为什么选它。我建议初版直接算平均分因为在竞赛场景里通常评委权重相同。4.5 成绩公布与Excel导出一个功能体现“完整闭环”成绩不公布竞赛管理系统就是不完整的。公布前管理员会有一个“复核”页面列出每个作品的平均分确认无误后点击公布此时批量把成绩写入race_score或直接修改相关状态学生端就能看到自己的分数了。导出Excel我推荐用EasyExcel代码量非常小ListScoreExportVO list scoreService.listScoreForExport(raceId); EasyExcel.write(response.getOutputStream(), ScoreExportVO.class) .sheet(成绩单) .doWrite(list);响应里记得设置Content-Disposition头为attachment并处理好文件名编码否则下载的Excel文件名会是乱码。这个环节你要额外准备一个“统计报表”页面图表展示每个学院的报名人数、各竞赛的参与热度用前端的图表库比如ECharts拉数据展示即可。这个页面属于视觉亮点做不复杂但非常拉分。5. 项目从跑通到演示我踩过的坑和填坑方案这一部分是我最想写给你的。这些坑不是我编的是历届学生项目中反复出现的高频问题每一个都值得你在开发时预留排查时间。5.1 坑一LocalDateTime序列化后在页面上变成一串数字Thymeleaf页面直接渲染时间或者通过JSON接口返回时间时LocalDateTime类型默认序列化结果是很长一串数字不是可读的日期格式。解决方法是统一配置Jackson的日期格式化在application.yml里加一行spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8如果用的是MyBatis Plus的自动填充功能create_time和update_time也要确认填充的元数据处理器使用的是LocalDateTime类型字段上加TableField(fill FieldFill.INSERT)等注解这样插入记录时时间自动补齐不需要手动set。5.2 坑二MyBatis Plus分页查不出来一页返回全部数据这是MP最常见的问题只添加了MyBatis Plus依赖但忘了配置分页插件。分页插件一个都不配Page对象的分页SQL根本不会生效。需要在配置类里加Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }注意DbType要按你的数据库版本设置MySQL基本都是MYSQL。5.3 坑三文件下载时中文文件名乱码下载附件的接口里Content-Disposition头的fileName如果用ISO-8859-1编码会乱。正确做法是String encodedFileName URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString()) .replaceAll(\\, %20); response.setHeader(Content-Disposition, attachment;filename*UTF-8 encodedFileName);这个写法做了URL编码同时兼容大部分主流浏览器的文件名解析。前端如果用Axios下载一定要配置responseType: blob否则你拿到手的是中文乱码的JSON。5.4 坑四事务没生效批量操作一半成功一半失败竞赛发布流程里可能要同时插入race_info和race_signup或许还有通知记录如果你把它们拆到多个Mapper方法里又没有事务就会出现竞赛信息进去了、通知没发出去这种脏数据。Spring事务生效有几个隐藏条件类要被Spring管理、方法用public修饰、调用的入口是外部同一个类内部调用、this.method()不会走代理。排查时依次检查这三点。还有一个情况要注意如果你用了try-catch把所有异常都吞掉了事务是不会回滚的因为异常没抛出去。正确写法是catch的同时记录日志或者直接抛运行时异常让事务管理器处理。5.5 坑五前端传参和后端字段对不上调试半天这个问题尤其在团队协作里高频爆发。如果你用Vue前端数据字段是驼峰命名而后端实体类字段是下划线映射数据库列时只要后端接收对象的命名规范保持一致就不会有问题。建议前端能直接沿用后端返回的字段名保持JavaScript和SQL风格一致。另外后端DTO和实体类不要混用Controller只接收DTO并手动转换这样即使数据库字段调整也不影响前端契约。6. 答辩与演示怎么让评委觉得这项目有含金量最后一个部分聊答辩。竞赛管理系统的代码量不算少但答辩时评委只会关注几件事你对自己的系统有多熟、核心设计有没有依据、出了问题能不能圆回来。6.1 用业务故事串起整个演示不要一个页面一个页面生硬地过功能那样评委很无聊。建议准备一条完整的故事线“管理员发布一个《程序设计竞赛》赛事 → 学生报名 → 管理员审核 → 学生提交代码作品 → 管理员分配三位评委 → 评委打分 → 管理员公布成绩 → 学生查看并下载证书”。每操作一步简单说一下背后的表名、关键状态变化整个过程5分钟讲完非常紧凑。故事线里顺便把你系统里的所有核心功能都过了一遍比菜单式演示好出不少。6.2 主动准备三个“亮点问答”你得想好老师会问你什么。我预判几个方向你提前想好答案“如果你的数据库表超过几十万条查询变慢怎么办”——答不用到那种规模但可以说用索引优化和MP分页查询保证响应时间将来可以升级Redis缓存热点竞赛数据。“多评委打分时最高分和最低分是不是应该去掉”——答当前用的是平均分后续版本可扩展去掉最高最低分的策略会有配置项控制。“学生上传的恶意文件怎么防护”——答做了文件类型白名单校验和大小限制大型文件用对象存储更好但毕设阶段本地存储足够。这些问题没有绝对标准答案关键是展现你理解系统的边界在哪里比张口就来的“应该没问题”靠谱得多。6.3 演示前要准备的数据和账号演示时最怕现场造数据尤其是评审环节临时创建三个评委再分配会很尬。所以准备一套完整的演示数据3个竞赛、8个学生报名、4个评委、至少6份作品、每份作品都有评分。提前把所有页面截图存一份到本地以防现场网络出问题。账号我建议准备三个固定角色账号用表格贴在项目文档首页角色账号用途管理员admin主演示账号评委reviewer1评审环节学生student1报名/提交/查分6.4 最后再送你一个增值项项目文档里放一段自己录的演示视频这个操作看起来很细微但效果极好。花半小时录一段3分钟的视频启动项目、登录、跑通刚才那条业务故事线。把视频链接放到README或者作品附件里。原因很实际一是毕设验收时可能项目环境起不来视频是完美兜底二是评委能直观感受到你确实完整跑通了整个项目而不是只写了文档。很多同学忽略了这一步其实性价比很高。如果你按这条路线走下来竞赛管理系统这个项目就不会停留在“又一个CRUD”的水平。它有一条完整业务链、有合理的表设计、有跨文件存储和统计导出的综合能力同时你的每一步都能解释出“为什么”。把这套逻辑吃透你答辩时只会自信——因为这确实是你亲手设计并一步步验证过的作品。