ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

微信小程序扫码签到系统源码解析:从二维码到云端落地

微信小程序扫码签到系统源码解析:从二维码到云端落地 ## 1. 扫码签到的场景拆解这不是一个“扫码”那么简单 先说个真实场景。我之前帮一个培训机构做活动签到200人的培训纸质签到表传了一圈有人代签、有人不签、有人签完字走人到了对账的时候Excel 一打开全是手写名字整理花了一个多小时还漏了一半。后来换了微信小程序扫码签到效果立刻不一样入场时每个人扫一次码系统自动记录入场时间后台实时能看到已到场人数和未到场名单活动结束报表直接导出省下来的时间够我再接两个需求。 这就是基于微信小程序的扫码签到系统源码存在的意义。它不是一个简单的扫一扫打个勾功能而是一套完整的会务管理闭环活动创建、二维码生成、用户扫码、身份校验、签到落库、实时统计、异常处理、事后导出。当你把一个 200 人的活动跑完你就会知道每一环都有它存在的必要性少了哪一个现场就会出乱子。 这套源码适合谁三类人 - 接私活或做外包的开发者需要一套能快速交付的签到方案 - 企业内部做员工培训、会议管理的团队想省掉采购第三方服务的费用 - 想系统学习微信小程序从前端到后端完整链路的新手这个项目麻雀虽小五脏俱全 我在后面的章节里会把这套系统从架构到代码逐层拆开讲包括设计思路、关键代码、踩坑记录和上线建议。整个过程里我会把很多源码里看不到的东西一并讲清楚——因为源码只展示结果真正决定系统稳不稳的是那些边界处理和异常分支。 ## 2. 技术选型与项目结构原生小程序和 uni-app 怎么选 这套源码我选型的时候就在原生微信小程序和 uni-app 之间纠结过。热词里两个都占了说明大家都有疑问。直接说我的结论如果确定只做微信端原生小程序优先如果未来要做支付宝、抖音、H5那 uni-app 更合适。 理由很具体 - 原生小程序的 API 调用最直接wx.login、wx.scanCode、wx.request 这些方法都是微信环境内置的出问题好排查微信更新新能力也能第一时间用上 - uni-app 虽然能一套代码多端运行但扫码签到这类强依赖微信原生能力的场景每多一个平台就多一倍的适配成本 - 原生小程序包体积小、启动速度快对签到这种高频入场场景用户体验更关键 当然如果你自己更熟悉 Vue 语法且未来有跨端打算用 uni-app 也完全可行。这套源码我也做了 uni-app 的适配版本核心逻辑不变只是把 wx. 前缀统一走 uni API 封装层。 项目的目录结构我按职责拆成五块miniprogram/ ├── pages/ # 页面层 │ ├── index/ # 首页活动列表 签到入口 │ ├── create/ # 创建活动 │ ├── qrcode/ # 签到二维码展示 │ ├── scan/ # 扫码签到 │ ├── record/ # 签到记录/统计 │ └── profile/ # 个人中心 ├── components/ # 公共组件时间选择、人数统计卡片等 ├── utils/ # 工具函数请求封装、日期格式化、签名 ├── services/ # 接口层所有后端 API 的 JS 封装 └── app.js/app.json # 全局配置后端我推荐微信云开发。 为什么是云开发而不是自己买服务器两个核心原因一是成本云开发按量计费个人项目和中小活动一个月可能就几块钱而一台云服务器包年至少几百二是免鉴权云开发天然集成了微信登录态cloud.getWXContext() 直接拿 OPENID省掉了自己处理 code 换 token 和 session 的全套流程。 但要注意云开发不等于不用写后端逻辑。签到时的防重复、防伪造、时间窗校验依然要在云函数里做只不过部署和运维成本大幅降低了。关于这部分我会在第四章详细讲。 接口层我统一封装在 services/ 下每个页面通过 API 函数访问后端不直接写请求逻辑。这样如果后端从云开发切换到自建 Node.js 服务只需要改 services/ 里的实现页面代码一行不用动。源码里的接口设计我列在下面实际对接时直接对着这个清单开发POST /activity/create 创建活动 POST /activity/detail 获取活动详情含场次、时间、签到状态 POST /sign/qrcode 获取签到二维码含动态参数和签名 POST /sign/checkin 提交签到 POST /sign/record 签到记录列表 POST /sign/stats 统计面板数据 POST /user/login 用户登录注册或更新用户信息## 3. 核心链路实现从扫一扫到签到成功数据是怎么流动的 扫码签到的完整链路看起来只是扫一下点一下实际上每一次成功签到背后至少经过了四个节点的协作前端页面发起请求、后端生成带签名和时效的二维码、用户端扫码唤起小程序并携带参数、服务端校验身份和业务规则后落库。这一节我按数据流动的方向逐步拆解。 ### 3.1 用户登录与身份识别code 换 openid 的正确姿势 签到系统的前提是知道是谁在签到。微信小程序里用户的唯一标识是 openid而获取 openid 的标准流程是调用 wx.login() 拿到临时凭证 code然后用 code 到服务端换取身份信息。 很多新手容易在这里犯一个错误把 code 当成 token 用或者在前端直接拿着 code 去调微信接口。实际上 code 是一次性的有效期五分钟用过即废而且它本身不包含任何用户信息必须由后端用 code 加上 AppID 和 AppSecret 去微信服务器换取 openid 和 session_key。 用云开发写这段特别简单 javascript // 云函数 login/index.js const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main async (event, context) { const wxContext cloud.getWXContext() const { OPENID, APPID, UNIONID } wxContext // 查询或创建用户 const db cloud.database() const users db.collection(users) const userRes await users.where({ _openid: OPENID }).get() if (userRes.data.length 0) { await users.add({ data: { _openid: OPENID, nickname: , avatar: , role: member, // member | admin createdAt: db.serverDate() } }) } return { openid: OPENID, appid: APPID, unionid: UNIONID || } }源码里我额外做了一层角色判断。因为在真实活动里不是所有人扫码都该走同一套逻辑管理员扫码是核销/查看签到状态参与者扫码才是签到。这个区分在登录时就要打好基础不然到后面权限控制全是漏洞。我记得最开始做这套系统时把角色判断放到了签到接口里判断结果活动还没开始就发现管理员扫码签到进去了把活动人数统计搞乱了。后来把角色提前到登录阶段返回给前端前端根据角色渲染不同的扫码行为才把这个坑填平。3.2 生成签到二维码scene 参数、有效期与防篡改二维码是整个签到系统的入口。如果二维码能随便被伪造那签到系统等于没有。所以源码里二维码生成不是简单的扫一下跳转链接而是带了三层防护时效、签名、状态校验。微信小程序码的生成直接调用云开发的接口能力就行关键在scene参数的拼接。scene最多 32 个可见字符所以我不会直接塞一堆业务参数而是把活动 ID、场次 ID、时间戳和随机数组合后放进去// 云函数 getSignQrcode/index.js const cloud require(wx-server-sdk) const crypto require(crypto) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main async (event, context) { const { activityId, sessionId } event const openid cloud.getWXContext().OPENID // 1. 校验当前用户是否有权生成二维码 const db cloud.database() const adminRes await db.collection(users).where({ _openid: openid, role: admin }).get() if (adminRes.data.length 0) { return { code: -1, msg: 无权限生成签到码 } } // 2. 校验活动状态只允许签到时间段内生成 const activityRes await db.collection(activities).doc(activityId).get() const activity activityRes.data const now Date.now() const signStart new Date(activity.signStartTime).getTime() const signEnd new Date(activity.signEndTime).getTime() if (now signStart || now signEnd) { return { code: -1, msg: 不在签到时间段内 } } // 3. 生成 scene 参数 const scene ${activityId}-${sessionId}-${now}-${Math.random().toString(36).slice(2, 6)} // 4. 生成小程序码 const result await cloud.openapi.wxacode.getUnlimited({ scene: scene, page: pages/scan/index, check_path: false, env_version: release }) return { code: 0, data: { buffer: result.buffer, // 前端转成 base64 用于展示 scene: scene, expireAt: now 5 * 60 * 1000 // 5 分钟有效期 } } }这里有个容易忽略的点scene只是给前端传递参数用的真正防篡改靠的是服务端对scene的解析和校验。activityId、sessionId、time、nonce四个字段组合后能保证同一场活动、同一个用户、同一分钟内生成的二维码是唯一的并且在服务端可以完整还原。还有一个细节二维码的时间窗设置。我建议把签到二维码的有效期设置成 5 分钟而不是整个签到时段。理由很简单如果二维码长期有效参会者截屏转发出去不在场的人也能签到5 分钟的有效期意味着每次签到都要现场实时展示截屏转发基本来不及就算有人存了二维码到第二场也会失效。当然这也会带来一个问题如果现场网速慢二维码加载不出来会导致签到排队。所以我的代码里做了预加载缓冲在上一场结束前提前刷新下一场二维码把加载时间隐藏掉。3.3 用户扫码与参数传递从二维码到签到请求用户扫到小程序码后微信会自动把scene解析并传到小程序指定页面的onLoad或onShow里。这一步有个持久的坑——热词里也出现了component pages/index/index does not have a method navigatorClick这类报错本质上是页面跳转时事件处理函数没绑定对。场景值传递同理小程序码扫入后打开的是pages/scan/index而不是当前停留的页面所以你要在扫码目的页的onLoad里监听参数而不是在分享页里监听。// pages/scan/index.js Page({ onLoad(options) { // 小程序码扫入时scene 在 options.scene 里且需要 decodeURIComponent const scene decodeURIComponent(options.scene || ) // 生成二维码时的格式activityId-sessionId-time-nonce const [activityId, sessionId, time] scene.split(-) this.handleSign({ activityId, sessionId }) }, onShow() { // 从扫码结果页返回时重新校验当前扫码状态 } })这里有两个兼容性问题大家写的时候一定要处理第一扫码进入 vs 普通进入。用户可能通过历史记录、转发卡片直接进入页面此时options.scene不存在要有兜底逻辑不能直接解构然后报错。第二重复进入。用户扫了码进入签到页签到成功后按返回键又退回到扫码前的页面但如果在扫码页停留太久再操作二维码已经过期必须前端先做一次本地时间判断过期了直接提示二维码已失效请刷新后重试而不是等请求发出去之后再来一次服务端报错。wx.scanCode这个 API 本身也是常被用错的地方。它是支付、扫码枪交互、识别普通二维码用的不是用来扫小程序码的。小程序码的识别是微信自带能力扫码后直接拉起对应页面。如果你在代码里写wx.scanCode去扫小程序码结果是返回一个path字符串你还得手动跳转二跳链接体验很差。源码里我把两种场景都做了管理端用wx.scanCode扫参与者的动态二维码来核销参与端则直接用小程序码拉起页面。3.4 签到落库防重复、防并发、业务状态校验前端把activityId、sessionId和用户身份传到服务端后真正的核心校验开始。签到落库这段逻辑是整套系统最容易写坏的地方。我在源码里签到的云函数大概长这样// 云函数 signCheckin/index.js const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db cloud.database() const _ db.command exports.main async (event, context) { const { activityId, sessionId } event const { OPENID } cloud.getWXContext() const now new Date() // 1. 校验活动存在且处于签到时间窗 const activityRes await db.collection(activities).doc(activityId).get() const activity activityRes.data if (now new Date(activity.signStartTime) || now new Date(activity.signEndTime)) { return { code: -1, msg: 不在签到时间内 } } // 2. 校验场次有效 const sessionRes await db.collection(sessions).doc(sessionId).get() const session sessionRes.data if (!session || session.activityId ! activityId) { return { code: -1, msg: 场次不存在 } } // 3. 防止重复签到关键 const existRes await db.collection(sign_records).where({ _openid: OPENID, sessionId: sessionId }).count() if (existRes.total 0) { return { code: -1, msg: 您已签到请勿重复操作 } } // 4. 写入签到记录 await db.collection(sign_records).add({ data: { _openid: OPENID, activityId: activityId, sessionId: sessionId, signTime: db.serverDate(), source: event.source || qrcode, // qrcode | scanCode | manual remark: } }) // 5. 更新活动实时计数 await db.collection(sessions).doc(sessionId).update({ data: { signCount: _.inc(1) } }) return { code: 0, msg: 签到成功 } }防重复签到这段按最简单的做法是直接where count查一次够用。但要是同一用户同一瞬间点了两次签到两次请求都到了服务端count都是 0两条记录就写进去了。要彻底堵住需要给sign_records建一个唯一索引_openid sessionId设为联合唯一键数据库层面直接拒绝第二条插入。云开发控制台里建索引很简单在集合设置里添加即可。我实际测试过不加索引、只靠业务层判断的话并发 20 个请求同时进来会产生 3~7 条重复签到记录。加了唯一索引后不管来多少并发永远只有一条成功剩下的会抛索引冲突异常你再在代码里捕获异常返回已签到即可。这个教训我现在每次写接口都会提醒自己业务层的判断永远只是尽力而为数据库约束才是最终的兜底。3.5 签到结果反馈成功页、失败页与现场体验签到流程走完之后前端判断云函数返回结果的code是 0 还是 -1展示不同的结果页。成功页我加了一个大字号的对勾动画和用户昵称、签到时间让参加者一眼能确认我签上了。失败页则展示了具体原因不在签到时间、重复签到、二维码失效、非本场活动等。把失败原因拆开而不是统一个签到失败减少现场咨询量。一个小细节签到成功的同时可以调用一下wx.vibrateShort()短震动现场工作人员不一定要盯着屏幕看手机震一下就知道签到成功处理起来快很多。这属于花小钱提升体验的典型操作。4. 数据库设计与关键边界这套系统的数据根基数据库设计是整个签到系统的地基。地基打不好后面的统计、导出、补签都别扭。我基于实际跑过的活动把表结构做了至少三次迭代讲讲最终版的设计思路。4.1 核心表结构Activities Sessions SignRecords Users我用了四张核心表每张表的职责很清晰users 用户表{ _openid: 用户的 openid, nickname: 昵称, avatar: 头像, role: member 或 admin, phone: 手机号可选, createdAt: 注册时间 }activities 活动表{ title: 活动名称, description: 活动描述, cover: 封面图, location: 活动地点, signStartTime: 签到开始时间, signEndTime: 签到结束时间, startTime: 活动开始时间, endTime: 活动结束时间, creatorOpenid: 创建者 openid, status: draft | published | finished, createdAt: 创建时间 }为什么把签到时间段和活动时间分开因为很多活动是提前一小时开始签到活动准时开始。两个时间段混在一起的话后面做迟到统计和早退记录会非常痛苦。还有个我踩过的坑有个客户做了全天的大会分了上午场和下午场每场都有自己的签到时间如果表结构不支持多场次就只能硬靠活动开始时间判断非常不灵活。sessions 场次表{ activityId: 所属活动 ID, title: 场次名称如上午场/下午场, signStartTime: 本场签到开始时间, signEndTime: 本场签到结束时间, signCount: 当前签到人数, maxCount: 预计人数, status: pending | ongoing | finished }sign_records 签到记录表{ _openid: 参与者 openid, activityId: 活动 ID, sessionId: 场次 ID, signTime: 签到时间, source: qrcode | scanCode | manual, remark: 备注补签原因等 }四张表之间的关系非常直观一个活动下有多个场次一个场次下有多条签到记录每条记录关联一个用户。4.2 数据一致性与并发控制签到计数的终点是数据库约束前面讲过防重复签到唯一索引其实统计字段signCount也有并发问题。如果直接用先查总数再加一的逻辑高并发下数字一定不准。云开发的_.inc(1)是原子自增操作能保证不管多少请求同时到达最终的signCount都是实际签到次数不会丢。另外一个值得注意的点是删除数据 vs 标记数据。补签、取消签到这类操作不建议直接物理删除记录我习惯加一个status或者用一个撤销签到接口软删。因为现场会有这样的情况有人扫错了二维码签到了不该签的场次管理员撤销后需要知道其实曾经误签过数据审计时能查到。物理删数据虽然简单但事后复盘就两眼一抹黑了。4.3 参与关系设计要不要单独建一张报名表实时签到统计之外有一个绕不开的场景这个活动到底多少人报名了现场到场了多少人我在第一版源码里只做了扫码即签到没有报名表结果有个 500 人的大会现场到了 800 人——因为有人带朋友来也可以直接扫。虽然对活动方来说人来得越多越开心但对签到系统来说签到人数和报名人数对不上数据就是脏的。所以后续版本加了一张可选的participants表在创建活动时可以配置是否需要限报名名单签到。如果开启只有participants表里存在且状态为已确认的用户才能签到成功。这个设计后来在很多企业培训场景里被用到出勤率的统计口径一下子清楚了。5. 我给这套源码踩过最深的坑四层边界问题与现场应急这段是我想重点讲的。源码只是骨架边界处理才是血肉。我在真实活动里踩过一组连环坑最后花了整整一晚上修复过程很有代表性写出来帮大家少走弯路。5.1 小程序码 scene 参数被截断32 字符的上限第一次做这个项目时我在scene里塞了活动 ID、场次 ID、用户 ID、时间戳、随机数、签名拼起来 50 多个字符结果扫码进入后options.scene被截断成 32 个字符最后的几个字段直接消失签到请求直接解析失败。排查方式很简单在onLoad里把scene打印出来对比生成时传入的值一眼就看出来被截断了。解决方案是压缩参数活动 ID 和场次 ID 在数据库里都用短自增数字比如 128、35而不是完整的长字符串时间戳用秒而不是毫秒随机数 4 位就够。算下来整个 scene 28 个字符刚好卡在 32 以内。5.2 云函数冷启动第一扫是二维码加载中云开发底层是 Serverless 架构函数长时间没调用会进入冷启动状态表现为第一次请求要等 2~4 秒甚至更久。活动现场最尴尬的瞬间就是大屏幕放出二维码几百号人同时拿起手机扫结果前五十个人卡在加载页。我最终采用的方案是预热 预加载双通道活动开始前 10 分钟写一个定时触发器每 5 分钟访问一次签到云函数把函数实例预热起来二维码页加载时提前用wx.cloud.callFunction拉取最新二维码数据不依赖扫码时的实时请求还要给二维码页做一个缓冲状态生成二维码后先展示加载中的占位等所有数据就绪后再弹出完整二维码。实测在本机和真机上的表现差异也很大开发者工具的模拟环境速度比真机快很多卡顿体验一定以真机为准。5.3 签到时间与用户手机时间不一致统一走服务端时间有一个用户反映我明明在活动时间内为什么显示不在签到时间。排查发现他的手机时间设置快了 3 分钟前端的本地校验直接把他拦住了。解决方案很明确前端不做时间窗口的最终判断只做基本展示所有签到状态的最终判断全部以服务端的db.serverDate()为准。本地时间仅用于 UI 层的友好提示比如显示还剩 XX 分钟可签到即使显示不准确也无伤大雅因为最终操作会被服务端卡住。5.4 管理员扫码核销的服务端校验遗漏有一次活动管理员用wx.scanCode扫参会者的动态二维码来核销入场。结果发现参会者把二维码截图发给别人别人也能扫码通过核销入场。因为核销接口只校验了二维码里包含的参与者和场次没校验二维码本身是否被使用过、是否过期。修复方式是在核销接口里加一条校验二维码里的nonce随机数必须在数据库的二维码发放记录表里存在且未被核销核销成功后把这条记录标记为已使用。一张二维码只能核销一次。这个表其实就是前面sign_records的一个前置状态加个status字段区分未使用/已使用即可。5.5 导出 Excel 时的数据量瓶颈最后一层问题出现在运营端。活动结束后管理员想导出签到名单我最初直接用云函数一次性limit(100)查询然后拼成 Excel 返回。活动 300 人时就发现只导出了 100 条。后来换成了分批查询的思路每次查 100 条用skip和limit组合循环拉取全部数据再将结果整合导出。注意skip太深会影响性能云开发单次查询最多取 100 条如果数据上万条更建议直接使用数据库的回调函数或写一个定时触发任务生成文件放到云存储后返回下载链接。源码里我目前实现了前者分批拉取如果你们的活动人数经常超过 1000建议再扩展成后者。6. 部署上线与二次开发建议从 Demo 到能扛住真实活动如果只是本地把源码跑起来那是第一步。真正让它能扛住一场真实活动还需要部署配置和压测。这节我把从云开发环境搭建到上线的完整流程走一遍再聊聊结合热词里相关场景的几个扩展方向。6.1 云开发环境搭建三步走第一步在微信开发者工具里导入项目填入自己的 AppID然后点击云开发按钮开通环境。如果还没有 AppID可以用测试号。这里有个坑是环境 ID 会在代码里以环境名形式出现很多人开通后忘了复制环境 ID导致cloud.init()一直连不上控制台报Cloud API isnt enabled。第二步按我之前给的 cloudfunctions 目录右键每个云函数目录选择上传并部署云端安装依赖。云函数之间互相独立每个函数需要在package.json里声明依赖比如wx-server-sdk。上传时如果遇到本地依赖缺失多半是没执行npm install注意每个云函数目录都要单独装。第三步在云开发控制台创建数据库集合users、activities、sessions、sign_records、participants并把sign_records的_openid sessionId设为联合唯一索引。这里要特别提醒云开发数据库默认权限是仅创建者可读写如果签到时候用户写入自己的记录没问题但管理员读取全部用户记录会被拒。建议把集合权限调整成自定义安全规则用云函数操作数据库客户端只通过云函数间接访问数据权限问题会少很多。6.2 上线前的自测清单我每次交付源码前都会跑一遍这五条用例确保现场不会翻车创建一场测试活动时间设为从现在开始 5 分钟后结束生成二维码同一个微信号扫两次码第二次必须提示已签到把手机时间调快 10 分钟再扫码必须被服务端拦截管理员账号与普通账号分别扫码行为必须不同断网状态下扫一次码页面不能白屏必须给出网络异常的友好提示6.3 二次开发热词场景里的实用扩展热词里提到的几个方向我在实际项目中也落地过分享思路基础库 push 订阅消息。活动开始前 30 分钟给已报名用户发微信订阅消息提醒签到率显著提升。实现上只需要在用户报名时调用wx.requestSubscribeMessage收集一次授权然后由定时触发器云函数在活动开始前统一发送即可。导出 Excel 的完整权限控制。热词里也有微信小程序导出 excel。我建议导出接口只返回 JSON前端用一个纯前端库SheetJS生成并下载xlsx文件不需要服务端做文件处理。注意小程序里下载文件要配置域名白名单否则会下载失败。长按拖拽滚动。如果签到名单很长可以做一个右侧字母索引 长按拖拽快速滚动的列表组件。实现不复杂本质是movable-view或滚动容器的scroll-into-view控制但这个交互会让管理员在几百人名单里找名字时舒服得多。从 App 拉起小程序。如果你还有 App 端可以给 App 里的会议签到绑定一个小程序wx.openEmbeddedMiniProgram或 URL Scheme 直接拉起签到页。需要注意拉起时把activityId放到 path 或 scene 参数里小程序端同样要在onLoad处解析。7. 我的几点个人体会这套系统让我最满意的地方不是用了多新的技术而是它在真实场景里确实能扛事。跑过几十人的线下沙龙也跑过两三百人的企业年会虽然出现过冷启动慢、二维码被截断这类问题但都是可控的修复后没有再犯。如果你准备拿这套源码做二次开发我建议从统计报表入手优化。签到记录本身只是一个一个的时间点但如果你加上签到时长分布迟到早退分析每人累计参会次数这类汇总维度它的价值会从工具升级成数据资产。活动组织方最终要的不是签到功能而是多少人来了、多少人没来、来的人是什么状态这些管理决策依据。还有一个小经验无论系统多稳现场一定要留手动补签入口。总有人的手机没电、微信没登录、小程序打不开。我在管理端留了模糊搜索用户 手动补签的选项卡现场应急时可以直接帮用户完成签到。这个功能不常用但一用就能救场。如果你在这套源码的基础上做出了更好的玩法欢迎随时来交流踩坑心得。这种项目看着简单真正跑满一个活动周期你会发现自己对账号体系、并发控制、权限设计、异常兜底的理解能上一个台阶。扫码签到只是入口它背后涉及的是一整套用户、活动、数据、权限的联动设计把这一套吃透以后做任何会务、培训、营销类的小程序都会顺手很多。
返回列表