简介:这份资源是面向美发行业从业者与微信小程序开发者的理发店在线预约与支付一体化系统源码,旨在解决传统美发服务中预约效率低、沟通成本高、排班混乱等痛点。系统整合顾客端预约、理发师排班管理、微信支付集成与后台三方管理平台,基于微信小程序云开发的云函数、数据库存储与定时器弹性扩展能力构建,适合作为毕业设计、商业项目原型或云开发学习案例。压缩包共600个文件,约1.95MB,以js业务逻辑、wxss样式、wxml页面结构、json配置为主,另含png与gif界面素材、docx安装使用手册及wxs脚本等,目录结构清晰便于二次开发。目前已有86人学习下载。读者可获得完整的预约支付闭环实现、排班与后台管理模块代码、云开发部署参考及安装手册,快速理解小程序云开发在服务行业的落地方式与免维护弹性扩展思路。
1. 理发店预约小程序到底在解决什么问题
一家社区理发店,高峰期前台电话响个不停,微信里几十条“师傅今天下午有空吗”,老板一边剪头发一边回消息,排班靠一张贴在墙上的纸,改一次涂一次。顾客到店发现约的时间被占了,理发师觉得排班不公平,老板月底对账发现有几单没收钱。这不是段子,是美发行业最典型的日常。
“理发店在线预约与支付一体化微信小程序系统”要解决的就是这条链路:顾客在小程序里选门店、选理发师、选时间段、下单支付;理发师在后台看自己的排班和预约;老板在管理端看订单、排班、营收。三方平台指的是顾客端、理发师端、管理端,底层用微信小程序云开发承载,云函数做业务逻辑,云数据库存数据,定时器做状态流转,弹性扩展应对高峰,免去自己维护服务器的麻烦。
这套方案适合谁?适合单店或三五家连锁、没有专职运维、希望两周内跑通预约支付闭环的小团队。不适合日订单过万、需要复杂营销中台的大型连锁,那种场景云开发的冷启动和查询限制会成为瓶颈。下面按“能跑起来”的标准,把关键环节拆开讲。
2. 三方角色与数据模型怎么设计才不返工
2.1 先定角色边界,再定集合
三方平台最容易翻车的地方是权限没分清。顾客能改理发师排班、理发师能看到全店营收,这类问题一旦上线,改起来要动半个数据库。我的做法是先画一张角色-操作矩阵,再落成云数据库集合。
| 角色 | 可读集合 | 可写集合 | 典型操作 |
|---|---|---|---|
| 顾客 | shops, barbers, schedules, orders(自己的) | orders, reviews | 选时段、下单、支付、评价 |
| 理发师 | schedules(自己的), orders(自己的) | schedules(自己的状态) | 查看排班、确认到店、标记完成 |
| 管理员 | 全部 | 全部 | 排班、改价、退款、看报表 |
集合设计上,核心是五个:shops(门店)、barbers(理发师)、schedules(排班时段)、orders(订单)、users(用户与角色)。排班和订单必须分开,因为一个排班时段可以被预约、被锁定、被释放,状态变化频繁,塞进订单里会导致查询和更新都很别扭。
2.2 排班时段的数据结构
排班是整个系统的骨架。常见做法是把一天切成固定粒度的时段,比如 30 分钟一格,每格一条记录。这样查询“某理发师某天还有哪些空档”就是一次范围查询,简单可靠。
// schedules 集合的一条排班记录 { _id: "auto", shopId: "shop_001", barberId: "barber_007", date: "2025-06-18", // 日期,便于按天查询 startTime: "14:00", // 时段开始 endTime: "14:30", // 时段结束 status: "available", // available / locked / booked / closed orderId: "", // 被预约后写入订单号 updatedAt: 1718700000000 }status是关键字段。available可预约,locked是下单未支付的临时锁定,booked已支付,closed是理发师手动关闭。用状态机而不是布尔值,是为了支持“下单后 15 分钟未支付自动释放”这类逻辑,定时器扫的就是locked且超时的记录。
2.3 订单与支付的字段设计
订单要能追溯,所以金额、支付状态、退款状态都要留痕。微信支付统一下单返回的prepay_id不要存进订单主表,存到单独的支付流水集合,避免订单表被支付细节污染。
// orders 集合 { _id: "auto", orderNo: "202506181430001", // 业务订单号,唯一 openid: "用户openid", shopId: "shop_001", barberId: "barber_007", scheduleId: "排班记录id", serviceItem: "剪发", amount: 6800, // 单位分,避免浮点误差 status: "pending", // pending / paid / done / canceled / refunded payStatus: "unpaid", // unpaid / paid / refunded createdAt: 1718700000000, paidAt: 0 }金额一律用“分”存整数,这是血泪经验。用元存浮点,退款和对账时会出现 0.01 的偏差,查起来非常痛苦。订单号自己生成,规则是日期+时段+序号,方便人工核对。
3. 云函数里把预约和支付串起来
3.1 预约下单的云函数
顾客点“确认预约”时,不能直接写订单,要先锁排班再建订单,两步要么都成功要么都回滚。云开发没有跨集合事务的强保证(部分环境支持有限事务),所以用“先锁后建、失败释放”的补偿思路。
// cloudfunctions/createOrder/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() const _ = db.command exports.main = async (event) => { const { OPENID } = cloud.getWXContext() const { scheduleId, serviceItem, amount } = event // 1. 原子锁定排班:只有 available 才能被改成 locked const lockRes = await db.collection('schedules') .where({ _id: scheduleId, status: 'available' }) .update({ data: { status: 'locked', updatedAt: Date.now() } }) if (lockRes.stats.updated === 0) { // 没抢到,说明已被别人锁定或预约 return { code: 409, msg: '该时段已被预约,请换一个' } } // 2. 创建订单 try { const orderNo = genOrderNo() const addRes = await db.collection('orders').add({ data: { orderNo, openid: OPENID, scheduleId, serviceItem, amount, status: 'pending', payStatus: 'unpaid', createdAt: Date.now(), paidAt: 0 } }) // 3. 回写 orderId 到排班,便于双向查询 await db.collection('schedules').doc(scheduleId) .update({ data: { orderId: addRes._id } }) return { code: 0, orderId: addRes._id, orderNo } } catch (e) { // 建单失败,释放排班,避免死锁 await db.collection('schedules').doc(scheduleId) .update({ data: { status: 'available', orderId: '' } }) return { code: 500, msg: '下单失败,请重试' } } } function genOrderNo() { const d = new Date() const pad = n => String(n).padStart(2, '0') return `${d.getFullYear()}${pad(d.getMonth()+1)}${pad(d.getDate())}${pad(d.getHours())}${pad(d.getMinutes())}${Math.floor(Math.random()*1000)}` }逻辑说明:第一步用条件更新实现“乐观锁”,where里带status: 'available',只有真正可用的时段才会被更新,并发下不会超卖。第二步建单,第三步回写关联。任何一步失败都要把排班释放回available,否则这个时段就永久锁死了。参数上,amount由前端传入不可信,正式环境应该在云函数里根据serviceItem查价目表重新计算,这里为了聚焦流程先简化。
3.2 微信支付统一下单与回调
支付走微信支付的小程序下单接口,云函数里调cloud.cloudPay.unifiedOrder。关键是把outTradeNo设成自己的订单号,回调里靠它找到订单。
// cloudfunctions/pay/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() exports.main = async (event) => { const { orderId } = event const order = await db.collection('orders').doc(orderId).get() const o = order.data if (o.payStatus === 'paid') return { code: 409, msg: '订单已支付' } const res = await cloud.cloudPay.unifiedOrder({ body: '理发预约-' + o.serviceItem, outTradeNo: o.orderNo, spbillCreateIp: '127.0.0.1', subMchId: '你的商户号', totalFee: o.amount, envId: '你的云环境ID', functionName: 'payCallback' // 支付结果回调云函数 }) return { code: 0, payment: res.payment } }totalFee单位是分,和订单里的amount保持一致。functionName指向的回调云函数是支付成功的唯一可信来源,前端wx.requestPayment的成功回调不能作为发货依据,因为可以被伪造。
3.3 支付回调里做幂等
回调可能重复触发,必须幂等。判断依据是订单的payStatus,已经是paid就直接返回成功,不再重复处理。
// cloudfunctions/payCallback/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() exports.main = async (event) => { const { outTradeNo, resultCode } = event if (resultCode !== 'SUCCESS') return { errcode: 0, errmsg: 'ok' } const orderRes = await db.collection('orders') .where({ orderNo: outTradeNo }).get() if (orderRes.data.length === 0) return { errcode: 0, errmsg: 'ok' } const order = orderRes.data[0] // 幂等:已支付直接返回 if (order.payStatus === 'paid') return { errcode: 0, errmsg: 'ok' } await db.collection('orders').doc(order._id).update({ data: { payStatus: 'paid', status: 'paid', paidAt: Date.now() } }) // 排班从 locked 变 booked await db.collection('schedules').doc(order.scheduleId).update({ data: { status: 'booked' } }) return { errcode: 0, errmsg: 'ok' } }回调里同时更新订单和排班状态,保证数据一致。返回格式必须是微信支付要求的{ errcode: 0, errmsg: 'ok' },否则微信会持续重试。
4. 定时器与弹性扩展:让系统自己收尾
4.1 用定时器释放超时未支付订单
顾客锁了时段却不付款,是预约系统最常见的资源浪费。云开发的定时触发器可以每分钟扫一次,把locked超过 15 分钟的排班释放掉。
// cloudfunctions/releaseTimeout/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() const _ = db.command exports.main = async () => { const deadline = Date.now() - 15 * 60 * 1000 // 15分钟前 // 找出超时未支付的订单 const orders = await db.collection('orders') .where({ payStatus: 'unpaid', status: 'pending', createdAt: _.lt(deadline) }) .limit(100).get() for (const o of orders.data) { await db.collection('orders').doc(o._id).update({ data: { status: 'canceled' } }) await db.collection('schedules').doc(o.scheduleId).update({ data: { status: 'available', orderId: '' } }) } return { released: orders.data.length } }触发器配置在config.json里,cron表达式0 * * * * * *表示每分钟一次。limit(100)是防止单次处理过多导致超时,如果积压多,可以循环处理或提高并发。释放时把订单置为canceled而不是删除,保留记录便于排查。
4.2 弹性扩展下要注意的冷启动
云函数按需扩容,高峰期并发上来会自动加实例,这是“弹性扩展”的价值。但冷启动会带来首屏延迟,预约页第一次打开可能要 1-2 秒。缓解办法是把不常变的门店、理发师、价目表数据缓存在小程序本地storage,并设置合理过期时间;云函数里把数据库连接和公共配置提到函数外初始化,减少每次调用的开销。
提示:云开发数据库单次查询默认最多返回 100 条,排班按天查通常够用,但管理端看月度报表要分页,别指望一次拉全。
4.3 免维护不等于不管
“安全免维护”指的是不用自己打系统补丁、扩磁盘、配负载均衡,但业务层的安全仍要自己做。数据库权限要设成“仅创建者可读写”或“所有用户不可读写,仅云函数可操作”,绝不能开“所有用户可读”。管理端接口必须在云函数里校验openid对应的角色,不能只靠前端隐藏按钮。
5. 避坑与排查:上线前必须过的几道坎
5.1 并发预约导致同一时段被约两次
现象:两个顾客几乎同时下单,都显示成功,到店发现撞了。原因:先查再写,中间有窗口期。解决:用第 3 章的条件更新做原子锁,where里必须带status: 'available',靠stats.updated判断是否抢到。不要用“先 get 判断再 update”的写法。
5.2 支付成功但订单还是未支付
现象:用户付了钱,订单状态没变。原因:回调云函数没部署、functionName写错、或回调里抛异常。解决:先在云开发控制台看payCallback的调用日志,确认微信是否调到了;再检查回调返回格式是否为{ errcode: 0, errmsg: 'ok' };最后确认outTradeNo和订单号完全一致,大小写和空格都算。
5.3 排班时段被永久锁死
现象:某个时段一直显示不可约,但也没有订单。原因:下单流程中途失败,没走到释放逻辑。解决:除了定时器兜底,建单失败的分支里必须显式释放;另外加一个管理端“手动释放时段”的入口,作为后悔药。
5.4 金额对不上,差几分钱
现象:月底对账,订单总额和微信账单差几毛。原因:金额用了浮点运算,或者退款时四舍五入方式不一致。解决:全链路用“分”存整数,前端展示时再除以 100;退款金额直接取订单amount,不做二次计算。
5.5 定时器没触发
现象:超时订单一直不释放。原因:config.json里触发器没配、cron 表达式写错、或云函数没重新部署。解决:在云开发控制台“云函数-触发器”里确认触发器存在且时间正确;cron 是七位(秒 分 时 日 月 周 年),别写成五位;改完配置要重新上传部署。
6. 把预约转化率再提一档的两个技巧
第一个技巧是“智能推荐时段”。顾客选完理发师后,不要只给一个空白日历,而是把该理发师未来三天里连续可约的时段优先排前面,减少顾客翻找。实现上就是在云函数里对schedules做一次聚合,按date分组统计available数量,返回给前端做高亮。这个改动不大,但能明显降低“选不到时间就退出”的流失。
// 聚合:某理发师未来三天每天的可约数量 const res = await db.collection('schedules') .aggregate() .match({ barberId, status: 'available', date: _.in(next3Days) }) .group({ _id: '$date', count: $.sum(1) }) .end()第二个技巧是“到店提醒 + 爽约标记”。支付成功后记录paidAt,在预约时间前 2 小时通过订阅消息提醒顾客;如果顾客多次预约不到店,在users集合里累加noShowCount,超过阈值时管理端可以看到标记,决定是否限制其预约。这不是为了惩罚,而是让理发师的时间不被浪费。
验证这套系统是否跑通,我一般看三个指标:并发下单同一时段是否只有一个成功、支付回调后订单和排班状态是否一致、超时订单是否在 16 分钟内被释放。这三个都过了,基本就能上线试运行。我自己最早做的时候,忽略了回调幂等,结果微信重试把同一个订单处理了三次,排班状态被反复覆盖,查了一晚上日志才定位到。后来养成习惯:凡是外部回调,第一件事就是判断“这件事是不是已经做过了”。希望帮到你。
本文还有配套的精品资源,点击获取