资讯动态

SpringBoot+Vue+MySQL体育馆预约系统全栈开发实战与踩坑记录

发布时间:2026/10/22 0:23:39 来源:尧图企业网站定制
毕业设计选了个“SpringBootVueMySQL 体育馆使用预约平台”当时第一反应是这种课题网上不是一大堆吗但真正动手做起来才发现把一个预约系统从数据库设计到前后端联调再到部署上线完整跑通中间暗坑多得超出预期。这篇文章就把我完整做完这个项目的思路、实现细节、踩坑记录都摊开讲给正在做类似选题的同学一个真实可参考的案例。这个平台不是简单的“增删改查”。它要解决的核心问题是用户怎么高效地预定场地、管理员怎么管理场地与订单、系统怎么避免重复预约和时间冲突。围绕这个目标我拆出了用户端、管理员端、场地管理、预约订单、消息通知和数据统计几大模块。全文会按设计思路、数据库、后端、前端、部署、问题排查的顺序展开结尾附上几条只有实际动手才会知道的经验。内容适合正在选毕业设计题目的本科生也适合想快速了解SpringBootVue全栈项目完整开发流程的人。1. 项目整体拆解先想清楚做什么再动手写代码1.1 需求分析预约平台的核心流程是什么体育馆预约平台最核心的用户流程是注册登录 - 浏览场地 - 选择时间段 - 提交预约 - 管理员审核/支付确认 - 生成订单。这个流程里隐含了两个角色视角用户端要看到的是“我能约什么、什么时间有空、我约了什么”。所以页面至少要包括场地列表、场地详情、预约表单、我的订单、个人信息。管理员端要看到的是“今天有哪些预约、哪些场地被占用、每个场地的使用率”。因此后台需要场地管理、预约审核、订单列表、数据统计。很多人做毕设容易一上来就写代码先建几个表再说后面发现字段不对、逻辑冲突返工成本很高。我建议先画一遍完整的流程图哪怕用纸笔把下面几个问题回答清楚一个场地一天内如何表示“上午/下午/晚上”这些时段是场地面向时段还是订单用开始时间结束时间用户可否取消预约取消后释放的时段是否允许他人继续预约管理员是否可以手动关闭某个场地的预约一个用户同一时间段最多预约几个场地这些看起来是细节其实直接决定数据库表结构和接口设计。我最终选择了“场地时段”的拆分模型具体在第二章里讲。1.2 技术选型为什么是SpringBootVueMySQL而不是别的毕设技术栈选“SpringBootVueMySQL”属于稳妥且性价比高的组合。SpringBoot负责提供RESTful APIVue负责前端交互MySQL存储业务数据。这个组合的好处是学习资料极多遇到问题搜索一下基本都有答案前后端分离符合现代Web开发主流模式答辩时容易讲清楚SpringBoot内置Tomcat和MySQL搭配不需要额外安装复杂中间件Vue的响应式数据和组件化开发适合做预约这种多页面交互系统如果换用SSH或者JSP代码会臃肿很多换成前后端不分离答辩的亮点少一大截。有人会问为什么不用更流行的微服务、Redis、消息队列我的回答是毕业设计的时间有限核心是完整解决问题不是堆技术。预约平台的瓶颈不在高并发而在业务状态一致性所以用SpringBoot自带的Transactional处理事务就够了。我最终在项目中加入了JWT做用户登录凭证后续想扩展Redis缓存也能平滑替换。数据库层面加了唯一索引防止预约冲突这是比单纯加锁更适合这种场景的做法。2. 数据库设计预约系统能不能稳住关键看表怎么建2.1 核心表结构与关系梳理这个平台我设计了6张核心表用户表、场地类型表、场地表、时段表、预约订单表、公告表。这样拆的好处是避免一张大表堆字段扩展性更好。用户表userid、username、password、nickname、phone、role普通用户/管理员、avatar、create_time。password用BCrypt加密存储不要明文。场地类型表venue_typeid、name如篮球场、羽毛球场。为了让场地更通用我拆分了这个维度。场地表venueid、type_id、name、location、description、image、status启用/停用。这里注意保留status因为管理员需要临时关闭场地维修。时段表time_slotid、venue_id、slot_date、start_time、end_time、status。这是预约系统的核心表。每个场地每天对应一组固定时段比如09:00-10:00、10:00-11:00。每个时段独立成为一条记录预约时直接锁定这个时段。这样设计的好处是时段的状态可预约/已预约/停用一目了然查询某天某场地是否可约就是一条SQL查表的事。预约订单表orderid、order_no、user_id、venue_id、time_slot_id、status待支付/已确认/已取消/已完成、price、pay_type、create_time、update_time。订单关联时段而不是直接存“开始时间结束时间”是为了保证时间维度统一在时段表里管理。公告表noticeid、title、content、create_time供管理员发布通知。表关系的核心是一个场地对应多个时段一个时段只能被一个有效订单占用。所以我在订单表中对time_slot_id加了唯一索引前提是同一时段不允许重复预约。这里有个坑如果允许用户取消后再约同一时段那唯一索引需要在状态字段上做条件。MySQL的普通唯一索引办不到可以用一个冗余字段例如“占用的时段状态”配合联合唯一索引或者干脆在应用层加分布式锁本项目单机部署用synchronized就够了。2.2 预约冲突检测与状态机设计预约最容易出问题的就是“超卖”和“时间重叠”。超卖指同一时段被两个订单同时抢占时间重叠指一个用户预约了重叠的时间段比如篮球场10点到11点同时预约了羽毛球场10点到11点这在高尔夫、网球场这些场景是否允许业务上一般要求同一个用户同一时间不能预约多个不同场地否则会出现分身乏术。我的处理办法是在提交预约的Service方法上加了事务先从时段表里用乐观锁更新status判断受影响行数。只有更新成功受影响行数为1才创建订单。伪代码如下Transactional public Result createOrder(OrderDTO dto) { // 1. 锁定用户 User user userMapper.selectByIdForUpdate(dto.getUserId()); // 2. 检查用户该时间段是否已有冲突订单 ListOrder conflictOrders orderMapper.selectConflictOrders(dto.getUserId(), dto.getStartTime(), dto.getEndTime()); if (!conflictOrders.isEmpty()) { return Result.error(您在该时间段已有预约); } // 3. 尝试更新时段状态 int count timeSlotMapper.lockSlot(dto.getTimeSlotId()); if (count 0) { return Result.error(该时段刚刚被预约了); } // 4. 创建订单 // ... }注意MySQL默认隔离级别是可重复读这里对用户行加锁是为了防止并发时读到旧数据。坦率讲毕设不会有高并发压力但答辩老师喜欢问“并发下怎么保证不冲突”这套逻辑说出来是加分项。状态机方面订单状态肯定是待支付 - 已确认 - 已完成待支付可以取消已确认也能取消并释放时段。我在取消订单时先判断当前状态是否允许取消再释放对应时段。释放时段不能简单把status改为0还要额外保证只有“没被其他新订单占用”时才释放所以释放时也走了更新状态的条件判断。3. 后端实现SpringBoot里的那些关键逻辑3.1 基于JWT的用户认证与权限控制登录认证选JWT而不是传统的Session。原因很简单前后端分离后端起多个端口扩缩容时Session同步很麻烦JWT天然无状态。我用的依赖是JWT库生成Token的流程是登录成功后生成一个包含userId、role、过期时间的token字符串返回前端。前端将token存进localStorage每次请求在请求头的Authorization字段带上。后端用一个拦截器解析token并把用户信息放入ThreadLocal或Request attribute里供Controller直接获取当前登录用户。这里有几个易错点token过期后前端要跳回登录页否则会一直报401管理员接口和普通用户接口要分别校验角色密码不要放token里只用userId和role就够了我写了一个简单的注解RequireRole(admin)配合拦截器做权限控制。代码大概长这样public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); Claims claims JwtUtil.parseToken(token); if (claims ! null) { request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } } response.setStatus(401); return false; } }实际使用时管理员接口还要在Handler里检测role是否为admin拦截器只能做登录校验不能完全代理角色校验因为角色不同响应不同。3.2 场地预约接口的设计与防重复提交预约接口是POST /api/order/create参数包括timeSlotId、venueId、price、payType。我在前端直接禁用了“正在提交”按钮但真正可靠的防重复依赖于后端幂等。我的做法是前端生成一个requestId第一次请求时后端查Redis或者表里是否已有相同requestId如果有就直接返回上一次的结果。但毕设没引入Redis所以我在Order表中增加了request_id字段且加上唯一索引。这样重复提交时数据库层面就会报唯一索引冲突Service层捕获DuplicateKeyException返回“请勿重复提交”。另一个接口是取消预约。取消时需要校验订单状态释放时段。我的经验是“先更新订单状态再释放时段”并且把两步放在同一事务里。代码里的顺序很重要如果先释放时段再改订单状态一旦第二句失败时段已经被释放就可能出现数据不一致。时段查询接口返回的是所有时段前端根据时段状态渲染“可预约/已约满/暂停”。为了让查询高效我用了一条联表SQL将venue和time_slot关联并返回。这里没做分页因为场地数量一般少于50个一次全量返回没啥问题。3.3 管理端统计与可视化数据接口管理端首页需要一个看板展示近7日预约量、热门场地Top5、用户增长趋势。这些数据不能直接写死要写统计接口。我做了三个接口GET /api/admin/stats/overview返回今日预约数、总用户数、总场地数、今日营收GET /api/admin/stats/trend?days7按天统计预约订单数GET /api/admin/stats/hot-venues统计每个场地被预约次数降序取前5趋势接口的关键SQL是按日期分组去重统计。因为order表里create_time是datetime去掉时间部分用DATE(create_time)分组。对于0记录的日子前端自己补齐。如果希望后端补齐可以在Java里遍历日期列表填充0。我选择前端补齐因为前端维护一个固定日期数组更简单。统计接口要控制权限不然任何登录用户都能看数据了。我这里在Controller上加自定义注解RequireRole(admin)并在WebMvcConfigurer中注册拦截器把/admin/**路径全部拦截校验角色。4. 前端实现VueElement UI 从页面到交互4.1 路由与权限拦截前端用的Vue 2 Element UI AxiosVue Router负责路由管理。路由表分成两部分公共路由如登录页、注册页、场地列表、场地详情需要登录后才能访问的如“我的订单”、“个人信息”以及管理员专属的“场地管理”、“预约管理”、“数据看板”。我在路由的meta字段里设置了requireAuth和requireAdmin在全局前置守卫里判断router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requireAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }); } else if (to.meta.requireAdmin token) { const role localStorage.getItem(role); if (role ! admin) { next({ path: /403 }); } else { next(); } } else { next(); } });这里要注意如果直接访问admin路径但用户是普通用户要跳到403页面而不是首页这样交互更友好。Axios拦截器统一处理后端返回的401状态然后清除本地token并跳转登录页。这个机制我一开始没做后来测试中token过期后所有请求都失败才补上的。4.2 预约表单的校验与交互细节预约表单涉及场地选择、日期选择、时段选择。用户选了一个日期前端要请求对应时段列表并过滤掉已约满和暂停的时段。时段组件用el-radio-group选择每个时段显示“开始时间-结束时间”如果状态不可用就disabled。表单校验不能只在后端做前端也要限制。比如用户必须选日期必须选时段手机号格式校验。Element UI的el-form自带校验规则我配置了日期必选和开始时间必选用自定义校验检查手机号。有一个细节用户选择日期时应该只允许选择今天及以后。我用disabled-date属性控制。同时如果某天场地维修管理员已经设置了当天时段全部停用用户看到就是空白或“暂无可预约时段”。提交时会调创建订单接口成功后跳转“我的订单”页面。这里要注意提交按钮的loading状态点击后立即置为loading避免双击产生两个订单。请求返回后无论成功失败都要关闭loading同时用message提示结果。“我的订单”页面我用tabs区分“待支付/已确认/已完成/已取消”。每个订单卡片显示场地名、日期时段、金额、状态、操作按钮。操作按钮根据状态显示待支付显示“取消订单”已确认显示“取消预约”已取消不显示操作。取消前还要二次确认Element UI的MessageBox.confirm很适合。4.3 管理员看板与图表的展示图表我用ECharts。安装echarts依赖引入后用init初始化。看板页面有四个统计卡片和一个折线图、一个饼图。折线图显示近7日预约量趋势X轴是日期Y轴是订单数。后端返回的是一个数组前端先构造一个长度为7的日期数组然后遍历后端返回的数据如果某天没有数据则填充0这样曲线不会断开。饼图显示热门场地预约占比。后端返回场地名称与预约数量前端用饼图组件渲染。颜色可自定义但不要用太花哨的配色保持清爽。管理员还需要处理预约审核。因为我的平台设计了两种模式一种是“预定即确认”另一种是“管理员审核”。毕设里我选择了用户预约后状态是待审核管理员在预约管理页面看到新订单点击“通过”后订单状态变为已确认同时时段锁定。如果管理员点击“拒绝”则释放时段用户订单状态变为已取消。这个流程更完整答辩时能讲的点也多。5. 部署上线从本地跑通到服务器部署5.1 环境准备与打包步骤本地开发完成之后最实在的事就是把它部署到一个可以在公网访问的服务器。我用了一台轻量云服务器系统是Ubuntu安装了JDK 1.8、MySQL 5.7、Nginx。部署步骤如下先在后端项目里配置生产环境的application-prod.yml关键内容是数据库地址、端口、JWT密钥等。然后用Maven打包mvn clean package -DskipTests打包后在target目录会生成一个jar文件。把jar传到服务器运行nohup java -jar venue.jar --spring.profiles.activeprod venue.log 21 需要注意的一点是服务器上MySQL的root账号密码、数据库名、时区都要和配置一致。我踩过最常见的坑是时区问题服务器默认时区是UTC而数据库连接串没有加serverTimezone导致插入订单时间比北京时间少了8个小时。解决办法是在JDBC连接串里明确加上serverTimezoneAsia/Shanghai。前端打包更简单。修改前端api.js里的baseURL为服务器IP或域名然后运行npm run builddist目录生成后上传到服务器的某个静态目录比如/opt/venue/dist。然后配置Nginx把前端静态文件和服务端API请求分开。5.2 Nginx SpringBoot 的部署配置Nginx的作用有两个一是托管前端静态文件二是反向代理后端API。我的配置如下server { listen 80; server_name your_domain_or_ip; root /opt/venue/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }这段配置里有几个关键点。一是location /api/的proxy_pass末尾带不带斜杠有区别我写的是带/api/的完整路径意思是将请求原样转发到后端的/api/路径下。如果proxy_pass只写到http://127.0.0.1:8080那么请求会去掉/api/前缀再转发两种方式必须清楚。二是try_files $uri $uri/ /index.html是为了支持Vue Router的history模式。如果用了hash模式则不用处理但history模式更美观我用history模式所以必须加这一行否则刷新页面遇到404。部署后还要开放服务器的80和8080端口安全组规则。我在云控制台把8080对公网开放了但这其实有风险因为后端接口没有做IP限制。实际上只要Nginx能访问8080就行公网没必要开放8080。正确做法是只开放80和443把8080留在内网供Nginx代理访问。我当时图省事直接开了后面收到一堆扫描日志才赶紧关上。同学们最好一开始就按最小权限原则来。6. 常见问题与避坑实录6.1 数据库连接和时区问题这是新手最容易碰到的。SpringBoot连接MySQL时如果URL里没写时区启动可能报错或者数据时间少8小时。后来我统一在URL里加了jdbc:mysql://localhost:3306/venue?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai同时确保数据库连接池的初始化参数initializationMode、validationQuery不影响启动。MySQL驱动版本如果和数据库版本太旧也会报SSL警告可以在URL末尾加上useSSLfalse。6.2 跨域拦截与CORS配置本地开发时前端在8081端口后端在8080端口浏览器直接发送请求会被CORS拦截。我的处理方式是在后端写一个CorsConfig类实现WebMvcConfigurer重写addCorsMappingsOverride public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true); }注意当登录接口使用Authorization请求头时allowedHeaders必须包含Authorization。使用allowCredentials(true)时allowedOrigins不能直接用得用allowedOriginPatterns()否则会报错。这段配置我调试了半天才弄明白。6.3 前端打包后访问404问题第一个问题是前端打包后直接双击index.html打开是一片空白因为静态资源路径是绝对路径。解决方法是修改vue.config.js里的publicPath为./这样打包后的index.html引用资源就是相对路径。第二个问题是在服务器上Nginx部署后刷新某个子路径页面出现404。原因前面说了Vue Router的history模式需要Nginx配置try_files。如果不想改Nginx也可以把Vue Router改成hash模式URL里会多一个#不美观但省心。我最后选择了historytry_files一劳永逸。6.4 预约并发超卖问题毕设答辩时老师最关心的问题通常是两个用户同时点击预约同一个时段会不会都成功我最初用普通查询判断先查时段状态如果为空再插入订单这样在并发时会出现两个事务都查询到时段可用然后都插入订单导致超卖。解决办法是在创建订单前先让时段表更新状态并返回受影响行数。但更本质的做法是对time_slot的id设置乐观锁版本号更新时带版本号判断。由于我的时段表本身有status字段我直接在更新语句里加上status限制UPDATE time_slot SET status 1 WHERE id ? AND status 0如果update返回0说明已被占用。这个条件更新解决了并发问题。配合订单表的time_slot_id唯一索引双保险。如果有条件再用Redis分布式锁毕设这个量级完全没必要。这里还想分享一个事务嵌套的坑。我一开始把时段更新和订单创建写在了同一个Service方法里结果发现事务有时不回滚。排查后发现是因为方法内部调用了同类中的另一个Transactional方法而Spring默认的事务代理是通过AOP生成的同类调用不会经过代理导致第二个事务没有生效。解决方案是把实际事务逻辑拆到不同Service类里或者干脆把两段逻辑写在一个事务方法里不要用自调用。最后再分享一个实用小技巧整个项目做完后我最想建议的是无论你的毕设课题是什么一定要先做出一版能跑通的“最小闭环”再慢慢加功能。我第一版只做了用户登录和场地列表预约功能先写了前端假数据后端接口用Mock数据整个流程通了才去接数据库。这样的好处是能尽早暴露架构问题不至于到最后一周发现数据库表关联设计错误手忙脚乱改代码。另外一定要把自己的项目录成视频或者保存好演示动图。答辩时如果现场网络故障或环境起不来一段录像就能救命。部署文档里我会附带一条启动顺序先启动MySQL再启动后端jar最后启动Nginx。如果能用脚本一键启动体验会更好。关于论文部分不需要写成产品说明书。把“需求分析-总体设计-详细设计-系统实现-测试”这条线串起来重点讲清预约冲突的处理、状态机设计、权限控制这三块再附上几张核心页面截图和核心表结构论文自然就丰富起来了。我的源码、sql脚本和部署文档都在本地留着后续如果有时间我还打算把微信小程序端做出来和现有Web端共用一套后端接口这样项目又能更进一步。如果你也在做类似的预约类毕设遇到具体问题欢迎按项目标题里的思路去复现踩过这些坑之后你会对整个全栈项目有更深的体会。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