资讯详情

微信小程序排队系统Demo:取号叫号过号全链路状态机实战

发布时间:2026/9/26 23:49:13

500+
企业客户服务经验
120+
行业领域内容覆盖
3000+
原创页面设计沉淀
98%
客户满意度

微信小程序排队系统Demo:取号叫号过号全链路状态机实战

简介这套微信小程序排队系统Demo完整源码面向餐饮、零售等有线上排队需求的门店经营者以及希望掌握小程序前后端交互的初中级开发者。源码覆盖用户授权登录、队列实时更新、进度展示与消息通知等核心环节可直接参考或快速移植。压缩包共14个文件以js逻辑、wxml页面结构、wxss样式和json配置为主并附带一份txt说明整体仅11KB结构清晰便于逐文件阅读。已有749人浏览学习特别适合用来理解微信小程序项目目录组织、排队模块拆分与基础交互实现。通过学习这份源码还能看清排队状态如何通过前端数据管理驱动页面渲染并在此基础上扩展后端服务与数据库设计落地更完整的商业化排队方案。1. 微信小程序排队系统demo完整源码先把取号、叫号、过号这条链路跑通一个能跑起来的微信小程序排队系统demo完整源码和网上那些只贴几个页面、一接手就报错的碎片代码不同它需要把取号、查号、叫号、过号四个动作串成一条完整链路。餐厅等位、银行叫号、医院分诊、政务大厅全是同一套需求只是换了名字。这个demo真正值钱的地方不在排号算法而在小程序端的登录态、轮询机制、页面跳转与后端状态机怎么配合。适合两种人刚接手微信小程序项目实例、想找一个可运行参考实现的前端工程师准备给客户做原型演示、需要现场改参数的产品经理。开源的排队源码不算少但很多只有小程序端后端还得自己造所以拿到手第一件事是确认数据流通不通而不是看页面长什么样。2. 先建模再写码排队状态机与四张基础数据表很多人拿到排队系统源码第一件事是打开前端页面看长什么样我习惯反过来先把数据模型捋清楚。排队系统看着简单实际上是一个状态机一张号从取号到销号中间至少要经历四个状态。如果数据结构没定好后面做叫号、过号、统计平均等待时间时全得返工。更现实的一点是现场给客户演示时数据对不上就很尴尬所以先把状态机和表结构定稳后面写接口和页面只是在填空。2.1 状态机一张排号从取号到完成要经历哪些状态一套最小可用的排队闭环里至少有四个状态。waiting代表已取号、正在等待called代表商家已经叫号completed代表消费者响应并完成服务expired代表叫号后超过宽限时间号码作废。canceled在demo里可以留一个入口但不强制做因为微信小程序端取消操作对现场演示不是必须的。为什么called和completed必须分开因为只有拿到这两个时间点才能算出消费者从被叫到响应用了多久。这组数据一方面用来验证过号宽限设置是否合理另一方面在后续做“预计等待时间”时它是队列推进速度的原始素材。用一张迁移表来描述状态流转当前状态触发动作目标状态触发方waiting商家点击叫号called商家端called消费者确认到店completed消费者端called超过过号宽限时间expired服务端定时器waiting消费者手动取消canceled消费者端在代码里我习惯用一个常量文件把状态定义出来避免小程序端和后端各写各的字符串。前端少写一个字母后端查不到数据这种黑匣子问题特别难排查。// constants/queue-status.js const QUEUE_STATUS { WAITING: waiting, CALLED: called, COMPLETED: completed, EXPIRED: expired, CANCELED: canceled }; module.exports QUEUE_STATUS;这里为什么用英文字符串而不用数字枚举主要原因是调试时可读性。现场演示时对着日志说“这条是waiting状态”比说“这条是0状态”直观得多另一个原因是后续要扩展到支付宝小程序或多端H5时语义一致不容易产生状态对不上的问题。数字枚举确实更省空间但那是生产环境才需要做的优化demo阶段优先把可读性保住。2.2 四张基础表queue_order、queue_caller、queue_config、queue_log很多人做demo把信息全塞进一张orders表我坚持拆成四张不是过度设计而是让演示时可以现场改参数、出事时有日志可查。先看一张总览表名用途核心字段queue_order每一笔排队记录id、queue_no、status、user_openid、channel_type、created_at、called_at、finish_atqueue_caller叫号终端或窗口id、store_id、caller_name、window_no、statusqueue_config商家排队参数store_id、queue_prefix、start_no、max_waiting、avg_serve_minutes、expire_after_minutesqueue_log状态流转日志id、order_id、action、operator_type、remark、created_atqueue_order是最重要的一张表建表语句我一般这么写-- 排队记录主表 CREATE TABLE queue_order ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, queue_no VARCHAR(12) NOT NULL COMMENT 排号字符串类型保留前导零, status VARCHAR(10) NOT NULL DEFAULT waiting COMMENT waiting/called/completed/expired/canceled, user_openid VARCHAR(64) NOT NULL COMMENT 取号用户的微信openid, channel_type TINYINT NOT NULL DEFAULT 0 COMMENT 0小程序 1电话 2柜台, window_no VARCHAR(6) DEFAULT NULL COMMENT 叫号窗口或桌台号, created_at DATETIME NOT NULL COMMENT 取号时间, called_at DATETIME DEFAULT NULL COMMENT 叫号时间, finish_at DATETIME DEFAULT NULL COMMENT 完成或过号时间, INDEX idx_status_created (status, created_at), INDEX idx_openid_created (user_openid, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;三个设计决策说明一下。queue_no必须用VARCHAR而不是INTA001存成数字会变成1前导零丢掉之后9号和10号的排序逻辑就乱了这是最容易翻车的点。status用带注释的字符串方便调试时直接读懂。索引优先建(status, created_at)的组合索引因为“谁在排队、谁最早”这种查询最频繁也就是商家端叫号时第一条要查的数据。queue_log表很多人觉得demo没必要我的看法相反-- 排队状态流转日志表 CREATE TABLE queue_log ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_id BIGINT UNSIGNED NOT NULL COMMENT 对应queue_order.id, action VARCHAR(20) NOT NULL COMMENT take/call/confirm/expire/cancel, operator_type TINYINT DEFAULT 0 COMMENT 0系统 1用户 2商家, remark VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL, INDEX idx_order_created (order_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;日志不是给消费者看的是给你自己准备后悔药的。演示现场最怕出现的局面是叫号按钮点了没反应你盯着数据表猜不出原因。如果每步操作都留了一条log按order_id一筛是前端没把请求发出来还是后端状态没推动一眼就能定位。日志在demo阶段可以只写action不写详情能省则省但不能没有。2.3 演示版数据源选型云开发、内存数组还是Node加MySQLdemo的数据落地方式有三种常见选型这直接决定你下载到的源码里后端长什么样。方案上手成本重启丢数据适合场景微信云开发云数据库低不丢个人开发者快速给客户演示Node.jsExpress内存数组极低丢本地跑通逻辑、断点调试Node.jsExpressMySQL中不丢准备直接迁移到生产云开发的优点是不用买服务器小程序直接调云函数天然绕开域名配置问题。缺点是本地调试不如普通Node服务方便而且数据权限规则要单独配。内存数组是最适合当demo的启动最快、代码最直白但服务一重启队列就清零现场演示前记得别手滑重启。Node加MySQL最接近生产如果你打算demo验收完直接改成正式系统建议直接用这个方案省得二次迁移。我个人做法是源码包里配Node加内存队列作为默认运行方式同时在代码注释里保留云开发的调用入口。这样下载下来跑demo的人不需要先装数据库而准备上生产的人也有迁移路径可循。数据源选型不影响业务逻辑只要前面状态机和表结构定了切换存储层就是换一个适配器的事。3. 后端接口与状态流转用Node.js把取号、叫号、过号写成最小服务前端页面做得再漂亮后端接口不通排队demo就是一个死壳。我一般把后端拆成五个接口对应五个人能感知到的动作取号、查状态、叫下一个、确认完成、手动过号。其余像配置查询、日志查询都是辅助不影响主链路。接口设计原则很简单入参尽量少返回尽量结构化让小程序端不用猜。3.1 五个核心接口与参数约定先把接口清单固定下来前端和后端就不用来回扯皮方法路径入参返回POST/api/queue/takeopenid、channelTypequeueNo、waitingCountGET/api/queue/statusqueueNostatus、waitingCount、estimateMinutesGET/api/queue/current无queueNo、windowNo、waitingCountPOST/api/queue/callwindowNoqueueNo、waitingCountPOST/api/queue/completequeueNo、actionstatus取号和叫号用POST不用GET是因为它们会改变服务端状态防止刷新页面或爬虫误触发查状态和查当前号码用GET方便在浏览器地址栏里直接验证。这里有个容易被忽略的约定所有时间字段在接口返回时统一用毫秒时间戳而不是格式化字符串。原因是小程序端要自己算“已经等了多久”“预计还要多久”拿到字符串再解析一遍很容易在iOS和Android上出现兼容性问题直接用时间戳做差值最稳妥。3.2 取号逻辑号码生成与排队上限先看取号服务的核心代码// services/queueService.js const orders []; const MAX_WAITING 200; function createQueueNo(prefix A) { const now new Date(); const dateStr [ now.getFullYear(), String(now.getMonth() 1).padStart(2, 0), String(now.getDate()).padStart(2, 0) ].join(); const todayCount orders.filter( (item) item.queueNo.startsWith(prefix dateStr) item.status waiting ).length; return ${prefix}${dateStr}${String(todayCount 1).padStart(3, 0)}; } function takeQueue(openid, channelType 0) { const waitingCount orders.filter( (item) item.status waiting ).length; if (waitingCount MAX_WAITING) { return { error: queue_full, message: 当前排队人数已满 }; } const order { id: orders.length 1, queueNo: createQueueNo(), status: waiting, openid, channelType, createdAt: Date.now(), calledAt: null, finishAt: null }; orders.push(order); return { queueNo: order.queueNo, waitingCount: waitingCount 1 }; } module.exports { takeQueue };号码里为什么带日期因为排号跨天之后会重置如果只用纯数字今天的A001和明天的A001在统计和查询时无法区分。生成规则是“前缀年月日三位流水号”比如A20250214001一眼就能看出是哪天哪个队列的第几号。waitingCount用filter现算而不是维护一个计数器是为了避免取号和叫号并发时计数器失真在单进程Node里同步遍历数组是最稳的做法。MAX_WAITING设成200是给演示留一个保护边界。真有人连着点取号排到两百个左右就该触发“当前排队人数已满”的提示而不是让数组无限膨胀把界面卡死。接着看路由层怎么把这些能力暴露出去// app.js const express require(express); const { takeQueue } require(./services/queueService); const app express(); app.use(express.json()); app.post(/api/queue/take, (req, res) { const openid req.body req.body.openid; if (!openid) { return res.status(400).json({ error: missing_openid }); } const result takeQueue(openid, req.body.channelType || 0); if (result.error) { return res.status(503).json(result); } res.json(result); }); app.listen(3000, () console.log(queue demo server on 3000));openid是必填参数缺失时返回400排队满时返回503前端拿到这两个错误码要做不同提示400说明登录态有问题503说明该换一家店或改配置。channelType默认0小程序端传1表示电话预约、传2表示到店再取方便演示时给店长看各渠道的排队比例。3.3 叫号、完成和过号状态怎么安全转移叫号逻辑是排队系统的发动机const EXPIRE_AFTER_MINUTES 3; function callNext(windowNo 01) { const waitingList orders .filter((item) item.status waiting) .sort((a, b) a.createdAt - b.createdAt); if (waitingList.length 0) return { error: empty_queue }; const target waitingList[0]; target.status called; target.calledAt Date.now(); target.windowNo windowNo; setTimeout(() { const order orders.find((item) item.queueNo target.queueNo); if (order order.status called) { order.status expired; order.finishAt Date.now(); } }, EXPIRE_AFTER_MINUTES * 60 * 1000); return { queueNo: target.queueNo, waitingCount: waitingList.length - 1 }; }叫号规则严格遵守先来先服务按createdAt升序取第一张waiting状态的号。排序放在filter之后确保窗口和桌台不被叫号顺序打乱。setTimeout是demo版过号逻辑最简洁的写法3分钟没响应就自动转expired不用额外起定时任务。需要清醒认识到一点服务重启后这个倒计时会丢失所以生产环境必须把called_at落库由后台任务扫描超时的记录demo这么做完全够用。消费者确认到店的接口要更严格function confirmOrder(queueNo) { const order orders.find((item) item.queueNo queueNo); if (!order) return { error: not_found }; if (order.status ! called) { return { error: invalid_status, message: 当前状态不可确认 }; } order.status completed; order.finishAt Date.now(); return { queueNo, status: order.status }; }这里最关键的判断是“只有called状态才能confirm”。如果号码已经被系统判成expired消费者再点确认也必须报invalid_status不能静默修改。否则会出现一种数据错乱人没来系统却记录成已完成后面统计叫号响应时长全部失真。宁可让前端弹一个“您的号码已过号请重新取号”也不要把脏数据写进去。3.4 演示版的参数表哪些值现场一定要能改写死在代码里的参数越少现场演示越从容。我一般把下面几个值抽出来参数demo推荐值含义与调法MAX_WAITING200排队上限演示时调小可以快速看到queue_full提示EXPIRE_AFTER_MINUTES3过号宽限改成1分钟可以现场演示“过号”效果POLL_INTERVAL3000小程序端轮询间隔单位毫秒AVG_SERVE_MINUTES5平均服务时长调大后预计等待时间会更长这些参数放进queue_config表或一个config.json里演示时改配置再刷新接口比改代码重启服务优雅得多。尤其是avg_serve_minutes前端的“预计等待X分钟”全靠它算想让演示效果好看就把它调到与实际叫号节奏匹配的值。4. 小程序前端登录态、排队状态页、叫号大屏三页联动后端接口就绪之后小程序端的任务是把取号、查状态、叫号大屏三个页面串起来。这里最容易被新手忽略的不是UI样式而是页面生命周期和定时器的关系。轮询放在哪个生命周期启动、离开页面时有没有清掉定时器直接决定现场演示会不会翻车。4.1 前端目录结构与页面跳转关系一个完整demo的源码里小程序端至少要有三个页面页面路径角色取号页pages/index/index展示当前叫号、选择排队类型、点击取号我的排队页pages/queue/queue展示我的号码、前方人数、预计等待时间叫号大屏页pages/board/board商家端展示当前叫号支持叫下一个页面跳转关系是取号成功用wx.redirectTo跳转到我的排队页而不是navigateTo。navigateTo会把取号页留在页面栈里用户点返回键又回到取号页再取一次号就产生两条排队记录演示时就乱了。redirectTo会替换当前页面返回键直接退到首页逻辑干净很多。4.2 wx.login与openid的两种获取方式小程序端第一次启动时要先通过wx.login拿到临时code再交给后端换openid// miniprogram/app.js App({ globalData: { baseUrl: http://localhost:3000, openid: }, onLaunch() { wx.login({ success: (res) { if (!res.code) return; wx.request({ url: ${this.globalData.baseUrl}/api/auth/login, method: POST, data: { code: res.code }, success: (r) { wx.setStorageSync(openid, r.data.openid); this.globalData.openid r.data.openid; } }); } }); } });这段代码的用意是小程序端不直接调用微信的code2Session接口因为那个接口需要AppSecretAppSecret一旦放在前端代码里就相当于公开了。正确做法是把code发给后端由后端拿着code去微信服务端换openid再返回给小程序。demo里如果不想接微信API后端可以直接返回一个固定格式的openid比如test_openid_加随机串前端逻辑完全不用变。如果你选择云开发方案这里要把wx.request换成wx.cloud.callFunction云函数内部自动拿到openid。两种模式各有适用场景本地Node服务调试方便云开发省事且正式环境更安全。我建议demo维持Node后端的写法因为多数人本地跑通后要观察的还是接口日志和数据表。4.3 我的排队页3秒轮询与定时器存废排队状态页是整个demo里生命周期最讲究的页面。我见过最典型的错误是把setInterval写在onLoad里页面在后台运行或锁屏后定时器被小程序挂起回到前台时页面显示的还是十分钟前的状态。正确做法是让轮询跟着onShow走// miniprogram/pages/queue/queue.js const app getApp(); let pollTimer null; Page({ data: { queueNo: , statusText: , waitingCount: 0, estimateMinutes: 0 }, onLoad() { const queueNo wx.getStorageSync(queueNo); if (!queueNo) { wx.redirectTo({ url: /pages/index/index }); return; } this.setData({ queueNo }); }, onShow() { this.refreshStatus(); pollTimer setInterval(() this.refreshStatus(), 3000); }, onHide() { if (pollTimer) clearInterval(pollTimer); }, onUnload() { if (pollTimer) clearInterval(pollTimer); }, refreshStatus() { wx.request({ url: ${app.globalData.baseUrl}/api/queue/status, data: { queueNo: this.data.queueNo }, success: (res) { const d res.data; if (d.status called) { wx.showModal({ title: 叫号提醒, content: 您的号码 ${d.queueNo} 请前往 ${d.windowNo} 号窗口, showCancel: false }); } this.setData({ statusText: d.status, waitingCount: d.waitingCount, estimateMinutes: Math.max( 1, Math.ceil(d.waitingCount * (d.avgServeMinutes || 5)) ) }); } }); } });关键点有三个。第一onShow里先立即拉一次状态再启动定时器避免页面刚显示时还要干等3秒。第二onHide和onUnload都要清定时器否则页面切走之后定时器还在执行请求浪费是小真机上可能出现“提示弹窗从上一个页面冒出来”的诡异现象。第三状态码为called时弹窗提示但不要在这里做自动跳转让用户自己点击确认这样才不会错过叫号。对应的wxml结构很简单view classcard text classno{{queueNo}}/text text classstatus {{statusText waiting ? 排队中 : (statusText called ? 请前往窗口 : 已结束)}} /text text前方还有 {{waitingCount}} 位/text text预计等待 {{estimateMinutes}} 分钟/text /view这里只把queueNo存在了Storage里status和waitingCount永远从服务端拉。这是一个很重要的缓存原则微信小程序设置缓存时间时不要把易变的状态一起缓存进去否则每次打开看到的都是旧数据会让人误以为系统坏了。4.4 取号页单选框、顶部导航高度与适配取号页需要一个渠道选择正好用微信小程序单选框来实现radio-group classchannel bindchangeonChannelChange labelradio value0 checked /小程序取号/label labelradio value1 /电话预约/label labelradio value2 /到店再取/label /radio-group// pages/index/index.js Page({ data: { channelType: 0 }, onChannelChange(e) { this.setData({ channelType: Number(e.detail.value) }); }, onTakeQueue() { wx.request({ url: ${app.globalData.baseUrl}/api/queue/take, method: POST, data: { openid: wx.getStorageSync(openid), channelType: this.data.channelType }, success: (res) { wx.setStorageSync(queueNo, res.data.queueNo); wx.redirectTo({ url: /pages/queue/queue }); } }); } });channelType在取号时就定死不要等排队之后再改。这样做的好处有两个一是演示时能跟店长解释“电话预约的可以先取号但没有到场优先权”二是后续统计线上、电话、到店三类来源的排队量一拉数据就有。如果源码里用了自定义导航栏还要处理微信小程序顶部导航栏高度。直接用固定数值在iPhone刘海屏上会顶到状态栏在部分安卓机胶囊按钮位置又不一样所以要用系统API动态计算// utils/nav.js function getNavBarHeight() { const sysInfo wx.getSystemInfoSync(); const rect wx.getMenuButtonBoundingClientRect(); return { statusBarHeight: sysInfo.statusBarHeight, navBarHeight: (rect.top - sysInfo.statusBarHeight) * 2 rect.height }; } module.exports { getNavBarHeight };statusBarHeight是状态栏高度navBarHeight是导航栏整体高度计算方式是胶囊顶部到状态栏底部的距离乘2再加胶囊高度。这个值在页面onLoad时取一次就行不要放在data里反复计算。4.5 叫号大屏页商家端的核心操作商家端页面逻辑比消费者端简单就是显示当前叫号然后提供一个“叫下一个”按钮// pages/board/board.js Page({ data: { currentNo: , waitingCount: 0 }, onShow() { this.loadCurrent(); }, loadCurrent() { wx.request({ url: ${app.globalData.baseUrl}/api/queue/current, success: (res) { this.setData({ currentNo: res.data.queueNo || 暂无排队, waitingCount: res.data.waitingCount || 0 }); } }); }, onCallNext() { wx.request({ url: ${app.globalData.baseUrl}/api/queue/call, method: POST, data: { windowNo: 01 }, success: () this.loadCurrent() }); } });叫号之后立即刷新当前号码不需要额外轮询。商家端和消费者端走的是同一套接口只是视角不同商家端调current和call消费者端调status和complete。这样设计最大的好处是演示时店长在电脑上点叫号顾客手机上的弹窗在3秒内就出现效果很直观。5. 避坑指南排队demo最容易翻车的五个环节状态机和接口都写完不等于现场演示就稳了。总结这几年帮人救场的经验微信小程序排队demo的翻车点高度集中在五个地方每个都值得在交付前自测一遍。5.1 现象开发者工具正常真机预览却一直请求失败开发工具里接口调得通一换到真机就报“request:fail”这是最经典的坑。原因在于开发者工具默认勾选了“不校验合法域名”而真机预览没有这个豁免。小程序的wx.request要求域名必须是HTTPS并且要在小程序管理后台配置到request合法域名列表里。解决方法是demo阶段把本地Node服务通过内网穿透或部署到带HTTPS的测试服务器在小程序后台临时加上这个域名如果只是本地联调开发者工具里可以暂时勾选不校验合法域名但这只限于开发工具真机预览要用开发版加调试模式才能临时绕过正式体验版和上线版本都不行。这个坑最气人的地方是它和代码无关纯属环境配置。所以demo交付前我习惯先问一句后端部署在哪、域名配了没有。没配就先把这一环补上否则再好的代码在真机上都是白搭。5.2 现象锁屏或切后台回来排队状态还停在十分钟前页面在后台待了一段时间再切回来显示的等待人数和状态都是旧的要手动下拉刷新才恢复。原因非常明确小程序在页面切到后台后会挂起定时器setInterval不再执行但页面没有重新拉数据所以UI停留在最后一次轮询的结果。解决方法是把轮询的生命周期绑到onShow和onHide上。每次onShow都先调一次refreshStatus再重新开启setIntervalonHide时清掉定时器。这样切回来第一秒就能看到最新状态不需要用户手动操作。这里还要注意一个细节不要只在onUnload清定时器。微信小程序的页面在切后台时可能不触发onUnload但一定会触发onHide两个生命周期都要清理才能保证不会有多个定时器叠加执行。5.3 现象排队号A001变成A1排序也跟着乱取号接口返回的queueNo明明是A001前端显示也是A001但后端数据表里存成了A1排序时1、2、10排成1、10、2。原因是存储字段用了INT类型数据库把前导零自动丢掉了。解决方法是建表时把queue_no定义成VARCHAR并且在插入时不要做任何数字转换。A001和A1在数据库里是两个字符串查询时严格按字符串匹配即可。这个坑在演示时特别容易暴露连续取十张号屏幕上的号码跳变顺序不对店长一眼就能看出来。除了字段类型还要注意前端拿到queueNo后不要用parseInt之类的函数做隐式转换直接作为字符串展示和传参。5.4 现象商家点了叫号消费者端没有任何反应商家端显示已经叫号消费者端却安安静静过一会儿商家又点了下一个消费者端直接跳过上一个号码。原因有两类。第一类是轮询数据没刷新消费者端定时器没有正确清掉或重新建页面一直拿的是缓存里的旧状态第二类是状态机没拦住脏数据叫号确认接口没有校验状态就执行覆盖同一张号被叫了两次。排查时先看队列日志。如果日志里这条号的叫号时间是连续两条说明是状态机问题去查call接口有没有判断status必须为waiting如果日志只有一条叫号记录而消费者端弹窗没出现去查前端定时器和onShow逻辑。90%的情况都出在第二类后端没有做状态前置校验接口被重复调用时数据就被覆盖了。5.5 现象后端重启一次整个队全部清零内存数组做存储的最大副作用就在这里。开发时每次改代码自动重启排队的号全没了只能重新取。真到了现场演示服务被同事无意重启队列数据归零场面非常难看。解决方法是给内存方案加一层快照持久化每次take、call、complete操作后把orders数组写入一个JSON文件服务启动时先读取这个文件到内存。批量写文件要注意别每秒钟写几十次可以简单做一个节流操作后标记数据脏了每5秒检查一次并落盘。这个方法不是高性能方案但足够demo用。如果你希望完全不丢数据还是老老实实上MySQL或云开发数据库把orders数组的读写改成数据库事务。6. 从demo到能落地过号窗口、订阅消息与并发自测三件事demo跑通之后真正要把排队系统交给客户用还差三件小事每一件都能看出团队有没有做过实际的排队业务。第一件是过号窗口的处理。demo里我用了3分钟定时器自动过号但真实场景里“叫了三遍没来”和“人就在门口没听见”是有区别的。更接近实际的做法是消费者在队列页提供“延后处理”按钮点一下可以把当前号码插回队尾但不插队标记为“已顺延”再被叫到时如果还没回应才真正过号。这样店长和顾客都少一些摩擦。第二件是订阅消息。纯靠轮询弹窗的体验在线上不够用用户关掉小程序就收不到提醒。微信小程序提供了订阅消息接口取号成功后引导用户订阅“叫号通知”wx.requestSubscribeMessage({ tmplIds: [your-template-id], success(res) { // 用户同意后后端在叫号时调用 subscribeMessage.send console.log(订阅成功, res); } });注意订阅是一次性的用户每次取号都要重新引导订阅。叫号时后端拿到模板消息ID和用户openid发送“您的号码A021已叫号请前往3号窗口”。这个能力把排队体验从“盯页面”升级为“等铃响”。第三件是并发自测。演示时一个人点取号当然没问题但上线后可能有几十个人同时点号码会不会重复、会不会乱序需要先验证// stress-test.js const concurrent 10; const tasks Array.from({ length: concurrent }, () fetch(http://localhost:3000/api/queue/take, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ openid: test_user_ Math.random().toString(36).slice(2) }) }).then((r) r.json()) ); Promise.all(tasks).then((results) { const numbers results.map((r) r.queueNo); const unique new Set(numbers).size numbers.length; console.log(并发取号唯一性校验:, unique ? pass : fail); });压测结果如果出现重复号码说明取号逻辑里用了共享计数器或异步操作顺序没保证需要回去查。Node单进程里同步过滤数组通常不会重复但只要你把存储换成了数据库就必须给queue_no加唯一索引同时把取号改成事务否则并发一上来就会出问题。这三件事做完demo就算真正迈过了“能演示”到“能被用”的门槛。我自己踩过最深的一个坑就是当初为了演示效果把所有参数写死在代码里结果客户现场要调过号时间我只能现改代码重启服务。后来所有排队项目的配置都统一外置宁可多写几行读取逻辑也不让自己在现场陷入改代码的窘境。希望帮到你排队系统做得好不好往往就体现在这些细节里。本文还有配套的精品资源点击获取
热门专题

