一节早上6点的课,容量写着14人。有人晚上9点订了,凌晨5点40分取消,位置空着,而候补名单上三个人还在睡觉。外地来的散客没签免责声明就出现在门口。两个会员在同一秒点了“预订”最后一个名额,教练最后面对15个人和14台划船机。

打开网易新闻 查看精彩图片

这类混乱在美国和加拿大的健身房通常靠管理软件加一个群聊解决。另一条路是自己搭一套小系统,把场馆真正在跑的规则写进去。真正难的部分——数据模型、最后一个名额、候补名单、时钟——换任何工具都一样存在。

先把规则写下来,再动手

大多数预约系统的毛病,源头是一条没人写下来的规则。动手前先回答五个问题,它们会直接变成给工具的提示词。

  • 能订什么:WOD课程、自由训练、新人入门课、指定教练的私教,各自的时长和容量不同。
  • 谁能订什么:不限次会员可订任意课程;次卡会员每节课扣一次;散客按次付费且必须先签免责声明;完成入门课的人才能解锁常规课。
  • 开放与关闭时间:例如提前7天开放,开课即关闭。
  • 取消规则:例如开课前2小时之前免费取消,之后算迟到取消;缺席和迟到取消都会被记录,次卡照样扣一次。
  • 空位出现后怎么办:候补第一位自动补位并发邮件,或者给一个短暂的认领窗口。自动更简单,认领窗口对那个可能被排进早上6点课却还在睡觉的人更友好。

每项只能选一个答案。以后可以改,但没法搭一个“看情况”。

描述数据,而不是描述界面

第一条提示词最关键,它决定数据库长什么样。先说清楚场馆里有哪些东西、彼此怎么关联,再谈页面。

需要的数据大致是四类:课程类型(名称、时长、默认容量、是否要求完成入门课);课程场次(类型、教练、开始时间、容量、状态);会员(姓名、邮箱、电话、会员类型、剩余次数、是否完成入门课、免责声明的签署时间与版本);预约(会员、场次、状态、创建时间、取消时间、候补位次)。

规则里要写明:提前7天开放、开课关闭;已订人数永远不超过容量,多出来的按顺序进候补;同一会员不能订时间重叠的两节课。

有三个细节值得注意。时区要一开始就写清楚,因为“早上6点”对一台跑在UTC的服务器毫无意义。容量要放在场次上而不只是课程类型上,这样教练在划船机坏了一半时可以临时缩小某一节课。预约状态要是一个带命名状态的单字段,这样“实际订了多少人”就是一条清晰的查询,而不是三个互相重叠的标记。

应用生成之后,第一件事是打开管理端,建一周的场次,看它到底存了什么。列缺了、状态名和你平时说话不一样,现在就改。等有了真实预约再改名,痛苦得多。

守住最后一个名额

这是最快击穿信任的bug。两个人同时打开课表,都看到“剩1个名额”,都点了预订。如果程序先查数量、再插入记录,分两步走,两次检查都会通过,两条预约都会写进去。

修法是把“查数量并插入”做成数据库里一个原子操作:在事务里锁住该场次那一行,第二个请求就会等待,看到课已满,落到候补名单里。这一步不必自己写,但必须点名要求,因为生成的第一版未必会这么做。

同样重要的是要求一个测试:对容量14的场次同时发20个预订请求,检查结果恰好14条“已订”、6条“候补”。“我点了一下看着没问题”证明不了任何并发问题,并行发请求的测试才能。

取消、候补与迟到取消

把前面定下的规则一次性写进去:开课前2小时之前取消,状态记为“已取消”,次卡退回一次;晚于这个时间,状态记为“迟到取消”,次数不退。有位置释放时,在同一事务里把候补第一位提升为“已订”,扣掉其次数,并发邮件告知课程时间和如何取消。

开课前30分钟内不再补位,把位置留给直接到店的人。课程结束后,教练3小时内没标记出席的“已订”记录转为“缺席”。管理端展示每位会员最近30天的迟到取消和缺席次数。

预览时盯两处。补位必须和取消在同一个事务里,否则中间可能被别人抢走。30分钟的截止线也不能省,否则候补的人可能被排进一节在他读完邮件之前就已开始的课,然后因为一节他根本不知道自己被排进去的课被记成缺席。

发邮件需要接一个邮件服务,API密钥放在项目的密钥管理里,不要写进代码。

散客与免责声明

散客往往是路过本地的陌生人,是你了解最少的会员。加一个不需要账号的公开页面:选场次,填姓名、邮箱、电话和紧急联系人,阅读免责声明并勾选同意,然后完成预约。会员记录上要存下声明的版本和同意时间。教练在名单上看到每个人名字旁的签署标记,对未签署的已订会员给出警告。

声明文本集中存放并做版本管理,这样才能说清某人同意的是哪一版措辞。系统能记录“某人同意了”,但判断这份声明在你所在的地方是否站得住,得找本地律师写或审。多数会员会用手机预订,常常还躺在床上,所以每个页面都要先在手机宽度下检查一遍。

测试时钟

时区bug在出事的那个早上之前都很安静。美国和加拿大大部分地区在11月第一个周日结束夏令时,有些地方(亚利桑那州大部分、萨斯喀彻温省大部分)根本不调时间。一节循环的早上6点课,在调整后的那个周一仍应当地时间6点开始,而不是5点或7点。

做法是:场次开始时间以UTC存储,但循环场次按场馆所在时区生成,这样6点的课跨夏令时仍是当地6点。所有时间按场馆时区展示,哪怕会员的手机设在别的时区。再加一个测试,生成11月夏令时调整前后各周的周一6点WOD,检查两者都显示6点。

上线与边界

测试通过就发布,会立刻得到一个公开网址,付费档位可以绑到自己的域名。然后和现有系统并行跑两周:用会员账号自己订几节课,故意迟到取消一次,用第二个账号进候补,看邮件和状态是否和最初写下的规则一致。

有几条边界要认。支付是另一件事,上面这套只跟踪会员身份和课程次数,刷卡和周期扣费要接支付服务商,还要处理扣款失败。规则归你所有,取消窗口一改,就得有人提出并验证,一句话的事,但仍是你的活。没有现成集成,专用健身软件常能对接训练记录、公开赛计分或门禁,自建应用只能对接你明确要求且能通过接口触达的东西。通知以邮件为主,短信需要短信服务商以及美加商业短信相关的合规手续。

如果这些边界可以接受,你得到的是一套贴合自己场馆的预约系统,而不是让场馆去迁就软件,数据库和代码都归你。