毕业设计年年换,但“体育馆预定系统”真是这个领域里的长青树。用SpringBoot做后端、Vue做前端,前后端分离,再配一套完整的预定流程,基本就把一个Web项目的核心能力都覆盖了:用户认证、数据建模、接口设计、联动交互、权限控制。这篇文章不打算做成那种照着敲就完事的教程,而是想站在“这个题目到底怎么拿高分”的角度,把它拆开揉碎讲清楚——从系统怎么设计、核心模块怎么实现,到论文怎么写、PPT怎么讲、搭建视频怎么录,一次聊明白。适合正在做这个题目的同学,也适合想快速落地一个前后端分离项目的开发者。
1. 项目整体设计与技术选型
1.1 为什么选SpringBoot+Vue这套组合
很多同学一开始纠结技术栈:用SSH?用JSP?用纯Servlet?还是用SpringBoot+Vue?我的建议非常直接,选SpringBoot+Vue,尤其当你要拿它去交毕业设计或者求职作品集的时候。
理由有三。
第一,开发效率高。SpringBoot把大量繁琐的XML配置干掉了,一个@SpringBootApplication注解就能把项目跑起来;Vue的组件化开发让前端页面也能拆成一块块独立模块,改样式、调逻辑不会影响到其他页面。对一个人要搞定整个系统的情况来说,这个效率差距是决定性的。
第二,这个组合是目前企业里非常主流的技术形态。国内大量中小型项目都是SpringBoot做后端接口、Vue做管理后台或者用户端页面。你做完这个项目,写进简历里的技术点是能直接对上招聘要求的,面试官看到这套组合就知道你具备基本的全栈开发能力。
第三,社区资料极其丰富。遇到问题搜索一下,几乎都能找到解决方案。这一点在赶进度的时候特别重要,你不太可能卡在一个冷门报错上好几天。相比之下,SSH、JSP这类老技术虽然还在部分老系统里用,但新项目基本不碰了。
当然,选型也要客观看待。SpringBoot+Vue的前后端分离模式,会引入跨域、接口联调、权限控制等问题,这些恰恰是毕设答辩时老师最爱问的点。你如果能把这些问题处理明白,反而成了加分项。所以,别怕麻烦,把这些坑提前踩一遍,后面就顺畅了。
1.2 功能模块整体规划
体育馆预定系统,核心就两件事:把场地展示给用户,把预定流程跑通。但作为一套要写进论文的完整系统,只有这两件事远远不够。我习惯先用一张功能清单把系统边界划清楚,避免开发到一半才想起缺模块。
用户端功能:
- 注册登录:邮箱/手机号注册,登录态用JWT维护。
- 场地浏览:按类型筛选,比如篮球场、羽毛球场、乒乓球室,展示场地图片、位置、收费标准。
- 在线预定:选择场地、选择日期和时段、提交订单。
- 订单管理:查看自己的历史订单、当前进行中的预订,支持取消预订。
- 个人中心:修改个人信息、查看公告通知。
管理端功能:
- 场地管理:维护场地的基础信息,上架/下架场地。
- 订单管理:查看所有订单,处理取消申请或者标记违约。
- 用户管理:查看注册用户列表,禁用异常账号。
- 统计报表:统计每天/每月的订单量、营收、热门场地排名。
- 公告管理:发布系统公告,用户端看到公告列表。
这个清单不是拍脑袋定的,它对应着一条完整的业务闭环。用户能完成“浏览-预订-查看-取消”的完整链路,管理员能完成“维护-处理-统计-发布”的管理链路。两头都有事可做,界面有交互,数据有流转,论文里写“实现了一个功能完整的预定系统”才不会虚。
1.3 数据库设计与核心表结构
功能清单定下来后,数据库设计就顺理成章了。我会预估一下:用户表、场地表、场地类型表、订单表、公告表,再加一个可选的时段表。
先看用户表,字段大概是:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| username | varchar(50) | 登录名,唯一 |
| password | varchar(255) | BCrypt加密后的密码 |
| nickname | varchar(50) | 昵称 |
| phone | varchar(20) | 手机号 |
| role | tinyint | 角色,0用户 1管理员 |
| status | tinyint | 状态,0正常 1禁用 |
| create_time | datetime | 注册时间 |
场地表是系统的核心资产:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| name | varchar(100) | 场地名称 |
| type_id | bigint | 关联场地类型表 |
| description | text | 场地描述 |
| price | decimal(10,2) | 每小时价格 |
| image | varchar(255) | 图片地址 |
| status | tinyint | 0可预订 1停用 |
订单表的字段设计中,有几个地方要特别想清楚。一是订单号,建议用order_no字段单独存一个业务编号,比如202506021030001这种格式,不要直接用自增id当订单号,方便扩展和展示。二是status字段,我习惯用整型枚举,0待支付、1已支付/预订成功、2已取消、3已完成、4违约,这样后续做状态流转时逻辑清晰。三是时段存储,建议直接用start_time和end_time两个datetime字段,而不是存一个字符串“10:00-12:00”,这样在查冲突时能直接用数据库范围比较,非常方便。
公告表和场地类型表相对简单,这里不展开。
设计数据库时有一条我一直强调的原则:宁可字段多一点,也别后续频繁加字段。尤其毕设周期短,一旦表结构设计不合理,写代码时就会发现接口怎么调都不顺手,回头改表再改代码,非常浪费时间。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心功能拆解与实现要点
2.1 用户从登录到预定的完整链路
很多人觉得登录不就是查一下用户名密码吗?实际上,把登录到预定这条链路完整走通,涉及SpringBoot拦截器、JWT、Vue路由守卫、Axios请求封装多个环节。
登录流程我一般这样设计:
- 用户输入账号密码,前端用POST请求发送到
/api/user/login。 - 后端校验用户名和密码,密码用
BCryptPasswordEncoder做校验。 - 校验通过后,生成JWT令牌,返回给前端,同时返回用户基本信息。
- 前端把JWT存储到
localStorage或者sessionStorage。 - Axios请求拦截器统一给请求头加上
Authorization: Bearer <token>。 - Vue路由守卫检查是否有token,没有就跳到登录页。
这里有一个容易被忽视的点:JWT的expiration过期时间要合理设置。太短了,比如15分钟,用户填个表单就登录过期;太长了,比如7天,安全上又有风险。毕设系统我一般设24小时,够用又不至于频繁登录。
预定链路的实现,前端流程是:用户在场地列表页选好类型,进入场地详情页,选择日期和时段,点击“提交预订”,确认订单信息,点击“支付”模拟支付成功。后端提供三个接口:查询可用时段、创建订单、模拟支付。这三个接口对应着不同的业务逻辑,尤其是“查询可用时段”和“创建订单”,需要和冲突检测结合起来。
2.2 预定冲突检测与状态机设计
预定系统最核心的难点,就是怎么保证同一个场地在同一个时间段不会被两个人同时预定。这里有两个层面的方案。
第一层,数据库层面做唯一约束。假如场地和时段是分开存储的,可以在order表里把site_id + start_time + end_time + status组合起来,对“已支付/待支付”状态做唯一索引。如果两条记录同时插入,数据库会直接拒绝,从源头防止冲突。
但这里有一个小陷阱:数据库的唯一索引在并发插入时,如果两个事务同时查“有没有冲突”,都发现没有,然后同时插入,可能有一个会报错。报错本身不是坏事,至少没产生脏数据,但体验不好。所以更稳的做法是第二层,加乐观锁或者分布式锁。毕设项目里用乐观锁最简单。
我分享一个实际用过的方案,后端创建订单时,先查询该时段是否已被占用:
java复制// 伪代码,体现核心思路
public synchronized Result createOrder(CreateOrderDTO dto) {
// 1. 检查场地是否存在、是否可预订
Site site = siteMapper.selectById(dto.getSiteId());
if (site == null || site.getStatus() != 0) {
return Result.error("场地不存在或已停用");
}
// 2. 检查时间段冲突
int count = orderMapper.selectCountByTimeRange(
dto.getSiteId(), dto.getStartTime(), dto.getEndTime(),
List.of(0, 1) // 待支付、已支付的订单,都不允许冲突
);
if (count > 0) {
return Result.error("该时间段已被预订,请选择其他时间");
}
// 3. 创建订单
Order order = new Order();
order.setOrderNo(generateOrderNo());
order.setSiteId(dto.getSiteId());
order.setUserId(currentUserId());
order.setStartTime(dto.getStartTime());
order.setEndTime(dto.getEndTime());
order.setStatus(0); // 待支付
orderMapper.insert(order);
return Result.success(order);
}
synchronized只是最简单的一种方式,更好的做法是给对应的siteId加分布式锁,或者用数据库SELECT ... FOR UPDATE锁行。毕设答辩时如果能说清楚“我这里用数据库唯一索引兜底,同时用乐观锁/同步来保证并发场景下不出现超卖”,老师基本就满意了。
订单状态机也需要提前设计好。我常用的状态流转:
- 待支付:用户创建订单后,15分钟内未支付自动取消,或者用户主动取消。
- 已支付:支付成功后进入此状态,代表预定生效。
- 已取消:用户取消或者管理员取消。
- 已完成:预定时间段结束后,系统自动或管理员手动置为已完成。
- 违约:用户已支付但未到场,管理员标记违约。
在代码里,所有状态变更最好走一个统一的方法,不要散落在各个业务代码中直接改status字段,否则后期排查问题时会疯掉。
2.3 管理端处理订单与场地排期
管理端页面做起来比用户端要“枯燥”得多,但反而更能体现系统设计的严谨性。
场地管理方面,管理员操作的是一张场地列表,包括新增、编辑、上下架。新增场地时,需要上传图片。这里有两种做法:一种是把图片转成Base64存数据库,简单但不专业;另一种是服务器本地存储一个上传目录,把图片文件存进去,数据库只存访问URL,我更推荐后者,也更贴近真实的项目。
订单管理方面,管理端要能按状态筛选订单列表,对待支付订单可以进行关闭,对已支付订单可以进行取消并标记退款。这里要注意一个问题:取消订单后,对应的时间段要重新变成可预订状态。如果你在创建订单时没有做任何锁定,只是查询时排除冲突订单,那么取消订单后只需把状态改成已取消,查询可用时段时自然就把它排除了,非常干净。
场地排期这块,很多同学会想做成一个日历视图,其实对毕设来说不必太花哨。我建议做成一个简单的表格,横轴是日期加时段,纵轴是场地,格子用颜色标出“空闲/已订/停用”。这样实现成本低,但展示效果和专业感都有。
2.4 统计报表的简单实现
统计报表是很多同学容易忽略的模块,但它是我强烈建议加进去的。原因很简单:论文里需要有成果展示图,答辩PPT里也需要数据可视化,统计模块能带给你现成的图表素材,还能体现你“具有一定的数据分析意识”。
统计的内容可以包括:
- 最近7天/30天的订单趋势。
- 不同类型的场地预定量占比。
- 热门场地排行。
- 每天的营收总额。
后端只需要三个聚合SQL就能搞定。比如统计每天订单量:
sql复制SELECT DATE(create_time) AS day, COUNT(*) AS order_count
FROM `order`
WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
GROUP BY DATE(create_time)
ORDER BY day;
前端用ECharts画折线图、饼图、柱状图。注意ECharts的包体积不小,按需引入就好,别一次性全部引进来。
3. 从零搭建的实操过程与关键配置
3.1 后端SpringBoot环境准备与项目初始化
先说版本选择。SpringBoot的版本真不能随便选,选太高或太低都会踩坑。2024到2025年这个时间点,我推荐SpringBoot 2.7.x,原因有两个:第一,2.7.x对JDK8的支持非常成熟,而很多学校的教学环境还是JDK8;第二,2.7.x的生态兼容性极好,各种第三方starter基本都支持,不会出现SpringBoot3.x那种javax迁移到jakarta的兼容问题。当然如果你熟悉SpringBoot3,用3.x也完全可以,只要把对应的starters和JDK版本配好就行。
初始化项目最快的方式是直接到Spring Initializr的网页生成,也可以用IDEA的Spring Initializr向导。这里提醒一下,在网页生成时,依赖勾选这几项:
- Spring Web
- MyBatis Framework(或MyBatis-Plus)
- MySQL Driver
- Lombok
- Validation
如果没有MyBatis-Plus,建议单独引入。MyBatis-Plus的代码生成器和LambdaQueryWrapper能省掉大量手写SQL的时间,特别适合毕设这种需要快速出结果的场景。
pom文件里,核心依赖就像这样:
xml复制<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
配置文件的重点有这几个:数据库连接、MyBatis-Plus的日志、JWT的密钥和过期时间。数据库连接记得加上useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,否则中文乱码和时区问题会让你怀疑人生。
3.2 前端Vue项目搭建与路由配置
前端我推荐用Vue 2 + Element UI,或者Vue 3 + Element Plus。如果你之前只学过Vue 2,那直接用Vue 2 + Element UI更顺手,很多模板和教程都是这个组合;如果你愿意学新东西,Vue 3 + Element Plus + Vite的体验其实更好,启动速度更快,组合式API也更适合复杂组件开发。
用Vite创建Vue 3项目的命令:
bash复制npm create vite@latest gym-front -- --template vue
cd gym-front
npm install
npm install vue-router@4 axios element-plus @element-plus/icons-vue
路由配置里,我建议用路由懒加载,这样首屏加载会明显更快:
javascript复制const routes = [
{
path: '/',
component: () => import('@/views/Home.vue'),
meta: { title: '首页' }
},
{
path: '/login',
component: () => import('@/views/Login.vue'),
meta: { title: '登录' }
},
{
path: '/sites',
component: () => import('@/views/SiteList.vue'),
meta: { title: '场地列表' }
},
{
path: '/orders',
component: () => import('@/views/OrderList.vue'),
meta: { title: '我的订单' }
}
];
路由守卫配合登录态一起用,我之前提到过,在router.beforeEach里判断:
javascript复制router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token');
if (to.meta.requiresAuth && !token) {
next('/login');
} else {
next();
}
});
Element UI的按需引入也要注意,全量引入虽然省事,但打包体积很大,答辩演示时不明显,但被问到工程化优化就说不过去了。我用的是按需引入,配合unplugin-auto-import和unplugin-vue-components,日常开发效率反而更高。
3.3 前后端联调与跨域处理
前后端分离项目中,跨域问题几乎一定会遇到。你在Vue项目里请求localhost:8080/api/site/list,浏览器基于同源策略,直接报CORS错误。
解决方案有两种。
第一种,后端开启跨域,写一个配置类:
java复制@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);
}
}
第二种,前端配置代理。以Vite为例,在vite.config.js中配置:
javascript复制export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
});
我更推荐第二种方案,因为代理方式在生产环境部署时更灵活,而且不需要在代码里写死http://localhost:8080这种绝对地址,统一用/api开头,后续不管后端部署到哪里,前端都不用改代码。
3.4 项目目录结构规范
一个清晰的目录结构,不仅自己开发时舒服,论文里画系统架构图也有素材。后端我是这样组织的:
code复制src/main/java/com/example/gym
├── controller // 接口层
├── service // 业务层
├── mapper // 数据访问层
├── entity // 实体类
├── dto // 入参对象
├── vo // 返回对象
├── config // 配置类(跨域、拦截器、MyBatis-Plus配置)
├── interceptor // 拦截器(JWT校验)
├── common // 通用类(统一返回结果、异常处理、常量)
└── utils // 工具类(JwtUtil、DateUtil)
前端目录结构:
code复制src
├── api // 接口请求封装
├── router // 路由配置
├── store // Pinia或Vuex状态管理
├── views // 页面组件
├── components // 公共组件
├── utils // 请求工具封装
└── assets // 静态资源
统一返回结果的封装也很重要。我习惯用Result类,里面包含code、message、data三个字段,配合全局异常处理器,前端所有请求都按统一格式解析,代码写起来非常整齐。
4. 常见问题排查与避坑指南
这个项目的坑主要集中在环境和细节上,我实测踩过,把真实经历整理一下。
4.1 SpringBoot版本太高导致的依赖冲突
有次我用SpringBoot 3.2.x,引入旧版MyBatis-Plus,启动直接报错。原因是Spring Boot 3.x把javax.*包迁移到了jakarta.*,老版本MyBatis-Plus不兼容。解决方案要么升级MyBatis-Plus到3.5.5以上,要么换回SpringBoot 2.7.x。毕设图省心的话,直接2.7.x,兼容性最稳。
4.2 时区问题导致时间错乱
如果你在数据库连接串里忘了写serverTimezone=Asia/Shanghai,存进去的时间和查出来的时间可能相差8小时。前端再显示一下,用户看着预定的时间全错位了。这个问题的坑在于不报错,你在数据库里看时间是对的,但Java程序里取出来就多8小时。排查方法是一旦发现时间不对,第一时间检查连接串和MySQL时区设置。
4.3 并发预定同一场地
模拟真实场景时,用两个浏览器同时提交同一个场地的同一个时段,结果两个订单都创建成功了,这就是冲突检测没做好。后来我在数据库加组合唯一索引,同时把订单创建方法加上同步处理,问题才真正解决。答辩时老师很可能会问“多人同时抢一个场地怎么办”,你回答的时候如果说到了“乐观锁”和“唯一索引”,印象分直接拉满。
4.4 Vue项目启动端口被占用
Vite默认跑在5173,如果你之前启动过别的项目,可能端口被占用。解决很简单,要么杀掉占用进程,要么在vite.config.js里改端口:
javascript复制server: {
port: 5173,
strictPort: false
}
4.5 页面刷新后登录态丢失
这是很多同学会遇到的问题:登录成功,跳转首页,一切正常,F5一刷新,立刻回到登录页。原因大概率是JWT存在组件的data里,而没有持久化到localStorage。我的建议是登录成功后,将token写入localStorage,同时用Pinia/Vuex存一份用户信息用于组件内响应式更新,刷新时再重新从localStorage恢复。另外,路由守卫要等token恢复完成后再判断,否则会有一次闪跳。
为了方便自查,我整理了一个速查表:
| 问题 | 现象 | 排查方向 |
|---|---|---|
| 跨域报错 | 浏览器控制台CORS错误 | 后段允许跨域或前端配代理 |
| 中文乱码 | 前端显示问号 | 连接串加characterEncoding=utf8 |
| 时间差8小时 | 预定时间不正确 | 连接串加serverTimezone=Asia/Shanghai |
| 登录失效 | 刷新回登录页 | JWT持久化到localStorage |
| 依赖冲突 | 启动报NoSuchMethodError | 检查SpringBoot版本和各starter版本 |
| 图片上传后访问403 | 图片无法显示 | 配置静态资源映射到本地上传目录 |
5. 毕业论文、答辩PPT和搭建视频怎么说才不翻车
5.1 论文:从功能列表到完整叙事
很多同学把系统做完了,论文却写得像“功能说明书”,全是“能注册、能登录、能预定”,老师看了没有任何记忆点。我建议论文的叙事逻辑分成三条线,三条线并行推进,整个论文的层次感就出来了。
第一条线是“需求分析线”。从用户痛点出发:传统体育馆预定靠电话、现场登记,效率低、易冲突、无法实时查看空场。由此引出系统的目标和范围,顺理成章地导出功能需求和非功能需求。
第二条线是“设计线”。包括系统架构设计(前后端分离的结构图)、功能模块设计、数据库设计。这一章的重点是画图,用例图、E-R图、系统架构图、时序图,我建议至少五张图。图标清楚,论文瞬间显得专业。
第三条线是“实现线”。按模块写核心功能的具体实现,每讲一个模块就放一段核心代码加上关键业务逻辑说明,比如冲突检测、JWT鉴权、跨域处理。要让老师看完知道这些代码真的是你写的,而不是从某仓库里拉下来就完事。
最后要有“系统测试”章节。测试用例要覆盖正常流程和异常流程,异常流程尤其加分,比如:并发预定测试、未登录访问受限接口测试、非法参数校验测试。这些测试用例的截图放在论文里非常能说明问题。
5.2 答辩PPT:一页一个重点
答辩PPT不需要炫酷,但一定要逻辑清晰,一页只讲一个重点。我见过很多同学把大段文字往PPT上一贴,然后照着念,评委的问题一深挖就懵。
我的PPT提纲大概是这样的:
- 封面:标题、姓名、学号、指导老师。
- 目录:研究背景、系统设计、系统实现、系统测试、总结。
- 研究背景:一到两页,简洁说明为什么做这个系统。
- 系统设计:技术架构图、功能模块图、数据库E-R图,各一页。
- 系统实现:选三到四个核心功能,每个功能配一个系统截图加一段核心逻辑说明。
- 系统测试:展示测试环境、测试用例结果,截图为主。
- 总结:取得的成果、存在的问题、改进方向。
PPT演示的节奏控制在8分钟到10分钟,重点放在“你怎么解决关键问题”上。比如讲冲突检测时,直接演示:开两个浏览器,同时抢同一个场地,只有一个成功。这个现场演示比任何文字都有说服力。
5.3 指导搭建视频:录屏前先跑通全流程
“指导搭建视频”这个资源,价值在于能让一个零基础的师弟师妹看着视频把项目跑起来。我录视频时总结出一条经验:一定要先把全流程完整跑一遍,再开始录制。
视频内容我一般分成三块:
- 环境准备:JDK、MySQL、Node.js、IDEA/VSCode的安装和配置。
- 后端启动:导入后端项目、修改数据库连接配置、初始化数据库、启动SpringBoot。
- 前端启动:导入前端项目、安装依赖、启动开发服务器、访问页面。
这里有几个细节值得注意。第一,数据库脚本要提前准备好,最好是一个.sql文件包含建库、建表、初始化数据。第二,启动过程中涉及的一些环境变量,比如JDK路径,要在视频里提前检查。第三,视频全程不要用后台录像软件切割,要保持从头到尾一镜到底,方便看的人跟上。
一个很真实的情况:录视频时经常遇到“按了启动但服务没起来”,查半天发现是MySQL没启动。所以录制前一定要先按文档走一遍,把可能出现的环境问题排查掉,再开始正式录制。视频时长控制在30分钟左右比较合适,太长了观众看不到重点。
6. 一些我踩过的坑和最后的建议
做这类全栈项目,最后拼的往往不是编码能力,而是对流程的把控。给自己定一个时间表:第一周定需求、建表;第二周完成后端基础模块;第三周完成前端页面和联调;第四周写论文、做PPT、录视频。留一周的缓冲时间,不要压哨完成,因为肯定会出意外。
数据库设计一定要反复看几遍。表结构一旦确定,后续所有接口都建立在它之上,后期改表影响面极大。我在做这个项目时,就是因为一开始没考虑到“取消预订后时间重新释放”这个需求,导致订单状态设计多返了一次工。
前端页面的视觉风格,我建议整体保持简洁、统一就好,不要过度追求花哨动画。预定的核心操作只有三步:选场地、选时间、提交。流程越顺畅,用户越愿意用,论文里写的“用户体验良好”才有实证支撑。
最后一个建议,所有这些成果,包括系统源码、数据库脚本、论文、PPT、搭建视频,一定要统一放到一个目录里,命名清晰。一方面方便指导老师查看,另一方面,万一答辩前要重新部署演示,你也不会找不到文件而慌乱。项目本身不难,难的是把每个环节都稳稳推进。按部就班做下来,这个题目拿优秀并不难。
