资讯详情

保洁预约小程序开发实战:微信云开发、订单状态机与微信支付全解析

📅 2026/10/25 16:34:23 | 华诺云谱 👁 阅读
保洁预约小程序开发实战:微信云开发、订单状态机与微信支付全解析
简介面向家政服务行业开发者的一份上门预约家庭保洁小程序源码整合预约、服务展示、微信支付、评价、消息推送与定位导航等完整链路可帮助创业者或小程序开发者在现成代码上快速搭建线上保洁服务平台降低从零开发与试错成本。资源共2005个文件压缩包约22.28MB。主要包含小程序前端页面所需的wxml、wxss、js及json文件同时涵盖后台php接口、html管理页面与layui等样式资源gif与png图片用于界面展示整体目录覆盖pages、utils、models、services等模块便于按业务进行二次开发。目前已有211人学习下载。源码提供预约流程、支付对接与服务端处理逻辑示例可基于需求扩展保洁项目、会员系统或第三方支付适合作为家政服务类小程序开发的基础模板与功能参考。1. 为什么“保洁预约小程序”比开发独立 App 更值得投入“上门预约家庭保洁服务小程序源码”说的是把“打电话约保洁、微信群排期、上门再结账”这一长串原始流程压成小程序里“授权手机号—选服务—选时间段—支付—等师傅上门”五个动作。对家政公司来说小程序就是线上门店对开发者来说它是一套完整的微信生态工程手机号授权、下单支付、订单状态机、消息提醒、排期防冲突全在一个工程里闭环。适合三类人保洁公司想自建接单渠道、家政平台做最小成本验证、以及想真正跑通微信支付和订单流转的开发新手。需要先泼一盆冷水小程序注册费不贵但想开通微信支付和“获取手机号”接口主体必须是企业或个体工商户个人主体做不了。2. 先设计订单核心表服务项、上门地址与状态机怎么落库很多第一次做预约类小程序的人一上来就写页面写到支付那一步发现订单字段缺一堆没有服务时长、没有地址经纬度、没有状态流转记录。结果是界面返工、数据库反复改。做这类工具型小程序我一般先把数据模型定死页面只是数据的陈列。2.1 五张表的关系服务表、地址表、订单表、日志表、师傅表预约核心不是一张订单表而是五张表配合。服务表services存“日常保洁2小时”“深度保洁4小时”这类可售商品地址表addresses存上门位置订单表orders存一次预约日志表order_log存状态变化师傅表technicians存保洁师可服务时段。// services 服务项示例 { _id: srv_001, name: 日常保洁·2小时, price: 99, unit: 次, duration_min: 120, skill_tags: [玻璃, 除尘], enabled: true }// addresses 上门地址示例 { _id: addr_001, user_openid: oXXXX, contact_name: 张姐, phone: 13800138000, city: 上海市, district: 浦东新区, detail: 张江路xx弄xx号, lat: 31.205, lng: 121.594, is_default: true }服务表里price的单位是“元”但后面支付时金额单位是“分”这个转换要在云函数里做不要在前端页面里乘 100否则改个价格还要重新发版。地址表单独一张而不是塞进订单表是因为同一个用户可能存了公司、父母家、自己家三个地址下单时只需要选不需要反复输入。经纬度字段最好存派单按距离排序时用得上。订单表orders里不直接存服务名、地址文本只存service_id和addr_id的外键。这样做的好处是服务涨价后历史订单依然能查到当时的价格快照吗不能。所以我在订单表里额外冗余存了service_snapshot和amount_at_order订单创建时把当前服务名称、单价复制进去。这是预约类系统里特别容易踩的坑不存快照三个月后查账发现金额对不上因为服务价格变了。2.2 微信登录与手机号授权新版 getPhoneNumber 的完整参数小程序登录不是传统意义上的账号密码而是wx.login()换 openid配合“获取手机号”按钮拿到用户手机号。老版本的做法是前端拿到encryptedData和iv以后丢给后端用code2Session的 session_key 解密新版本微信出了动态令牌code后端拿这个 code 直接换手机号不再需要解密。如果刚开始做直接走新版。!-- pages/login/login.wxml -- button open-typegetPhoneNumber bindgetphonenumberonGetPhone loading{{phoneLoading}} 微信一键登录/button// pages/login/login.js async onGetPhone(e) { if (e.detail.errMsg ! getPhoneNumber:ok) { wx.showToast({ title: 需要手机号才能下单, icon: none }); return; } // 新版动态令牌5分钟内有效且只能消费一次 const { code, cloudID } e.detail; const res await wx.cloud.callFunction({ name: login, data: { code, cloudID } }); const { openid, phone } res.result; this.setData({ openid, phone }); wx.setStorageSync(openid, openid); }逻辑说明bindgetphonenumber事件里新版e.detail.code是动态令牌一次性使用cloudID是云开发专属能力——手机号数据可以直接在云函数里通过cloud.getOpenData读取前端根本不碰手机号明文隐私合规压力小很多。errMsg一旦不是getPhoneNumber:ok不要做任何提示以外的操作用户点了取消按钮就会触发这个分支。参数说明里最容易被忽略的是「基础库版本」。新版动态令牌要求微信基础库2.21.2以上在app.json里最好声明libVersion: 2.21.2或更高。老项目如果一直没升级基础库这个按钮会在部分安卓机上直接没反应排查时先看版本。2.3 订单状态机为什么不能只改订单字段订单表里status字段的值我定义为PENDING_PAY待支付、PAID已支付待派单、ASSIGNED已派单、WORKING服务中、DONE已完成、CANCELLED已取消。只改订单字段最大的问题是订单是哪个环节变成 DONE 的是用户点的还是保洁师点的中间有没有人误操作出了纠纷连审计证据都没有。所以必须有一张order_log表每次状态变化都追加一条日志订单表和日志表的写入必须在同一事务里完成。// 状态流转换表放在代码注释里当契约用 // PENDING_PAY - PAID 支付成功回调 // PAID - ASSIGNED 后台派单 // ASSIGNED - WORKING 保洁师点击开始服务 // WORKING - DONE 保洁师点击完工 // PENDING_PAY - CANCELLED 用户取消 // PAID - CANCELLED 运营取消并退款有了这张状态机后续所有逻辑像多米诺骨牌一样简单只有PAID状态能派单只有ASSIGNED能开始服务页面按钮的显示全由状态驱动。上线后第 6 章我会讲怎么用日志表做时间轴回放来验证这套状态机。3. 用微信云开发跑通“预约支付”初始化环境到云函数签名的完整链路这里选型我直接说结论个人或小团队做预约类小程序优先用微信云开发而不是自己买服务器部署后端。原因很简单云开发自带数据库、文件存储、云函数免鉴权获取 openid省掉证书和域名备案这一整套麻烦。如果你以后想跨端用 uni-app 基于云开发封装层做迁移也能走通但第一版别分心原生小程序加云开发是阻力最小的路。3.1 初始化云环境和数据库集合在app.js里做全局初始化这一步是后续所有云能力的入口。// app.js App({ onLaunch() { if (!wx.cloud) { console.error(基础库版本过低请升级到 2.2.3 以上); return; } wx.cloud.init({ env: cleaner-prod-xxxx, // 云环境 ID在云开发控制台查看 traceUser: true // 将用户访问记录到云开发控制台 }); } })env必须填实际环境 ID不是cloud1这种默认值。很多人开发时用默认环境上线前切了正式环境结果云函数里写的环境还是旧的下单时openid对不上数据全写到了测试库。traceUser: true会在控制台里按用户维度记录访问记录调试阶段开着上线后可以关。数据库集合在云开发控制台手动创建不需要写初始化脚本。需要建六个集合services、addresses、orders、order_log、technicians、users。所有集合的权限建议设置为“仅创建者可读写管理员可读写”因为用户只能动自己的数据跨用户写入靠云函数完成——云函数端有管理权限不受这条限制。3.2 预约下单页面选服务、选时间、提交订单的完整代码下单页是核心页面WXML 部分注意用picker处理时间段、用radio-group处理支付方式。家庭保洁场景不需要用户自己挑小区楼栋号地址是预先维护好的。!-- pages/order/order.wxml -- view classservice-card picker modeselector range{{services}} range-keyname bindchangeonServiceChange view服务类型{{selectedService.name}}/view /picker picker modedate bindchangeonDateChange view上门日期{{selectedDate}}/view /picker picker modetime bindchangeonTimeChange view上门时间{{selectedTime}}/view /picker view订单金额¥{{selectedService.price}}/view /view button typeprimary bindtapsubmitOrder disabled{{submitting}} 提交预约并支付 /button// pages/order/order.js Page({ data: { services: [], selectedService: {}, selectedDate: , selectedTime: , submitting: false }, async onLoad() { const db wx.cloud.database(); const res await db.collection(services).where({ enabled: true }).get(); this.setData({ services: res.data, selectedService: res.data[0] }); }, onServiceChange(e) { this.setData({ selectedService: this.data.services[e.detail.value] }); }, onDateChange(e) { this.setData({ selectedDate: e.detail.value }); }, onTimeChange(e) { this.setData({ selectedTime: e.detail.value }); }, async submitOrder() { if (!this.data.selectedDate || !this.data.selectedTime) { wx.showToast({ title: 请补充上门时间, icon: none }); return; } this.setData({ submitting: true }); try { const res await wx.cloud.callFunction({ name: createOrder, data: { serviceId: this.data.selectedService._id, timeRange: { start: ${this.data.selectedDate} ${this.data.selectedTime}, end: this.addMinutes(this.data.selectedTime, this.data.selectedService.duration_min) } } }); await this.requestPayment(res.result.payParams); wx.redirectTo({ url: /pages/order/detail?id${res.result.orderId} }); } catch (err) { wx.showToast({ title: 下单失败稍后再试, icon: none }); } finally { this.setData({ submitting: false }); } } });每个时段的选择都对应状态数据最后组装成一个timeRange区间传给云函数。addMinutes是我自己写的工具函数把duration_min加到开始时间上确保结束时间一定正确。页面端不直接写数据库所有数据变更都走callFunction这是云开发应用的关键约束云函数端可以校验用户是否有权下单、时段是否冲突页面端调数据库是绕过所有业务规则的后门。3.3 云函数 createOrder事务写订单和支付签名云函数是整个小程序的“后端”。createOrder负责四件事组装订单数据、校验时间段是否冲突、生成支付参数、返回给前端拉起支付。// cloudfunctions/createOrder/index.js const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db cloud.database() exports.main async (event) { const { OPENID } cloud.getWXContext() const { serviceId, timeRange } event // 查服务价格 const serviceRes await db.collection(services).doc(serviceId).get() const service serviceRes.data // 检查同一时段有没有已支付/已派单的冲突订单 const conflictRes await db.collection(orders) .where({ status: _.in([PAID, ASSIGNED, WORKING]), timeRange.start: _.lt(timeRange.end), timeRange.end: _.gt(timeRange.start) }) .count() if (conflictRes.total 0) { return { code: TIME_CONFLICT, msg: 该时段已被预约请换一个时间 } } const orderId ORD Date.now() Math.random().toString(36).slice(-4) const orderData { _id: orderId, user_openid: OPENID, service_id: serviceId, service_snapshot: { name: service.name, price: service.price, duration_min: service.duration_min }, time_range: timeRange, amount: service.price, status: PENDING_PAY, create_time: db.serverDate() } await db.runTransaction(async transaction { await transaction.collection(orders).add({ data: orderData }) await transaction.collection(order_log).add({ data: { order_id: orderId, from_status: , to_status: PENDING_PAY, operate: 用户提交预约, create_time: db.serverDate() } }) }) // 微信支付统一下单v2 示例 const payParams await unifiedOrder({ openid: OPENID, orderId, amount: service.price, body: service.name }) return { code: OK, orderId, payParams } }逻辑说明timeRange.start timeRange.end并且timeRange.end timeRange.start这个条件判断的是区间重叠是我第 4 章要展开的排期核心。service_snapshot存的是快照防止服务涨价后订单金额对不上账。runTransaction保证订单表和日志表要么同时写入成功、要么同时失败不会出现订单在但日志缺失的情况。支付签名这里用 v2 接口举例参数不能少appid、mch_id、nonce_str、body、out_trade_no、total_fee、spbill_create_ip、notify_url、trade_typeJSAPI、openid。其中total_fee是“分”订单表里存的是“元”所以云函数里要乘 100async function unifiedOrder({ openid, orderId, amount, body }) { const params { appid: 你的小程序appid, mch_id: 你的商户号, nonce_str: Math.random().toString(36).slice(-8), body, out_trade_no: orderId, total_fee: Math.round(amount * 100), spbill_create_ip: 127.0.0.1, notify_url: https://your-domain.com/pay/notify, trade_type: JSAPI, openid } const sign buildSign(params, 商户密钥) // 请求 https://api.mch.weixin.qq.com/pay/unifiedorder // 成功后拿到 prepay_id // 再生成供 wx.requestPayment 使用的 paySign return { timeStamp: String(Math.floor(Date.now() / 1000)), nonceStr: params.nonce_str, package: prepay_id${prepay_id}, signType: MD5, paySign: buildPaySign({ ... }) } }注意v3 支付的签名方式与 v2 不同v3 用商户私钥做 RSA 签名、接口路径是/v3/pay/transactions/jsapi。不建议新项目再学 v2这里用 v2 举例只是因为它最容易讲清楚参数结构如果你接入 v3把unifiedOrder内部换成 v3 的参数组即可云函数外围代码不变。4. 保洁师排期与派单时段防冲突、订阅消息和动态标题的三个落地细节订单生成只是第一步真正让系统跑起来的是排期和派单。这里三个细节决定系统能不能“用得住”时段防冲突、消息触达、页面状态反馈。4.1 时段防冲突为什么不能只比较字符串两个订单都选“2025-06-01 10:00”看起来很整齐但实际业务里保洁师可能住得很远、上一单延时订单会有一个“服务中”的浮动区间。最朴素的实现是把时间存成字符串然后where相等匹配——这在新手代码里非常常见也是第一处翻车点。// 区间重叠判断两个时间段只要有交集就冲突 function isOverlap(startA, endA, startB, endB) { return startA endB startB endA } // 示例10:00-12:00 和 11:00-13:00 // isOverlap(10:00,12:00,11:00,13:00) true这个判断看起来简单实际容易错在接受边界值。比如 A 单 10:00-12:00B 单 12:00-14:00这不算重叠上一单的结束时间正好等于下一单的开始时间保洁师可以接。但页面上的时间选择器如果粒度是小时12:00 这个边界就会被两个订单同时选中需要在存数据时统一规定“开始时间包含、结束时间不包含”即end是开区间。习惯上我把这个规则写进云函数的常量区防止不同开发者理解不一致。数据库查询时用_.gt和_.lt组合匹配而不是where一个等值条件。另外在后台“派单”操作里也要校验一次冲突用户下单时校验一次管理员派单时再校验一次因为派单的时间点可能离下单时间已经过了一整天这段时间其他订单可能已经抢占了同一时段。两次校验不互相替代都是必做的。4.2 订阅消息一次性授权怎么设计成“预约提醒”和“完工通知”微信订阅消息是预约类小程序触达用户的主要通道但它有个让很多人意外限制wx.requestSubscribeMessage授权一次只能推一条消息。也就是说用户下单前授权提示“预约成功通知”那么这条消息消耗掉以后下次再想推“师傅已出发”就需要重新授权。// 用户提交订单前先请求订阅授权 async requestMsgPermission() { let tmplIds [] // 预约成功通知 if (this.data.orderStep submit) { tmplIds [预约成功通知模板ID] } // 派单提醒在下单按钮点击瞬间再请求一次 if (this.data.orderStep pay) { tmplIds [派单提醒模板ID] } const res await wx.requestSubscribeMessage({ tmplIds }) if (res[预约成功通知模板ID] accept) { this.setData({ msgAuthorized: true }) } }这里的设计教训是不要在页面加载时一次性请求三个模板用户大概率拒绝要在用户真正执行某个动作前请求对应的那一个把授权动作和用户需求绑在一起。比如“提交预约前请求预约成功通知”“支付成功后选提醒时间再请求派单提醒”这样授权成功率会明显好看。如果用户拒绝授权消息触达就断了。我做了兜底小程序的“服务通知”里如果历史有订单会以“待办事件”的形式出现在微信的“服务通知”列表里但这个不稳定。真实兜底方案是短信——在云函数里调用短信 API 给用户手机号发提醒缺点是每一分钱都是成本所以我只对前一天晚上还没付款的订单发催付短信真到了上门当天靠用户自己看订单页。4.3 动态标题和自定义导航栏别让状态显示挤在页面里订单详情页是用户自查预约进度的核心地方。很多人把状态放在页面上的一个角落用户找不到最简单的做法是把导航栏标题直接变成状态待支付、待派单、服务中、已完成一目了然。// pages/order/detail.js const statusTitle { PENDING_PAY: 订单待支付, PAID: 等待派单, ASSIGNED: 保洁师已接单, WORKING: 服务进行中, DONE: 订单已完成, CANCELLED: 订单已取消 } wx.setNavigationBarTitle({ title: statusTitle[this.data.order.status] })还有一个容易被忽略的问题自定义导航栏。很多人为了样式好看把navigationStyle设为custom然后自己画标题栏。此时页面顶部内容的定位高度不是固定的它等于状态栏高度加胶囊按钮高度。不同机型差异很大iPhone 和安卓的状态栏高度不一样一不留神标题就顶到胶囊按钮上了。保险的写法是拿到胶囊按钮的实际尺寸后再布局const { top, height } wx.getMenuButtonBoundingClientRect() // 导航栏高度 top height 6上下留白不用自定义导航栏就没这个事但用了就要算。小程序头部标题一旦被顶上去在真机上直接显示错位开发工具里却看不出问题这属于典型的真机踩坑。4.4 派单与“保洁师端”状态流转的权限边界订单从PAID到ASSIGNED这个动作要做权限控制。如果只是一个小程序面向用户后台派单需要在管理员侧做。我见过的最小可行做法做一个只有管理员能进的“派单页”用 openid 白名单判断身份管理员在订单列表里把PAID状态的订单指派给保洁师云函数里校验当前操作者确实是管理员然后才允许更新tech_userid和status。保洁师端如果做成独立小程序要额外注册账号体系成本一下子上去。更常见做法是第一版做“轻管家”派单后不开放保洁师登录由公司客服在内部订单列表中人工流转状态消息提醒靠客服手动点击“发送服务中通知”。等订单量一天超过 50 单再上保洁师端这是小团队最务实的节奏。5. 避坑清单保洁小程序上线前最容易翻车的五个点现象→原因→解决5.1 现象两个订单撞在同一个保洁师的同一个时段原因下单时只在页面端校验了时间是否冲突没有在云函数里做二次校验或者数据库并发高的时候两个请求同时读到“无冲突”同时写入。解决把时段冲突校验放进云函数配合db.runTransaction做事务写入并且为orders集合把status、time_range.start、time_range.end建复合索引。事务能保证并发请求下只有一个成功这是数据库层面的最后一环。5.2 现象定时触发的提醒从来没发出去原因前端setTimeout只在小程序存活期间有效用户退出小程序、手机息屏定时器全部挂掉。解决提醒这类任务不要依赖前端改用云开发“定时触发器”——在cloudfunctions/config.json里配置定时规则让云函数每天 08:00 触发一次扫描当天的PAID订单并推送订阅消息。这类定时任务在云端跑跟用户手机在不在线无关。5.3 现象新版手机号授权按钮在部分手机上点击没反应原因基础库版本低于 2.21.2新版code动态令牌不生效或小程序后台没有声明“获取手机号”隐私接口。解决app.json声明基础库版本在微信公众平台“设置—服务内容声明—用户隐私保护指引”中勾选手机号相关接口。开发工具里用“真机调试”反复测试授权链路不要只在开发者工具里点。5.4 现象支付金额和订单金额差一分钱原因微信支付金额单位是“分”订单金额单位是“元”云函数里没有做转换或者用了parseInt把小数直接截断。解决统一在云函数入口做Math.round(amount * 100)前端展示永远用“元”数据库存储也存“元”只在生成支付参数那一刻转“分”。这个转换逻辑单独放在一个工具函数里写单元测试防止改价格时顺手改坏。5.5 现象体验版能打开但数据全是测试库的原因云函数里的环境 ID 写死在开发环境cloud.init的env参数没有切到正式环境或者小程序 appid 和云环境没有绑定。解决上线前全局搜索env:字符串把所有出现的环境 ID 统一替换为正式环境 ID。如果开发测试切换很频繁就把环境 ID 放进config.js单独维护发版前只改这一个文件。真机调试时用小程序抓包工具如 Charles确认云函数请求到达的是哪个域名这条排查路径能省一到两小时。6. 上线后先做一遍时间轴回放验证订单状态机与支付时序上线后第一件事别急着铺推广先用订单日志把状态机完整走一遍。打开云开发控制台找一个测试订单在order_log集合里按时间排一下SELECT order_id, to_status, operate, create_time FROM order_log WHERE order_id ORD20250601xxxx ORDER BY create_time ASC能看到这个样子说明状态机是健康的时间事件结果10:00:00用户提交预约PENDING_PAY10:00:05用户完成支付PAID10:15:00客服派单ASSIGNED13:50:00保洁师开始服务WORKING16:05:00保洁师完成服务DONE如果日志里出现PAID之后直接跳到DONE、中间没有ASSIGNED说明派单逻辑被绕过了立刻查一下页面是不是有直接操作订单状态的代码。这类验证脚本可以做成一个 node 工具定时巡检所有状态异常订单因为人工盯日志很容易漏。我自己的习惯是上线当天创建一个测试账号走完整路径注册—选服务—支付—派单—开始服务—完结中间用抓包工具看一眼支付回调有没有正常回到云函数。这套流程走完再把测试订单的日志清掉。之后每次改状态机代码都先跑一遍回放脚本再发版。排期、支付、状态机这三件事做扎实以后这个上门预约保洁的方案就已经能扛住一天上百单的真实业务了。剩下的是运营层面的问题保洁师的完工照片上传、用户评价、复购优惠券这些都可以在后续版本里平滑叠加。希望帮到你少踩一个坑就多省一个晚上。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