简介:本资源是一套面向微信小程序开发者的学习型排队系统实战源码,适用于餐饮、零售等需线上预约与等候服务的轻量级业务场景,特别适合具备基础小程序开发能力的学习者进行全栈实践。压缩包共14个文件(5个JS逻辑文件、3个WXSS样式文件、3个JSON配置文件、2个WXML页面结构文件及1个说明文本),总大小仅11KB,结构精简但覆盖完整链路:从前端页面渲染、用户授权登录、队列状态展示,到后端接口对接逻辑与基础数据管理设计。已有747人下载学习,源码基于真实项目简化而来,包含app.json全局配置、pages目录下的多页面路由、utils工具函数封装,以及关键的实时排队状态更新与用户信息绑定实现思路,可直接运行调试,是理解微信排队系统核心机制(如队列排序策略、模板消息触发时机、本地缓存与服务器同步协同)的优质入门参考。
1. 项目背景与核心价值:为什么需要一个微信小程序排队系统?
最近在整理项目资料时,翻出了一个几年前做的微信小程序排队系统Demo的完整源码。当时是为了解决一个线下餐饮门店的排队叫号痛点而做的原型验证,虽然项目本身没有大规模商用,但整个架构和实现思路非常清晰,涵盖了小程序开发中从用户端到管理端的多个核心环节。今天把它分享出来,一方面是给正在学习微信小程序开发的朋友一个完整的、可运行的参考项目;另一方面,也是想聊聊,在看似简单的“排队”功能背后,隐藏着哪些技术选型、架构设计和业务逻辑的思考。
排队系统,听起来简单,不就是取个号、等叫号嘛。但在实际线下场景中,它连接着用户、服务员、后厨、收银等多个角色,是一个典型的“高并发、实时性、状态同步”的微场景。用户希望实时看到自己的排队进度,服务员需要高效、无差错地叫号,管理者则关心队列的流转效率和数据统计。微信小程序凭借其免安装、即用即走、依托微信庞大用户体系的特性,成为实现这类线下服务数字化工具的绝佳载体。
这个Demo源码,我把它命名为“grandmothern1j”(一个随机的项目代号),完整实现了从顾客扫码取号、查看实时队列、到店通知,到后台管理端进行队列管理、叫号、过号处理等核心功能。它不仅仅是一堆代码的堆砌,更是一个包含了前后端交互、WebSocket实时通信、云开发基础能力运用的综合案例。对于想深入理解小程序如何与线下业务结合,如何设计实时交互系统的开发者来说,具有很高的参考价值。
2. 系统架构与核心技术栈选型解析
一套可用的排队系统,远不是一个前端页面就能搞定的。它需要一个稳定、高效、能实时同步数据的技术架构来支撑。在这个Demo中,我基于当时的技术环境和项目需求,做出了以下核心选型。
2.1 前端技术栈:微信小程序原生开发
选择微信小程序原生开发框架,而非uniapp、taro等跨端方案,主要基于以下几点考虑:
- 性能与体验最优:原生开发能最大程度地利用微信提供的底层能力,如
<live-player>、<live-pusher>(虽然本项目未涉及直播,但说明原生组件优势)、更流畅的动画API等。对于排队这种需要频繁更新UI、强调即时反馈的场景,原生小程序的性能表现是最稳定可靠的。 - 生态与工具链成熟:微信开发者工具提供了完善的调试、真机预览、性能分析工具。原生语法(WXML、WXSS、JS)的学习曲线对于前端开发者来说也相对平缓,社区资源丰富,遇到问题更容易找到解决方案。
- 避免跨端复杂性:本项目初期定位明确,就是服务于微信生态内的线下门店。引入跨端框架虽然能获得多端发布的能力,但也会增加编译复杂度、可能引入额外的兼容性问题,对于追求快速验证和稳定性的Demo来说,得不偿失。
在代码组织上,我采用了比较清晰的分层结构:
pages/: 存放所有小程序页面,如index(取号首页)、queue(我的排队)、admin(管理后台)。components/: 封装可复用的UI组件,如queue-card(排队信息卡片)、number-call(叫号组件)。utils/: 工具函数库,包括网络请求封装、时间格式化、本地缓存管理等。app.js/app.json/app.wxss: 全局配置、样式和逻辑。
2.2 后端与数据同步方案:云开发 + WebSocket
这是本系统的技术核心。排队数据的实时性是生命线。传统的HTTP轮询(每隔几秒请求一次服务器)方案,不仅浪费资源,还会带来明显的延迟,用户体验很差。
1. 为什么选择云开发(CloudBase)?对于个人开发者或中小型项目来说,自建服务器(购买ECS、配置Nginx、部署Node.js/Python服务、维护数据库)是一笔不小的成本和精力开销。微信云开发提供了开箱即用的后端能力:
- 云数据库:一个JSON文档型数据库,直接在小程序端通过SDK即可读写,无需自行开发API接口。这对于排队队列(一个数组或集合)、用户信息(文档)这种结构的数据存储非常友好。
- 云函数:用于处理复杂的、需要安全校验的业务逻辑。例如,用户取号时,需要在云函数中校验门店状态、生成唯一的排队号、并初始化排队数据。云函数的运行环境是隔离的,安全性更高。
- 云存储:可以用来存放门店的Logo、叫号语音提示文件等静态资源。
在app.js中初始化云开发环境是第一步:
// app.js App({ onLaunch: function () { wx.cloud.init({ env: 'your-env-id', // 替换为你的云环境ID traceUser: true, }) } })2. 实时通信的基石:WebSocketHTTP轮询无法满足实时性要求,而云数据库的“实时数据推送”能力(监听某个集合的变化)在复杂场景下(如多门店、多队列)的管控粒度可能不够灵活。因此,我引入了WebSocket来实现服务端向客户端的主动消息推送。
- 工作原理:在管理端进行“叫号”操作时,后端服务(可以是一个云函数,或一个单独的Socket服务)会通过WebSocket连接,向所有正在排队页面(
queue)的在线用户广播一条消息,内容包含最新的队列信息和被叫号码。 - 实现细节:在小程序端,使用
wx.connectSocketAPI建立连接,并在onSocketMessage回调中处理接收到的消息,更新本地页面数据。为了保持连接稳定,还需要实现心跳机制和断线重连逻辑。
// 在queue页面建立WebSocket连接 Page({ onLoad() { this.connectWebSocket(); }, connectWebSocket() { const socketTask = wx.connectSocket({ url: 'wss://your-websocket-server.com', success: () => console.log('Socket连接成功') }); socketTask.onMessage((res) => { const data = JSON.parse(res.data); if (data.type === 'queue_update') { // 更新本地排队列表 this.setData({ queueList: data.list }); } else if (data.type === 'call_number') { // 收到叫号通知,可以震动或弹窗提示 wx.vibrateLong(); this.showCallModal(data.number); } }); // 存储socketTask以便后续关闭 this.data.socketTask = socketTask; }, onUnload() { // 页面卸载时关闭连接 this.data.socketTask && this.data.socketTask.close(); } })这个“云数据库 + WebSocket”的混合架构,既利用了云开发的便捷性处理数据持久化和业务逻辑,又通过WebSocket保证了核心排队状态变化的毫秒级实时同步,是兼顾效率与实时性的优选方案。
2.3 数据库设计:如何用一张表管理排队?
很多人会觉得,排队系统至少需要“用户表”、“排队记录表”、“门店表”。但在Demo中,为了极致简化,我主要用云数据库的一个核心集合(collection)queues来管理所有状态。这并不是说多表设计不对,而是针对Demo场景的一种高效设计。
queues集合的文档结构设计如下:
{ "_id": "queue_001", // 队列唯一ID,可与门店ID关联 "shopName": "外婆家杭州西湖店", "currentNumber": "A102", // 当前叫到的号码 "waitingList": [ // 等待队列,一个有序数组 { "queueNumber": "A103", "userId": "user_openid_xxx", "userNickName": "张三", "createTime": "2023-10-27T10:00:00Z", "status": "waiting" // waiting, called, missed, completed }, // ... 更多等待用户 ], "calledList": [ // 已叫号但未处理的列表 { "queueNumber": "A102", "userId": "user_openid_yyy", "callTime": "2023-10-27T10:05:00Z" } ], "settings": { "prefix": "A", // 号码前缀 "startFrom": 100, // 起始号码 "estimatedWaitTimePerCustomer": 5 // 预估每人等待时间(分钟) }, "updateTime": "2023-10-27T10:05:30Z" // 最后更新时间,用于监听变化 }这样设计的好处:
- 原子操作:对单个队列的“取号”、“叫号”、“过号”操作,都可以通过更新这一个文档来完成。利用云数据库的“原子操作”能力(如
db.command.inc、db.command.push、db.command.pull),可以保证在高并发取号时不会出现号码重复或数据错乱。 - 实时监听简便:小程序端只需要监听这一个
queues集合中特定_id文档的变化,即可获取该队列的所有最新信息,包括等待列表、当前号码等。 - 减少联表查询:所有必要信息都在一个文档里,读取效率高。
当然,在更复杂的生产环境中,可能需要将用户信息独立成表,并增加订单表、历史记录表等。但当前这个结构足以支撑Demo的所有功能,并且清晰地展示了核心数据流。
3. 核心功能模块实现与代码拆解
有了架构和设计,我们来看看关键功能是如何一行行代码实现的。这里我会挑几个最有代表性的模块,结合代码片段进行讲解。
3.1 用户端:扫码取号与实时队列
用户端的核心流程是:扫码(或手动选择)→ 输入信息 → 生成排队号 → 进入排队页面实时等待。
1. 取号页(index)取号通常通过扫描门店二维码进入,二维码中携带了门店或队列的ID参数。在页面的onLoad生命周期中,会解析这个参数。
// pages/index/index.js Page({ data: { shopInfo: null, queueId: '' }, onLoad(options) { // options.scene 是扫码进来的场景值,需要解码 const scene = decodeURIComponent(options.scene); // 假设scene格式为 `queueId=xxx` const queueId = scene.split('=')[1]; this.setData({ queueId }); this.loadShopInfo(queueId); }, async loadShopInfo(queueId) { const db = wx.cloud.database(); const res = await db.collection('shops').doc(queueId).get(); // 假设有shops集合存储门店信息 this.setData({ shopInfo: res.data }); }, // 用户点击取号按钮 async takeNumber() { const { queueId } = this.data; // 调用云函数,在服务端完成取号逻辑,保证原子性 wx.cloud.callFunction({ name: 'takeQueueNumber', data: { queueId }, success: async (res) => { const { queueNumber, position } = res.result; // 取号成功,跳转到排队页面,并传递排队号等信息 wx.navigateTo({ url: `/pages/queue/queue?queueId=${queueId}&queueNumber=${queueNumber}&position=${position}` }); }, fail: (err) => { wx.showToast({ title: '取号失败', icon: 'none' }); } }); } })2. 排队页(queue)这是用户停留时间最长的页面,需要实时展示排队进度、预估等待时间,并接收叫号通知。
- 实时数据获取:除了使用前面提到的WebSocket,也可以使用云数据库的实时监听功能作为备选或补充。
// pages/queue/queue.js Page({ data: { queueInfo: {}, myNumber: 'A103', myPosition: 5, estimatedTime: 25 // 分钟 }, onLoad(options) { this.setData({ queueId: options.queueId, myNumber: options.queueNumber }); // 方法一:监听云数据库队列文档变化 this.watchQueue(); // 方法二:建立WebSocket连接(代码见前文) this.connectWebSocket(); }, watchQueue() { const db = wx.cloud.database(); const _ = db.command; this._watcher = db.collection('queues').doc(this.data.queueId).watch({ onChange: (snapshot) => { const queueInfo = snapshot.docs[0]; if (queueInfo) { this.setData({ queueInfo }); // 计算我的位置和预估时间 this.calculateMyPosition(queueInfo); } }, onError: (err) => console.error('监听失败', err) }); }, calculateMyPosition(queueInfo) { const waitingList = queueInfo.waitingList; const myIndex = waitingList.findIndex(item => item.queueNumber === this.data.myNumber); if (myIndex > -1) { const position = myIndex + 1; const waitTime = position * queueInfo.settings.estimatedWaitTimePerCustomer; this.setData({ myPosition: position, estimatedTime: waitTime }); } }, onUnload() { // 页面卸载时关闭监听和Socket this._watcher && this._watcher.close(); this.data.socketTask && this.data.socketTask.close(); } })- 叫号通知:当WebSocket收到叫号消息,且被叫号码是自己的号码时,触发强提醒(震动、弹窗、播放语音),引导用户前往服务台。
3.2 管理端:叫号、过号与队列管理
管理端通常以PC Web页面或另一个小程序页面的形式存在,供店员使用。核心功能是“叫号”。
叫号逻辑的实现(云函数callNumber): 叫号不是一个简单的前端操作,它涉及状态变更、广播通知、可能的历史记录,必须在服务端(云函数)中完成以保证数据一致性和安全性。
// cloudfunctions/callNumber/index.js const cloud = require('wx-server-sdk'); cloud.init({ env: process.env.ENV_ID }); const db = cloud.database(); const _ = db.command; exports.main = async (event, context) => { const { queueId } = event; const wxContext = cloud.getWXContext(); // 1. 获取当前队列文档 const queueDoc = await db.collection('queues').doc(queueId).get(); const queueData = queueDoc.data; const waitingList = queueData.waitingList; if (waitingList.length === 0) { return { code: 1, msg: '当前没有等待的顾客' }; } // 2. 取出等待队列中的第一个顾客 const customerToCall = waitingList[0]; const calledNumber = customerToCall.queueNumber; // 3. 原子操作:从waitingList移除,并添加到calledList,更新currentNumber try { await db.collection('queues').doc(queueId).update({ data: { currentNumber: calledNumber, waitingList: _.shift(), // 移除数组第一个元素 calledList: _.push([{ // 添加到已叫列表 ...customerToCall, callTime: new Date(), status: 'called' }]), updateTime: new Date() } }); // 4. 通过WebSocket服务广播叫号消息(这里假设有另一个WebSocket服务) // 在实际项目中,可以调用一个内部API,或使用云开发的HTTP API触发WebSocket服务 // 此处为伪代码 // broadcastToQueue(queueId, { type: 'call_number', number: calledNumber }); // 5. (可选)发送模板消息给被叫号的用户 // ... return { code: 0, msg: '叫号成功', data: { calledNumber } }; } catch (err) { console.error('叫号失败', err); return { code: -1, msg: '叫号操作失败,请重试' }; } };过号处理:如果顾客过号未到,管理端可以执行“过号”操作。这个操作通常是将该顾客从calledList移回waitingList的末尾,或者移到一个单独的missedList,并更新其状态为missed。同样,这个操作也必须在云函数中原子化完成,并广播队列更新消息。
3.3 云函数:安全与业务逻辑的守护者
从上面的例子可以看出,所有涉及核心数据写操作(取号、叫号、过号)的逻辑,都放在了云函数中。这是小程序开发中的最佳实践,原因有三:
- 安全性:云数据库的权限设置可以配置为仅云函数可写,小程序端只读。这样避免了前端代码被破解后直接恶意篡改数据库的风险。
- 原子性:云函数在执行数据库操作时,可以确保一系列更新动作的原子性,防止出现数据不一致(例如,两个用户同时取到同一个号)。
- 复杂性封装:像生成唯一排队号(结合日期、前缀、自增序号)、计算预估等待时间、发送模板消息等复杂逻辑,都适合在云函数中完成,保持前端代码的简洁。
例如,取号云函数takeQueueNumber的核心逻辑就包括:检查队列状态、生成下一个排队号、将用户信息原子性地添加到waitingList数组末尾。
4. 开发中的关键细节与避坑指南
在实际开发这个Demo的过程中,我遇到了不少坑,也总结出一些让系统更健壮、体验更好的细节。这些是文档里不会写的“实战经验”。
4.1 排队号生成策略:如何保证唯一且有序?
排队号不能简单用自增ID,因为每天、每个队列都需要重置。我采用的策略是:前缀 + 日期 + 当日自增序号,例如A202310270015。
- 前缀:可以区分不同队列(如A区、B区)或业务类型。
- 日期:保证每天从1开始计数。
- 自增序号:需要原子性递增。在云开发中,可以借助一个独立的
counters集合来实现分布式原子递增。
// 在takeQueueNumber云函数中 async function generateQueueNumber(queueId, prefix) { const db = cloud.database(); const _ = db.command; const counterDoc = await db.collection('counters').doc(`queue_${queueId}_${getTodayString()}`).get(); let nextSeq = 1; if (counterDoc.data) { // 原子增加序列号 const updateRes = await db.collection('counters').doc(`queue_${queueId}_${getTodayString()}`).update({ data: { seq: _.inc(1) } }); // 这里需要处理并发,更严谨的做法是用事务,但云函数环境本身是串行化执行可降低冲突概率 nextSeq = counterDoc.data.seq + 1; } else { // 当天第一条记录 await db.collection('counters').doc(`queue_${queueId}_${getTodayString()}`).set({ data: { seq: 1 } }); } const seqStr = nextSeq.toString().padStart(3, '0'); // 补零到3位 return `${prefix}${getTodayString()}${seqStr}`; }注意:在高并发取号场景下,上述代码仍有极小概率产生重复号。生产环境应考虑使用数据库事务(如果支持),或使用具备更强原子性能力的服务(如Redis的INCR命令)。对于Demo和大多数线下排队场景,云函数的单实例执行模型已能很大程度上避免冲突。
4.2 实时性的权衡:WebSocket vs. 云数据库监听
前面提到了两种实时方案。在实际中如何选择?
- WebSocket:延迟最低(毫秒级),可控性最强。适合需要精准控制消息格式、广播范围(如只广播给特定队列的用户)的场景。缺点是需要自己维护一个WebSocket服务(可以是另一个云函数或独立服务器),增加了架构复杂度。
- 云数据库监听:开发简单,无需额外服务。只需一行
watch()代码。适合数据模型简单、变更即通知全体的场景。缺点是监听的是整个文档的变化,任何字段更新都会触发回调,需要前端做过滤;且对于深层嵌套数组内元素的变化,监听可能不够精细;在极端网络情况下,可能会有秒级的延迟。
我的建议:对于核心的叫号通知,使用WebSocket保证即时性。对于排队列表、当前号码等信息的更新,可以同时使用云数据库监听作为数据同步的兜底和补充。两者结合,体验更佳。
4.3 小程序端的性能与体验优化
- 列表渲染优化:排队列表可能很长。一定要使用WXML的
wx:for配合wx:key,并且对于复杂卡片,考虑使用<block>包装或虚拟列表技术(虽然小程序原生支持有限,但可以自己实现简单的按需渲染)。 - 心跳与重连:WebSocket连接可能因为网络波动而断开。必须实现心跳包机制(每隔一段时间发送一个ping)和自动重连逻辑。重连时还需要重新订阅之前的队列频道。
- 本地缓存策略:用户再次打开排队页面时,可以先从本地缓存
wx.setStorageSync中读取上一次的排队信息展示,然后再去请求最新数据,避免页面长时间白屏。 - 后台运行限制:小程序切到后台后,WebSocket连接和定时器可能会被暂停。恢复前台时,要检查连接状态并重新初始化数据。可以在
onShow生命周期里做这个检查。
4.4 管理端的安全与权限控制
管理端绝对不能对所有人开放。最简单的实现方式是:
- 管理员白名单:在云数据库建立一个
admins集合,存储有权限管理员的OpenID。 - 云函数校验:在所有管理操作的云函数(如叫号、过号)开头,校验调用者的OpenID是否在白名单内。
- 管理端登录:管理端小程序或页面启动时,要求管理员微信登录,获取其OpenID并与白名单比对。
// 在每个管理云函数开头加入校验 const cloud = require('wx-server-sdk'); cloud.init(); exports.main = async (event, context) => { const wxContext = cloud.getWXContext(); const openId = wxContext.OPENID; const db = cloud.database(); const adminRecord = await db.collection('admins').where({ _openid: openId }).get(); if (adminRecord.data.length === 0) { return { code: 403, msg: '无权限进行此操作' }; } // ... 后续业务逻辑 };5. 从Demo到生产:还需要考虑什么?
这个Demo提供了一个可运行的核心模型,但要投入真实营业场景,还需要在以下几个方面进行增强:
- 多门店与多队列支持:当前的
queues集合设计天然支持多门店(每个门店一个文档)。需要增加一个shops集合来管理门店基本信息,并在用户取号时提供门店列表选择或扫码定位。 - 预估等待时间算法:Demo中使用了简单的“人数×平均时间”。生产环境可以根据历史叫号数据,动态计算每个时段的平均处理时间,让预估更准确。
- 过号与重排策略:需要定义清晰的过号规则。是直接作废?还是允许用户在过号后一段时间内(如5分钟)通过小程序一键“重新排队”并排到队尾或特定位置?
- 数据统计与分析:为管理者提供仪表盘,展示各时段排队人数、平均等待时间、过号率、服务员叫号效率等数据。
- 容灾与降级方案:万一网络故障或WebSocket服务不可用,系统如何降级?可以考虑切换到HTTP轮询模式,并提示用户“当前为弱网模式,信息更新可能有延迟”。
- 用户体验深化:增加“延迟排队”(线上取号,预估时间快到了再来)、”多人同行“(一个号代表多人)、”特殊需求备注“(如需要包间、有儿童椅)等功能。
这个微信小程序排队系统Demo的完整源码,就像一套精心打磨的乐高积木。它展示了如何用微信生态内最基础、最核心的技术(小程序、云开发、WebSocket),搭建出一个解决真实问题的数字化工具。代码本身是开源的,你可以直接运行、修改、扩展。但我更希望分享的,是背后这种“以用户场景为中心,以技术可行性为边界”的设计和实现思路。无论是用于学习,还是作为你自己项目的起点,相信它都能给你带来实实在在的启发和帮助。
本文还有配套的精品资源,点击获取