ARTICLE DETAIL

资讯详情

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

Java毕设实战:SpringBoot羽毛球预约系统设计与避坑指南

Java毕设实战:SpringBoot羽毛球预约系统设计与避坑指南 简介本资源是一套完整的Java毕业设计项目——羽毛球馆在线预约系统源码面向计算机专业本科生及Spring Boot初学者解决场馆管理、用户预约、订单支付与评价反馈等典型业务场景开发需求。压缩包共1156个文件含39个Java后端控制器与实体类如YuyueController、User、ChangdiController等、518个前端JS交互脚本、168个GIF/94个PNG/18个JPG等静态资源、84个HTML页面与74个CSS样式文件辅以SQL建表语句、Maven配置、说明文档及答辩用PPT整体大小69.69MB。已有58人学习下载资源开箱即用部署后可完整运行涵盖用户注册登录、场馆检索、时段预约、订单管理、微信/支付宝对接模拟、管理员公告发布与权限分级控制等八大模块代码结构清晰、注释规范特别适合毕设开发参考、Spring Boot全栈实践与MySQL数据库应用训练。1. 为什么一个羽毛球馆预约系统成了Java毕业设计里最常被选、也最容易翻车的“练手项目”你不是在找“能跑起来的SpringBoot模板”而是在找一个边界清晰、业务真实、技术栈可控、答辩能讲清楚、老师挑不出硬伤的毕设载体。羽毛球馆预约系统就是这么个“玄学平衡点”它不像电商系统要搞分布式事务也不像社交平台得啃消息队列和实时推送但它又比“学生信息管理系统”多出时间冲突校验、资源动态锁定、状态机流转这些真刀真枪的业务逻辑。用SpringBootMySQL搭起来不堆砌高大上组件却能把MVC分层、MyBatis-Plus CRUD、事务控制、RESTful接口设计、前端联调这些核心能力全串一遍。我带过17届毕设83%选Java方向的同学第一轮选题筛下来剩下能稳稳落地的基本就卡在这类“小而完整”的场馆预约系统上——它不考验你造轮子但会狠狠检验你对框架底层约束的理解。如果你正卡在开题报告写不出业务流程图、答辩PPT里全是截图没有逻辑主线、或者部署到服务器后发现“预约成功但数据库没写入”这种黑匣子问题这篇笔记就是为你写的血泪经验复盘。2. 从零启动用SpringBoot 2.7.x MyBatis-Plus 3.5.x 搭建最小可行骨架2.1 创建工程为什么坚持用 SpringBoot 2.7.x 而非 3.x毕业设计环境必须“稳”字当头。SpringBoot 3.x 强制要求 JDK 17、Jakarta EE 9 命名空间javax.*→jakarta.*而学校实验室服务器、老师本地开发机大概率还卡在 JDK 8/11Tomcat 版本也普遍是 8.5/9.0。一旦你用 3.x 启动报java.lang.NoClassDefFoundError: javax/servlet/Filter就得花半天改依赖、降级、重配编译插件——这纯粹是给答辩前两周埋雷。实操命令Maven方式mvn archetype:generate \ -DgroupIdcom.example.badminton \ -DartifactIdbadminton-reservation \ -DarchetypeArtifactIdmaven-archetype-webapp \ -DinteractiveModefalse然后手动引入 SpringBoot 2.7.182023年最后一个2.x维护版!-- pom.xml -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version !-- 严格匹配 SpringBoot 2.7.x -- /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependencies提示MyBatis-Plus 3.5.x 是兼容 SpringBoot 2.7.x 的最终稳定版。若误用 4.x适配 SpringBoot 3.x启动时会报Invalid bound statement (not found)—— 因为 XML 映射路径解析机制变了这是新手最常踩的“版本幻觉坑”。2.2 数据库建模一张表解决核心矛盾三张表撑起完整闭环羽毛球馆的核心矛盾就一个同一块场地、同一时段不能被两个人同时预约。所以主表设计必须把“场地时间”作为不可分割的原子单元。我见过太多同学一上来就建user、court、booking三张表结果在booking表里存start_time和end_time字段导致查询“某时段是否有空场”时要用BETWEEN配合NOT EXISTS子查询性能差且易漏判比如预约 14:00-16:00新预约 15:30-17:00 就会被漏掉。正确解法按“时间片”切分场地资源。假设场馆有3块场地每块按1小时切片早9点到晚10点共14个时段则初始化数据量仅 3×1442 行查询直接走主键或唯一索引-- court表场地基础信息 CREATE TABLE court ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL COMMENT 场地名称如A区1号场, status TINYINT DEFAULT 1 COMMENT 1-可用0-停用 ); -- time_slot表预定义所有可预约时段固定粒度 CREATE TABLE time_slot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, start_time TIME NOT NULL, end_time TIME NOT NULL, slot_name VARCHAR(10) NOT NULL COMMENT 如14:00-15:00 ); -- court_slot表场地时段的占用状态核心 CREATE TABLE court_slot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, court_id BIGINT NOT NULL, slot_id BIGINT NOT NULL, status TINYINT DEFAULT 0 COMMENT 0-空闲1-已预约2-已取消3-已过期, booked_by BIGINT DEFAULT NULL COMMENT 预约用户ID, booked_at DATETIME DEFAULT NULL, UNIQUE KEY uk_court_slot (court_id, slot_id) -- 强制唯一杜绝重复占位 );参数说明court_slot.status不用布尔值而用数字枚举方便后续扩展比如加“维修中”状态UNIQUE KEY uk_court_slot是防并发预约冲突的物理屏障比代码层加锁更可靠booked_by允许为 NULL因为初始空闲状态无需关联用户避免冗余JOIN。2.3 自动生成实体与Mapper用MyBatis-Plus逆向工程省掉80%样板代码手写Court.java、TimeSlot.java、CourtSlot.java这三个实体类及其TableField注解至少耗掉2小时且极易拼错字段名。MyBatis-Plus 提供的AutoGenerator可一键生成// codegen/CodeGenerator.java public class CodeGenerator { public static void main(String[] args) { AutoGenerator generator new AutoGenerator(); // 数据源配置指向你的MySQL DataSourceConfig dsc new DataSourceConfig() .setUrl(jdbc:mysql://localhost:3306/badminton?useSSLfalseserverTimezoneAsia/Shanghai) .setDriverName(com.mysql.cj.jdbc.Driver) .setUsername(root) .setPassword(123456); generator.setDataSource(dsc); // 包配置 PackageConfig pc new PackageConfig() .setParent(com.example.badminton) .setEntity(entity) .setMapper(mapper) .setXml(mapper.xml) .setService(service) .setServiceImpl(service.impl) .setController(controller); generator.setPackageInfo(pc); // 策略配置指定表名关闭Lombok毕设答辩时老师可能不认Lombok StrategyConfig strategy new StrategyConfig() .setNaming(NamingStrategy.underline_to_camel) .setColumnNaming(NamingStrategy.underline_to_camel) .setEntityLombokModel(false) // 关键禁用Lombok避免答辩时被问“Data是什么” .setInclude(court, time_slot, court_slot); // 只生成这三张表 generator.setStrategy(strategy); generator.execute(); } }运行后自动生成Court.java含TableName(court)、CourtMapper.java继承BaseMapperCourt、CourtMapper.xml空文件因MP默认不用XML等全套代码。关键细节setEntityLombokModel(false)是毕设刚需——答辩老师可能连Data注解都没见过看到一堆getXXX()方法反而觉得“代码规范”setInclude()明确限定表名避免把sys_user或测试表也扫进来生成的CourtSlot实体类中booked_by字段会自动映射为bookedBy符合驼峰规则无需手动改名。3. 核心业务落地预约、取消、查询三步闭环每步都带事务与幂等3.1 预约接口用数据库唯一索引事务回滚实现强一致性预约的本质是“抢占一个court_slot记录”。如果先查后插Select-then-Insert高并发下必然出现超卖。正确姿势是直接INSERT靠唯一索引失败触发事务回滚。// controller/ReservationController.java PostMapping(/api/reservations) public Result? createReservation(RequestBody ReservationRequest request) { try { boolean success reservationService.bookCourt( request.getCourtId(), request.getSlotId(), request.getUserId() ); return success ? Result.success(预约成功) : Result.fail(该时段已被预约); } catch (DuplicateKeyException e) { // 唯一索引冲突说明已被他人抢占 return Result.fail(预约失败该场地时段已被占用); } catch (Exception e) { log.error(预约异常, e); return Result.fail(系统繁忙请稍后再试); } } // service/impl/ReservationServiceImpl.java Transactional(rollbackFor Exception.class) Override public boolean bookCourt(Long courtId, Long slotId, Long userId) { // 构造待插入记录 CourtSlot courtSlot new CourtSlot(); courtSlot.setCourtId(courtId); courtSlot.setSlotId(slotId); courtSlot.setStatus((byte) 1); // 1-已预约 courtSlot.setBookedBy(userId); courtSlot.setBookedAt(new Date()); // 执行插入唯一索引保证原子性 int rows courtSlotMapper.insert(courtSlot); return rows 1; // 插入成功返回true }逻辑说明Transactional确保整个方法在同一个数据库连接内执行insert()方法底层执行INSERT INTO court_slot (...) VALUES (...)若court_idslot_id组合已存在MySQL 抛SQLIntegrityConstraintViolationException被DuplicateKeyException捕获事务自动回滚不会残留脏数据。3.2 取消预约状态机驱动拒绝“伪取消”很多同学写取消逻辑就是UPDATE court_slot SET status0 WHERE id?这会导致两个致命问题用户A取消后用户B立刻预约同一时段但A的取消操作还没commitB查不到最新状态用户A恶意重复点击取消按钮把状态从1→0→1→0反复切换破坏数据一致性。解决方案用状态码做前置校验 乐观锁防止并发覆盖// entity/CourtSlot.java TableField(version) private Integer version; // 乐观锁字段MyBatis-Plus自动处理 // service/impl/ReservationServiceImpl.java Transactional(rollbackFor Exception.class) Override public boolean cancelReservation(Long courtSlotId, Long userId) { // 1. 查询当前记录校验是否属于该用户且状态为已预约 CourtSlot current courtSlotMapper.selectById(courtSlotId); if (current null || !current.getStatus().equals((byte) 1) || !current.getBookedBy().equals(userId)) { return false; // 不是本人预约或状态不对拒绝取消 } // 2. 构造更新条件必须是当前version且status1 UpdateWrapperCourtSlot wrapper new UpdateWrapper(); wrapper.eq(id, courtSlotId) .eq(status, (byte) 1) // 确保只取消已预约状态 .eq(booked_by, userId) .eq(version, current.getVersion()); // 乐观锁防止并发修改 CourtSlot update new CourtSlot(); update.setStatus((byte) 2); // 2-已取消 update.setVersion(current.getVersion() 1); // version1 int rows courtSlotMapper.update(update, wrapper); return rows 1; // 更新成功才返回true }参数说明UpdateWrapper的eq(status, (byte)1)是状态机守门员确保只能从“已预约”变为“已取消”eq(version, current.getVersion())是乐观锁若并发请求中有一个先更新了version另一个的update将影响0行返回falsesetVersion(current.getVersion() 1)是乐观锁标准写法避免ABA问题。3.3 场地查询用LEFT JOIN CASE WHEN 实现“空闲/已预约/已取消”三态渲染前端需要展示每块场地每个时段的状态绿色空闲、红色已约、灰色已取消但court_slot表只存占用记录未预约的时段在表里根本不存在。传统做法是查出所有court×time_slot组合再循环判断N²复杂度。高效解法是用LEFT JOIN补全空记录再用CASE WHEN统一赋值// mapper/CourtSlotMapper.java Select(SELECT c.id AS court_id, c.name AS court_name, t.id AS slot_id, t.slot_name, COALESCE(cs.status, 0) AS status, // 未预约则status0 cs.booked_by FROM court c CROSS JOIN time_slot t LEFT JOIN court_slot cs ON c.id cs.court_id AND t.id cs.slot_id WHERE c.status 1 ORDER BY c.id, t.id) ListCourtSlotView selectAllCourtSlots();// entity/CourtSlotView.java 非数据库表纯VO public class CourtSlotView { private Long courtId; private String courtName; private Long slotId; private String slotName; private Byte status; // 0-空闲1-已预约2-已取消 private Long bookedBy; // 预约用户ID空闲时为null }为什么用 CROSS JOINCROSS JOIN生成笛卡尔积确保每块场地都与每个时段配对LEFT JOIN保留所有组合未匹配的cs.*字段为NULLCOALESCE(cs.status, 0)将其转为0空闲查询结果直接对应前端表格的行列结构无需后端二次组装。4. 避坑指南毕业设计答辩前最常被问倒的5个问题及血泪解法4.1 现象本地Postman测试预约接口返回200但MySQL里court_slot表无新增记录原因Transactional注解失效。常见于Service方法被本类其他方法直接调用如A.method1()内调A.method2()绕过Spring代理Service类未被Spring容器管理忘记加Service注解或包扫描路径没覆盖到MySQL引擎不是InnoDBMyISAM不支持事务。解决检查ReservationServiceImpl类上有Service确保启动类BadmintonApplication.java的SpringBootApplication扫描路径包含service.impl包执行SHOW ENGINES;确认court_slot表引擎为InnoDBALTER TABLE court_slot ENGINEInnoDB;。4.2 现象前端选择“14:00-15:00”时段预约后端收到的slotId却是0或null原因前端传参格式与后端接收类型不匹配。常见于前端用axios.post(/api/reservations, {courtId: 1, slotId: 2})传字符串而后端ReservationRequest的slotId定义为LongSpringBoot默认不开启spring.jackson.deserialization.fail-on-unknown-propertiesfalse遇到未知字段直接400。解决前端确保传数字{courtId: 1, slotId: 2}去掉引号在application.yml中添加spring: jackson: deserialization: fail-on-unknown-properties: false4.3 现象court_slot表里出现status1但booked_bynull的脏数据原因bookCourt()方法中courtSlot.setBookedBy(userId)被注释或遗漏且booked_by字段允许NULL。解决在court_slot表结构中将booked_by改为NOT NULLALTER TABLE court_slot MODIFY booked_by BIGINT NOT NULL;对应实体类CourtSlot.java中bookedBy字段加TableField(fill FieldFill.INSERT)并实现自动填充逻辑或直接在Service中强制赋值。4.4 现象部署到Linux服务器后访问http://ip:8080页面空白Chrome控制台报net::ERR_CONNECTION_REFUSED原因SpringBoot默认绑定localhost127.0.0.1外部无法访问。解决在application.yml中显式配置server: port: 8080 address: 0.0.0.0 # 绑定所有网卡而非仅localhost4.5 现象答辩时老师问“你怎么保证预约不超时比如用户付完款没确认占着场地怎么办”原因未实现超时释放机制业务逻辑不闭环。解决在court_slot表加expire_at字段预约后15分钟过期启动定时任务扫描status1 AND expire_at NOW()的记录批量更新为status0代码示例// config/SchedulingConfig.java Configuration EnableScheduling public class SchedulingConfig {} // service/impl/ReservationServiceImpl.java Scheduled(fixedRate 60000) // 每分钟执行一次 public void releaseExpiredReservations() { UpdateWrapperCourtSlot wrapper new UpdateWrapper(); wrapper.eq(status, (byte) 1) .lt(expire_at, new Date()); CourtSlot update new CourtSlot(); update.setStatus((byte) 0); courtSlotMapper.update(update, wrapper); }5. 毕设答辩加分项用Vue打包静态资源嵌入SpringBoot实现真正“单jar部署”很多同学把Vue项目单独部署在Nginx答辩时得解释“前后端分离”反而暴露架构理解浅薄。其实SpringBoot官方支持静态资源托管把Vue打包产物扔进src/main/resources/static目录就能用java -jar badminton-reservation.jar一键启动全栈应用。5.1 Vue项目配置关掉路由history模式避免404陷阱Vue Router默认用history模式访问/user时浏览器发请求到/user但SpringBoot静态资源只响应/static/**下的文件导致404。必须切到hash模式// src/router/index.js const router new VueRouter({ mode: hash, // 关键改为hash模式 routes: [ { path: /, component: Home }, { path: /book, component: Booking } ] })5.2 Vue打包输出目录精准匹配SpringBoot静态资源路径Vue CLI默认打包到dist/需修改vue.config.js// vue.config.js module.exports { outputDir: ../backend/src/main/resources/static, // 直接输出到SpringBoot的static目录 assetsDir: , // 静态资源不加前缀 productionSourceMap: false }注意../backend/是相对路径假设你的Vue项目和SpringBoot项目在同一父目录下结构为project-root/ ├── backend/ ← SpringBoot项目 └── frontend/ ← Vue项目5.3 SpringBoot接管前端路由让所有非API请求都返回index.htmlVue的hash模式下URL形如http://localhost:8080/#/book#后面的内容由前端JS解析SpringBoot只需管好#前的部分。但为防用户手动输入/book刷新页面需配置SpringBoot兜底// config/WebMvcConfig.java Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { // 所有非/api/* 请求都返回index.html由Vue Router接管 registry.addViewController(/).setViewName(forward:/index.html); registry.addViewController(/book).setViewName(forward:/index.html); registry.addViewController(/my-reservations).setViewName(forward:/index.html); } }5.4 最终验证单jar包启动全功能在线执行以下步骤cd frontend npm run build生成静态文件到backend/src/main/resources/staticcd backend mvn clean packagejava -jar target/badminton-reservation-0.0.1-SNAPSHOT.jar浏览器访问http://localhost:8080所有页面、预约、取消功能全部可用。此时你的毕设项目就是一个真正的fat jar答辩时老师只要求你java -jar xxx.jar就能看到完整系统——这比“我前端用Vue后端用SpringBoot部署在Nginx上”这种描述可信度高出三个数量级。我带过的最后一届学生有个姑娘答辩时被问“你这个系统怎么部署”她直接掏出U盘插进老师电脑打开CMD敲java -jar badminton.jar然后打开浏览器演示全流程。老师当场说“这个思路很干净代码也扎实。”——毕设不是炫技是证明你能把一件事从0到1闭环落地。希望帮到你。本文还有配套的精品资源点击获取
返回列表