继续阅读更多专题内容

围绕企业服务、数字化转型与官网运营的常青话题,持续输出深度内容

企业官网建设指南 企业托管服务模式 财税政策与解读 企业数字化转型 官网SEO与获客 网站安全与运维
配套服务

读完这篇文章,了解更多服务

从整站搭建到SEO布局,17项核心服务助您打造高转化的企业官网

01

企业托管整站搭建

从信息架构到栏目预留,搭建可生长的企业站点骨架,每个页面独立原创设计。...

了解详情
02

规整可信网页设计

雪地靴温暖风原创设计,金属铜线条贯穿全页,拒绝通用模板与AI流水线。...

了解详情
03

企业服务SEO布局

关键词体系与语义化结构,从建站源头为搜索排名而生。...

了解详情
04

业务预约咨询表单

多场景表单与线索收集体系,把访问流量转化为可追踪的销售线索。...

了解详情
05

企业服务站点运维

安全巡检、数据备份与内容更新支持,全年守护网站稳定运行。...

了解详情
06

全终端商务适配

电脑、平板、手机一致呈现,移动端体验与转化同样出色。...

了解详情
需要专业建议?

让专业顾问为您解读行业趋势

关于企业官网建设、SEO获客与数字化转型的任何疑问,欢迎一对一咨询我们的专业顾问。