☰
SpringBoot+Vue校园竞赛管理系统:从环境配置到答辩演示全流程避坑指南
2026/10/10 7:29:32 网站建设 项目流程

简介:这是一份面向计算机相关专业学生及Java学习者的毕业设计实战资源,基于Spring Boot与MySQL构建校园竞赛管理系统。系统覆盖用户管理、竞赛发布、参赛报名、成绩管理等完整闭环,支持学校便捷发布竞赛信息、管理参赛学生,并实现成绩的自动化统计与分析;学生可随时查看竞赛、在线报名及查询个人成绩。压缩包以RAR格式打包,约27.36MB,内含项目源代码、SQL数据库脚本及毕业论文参考文档,源码结构清晰、数据库设计合理,易于扩展和维护。开发环境配置简便,项目运行测试通过,稳定性高。目前已有54人浏览学习,既适合作为毕业设计成果展示,也可作为课程设计或大作业的实战模板,便于在此基础上定制个性化功能。

1. 校园竞赛管理系统:披着“增删改查”外衣的毕业设计硬骨头

看到“基于SpringBoot+Vue开发校园竞赛管理系统附毕业论文+源代码+SQL”这个标题,你能猜到这是近三年毕业设计里最不缺货的选题之一:技术栈是标准答案,SpringBoot后端、Vue前端、MySQL存数据,再配一份论文和可导入的SQL脚本。但真把这个rar解压出来,照着跑一遍,一周之内大多数人会卡在三个地方:SpringBoot版本一高,javax悄悄变成jakarta,项目起不来;Vue环境装好了,路由和接口代理又跨域;SQL脚本往MySQL里导一半就开始报外键错误,论文却还等着截运行图。

这篇笔记就把这个项目从“拿到压缩包”到“能演示能答辩”的全过程拆开讲:先处理后端结构和数据访问,再搭Vue前端的环境与页面,然后处理SQL脚本和论文这两个最容易拖进度的交付物,最后集中排一批高发坑。内容全部来自我做同类项目的实际操作,参数和命令都可复现,你照着走完,剩下的事情就是把演示数据填满、把论文截图替换成你自己的运行页面。

2. SpringBoot后端:把“竞赛-报名-评审”拆成分层清晰的工程

2.1 项目结构:按业务线分包而不是按技术分层堆目录

拿到项目源码第一步,是先把包结构看明白。常见做法是两种组织方式:一种是controller/entity/mapper/service四大包平铺,适合只有两三个表的小项目;另一种是按业务域分包,每个包内自带controller、service、mapper。竞赛管理系统至少涉及用户、竞赛、报名、评审、成绩五类数据,我一般会采用按业务域分包的结构。

com.example.competition ├── CompetitionApplication.java ├── common │ ├── Result.java │ ├── PageResult.java │ └── GlobalExceptionHandler.java ├── user │ ├── UserController.java │ ├── UserService.java │ ├── UserMapper.java │ └── entity/User.java ├── competition │ ├── CompetitionController.java │ ├── CompetitionService.java │ ├── CompetitionMapper.java │ └── entity/Competition.java ├── registration │ ├── RegistrationController.java │ ├── RegistrationService.java │ ├── RegistrationMapper.java │ └── entity/Registration.java └── review ├── ReviewController.java ├── ReviewService.java ├── ReviewMapper.java └── entity/Review.java

这个结构的核心逻辑是:以后加一个“证书管理”模块,直接复制competition包改造,不影响其它业务域。而技术分层的大包结构在项目中期会出现一个实际问题:一个“查询竞赛详情并附带报名人数”的接口,同时要动Controller、Service、Mapper三层,文件分散在四五个包目录里来回跳。

分包的另外一层考虑是答辩时的代码讲解路线更好走。评委问“报名模块怎么实现”,你直接定位到registration包,从Controller往下讲;如果评委问“事务控制在哪里配置”,你会从service层往上讲。两种路线都清晰,这比在一个大Service里堆几百行方法体更容易说清楚。

