SpringBoot+MySQL羽毛球馆预约系统源码:毕业设计完整工程解析
简介这是一份基于Spring Boot与MySQL的羽毛球馆在线预约系统毕业设计完整源码包面向计算机相关专业毕业生、课程设计学生以及需要快速搭建同类型预约项目的开发者。系统覆盖用户注册登录、场馆信息浏览与搜索、场地预约与取消、订单管理、在线/线下支付、场馆评价、公告发布及权限控制等核心业务模块采用MyBatis进行数据库持久化前端以HTML、CSS、JS及Vue组件实现可作为毕设选题、课程设计或项目练手的完整参考。压缩包共包含1156个文件约69.69MB其中以js、html、css等前端页面java、class构成的后端逻辑以及sql数据库脚本为主另附说明文档、LW论文和PPT答辩材料结构清晰部署后即可正常运行。目前已有58人学习浏览适合希望获得可运行范例、快速理解Spring Boot项目分层开发流程并借助文档完成毕业设计或答辩准备的读者。1. 羽毛球馆在线预约系统源码不是玩具项目是能跑完答辩的完整工程这是一份用 Java 写成的 springbootmysql 羽毛球馆在线预约系统源代码压缩包里含后端工程、SQL 脚本、说明文档、LW 和答辩 PPT。它不是只有登录页的 demo而是把场地管理、预约下单、订单状态流转、双角色权限都做完的完整工程SQL 导入后自带初始数据适合当计算机毕业设计的主项目也适合完整过一遍 springboot 实战代码的开发者。我帮学弟拆这份资源时最看重预约冲突判断那段同一场地同一时段的重叠条件处理得完整同类毕设里不多见。下文先拆模块和表设计第 2、3 章再讲怎么跑起来第 4 章接着是五条翻车记录第 5 章最后给一个答辩前能现场演示的并发验证技巧第 6 章。如果你正被「系统能跑但逻辑不严」卡住这份源码值得照着改一遍。2. 模块拆解与预约主流程两类角色、三条状态流转2.1 管理端与用户端的功能清单拿到这类 springbootmysql 毕设源码我习惯先做一件事不看代码先翻 controller 层数一数有多少个 RequestMapping再对照功能点列表。原因很简单技术面试和毕业答辩里评委最先问的永远是「你这个系统有哪些模块、每个模块解决什么问题」路由清单就是你回答这个问题的底稿。这个羽毛球馆预约系统的功能划分大致是下面这样你拿到手后先对一遍。如果发现某些方法缺失说明那份压缩包可能被精简过后面跑起来之前要先补。端模块功能点后端落点用户端账号注册、登录、退出登录AuthController用户端场地按类别浏览场地、按时间查空闲VenueController用户端预约下单预约、取消预约、查看我的订单BookingController管理端场地场地信息增删改查、上架/停用AdminVenueController管理端订单确认预约、完成订单、取消订单AdminBookingController管理端用户与公告停用账号、重置密码、发布公告AdminUserController表里的落点 Controller 对应后端代码的入口类比如 BookingController 里通常有 list、create、cancel 三个方法。这类工程的命名习惯比较统一Controller 层只做参数接收和结果包装真正的业务判断在 service 层。改功能时记住一条原则controller 里不要写业务判断所有规则都下沉到 service否则答辩时被追问「这段判断为什么放在这里」会答得很被动。为什么要单独给场地类别建一张表而不是直接在 venue 表里写死「羽毛球场地 1 号」因为价格和单位挂在类别上同一类别下的多块场地价格一致后面做统计查询时直接 group by type_id 就能算账。类似地公告、留言这类辅助功能通常单独成表避免核心订单表被次要字段撑大。2.2 一条预约订单的完整生命周期0→1→2 与取消支线预约系统的核心不在 CRUD而在订单的状态流转。状态字段如果直接用字符串「待确认」「已确认」写在代码里属于散落的魔法值改一个名字要全局替换还容易写错。我一般建议用枚举收敛这套源码里 booking 的状态也是这个思路四个状态值含义如下code状态触发者触发条件0待确认用户用户提交预约成功后生成1已确认管理员管理员在后台确认该预约2已完成管理员预约时间段结束后标记完成3已取消用户/管理员用户取消或管理员强制取消对应到代码里是这样一组枚举。构造器里把 code 和描述一起收进来后续写判断时直接用枚举比较避免满屏的if (status 0)public enum BookingStatus { PENDING(0, 待确认), CONFIRMED(1, 已确认), FINISHED(2, 已完成), CANCELED(3, 已取消); private final int code; private final String desc; BookingStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } }这个枚举类只是把状态值和中文描述绑定真正驱动流转的是 service 层的方法。比如 confirmBooking 里会把「待确认」改成「已确认」而「已完成」和「已取消」是两个终态任何方法都不允许从终态再跳回待确认。这里最容易被评委揪住的点是取消操作是否校验了「当前状态必须还在待确认或已确认」。如果一段代码允许已完成的单子被取消那说明状态机没有闭环是明显的逻辑漏洞。我在帮学弟改的时候特意把取消方法的前置校验补上了逻辑很简单if (status FINISHED || status CANCELED) throw一行注释讲清楚为什么答辩时这就是一个能主动讲出来的设计点。2.3 角色权限与数据可见范围的落地方式羽毛球馆预约天然分两类角色管理员和普通用户。普通用户只能看自己的订单管理员能看到全部订单。很多毕设代码把角色判断散落在十几个 controller 方法里每次都要从 session 取值再比较写得又臭又容易漏。这个工程用一个登录拦截器统一拦只有注册、登录和静态资源放行其余请求都要过 session 校验Component public class LoginInterceptor implements HandlerInterceptor { // role: 1管理员 0普通用户登录成功时写入 session Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object role request.getSession().getAttribute(role); if (role null) { response.sendRedirect(/login.html); // 未登录直接踢回登录页 return false; } String uri request.getRequestURI(); if (uri.startsWith(/admin/) !Integer.valueOf(1).equals(role)) { response.setStatus(403); // 非管理员访问后台接口返回 403 return false; } return true; } }参数说明preHandle 是 Spring MVC 拦截器的回调request.getSession 取到的 role 是登录时写入的uri 判断放在角色判断之后是为了让未登录用户也能看到静态登录页setStatus(403) 让前端拿到状态码后弹出「无权限」。这里有一个实际细节拦截器放行静态资源时还要在注册拦截器的配置类里排除 /css/、/js/、/images/ 路径不然页面样式会全部丢失很多新手在这步直接栽跟头症状是前端页面能打开但没有样式。数据可见范围除了拦截器还要在 mapper 层落实。「我的订单」列表必须强制拼上 user_id 条件不能只靠前端传参。常见写法是登录后把 userId 放进 session查询接口从 session 取 userId 而不是接收前端参数这样能避免越权查询别人的订单。这个点在答辩时属于加分项小成本高收益。3. 数据库设计与防重 SQL四张核心表和一个时间重叠判断3.1 四张核心表的字段与关系数据库设计是毕设答辩的重灾区。很多人的表导得进、跑得通但一问「为什么这么建」就卡壳。这套系统的表结构按常规包含四张核心表用户表 user、场地表 venue、场地类别表 venue_type、预约订单表 booking。字段大致如下表字段类型说明userid / username / password / nickname / phone / role / statusbigint / varchar / tinyintrole 区分角色status 控制账号是否停用venue_typeid / name / price / unitbigint / varchar / decimal场地类别price 是每小时价格venueid / type_id / name / location / statusbigint / varchar / tinyinttype_id 关联 venue_type.idstatus 控制上下架bookingid / booking_no / user_id / venue_id / start_time / end_time / statusbigint / varchar / datetime / tinyint订单核心时间字段决定冲突判断关系上venue_type 和 venue 是一对多venue 和 booking 是一对多user 和 booking 是一对多。设计时注意尽量避免在 venue 表里重复存类别名称冗余字段少一点后面做统计 SQL 时不容易出现数据对不上的问题。还有一个容易被忽略的点密码字段要存加密结果不要存明文。Spring Boot 里加一个 spring-security-crypto 依赖用 BCryptPasswordEncoder 加密这是近年评委一定会追问的安全点。3.2 状态字段用 tinyint 不用字符串的三个理由建表时订单状态、用户状态、场地状态我都建议用 tinyint而不是 varchar 存「已确认」这种中文。理由有三个一是 tinyint 只占 1 字节字段长度固定索引效率比变长字符串高二是避免同一含义多种写法「已取消」和「已取消」带不带空格都对不齐三是在 Java 代码里可以用枚举对接数字天然与上一章的 BookingStatus 对应。下面是 booking 表的建表脚本注意为预约时间和场地建联合索引否则数据量上来之后冲突查询会全表扫描CREATE TABLE booking ( id bigint NOT NULL AUTO_INCREMENT, booking_no varchar(32) NOT NULL COMMENT 预约单号, user_id bigint NOT NULL COMMENT 预约用户ID, venue_id bigint NOT NULL COMMENT 场地ID, start_time datetime NOT NULL COMMENT 预约开始时间, end_time datetime NOT NULL COMMENT 预约结束时间, status tinyint NOT NULL DEFAULT 0 COMMENT 0待确认 1已确认 2已完成 3已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_booking_no (booking_no), KEY idx_venue_time (venue_id, start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约订单表;脚本要点booking_no 用唯一索引主要是为了前端展示一个不重复的单号同时可以作为幂等键idx_venue_time 覆盖了防重查询的 where 条件venue_id 等值 start_time 范围这条索引会不会被用上可以直接用 EXPLAIN 验证。注意这里没有给 user_id 加索引如果用户订单量大建议补一个KEY idx_user (user_id)属于低成本高收益的调整。3.3 时间重叠判断最容易被评委追问的一段 SQL预约系统最核心的查询是判断「某个场地在某个时间段是否已被占用」。表面看是查 start_time 相等实际上是查区间重叠新预约 [new_start, new_end) 与已有预约 [old_start, old_end) 只要有交集就冲突。两个区间重叠的条件写成一句就是old_start new_end AND old_end new_start。放到 MyBatis 的 mapper XML 里是这样注意小于号要转义select idcountOverlap resultTypeint SELECT COUNT(*) FROM booking WHERE venue_id #{venueId} AND status ! 3 AND start_time lt; #{endTime} AND end_time gt; #{startTime} /select写法说明status ! 3 是为了排除已取消的订单不然用户取消后同样的时间再约会被误判冲突。start_time #{endTime} 和 end_time #{startTime} 这一对条件覆盖了四种重叠形态新预约在中间、旧预约在中间、完全包含、完全扩大全部会被拦下来。如果这套系统约定场地按整点/半点出租还可以把时间归一化再做精确等值判断会更快但作为毕设上面的区间判断已经够用而且更容易被评委理解。3.4 SQL 脚本导入与编码检查拿到压缩包后SQL 脚本通常命名为 xxx.sql 或 db_badminton.sql。导入前先建库字符集选 utf8mb4不要用默认的 latin1。Navicat 的操作路径是右键连接 → 新建数据库 → 数据库字符集选 utf8mb4 → 排序规则选 utf8mb4_general_ci → 然后运行 SQL 文件。导入完成后用下面这句确认字符集没跑偏SHOW VARIABLES LIKE character_set_database;如果结果不是 utf8mb4说明建库时选错了重新建库再导入。还要检查 SQL 文件本身的编码Windows 上用记事本另存为 UTF-8 容易带 BOMNavicat 识别 BOM 偶尔会报第一行语法错误遇到这种情况就用 VS Code 或 Notepad 把文件转成 UTF-8 无 BOM 再导。4. 本地复现的关键 20 分钟版本组合、三处配置和核心接口4.1 版本组合JDK 8 Spring Boot 2.x MySQL 5.7/8.0代码能跑起来的前提是环境别乱配。这套毕设源码打包时间通常在 2021-2023 年之间最稳妥的组合是下表这套尤其是 Spring Boot 版本。不要看到一个 3.x 就往上冲3.0 之后包名从 javax 换成了 jakarta老代码大面积报红5.1 节会讲具体现象。组件推荐版本说明JDK1.8 或 11不要用 17部分老依赖反射会报错Maven3.6.x 或 3.8.xIDEA 自带的也行Spring Boot2.7.18parent 和依赖都以它为准MySQL5.7 或 8.08.0 需额外配置 SSL见 5.2数据库客户端Navicat 或 DataGrip导入脚本用强调一下先装 MySQL 再装客户端MySQL 的 root 密码记牢后面 yml 里要用。如果之前装过 MySQL 8 又忘了密码直接卸载重装5.7 和 8.0 在这套代码里都能跑只是驱动配置不一样。MySQL 安装时字符集那一步勾选 utf8mb4 选项能少踩后面好多乱码的坑。4.2 导入项目与 application.yml 三处必改IDEA 打开压缩包解压后的 pom.xml等 Maven 把依赖拉完。然后打开 src/main/resources/application.yml这个文件是所有启动问题的集中地三处必改server: port: 8080 servlet: encoding: charset: UTF-8 force: true spring: datasource: url: jdbc:mysql://localhost:3306/badminton_booking?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver参数含义url 里的 useUnicode 和 characterEncoding 保证中文不乱码serverTimezoneAsia/Shanghai 是 MySQL 8 必须加的否则报时间时区错误useSSLfalse 关掉 SSLallowPublicKeyRetrievaltrue 解决 MySQL 8 的 caching_sha2_password 认证报错。username 和 password 改成你自己本机的账号。改完这三处90% 的启动问题都能消除。4.3 预约下单接口从 controller 到 service 的完整链路看核心功能别停留在打开页面要看「下单」这条链路。controller 只做参数包装RestController RequestMapping(/api/booking) public class BookingController { Autowired private BookingService bookingService; PostMapping public Result create(RequestBody BookingDTO dto, HttpServletRequest request) { // dto 里是 venueId、startTime、endTimeuserId 从 session 取 Integer userId (Integer) request.getSession().getAttribute(userId); return Result.ok(bookingService.createBooking(userId, dto)); } }真正写逻辑的 service 层长这样public boolean createBooking(Integer userId, BookingDTO dto) { // 1. 场地必须存在且处于开放状态 Venue venue venueMapper.selectById(dto.getVenueId()); if (venue null || venue.getStatus() ! 1) { throw new BizException(场地不存在或未开放); } // 2. 同一时间段冲突检查3.3 节那个 SQL 在这里被调用 int count bookingMapper.countOverlap( dto.getVenueId(), dto.getStartTime(), dto.getEndTime()); if (count 0) { throw new BizException(该时间段已被预约请更换时间); } // 3. 写入订单初始状态待确认 Booking booking new Booking(); booking.setBookingNo(BK System.currentTimeMillis()); booking.setUserId(userId); booking.setVenueId(dto.getVenueId()); booking.setStartTime(dto.getStartTime()); booking.setEndTime(dto.getEndTime()); booking.setStatus(BookingStatus.PENDING.getCode()); return bookingMapper.insert(booking) 0; }这段代码有两个值得注意的细节。一是 userId 不从 dto 接收而是从 session 拿防止用户伪造参数预约别人名下的订单二是校验顺序是「场地状态 → 时间冲突 → 插入」如果把时间冲突判断放最后同一时刻两个请求可能都通过校验第 6 章的并发验证就是盯着这个点。参数格式上startTime 和 endTime 建议用 yyyy-MM-dd HH:mm:ss 字符串传入MyBatis 会交给 MySQL 的 datetime 解析前端传 ISO 格式反而容易报转换异常。4.4 启动验证与日志定位IDEA 里直接运行启动类右键 Run 就行。看到下面日志说明端口起来了Tomcat started on port(s): 8080 (http)接着用 curl 冒烟一下不必非得开浏览器curl -i http://localhost:8080/login.html返回 200 和 HTML 内容说明静态资源正常。如果返回 404先看 target/classes 里有没有把静态资源打进去如果是接口报 500去日志里找异常堆栈第一行大部分是 SQL 语句拼错或字段名对不上表结构。控制台日志里出现BadSqlGrammarException时把它后边跟的那条 SQL 复制到 Navicat 里手动执行能很快定位是表名还是字段名的问题。提示改 yml 之后一定要重新启动 Spring Boot 进程。datasource 参数属于启动期绑定IDEA 只热加载不生效别在这个地方花时间去点「重新构建」。5. 高频翻车点与排查记录启动失败、SSL 报错、乱码、缓存5.1 Spring Boot 3.x 导致 javax 包全部报红现象导入项目后 pom.xml 里 spring-boot-starter-parent 是 3.x代码里 import javax.servlet.* 大面积报红编译直接失败启动类都找不到。原因Spring Boot 3.0 起 Java EE 的包名从 javax 迁移到 jakarta。老毕设代码全是 javax版本升级后类路径对不上。这不是代码的问题是环境配对的问题很多学弟一上来就把版本升到最新等于给自己挖坑。解决把 parent 版本改回 2.7.18并确认 JDK 用 8 或 11。改动量最小不需要动任何 import。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent有一个经验改完 pom 后一定要在 IDEA 右侧 Maven 面板点一次刷新等依赖重新解析完再 compile。如果还有残留报错执行mvn clean把 target 清掉很多「改了还是报错」其实是被增量编译的旧类干扰了。5.2 MySQL 8 的驱动类名与 SSL 连接报错现象启动时报ClassNotFoundException: com.mysql.jdbc.Driver或者报Public Key Retrieval is not allowed再或者控制台出现 SSL 警告后连接失败。原因MySQL 8 的驱动类更名成 com.mysql.cj.jdbc.Driver同时 8.0 默认认证插件是 caching_sha2_password第一次连接需要取服务端公钥默认不允许就会报错。这个问题在 mysql 安装配置教程里经常被一笔带过实际踩坑的人极多。解决yml 里 driver-class-name 改成com.mysql.cj.jdbc.Driverurl 加useSSLfalseallowPublicKeyRetrievaltrue。这两项参数在 4.2 节已经写进示例。如果你本机装的是 5.7旧驱动能跑但换成 cj 驱动更省事连接串不用区分版本。顺带说一句SSL 警告和 SSL 错误是两回事。警告日志Establishing SSL connection without servers identity verification is not recommended不阻断连接只刷日志真正的CommunicationsException才是连接失败。遇到警告可以不管遇到异常才需要加 useSSLfalse。5.3 端口被占用上次进程没退干净现象启动日志出现Port 8080 was already in use应用秒退或者页面一直转圈。原因上一次运行没正常停止IDEA 的 Stop 按钮有时没杀掉 JVM或者本机有其他服务占了 8080。特别是同时打开多个 IDEA 窗口时每次都容易踩。解决先查再杀Windows 下两个命令netstat -ano | findstr 8080 taskkill /PID 12345 /F杀完再启动。也可以一劳永逸在 yml 里把 server.port 改成一个不那么常用的端口比如 8088连冲突的判断都省了。改端口后注意前端页面里有没有写死 8080 的 ajax 地址有的话要一起改这是另一个常见坑。5.4 中文乱码三种表现源头各不相同现象页面中文显示乱码数据库查出来的备注是问号接口返回 JSON 中文乱码。三种症状常常同时出现让人以为是同一个原因。原因三种乱码原因不同。页面乱码是 HTML 文件编码与浏览器解析编码不一致数据库乱码是建库时字符集不是 utf8mb4JSON 乱码是 Spring Boot 响应编码没有强制。逐层排查才不会干着急。解决页面检查 index.html 等文件的meta charsetutf-8数据库按 3.4 节重建 utf8mb4并且导入前确认 SQL 文件用 UTF-8 无 BOM 保存JSON 乱码靠 yml 里server.servlet.encoding.force: true强制响应编码这个配置在 4.2 节已经写过。另外 IDEA 右下角把文件编码切到 UTF-8再重新编译一次文件编码和编译编码保持一致后乱码问题基本绝迹。5.5 改了前端文件却不生效浏览器缓存与 target 旧文件现象明明改了 login.html 的标题刷新页面还是老样子或者删了一段 JS浏览器里还在执行。原因浏览器对静态资源有缓存IDEA 重新构建时 target 目录里残留旧文件也会被访问到。这两个因素叠加就会产生「改了等于白改」的错觉属于前端调试里最典型的玄学问题。解决先用 CtrlF5 强刷排除浏览器缓存还是旧内容就去 target/classes/static 下确认文件是否更新。Spring Boot 2.7 可以在 yml 里临时把静态资源缓存时间设为 0 方便调试线上再放开。也可以用 spring-boot-devtools 做热重启但我个人不建议毕设阶段依赖它重启一次不过十几秒比排查「为什么没生效」快得多。6. 验收技巧并发预约压一次就知道锁写没写6.1 同场地同时段的并发请求看落库几条答辩最有说服力的演示不是截图而是现场「压」一下核心接口。预约系统的核心风险是并发下同一场地被约重。先登录拿到 cookie然后同时发 5 个相同参数的预约请求模拟 5 个人同时抢同一块场地curl -c cookies.txt -s -X POST http://localhost:8080/api/login \ -H Content-Type: application/json \ -d {username:zhangsan,password:123456} for i in $(seq 1 5); do curl -b cookies.txt -s -X POST http://localhost:8080/api/booking \ -H Content-Type: application/json \ -d {venueId:1,startTime:2025-06-10 19:00:00,endTime:2025-06-10 20:00:00} done wait然后查预约表统计同一场地同一时段落了多少钱SELECT COUNT(*) FROM booking WHERE venue_id 1 AND start_time 2025-06-10 19:00:00 AND end_time 2025-06-10 20:00:00;如果返回 5 行说明 service 层「先查后插」存在竞态5 个请求都认为没冲突直接插进去了。如果返回 1 行说明项目里做了并发控制——常见的实现是给查询加FOR UPDATE悲观锁或者在 booking 表针对 (venue_id, start_time, end_time) 建唯一约束。这个测试结果可以直接写进答辩 PPT比「系统很稳定」有说服力得多。6.2 没有并发控制时的补救与验证如果查询结果是 5 条别慌先把 4.3 节的 service 改成悲观锁方案查询冲突时用SELECT ... FOR UPDATE锁住该场地的时间行让其他请求排队等待。改完后重新跑 6.1 的循环COUNT 应该稳定为 1。注意并发验证要看落库结果不能只看接口返回值接口返回的 success 可能是「已经查到冲突并抛出异常」的正常逻辑也可能是什么都没做就返回成功。从那以后我每次接手一套 springboot 毕设源码第一件事不是急着跑起来而是先找核心业务那条链路用并发命令反复压。预约这种带状态流转和时间判断的功能跑通了不算数并发下不重不漏才算过关。这套流程也推荐你按序走一遍第 2 章对功能、第 3 章对表、第 4 章对配置、第 6 章做验证一轮下来自己心里有数答辩也能接得住追问希望帮到你。本文还有配套的精品资源点击获取