去年夏天,我帮本地一家游泳馆做了一套小程序,核心解决三件事:分时段预约、扫码核销、会员次卡管理。上线三个月,前台排队时间从平均8分钟降到1分钟以内。这篇文章把踩过的坑和关键实现整理出来,给准备做同类项目的朋友参考。
一、先想清楚业务模型
游泳馆和普通健身房不同,它的瓶颈在泳池承载量。一个50米标准池,同时段超过120人体验就会急剧下降。所以产品设计的第一原则是:预约必须按场次切分,而不是按天。
- 场次划分:早场 06:30-09:00、上午场 09:30-12:00、下午场 14:00-17:00、晚场 18:00-21:00
- 每场次独立库存,独立核销
- 会员次卡按“次”扣减,而非按时间
如果你一上来就做“全天畅游”,后面一定会被高峰期投诉淹没。
二、技术选型
个人开发者做这类项目,我推荐微信小程序原生 + 云开发。理由很直接:
- 不用买服务器、不用备案,云函数直接跑业务逻辑
- 云数据库自带权限控制,前台只能读、云函数才能写
- 支付走微信支付云调用,省去证书配置的麻烦
如果后期要接抖音小程序或支付宝,再考虑用 uni-app 重构。前期别过度设计。
三、数据库设计
核心集合只有四个,保持简单:
// sessions 场次
{
_id: "s_20240801_am",
date: "2024-08-01",
startTime: "09:30",
endTime: "12:00",
capacity: 120,
booked: 87,
status: "open"
}
// orders 订单
{
_id: "o_xxx",
openid: "oXXXX",
sessionId: "s_20240801_am",
type: "single", // single | card
amount: 3800,
status: "paid", // paid | used | refunded
code: "8F3K2A"
}
// cards 次卡
{
openid: "oXXXX",
total: 30,
remain: 22,
expireAt: "2024-12-31"
}
// checkins 核销记录
{
orderId: "o_xxx",
staffOpenid: "oYYYY",
checkinAt: Date.now()
}四、预约库存的并发问题
这是最容易翻车的地方。用户A和用户B同时下单最后一个名额,如果先查再写,必然超卖。
正确做法是在云函数里用原子自增 + 条件更新:
const db = cloud.database()
const _ = db.command
// 先原子占位,只有 booked < capacity 才成功
const res = await db.collection('sessions')
.where({
_id: sessionId,
booked: _.lt(capacity),
status: 'open'
})
.update({
data: { booked: _.inc(1) }
})
if (res.stats.updated === 0) {
return { code: -1, msg: '该场次已满' }
}
// 占位成功后再创建订单
// 若后续支付失败,在超时任务里回滚 booked: _.inc(-1)注意一定要配一个15分钟未支付自动回滚的定时云函数,否则库存会被恶意占单吃光。
五、核销码的设计
前台核销要快,扫码到出结果最好在1秒内。我的方案是:
- 订单支付成功后生成6位短码,同时存一份到
orders.code - 核销端(员工版小程序页)调用
wx.scanCode拿到码后,直接调云函数校验 - 校验逻辑:订单存在 → 状态为 paid → 场次为当天 → 未核销过
exports.main = async (event) => {
const { code, staffOpenid } = event
const order = await db.collection('orders')
.where({ code, status: 'paid' }).get()
if (!order.data.length) return { ok: false, msg: '无效码' }
const o = order.data[0]
// 当天有效校验,防止提前核销
const today = new Date().toISOString().slice(0, 10)
const session = await db.collection('sessions').doc(o.sessionId).get()
if (session.data.date !== today) return { ok: false, msg: '非当日场次' }
await db.collection('orders').doc(o._id)
.update({ data: { status: 'used', usedAt: Date.now() } })
await db.collection('checkins').add({
data: { orderId: o._id, staffOpenid, checkinAt: Date.now() }
})
return { ok: true, name: o.userName }
}六、几个容易忽略的细节
- 退票规则要写死在代码里:开场前2小时可退,之后只能改签。别指望前台手动处理,会乱。
- 未成年人陪同:1.2米以下儿童通常免票但需占名额,库存计算要把陪同人数算进去。
- 次卡扣减用事务:云开发支持
db.startTransaction(),扣次和写核销记录必须一起成功。 - 数据看板给老板看:今日各场次预约率、核销率、次卡剩余总次数。这三个指标能直接指导排班。
七、上线后的效果
这套小程序开发周期约三周,成本主要是云开发资源费(月均不到50元)。上线后最明显的改变是:
前台从“接电话+登记本”变成“扫一下就行”,高峰期投诉量下降约70%。
如果你也想做游泳馆小程序,建议先从单店、单泳池、四个场次跑通闭环,别一上来就做连锁多门店。业务跑顺了,再抽象多租户结构也不迟。
评论 (0)
还没有评论,快来抢沙发吧~