2.2 数据访问选择:MyBatis-Plus在毕设项目里的三点优势

SpringBoot的数据访问方案有JPA、MyBatis、MyBatis-Plus三种主流选择。竞赛管理系统这种表关系清晰、以单表查询和简单关联为主的场景,MyBatis-Plus是目前最省力的选择,没有之一。

第一,lambda条件构造器能省掉大量XML。查询“状态为报名中且开始时间在今天的竞赛”,一行搞定:

LambdaQueryWrapper<Competition> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Competition::getStatus, 1) .ge(Competition::getStartTime, LocalDateTime.now()); List<Competition> list = competitionMapper.selectList(wrapper);

这里的selectList是MyBatis-Plus内置方法,不需要自己在XML里写结果集映射。lambda条件构造器相比手拼SQL字符串的好处是类型安全:字段名写成方法引用,编译期就能发现字段改名导致的错误,而不是运行到一半才报SQL异常。

第二,分页插件是竞赛列表的刚需。竞赛管理后台要展示报名人数、按状态筛选、排序翻页,这种组合查询用Page对象直接搞定:

Page<Competition> page = new Page<>(1, 10); LambdaQueryWrapper<Competition> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Competition::getStatus, 1).orderByDesc(Competition::getCreateTime); Page<Competition> result = competitionMapper.selectPage(page, wrapper);

返回值result.getRecords()是当前页数据,result.getTotal()是总记录数。分页插件需要在MybatisPlusInterceptor里注册PaginationInnerInterceptor,否则这个接口只会返回全量数据再在内存里截取,数据量一大性能就会掉。

第三,字段自动填充能统一处理createTime、updateTime这类每个表都有的字段。在实体字段上加注解:

@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;

再配一个MetaObjectHandler实现类,插入和更新时就自动带上时间。这个特性给论文里的“通用字段设计”一节提供了素材,答辩时也很好讲。

2.3 Controller-Service-Mapper三层的最小实现与参数约定

竞赛管理系统的核心接口可以控制在一组约定里。实体类、Controller、Service的基本写法如下,这是一套可以直接套用的模板。

// Competition.java @Data @TableName("competition") public class Competition { @TableId(type = IdType.AUTO) private Integer id; private String name; private String type; private Integer status; private LocalDateTime startTime; private LocalDateTime endTime; private String description; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }

@TableName对应表名,@TableId(type = IdType.AUTO)表示主键自增。如果实体字段与表字段命名不一致,需要加@TableField("实际列名"),否则MyBatis-Plus会按驼峰转下划线规则猜测,猜错了运行时才报字段不存在。

// CompetitionController.java @RestController @RequestMapping("/api/competition") public class CompetitionController { @Resource private CompetitionService competitionService; @PostMapping("/create") public Result<?> create(@RequestBody Competition competition) { competitionService.createCompetition(competition); return Result.success(); } @GetMapping("/page") public Result<?> page(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword) { return Result.success(competitionService.pageQuery(pageNum, pageSize, keyword)); } }

@RequestParam(defaultValue = "1")让分页参数不带也能访问,required = false让查询关键字可传可不传。统一返回体用Result.success()包装,Controller不直接返回实体,是为了前端可以统一从data字段取值,也方便统一处理业务异常。

Service层有两个点要注意。一是创建竞赛时要校验时间合法性:开始时间必须在结束时间之前,否则存进库里,后续报名、评审阶段的逻辑会跟着错乱。二是修改竞赛状态要走专门的方法,不要直接改status字段了事,比如报名中状态不能随便跳到已结束,要校验当前时间与赛程的关系。

2.4 状态字段与建表SQL:别在if里写魔法数字,也别手写一大堆DDL

竞赛状态是整个系统里最容易出乱子的地方。常见错误是把1、2、3散落在各个if判断里,过两周再看代码完全不知道1代表什么。我一般会在common包里放一个常量类:

