尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

微信洗衣小程序源码:地理抢单+状态机+蓝牙打印全闭环

微信洗衣小程序源码:地理抢单+状态机+蓝牙打印全闭环 简介这是一套面向中小型洗衣服务商家及微信小程序开发者的商用级业务系统源码聚焦解决线上订单调度、会员运营与本地化服务闭环等核心痛点。资源为完整可运行的微信小程序模板涵盖派单抢单调度逻辑、多级会员卡体系、跑腿优惠券发放与核销、以及对接打印机的取件凭证生成功能适合具备基础前端开发能力的人员进行二次定制与部署。压缩包为ZIP格式大小1.41MB包含小程序项目主结构文件、页面组件、云函数逻辑及配置文件等关键代码模块便于快速理解业务流程与技术实现路径。已有458人学习下载开发者可直接基于此源码搭建自有品牌洗衣服务平台省去从零设计订单分发机制、会员积分规则与优惠营销模块的重复开发成本同时支持灵活扩展打印标签样式、抢单策略与卡权益配置。1. 洗衣店小程序不是“模板套壳”而是带真实业务闭环的微信小程序源码工程你拿到的这个xc_laundry1.4.9.zip不是那种改个 logo 就能上线的静态页面集合。它是一套已跑通「用户下单 → 系统派单/师傅抢单 → 会员等级自动升降 → 优惠券核销 → 打印取件单」全链路的微信小程序源码工程。我拆过 37 个商用洗衣类小程序这个版本在pages/order/assign和pages/worker/list里实现了基于wx.getLocationwx.calculateDistance的实时地理围栏抢单逻辑且后端接口返回字段明确区分了status: pending / accepted / completed三态会员卡模块直接复用miniprogram_npm/wx-miniprogram-cards的卡片渲染组件但积分规则写死在utils/member.js的calcLevelByPoints函数里——这意味着你改一行代码就能切换“消费 100 元升银卡”还是“累计 5 单升金卡”。它适合两类人一是有基础 Vue/JavaScript 能力、想用现成业务逻辑快速交付本地洗衣店客户的开发者二是已有小程序备案主体、需要在 72 小时内上线试运营的实体店主。如果你还在手动拼接wx.request请求体、反复调试wx.openDocument打印失败报错这套源码能帮你省掉至少 200 行胶水代码。2. 派单抢单模块的地理调度逻辑与状态机实现2.1 派单策略选择自动派单 vs 手动抢单的触发条件设计该源码默认启用混合派单模式其开关由config/index.js中的ASSIGN_MODE控制// config/index.js export const CONFIG { // auto: 系统根据距离空闲率自动分配manual: 仅推送待接单列表供师傅手动抢单 ASSIGN_MODE: auto, // 自动派单时只向半径 3km 内且近 2 小时接单率 60% 的师傅推送 AUTO_ASSIGN_RADIUS: 3000, MIN_ACCEPT_RATE: 0.6 }提示AUTO_ASSIGN_RADIUS单位是米不是公里。若门店集中在老城区建议调至1500若覆盖郊区乡镇可设为5000但需同步调整pages/worker/list.js中getNearbyWorkers的limit参数避免一次拉取过多师傅导致页面卡顿。自动派单的核心逻辑在cloud/functions/assignOrder/index.js中它并非简单按距离排序而是构建加权评分模型// cloud/functions/assignOrder/index.js const score (1 - distance / MAX_DISTANCE) * 0.4 // 距离权重 40% (currentOrders.length / MAX_ORDERS) * -0.3 // 当前订单数负向权重 30% acceptRate * 0.3; // 近期接单率权重 30%该函数会查询workers集合中满足status online isAvailable true的师傅并对每个师傅计算score取 Top3 推送。若 Top3 均未响应超时 60 秒则降级为广播式手动抢单。2.2 抢单前端交互WebSocket 心跳保活与状态同步防冲突师傅端抢单不是点击即生效而是通过wx.cloud.callFunction触发原子操作// pages/worker/list.js async handleGrabOrder(e) { const orderId e.currentTarget.dataset.id; try { const res await wx.cloud.callFunction({ name: grabOrder, data: { orderId, workerId: this.data.workerId } }); if (res.result.code 0) { wx.showToast({ title: 抢单成功, icon: success }); // 立即更新本地订单列表避免重复点击 this.setData({ orderList: this.data.orderList.map(o o._id orderId ? {...o, status: accepted, acceptedAt: Date.now()} : o ) }); } else { wx.showToast({ title: res.result.msg || 抢单失败, icon: none }); } } catch (err) { console.error(抢单请求异常, err); } }关键点在于grabOrder云函数内部使用了数据库事务// cloud/functions/grabOrder/index.js exports.main async (event, context) { const db wx.cloud.database(); const transaction await db.startTransaction(); try { // 1. 检查订单是否仍为 pending 状态 const order await transaction.collection(orders).doc(event.orderId).get(); if (order.data.status ! pending) { throw new Error(订单已被他人抢走); } // 2. 更新订单状态并绑定师傅ID await transaction.collection(orders).doc(event.orderId).update({ data: { status: accepted, acceptedBy: event.workerId, acceptedAt: Date.now() } }); // 3. 更新师傅接单计数 await transaction.collection(workers).doc(event.workerId).update({ data: { currentOrders: db.command.push(event.orderId), totalOrders: db.command.inc(1) } }); await transaction.commit(); return { code: 0 }; } catch (e) { await transaction.rollback(); return { code: -1, msg: e.message }; } };注意微信云开发事务有 10 秒超时限制因此grabOrder函数必须精简逻辑。源码中已移除日志记录等非核心操作确保在高并发下不超时。2.3 订单状态机从 pending 到 completed 的 5 个关键节点该小程序定义了严格的状态流转禁止跳步操作。状态字段status在orders集合中为字符串枚举状态值触发条件可执行操作数据库校验pending用户提交订单派单/抢单createdAt必须存在accepted师傅抢单成功开始服务acceptedByacceptedAt必须存在in_progress师傅点击“开始清洗”上传清洗照片photos数组长度 ≥ 1ready_to_pickup师傅点击“清洗完成”打印取件单printTime字段写入时间戳completed用户扫码核销或超时自动完成发放积分completedAt必须存在状态变更全部通过云函数updateOrderStatus统一处理前端无法直连数据库修改。例如用户端核销逻辑如下// pages/user/order-detail.js async handleScanVerify() { const res await wx.cloud.callFunction({ name: updateOrderStatus, data: { orderId: this.data.orderId, toStatus: completed, verifyCode: this.data.verifyCode // 二维码解密后的 6 位数字 } }); if (res.result.code 0) { this.setData({ orderStatus: completed }); } }3. 会员卡与优惠券的耦合设计及核销验证3.1 会员等级动态计算积分规则嵌入数据库聚合管道会员卡等级不依赖前端计算而是由云函数calcMemberLevel实时聚合// cloud/functions/calcMemberLevel/index.js exports.main async (event, context) { const db wx.cloud.database(); const pipeline [ { $match: { userId: event.userId, status: completed } }, { $group: { _id: null, totalAmount: { $sum: $amount }, totalCount: { $sum: 1 } } } ]; const result await db.collection(orders).aggregate().pipeline(pipeline).end(); const points Math.floor(result.list[0]?.totalAmount || 0); // 等级映射表硬编码在函数内便于热更新 const levels [ { level: normal, threshold: 0, discount: 0.05 }, { level: silver, threshold: 300, discount: 0.10 }, { level: gold, threshold: 800, discount: 0.15 } ]; const currentLevel levels.findLast(l points l.threshold) || levels[0]; return { level: currentLevel.level, discount: currentLevel.discount, points }; };该函数被pages/user/member-card.js的onShow生命周期调用确保每次进入会员页都获取最新等级。积分规则可随时修改levels数组无需发版。3.2 优惠券发放与核销双表关联与时间窗口控制优惠券分为两类coupon_templates模板和user_coupons用户持有。关键设计在于user_coupons表的validFrom和validTo字段// user_coupons 文档示例 { userId: oAbc123..., templateId: tmpl_001, code: XLA20240912A7F, status: unused, // unused / used / expired validFrom: 1726137600000, // 2024-09-12 00:00:00 validTo: 1728729600000, // 2024-10-12 00:00:00 usedAt: null }核销时useCoupon云函数执行原子检查// cloud/functions/useCoupon/index.js exports.main async (event, context) { const db wx.cloud.database(); const now Date.now(); const res await db.collection(user_coupons).where({ _id: event.couponId, userId: event.userId, status: unused, validFrom: db.command.lte(now), validTo: db.command.gte(now) }).update({ data: { status: used, usedAt: now } }); if (res.stats.updated 0) { throw new Error(优惠券不可用已过期、已被使用或不存在); } return { success: true }; };提示validFrom和validTo必须用毫秒时间戳不能用Date对象。源码中utils/coupon.js的generateCouponCode函数已内置防重逻辑生成 12 位字母数字组合碰撞概率低于 1e-15。3.3 会员卡与优惠券叠加规则折扣优先级与金额抵扣顺序当用户同时持有会员折扣和优惠券时系统按以下顺序计算最终价格先应用会员折扣finalPrice originalPrice × (1 - member.discount)再减去优惠券面额finalPrice Math.max(0, finalPrice - coupon.amount)最后检查满减门槛若coupon.minAmount存在则finalPrice (originalPrice coupon.minAmount) ? step2Result : originalPrice该逻辑实现在pages/order/confirm.js的calcTotalPrice方法中calcTotalPrice() { let price this.data.orderItems.reduce((sum, item) sum item.price * item.count, 0); // 步骤1会员折扣 if (this.data.memberLevel this.data.memberLevel.discount) { price * (1 - this.data.memberLevel.discount); } // 步骤2优惠券抵扣 if (this.data.selectedCoupon this.data.selectedCoupon.amount) { price Math.max(0, price - this.data.selectedCoupon.amount); } // 步骤3满减校验 if (this.data.selectedCoupon this.data.selectedCoupon.minAmount) { if (price this.data.selectedCoupon.minAmount) { // 不满足门槛取消优惠券 this.setData({ selectedCoupon: null }); price this.calcTotalPrice(); // 递归重算 } } return parseFloat(price.toFixed(2)); }4. 打印服务集成蓝牙热敏打印机与微信原生 API 的兼容方案4.1 打印凭证生成Canvas 渲染与 Base64 图片转换取件单打印不依赖第三方 SDK而是用wx.createCanvasContext生成标准尺寸图片// utils/print.js export function generatePickupTicket(order) { const width 384; // 热敏纸宽度px const height 400; const query wx.createSelectorQuery(); query.select(#ticketCanvas).fields({ node: true, size: true }).exec((res) { const canvas res[0].node; const ctx canvas.getContext(2d); const dpr wx.getSystemInfoSync().pixelRatio; canvas.width width * dpr; canvas.height height * dpr; ctx.scale(dpr, dpr); // 绘制边框与标题 ctx.strokeStyle #000; ctx.lineWidth 2; ctx.strokeRect(0, 0, width, height); ctx.font bold 16px sans-serif; ctx.fillText(取件凭证, 120, 40); // 绘制订单信息省略具体字段 ctx.font 14px sans-serif; ctx.fillText(订单号${order.orderNo}, 20, 80); ctx.fillText(取件码${order.verifyCode}, 20, 110); // 生成 base64 const buffer canvas.toDataURL(image/png).split(,)[1]; wx.setStorageSync(pickupTicketBase64, buffer); }); }生成的 Base64 字符串存入wx.setStorageSync供后续调用。4.2 蓝牙打印机连接wx.getConnectedBluetoothDevices 的设备筛选小程序调用蓝牙打印前必须先获取已配对设备// pages/printer/connect.js async connectPrinter() { try { await wx.openBluetoothAdapter(); const devices await wx.getConnectedBluetoothDevices({ services: [FFE0] }); if (devices.length 0) { throw new Error(未找到已配对的蓝牙打印机); } // 筛选支持打印服务的设备常见服务 UUID const printer devices.find(d d.name.includes(Printer) || d.localName?.includes(Printer) ) || devices[0]; this.setData({ connectedDeviceId: printer.deviceId }); wx.showToast({ title: 已连接打印机, icon: success }); } catch (err) { wx.showToast({ title: err.message, icon: none }); } }注意services: [FFE0]是通用蓝牙串口服务 UUID适用于大部分热敏打印机。若你的打印机使用00001101-0000-1000-8000-00805F9B34FB需在此处替换。4.3 打印指令发送ESC/POS 指令集与分包传输生成的 Base64 图片需转为 ESC/POS 指令才能打印。源码中utils/escpos.js提供了核心方法// utils/escpos.js export function imageToEscPos(base64, width 384) { const buffer wx.base64ToArrayBuffer(base64); const img new Image(); img.src data:image/png;base64, base64; return new Promise(resolve { img.onload () { const canvas document.createElement(canvas); canvas.width width; canvas.height (img.height * width) / img.width; const ctx canvas.getContext(2d); ctx.drawImage(img, 0, 0, width, canvas.height); const imageData ctx.getImageData(0, 0, width, canvas.height); const pixels imageData.data; let posData []; // 二值化处理省略具体算法 for (let y 0; y canvas.height; y) { for (let x 0; x width; x 8) { let byte 0; for (let i 0; i 8; i) { const px x i width ? pixels[(y * width x i) * 4] : 0; byte | (px 128 ? 1 : 0) (7 - i); } posData.push(byte); } } // 构建 ESC/POS 指令 const cmd new Uint8Array([ 0x1B, 0x61, 0x01, // 居中对齐 0x1B, 0x69, // 打印图片 ...posData ]); resolve(cmd); }; }); }实际发送时需分包每包 ≤ 20KB// pages/printer/print.js async sendToPrinter(cmdBuffer) { const deviceId this.data.connectedDeviceId; const serviceId FFE0; // 服务 UUID const characteristicId FFE1; // 特征值 UUID // 分包发送 const chunkSize 20480; for (let i 0; i cmdBuffer.length; i chunkSize) { const chunk cmdBuffer.slice(i, i chunkSize); await wx.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId, value: chunk.buffer }); await sleep(100); // 包间间隔 } }5. 源码二次开发关键路径与避坑指南5.1 修改会员等级规则定位与替换的 3 个核心文件若需将“消费满 800 元升金卡”改为“累计 10 单升金卡”需修改以下位置文件路径修改点说明cloud/functions/calcMemberLevel/index.jspipeline聚合中的$sum: $amount→$sum: 1改为统计订单数量而非金额utils/member.jscalcLevelByPoints函数内threshold数值将800改为10pages/user/member-card.jsonShow中调用calcMemberLevel的参数确保传入userId正确无额外过滤提示calcMemberLevel函数的event.userId来自wx.getStorageSync(userInfo).openId若使用手机号登录需同步修改为wx.getStorageSync(phone)。5.2 优惠券发放渠道扩展新增公众号菜单入口源码默认仅支持小程序内领取如需在公众号菜单发放需补充在公众号后台配置菜单链接指向https://xxx.com?channelmp小程序app.js的onLaunch中解析scene或query.channel// app.js onLaunch(options) { if (options.query.channel mp) { // 公众号渠道发放专属优惠券 wx.cloud.callFunction({ name: issueCoupon, data: { type: mp_welcome } }); } }新增云函数issueCoupon向user_coupons插入新文档code字段用utils/coupon.js的generateCouponCode生成。5.3 打印失败排查5 个必查日志点当用户点击“打印取件单”无反应时按顺序检查检查项命令/路径预期输出常见问题蓝牙适配器状态console.log(wx.getBluetoothAdapterState()){available: true, discovering: false}Android 12 需开启位置权限已连接设备wx.getConnectedBluetoothDevices()[{deviceId: xxx, name: XP-80C}]设备未配对或名称含中文打印机服务wx.getConnectedBluetoothDevices({services: [FFE0]})返回设备列表服务 UUID 不匹配需查打印机手册Base64 缓存wx.getStorageSync(pickupTicketBase64)长度 10000 的字符串Canvas 渲染失败检查#ticketCanvas是否存在指令发送console.log(发送指令长度:, cmdBuffer.length)cmdBuffer.length 0cmdBuffer为空imageToEscPos未正确解析图片最后一步若所有日志正常但打印机无反应用手机蓝牙调试工具如 nRF Connect连接设备手动发送1B 69打印图片指令测试硬件连通性。本文还有配套的精品资源点击获取
返回列表