
一个健身工作室的老板找我说想上一个线上预约系统因为每天前台都要接几十通电话约课约场地约重了还得挨个打电话协调。我盘了一下需求决定直接做一个健身预约小程序前端用微信小程序承载后端单独搭一套服务。这篇就把这个项目的完整思路写出来——从业务拆解、技术选型、核心源码实现到部署联调里容易踩的坑全部摊开讲。正在做类似预约类小程序、或者刚拿到一套源码不知道怎么改的朋友都可以参考着来。先说清楚这套源码解决什么问题用户打开微信小程序能看到可预约的时段和场地选择后提交预约后台记录并限制同一时段的人数或场次支持取消预约、查看我的预约记录。管理员端可以设置可预约项目、管理时段、查看预约列表。整套包含小程序前端工程和后端接口服务是典型的前后端分离结构。1. 业务拆解健身预约场景里的核心矛盾点1.1 为什么预约类小程序比H5或APP更适合这个场景很多朋友纠结预约系统用什么载体我的建议很简单健身房的用户天然就在微信里小程序用完即走不需要下载APP也不像H5那样每次都要重新登录授权。微信小程序打开速度接近原生配合订阅消息还能在预约成功或取消时推送通知这是H5网页要做很久才能达到的效果。再往深一层说小程序有现成的用户授权体系。微信登录获取openid后端拿openid关联用户不需要自己搭建账号密码体系也不用做短信验证码。对一个小型健身房来说这能省掉一大批开发量也能降低用户上手的心理门槛。1.2 功能清单和业务边界这个项目定位很明确围绕“场地预约”而不是“完整SaaS健身房系统”。所以功能边界我控制在以下范围用户端小程序微信授权登录、浏览可预约项目列表、按日期查看时段、提交预约、取消预约、我的预约记录。管理员后端接口项目/场地管理增删改、时段配置每天几点到几点开放、预约记录查询、取消或核销预约。数据统计基础的预约数量统计方便看哪些时段热门。不需要做的支付闭环、会员卡储值、私教课程售卖、请假签到。这些可以在后续版本扩展第一版先跑通核心预约流程。这样做的好处是代码量可控源码结构清晰二次开发的人拿到手不会被一堆无关模块干扰。1.3 角色划分与用户旅程系统里就两类角色普通用户和管理员。普通用户是小程序端的使用者管理员走的是后端接口通过简单的接口认证来识别。典型用户旅程是这样的用户打开小程序看到今天可约的项目列表比如“器械区”“操课房”“动感单车房”选一个项目进入日期选择看到当天每个时段的剩余名额点击预约填写备注可选提交然后在我的预约里看到记录。如果临时有事点击取消名额释放回时段。管理员旅程登录管理后台界面新建“动感单车房”配置周一至周日每天18:00-21:00可预约容量5人查看某天的预约列表看到某个用户预约了两节连堂手动取消其中一节核销时根据用户出示的预约码标记已使用。2. 技术选型小程序前端与后端框架的搭配逻辑2.1 小程序端原生还是uni-app这个小程序前端我用的原生微信小程序实现。为什么不直接上uni-app因为项目只需要运行在微信平台没有多端需求原生框架体积小、调试方便API调用也是最直接的。原生小程序的核心结构就是app.json配置页面路由pages目录下放每个页面每个页面由wxml、wxss、js、json四个文件组成。相比uni-app的vue语法原生语法更贴近微信生态本身做编译类的问题排查时也少一层转换。如果你是新手源码里需要重点看两个文件app.js里做了全局初始化包括登录态的处理utils/request.js里封装了所有后端请求统一处理token和异常提示。这两个地方理解了整个前端的请求逻辑就通了。2.2 后端技术栈Spring Boot为主体兼顾易读性和扩展性后端我用的Spring BootJava语言。选它不是因为“流行”而是因为预约系统有并发请求场景Java生态里做接口限流、数据库事务、线程池控制都比较成熟。源码里按照controller-service-mapper三层结构组织目录清爽任何人拿到都能快速定位代码。数据存储使用MySQL核心表就三张user表、appointment表、course场地项目表。为什么不用复杂的表结构因为预约业务的数据关系本身没那么复杂一张预约记录表关联用户ID和项目ID就够了。真正需要花心思的是时段字段的设计这个我在后面源码解析里详说。2.3 前后端交互的API设计原则接口设计我遵循了三条原则。第一所有接口返回统一的JSON结构包含code、message、data三个字段前端封装层只判断codemessage直接用来弹出提示。第二用户标识通过token获取不把openid直接暴露在URL里。第三写操作全部使用POST查询用GET接口命名按资源路径来比如/appointment/create、/appointment/list、/appointment/cancel。这套设计看起来简单但对联调非常友好。前端只需要维护一个request.js后端出接口文档时也只需要按统一格式说明。实际开发中很多团队的问题就出在接口格式不统一有的直接返回字符串有的字段名大小写混乱调试成本翻倍。3. 核心模块源码解析预约流程怎么从0到1落地3.1 表结构与时段管理日期是日期时段是时段先看数据库设计这是整个项目的地基。预约一个场地本质上要确定三件事哪天、哪个时段、哪个项目/场地。最容易出现的建模错误是把日期和时段混在一个字段里比如存一个“2025-06-01 18:00~19:00”的字符串后续做日期筛选和时段统计时痛苦到怀疑人生。我的做法是拆开。project表存项目基础信息包括名称、位置、封面图、单次最大容量。appointment表存预约记录关键字段是bind_date预约日期、time_slot时段编码、user_id用户标识、status状态其中time_slot用一个字符串编码表示比如“S1”代表18:00-19:00“S2”代表19:00-20:00。编码的好处是修改展示文案时不需要改表前端通过配置映射时段编码和显示文本。CREATE TABLE project ( id int NOT NULL AUTO_INCREMENT, name varchar(64) NOT NULL COMMENT 项目名称, location varchar(128) DEFAULT COMMENT 场地位置, capacity int NOT NULL DEFAULT 1 COMMENT 同一时段最大可预约人数, status tinyint NOT NULL DEFAULT 1 COMMENT 0下架 1上架, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE appointment ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 用户ID, project_id int NOT NULL COMMENT 项目ID, bind_date date NOT NULL COMMENT 预约日期, time_slot varchar(8) NOT NULL COMMENT 时段编码 S1/S2/S3, status tinyint NOT NULL DEFAULT 0 COMMENT 0已预约 1已核销 2已取消, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, cancel_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_date_slot (bind_date, time_slot, project_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里索引很关键。查询一个项目某天某时段是否约满走的就是idx_date_slot这个联合索引。如果不在建表时把这个索引加上后期预约量上来每次查询都全表扫描接口响应时间肉眼可见地变慢。我在源码注释里专门标了这个点就是提醒拿到代码的朋友不要顺手删掉。3.2 预约状态机提交、锁定、核销、取消预约不是“提交了就结束”它背后是一条状态流转链。这套源码里我定义了四种状态0已预约、1已核销、2已取消另外还有一个隐藏的“已锁定”概念用事务来实现。为什么需要状态机因为用户可能取消管理员可能核销数据在不同阶段有不同的语义。如果没有状态字段后续统计预约率、履约率都会无从下手。设计中我把状态流转控制在以下三个方向已预约 - 已核销用户到场后管理员确认完成履约。已预约 - 已取消用户主动取消或管理员手动取消。已核销 / 已取消终态不可再变。取消操作有一个限制只能取消未来时间的预约不能取消已经发生过的时间段。这个判断用bind_date和当前日期做比较如果bind_date小于今天直接返回“当前时间已过无法取消”。逻辑很简单但很多人会漏掉导致后台出现一堆“昨天约了但没去”的脏数据。3.3 防超卖与并发控制的实现思路预约最大的技术难点不是CRUD而是并发。想象一个场景动感单车房每个时段只有5个名额第6个人提交预约时刚好第5个人还没释放如果代码不做控制两个人同时提交都可能判定成功。最简单的做法是用数据库行锁。在创建预约前先查询当前时段已预约数量然后插入记录但这个“查询-判断-插入”如果不在一个事务里并发照样穿帮。所以我的实现是直接利用数据库的唯一约束来做兜底同时配合事务Transactional public AppointmentResult createAppointment(AppointmentRequest request) { // 1. 查询项目信息 Project project projectMapper.selectById(request.getProjectId()); if (project null || project.getStatus() ! 1) { return AppointmentResult.fail(项目不存在或已下架); } // 2. 统计当前时段已预约数量 Integer used appointmentMapper.countActive( request.getBindDate(), request.getTimeSlot(), request.getProjectId()); if (used project.getCapacity()) { return AppointmentResult.fail(该时段已约满); } // 3. 插入预约记录让数据库的唯一约束兜底 Appointment appointment new Appointment(); appointment.setUserId(request.getUserId()); appointment.setProjectId(request.getProjectId()); appointment.setBindDate(request.getBindDate()); appointment.setTimeSlot(request.getTimeSlot()); appointment.setStatus(0); appointmentMapper.insert(appointment); return AppointmentResult.success(); }光看这段逻辑其实还有一个并发漏洞两个请求同时通过第2步的count判断然后同时走到第3步可能会插入两条记录。所以我建表时加了一个唯一约束字段组合是project_id bind_date time_slot user_id。这样做让同一个用户不能在同一时段重复预约如果两个请求同时插入数据库只会让一个成功另一个报重复键异常我在异常捕获里转成“请勿重复预约”的提示。这里要说明的是这个方案在小规模预约场景下足够但如果你的项目峰值达到几千上万的并发就需要引入Redis分布式锁或者使用数据库悲观锁for update。源码里保留的是轻量级方案适合起步阶段扩展思路我在文末一起讲。3.4 小程序端预约页面的状态渲染逻辑小程序前端的预约页面最核心的是处理“可约/约满/已约”三种状态。后端列表接口返回每个时段的对象包括slot、displayText、available、booked四个字段前端wxml通过条件渲染展示样式。状态处理有个细节很容易让新手卡壳用户进入页面后如果一直停留在当前页面别人把名额抢了这时用户看到的还是“可约”。解决方案有两个层面简单的是提交时后端再次校验失败就提示体验更好的是页面onShow时重新拉取列表接口保证每次从后台回到小程序时数据刷新。源码里两个都做了前端在提交时await create接口失败则toast错误信息并刷新列表。4. 部署联调中的坑从小程序后台到后端服务的完整链路4.1 小程序端配置AppID和请求域名拿到源码的第一个操作不是跑代码而是先改配置。打开project.config.json把appid替换成你自己的小程序AppID。如果你只是本地预览调试可以在开发者工具里勾选“不校验合法域名”这样请求http://localhost或内网IP都能发出去。但上线前必须通过微信公众平台配置request合法域名域名必须是HTTPS且ICP备案过的。这一步踩坑概率极高。很多新手把代码跑起来后发现请求全部失败打开调试器一看是“url not in domain list”报错。这不是代码问题是域名配置问题。本地联调阶段直接把“不校验合法域名”打开就可以忽略这个报错。重要的是记得上线前关闭调试模式否则真机体验版一样请求失败。还有一个小程序里容易忽略的地方request的header里需要带上token。源码的utils/request.js拦截了所有响应如果code返回401说明token过期自动跳转到登录页重新授权。这是一个约定如果你二次开发时新加了接口记得保持这个返回码的约定否则前端不会正确处理。4.2 后端运行环境JDK版本、MySQL初始化、配置文件后端是标准的Spring Boot工程JDK要求1.8以上Maven构建。首先创建一个数据库命名为fitness_appointment然后把源码目录下的sql/init.sql导入。初始化脚本里包含了建库、建表、插入基础项目数据的语句。配置文件的重点是application.yml需要改动的地方就三处数据库连接地址、数据库账号密码、服务启动端口。其他像Mybatis的mapper-locations路径是约定值如果没专门动过目录结构就保持默认。启动命令很简单cd到项目根目录执行mvn spring-boot:run。如果启动日志里出现Tomcat started on port(s): 8080说明后端服务已经起来了。我用postman先测一个接口——GET /project/list返回结果中有项目列表的JSON数组就证明数据库连接和Mapper层都没问题。4.3 部署到服务器时的常见报错排查本地跑通之后部署到云服务器会遇到一批新问题我把排查经验列出来都是我实际处理过的端口不通后端启动了但小程序请求不到。先确认云服务器安全组里是否放行了对应端口比如默认8080。另外Spring Boot的启动端口不要用80避免和Web服务器冲突。数据库连接失败检查MySQL是否允许远程连接。云数据库一般没问题但如果你是自建的MySQL默认可能只绑定了127.0.0.1需要修改配置文件并授权远程用户。HTTPS证书缺失小程序正式版强制要求HTTPS。最省事的方案是给域名配置Nginx反向代理用Certbot自动申请免费SSL证书再把/api路径代理到后端服务的8080端口。跨域问题小程序端请求不存在浏览器跨域限制但如果你用Postman测试或后续要加Web管理后台后端需要配置跨域过滤器。源码里我加了一个CorsFilter允许所有来源访问正式环境建议收紧。部署时的建议是先用IP端口测试再用域名HTTPS测试每一层都通了再切正式环境。不要一上来就把小程序配置成正式域名联调半天分不清是后端挂了还是证书没配好。5. 二次开发指南怎么把这套源码改造成自己的预约系统5.1 源码目录结构与定制化入口我拿到一套源码时不会急着改代码而是先花半小时梳理目录。这个项目的结构很简单前端小程序部分pages/booking是预约页pages/my是个人中心pages/index是首页项目列表后端部分controller放接口入口service写业务逻辑mapper是数据库访问层。如果你要改造优先关注三个位置小程序端api.js里定义的接口地址前缀后端application.yml里的数据库配置以及后端ProjectController.java里的接口路由。大多数定制需求——比如增加预约项目字段、修改时段数量、调整每日可预约次数在这三个位置附近就能完成不需要动核心引擎。5.2 我建议的功能扩展方向这套源码是骨架但骨架意味着可塑性很强。按实际需求我推荐加这三个方向私教课预约在project表增加一个type字段区分“场地”和“课程”课程需要关联教练ID预约时校验教练当天的时间冲突。改动集中在后端预约校验逻辑。会员卡绑定把user表扩展一个会员等级字段预约时校验是否持有有效会员卡。这需要引入卡种表、用户卡关系表工作量小但对健身房的运营价值很大。团课抢位提醒结合微信订阅消息在开抢前24小时推送提醒。订阅消息需要在小程序端申请模板ID后端在发送前先查用户是否订阅了该消息类型。这三个方向任何一个做出来这套源码的价值都能再上一个台阶。我不建议一上来就加支付功能因为涉及商户号、微信支付回调、退款流程复杂度会膨胀得很厉害。5.3 上线前必须过一遍的检查清单我从实际项目里总结了一份检查清单每次上线前逐项打勾后端接口加入登录拦截避免未授权访问。源码里用了一个简单的Interceptor检查所有/appointment开头的请求如果header里没有token直接返回401。小程序端把request合法域名改成正式域名关闭“不校验合法域名”选项。数据库备份策略至少每天一次自动备份预约数据是核心资产丢了很难找回。异常日志输出到文件方便线上排查。Spring Boot里配置logback把error级别单独输出到error.log。测试一遍完整用户旅程授权登录 - 浏览项目 - 选择时段提交预约 - 我的预约里看到记录 - 取消预约 - 时段名额释放。最后一项特别容易被忽略。很多人部署完只测“预约成功”就上线结果取消功能在线上出问题用户体验直接崩。健身体验本来就讲究顺滑预约环节卡壳一次用户很可能就流失了。6. 并发量上来之后这套架构可以怎么演进先声明这套源码的设计目标是支撑单店或小型连锁健身房的预约需求日活几百到几千都没问题。但如果你的运营活动带来瞬时流量暴涨比如双十一促销同时几千人涌入预约就需要对架构做几个针对性的升级。第一步是加缓存。把每个项目、每个时段的剩余名额缓存到Redis里读接口先查缓存写接口更新数据库后同时更新缓存。这一步能把数据库的读压力降掉90%以上。需要注意的是缓存和数据库的一致性问题简单方案是写操作完成后主动删除缓存下次读取时回源重建而不是去更新缓存能少踩很多坑。第二步是引入分布式锁。当缓存判断“名额不为0”后真正扣减名额时用Redis分布式锁锁住project_id bind_date time_slot这个key防止同一时段的并发扣减。单机版可以直接用Spring的Lock注解或者直接用数据库的for update。分布式场景下Redis锁更合适。第三步是异步化。取消预约时的“释放名额”操作以及推送订阅消息的通知都可以丢进消息队列异步处理让用户请求的响应时间不受这些旁路逻辑拖累。这个阶段一般出现在日活过万之后初期不需要但心里要有这根弦。演进方案说到底就是一个原则把热点数据从持久层往前端推一层一层加保护而不是让所有请求都打到MySQL上。预约系统的瓶颈几乎都集中在读多写少的特征上只要缓存策略得当扛住十倍流量不是问题。我个人在实际操作中的感受是预约类小程序最忌讳一上来就设计得重。先跑通核心流程确认用户愿意用再逐步加功能、抗并发。这套源码就是按这个思路组织的拿到手之后最该做的事不是膜拜代码而是把它当起点往自己的业务方向添砖加瓦。如果哪天你把它改造成了私教预约版欢迎来和我分享你的实现方案。