public class CompetitionStatus { public static final int DRAFT = 0; public static final int REGISTERING = 1; public static final int REVIEWING = 2; public static final int FINISHED = 3; }

使用状态时直接引用常量,条件分支可读性高很多。另外状态与关联操作要做约束:只能从DRAFT改为REGISTERING、只能从REGISTERING改为REVIEWING。这个规则在Service方法里校验,面试时可以顺势引出状态机的概念。

关于生成建表SQL,MyBatis-Plus并没有官方提供“根据实体类生成CREATE TABLE”的功能,但有一个偏方是用TableInfoHelper读取实体字段信息拼出DDL。实际上我并不会在项目里跑这段代码,而是用它来核对字段差异:

TableInfo tableInfo = TableInfoHelper.getTableInfo(Competition.class); System.out.println(tableInfo.getTableName()); tableInfo.getFieldList().forEach(field -> System.out.println(field.getColumn() + " " + field.getPropertyType().getSimpleName()));

这个思路的意义在于:你先写实体,再生成SQL,实体和表结构天然对齐,不会出现“Java字段叫competitionType,SQL列名叫type”这种低级的字段错位。真正落地时还是建议手写或让Navicat生成建表语句,但要保证实体类、表名、字段名三者一致。

3. Vue前端:从环境配置到可演示页面的完整链路

3.1 环境配置的版本线:Node、npm源、脚手架、端口

前端环境配置是新手最容易翻车的环节,而且经常是同一个项目在自己机器上能跑,换一台机器就各种报错。这里有一条我验证过的版本线:Node.js 16或18、npm源切换为国内镜像、用vue-cli或者Vite创建项目。

# 检查版本,Node 16/18都可用,Node 20+部分老项目会报错 node -v npm -v # 切换npm源为国内镜像,解决依赖下载慢的问题 npm config set registry https://registry.npmmirror.com # 创建项目,vue 3版本用vue-cli或vite都行 vue create competition-web # 或者 npm create vite@latest competition-web -- --template vue # 安装依赖并启动开发服务器 cd competition-web npm install npm run serve

vue create走的是webpack,构建慢但稳定;vite启动快但个别老依赖插件不兼容。做毕设项目我倾向用vue-cli,因为网上能查到的资料和老师熟悉度都更高。启动后默认端口8080,如果被占用,可以在vue.config.js里改port为8081。

IDE方面,IDEA社区版配Vue插件可以直接打开前端工程,但不建议在一个IDEA窗口里同时开后端和前端两个项目,容易内存吃紧。我是用IDEA开后端,用VS Code开前端,两边独立,改代码互不干扰。

环境配置里最容易被忽略的是npm install之后node_modules体积很大,压缩发给别人时一定要删掉这个目录,让对方自己执行npm install。这一点在你最后打压缩包交付时会很重要。

3.2 路由与角色:竞赛页面的访问控制怎么设计

竞赛管理系统有三类角色:管理员、评委、学生。前端路由要做两件事:一是把页面URL和组件映射起来,二是控制不同角色能访问的范围。新版Vue Router的写法如下:

// router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', component: () => import('@/views/Home.vue') }, { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/admin', component: () => import('@/layouts/AdminLayout.vue'), meta: { roles: ['admin'] }, children: [ { path: 'competitions', component: () => import('@/views/admin/CompetitionList.vue') }, { path: 'reviews', component: () => import('@/views/admin/ReviewList.vue') } ] }, { path: '/student', component: () => import('@/layouts/StudentLayout.vue'), meta: { roles: ['student'] }, children: [ { path: 'register', component: () => import('@/views/student/RegisterCompetition.vue') } ] } ] router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { next('/login') } else { next() } })

