健身房预约小程序全栈开发:Spring Boot+Vue+微信小程序实战
发布时间:2026/10/6 21:41:25 锦皓数字建站

1. 项目拆解健身房预约小程序到底在做什么先把这个项目的本质说清楚。表面看它是一个“健身房预约小程序”有源码、数据库和文档这三件套但剥开外壳它其实是典型的多端联动的业务系统用户通过微信小程序端浏览教练、查看空闲时段、提交预约管理员通过Vue搭建的Web管理后台维护教练信息、排班、课程和订单中间由Java后端Spring Boot框架最常用提供数据接口MySQL负责持久化存储。这三部分各司其职缺一不可。为什么非得是“Java Vue 小程序”这种组合因为成本和效率最平衡。JavaSpring Boot在后端生态里属于“居家必备”的选择稳定、资料多、招人容易Vue做管理后台开发速度快组件生态完善Element UI一套下来界面就很像样小程序端则直接命中用户“即用即走”的使用习惯——预约这种低频操作让用户为了订一节操课去下载安装App那转化率基本凉凉。所以这个技术组合不是炫技而是围绕业务场景做出的自然选择。那这个系统解决了什么问题健身房的真实痛点其实很具体教练时间冲突一个教练同一天被约了三四次线下靠Excel记录迟早乱成一锅粥。用户到场率低电话预约随口答应到点人没来教练干等器械空置。高峰期排队高峰期器械和团课都紧张没有预约机制用户体验差。会员管理粗放办卡、次卡、剩余次数全靠收银员脑子记换个人就说不清。预约系统就是把“教练时间”、“器械名额”、“会员卡次”这三块资源统一成一个在线可管理、可查询、可结算的状态机。这是所有预约类系统的核心逻辑资源和时间的绑定关系。健身房如此理发店、美容院、自习室其实一个道理。从交付物来看源码解决“怎么改”、数据库文件解决“怎么建”、文档解决“怎么跑起来、怎么联调、怎么部署”。对于一个毕业设计或者中小型商业项目来说这是标准的三件套我后面会把每部分的关键细节都过一遍。2. 技术选型的背后逻辑与项目整体架构2.1 为什么选 Spring Boot MyBatis Plus 而不是 SSM 手写很多早期的Java项目资料还在用SSMSpring SpringMVC MyBatis那套XML配置新项目我强烈建议直接上Spring Boot。别误会不是SSM学不会而是Spring Boot把绝大多数繁琐配置变成了“约定优于配置”比如内嵌Tomcat、自动化装配、起步依赖这些能省掉初级开发一两天的环境搭建时间。尤其是一个健身房预约系统业务复杂度远没有达到需要手写大量配置的程度把时间花在业务逻辑上更值。持久层我个人推荐MyBatis Plus而不是纯MyBatis。核心原因是它的单表CRUD能力太省事了BaseMapper里已经封装好selectById、selectList、insert、updateById这些方法健身房系统里教练管理、会员管理、时段管理几乎全是单表操作用MyBatis Plus能省掉大量重复的SQL手写。遇到多表联查比如预约列表带出教练姓名和课程名再写XML里的resultMap自定义映射也不迟。Vue这边没什么争议管理后台用Vue 2 Element UI最稳妥。Vue 3虽然性能好且有Composition API但Element UI的生态和资料成熟度明显更高遇到问题随便一搜就有解决方案这对开发效率的影响其实是决定性的。管理系统不需要花哨动画表格、表单、日期选择器、弹窗确认Element UI全是现成的。2.2 整体架构一个系统分成三段边界要清晰整个项目建议按经典的前后端分离模式组织目录上分成backend、admin-web、miniapp三块ginas-app/ ├── backend/ # Spring Boot 后端提供 RESTful API ├── admin-web/ # Vue 管理后台给健身房运营人员用 ├── miniapp/ # 微信小程序端给C端用户用 └── sql/ # init.sql建库建表、data.sql种子数据后端模块划分建议按业务域分包不要按技术类型硬拆所有controller扔一个包、所有service扔一个包是最坑的做法。按member会员、coach教练、course课程、booking预约、admin后台管理分包每个包内含controller、service、mapper、entity。这样后面加功能的时候改动范围非常清晰。关于接口风格我只说一点前后端分离项目务必用统一返回体ResultT包含code、msg、data三个字段。万一某个接口要返回的字段是从null变成空数组或者从200变成40001你不需要动前端调用方的解析逻辑。这是我用血泪换来的经验——早期项目直接返回Map或裸数据接口一升级前端就崩。2.3 小程序端原生还是 uni-appVue和小程序的结合通常有两条路一是小程序端完全用原生WXML WXSS JS开发管理后台用Vue二是小程序端用uni-app这种Vue语法跨端框架开发。两种方案都有人用取舍很简单。原生小程序的好处是性能最优、调试方便、调试工具的能力能100%发挥比如真机预览、性能面板、wxml节点审查但代码复用到其他平台支付宝小程序、抖音小程序基本没戏。uni-app则把Vue 2的语法直接搬进小程序开发体验更贴近Vue一套代码能发布到多个平台网上模板也特别多。我这个项目里用的方案是原生小程序为主因为功能复杂度不高涉及的核心页面就首页、预约页、订单页和个人中心原生小程序的Page生命周期足够承载这些逻辑。但如果你本身对Vue更熟用uni-app一样能把项目跑起来这取决于你自己更顺手哪种。这里不做强制推荐说清楚差异让你自己判断。3. 数据库设计预约系统的地基表结构到底怎么建3.1 核心表拆解9张表覆盖全部业务健身房预约系统的数据量远没有电商那么大但表之间的关系约束做不好后期改起来真是想哭。我比较推荐这套表设计根据实际项目做微调即可表名用途关键字段说明member会员表openid微信唯一标识、nickname、phone、balance余额单位分、statuscoach教练表name、avatar、specialty擅长领域、intro、statuscourse课程表name、cover、type团课/私教、duration分钟、capacity人数上限、priceclass_schedule排课表course_id、coach_id、start_time、end_time、date、remaining剩余名额booking_order预约订单表order_no、member_id、schedule_id、status0待支付/1已确认/2已完成/3已取消、amountmember_card会员卡表member_id、card_type次卡/月卡、total_times、used_times、expire_timepayment_log支付流水表order_id、pay_type、pay_amount、transaction_id微信支付单号、statusadmin_user后台管理员表username、passwordBCrypt加密、rolenotice公告表title、content、publish_time、status这里有几个设计细节值得单独说。金额一律存“分”。数据库里凡是涉及钱的字段都用整数存储如balance存的是分避免浮点误差。显示的时候前端转换成“元”后端返回给前端也应该是字符串形式的“分”不要让JSON序列化把大整数变科学计数法——一个1200分的余额变成1.2E3前端展示直接裂开。这是所有交易系统的共识。订单号用时间戳 随机数不要用自增ID做对外单号。自增ID会暴露业务量今天第10000个订单明天第10001个也不方便做并发去重。直接SimpleDateFormat格式化时间戳加几位随机数生成如yyyyMMddHHmmss6位随机的订单号前置一条唯一索引。这样既防了重复单号也符合微信支付对商户订单号的字符要求。class_schedule表要加唯一索引。这句最重要。UNIQUE KEY uk_coach_time (coach_id, start_time)意思就是同一个教练在同一个开始时间只能有一条排课记录。这个索引是防止“教练撞档”的数据库层面兜底哪怕代码里忘记判断冲突数据库也会拒绝插入。预约系统的可靠性很大程度上依赖这类数据库约束而不是单纯靠应用层if判断。3.2 预约状态流转从待支付到已完成一张状态机图讲清楚预约订单的status字段是整个系统的灵魂。建议设置4种状态并且在前端文档里明确状态流转规则0待支付 → 1已确认/已约 → 2已完成/已核销 ↘ 3已取消由用户主动取消或超时未支付系统自动取消0 → 1用户提交预约且支付成功或者免费预约场景下直接确认。1 → 2用户到店教练或前台在管理后台点击“核销”标志着次卡扣减或课程完成。3取消后不回流不允许从取消状态变成已确认。因为名额一旦释放就可能被别人占了。2 不可逆完成状态的订单不能再取消不然一个用户每次上完课都取消规则就形同虚设。这种状态机的好处是可审计任何时候打开订单列表看到状态4就知道订单的全生命周期到哪一步了。前端小程序端的状态显示也是映射到这几档待支付显示“去支付”、已确认显示“已预约”、已完成显示“已完成”、已取消显示“已取消”。这里有一个容易踩的坑取消预约必须释放名额。你就想用户取消了但排课表的remaining还是少的那其他想约的会员永远约不上。所以取消操作要做两件事第一步更新订单状态为3第二步把排课表remaining加回来。这两步必须在同一个事务里执行否则就会出现订单取消了、名额没释放的bug。数据库事务就是为了这一类场景存在的。-- 释放名额的语句要在事务中执行 UPDATE class_schedule SET remaining remaining 1 WHERE id #{scheduleId};3.3 并发预约的核心矛盾最后一个名额怎么别超卖预约系统最容易出现的线上事故是什么超卖。100个会员同时抢最后一个瑜伽课名额最后10个人都显示“预约成功”到店后现场一片混乱。千万别以为这是电商秒杀才要防的事预约系统的并发量级虽然小但并发问题一样存在哪怕一天就10个人同时点提交也可能撞车。防超卖有三种常见做法按可靠程度排序方案一数据库行锁最简单可靠预约时不是先查询再更新而是直接用带条件的UPDATE语句-- 先尝试扣减名额affected rows 1 说明扣减成功 UPDATE class_schedule SET remaining remaining - 1 WHERE id #{scheduleId} AND remaining 0;影响行数为1才继续创建订单影响行数为0说明没名额了直接返回“已约满”。这个方案之所以可靠是因为数据库的UPDATE操作自带行级锁同一时刻只有一个事务能成功执行这条语句天然避免了超卖。这也是我目前最推荐的做法。方案二乐观锁版本号控制在class_schedule表加一个version字段更新的时候带上旧版本号UPDATE class_schedule SET remaining remaining - 1, version version 1 WHERE id #{scheduleId} AND version #{oldVersion};一旦版本号对不上说明有人已经改过这行数据了本次更新失败重试或提示用户“名额紧张请刷新后重试”。这个方案比行锁多一点代码量好处是连接不用一直持有行锁性能更好。方案三Redis分布式锁并发量特别高时才需要用SET key value NX EX拿锁拿到锁才执行扣减执行完释放锁。这方案并发几千上万才刚需健身房预约这个量级属于杀鸡用牛刀还会引入Redis运维成本和锁超时问题。我直接不推荐。真正的做法是方案一为主方案二作为排课修改时的升级预留。第一版系统完全可以用方案一撑住后续如果做活动引流、早起扫码抢课再考虑上Redis不迟因为表结构已经预留了版本号的余地。4. 后端Java核心接口的代码级讲解4.1 登录接口微信登录与Token签发用户端小程序登录走的是微信官方code2session接口前端调用wx.login()拿到临时code传给后端后端拿着code、appid、secret调用微信接口换openid和session_key。这个流程看起来简单但有个坑不能把openid直接当登录凭证用在后端和前端之间的鉴权上。因为openid仅仅是用来标识用户身份的真正的会话凭证应该由后端自己签发一个Token返回给前端以后前端每次请求都带这个Token。Token建议直接用jwtJSON Web Token把memberId和过期时间塞进去用HMAC-SHA256签名。登录流程简化为后端拿到code换取openid→ 查询member表有没有该openid没有就自动注册一个默认会员 → 用memberId签发JWT → 返回token和memberId给前端。PostMapping(/api/auth/login) public ResultLoginVO login(RequestBody LoginDTO dto) { // 1. 调用微信接口获取 openid String openid wechatService.code2Session(dto.getCode()); // 2. 查找或创建会员 Member member memberMapper.selectOne( new LambdaQueryWrapperMember().eq(Member::getOpenid, openid)); if (member null) { member new Member(); member.setOpenid(openid); member.setNickname(微信用户 openid.substring(0, 6)); member.setStatus(1); memberMapper.insert(member); } // 3. 生成JWT有效期7天 String token jwtUtil.generateToken(member.getId(), member.getNickname()); // 4. 返回 return Result.success(new LoginVO(token, member.getId())); }微信的临时code有效期只有5分钟且只能使用一次。所以拿到code后要尽快调接口换openid不要做任何耗时的中间处理。服务端存放JWT密钥时不要写在代码里放在application.yml配置文件中并且生产环境通过环境变量注入。密钥一旦泄露别人就能伪造任意会员身份伪造Token这一点怎么强调都不过分。4.2 预约操作的核心Service逻辑事务 防超卖提交预约是整个系统技术含量最高的接口。我来贴一段完整的关键代码把事务、防超卖、状态机一次性串起来Override Transactional(rollbackFor Exception.class) public ResultBookingOrderVO createBooking(BookingCreateDTO dto, Long memberId) { // 1. 校验会员状态和余额/次数 Member member memberMapper.selectById(memberId); if (member null || member.getStatus() ! 1) { return Result.fail(会员不存在或已禁用); } // 2. 查询排课校验课程状态 ClassSchedule schedule scheduleMapper.selectById(dto.getScheduleId()); if (schedule null || schedule.getStartTime().before(new Date())) { return Result.fail(该课程已开始或已下架); } // 3. 通过行锁防超卖扣减名额 int affected scheduleMapper.decreaseRemaining(schedule.getId()); if (affected 0) { return Result.fail(课程名额已满请选择其他时段); } // 4. 生成订单号 String orderNo generateOrderNo(); // 5. 计算金额可以从course表带出 BookingOrder order new BookingOrder(); order.setOrderNo(orderNo); order.setMemberId(memberId); order.setScheduleId(schedule.getId()); order.setCourseId(schedule.getCourseId()); order.setCoachId(schedule.getCoachId()); order.setAmount(schedule.getPrice()); order.setStatus(0); // 待支付 bookingOrderMapper.insert(order); // 6. 返回订单信息前端拿着去调支付 return Result.success(new BookingOrderVO(orderNo, order.getAmount())); }这段代码有四个关键点需要仔细理解事务Transactional保证decreaseRemaining和insert要么都成功要么都失败。如果订单插入失败名额扣减也会回滚不会出现“名额少了但没订单”的情况。decreaseRemaining的SQL长这样注意这段SQL必须自动带remaining 0这个条件防止扣成负数update iddecreaseRemaining UPDATE class_schedule SET remaining remaining - 1 WHERE id #{id} AND remaining gt; 0 /update并发场景多个请求同时执行这条UPDATE时数据库行锁会让它们排队执行只有一个请求的remaining 0条件成立其他请求affected为0直接返回约满。这比“先select再update”高了一个安全级别。状态机新订单状态是0待支付同时也就意味着——预约名额其实在“待支付”那一刻已经锁定了。这引出了下一个问题用户一直不支付怎么办名额不该一直被占着所以需要超时取消机制。4.3 超时未支付自动取消定时任务 vs 延迟队列“待支付订单锁死名额”的设计是合理的因为用户在支付流程中可能正输密码呢名额就被别人抢了体验差。但名额不能无限期占用需要给待支付订单加一个有效期比如15分钟。最朴素的方案是Spring的Scheduled定时任务每分钟扫描一次status 0且create_time超过15分钟的订单执行取消。好消息是配合之前decreaseRemaining的思路释放名额也能复用行锁更新不会出并发问题。定时任务的代价是时间不精确最差情况延迟59秒且每分钟全表扫描一次消耗不大这个量级完全够用。更优雅的方案是RabbitMQ延迟队列但这对基础架构组件的要求比较高健身房预约系统引入MQ属于过度设计。我不建议在新手项目里用MQ做这个事——你最终要的是让用户在15分钟内完成支付精确到秒没有意义。定时取消的代码思路Scheduled(cron 0 * * * * ?) // 每分钟执行一次 Transactional public void cancelExpiredOrders() { // 1. 查出所有待支付且超时的订单 ListBookingOrder expiredOrders bookingOrderMapper.selectExpiredOrders(15); for (BookingOrder order : expiredOrders) { // 2. 取消订单 order.setStatus(3); bookingOrderMapper.updateById(order); // 3. 释放名额 scheduleMapper.increaseRemaining(order.getScheduleId()); } }这里又用到了“更新订单状态 释放名额”这两个动作仍然在同一个事务里逻辑基本一致。你会发现数据库设计里那张class_schedule表的remaining字段确实是并发预约的“主战场”后面所有经验几乎都在围绕它做文章。5. 前端小程序核心页面与Vue管理后台的实现5.1 小程序端五个核心页面的业务逻辑小程序端不算登录和支付中间页真正的核心页面主要这几张页面功能pages/index/index首页金刚区入口 公告栏 当日热门课程轮播pages/course/list按日期和课程类型筛选排课展示剩余名额pages/course/detail课程详情、教练信息、剩余名额、立即预约按钮pages/order/confirm预约确认页展示金额、选择支付方式pages/order/list我的预约列表区分待支付/已确认/已完成/已取消这里我不逐个贴所有页面代码只挑两个最值得讲解的。课程列表页是预约转化率的关键。用户来不是看课程介绍的是来看“哪节课现在还能约”。所以列表页的每个课程卡片什么信息最重要date星期几、start_time上课时间、remaining剩余名额。真正做过小程序的人都知道页面越聚焦用户决策成本越低。这里要强调一个交互细节当remaining 0时按钮要置灰显示“已约满”同时不能让用户点击后才知道没名额对用户体感的伤害太大了。预约确认页展示的金额不能直接从后端schedule.price读就完事要展示明细课程费、可用余额抵扣、实际支付金额。哪怕第一版没有余额抵扣功能也要预留这个计算位置显示“余额支付”与否的开关后续运营提需求的时候改动范围会小很多。5.2 小程序端请求封装与登录态刷新小程序端的request请求不能直接裸用wx.request建议封装一个request.js工具。核心逻辑如下// utils/request.js const BASE_URL https://api.example.com; // 改成你的后端域名 function request(options) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) }, success: (res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else if (res.statusCode 401) { // token过期重新登录 wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); reject(new Error(登录已过期)); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(new Error(res.data.msg)); } }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request };为什么单独封装这一层因为一旦后端接口变更比如需要加签名字段、需要换token刷新逻辑你只需要改这一个文件不需要在几十个页面里分别修改调用代码。封装的价值在接口规范变动的场景下会体现得非常明显。5.3 Vue管理后台Element UI排课管理页管理后台是运营人员的“工作台”核心页面除了登录页最主要的是排课管理和预约订单管理。排课管理页面我用Element UI的el-date-picker做日期选择el-table展示当天所有课程和教练的排班用el-dialog弹窗进行新增排班。每次新增排课前端把courseId、coachId、startTime、duration、capacity这些参数拼好后POST到后端接口。后端保存前先查一次“这个教练这个时间段有没有已存在的排课”有就直接报错“该教练此时间段已有课程”这条判断逻辑对运营每天排课非常关键避免人手滑排重。订单管理页面比较简单一个表格展示orderNo、会员昵称、课程名、教练名、预约时间、金额、状态状态列用el-tag渲染不同颜色再配一个筛选条件即可。真正的高频操作是“核销”按钮运营扫一眼确认用户到店后在订单行点“核销”后端把订单状态从1改成2并把member_card的used_times加1如果是次卡场景。这里有一个产品层面容易忽略的细节核销动作要有二次确认弹窗。你在管理后台点错一个核销用户的次卡被扣了要去数据库手动改运营会直接骂人。el-popconfirm包一层就够了。6. 支付环节的坑与解决方案6.1 微信支付流程在小程序端的正确姿势小程序端发起支付的流程大家应该都见过前端调wx.requestPayment后端需要先调用微信支付统一下单接口获取paySign等参数。这里最容易被新手搞乱的是签名算法。正确流程是前端拿到订单号后调后端/api/pay/unifiedOrder接口。后端拿着out_trade_no、total_fee、openid、notify_url这些参数去请求微信支付统一下单API。微信返回prepay_id后后端生成paySign并返回给前端。前端拿着paySign等参数调wx.requestPayment唤起支付面板。注意绝对不能在后端直接返回prepay_id就让前端去支付paySign必须由后端生成。paySign的算法是把appId、timeStamp、nonceStr、packageprepay_idxxx进行字典序排序后拼接用商户API密钥做HMAC-SHA256签名。简化后的后端核心代码public PayParamsVO unifiedOrder(Long orderId, Long memberId) { // 1. 查订单校验属于当前会员且状态为待支付 BookingOrder order bookingOrderMapper.selectById(orderId); if (order null || !order.getMemberId().equals(memberId) || order.getStatus() ! 0) { throw new BizException(订单状态异常); } // 2. 调微信统一下单接口获取 prepay_id String prepayId wxPayService.unifiedOrder( order.getOrderNo(), order.getAmount(), member.getOpenid()); // 3. 生成前端支付参数 String timeStamp String.valueOf(System.currentTimeMillis() / 1000); String nonceStr UUID.randomUUID().toString().replace(-, ); // 4. 重新用 prepay_id 生成 package 和 paySign String packageStr prepay_id prepayId; String paySign wxPayService.generatePaySign(timeStamp, nonceStr, packageStr); return new PayParamsVO(timeStamp, nonceStr, packageStr, paySign); }6.2 支付回调订单状态的最终确认支付结果不能以“前端收到支付成功回调”为准必须以微信服务器的异步通知为准。因为前端回调可能被伪造也可能因为用户退出小程序而丢失。后端需要提供一个/api/pay/notify接口接收微信的回调请求用微信支付平台证书验证签名然后更新订单状态。回调接口有一个请求幂等性问题微信同一个支付结果可能会通知多次你的处理逻辑必须保证“重复通知不会重复处理”。最简单的方式是查询订单当前状态——如果已经是1已确认就直接返回success不做重复操作。回调接口的简化逻辑PostMapping(/api/pay/notify) public String payNotify(RequestBody String xmlData) { // 1. 验证签名用微信支付证书 if (!wxPayService.verifyNotify(xmlData)) { return fail; } // 2. 解析出订单号和支付结果 String orderNo wxPayService.parseOrderNo(xmlData); boolean success wxPayService.isSuccess(xmlData); // 3. 找到订单幂等处理 BookingOrder order bookingOrderMapper.selectByOrderNo(orderNo); if (order ! null order.getStatus() 0 success) { // 更新订单状态、记录支付流水 order.setStatus(1); bookingOrderMapper.updateById(order); // 记一条支付流水 paymentLogMapper.insert(...); } // 4. 返回给微信收到通知请勿重复发送 return success; }支付这一块特别强调一个细节回调接口一定不要加统一鉴权拦截器。很多新手的全局拦截器会把所有/api/**接口都要验证Token结果微信服务器来访问时没有Token接口直接返回401微信就一直重试订单永远是待支付。正确做法是对/api/pay/notify这个地址做白名单放行只校验签名。7. 常见问题与排查实录7.1 并发预约导致超卖先看日志里有没有“已约满”有一次系统上线后运营反馈“下单量超过课时人数了”我第一反应不是去看代码而是去查后端日志。果不其然日志里同时出现了好几条课程名额已满请选择其他时段的报错——这说明防超卖逻辑已经在工作了只是前端没感知到这个错误提示用户疯狂点“提交”按钮造成了多次请求。排查结论这不完全是代码bug更可能是前端没有做按钮防抖处理。用户点了一次没反应又点了一次后端事务还没提交第二次请求进来了名额又扣了一次。修复方式前端在点击提交后按钮进入disabled状态3秒内不可点击后端再兜底一个“同一会员同一排课5秒内不能重复发起预约”的幂等校验。7.2 小程序打开页面报“token过期”但明明刚登录过这个问题的根源几乎都是JWT的有效期设得太短或者小程序端在收到401后没有做静默刷新直接跳登录。在实际场景中用户可能在小程序里待半小时不操作Token过期当然正常。合理方案是Token有效期设为7天体验型App可以更长并且在小程序端网络请求里统一捕获401状态——如果是登录过期弹提示并去登录如果只是做个临时校验失败则需要后端能区分“Token无效”和“Token过期”降级为用户重新调用登录。另一个经验是不要把session_key存在本地并用于解密用户手机号之后就不管了。session_key的有效性和微信服务器同步一旦失效前端提交phone解密就失败。这通常发生在用户上次授权登录之后隔了好几天才再次进入页面。建议手机号授权获取phone_code后直接传给后端由后端通过phone_code调用微信接口换取手机号而不是用缓存里的session_key自己解密这样时效性更好。7.3 数据库导入了但项目连不上库八成是这里很多新手拿到源码后数据库文件导入了Spring Boot启动也显示成功但一查接口就报Table ginas doesnt exist。原因基本是application.yml或application.properties里的数据库名和实际建的库名不一致。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/ginas_app?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver这里有个特别容易忽略的点serverTimezone必须设置成Asia/Shanghai否则本地时间和数据库时间差8小时预约时间会出现“提前8小时开始”的诡异问题排查起来非常头疼。另外MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver用MySQL 5.x的com.mysql.jdbc.Driver会报警告。7.4 管理后台跨域问题Cookie还是Token前后端分离项目管理后台在localhost:8081后端在localhost:8080浏览器默认是不允许跨域调用的。最常见的解决方式是后端配置跨域过滤器。这里建议用CorsFilter允许指定源不要用*因为要允许携带Cookie的话*不安全或者直接用CrossOrigin注解加在Controller上。注意一个关键细节如果你在管理后台登录时用的是session保持登录态那必须开启allowCredentials(true)并指定具体的allowedOrigins而非*如果你用的是JWT Token那就不存在cookie跨域问题把Token放在Authorization头里即可。这里建议新项目直接上JWT能省掉session共享、负载均衡等一堆后续问题。7.5 其他高频问题速查现象可能原因排查方向小程序真机上图片不显示图片域名没配到小程序后台downloadFile合法域名登录微信公众平台 → 开发 → 开发管理 → 服务器域名订单状态一直是“待支付”但钱扣了回调接口没写对或被拦截器拦截了打开ngrok或内网穿透看下有没有微信请求到达后端检查回调URL是否可公网访问预约成功后列表页不刷新小程序页面生命周期问题用了onLoad缓存了数据改成onShow每次显示时重新拉取列表库存异常扣减排课有并发下单使用remaining 0条件更新并检查affected rows服务启动报Data too long字段长度定义过短调整数据库表字段长度比如nickname VARCHAR(64)8. 一点实操心得从零开始怎么把这个系统跑通最后分享几条实际开发中非常有体感的经验。第一先跑通最小闭环再完善功能。不要一上来就想把会员卡、次卡、优惠券、积分全做完。先搞定“用户登录 → 查看课程 → 提交预约 → 支付成功 → 管理后台核销”这一条最核心的链路然后逐步加功能。这条主链路通了系统已经具备上线条件了后面加的都是增量。第二接口文档一定要在开发前写好。哪怕自己一个人开发也要把接口定义、参数含义、返回结构写出来放到项目的docs目录下。因为后期小程序端和后端是分开开发的没有文档联调的时候全靠口口相传效率极低。我自己常用的方式是维护一个API.md后端每完成一个接口就更新一次小程序端照着文档对接即可。第三数据库设计和表字段的命名统一用下划线。member_id、class_schedule_id、create_time这样的命名在Java实体类里用MyBatis Plus的驼峰自动映射memberId、createTime很自然地就能对应上。如果数据库字段叫memberID或者MemberId这种不规则写法会给ORM映射增加不必要的麻烦。第四数据备份和SQL脚本管理从第一天就开始做。sql/init.sql只放表结构和基础数据不要每次改表都手动在数据库里操作。推荐使用flyway这类工具管理数据库版本或者至少做到每次表结构变更就把对应的ALTER TABLE语句存到一个sql/update_v1.1.sql文件里。后面项目的复现和排产全依赖这套脚本。小程序开发调试时wx.request请求的域名必须是HTTPS且已经备案本地开发时的处理办法是在微信开发者工具右上角“详情”里勾选“不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”这样才能用http://localhost:8080进行本地联调。这个选项比较隐蔽经常有新手卡在这一步半天不知道什么原因。还有一个小细节不要把appid和secret写在前端代码里。小程序端的appid会明文存在代码包里而secret只应该存在于后端配置里前端只需要把code传给后端换取openid即可。这也是为什么登录接口一定要走后端的原因无论是appsecret、支付证书还是数据库密码都是后端专属机密前端一概不碰。9. 这个项目还能怎么扩展第一接一个课程评价体系。健身房很需要口碑数据用户上完课可以给教练打分、写评价教练端或者管理后台能看到平均分和评价列表。这个功能只涉及两张表评价表、评分汇总字段对现有系统侵入极小。第二增加会员储值卡和优惠券模块。目前系统只做了预约逻辑如果把储值、优惠券、积分加进来配合支付回调一起处理就是一个比较完整的商业闭环了。数据库结构也可以提前预留member_wallet钱包和coupon优惠券两张表。第三引入数据统计看板。管理后台加一个首页统计面板今日预约人数、本周营收、课程约满率Top10、教练排班利用率。这些数据可以直接从booking_order和class_schedule表里用GROUP BY聚合查出来再用Vue的图表库如echarts展示运营价值非常高。这也是一般“毕设”项目与商业项目拉开差距的地方。健身房预约系统的核心说到底是对“教练时间”和“课程名额”两类资源的在线管理。数据模型建好了交易状态机理清了剩下的功能基本都是在既有框架上做增量。把上面说的角色权限、事务处理、并发控制这些基本功练扎实这个项目你能带走的绝对不只一套源码。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。