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

资讯详情

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

SpringBoot健身房预约系统:权限设计、状态机与支付宝沙箱实战解析

SpringBoot健身房预约系统:权限设计、状态机与支付宝沙箱实战解析 简介基于Java SpringBoot与MySQL的健身房在线预约管理系统源码面向Java Web学习者、课程设计及毕业设计人群提供一套多角色、业务闭环完整的项目参考。系统分管理员、职工、教练、前台会员四类角色功能覆盖会员管理、项目预约、健身百科、器材管理、请假反馈、活动发布、支付宝沙箱支付等适合需要快速上手SpringBoot整合开发或进行二次扩展的开发者。压缩包内共2000个文件约53.17MB以Java源码、class编译文件、js/css/html前端页面、sql数据库脚本及png/gif/jpg图片素材为主同时包含ftl模板、properties/xml配置等目录结构完整便于导入IDE运行调试。已有915人学习下载下载后可获得完整项目代码与数据库脚本工程内已封装系统控制、人员服务、支付日志等核心类模块划分清晰能有效帮助理解预约流程、权限控制与支付对接的实现思路。1. 为什么一个健身房预约系统值得你拆开看很多入行两三年的后端开发拿到“健身房预约管理系统”这类项目第一反应是“无非就是 CRUD”。实际拆开源码会发现它比表面复杂得多角色有四类权限粒度不同预约涉及教练和项目双重库存充值走支付宝沙箱回调要验签、要幂等会员请假还会触发课程名额回补。这套逻辑几乎覆盖了中小型业务系统最常见的状态机、数据权限和支付对账三个难点。适合正在准备 Java 后端面试、想补项目经验的开发者也适合刚接手 SpringBoot Maven MySQL 技术栈的团队用来做内部培训基线。接下来的内容以我拆这个项目时的顺序展开先看数据模型和 Maven 结构再跟着启动步骤跑起来然后逐段分析预约、充值、请假的状态切换最后给出支付宝沙箱的接入细节和几个容易踩的坑。2. 从业务拆分到数据模型SpringBootMavenMySQL 的选型依据2.1 角色权限边界如何映射到表结构项目里有管理员、职工、教练、前台用户四个角色。起初我以为是常见的 user 表 role 字段但打开实体类后发现采用的是“用户—角色—菜单”三层权限模型。People 类里没有直接写 role而是关联了角色实体再由角色关联菜单表。这样做的好处是管理员可以在后台动态增删菜单而不用改代码。核心表大概有这些people用户、role角色、menu菜单、people_role用户角色关联、item健身项目、course_reservation教练预约、member_card会员卡、recharge_record充值记录、pay_order_log支付日志、leave_record请假记录。其中 pay_order_log 这个类既是支付流水也充当幂等表后面接入支付宝时会重点用它。PeopleController 和 SystemController 的代码里几乎每个接口都先取当前登录用户再判断是否拥有某菜单权限。这种设计比 PreAuthorize(“hasRole(‘admin’)”) 这种硬编码方式更灵活。常见做法是登录后把用户拥有的菜单权限码放进 session 或 Redis接口层用一个拦截器统一校验。这个项目的拦截器在 CpachaUtil 生成验证码之后介入实际渲染前做权限过滤。2.2 表关系设计中的关键约束看 PeopleController 里的 save 方法发现新增职工时强制校验账号唯一数据库层也建了唯一索引。双保险是必要的因为并发请求下先查后插会有竞态窗口。另一个关键约束是预约表的外键course_reservation 同时引用了 people教练和 people会员要做两个单独的关联列而不是简单的一个 user_id。请假表 leave_record 与预约表的关系比较隐蔽请假不是直接删除预约而是修改预约状态同时把对应时段的项目库存加回。所以业务上“请假成功”依赖事务代码里用 Transactional 把状态更新和库存回补绑在同一个方法里。如果缺了事务回补错乱后会出现预约人数超过项目容量的问题。下面用一张表概括各表之间的核心关联表名关键外键业务含义peoplerole_id用户归属角色item无健身项目基础信息course_reservationpeople_id, coach_id, item_id一次预约关联会员、教练、项目pay_order_logpeople_id, recharge_record_id支付流水与充值单关联leave_recordcourse_reservation_id请假针对某条预约2.3 Maven 依赖与项目骨架Maven 在这个项目里做得规整的一点是把测试依赖单独隔离主 pom.xml 只留 spring-boot-starter-web、mybatis-plus、mysql-connector-java、alipay-sdk、lombok。没有出现传统 SSM 项目里那种到处是 jar 包的混乱局面。推荐在 idea 里以 Maven 方式导入等待依赖下载完成后注意检查 spring-boot-maven-plugin 的版本与 JDK 8 是否匹配。如果本机 JDK 版本较高比如 17直接运行启动类会报“UnsupportedClassVersionError”。解决方案是安装 JDK8 并在 idea 的 Project Structure 里把 SDK 切换成 1.8。很多人卡在依赖下载慢的问题上我一般会在 settings.xml 里配置阿里云镜像。注意 mirrors 要写在 mirrors 标签内并且不要同时保留多个 active 仓库地址不确定的 mirror否则依赖版本解析会非常慢。3. 搭建与运行JDK8/MySQL5.7 环境下的落地步骤3.1 数据库初始化与配置文件项目里附带 sql 文件名字类似 gym.sql。用 MySQL5.7 导入时先建一个空库再执行 source 命令。注意字符集要统一成 utf8mb4否则后面支付宝回调里的中文备注会乱码。配置在 application.yml 里最值得关注的是数据源和 MyBatis 配置。贴一段我实际调整过的配置spring: datasource: url: jdbc:mysql://localhost:3306/gym_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true这段配置里 driver-class-name 用的是 com.mysql.jdbc.Driver这是 MySQL5.7 时期的写法。如果换成 MySQL8要改成 com.mysql.cj.jdbc.Driver并且 pom 里的依赖版本跟着换成 8.x。map-underscore-to-camel-case 开起来后数据库字段 user_name 能自动映射到实体属性 userName省掉一堆 resultMap。启动前还要确认 MySQL 的时区。常见报错是 “The server time zone value is unrecognized”这时在连接串上加 serverTimezoneAsia/Shanghai 即可。3.2 SpringBoot 启动参数项目里启动类就是标准的 SpringBootApplication。因为用了 JSP需要配置视图解析器。很多人在 idea 里直接跑 main 方法会发现访问页面 404原因是 JSP 文件没有编译到 classes 目录。先在 pom.xml 里确认 packaging 是 war再在 idea 的 Artifacts 配置里把 webapp 目录标记为资源目录。我的启动方式是先用 Maven 打包mvn clean package -DskipTests java -jar target/gym-0.0.1-SNAPSHOT.war-DskipTests 比 -Dmaven.test.skiptrue 更温和前者编译测试类但跳过执行后者连测试代码都不编译。如果改过了配置优先用前者能更早暴露语法问题。启动成功后日志里出现 “Tomcat started on port(s): 8080”说明端口正常。想要改端口在 application.yml 里加 server.port但注意项目里的 JSP 静态资源路径如果是绝对路径端口变了也不会影响功能。3.3 常见启动失败排查启动失败时先看最前面的异常而不是最后的堆栈。最频繁的三类第一数据库连不上。检查 MySQL 服务是否启动账号密码是否正确。第二MyBatis 映射文件绑定异常报 Invalid bound statement。这时检查 mapper 接口的包路径和 XML 的 namespace 是否完全一致。第三端口被占用。Windows 下用 netstat -ano | findstr 8080 找到 PID再在任务管理器里结束进程。还有一个隐藏坑项目里如果引入了 CpachaUtil 验证码工具类它依赖 Java 的 AWT 图形库。在 Linux 无桌面环境下跑会报 HeadlessException。解决办法是在启动参数里加 -Djava.awt.headlesstrueSpringBoot 的启动类里也可以手动设置 System.setProperty。4. 核心业务实现预约、充值、请假的状态机4.1 预约状态流转与并发控制预约状态我整理为四态待支付、已确认、已取消、已完成。前台用户选定教练和项目后先生成订单状态为待支付支付成功回调后变为已确认教练或管理员可以取消未开始的预约用户到店完成后由职工标记完成。代码层面最核心的是扣减可用名额时的 SQL 条件而不是 Java 的 if 判断。常见做法是让 Update 自带状态条件Update(UPDATE item SET reserved_count reserved_count 1 WHERE id #{itemId} AND reserved_count max_count) int incrReservedCount(Param(itemId) Long itemId, Param(maxCount) Integer maxCount);这段 SQL 的返回值如果为 0表示当前预约数已经达到上限直接抛业务异常提示“该时段名额不足”。用原子更新代替先查后插能避免两个用户同时拿到相同剩余名额的并发问题。编码层再根据返回值做回滚提示接口层捕获异常后返回统一的 JSON 错误码。在事务方法里调用这个 update 之后再插入 course_reservation 记录。因为我用的是 MyBatis 注解版没有额外的 XML业务逻辑注释写清楚“只有此时段有剩余名额才允许预约”。4.2 会员充值支付回调支付宝沙箱支付的回调地址必须是外网可访问的 URL。本地联调时我用的是内网穿透工具把本机 8080 端口暴露到公网。在支付宝开放平台沙箱环境里配置回调地址为 http://你的域名/支付宝/notify。这个地址对应项目里的 PayOrderLogController 或类似类中的回调方法。回调处理要分三步走。第一步验签用支付宝公钥对 POST 参数做 RSA2 验证。第二步查单调用 alipay.trade.query 确认订单状态确实是 TRADE_SUCCESS防止伪造通知。第三步幂等处理先查 pay_order_log 表看该笔 trade_no 是否已处理过处理过直接返回 success。伪代码结构大概是这样public String notify(HttpServletRequest request) { MapString, String params AlipaySignature.rsaCheckV1(request.getParameterMap(), alipayPublicKey, UTF-8, RSA2); if (!params.get(trade_status).equals(TRADE_SUCCESS)) return failure; PayOrderLog log payOrderLogService.getByTradeNo(params.get(trade_no)); if (log.getStatus() 1) return success; payOrderLogService.handlePaidOrder(params); // 更新充值单、会员余额 return success; }AlipaySignature.rsaCheckV1 是 alipay-sdk-java 里提供的方法参数顺序分别是请求参数 Map、支付宝公钥、字符集、签名算法。注意不要用应用私钥去验那是用来发起请求时签名的。4.3 请假逻辑与库存回补请假发生在已确认的预约上。用户提交请假后管理员审批。审批通过时不仅要改预约状态还要把对应项目的 reserved_count 减回来。项目组在这个方法上用了事务并结合乐观锁确保不会有误操作Transactional(rollbackFor Exception.class) public void approveLeave(Long leaveId, Long approverId) { LeaveRecord leave leaveMapper.selectById(leaveId); CourseReservation reservation reservationMapper.selectById(leave.getReservationId()); reservation.setStatus(4); // 已取消 reservationMapper.updateById(reservation); itemMapper.decrReservedCount(reservation.getItemId()); leave.setStatus(1); // 审批通过 leaveMapper.updateById(leave); }这里有一个容易忽略的问题如果用户已经在“待支付”状态提交请假会直接取消订单不涉及库存回补。所以在 approveLeave 里要先判断原始预约状态。回访信息管理也复用了这个状态机。职工对健身会员做回访后如果会员提到身体不适系统会生成一条新的建议记录关联到该会员的最近一条预约上。这类操作不像签到那样高频但必须要有事务因为回访记录和预约状态变更不能出现半成功。5. 教练端与前台用户的联动数据权限实战5.1 教练只能看自己的预约教练登录后健身会员管理、健身项目管理等模块理论上只能读不能改。这个项目没有专门的“教练角色菜单”限制读按钮而是在查询方法里强制拼接 coachId 条件。正常运行时教练端预约列表接口的查询条件是这样构造的QueryWrapperCourseReservation wrapper new QueryWrapper(); wrapper.eq(coach_id, currentUser.getId()); wrapper.orderByDesc(start_time); if (StringUtils.isNotBlank(status)) { wrapper.eq(status, Integer.parseInt(status)); }QueryWrapper 是 MyBatis-Plus 的条件构造器eq 方法对应 SQL 里的等值条件。这里的关键是 currentUser.getId() 必须来自 session 中的登录对象而不是块参数否则教练 A 能查到教练 B 的预约。管理员侧的查询则不带 coachId直接用 SqlRunner 或自定义 mapper XML 做连表查询把人物名称、项目名称映射进一个 VO。前端表格里展示的是 coachName 和 itemName而不是存尸体的 coach_id避免用户看到数字。5.2 前台用户个人中心接口前台会员的个人中心包含充值记录、预定记录、修改密码。这三个接口的共性是需要频繁使用当前登录人 ID所以项目封装了一个 BaseController里面提供 getLoginUser() 方法。在 Controller 中每次调用都要判空否则伪造的 session 会导致空指针。修改密码这个接口容易出安全漏洞。项目里做法是前端先把旧密码加密后端比对时再用 MD5 加盐方式匹配。CpachaUtil 本身负责生成验证码和密码加密是两个模块。预约记录列表需要区分“当前用户是会员还是教练”同一张表的数据通过查询条件切换即可。有些新开发的同学会把“我的预约”做成两张表这是错的。会员预约记录和教练被预约记录来自同一个表只是过滤字段不同。第七天的复盘里我把这部分设计单独画了一遍整个系统 80% 的业务都围绕这张表展开。前台还有一个举报功能反馈表 feedback 里通过 type 字段区分是“留言”还是“举报”。举报不会自动给管理员发消息而是进入反馈管理列表由管理员标记处理状态。这个实现相对简单但业务上值得注意举报内容必须做敏感词过滤否则会污染后台数据。6. 支付宝沙箱支付接入与排错技巧6.1 沙箱密钥配置支付宝沙箱环境需要先在开放平台生成一对应用私钥和公钥再把应用公钥上传换取支付宝公钥。这两个结果值都要配置到项目里。注意应用私钥是 PKCS8 格式开头是 “-----BEGIN PRIVATE KEY-----”而支付宝公钥是公钥不能填反。配置写在 application.yml 里alipay: app-id: 2021000123456789 private-key: | MIIEvQIBADANBgkqhkiG9w0BAQEFAAS...省略 alipay-public-key: | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...省略 notify-url: http://你的地址/支付宝/notify return-url: http://你的地址/支付宝/returnYAML 里的竖线符号用来保留私钥字符串里的换行。很多人的坑是直接把私钥粘贴成一行导致签名验签失败报 “sign check fail”。私钥内容必须包含换行符用上面这种格式最稳妥。6.2 回调验签与幂等回调通知会重复发送多次支付宝官方文档里写得很清楚只有在收到 success 字符串后才会停止通知。每次通知都走完整的验签和查单逻辑但落库前一定要用 trade_no 查 pay_order_log。如果该 trade_no 已经有成功记录直接返回 success不再重复加钱。幂等还有一种边界回调成功但查询接口超时。这时系统可能已经处理过订单再次收到回调会重复处理。所以幂等校验最好放在事务最前面并且用 synchronized 或数据库唯一索引做二次保障。推荐做法是在 pay_order_log 表的 out_trade_no 上建唯一索引插入时如果报 DuplicateEntry就按已处理返回 success。6.3 几个常见坑我在接入过程中踩过的坑有三个。第一个是回调验签失败检查是否把支付宝公钥填成了应用公钥两个公钥虽然都是字符串但长度和内容完全不同。第二个是沙箱账号无法支付确认使用的是沙箱买家账号而非真实支付宝账号。第三个是异步回调取不到参数原因是 SpringBoot 接收 POST 请求时用 RequestBody Map 会把参数放在 body 里而支付宝表单提交是 application/x-www-form-urlencoded应该用 HttpServletRequest 逐个 getParameter。另一个小技巧是沙箱支付的 returnUrl 同步跳转只做页面展示不能作为订单完成依据。所有资金变更必须以异步通知为准。我在项目里把 return 和 notify 分成了两个接口一个返回前端页面一个做业务处理。这样测试时也能手动模拟 POST 通知来验证幂等逻辑。如果你的项目后续要上线正式环境只需要把沙箱网关地址换成正式网关密钥换成正式密钥其余代码几乎不用动。但正式环境建议再增加一个“金额与订单匹配校验”把用户下单时保存的金额和回调里的 total_amount 做比对防止恶意用户篡改金额。本文还有配套的精品资源点击获取
返回列表