路由层做访问控制的力度有限,真正拦截要放在后端接口上,前端路由守卫只是体验层的控制。meta.roles数组是给菜单渲染用的,页面加载时从后端拉当前用户角色,再决定渲染哪些菜单项。常见的做法是侧边栏菜单用router.options.routes过滤生成,角色不匹配的路由不显示,而不是把隐藏菜单的静态代码写在每个页面里。

Vue Router 4对应Vue 3,路由参数传递用useRoute和useRouter组合API:

import { useRoute } from 'vue-router' const route = useRoute() const competitionId = route.query.id || route.params.id

这里route.query.id对应URL里的?id=1,route.params.id对应路径参数/detail/:id。在竞赛详情页跳转时我一般用命名路由传参,比手工拼URL更不容易出错。

3.3 axios封装与跨域代理:vue.config.js里的两个关键字段

前端页面要调后端接口,最大的障碍是跨域:前端跑在8080端口,后端跑在8080端口(或8081),浏览器默认会拦截非同源的AJAX请求。解决方案不是在后端写@CrossOrigin到处放飞,而是在vue.config.js里配置代理,让浏览器认为请求是同源的。

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

target指向后端服务地址,changeOrigin: true让代理服务器伪装成后端同源。这样前端请求/api/competition/page时会被代理转发到http://localhost:8080/api/competition/page,开发阶段不需要后端额外处理跨域。有一个坑是:如果后端context-path配置了/api前缀,代理转发时会导致路径叠加变成/api/api,这时需要加上pathRewrite: { '^/api': '' }。

axios建议在utils目录下封装一个request实例,统一处理baseURL、token注入和错误提示:

// utils/request.js import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => response.data, error => { ElMessage.error(error.response?.data?.message || '网络异常') return Promise.reject(error) } ) export default request

baseURL: '/api'让每个接口调用只需要写业务路径,比如request.get('/competition/page')。拦截器自动加token可以避免每个接口重复写请求头。响应拦截器里response.data直接返回后端统一包装后的数据体,所有页面拿到的就是{ code, message, data }结构,不需要每处都解一层套一层。

3.4 列表页的插槽与指令:复用竞赛卡片,少写200行重复代码

竞赛管理系统的前端页面高度重复:管理员看竞赛列表、学生看可报名列表、评委看待评审列表,卡片的样式和数据字段几乎一样,只是操作按钮不同。这里用Vue插槽做共用组件最合适。

<!-- components/CompetitionCard.vue --> <template> <div class="competition-card"> <h3>{{ competition.name }}</h3> <p>类型:{{ competition.type }}</p> <p>开始时间:{{ competition.startTime }}</p> <slot name="actions" :item="competition"> <el-button type="primary" @click="emit('view', competition)">查看详情</el-button> </slot> </div> </template> <script setup> const props = defineProps({ competition: { type: Object, required: true } }) const emit = defineEmits(['view']) </script>

使用方在调用时覆盖默认按钮:

<CompetitionCard v-for="item in list" :key="item.id" :competition="item"> <template #actions="{ item }"> <el-button v-if="role === 'student'" type="success" @click="register(item)">立即报名</el-button> <el-button v-else-if="role === 'reviewer'" type="warning" @click="review(item)">开始评审</el-button> </template> </CompetitionCard>

插槽的关键点是<slot name="actions" :item="competition">向外传递数据,父组件用#actions="{ item }"接收,这样子类组件不用感知父页面的业务逻辑。同样的卡片组件能同时服务三个页面,改样式只改一处。

v-for循环里必须加:key="item.id",这不仅是避免Vue告警,更实际的价值是列表项变更时Vue能精准复用DOM,避免表格操作后列表错位的诡异Bug。Vue的路由、指令简写也要注意:v-model:name等价于v-model加:name和@update:name的组合,在表单组件里用这种简写能少写大量事件绑定代码。

4. SQL脚本与毕业论文:两个比写代码更拖进度的交付物

4.1 SQL脚本的四个组成部分与导入顺序

