尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot+Vue健身房私教预约系统设计与实现:从表结构到部署全解析

SpringBoot+Vue健身房私教预约系统设计与实现:从表结构到部署全解析 在做这套SpringBootVue的健身房私教预约系统之前我其实已经接手过好几个类似的管理系统项目但私教预约这个场景有个很特殊的地方它不仅是简单的增删改查还涉及排期冲突、状态流转、教练与会员的双向权限以及前台展示与后台管理的双重逻辑。用Excel排课的时代早就过去了微信群接龙约课更是灾难现场。所以当朋友找到我帮忙做一套预约系统时我几乎没有犹豫就选了SpringBootVue这套组合前后端分离一套代码管会员端和教练端后台再用一个管理界面兜底。这篇博文就把整个设计与实现过程完整拆一遍从选型理由、表结构设计、后端核心接口、前端页面逻辑到联调阶段的坑和部署方案全部讲透。1. 为什么选SpringBootVue这套组合选型背后的真实考量很多初学者拿到这种项目第一反应是“这不就是个普通的管理系统吗”然后直接套用现成的后台模板开写。但健身房私教预约的场景和普通的进销存系统完全是两码事选型如果不对后面开发到一半就会非常痛苦。1.1 后端为何是SpringBoot而不是SSH或Node我早期也写过SSHSpringStrutsHibernate的老项目说实话那个时代已经过去了。SpringBoot最大的价值在于“约定大于配置”不需要再写一堆XML配置文件。以这个项目为例我需要集成MyBatis-Plus操作MySQL、集成JWT做登录鉴权、集成Redis做缓存在SpringBoot里这些都是通过starter依赖一次性引入的几分钟就能把基础骨架搭起来。还有一个很现实的考量这套系统最终要部署在朋友的健身房前台一台普通Windows电脑上运维能力几乎为零。SpringBoot打成jar包直接用java -jar就能跑不需要额外装Tomcat这对没有专职运维的小型健身房来说非常友好。这里也要特别提一句版本选择。我见过很多人一上来就跟着官网下最新版SpringBoot结果踩了一堆坑。当前最新的大版本需要JDK 17或者更高版本而很多小型健身房已有的服务器上装的是JDK 8如果强行上高版本光是升级JDK就够折腾半天。所以我用的是SpringBoot 2.7.18这个版本稳定、资料多、网上踩坑案例也容易搜到完全够用。1.2 前端为何是Vue而不是React或Angular前端这块Vue的最大优势是上手门槛低模板语法直观组件化开发方式也清晰。做私教预约系统这种以表单、列表、日历为主的项目Vue的响应式数据绑定能让开发效率提升非常明显。具体到技术栈我选的是Vue 3 Vite Element Plus Pinia Vue Router。Vite的启动速度比Webpack快了一个量级尤其在开发阶段改代码热更新几乎是一瞬间的事。Element Plus弥补了Vue 3生态里UI组件库的空白表格、表单、弹窗、日期选择器这些现成组件直接拿来用不用自己写样式。有人可能会问为什么不用Vue 2EOL之后Vue 2已经停止维护了新项目再入坑Vue 2属于给自己埋雷。而且Vue 3现在生态已经非常成熟网上资料和踩坑案例也很多这套系统我从选型到上线完全没有因为Vue版本问题卡过壳。1.3 前后端分离方案的明确收益分离架构在这个项目里的收益不是“潮流驱动”而是实际业务倒逼出来的。健身房私教预约系统至少有三个端会员端小程序或网页、教练端看自己的排期和约课记录、管理后台管理员维护教练、课程、会员、统计报表。如果套一套传统模板发动机渲染后端写Controller的同时还要套模板页面三个端各写一套模板代码重复度太高了。前后端分离后后端只负责提供RESTful API前端三个端共用同一套接口。会员端我做的是H5页面教练端和管理后台分别建了不同的路由和权限控制但调用的接口是一样的。后端工程师只需要把接口定义清楚三端各自对接互不阻塞项目整体进度快了不少。2. 核心数据模型设计预约流程背后的表结构逻辑这个项目的数据模型我花了一周时间反复推敲。表面上看就是几张表但预约这个动作牵涉到用户、教练、课程、排期、预约记录、支付记录等多方数据表结构设计得不好后面写SQL的时候就会不停返工。2.1 围绕“人、课、时、约”四要素建模我习惯在建模前先把业务实体理清楚。这个系统的核心实体是“人”“课”“时”“约”四类人包括会员、教练、管理员三种角色课是指私教课程时是指教练的具体可约时间段约则是会员对某个时间段的预约动作。基于这个思路我设计了六张核心表用户表user、教练信息表trainer、课程表course、排期表schedule、预约表appointment、订单表orders。用户表里通过role字段区分身份教练信息表单独存教练的特长、简介、头像、评分等展示信息通过user_id和用户表关联。这里有一个设计细节用户表和教练信息表为什么要拆开因为登录用的是用户名、密码、手机号、角色这些通用字段而教练有自己独特的业务属性。如果全部塞同一张表会员记录里会出现大量空字段而且后续如果给教练加“从业年限”“资格证书”等字段又要改表结构。拆开之后扩展性就好很多。2.2 教练排期表与预约记录表的状态设计排期表schedule是整个预约系统的核心。我的设计是这样的每条记录表示“某个教练在某个时间段可以上某门课”字段包括trainer_id、course_id、start_time、end_time、max_members、booked_count、status。这里的status我设计了三个值0表示可预约1表示已约满2表示已锁定比如教练临时请假。每次成功预约一个名额就检查booked_count是否超过了max_members没有超过才允许下单然后booked_count加一。预约表appointment记录会员和排期的对应关系字段包括member_id、schedule_id、status、create_time。这里的status状态机是整个系统的灵魂我设计了四个状态0待上课1已完成2已取消3已爽约。会员取消预约只在“待上课”状态下允许课程时间到了之后系统自动把待上课改成已完成如果会员没去也没提前取消就记为爽约。状态机设计清晰之后所有接口逻辑都变得非常简单和不容易出错。比如会员端“我的课程”列表只需要查appointment表里member_id等于当前用户、status等于0或1的记录然后join排期表和课程表把课程名称、教练名称、上课时间展示出来即可。2.3 数据一致性与防超卖设计私教预约系统最核心的并发问题是“防超卖”。一个畅销教练的黄金时间段比如晚上7点到8点可能同时有好几个会员在抢如果并发控制做得不好就会出现两个会员同时预约成功的情况这在真实运营中就是客诉事故。我的方案是数据库唯一索引加事务控制。具体来说给排期表加一个“实际已预约数”的字段每次插入预约记录时在事务里同时更新排期表的已约人数。在更新语句里带上一个条件UPDATE schedule SET booked_count booked_count 1 WHERE id ? AND booked_count max_members如果受影响的行数为0说明已经满了事务回滚预约失败。这个方案比纯应用层加锁更可靠因为数据库的行锁天然保证了并发下的正确性。当然如果预约量再大一个量级也可以考虑用Redis的Lua脚本做分布式限流但中小型健身房预约量根本到不了那个瓶颈数据库方案简单可靠运维成本为零。需要注意的是这里要配合事务一起使用。我虽然用的是MyBatis-Plus但预约和扣减已约数这两个操作我并没有用他默认的单方法事务而是手动在Service层加了Transactional注解。默认的Spring事务管理器在单数据源场景下完全够用不需要引入分布式事务框架。3. 后端核心模块实现从登录鉴权到预约状态机后端实现我按业务模块拆成了五个部分认证模块、会员模块、教练模块、预约模块、管理后台模块。其中认证模块和预约模块是最核心的也是逻辑最复杂的部分。3.1 JWT鉴权与拦截器设计登录鉴权我用的JWT方案而不是传统的Session方案。核心原因有两个一是前后端分离架构下Session的跨域处理比较麻烦需要额外配置CORS的allowCredentials属性和Cookie的SameSite属性稍微不注意就踩坑二是JWT本身携带用户信息后端服务不需要存储会话即使以后想把接口开放给微信小程序也可以无缝对接。JWT的生成逻辑很简单用户调用登录接口后端校验用户名密码通过后用用户的id、角色、过期时间生成一个token字符串返回给前端。这里的token里塞的信息我比较克制只放了userId和role两个字段不存敏感信息因为JWT的payload是Base64编码的任何人都能解码看内容。拦截器方面我写了一个JwtInterceptor实现HandlerInterceptor接口在preHandle方法里从请求头中取出token调用JwtUtil工具类验证签名和过期时间验证通过就把userId和role放到ThreadLocal里方便Controller层直接获取当前用户。配置类里通过addInterceptors方法注册拦截器同时用excludePathPatterns放行登录、注册、获取课程列表等不需要鉴权的接口。这里有个细节值得分享JWT的过期时间我设置为7天过长会导致安全风险过短会影响会员使用体验三两天就要重新登录一次。7天对私教预约场景来说是比较合适的折中方案。后续如果要优化还可以引入Redis做token黑名单用户主动退出登录时把token加入黑名单这样即使token未过期也无法继续使用。3.2 预约与取消预约的核心接口逻辑预约接口是整个系统里代码量最多、逻辑最复杂的部分。我贴一下核心代码的伪代码逻辑Transactional(rollbackFor Exception.class) public void book(Long memberId, Long scheduleId, Long courseId) { // 1. 检查排期是否存在以及是否可预约 Schedule schedule scheduleMapper.selectById(scheduleId); if (schedule null || schedule.getStatus() ! 0) { throw new BusinessException(该时段暂不可预约); } // 2. 检查是否已经预约过这个时段 Long count appointmentMapper.selectCount( new LambdaQueryWrapperAppointment() .eq(Appointment::getMemberId, memberId) .eq(Appointment::getScheduleId, scheduleId) .ne(Appointment::getStatus, 2)); // 不包括已取消的 if (count 0) { throw new BusinessException(您已预约过该时段); } // 3. 插入预约记录 Appointment appointment new Appointment(); appointment.setMemberId(memberId); appointment.setScheduleId(scheduleId); appointment.setStatus(0); appointmentMapper.insert(appointment); // 4. 已约数1注意这里的乐观锁条件 int rows scheduleMapper.updateIncreaseBookedCount(scheduleId); if (rows 0) { throw new BusinessException(手慢了该时段已被约满); } // 5. 生成一条订单记录初始状态为待支付这里不做强制支付仅归档用 ordersMapper.insert(...); }第4步的“已约数1”我用的是MyBatis-Plus的Mapper里自定义SQL方式实现的而不是先查询再更新。因为先查再更新存在竞态问题两个请求同时查到booked_count为1max_members为2都认为可以预约然后各自1最后就变成了3个人都约上但容量只有2的BUG。用一条带条件的更新语句数据库的行锁会保证只有一个请求更新成功这样就把并发问题交给数据库去处理了。取消预约的逻辑相对简单检查当前预约状态是否为“待上课”如果是更新预约状态为“已取消”同时排期表的booked_count减一。另外我加了一个限制开课前2小时内不允许取消避免教练已经到店准备上课却突然被放鸽子。这个限制是在业务层做的如果要更严谨也可以用定时任务扫描所有状态为“待上课”但开始时间已过的预约自动更新为“已爽约”。3.3 教练端排期管理接口教练端接口主要解决两个问题教练看自己有哪些排期和预约教练手动设置自己的可约时间段。关于排期管理我并没有让教练自己在页面上复杂地创建时间段而是做了一个还算实用的设计教练可以选择某一天然后批量生成多个固定时间段比如18:00-19:00、19:00-20:00每个时间段生成一条排期记录。这样既方便了教练操作也保证了数据的最小粒度是一条排期后续预约流程才能正常关联。后端接口设计上排期批量生成的接口接收一个DTO包含日期、开始时间、结束时间、课程ID。代码里循环遍历教练配置的每个课段时间生成对应的排期记录同时检查是否有时间重叠。避免一个教练在同一时间段存在两条排期记录。管理后台的接口就更常规了会员管理分页查询、禁用/启用账号、教练管理审核入驻、修改信息、课程管理增删改查、订单管理查询退款。这部分本质上是标准的管理系统CRUDMyBatis-Plus的IService接口自带的分页能力直接用就行主要精力花在数据校验和字段权限控制上。4. 前端核心页面拆解从登录到扫码签到的完整链路前端部分我按页面模块来拆解。整个前端工程包括会员端和管理端两块共用一个项目仓库通过路由做区分/ 前缀是会员端页面/admin 前缀是管理后台页面。4.1 技术栈与目录结构前端技术栈是Vue 3 Vite Vue Router Pinia Element Plus Axios。目录结构上我按“页面组件-业务逻辑-接口调用”三层来组织views目录存页面组件按模块分文件夹比如views/member、views/trainer、views/adminapi目录存接口调用封装每个模块一个js文件比如api/member.js、api/schedule.jsstores目录存Pinia状态管理目前主要存用户登录信息和tokenrouter目录存路由配置配置了全局前置守卫做登录校验状态管理和接口封装是容易被新手忽略的部分。状态管理如果只用来存用户信息看起来有点大材小用但在应用里会有多个组件同时需要判断“当前是否是管理员”“当前用户ID是多少”的场景用Pinia集中管理会方便很多。接口封装层则是所有页面和后台通信的唯一通道统一处理token注入、错误提示、请求超时等逻辑。4.2 预约日历与教练卡片设计预约页面的核心交互是“选教练→选时间→确认预约”三步。教练列表页展示教练的头像、姓名、擅长领域、课程价格、综合评分数据通过调用后端的教练列表接口获取前端用Element Plus的Card组件渲染成卡片。用户点击教练卡片后进入该教练的预约详情页。详情页的核心是一个周视图日历组件这里我没有直接用第三方日历库而是自己写了一个简单的周视图组件。周一至周日横排展示每个时段一格格子的颜色表示可约绿色、已约满灰色、不可约橙色。用户点击绿色格子后弹出确认弹窗显示课程名称、上课时间、价格确认后调用预约接口。自己写周视图组件的成本比预想的高一些但换来的是完全可控的交互体验也不用担心第三方库的样式与项目风格不搭。如果你不想自己写可以考虑fullcalendar这个库它支持月视图、周视图、日视图但定制程度相对有限。前端还有一个值得说的页面是“我的预约”。会员在这里看到自己所有预约记录按状态分为“待上课”“已完成”“已取消”三个Tab。每条记录展示课程名称、教练、上课时间、预约时间、操作按钮取消预约/去评价。这个页面的数据来自后端预约列表接口前端根据status字段做分组展示。4.3 会员端与教练端的权限控制前端权限控制的核心是路由守卫。我的逻辑是这样的router.beforeEach里判断目标路由是否需要登录权限如果需要就去Pinia store里取token没有token就跳转到登录页并记录来源路径。登录成功后跳回来源路径体验比较流畅。对于管理后台的路由我加了一道管理员的校验。如果当前用户的role不是1管理员访问/admin下的任何页面都会被重定向到404页面。这样的控制虽然简单粗暴但非常有效避免了很多“无权限却看到管理界面”的尴尬情况。菜单展示上不同角色的侧边栏菜单不同。管理员看到的是“会员管理、教练管理、课程管理、排期管理、订单管理、统计报表”教练登录后自动跳转到“我的排期、我的预约”页面。我也是在路由里配置了meta.roles字段菜单组件根据当前用户的角色过滤渲染。4.4 打包部署与vue路由模式配置前端开发和部署中有几个非常容易踩坑的点。首先是路由模式Vue Router默认是hash模式URL里带个#号不太好看而且部分场景下会导致微信内分享链接参数丢失。所以我改成了history模式URL干净美观。但history模式有个致命问题前端路由路径在服务端没有对应文件用户直接刷新某个子页面时会出现404。解决方式是让nginx把所有请求都转发到index.html由前端路由自己处理。nginx配置里我加了这样一段location / { try_files $uri $uri/ /index.html; }这样用户不管是刷新还是直接输入子页面链接都能正常访问。第二个坑是静态资源路径。Vue项目build时使用的绝对路径 /assets/... 在部署到服务器子目录时会导致资源找不到起来的页面是全白的。解决方式是在vite.config.js里设置base属性。如果部署在域名根目录就写./如果部署在子目录就要写对应路径。Element Plus按需引入的问题也值得一提。如果直接用全量引入打包后的js文件能达到一两MB首屏加载会慢。我用了unplugin-auto-import和unplugin-vue-components这两个Vite插件做按需引入只打包用到的组件体积能缩小一半以上。5. 联调阶段的坑前后端分离项目最容易翻车的几个细节整个项目我最想分享的是联调阶段踩到的一堆问题。前后端分离开发时前后端各自开发各自调试没问题但一旦两个端凑在一起联调各种问题就开始暴露了。这里挑几个典型的坑细讲。5.1 跨域问题的标准解法与一个反直觉细节第一个坑就是跨域。我在开发环境用的是Vite的代理配置把前端开发服务器上的 /api 请求转发到后端的8080端口。这个方案在开发阶段基本不会出问题。但到了生产环境前端部署在nginx的443端口后端跑在服务器的8080端口浏览器直接发请求的话必然会跨域。解决方式我选择了后端开启CORS写一个WebMvcConfigurer配置类重写addCorsMappings方法。需要注意的一个反直觉细节allowedOrigins不能写成 *因为你一旦用了浏览器会拒绝携带凭证的跨域请求而我们的请求头带着token如果token通过Authorization头传递通常问题不大但如果涉及Cookie必然失败。正确的做法是写成具体的域名列表。还遇到过一个问题CORS请求的预检测OPTIONS请求返回404。原因是后端的安全拦截器把OPTIONS请求也拦截了导致预检测请求根本没到达Spring MVC的CORS处理器。后来我在拦截器里加了个逻辑如果是OPTIONS请求直接放行跨域响应头由Spring自动处理。5.2 axios拦截器与401状态处理axios拦截器是所有前端请求的统一出口把token注入逻辑和错误处理逻辑集中在里面。我在请求拦截器里从Pinia store里取token加到请求头Authorization字段这样每个接口都自动携带了身份信息。响应拦截器的核心是两个逻辑一是业务码校验后端接口统一返回{ code, message, data }格式判断code是否等于200不为200就使用Element Plus的Message组件弹出错误提示二是HTTP状态码为401时的处理说明token过期清掉本地的用户信息和token跳转到登录页。有一点需要特别留意后端鉴权失败时返回的HTTP状态码到底是401还是200前后端一定要提前对齐。有些团队喜欢不管成败都是HTTP 200然后靠业务码字段区分。我个人建议401的语义就是401HTTP状态码和业务码各司其职。因为像nginx这样的反向代理层会处理HTTP状态码如果鉴权失败也算200代理层就无法根据状态码做缓存和日志统计。5.3 时间字段格式化的血泪教训这个坑是隐藏最深的。后端实体类里我用的LocalDateTime类型前端展示时发现时间格式是2024-01-15T18:00:00这种带T的格式而业务上需要的是2024-01-15 18:00:00这种人类友好型的格式。原因很简单SpringBoot默认的Jackson序列化LocalDateTime时用的是ISO标准格式带T字符。我当时一度想去每个前端页面手动格式化时间后来想想不对劲如果一个项目要手动的地方变多了一定是通用方案出了问题。最后我在后端加了统一配置在application.yml里配置了Jackson的日期格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8但这样配置完发现LocalDateTime还是带T格式。后来排查发现LocalDateTime的序列化格式matter是不走spring.jackson.date-format配置的需要单独给LocalDateTime添加注解或者注册自定义序列化器。我用的是在系统里注册一个Jackson的自定义JavaTimeModule统一修改LocalDateTime和LocalDate的序列化格式把问题一次解决。时间时区的问题也要注意数据库连接串上加上了serverTimezoneAsia/Shanghai否则数据库连接默认时区和本机时区不一致存进去的时间就会差8个小时。6. 从开发到上线部署方案与后续扩展思路这个系统从开发到上线用了不到一个月的时间部署方案也比较简单但结构和扩展性都是经过考量的。6.1 一套简单可靠的部署拓扑我的部署架构是这样的一台2核4G的云服务器装了MySQL 8.0、Redis 7.0和Nginx。SpringBoot后端打成jar包用systemd服务托管开机自启日志输出到文件方便排查问题。前端Vue项目打包成静态文件由Nginx直接服务。部署时有一个容易被忽略的问题服务器上MySQL的字符集默认可能是latin1存中文会乱码。所以建库时我明确指定了utf8mb4字符集。这个字符集不仅支持中文还支持emoji表情和生僻字现在的新项目基本上都应该用utf8mb4。另外后端连接数据库的配置里不能明文写密码。我用了环境变量的方式启动命令里通过--spring.datasource.password${DB_PASSWORD}传入这样即使有人看到进程列表也不会暴露密码。对于这种小型项目来说不需要上Spring Cloud Config这种重量级配置中心环境变量方案已经够用。6.2 后续功能扩展方向系统上线后我陆续收到一些反馈和需求。比较集中的几个方向是在线支付、微信小程序端、消息通知、课程评价体系。在线支付目前用的是“上课时到店前台支付”的方式接微信支付或支付宝支付接口的代码结构我已经留好了订单表里已经预留了pay_status和transaction_id字段后续要接第三方支付只需要在支付模块添加一个适配层即可。消息通知方面目前会员预约成功或取消时只是站内消息在“我的预约”里展示后续可以接入企业微信机器人或短信平台在关键节点给会员推送提醒减少爽约率。课程评价体系目前只有评分字段没有评价内容。后续可以做成交互式评价组件让会员对课程体验、教练服务、场地环境三个维度分别打分再把评价数据汇总到教练详情页展示形成教练之间的良性竞争。整个项目的代码量不算大核心逻辑就集中在预约冲突处理、状态机设计和权限控制三块。把这几个核心问题想清楚SpringBootVue的组合做私教预约系统比想象中要顺利得多。如果看完这篇博文你也想自己动手做一套我的建议是先把表结构和状态机画清楚再写代码别看一步走一步那样后边改起来会非常痛苦。
返回列表