2026 健身预约系统后端实现:预约表设计、冲突检测与课时扣减

(平台提示:本文可能是商业推广软文)

2026 年做健身房、瑜伽馆的预约系统,前端约课页面其实最好写,真正难的是后端那一巴掌看不见的东西:一个课只能坐二十个人,两个会员同一秒抢最后一个名额怎么办?会员约了私教课又临时不来,课时到底扣不扣?这些问题表面是产品规则,根子是预约表结构、冲突检测和课时扣减流程没设计对。这篇把我做约课系统时这几块的表结构、算法和事务处理整理一遍,给要自研或评审方案的后端同行做个参考。
一、核心表结构:别把课程和预约揉成一张表
底层至少四张表:course(课程/团课)、coach(教练)、member(会员)、booking(预约记录)。course 记一次排课,含上课时间、容量、已约人数;booking 是某会员对某节课的一条预约。
CREATE TABLE booking (
id BIGINT PRIMARY KEY,
course_id BIGINT, -- 哪节课
member_id BIGINT, -- 谁约的
status TINYINT, -- 1已约 2已到店 3已取消 4爽约
created_at DATETIME,
UNIQUE KEY uk_course_member (course_id, member_id, status)
);
那个联合唯一索引是给“同一会员不能重复约同一节课”兜底的。注意别把已取消的记录也挡在唯一键里——取消后允许重新约,所以通常把取消记录软删或在唯一键里带上状态变体,靠应用层补一道判断。
二、冲突检测:超卖是必然的
课程容量是二十,同时来三十个约课请求,不做并发控制必然超卖。工程上两道防线。
一层是数据库层。扣已约人数用条件更新,不要先查再改:
UPDATE course SET booked = booked + 1
WHERE id = ? AND booked < capacity;

影响行数为0说明满了,直接返回约满

这条 UPDATE 本身带行锁,并发下只有能满足 booked<capacity 的请求会成功,剩下的返回“名额已满”,比在应用层查完再判断安全得多。
第二道是提前占位。约课请求先写一条 status=待确认 的 booking,拿到名额后再让会员确认,避免用户点了约课却没走完支付/签到,把名额白占着。
三、课时扣减流程:私教课才是重灾区
团课按名额抢,私教课扣的是会员账户里的剩余课时。扣课时要和预约放在同一个事务里,要么约成同时扣课,要么一起回滚。
BEGIN;
INSERT INTO booking(course_id,member_id,status) VALUES(?,?,'已约');
UPDATE member SET balance = balance - 1
WHERE id = ? AND balance > 0;
-- 影响行数0则整体回滚
COMMIT;
取消规则决定课时何时退回。常见做法是开课前 N 小时以上取消,课时原路退回;临近取消或爽约,课时照扣,否则会员会无限临时放鸽子。这条 N 写成可配置参数,别硬编码。
四、签到与爽约
会员到店扫码或刷脸,把 booking 从已约改成已到店。课程结束后扫一遍:还是已约状态的,标记为爽约并按规则扣课。这一步适合用定时任务在每节课结束后批量跑,不用实时。
爽约次数要累计。一个会员连续爽约几次,自动限制他未来一段时间不能约热门课——这个限制靠规则表配,别写死在代码里。
五、候补排队:约满了别直接拒绝
热门课一放出就满,直接告诉会员“约满了”等于把人推给竞品。通行做法是加一个候补队列:课满之后,新请求写一条 status=候补 的记录排队,不占正式名额。
正式预约有人取消、名额空出来时,按候补队列的顺序通知排最前的会员,给他一小段时间确认,超时再顺延给下一位。整个候补晋升也是条件更新——把“候补”改成“已约”只有成功扣到名额的那一条才生效,别让两个候补同时顶上同一个空位。
when_slot_release(course_id):
wait = next_waiter(course_id) -- 队列最早一位
if not wait: return
notify(wait.member); 等待确认窗口
候补机制把“约满”变成了“排队等空位”,会员留存和到课率都会好看不少,工程上只多一张候补表和一个晋升任务。
排课本身也值得说一句。瑜伽馆、单车课这类周期课,每周固定时间开,别让教练每周手动排。做法是做一个课程模板:定好每周几、几点、哪个教练、容量多少,批量生成未来几周的 course 记录。模板和实例分开,改下周的课不影响已经生成的历史排期,会员约的是实例不是模板。教练临时请假要调课,就把这一节实例整体改时间并通知已预约的会员,别去动模板。
六、适用规模与技术局限(复盘)
这套四表加条件更新扣名额、事务化扣课时,对单店、十几名教练、日预约几百条的场馆完全够用。表都不复杂,两周能上线。
规模上来后几个边界要清楚。一是连锁多馆,course 和 booking 都要挂场馆标识,查询强制带上;二是约课并发再高,booking 按 course_id 分表或加缓存预热热门课库存;三是选位(约固定单车、固定瑜伽垫位)会把 booking 和座位再拆一层关系表,那是另一个量级的设计。初期别过度设计,先把“不超卖、不重复扣课”这两件事做扎实。
几个常被问到的点
问:为什么不用分布式锁?答:条件更新的行锁已经足够,多数场馆量级用 Redis 锁反而引入一致性问题,数据库乐观/条件更新最省事。
问:取消退课时怎么保证不重复退?答:退课动作也走条件更新(把已约改成已取消时才退课),状态本身就是幂等键,重复请求不会退两次。
问:会员约了课没来,课时什么时候扣?答:课程结束后定时任务扫未到店记录批量扣,和签到状态联动,不靠人工。

posted @ 2026-09-27 20:22  小富子呀  阅读(10)  评论(0)    收藏  举报