rar里的SQL脚本是整篇论文的数据地基。一份合格的交付SQL脚本应该按顺序包含四段内容:建库建表、初始数据、索引与约束、测试数据。数据库建议统一用MySQL 5.7或8.0,字符集utf8mb4,排序规则utf8mb4_general_ci。

-- 1. 建库 CREATE DATABASE IF NOT EXISTS competition_system DEFAULT CHARACTER SET utf8mb4; USE competition_system; -- 2. 建表(先建主表,再建从表) CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role TINYINT NOT NULL DEFAULT 2 COMMENT '0-管理员 1-评委 2-学生', real_name VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE competition ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, type VARCHAR(50), status TINYINT DEFAULT 0 COMMENT '0-草稿 1-报名中 2-评审中 3-已结束', start_time DATETIME, end_time DATETIME, description TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 3. 外键与索引(第三段再建外键,避免建表顺序导致报错) ALTER TABLE competition ADD INDEX idx_status (status); -- 4. 初始数据 INSERT INTO user (username, password, role, real_name) VALUES ('admin', '$2a$10$...', 0, '系统管理员');

建表顺序的设计逻辑,是为了让外键约束在第三段统一添加。如果建表时就带上FOREIGN KEY子句,而引用的父表还没创建,MySQL会直接报Cannot add foreign key constraint。初始数据里密码不建议明文存放,用BCrypt加密后的字符串,这也是论文安全设计章节的一个亮点。

注意:预置账号不要用admin/123456这类常见弱口令,登录开发阶段可以临时用,答辩前一定要换成BCrypt密文。登录接口的参数不要用字符串拼接SQL,改用#{}占位符或PreparedStatement参数化查询,防止SQL注入。评委提问“万能密码怎么防”时,这两点就是答案。

4.2 mysql执行SQL脚本的三种方式及报错对照

拿到SQL脚本后,执行方式有三种,从最推荐到最不推荐:

# 方式一:命令行直接导入(最推荐) mysql -u root -p competition_system < competition.sql # 方式二:进入mysql后source执行(交互式) mysql -u root -p source /path/to/competition.sql; # 方式三:可视化工具,如Navicat # 连接后选择数据库,右键运行SQL文件

命令行导入的优点是报错信息带行号,比如ERROR 1064 (42000) at line 35,你能直接定位到第35行。用source方式一不小心会把“用root登录后没选择数据库”的状态带进来,导致表建到了错误库。

常见的SQL导入报错有四个:一是Unknown database,说明还没建库,脚本里缺少CREATE DATABASE语句或你导入时指定了一个不存在的库名;二是Cannot add foreign key constraint,要么是外键引用的表还没建好,要么是两个关联表的字段类型不一致(比如一边是INT一边是BIGINT);三是Data too long for column,说明某条数据超长,通常是description字段用了VARCHAR(50)放长文本;四是Duplicate entry,原因是重复执行脚本,插入语句没有使用INSERT IGNORE或ON DUPLICATE KEY UPDATE。

执行成功后验证分两步:先SHOW TABLES;确认表数量,再SELECT COUNT(*) FROM competition;确认数据量。别急着写论文里的运行截图,先跑通查询再截图。

4.3 毕业论文的章节骨架与图表组织

论文是很多人的噩梦,其实结构走典型的信息管理系统套路就不会有大问题。章节安排可以参考下面这个骨架,其中截图和配图的节奏是答辩时评委翻页关注的焦点。

章节内容要点配图建议
绪论背景、国内外现状、研究内容不需要图
相关技术SpringBoot、Vue、MySQL简述技术架构图
需求分析用例图、角色权限、功能清单用例图
系统设计总体架构、功能模块、ER图、数据库表设计架构图、ER图、表结构截图
系统实现按模块贴核心代码+运行效果图页面截图、关键代码片段
系统测试测试用例表、测试结果功能测试表格

数据库表设计一节里,把本地的表结构截图出来,重点标注主键和外键关系。功能实现一节,不要贴大段代码,挑每个模块的关键方法贴5到10行,配一张运行页面截图。

