ARTICLE DETAIL

资讯详情

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

健身预约小程序开发实战:Spring Boot后端与并发预约实现

健身预约小程序开发实战:Spring Boot后端与并发预约实现 说实话健身类预约小程序这两年找我咨询的人特别多。有的是健身房老板想做个会员约课工具有的是刚转行的前端朋友想拿一个完整项目练手还有人是手上拿到一份源码但不知道怎么跑起来。这个标题里的“健身预约小程序含小程序源码、后端源码”我太熟悉了它几乎把所有热门要素都占了微信小程序、Spring Boot后端、前后端分离、预约业务、并发控制哪怕只把一个预约流程吃透放到简历上都能撑起一大段项目经验。先说清楚这个东西能解决什么问题。用户端打开小程序看到教练排期、课程列表选中时间段一键预约到店扫码或报手机号核销管理端维护教练、排课、查看预约记录后端负责用户登录鉴权、预约冲突处理、订单状态流转。听起来不复杂但真做起来坑比想象中多。这篇文章我就用自己的开发视角从项目拆解、表结构设计、核心流程实现、前后端联调到上线备案和常见问题排查完整捋一遍适合有基础的前端开发者、想转后端的初级工程师以及准备拿小程序项目练手的同学参考。1. 项目整体设计健身预约小程序到底在做什么1.1 先拆业务再谈技术选型拿到一个预约类小程序第一步不是写代码而是把业务角色和状态流转盘清楚。这个项目里至少有三种角色普通用户、教练、管理员。用户关心的是“我能不能约到想上的课”教练关心的是“我的排期谁约了、有没有冲突”管理员关心的是“每天有哪些预约、有没有人放鸽子”。预约业务的核心是“资源”和“时段”。资源是某位教练在某一天、某一个时间段的可预约名额。用户提交预约时系统要判断这个时段是否还有名额一旦约上就锁定名额其他人不能再约。很多新手把预约做成简单的“插入一条记录”结果就是两个用户同时约同一个时段数据库里出现两条记录教练一天被约了两遍。这个问题必须从表设计和代码逻辑两个层面同时解决。技术选型上这个小程序的常见组合是原生微信小程序加Spring Boot后端加MySQL数据库。原生小程序的好处是微信API调用直接没有额外框架的学习成本Spring Boot是目前后端源码里出现频率最高的框架生态成熟社区资料多遇到问题搜得到MySQL负责存业务数据。前端同学如果只写过页面用这个项目练后端是个特别好的切入点因为Spring Boot的Controller-Service-Mapper分层很直观配合MyBatis Plus几乎可以照着RuoYi这类开源框架的思路去理解。为什么不用纯云开发或者云函数省事是真省事但小程序云开发的数据库权限模型、并发处理能力、部署方式都跟传统前后端分离项目差别很大。如果你以后想转企业级开发Spring Boot这套从登录鉴权到数据库事务的完整链路是绕不开的云开发反而学不到这些东西。这套带后端源码的项目价值恰恰在于能让你把“前端怎么调接口、后端怎么吐数据、数据怎么落库”整条链路跑通。1.2 项目目录结构一份能直接上手的源码长什么样拿到源码之后第一件事是看目录。规范的目录结构用不着看文档就能猜出个七八成。我按常见结构还原一下fit-reservation ├── miniprogram/ # 微信小程序前端 │ ├── pages/ │ │ ├── index/ # 首页课程推荐、今日排期 │ │ ├── booking/ # 预约页选教练、选时段 │ │ ├── order/ # 我的预约待上课、已完成、已取消 │ │ └── mine/ # 个人中心登录信息、头像昵称 │ ├── utils/ │ │ ├── request.js # 请求封装统一注入token │ │ └── auth.js # wx.login登录逻辑 │ └── app.js ├── backend/ # 后端源码Spring Boot工程 │ ├── src/main/java/com/xxx/ │ │ ├── controller/ # 接口层 │ │ ├── service/ # 业务层 │ │ ├── mapper/ # 数据访问层 │ │ ├── entity/ # 数据库实体 │ │ └── common/ # 统一返回、异常处理、JWT工具 │ ├── src/main/resources/ │ │ └── application.yml # 数据源、端口等配置 │ └── pom.xml └── sql/ └── init.sql # 建库建表脚本含初始数据这个结构最大的好处是前后端彻底分离你单独看前端或者单独看后端都能跑通联调时接口契约对了就行。小程序端没有使用uni-app之类的跨端框架好处是去掉了一层编译转换原生组件的生命周期、路由、API调用都直接可见排查问题更直观。后端也不是那种把代码全堆在一个Controller里的“教学代码”而是按标准分层写的对理解工程化项目有实际帮助。2. 从需求到表结构后端数据模型的落地2.1 五张核心表理清预约业务的数据流转预约类业务的数据模型是有套路可循的。我用过好几套方案最后沉淀下来这套核心表结构简单、没有冗余、不容易出并发问题。用户表。存储微信用户的基本信息openid是微信小程序用户唯一标识一定不能只存昵称和头像同一微信号换昵称是常有的事。还需要一个status字段做拉黑或禁用处理时用得上。教练表。这里有一个小陷阱教练也应该是用户或者跟用户表关联否则后续做“教练登录查看自己的排期”会很痛苦。简化做法是在教练表里直接放一个user_id字段关联用户表这样教练登录后也能进小程序管理端。排期表。这是整个项目的核心语义表它描述的是“某位教练在某一天有哪几个可预约时段”。我习惯把每天拆成固定时段比如09:00-10:00、10:00-11:00这种按半小时或一小时分片。时段不要用datetime拎出来单独存就用一个日期字段加一个开始时间字段再加一个结束时间字段通过查询条件去判断重叠。预约表。用户点击预约后产生的一条记录。这张表除了user_id、schedule_id、教练信息之外一定要有一个状态字段因为预约不是一锤子买卖它有预约成功、已取消、已完成、已过期这么几个状态状态流转必须可控。课程表或服务类型表。不同教练可能带不同课程比如私教课、搏击课、拉伸课课程表跟教练表之间可以是多对多也可以在排期表里直接加一个course_type字段。小项目建议后者简单直接不用为了“灵活”把关系搞得过于复杂。对应的建表SQL大概是这个思路CREATE TABLE t_user ( id bigint(20) NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL COMMENT 微信openid, nickname varchar(64) DEFAULT NULL, avatar varchar(255) DEFAULT NULL, phone varchar(20) DEFAULT NULL, status tinyint(4) DEFAULT 1 COMMENT 1正常 0禁用, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_trainer ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) DEFAULT NULL COMMENT 关联用户表, name varchar(32) NOT NULL, avatar varchar(255) DEFAULT NULL, intro varchar(500) DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_schedule ( id bigint(20) NOT NULL AUTO_INCREMENT, trainer_id bigint(20) NOT NULL, course_name varchar(64) NOT NULL, schedule_date date NOT NULL, start_time varchar(10) NOT NULL COMMENT HH:mm, end_time varchar(10) NOT NULL COMMENT HH:mm, total_slots int(11) DEFAULT 1 COMMENT 可预约名额, booked_slots int(11) DEFAULT 0, status tinyint(4) DEFAULT 1 COMMENT 1可约 0已截止, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_appointment ( id bigint(20) NOT NULL AUTO_INCREMENT, schedule_id bigint(20) NOT NULL, user_id bigint(20) NOT NULL, trainer_id bigint(20) NOT NULL, appointment_date date NOT NULL, start_time varchar(10) NOT NULL, status tinyint(4) DEFAULT 1 COMMENT 1已预约 2已取消 3已完成 4已过期, cancel_reason varchar(255) DEFAULT NULL, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_schedule_id (schedule_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.2 为什么排期表里要有“已约人数”字段很多人设计排期表时只放一个“可约总名额”预约表里插入一条记录就算完成。这个方案在并发量低的时候没问题但一旦两个人同时提交就会出现超卖。解决思路是在排期表里增加booked_slots字段预约时通过一条update语句原子性地把已约人数加一同时判断加一之后有没有超过总名额。这里补充一个很多后端新手没注意到的经验booked_slots是冗余字段但它是有意为之的。它省去了每次预约都去count一次预约表的性能开销更重要的是它能把“名额剩余判断”变成一个原子操作这是后面处理并发预约的基石。如果你把这个字段去掉每次先查再插在高并发下大概率会出事。教练信息、课程信息这些字段也冗余进了预约表这样做虽然违背了教科书上的“第三范式”但实际操作中能减少很多关联查询尤其是小程序端列表页要展示教练头像、课程名时不用一张张去查排期表。记住一点查询频繁、不经常变化的字段冗余出来是提效不是设计错误。3. 核心功能实操登录鉴权、预约并发、动态标题一个都不能少3.1 微信登录与后端JWT鉴权怎么配合小程序端拿到wx.login产生的code之后要传给后端后端调用微信接口换取openid和session_key拿到openid后再查用户表。存在就返回登录态不存在就自动注册一条用户记录然后签发token返回给小程序。这个token我推荐用JWT因为它无状态后端不用把session存在内存里重启服务用户不会掉线。具体的登录流程写出来大概是这么几步小程序端wx.login()获取临时code。小程序把code通过request.js封装好的post请求发给后端。后端用code调微信的code2Session接口拿到openid。拿着openid查t_user表查不到就insert一条新用户。用openid和userId生成JWT返回给前端。小程序把JWT存到storage里之后所有请求header里带Authorization: Bearer token。后端加一个拦截器校验JWT校验通过才放行。这里要注意JWT的密钥一定不要写死在代码里我习惯放到application.yml里而且用足够长的随机字符串。token过期时间一般设置成7天小程序用户没有频繁输入密码的习惯太短了体验差太长了不安全。3.2 预约流程一条SQL解决并发冲突问题预约接口的核心逻辑并不复杂关键就一条用update语句把“判断名额”和“扣减名额”合成一步。int updated scheduleMapper.reduceBookedSlots(scheduleId); if (updated 0) { // 预约失败已约满或排期已截止 }对应的SQL是UPDATE t_schedule SET booked_slots booked_slots 1 WHERE id #{scheduleId} AND booked_slots total_slots AND status 1这条update执行后如果影响行数是1说明名额扣减成功后端再插入一条预约记录整个流程放在同一个事务里。如果影响行数是0说明排队期已经约满或者排期被设成了截止状态直接提示用户“该时段已被约满”。为什么这条update是线程安全的MySQL的update语句本身会加行锁两个并发请求同时到达时后一个会等前一个提交后再执行这时booked_slots已经被更新过了where条件里的booked_slots total_slots自然就不满足了影响行数就变成0。这个方案我用过很多次在没有引入Redis的情况下单机部署完全够用。补充一个细节插入预约记录时一并在t_appointment表里写schedule_id、trainer_id、appointment_date、start_time这些冗余字段一方面是为了前端列表展示方便另一方面也保留了后续做“用户预约历史”的查询能力。取消预约的逻辑是对称的先把预约记录状态改成已取消再对排期表执行update t_schedule set booked_slots booked_slots - 1同样需要事务。3.3 小程序动态设置页面标题这个需求在项目里很常见比如预约成功后预约成功页的导航栏标题要动态显示“预约成功”或者进入不同教练的详情页时标题显示教练名字。原生小程序提供了现成的方法在页面的onLoad或者onShow里调用wx.setNavigationBarTitle({ title: 预约成功 });这个方法只对当前页面生效不会影响其他页面。如果希望在页面顶部navigationStyle设为custom的时候做自定义导航栏那就需要自己画一个固定定位的标题栏用状态栏高度加导航栏高度来计算top值这套逻辑在真机上调试时会有差异安卓和iOS的状态栏高度不一样建议在app.js里通过wx.getSystemInfoSync()读取状态栏高度写进globalData。3.4 前端请求封装与token失效处理小程序端所有请求我都会走一个统一的request.js封装。这个封装主要干三件事把baseURL统一管理、自动在header里带上token、对接口返回的错误码做统一处理。特别是token过期不能只在每个页面的回调里写一遍“请重新登录”而是要在封装层统一判断遇到401或特定的业务码时清掉storage里的token并跳转回登录页。function request(url, method, data) { return new Promise((resolve, reject) { wx.request({ url: baseUrl url, method: method, data: data, header: { Content-Type: application/json, Authorization: Bearer wx.getStorageSync(token) }, success(res) { if (res.data.code 401) { wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); return; } resolve(res.data); }, fail(err) { reject(err); } }); }); }注意一点小程序端不要用后端返回的完整消息去覆盖页面上的UI文案后端提示语设计成“预约名额不足”之后前端最好再配合控制wx.showToast的图标类型给用户一个明确的视觉反馈。4. 前后端联调与上线避坑实录4.1 开发环境跨域问题其实可以绕开前后端分离开发时跨域是绕不开的话题。不过很多小程序开发者会忽略一个问题小程序里的wx.request是不受浏览器同源策略限制的它不存在你在Web开发时遇到的XMLHttpRequest跨域问题。真正需要处理跨域的场景是你在浏览器里调试管理后台或者用Swagger测试接口时遇到CORS。如果你用的是Spring Boot后端最简单的跨域配置是加一个CorsFilter。我习惯直接写一个配置类Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }这里有个容易踩的坑如果用了allowCredentials(true)allowedOrigins就不能用*要么用allowedOriginPattern(*)要么把前端域名写死。很多人配完还是报跨域大概率就是死磕了这两个参数的组合。4.2 小程序上线备案备注、合法域名一个都不能漏从2023年之后微信小程序上线必须完成ICP备案这是卡了很多人的一步。备案时候有一个“小程序备案备注信息怎么填”的问题这里的备注要写清楚小程序的实际用途不能空着也不能只写“小程序”。健身预约类小程序建议这样写“用于健身房课程查询、教练预约、会员预约记录管理等健身服务功能。”简短、明确、有业务指向。如果你涉及教练付费预约还要留意支付类目相关资质个人主体是做不了这类交易的。上线前还要在微信公众平台配置服务器域名request合法域名一定要是HTTPS而且不能带端口。开发时用http://127.0.0.1:8080能调通上线后忘改baseURL或者域名没有备案、没有配置白名单都会导致所有请求直接失败。这几个问题在真机调试时最有迷惑性因为开发者工具里“不校验合法域名”这个开关默认是开着的很多人开发工具里跑得通一到真机就黑屏十有八九是域名问题。4.3 常见问题速查表问题现象可能原因排查思路真机请求全部失败request合法域名未配置或域名未备案登录微信公众平台确认域名已备案且已配置到request合法域名登录后接口返回401token未保存或已过期检查wx.setStorageSync时机确认请求拦截器是否加了Authorization头预约成功但排期没扣减事务没生效或SQL条件不对确认Transactional生效检查mapper的update语句是否返回影响行数同一时段被重复预约没有用原子扣减逻辑改成update t_schedule set booked_slots booked_slots 1 where booked_slots total_slots头像昵称显示不出来微信头像昵称填写能力调整新版小程序要用open-typechooseAvatar和昵称填写组件不能直接getUserInfo页面标题不更新调用了wx.setNavigationBarTitle但时机不对放到onReady之后调用或在onShow里调用并判断当前页面栈这里再提一个容易忽略的细节小程序用户头像昵称获取规则改了很多次现在的推荐做法是引导用户主动填写而不是在onLoad里直接拿。健身预约场景里可以把头像昵称放在个人中心里让用户自己编辑不要拦在登录流程里否则大量用户会卡在第一步。4.4 后端部署踩坑记录Spring Boot后端部署相对简单打jar包扔到服务器上java -jar启动就行但有几个坑是多数人都会遇到的。第一个是端口和防火墙8080端口没在安全组或防火墙规则里放行外部永远访问不到。第二个是数据库连接配置线上环境一定要用独立的数据库账号权限最小化密码不要设成root这种弱口令。第三个是日志没人喜欢凌晨被人叫起来看报错logback配置里记得加上按照日期滚动和大小滚动日志保留7天就够。还有一个和部署相关的细节小程序请求的HTTPS证书。很多人图省事用IP或者自签名证书但微信小程序只认合法CA签发的证书且必须绑定域名不能用IP。最省事的方案是用Nginx做反向代理证书放在Nginx层后端服务不用处理SSL只监听本地8080端口就够了小程序请求打到Nginx的443端口。5. 源码里隐藏的学习点从“能跑”到“会改”5.1 前端开发者学习后端Java的几个切入点很多前端朋友拿到这套源码打开Spring Boot工程一脸懵不知道从哪里看起。我的建议是不要按package的字母顺序去看代码而是顺着一次请求的路径走一遍。比如用户打开小程序首页请求排期列表这个请求先进ControllerController调ServiceService调MapperMapper对着实体类操作数据库最后把结果一层层返回给前端。你把首页这个接口从Controller到SQL完整跟一遍后端分层的逻辑就通了。看完接口再看登录。登录涉及小程序code、HTTP请求、数据库查表、JWT生成它把网络、数据、缓存、安全串在一起是整个后端流程最浓缩的一段代码。能独立把登录讲清楚说明你对后端已经有了基本的掌控感面试聊项目也更有底气。5.2 预约业务还能怎么扩展这个项目做完以后如果你想继续深挖有几个很自然的扩展方向。排期模块可以引入循环规则比如某个教练每周一、周三、周五有课做一个周规则排期能省掉管理员大量手工操作。预约成功后增加微信订阅消息通知上课前一天提醒用户能有效降低放鸽子率。再进一步可以给排期表增加教练个人每日限额比如一天最多上6节课防止教练被连续排满了导致疲劳。付费预约也是常见需求但要把微信支付集成进来就会牵扯到商户号、退款、支付回调业务复杂度上一个台阶建议先把免费预约跑通再考虑。我在实际项目里见过很多次“免费预约很稳一接支付就各种问题”的案例支付回调的幂等性和对账逻辑是另一篇长文的容量。5.3 代码里的两个“隐藏细节”值得反复读一份好的源码细节都在不经意的地方。比如排期列表查询接口里大概率会有对时间的格式化处理把datetime字段按YYYY-MM-DD HH:mm格式返回给前端别小看这一步前端直接拿到数据库原始时间字段做展示真机上会出现时区、格式化不一致的问题。再比如预约成功后会做一次数据库行锁提交这个代码写得好不好直接影响你压测时能不能顶住几百个用户同时约课。我个人很喜欢看这套项目里对统一返回体的封装。前后端分离项目最忌讳每个接口返回结构都不一样前端处理起来要写一堆if else。规范的项目会在common包下定义一个Result类包含code、message、data三个字段成功失败一个结构前端解析统一处理。写后端接口时返回体设计得清楚联调效率至少提升一半。最后再分享一个小经验程序员拿到一份源码最容易犯的错误是迫不及待打开IDE去跑。我更建议先花半小时看README、看SQL脚本、看目录结构把项目跑起来之后再找一个最小功能点去修改改完观察效果。健身预约小程序这个项目从预约流程切入最合适——它既有前端交互又有后端逻辑还牵扯并发问题一个功能点能学到的东西比把整个项目看一遍还多。
返回列表