去年我接手维护一套连锁自助洗衣房的预约小程序,本以为只是个"扫码—选机—下单"的简单工具,结果第一周就被老设备的状态同步问题搞得焦头烂额。洗衣机预约系统在微信小程序里看着不难,但真正落地时涉及设备状态一致性、支付回调、消息触达、并发抢约这一整条链路,每一个环节都可能让用户体验归零。这篇文章不写泛泛的"系统意义",直接聚焦微信小程序自助洗衣房洗衣机预约系统的真实技术拆解:核心模块设计、状态管理、支付对接、消息推送、硬件联动,以及我踩过的那些文档里查不到、只有上线后才暴露的坑。
1. 自助洗衣房预约系统的边界:别把"预约"做成"排队"
先理清业务边界。很多第一次做洗衣房预约的朋友,上来就照着"餐厅排队叫号"的思路设计系统,这是一个典型的认知偏差。自助洗衣房的核心场景是**"找空闲设备—锁定时间窗—到店即用"**,而不是"到店取号—等待分配"。两者最大的区别在于:洗衣房不需要人工调度,设备是物理隔离的,用户要预约的是"某台具体机器在某个时间段的使用权",而不是"队伍里的第N个位置"。
基于这个理解,系统的核心模块应该划分为五块:
- 设备管理模块:维护每台洗衣机的物理状态(空闲、运行中、故障、离线)和位置信息。
- 预约/排期模块:以时间片为单位管理设备的使用计划,处理预约冲突。
- 用户与订单模块:微信授权登录、订单创建、支付状态流转。
- 消息触达模块:预约成功通知、设备空闲提醒、订单状态变更通知。
- 后台管理模块:给运营人员用的设备监控、预约记录查询、费率设置、异常处理。
这里有一个关键设计决策值得展开:预约的时间粒度设多少?我见过有系统把时间片设为15分钟,结果设备空转率极高——一台标准洗衣机标准程序是45分钟,用户预约15分钟根本用不完;也有系统设成"仅支持预约当前时段",那又失去了预约的意义。最终我们按设备类型区分:波轮洗衣机设40分钟时间片、滚筒洗衣机设60分钟时间片、烘干机设50分钟时间片,并且允许用户选择'连续两个时间片'。这个逻辑在后台配置里做,小程序端只负责展示可预约的起始时间列表,灵活性高很多。
另一个容易被忽略的模块是"故障上报"。自助洗衣房是无人值守场景,设备故障如果只能靠用户打客服电话,效率极低。我们做了一键上报功能,用户扫描设备二维码后如果发现设备无法启动,可以直接在小程序内提交故障描述和照片,后台自动将该设备状态置为"维护中",已预约该设备后续时段的用户自动收到改期提醒。这个功能看似边缘,实际对用户留存帮助极大。
2. 设备状态流转与预约数据模型:核心难点不在"约"而在"一致性"
预约系统最核心的技术难点不是预约本身,而是多端状态的一致性。洗衣房的现实是:设备可能被现场用户直接投币使用,也可能被小程序用户远程预约,还可能因为网络故障导致云端状态和物理状态不一致。这三类情况叠加,如果数据模型设计得不严谨,就会出现"小程序显示空闲、到店发现机器正在被人用"的信任危机。
2.1 状态机的定义必须"窄进宽出"
我给设备状态设计了五个枚举值:IDLE(空闲)、RESERVED(已预约)、RUNNING(运行中)、FAULT(故障)、OFFLINE(离线)。这里要注意的是状态转移路径:
IDLE -> RESERVED:用户预约成功。RESERVED -> IDLE:用户取消预约,或预约超时未支付。RESERVED -> RUNNING:用户到店扫码启动设备。IDLE -> RUNNING:现场用户不经过预约直接投币使用。RUNNING -> IDLE:洗衣程序结束。任意状态 -> FAULT/OFFLINE:上报故障或心跳超时。
"窄进宽出"的意思是:进入预约状态的条件必须严格(用户必须实名、必须支付、同一时间段不能重复预约),但离开预约状态的路径必须充分。实际开发中常见的错误是只处理了"用户主动取消"和"预约后启动"两条正常路径,遗漏了"预约后未到店"的自动释放路径。
2.2 预约数据表的字段设计
我习惯用一张单独的预约表而非在设备表上直接加"当前预约人"字段,原因是预约需要支持"未来时间段"的记录,一张表存不下。核心字段如下(以MySQL为例):
CREATE TABLE `reservation` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `device_id` bigint(20) NOT NULL COMMENT '设备ID', `user_openid` varchar(64) NOT NULL COMMENT '用户微信openid', `start_time` datetime NOT NULL COMMENT '预约开始时间', `end_time` datetime NOT NULL COMMENT '预约结束时间', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0待支付 1已支付 2已取消 3已使用 4已过期 5已退款', `order_no` varchar(32) DEFAULT NULL COMMENT '关联订单号', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY `idx_device_time` (`device_id`, `start_time`, `end_time`), KEY `idx_user_status` (`user_openid`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约记录表';防冲突的SQL是关键,插入前一定要做重叠校验。我用的是最简单也最可靠的方式——锁表查询:
SELECT COUNT(*) FROM reservation WHERE device_id = #{deviceId} AND status IN (0, 1) -- 待支付和已支付都算占用 AND start_time < #{endTime} AND end_time > #{startTime};查到大于0就拒绝新预约。有人担心并发下这个查询会出问题,实际在单体应用里配合事务和行锁足够了,洗衣房这种低并发场景远没到需要Redis分布式锁的地步。如果非要上锁,建议用SELECT ... FOR UPDATE把设备记录锁住再查预约表,也能解决问题。
2.3 预约超时释放的调度策略
预约后不支付、支付后不到店,是洗衣房运营最大的空转浪费。我们的策略是三层递进:
- 待支付超时:用户提交预约单后15分钟未支付,系统自动取消预约,释放时间片。这个用延迟队列或定时任务扫表都行,量不大,直接定时任务每5分钟扫一次
create_time超过15分钟且status=0的记录即可。 - 预约后未启动:用户已支付但未在预约开始时间后30分钟内扫码启动设备,订单标记为"已过期",费用不退(会弹窗提示用户原因),设备释放为空闲。这一步需要一套相对宽容的提醒机制,我们会在预约开始前1小时、30分钟分别推送订阅消息提醒用户。
- 提前释放保护:如果设备在前一个用户使用中出现了意外延迟(比如衣服没取走导致机器无法释放),后台会自动将后续所有预约整体顺延,并批量推送通知。这个功能很考验表设计——顺延操作要遍历受影响的全部预约记录,逐条更新起止时间。
3. 微信支付V3对接的实操细节:证书、回调与幂等
预约必须和支付绑定,否则恶意占位会把设备全部锁死。微信支付V3是目前的主流选择,但它的对接过程比V2繁琐不少,尤其是平台证书和回调验签这一块,热搜里"小程序微信支付v3对接 无可用的平台证书"就是最常见的问题。
3.1 三步完成V3支付接入
第一步是准备商户信息和证书。你需要的东西有:mchid(商户号)、appid(小程序AppID)、apiv3key(APIv3密钥)、商户私钥(apiclient_key.pem)、商户证书序列号、微信支付平台证书(wechatpay_public_key.pem或pub_key.pem)。
第二步是创建支付订单。小程序端先调用后端接口,后端用商户私钥对参数签名,然后调用微信支付V3的POST /v3/pay/transactions/jsapi接口,获取prepay_id,再生成二次签名参数返回给小程序端,最后小程序端调用wx.requestPayment拉起支付。
这里最容易踩的坑是:很多教程让你从小程序端直接把openid传过来创建订单,其实openid应该由后端通过code2Session接口换取,绝对不能由前端传入。因为code2Session需要用到AppSecret,这个密钥绝不能暴露在小程序代码里。
第三步是支付回调处理。微信支付V3的回调地址需要用HTTPS,微信服务器会POST一个加密的报文过来,结构如下:
{ "id": "ev-xxx", "resource": { "ciphertext": "...", "nonce": "...", "associated_data": "..." } }ciphertext需要用APIv3密钥做AES-256-GCM解密,解密后拿到订单状态和金额。验签流程我用Java的wechatpay-javaSDK写了完整实现,核心代码如下:
// 使用SDK的NotificationParser自动处理验签和解密 Config config = new RSAAutoCertificateConfig.Builder() .merchantId(mchId) .privateKey(privateKey) .merchantSerialNumber(mchSerialNo) .apiV3Key(apiV3Key) .build(); NotificationParser parser = new NotificationParser(config); Transaction transaction = parser.parse(request, Transaction.class); // 业务校验:金额和订单号 if ("SUCCESS".equals(transaction.getTradeState()) && amountCheck(transaction.getAmount().getTotal(), orderNo)) { // 更新订单状态 }注意:解密后的
amount.total单位是分,很多新手在这里把单位写错,导致订单金额校验全部失败。
3.2 "无可用的平台证书"的根因分析
热搜里那个"无可用的平台证书"错误,绝大多数情况是两种原因:
- 没有上传商户证书序列号对应的证书文件。V3接口要求设置平台证书用于验证微信服务器签名,但微信支付平台证书是定期轮换的,你可以使用
RSAAutoCertificateConfig让SDK自动下载并更新平台证书,不需要手动维护。 - 证书序列号写错了。商户证书序列号和微信支付平台证书序列号是两个完全不同的东西。商户证书序列号是你的
apiclient_cert.pem证书里的序列号,在商户平台"账户中心-API安全"能看到;而平台证书序列号是用来验证回调消息签名的,用RSAAutoCertificateConfig后SDK自动管理。
我当时的排查顺序是:先打印Authorization请求头确认商户序列号与证书文件一致,再用微信官方提供的验签工具验证签名,最后确认请求头里的Wechatpay-Serial是平台证书序列号。排查完发现是证书文件串了,换回正确的apiclient_cert.pem立刻就好了。
3.3 支付回调的幂等处理
微信支付回调不保证只发送一次,如果业务代码没有做幂等,用户支付成功后订单状态可能被重复更新,造成赠送次数翻倍之类的bug。我的处理方式是在更新订单状态前先加一个条件:
// 使用乐观锁,只更新当前状态为"待支付"的订单 int rows = orderMapper.updateStatusIfPending(paymentOrderNo, "PAID", "PENDING"); if (rows == 1) { // 更新设备预约状态 reservationService.activateByOrderNo(paymentOrderNo); }还有一点:回调处理里除了更新数据库,往往还会触发预约激活、推送订阅消息等副作用操作。为了避免一个回调处理成功、另一个副作用操作失败导致数据不一致,建议把这些操作放到同一个事务里,或者用消息队列做最终一致性。对洗衣房这种量级,直接本地事务就够了,不要过度设计引入MQ。
3.4 注意支付合规风险
热搜里有一条"由于小程序违规,支付功能暂时无法使用",这是很多开发者会遇到的晴天霹雳。小程序支付被封禁通常是因为类目不符或诱导支付,做洗衣房预约时要特别谨慎:
- 类目选择:洗衣服务属于"生活服务-丽人/洗浴"还是"商家自营-服饰箱包鞋"?不同类目要求不同资质。最稳妥的做法是提前在微信公众平台"设置-服务内容声明"里确认你选的服务类目支持在线支付。
- 避免自动续费/虚拟支付:洗衣房预约是实物服务,不要在系统里出现"会员自动续费""购买虚拟币"等功能,很轻易就会被判违规。
- 付款后立刻锁设备:不要让用户处于"付了款但没锁到设备"的状态,这既是体验问题也是合规问题。
4. 消息推送的三种姿势:订阅消息、客服消息与短信兜底
洗衣房预约系统的消息触达直接关系到用户是否按时到店。微信小程序目前支持的消息能力有限,主要是一次性订阅消息,这给预约提醒场景带来了不小的麻烦。
4.1 订阅消息的正确用法
微信小程序的订阅消息分为"一次性订阅消息"和"长期订阅消息"。长期订阅消息仅面向特定行业类目开放,普通洗衣房很难申请成功,所以绝大多数场景使用的是一次性订阅消息。
一次性订阅消息的坑在于:用户每次授权只能接收一条消息,而且授权行为必须由用户主动触发。我们的策略如下:
- 用户提交预约订单时,弹出授权窗请求授权"预约成功通知"——这是最自然的一次授权机会。
- 用户支付成功后,再弹出一次授权窗,请求授权"设备空闲提醒"或"订单完成通知"——这次授权用于后续的状态变更通知。
- 如果用户两次都拒绝了,系统就只能走短信兜底。
这里要注意一个细节:wx.requestSubscribeMessage必须在用户点击行为(bindtap)的事件回调中调用,不能在onLoad里直接调用,否则会报requestSubscribeMessage:fail can only be invoked by user TAP gesture。这是小程序的硬性限制,没有绕过办法。
4.2 订阅消息的下发模板配置
在微信公众平台配置模板的时候,要注意字段的语义准确。洗衣房场景最常见的几个模板是:
| 场景 | 模板标题 | 关键字段 |
|---|---|---|
| 预约成功 | 预约成功通知 | 设备名称、预约时间、订单编号 |
| 开始提醒 | 服务即将开始提醒 | 设备名称、开始时间、门店地址 |
| 完成提醒 | 服务完成通知 | 设备名称、完成时间、取衣提示 |
| 取消/改期 | 预约变更通知 | 变更原因、新的预约时间 |
| 故障通知 | 服务暂停提醒 | 暂停原因、预计恢复时间 |
订阅消息的下发有频次限制,一次性订阅消息模板对应的事件只能下发一次,所以不要把"预约成功"和"即将开始"都塞在一个模板里,拆开才能获得多次下发机会,前提是用户也多次授权了。
4.3 短信兜底与成本控制
订阅消息授权率很难做到100%,即使授权了也会因为微信的某些限制下发失败(比如用户关闭了通知权限)。所以必须要有短信兜底方案,但也不能所有用户都发短信,成本太高。
我们的策略是优先级递进:
- 用户授权了订阅消息且下发成功 → 不发短信。
- 订阅消息发送返回
43101(用户拒绝授权)或40001(access_token无效)等错误 → 发短信提醒。 - 预约开始前30分钟,如果用户仍未到店且没有取消,不管之前哪种情况,都发一条短信。
短信服务商的选择主要看到达率和成本,目前主流的阿里云、腾讯云短信单价都在几分钱一条,洗衣房一天几十单的量完全扛得住。
4.4 小程序后台消息推送配置
小程序后台的"消息推送"配置是给服务器接收用户消息和事件回调用的,和订阅消息下发是两回事。这个配置经常有人搞混,打开"开发管理-开发设置-消息推送"后,需要填一个接收回调的URL和Token。
我用的是开发者服务器,直接填了一个/api/wx/callback的地址。这里有个小坑:消息推送URL必须直接返回success明文,且要通过微信服务器的Token验证。Token验证的规则是微信服务器GET请求带上signature、timestamp、nonce、echostr参数,开发者服务器需要按字典序拼接Token、timestamp、nonce三参数,做SHA1校验,校验通过后原样返回echostr。
@GetMapping("/api/wx/callback") public String verifySignature(@RequestParam("signature") String signature, @RequestParam("timestamp") String timestamp, @RequestParam("nonce") String nonce, @RequestParam("echostr") String echostr) { String[] arr = new String[]{token, timestamp, nonce}; Arrays.sort(arr); String s = String.join("", arr); String sha1 = DigestUtils.sha1Hex(s); if (sha1.equals(signature)) { return echostr; } return "error"; }配置完成后,用户给公众号或小程序发的消息、扫码事件等都会POST到这个URL上。在洗衣房场景中,我主要用它来接收用户扫描设备二维码的事件——用户到店扫码时,微信服务器会推一个SCAN事件过来,服务器记录下来作为"用户已到店"的判断依据。
5. 硬件联动的真实代价:蓝牙、轮询与离线容灾
自助洗衣房的预约系统如果只做"人员预约"而不联动"设备控制",整个系统的价值会大打折扣。但真正的硬件联动远比想象中复杂,尤其是老设备的改造问题。
5.1 方案选型:物联网模块 vs 蓝牙直连 vs 纯软预约
市面上做洗衣房设备联动主要有三种方案:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 4G/5G物联网模块 | 在设备主板上集成通信模块,云端直接下发指令 | 实时性强,可远程控制 | 改造成本高,老设备不支持 |
| 蓝牙BLE直连 | 用户手机通过蓝牙连接洗衣机,小程序控制 | 无需改硬件,成本低 | 体验差,无法远程控制 |
| 纯软预约 | 只做预约锁时,用户到店后手动操作设备 | 零改造成本 | 无法验证用户是否真的使用了设备 |
现在的第三方物联网方案(比如机智云、涂鸦智能)做洗衣房场景很成熟,但那要整机替换或加装采集模块。我们当时的现实约束是:门店里既有新机器也有服役十年的老波轮,不可能全部替换。最终采用了混合方案:新设备(带物联网模块的)接入云端实现远程锁定和状态回传,老设备用"预约锁定+扫码拍照回传"的人工验证方案。
这一点值得所有要做的团队提前想清楚:预约系统必须解决"用户预约了但到店后没有使用"如何验证的问题。如果在物联网设备上,用户可以扫码启动,服务端记录启动时间,订单状态自动从"已支付"变为"使用中";老设备上,我们让用户扫码后上传一张洗衣机启动的照片,由后台人工审核(或超时自动放行)。虽然人工审核成本高,但老设备改造费用更低,前期可以接受。
5.2 蓝牙BLE搜索不到设备的排查链路
热搜里"微信小程序使用蓝牙搜索设备 有的手机可以收索到设备有的手机搜索不到设备"是典型的兼容性问题。我之前在这个坑里卡了两周,这里把我的排查链路完整记录下来。
现象:同一台洗衣机,iPhone 13可以正常搜到,小米11搜不到,华为P40偶尔能搜到。
排查步骤:
确认权限配置。小程序蓝牙接口需要在
app.json里声明requiredBackgroundModes: ['bluetooth-central']吗?实测不需要,但Android 12及以上版本需要动态申请定位权限(蓝牙扫描依赖定位权限),否则wx.startBluetoothDevicesDiscovery根本发现不了设备。这个权限要用户在系统设置里授权,很多设备在未授权定位时蓝牙扫描静默失败且不报错。确认蓝牙版本兼容。老设备用的是蓝牙4.0,加密方式比较古老,部分新手机在系统层面默认关闭了"允许旧设备连接"。这个无法通过小程序API控制,只能引导用户在系统蓝牙设置里手动搜索并配对一次。
扫描参数调整。
wx.startBluetoothDevicesDiscovery提供allowDuplicatesKey参数,表示是否允许重复上报同一设备。默认false,但实测有些手机在true时才能稳定发现设备。另外扫描间隔写interval: 500(毫秒),扫描时间别太长——Android上扫描超过30秒会静默失败。ibeacon的广播间隔。老设备广播间隔如果设成了1秒,扫描方扫到概率会很低。可以用nRF Connect这类工具先确认设备确实在广播,再看广播间隔和功率。如果设备广播间隔大于500ms,建议让硬件方调整。
万不得已加"手动添加设备"。蓝牙扫描天生就有兼容死角,在"扫码失败/搜索不到"的页面上一定要做手动输入设备编号添加的兜底入口。这是体验的最后防线,哪怕只是让用户扫码上的二维码直接绑定设备,也比卡死在搜索页面强。
5.3 设备状态回传与离线检测
物联网设备的状态回传频率是另一个现实问题。实时心跳每30秒一次,功耗和流量成本不高,但云端判断设备离线需要连续3次心跳超时,也就是90秒。这个延迟在预约场景下是可以接受的,因为用户预约的是未来一个小时的时间窗,不需要秒级实时。
真正的坑在在线状态和预约状态两个状态机的耦合。我初版设计时把OFFLINE当成设备的最高优先级状态——只要设备心跳丢了就立刻取消所有预约,结果造成了大量误伤:设备只是网络临时抖动,恢复后却发现预约全没了。后来改为:设备离线超过10分钟才触发预约取消逻辑,且取消前一定要给所有受影响用户推送改期通知。
另一个经验是:设备恢复在线后,不能直接回到"空闲"状态,而应该进入"待确认"状态,因为离线期间可能有现场用户直接投币使用。云端需要一次校准——由现场人员(或用户扫码上报)确认设备真实状态后,再把设备标记回空闲。这个"云端状态永远不可信"的设计理念,是做硬件联动类小程序最核心的心法。
5.4 日程排期与真实使用时长
预约系统的时间片和真实洗衣时长总会存在偏差。波轮洗衣机标准洗40分钟,但用户可能会选"加强洗"(60分钟)或"快洗"(25分钟)。因为设备上的旋钮用户可能会自己调,所以预约结束时间和真实结束时间几乎必然错位。
这会导致两个问题:
- 前一个用户预约了50~90分钟时段,实际只用了30分钟,设备提前空闲,后面的用户无法提前开始。
- 前一个用户选了超长程序,预约时段结束时还在运行,后一个用户按时到店却用不了。
我最后的处理方案是:预约时段是"可开始时段",不是"必须结束时段"。用占用状态而非固定时刻来管理排期,系统记录的不是"这台机器某个时刻属于谁",而是"这台机器当前是否被占用、被谁占用、预计最多占用到几点"。设备提前结束后状态立即释放给下一位排队用户,这样设备利用率最高。这套思路在排期算法上复杂度没有增加多少,但对用户体验提升非常明显。
6. 工程化落地的几个硬骨头:反编译、抓包调试与网络异常
微信小程序开发到中后期,遇到的更多是工程性问题,而不是功能开发问题。下面这几块是我自己测试和上线后遇到最多的问题合集。
6.1 关于小程序源码保护
热搜词里"已经部署的微信小程序怎么能拿到源码""微信小程序反编译"频频出现,说明很多开发者对小程序包的安全性有顾虑,也有些是接手别人的老项目找不到原始代码了。我需要直说:小程序的前端代码运行在用户手机本地,无论官方怎么加密,从设备上提取并还原出代码是可能的。客观上说,任何客户端代码都无法做到绝对不可破解。
做洗衣房预约系统需要关注的点不是纠结于防止反编译(那是不可能的),而是把核心逻辑放到服务端:
- 设备控制指令、预约排期算法、支付签名全部在服务端完成。
- 小程序端不放任何密钥、AppSecret、支付私钥。
- 敏感接口做访问鉴权,校验登录态和操作权限。
- 前端代码里不要写死任何"管理端"逻辑,运营后台必须是独立的Web系统。
如果仅是为了"找回源代码"(比如原开发者离职、公司没有代码管理习惯),可以用一些公开工具尝试还原,但要清楚:反编译还原出来的代码只能作为参考,不可能100%还原出可编译运行的工程。与其花时间还原,不如根据功能逆向重构。
6.2 burp suite抓包小程序的卡点
热搜里有几条关于抓包的。微信小程序的网络请求用的是HTTPS,常规抓包配置需要两步:把Burp的CA证书安装到手机、并在微信里设置代理。但小程序会对部分请求做证书校验(SSL Pinning),导致代理工具看到一堆SSL handshake failed。
我自己的调试方案分三种:
- 开发阶段:在微信开发者工具里直接打开"不校验合法域名",开发者工具的Network面板自带请求查看功能,根本不需要抓包。
- 真机调试阶段:微信开发者工具支持"真机调试",打开后可以在PC端看到真机的请求流。运费最低,强烈推荐。
- 上线后问题排查:如果问题只在线上环境出现,而线上又不能开调试模式,这时候才需要抓包。方法是用Burp或Charles做中间人代理,配合安装CA证书。小程序是否做证书校验完全取决于代码,我们自己的小程序为了安全做了校验,所以外网抓包看到的都是加密流量,真正的排查还是依赖服务端Nginx日志和前端埋点。
强烈建议:不要把大量时间花在抓包小程序的加密流量上。小程序端能抓到的信息有限,服务端日志和DB记录才是排障的主战场。我见过有同事为了看一个请求参数在抓包上折腾了两天,最后发现服务端日志里全都有。
6.3 网络不可用时的全局错误提示
热搜词"uniapp微信小程序,当网络不可用或者网络不好的时候,如何全局统一统一显示网络不可"问的是弱网处理。洗衣房场景有个特殊性:洗衣房通常在地下室或商业楼角落,手机信号差,网络异常太常见了。
我用的方案是在小程序全局的app.js里注册网络状态监听:
// app.js App({ onLaunch() { wx.onNetworkStatusChange((res) => { if (!res.isConnected) { wx.showToast({ title: '网络已断开,请检查网络', icon: 'none', duration: 2000 }); } }); } });这只是全局toast。对于实际请求失败,更关键的是在request封装里统一处理。我给所有请求加了一个catch分支,当错误码是网络相关(request:fail、timeout等)时,弹一个全屏提示页面而不是默认的错误toast。因为预约操作如果在弱网下超时,用户不知道请求是否成功,很容易重复提交,造成重复订单。
针对"防重复提交"这个问题,前端可以加按钮loading态禁止二次点击,后端也要做幂等:预约接口要求客户端传一个UUID作为幂等键,服务端用唯一索引防重。
ALTER TABLE reservation ADD COLUMN idempotent_key VARCHAR(64) UNIQUE;用户点击"立即预约"时前端生成UUID,如果服务端发现同一个幂等键已经存在,直接返回该键对应的订单,不创建新订单。这样即使网络超时用户重试,也不会生成两笔预约。
6.4 顶部导航栏高度适配
热搜词里"微信小程序顶部导航栏高度""微信小程序自定义标题,上边距怎么弄"这类问题看起来很小,但确实能浪费初学者半小时。微信小程序的顶部导航栏分为两种:默认导航栏和自定义导航栏。默认导航栏高度不用管,系统自动适配;自定义导航栏时,需要动态获取状态栏高度:
const { statusBarHeight } = wx.getWindowInfo(); // 胶囊按钮位置 const { top, height } = wx.getMenuButtonBoundingClientRect(); // 导航栏高度 = (胶囊top - 状态栏height) * 2 + 胶囊height const navBarHeight = (top - statusBarHeight) * 2 + height;这个公式算出来的是导航栏总高度,statusBarHeight是刘海屏状态栏的高度,top是胶囊按钮距顶部的距离。拿到后设置自定义导航栏的padding-top: statusBarHeight px; height: navBarHeight px;即可。
在洗衣房小程序的首页和设备详情页,我用的是自定义导航栏,因为需要在导航栏右边放一个"扫码报修"的胶囊按钮,而默认导航栏不支持右侧自定义按钮。实际部署时经历过iPhone 14 Pro的灵动岛和普通安卓机的差异,用上面的公式实测都能适配,没有发现明显偏移。
7. 踩过的坑与上线后的迭代方向
最后写几个文档里查不到的经验,以及这套系统后续可以扩展的方向,也是我自己正在做的。
7.1 预约系统最容易忽视的三个"小"功能
第一个是"提前到店"的处理。用户预约了14:00,但13:40就到了,此时设备正在被前一个用户使用或处于空闲状态。我们的处理是:空闲设备允许提前启动,不需要强制等到预约时间;但如果设备被占用,用户只能等待。这个逻辑在订单状态里加了"待启动"和"已启动"的区分。
第二个是"忘记取衣"的处理。洗衣程序结束后衣服留在机器里,后面的用户无法使用。我在App里加了一个"取衣提醒"推送,结合门店的摄像头做AI识别(如果有条件)——没有条件的话,最简单的做法是系统检测到设备运行结束30分钟后仍未被标记为"已取衣",就推送一条"您有衣物在XX店未取走"的通知。这个功能对用户体验的提升非常大,因为它直接避免了下一位用户到店后发现一堆衣服堵在机器里的尴尬。
第三个是"订单取消后的动态释放"。用户取消预约后,时间片立刻释放,但微信小程序端用户如果在取消前已经加载了设备详情页,页面上显示的仍可能是"已预约"。这里要用wx.onShow重新拉取设备状态,而不是在页面onLoad时一次性拉取。
7.2 性能与扩展:从单店到连锁
单店版本跑通后,连锁扩张带来的第一个变化是数据量增长。预约表按设备和时间组合会产生大量历史数据,单表查询会越来越慢。好在预约场景的时间维度非常规律,按月分表就够了:reservation_202501、reservation_202502……分表键用预约开始时间start_time。
第二个变化是多门店维度。设备表要增加store_id字段,预约查询必须按门店过滤。用户的"最近门店"可以通过wx.getLocation拿到的经纬度做距离排序,这里我用的是Haversine公式,因为门店数量在几十家量级,全量计算性能完全够,不需要上GeoHash。
第三个扩展方向是会员储值。如果运营需要推"充值送洗衣次数"之类的活动,需要接入微信支付的余额充值能力,以及一套账户余额管理模块。这块的核心难点是资金对账,不建议在早期版本做。
7.3 版本发布与灰度
微信小程序的发布流程比较固定:开发版→体验版→审核→线上版。洗衣房预约系统因为是线下强依赖的服务,我吃过一次"审核通过后立即全量上线"的亏——新版本有个设备列表的接口字段调整,导致点击某台设备白屏,用户直接投诉到门店。
之后的发布策略变成:体验版先在内部测试群放两天,线上版先按10%灰度发布,第二天观察无异常再全量。小程序的灰度发布可以在"小程序后台-版本管理-灰度发布"里设置,按用户比例或按地域灰度都可以。另外强烈建议在上线前打开"接口域名白名单"校验,并在app.json里关闭"调试模式",减少线上问题。
说实话,洗衣房预约这个场景在微信小程序里做起来并不复杂,真正的复杂度全部来自"线下设备和线上状态的同步"、"支付和订单的一致性"、"消息表达和用户预期的对齐"这三个地方。把这三点想透了,系统就稳定了大半。这也是我在做这套系统时收获最大的部分——技术本身是透明的,难的是理解它服务的那个真实物理世界。