论文和代码必须严格对应,最怕的是论文截图里的页面样式和最终代码完全不同,评委一眼看穿,这是毕业设计答辩翻车的常见理由。写论文之前先把系统跑起来,截完图再写文字;改代码之后,对应章节的截图也一定要重新截。

5. 校园竞赛管理系统避坑:版本、数据、部署三个维度的踩坑记录

5.1 SpringBoot版本太高:javax到jakarta的迁移

现象:按课程PPT或者老教程创建SpringBoot项目,启动时报java.lang.ClassNotFoundException: javax.servlet.FilterRegistration,或者@Autowired注入的类能编译但启动时提示找不到Bean。

原因:SpringBoot 3.0开始,底层的Java EE API从javax包迁移到了jakarta包,Servlet、Validation、注解全部换了命名空间。你用的是3.x版本,但代码里还在写javax.servlet、javax.validation,自然会找不到类。

解决:两个方案任选。方案一是锁SpringBoot版本为2.7.x,在pom.xml里显式指定版本:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent>

方案二是坚持用3.x,但MyBatis-Plus的依赖也要换成适配SpringBoot 3的:

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.7</version> </dependency>

同时把所有import javax.*改成import jakarta.*,这个改动很小,但很多人总在jar包对不上时卡住。我的建议是毕设统一用SpringBoot 2.7.18,网上资料最丰富,课设代码和论文模板基本都基于这个版本线。

5.2 Maven依赖下载慢与版本不匹配

现象:新建项目后pom.xml一直在报红,进度条卡在Downloading,几十分钟过去还在转圈。还有一种是依赖下载了但启动报NoSuchMethodError。

原因:下载慢是Maven中央仓库访问不稳定;NoSuchMethodError是依赖版本冲突,比如spring-boot-starter-web传递的Jackson版本与mybatis-plus依赖的旧版Jackson冲突。

解决:在~/.m2/settings.xml里配置阿里云镜像:

<mirrors> <mirror> <id>aliyun</id> <mirrorOf>central</mirrorOf> <url>https://maven.aliyun.com/repository/public</url> </mirror> </mirrors>

版本冲突用Maven Helper插件排查,在IDEA的pom.xml页面打开Dependency Analyzer,搜出有哪些冲突的jar,在pom.xml里用<exclusion>排除不需要的传递依赖。这类问题不致命但极其烧时间,建议新建项目时统一从一个能跑的pom.xml复制,别从头敲依赖。

5.3 SQL脚本执行失败:外键顺序、字符集、关键字

现象:按照第4章的导入方式执行SQL,报Cannot add foreign key constraint;或者中文字段显示乱码。

原因:外键失败通常有两个来源:一是建表顺序问题,子表先于父表创建;二是字段类型不一致,MySQL要求外键字段与引用字段的类型和长度完全一致,INT对BIGINT、VARCHAR(50)对VARCHAR(100)都会报同一个错误。乱码问题则是脚本文件编码与数据库连接字符集不一致,Windows的GBK脚本导入utf8mb4库就直接乱码。

解决:把外键语句从建表子句里抽出来,统一放到脚本末尾段执行;字段类型统一用INT且UNSIGNED保持一致。脚本文件的编码用记事本另存为或IDEA右下角统一改成UTF-8。执行前在脚本开头加一句:

SET NAMES utf8mb4;

如果是SQL脚本里的表名或字段名碰上了MySQL保留字,比如order、group,报错会是You have an error in your SQL syntax,解决办法是给这些名称统一加反引号,或者直接改名成order_info、group_info。

5.4 慢SQL:竞赛统计报表为什么越查越慢

现象:管理员页面统计“各竞赛报名人数排行”,数据量几百条时秒开,数据量到几千条后页面卡顿好几秒,接口响应时间从200ms飙升到5s。

