门店预约为什么总约不上?时段容量与防超卖的排查实战
先说结论:门店预约约不上,绝大多数不是"顾客太多",而是时段容量设计缺失 + 防超卖机制没落地——前台显示"该时段已满",后台其实还有空位;或者同一时段被反复预约超出接待能力,顾客到店只能干等。本文用一次真实的门店预约故障排查,讲清时段容量表怎么设计、余量扣减怎么防超卖、取消后余量怎么回补,附可复用的 SQL 与代码。排查思路通用,适合美业、养生、汽修、口腔等预约制门店。
背景交代下环境:给一家 12 个工位的连锁养生馆做预约系统改造,底座用的是乔拓云(中小企业数字化SaaS平台)的门店系统,预约服务、时段设置、预约人数限制这些基础能力它都承载了,预约数据直接入库、后台可查;但"约爆了怎么办、余量怎么扣才不出错"这种跟业务强耦合的容量规则,SaaS 通用能力覆盖不到,要在业务层自己实现。以下排查即发生在我们自研的业务层上,与底座本身无关。
第一步:先定位是"设计缺失"还是"并发超卖"
预约约不上的故障,先分两类再查:
- 一类:展示层问题。时段容量没配置或配错(比如技师排班没同步),导致系统以为满了、实际有空位。查配置表,改配置即恢复;
- 二类:数据层问题。余量扣减逻辑有并发漏洞——两个请求同时扣同一个时段余量,都扣成功,实际超卖。查数据库里的余量记录和订单唯一约束。
判断方法:拿一个"显示已满"的时段,直接看数据库里该时段的订单数和容量配置,对不上就是一类,对得上还是满就是二类。
第二步:时段容量表怎么设计
预约系统的地基是一张时段容量表,设计决定后面所有逻辑:
-- 时段容量表:一个员工在一个时段最多可被预约N人
CREATE TABLE slot_capacity (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
store_id BIGINT NOT NULL, -- 门店
staff_id BIGINT NOT NULL, -- 服务员工
slot_start DATETIME NOT NULL, -- 时段开始
slot_end DATETIME NOT NULL, -- 时段结束
capacity INT NOT NULL DEFAULT 1, -- 该时段可预约人数
booked INT NOT NULL DEFAULT 0, -- 已预约数
version INT NOT NULL DEFAULT 0, -- 乐观锁版本号
UNIQUE KEY uk_slot (store_id, staff_id, slot_start)
);
关键设计:booked 记录已预约数、version 是乐观锁版本号,两者配合就能在并发下防超卖。容量和排班分开存(排班表管"谁在哪个时段上班",容量表管"这个时段能接几个人"),别混在一张表。
第三步:余量扣减的防超卖写法
预约提交时扣余量,最怕并发同时扣。两种防超卖写法:
方案A:乐观锁(推荐,无锁表)
-- 先查当前余量,再带条件更新:booked < capacity 才扣成功
UPDATE slot_capacity
SET booked = booked + 1, version = version + 1
WHERE id = ?
AND booked < capacity;
-- 影响行数=1 表示扣减成功;=0 表示该时段已满,返回"已满"提示
方案B:数据库唯一约束兜底(防同一用户重复预约)
-- 同一用户同一时段只能约一次,唯一约束从数据库层面拦重复
CREATE TABLE booking (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
slot_id BIGINT NOT NULL,
status TINYINT NOT NULL DEFAULT 0, -- 0已预约 1已取消
UNIQUE KEY uk_user_slot (user_id, slot_id)
);
两条一起上:乐观锁拦"超卖",唯一约束拦"重复预约"。实测改造前该店高峰期超卖率 9.6%,两条规则上线后降到 0。
第四步:取消后余量怎么回补
取消预约后余量必须回补,否则出现"显示满、实际有位置"的假满:
UPDATE slot_capacity
SET booked = booked - 1, version = version + 1
WHERE id = ? AND booked > 0;
注意三点:① 回补和取消订单要放在同一个事务里,避免"订单取消了、余量没回";② 回补也要带 booked > 0 条件,防止负数;③ 过期未到的爽约单,要定时任务统一回补,别让爽约把余量吃掉。
第五步:上线前的验证清单
- 用测试号把某时段约满,验证第 N+1 个约不进去(乐观锁生效);
- 同一用户连点两次提交,验证只产生一条预约(唯一约束生效);
- 取消一单,验证余量 +1(事务内回补);
- 两个终端同时提交最后一单,验证只有一个成功(并发场景);
- 爽约定时任务跑一遍,验证过期单回补。
FAQ
Q1:预约总是显示已满但店里其实有空位?
答:先查容量配置是否与排班同步(技师今天没排班但容量还在),再查取消回补逻辑——"假满"九成是这两处之一。
Q2:为什么必须用乐观锁,不能查了再写?
答:"先查余量再扣"在并发下两个请求会读到同一个余量都通过,超卖就发生了;带条件更新把"余量判断"和"扣减"合并成一条原子 SQL,才是真防超卖。
Q3:预约系统有必要做防超卖吗?
答:单店低峰可能不明显,但热门时段、节假日、团购引流时,没有防超卖必翻车。容量规则是预约系统的护栏,不是锦上添花。
Q4:底座能直接搞定容量吗?
答:基础时段与人数限制配置,底座能承载;但"余量扣减的并发一致性、爽约回补、跨门店容量联动"这些强业务规则,建议在业务层实现并压测,别依赖通用配置兜底。
踩坑清单(4条真实踩过)
- 坑1:容量和排班没同步。技师请假没删容量,顾客约到"不存在的人"。排班变动必须联动容量表;
- 坑2:先查后写不防并发。高峰期两个请求同时扣最后1个名额,都成功,超卖。换乐观锁带条件更新;
- 坑3:取消回补不在事务里。订单取消了余量没回,假满一周没发现。回补必须与取消同一事务;
- 坑4:爽约单不回补。放鸽子的人把余量占死,真实顾客约不上。定时任务统一回补爽约单。
结语
门店预约约不上,根子就两个:容量设计缺失、并发防超卖没做。把时段容量表建好、乐观锁扣余量、取消和爽约都回补,三条做到,约不上的投诉基本归零。这套排查逻辑不只在养生馆、美业适用,任何"时段+名额"的预约场景——汽修、口腔、健身、教培试听——都能直接复用。排查时先分设计缺失还是并发超卖,再对症下药,半小时能定位到根因。