原因:经典错误是把报名人数统计放在循环里查:先查竞赛列表,然后在循环内逐条执行SELECT COUNT(*) FROM registration WHERE competition_id = ?。几百条时这种方式还能扛,几千条时循环内查询持续累加延迟,产生N+1问题。

解决:改成一次JOIN聚合查询:

SELECT c.id, c.name, COUNT(r.id) AS register_count FROM competition c LEFT JOIN registration r ON r.competition_id = c.id WHERE c.status IN (1, 2) GROUP BY c.id, c.name ORDER BY register_count DESC;

一次查询返回全部竞赛和报名人数的排行。如果竞赛表数据继续扩大,再给registration.competition_id加索引。另外用MyBatis-Plus的selectCount时,只对单表可以放Service层,只要涉及多表统计就要写SQL到Mapper里,不要迷信ORM能解决一切。

5.5 把整个项目发给别人:环境说明比压缩包更缺不得

现象:把项目打包发给同学或评审老师,对方跑不起来,日志不是报数据库连接失败就是前端页面白屏。你这边跑得好好的,一换机器全废。

原因:压缩包里缺少环境说明,对方不知道用的什么版本JDK、什么版本MySQL、怎么建库、前端怎么启动。

解决:在项目根目录放一个README.md,内容至少包含JDK版本、Maven版本、MySQL版本、Node版本、后端启动命令、前端启动命令、SQL脚本导入路径。这是整套交付物里最容易被忽略的一部分。另外前端node_modules目录一定要删除再压缩,否则压缩包体积能到几百MB,发给别人解压时也容易把一些本机编译过的二进制文件带过去。

git的源代码管理也建议从一开始就做起来,在项目根目录执行git init,每个功能模块做完就提交一次,版本回退有后悔药,论文里也有“项目采用Git进行版本管理”的素材。

6. 答辩演示前的最后一步:预置数据与运行时兜底

项目开发完到答辩之间,有一个最容易在关键时刻翻车的环节:现场演示。你准备的裁判员账号密码记错了,或者演示时发现某个列表是空的,整个讲解节奏就断了。我的习惯是提前在SQL脚本里写好一套演示数据,并在答辩前完整跑三遍导入流程。

INSERT INTO user (username, password, role, real_name) VALUES ('teacher01', '$2a$10$CmZS...', 1, '张老师'), ('student01', '$2a$10$CmZS...', 2, '李明'), ('student02', '$2a$10$CmZS...', 2, '王芳'); INSERT INTO competition (name, type, status, start_time, end_time, description) VALUES ('2025年校级程序设计竞赛', '程序设计', 1, '2025-05-10 09:00:00', '2025-05-10 17:00:00', '考察算法与数据结构'), ('2025年校园创新创业大赛', '创新创业', 2, '2025-04-20 09:00:00', '2025-04-22 17:00:00', '面向全校学生');

这套数据的三个角色是演示路径的骨架:用管理员看竞赛管理、审批报名;用评委进评审页面打分;用学生登录报名可报的竞赛。每条路径在答辩前至少完整操作一遍,确认页面按钮都和预设数据对得上。演示密钥是:电脑提前关掉系统自动更新,浏览器提前登录好评委和学生的账号放到不同标签页,演示时直接切标签页切换身份,比现场输入密码快得多,也让评委觉得你考虑得很周全。

还有一招是后端启动失败时的兜底:在IDEA里把SpringBoot的运行配置和前端npm run serve命令都保存下来,答辩现场即使换了一台电脑,照着记录敲命令也能快速恢复。别依赖自己临场回忆命令,这些细节在紧张时最容易掉链子。我上次答辩前就吃过亏——预置数据里密码字段拷错了一段哈希值,现场改库花了三分钟。从此之后,任何预置脚本我都会重跑一遍并且亲自用三个账号各走一遍流程再提交。希望这篇笔记能帮你绕开这些坑,让这个毕设项目从rar包到答辩现场都顺顺利利。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询