
1. 项目概述与整体设计思路1.1 扫码签到到底解决了什么痛点前阵子有个朋友跟我吐槽说他们公司搞年会两百多号人排队手写签到登记处挤成一团还有不少人签完就溜主办方根本不知道谁来了谁没来。会后统计到场人数全靠人工数签到表折腾了一整天才把数据理清楚。这个场景我太熟悉了会议活动、培训课程、展会论坛、公司晨会……凡是需要确认人到没到的场合传统签到方式都让人头疼。微信小程序的扫码签到系统说白了就是让参会者到了现场之后用微信扫一扫活动方提供的二维码系统自动记录谁在什么时间扫码签到同时把签到状态实时同步给管理端。整个过程三五秒搞定不需要额外装App不需要排队登记更不需要人力核验。管理者在后台随时能看到实时签到人数、未签到名单、签到时间分布活动结束还能一键导出Excel表格直接省掉大量人工统计的功夫。从技术角度看这个项目麻雀虽小但五脏俱全。它涉及小程序端扫码、二维码生成与解析、后端接口设计、数据库建模、权限校验、数据可视化等多个环节非常适合作为小程序开发的学习实战项目也适合直接拿去改一改用于实际的会议或活动场景。我这里写的内容是基于我自己实际开发过的一个完整版本整理的代码思路和踩坑经历都是真实可用的你可以直接照着做也可以拿源码来参考改造。1.2 为什么选择微信小程序这个载体选微信小程序而不是H5网页也不是原生App这个决定背后有几个非常实际的原因。第一微信的社交生态天然适合扫码这个动作。中国几乎人人都有微信扫一扫是全民都会的基本操作不需要教育用户不需要下载安装扫码即用。相比App需要安装注册小程序的进入成本几乎为零。第二微信小程序提供了成熟的扫码API——wx.scanCode封装了相机扫码、相册识别二维码的能力开发者只需要调用这个接口就能拿到二维码携带的参数信息。要知道如果换成H5网页浏览器对调用摄像头的权限限制非常严格尤其是iOS系统上的体验很糟糕用户需要手动授权、页面跳转各种兼容问题层出不穷。而小程序在扫码这块的体验是原生级别的。第三管理端维护成本低。如果使用微信云开发连服务器都不用自己买云函数、云数据库、云存储全都由微信平台托管个人开发者零成本起步尤其适合中小型活动的签到需求。即使改用自建后端小程序前端的代码也可以无缝复用。1.3 整体架构与技术选型思考这个系统的核心链路很简单管理端创建活动生成签到码 → 参会者扫码 → 小程序解析码内参数 → 请求后端校验 → 写入签到记录 → 前端展示签到成功状态。在技术选型上我采用的是微信小程序原生框架加微信云开发的组合方案。为什么不用uni-app或者Taro这类跨端框架因为签到场景基本只跑在微信小程序上没有多端复用的需求引入跨端框架反而增加了编译链路的复杂度。原生框架虽然写起来啰嗦一点但它的API调用最直接调试时定位问题最快。后端我选择了云开发理由有三条一是省掉服务器购买和域名备案的流程个人开发者想快速上线这步能省下大量时间二是云函数天然支持Node.js写签到逻辑非常顺手三是云数据库的权限控制和小程序端的登录态打通是实打实的方便不需要自己维护session。当然如果你已经有自己的服务器和后端服务也可以用传统方式对接原理是一样的无非是把云函数换成HTTP接口而已。2. 核心流程设计与后端接口实现2.1 扫码—解码—签到的完整链路拆解别小看扫码签到这四个字完整链路里的每一步都有讲究。我把它拆成三段来看码的生成、码的解析、签到的写入。先说码的生成。管理端创建活动时系统要为这个活动生成一张专属的签到二维码。生成方式有几种最简单的是调用第三方二维码生成接口把携带活动ID和其他参数的内容编码成二维码图片自建后端则可以用node.js的qrcode库在服务端生成图片然后存到云存储或者直接返回给前端展示。我建议用后者可控性更强而且不依赖外部服务的稳定性。再说码的解析。参会者扫码后小程序通过wx.scanCode拿到的是一串字符串这串字符串需要按约定好的格式解析出活动ID、签到类型等参数。这里有个特别容易踩的坑——不要直接把活动ID裸放在二维码里否则别人只要拿到了这个码就可以伪造签到请求。我通常在参数里附加一个签名串类似activityIdxxxtokenxxxtimestampxxx后端点会校验这个签名和时间戳的有效性。最后是签到的写入。后端收到签到请求后要做几件事校验签名是否有效、校验活动是否存在且处于签到时间段内、校验当前用户是否已签过到。全部通过后才真正写入一条签到记录并更新统计缓存。注意这里面最容易被忽略的是防重复签到后面我单独讲。2.2 数据库表结构设计三张表支撑整个系统我实际项目里的数据库设计非常简洁核心就是三张表活动表、签到记录表、用户表。很多人一上来就设计一堆表把系统搞得很复杂其实签到场景的需求非常清晰表多了反而增加维护成本。活动表结构大致如下{ _id: 活动ID, name: 活动名称, location: 活动地点, startTime: 活动开始时间, endTime: 活动结束时间, signStartTime: 签到开始时间, signEndTime: 签到截止时间, qrContent: 该活动专属二维码携带的内容已含签名, createTime: 创建时间 }签到记录表是核心表每条记录代表一次签到行为{ _id: 签到记录ID, activityId: 关联的活动ID, openid: 签到用户的唯一标识, signTime: 签到时间, location: 签到地点如果有定位权限, type: 签到类型如normal或guest }用户表就简单了可以只存openid和昵称头像。如果你用云开发用户身份可以直接通过cloud.getWXContext()从云函数里拿到openid不需要用户手动登录这个机制给开发省了非常多事。关于索引我给签到记录表的activityId openid加了联合唯一索引这个索引是防重复签到的关键手段后面细说。2.3 后端核心接口逻辑与防重复签到处理先看云函数签到的核心代码这段逻辑是整个系统的命脉const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db cloud.database() exports.main async (event) { const { activityId, signToken, timestamp } event const { OPENID } cloud.getWXContext() // 1. 校验时间戳防止重放攻击允许5分钟误差 if (Math.abs(Date.now() - timestamp) 5 * 60 * 1000) { return { code: 40001, msg: 签到码已过期请重新扫码 } } // 2. 校验签名 const activity await db.collection(activities).doc(activityId).get() const expectedToken generateSign(activityId, timestamp) if (signToken ! expectedToken) { return { code: 40002, msg: 签到码校验失败 } } // 3. 校验签到时间窗口 const now Date.now() if (now new Date(activity.signStartTime).getTime() || now new Date(activity.signEndTime).getTime()) { return { code: 40003, msg: 不在签到时间段内 } } // 4. 防重复签到数据库唯一索引兜底 业务层前置判断 try { await db.collection(sign_records).add({ data: { activityId, openid: OPENID, signTime: db.serverDate(), location: event.location || } }) } catch (e) { if (e.errCode -502001 e.errMsg.includes(duplicate)) { return { code: 40004, msg: 您已签到请勿重复操作 } } return { code: 50000, msg: 签到失败请稍后重试 } } return { code: 0, msg: 签到成功 } }generateSign这个函数是本地约定的签名生成方法把activityId timestamp 密钥拼接后做MD5或SHA256哈希。密钥存在云函数环境变量里前端不知道密钥就没办法伪造签到请求。防重复签到我做了两层保险。第一层是业务判断在写入前查一次用户是否已有该活动的签到记录第二层是数据库唯一索引。为什么要两层因为在高并发情况下第一次查询和写入操作之间有时间差两个请求可能同时通过业务判断然后同时在数据库层写入如果只有业务判断就必然产生重复数据。唯一索引能从根本上兜底虽然会抛异常但这种异常是预期中的冲突异常本身充当了最后的防线。3. 小程序端实操从扫码到签到成功页面3.1 小程序端扫码能力的接入与参数处理小程序端的核心代码其实非常简洁主要是wx.scanCode的调用。我在实际项目中把扫码逻辑放在首页用户一进小程序就能看到扫码签到的按钮。scanToSign() { wx.scanCode({ onlyFromCamera: true, // 只允许相机扫码防止相册残留二维码造成误签 success: (res) { const result res.result // 扫码结果格式约定sign?activityIdxxxtokenxxxtimestampxxx if (!result.startsWith(sign?)) { wx.showToast({ title: 无效的签到码, icon: none }) return } const params parseUrlParams(result.split(?)[1]) this.handleSign(params) }, fail: (err) { if (err.errMsg.includes(cancel)) { // 用户主动取消不做提示体验更好 return } wx.showToast({ title: 扫码失败请重试, icon: none }) } }) }这段代码里有几个细节值得注意。onlyFromCamera: true是我实操中特意加的因为遇到过用户截图保存签到码、隔天翻相册扫出来的情况结果系统判断过期体验很差。限制只能相机扫码从源头上杜绝了这种误操作。parseUrlParams是解析URL参数的工具函数把扫码得到的字符串里的activityId、token、timestamp提取出来。这些参数都很关键activityId告诉后端是哪场活动token是防伪标识timestamp用于时间窗口校验。这里再补充一个自己踩过的坑早期的版本我让二维码内容直接就是活动ID结果有个聪明人拿别人的二维码拍了张照片发给没到场的朋友让对方远程扫码签到。虽然签到本身看起来没毛病但这明显违背了签到场景的初衷。后来我加上token和timestamp校验后即使别人拿到了码的照片超过5分钟就无法使用了这样既保证了签到码的时效性又降低了冒签的概率。3.2 签到后的页面状态与反馈处理签到成功后页面如何反馈也是一门讲究。我一开始只弹了个toast提示签到成功结果用户手一抖点掉了自己都没看明白到底成功没有还得反复扫码确认。后来我重新设计了签到结果页代码如下handleSign(params) { wx.showLoading({ title: 签到中... }) wx.cloud.callFunction({ name: quickSign, data: { ...params, location: this.location } }).then(res { wx.hideLoading() const { code, msg } res.result if (code 0) { this.setData({ showResult: true, resultType: success, resultMsg: 签到成功欢迎参加本次活动。 }) } else { this.setData({ showResult: true, resultType: fail, resultMsg: msg }) } }).catch(() { wx.hideLoading() this.setData({ showResult: true, resultType: fail, resultMsg: 网络异常请检查网络后重试 }) }) }结果页上有活动名称、签到时间、签到状态还有两个按钮重新签到和返回首页。签到失败时还能给出明确的失败原因比如您已签到请勿重复操作用户一看就明白不会再一脸懵地扫第二次。这里面的用户体验细节是我被真实用户教育后才悟出来的。签到成功的反馈一定要显眼且不可错过让人不用思考就知道自己搞定了。而签到失败的反馈一定要具体且可操作不能只说签到失败要告诉用户为什么失败、接下来该怎么办。这些细节看似琐碎但直接影响一个系统在真实场景中能否被顺畅使用。3.3 管理端功能创建活动、生成签到码、查看统计管理端和小程序端是同一个项目的两个角色。我用了一个简单的角色判断根据当前微信用户的openid是否在管理员名单里来决定显示哪些菜单。管理员名单可以放在云数据库的配置表里也可以在云函数里写死。我建议放在数据库里这样以后加管理员不需要改代码重新发版本。管理端的主要功能有三个。第一个是创建活动填写活动名称、开始时间、结束时间、签到开始时间、签到截止时间、活动地点等信息。创建后系统生成该活动专属的签到码前端用canvas展示二维码图片。第二个是查看签到统计按活动维度展示总数、已签到数、未签到数、签到率还能按小时看签到趋势。第三个是导出Excel把签到记录导出成表格方便活动后存档。统计功能我直接用db.collection(sign_records).where({ activityId }).count()来统计人数因为签到系统数据量不大这种简单的统计方式完全够用。如果你预计一场活动有几千上万人的规模建议换成云函数内使用聚合操作或者对签到记录做定时汇总避免前端一次性拉太多数据。导出Excel这里有个经验小程序端不适合直接生成Excel文件因为涉及文件流的处理和小程序沙箱的限制。我的做法是写一个云函数在服务端用node-xlsx库生成Excel文件存入云存储然后把临时文件链接返回给前端。用户点击按钮后复制链接到浏览器打开下载或者用wx.downloadFile下载后通过wx.openDocument打开。这个流程在真机上实测是稳定可行的。4. 部署上线与常见问题排查实录4.1 云开发模式下的部署流程与权限配置如果跟我一样用微信云开发部署流程其实非常简单但有几个细节必须注意不然会浪费很多调试时间。第一步在app.js里初始化云环境App({ onLaunch() { if (!wx.cloud) { console.error(请使用 2.2.3 或以上的基础库以使用云能力) } else { wx.cloud.init({ env: your-env-id, // 替换成你的云环境ID traceUser: true }) } } })第二步新建云函数quickSign把签到的核心逻辑传上去右键选择上传并部署云端安装依赖。注意依赖安装方式尽量选中云端安装依赖这样本地不需要有完整的node_modules。第三步配置数据库权限。云数据库的默认权限是仅创建者可读写这个对签到记录表来说是不够的因为小程序端通过云函数读写云函数拥有管理员权限不受这条规则限制。但如果你要让管理端直接在前端读取统计数据就需要在权限设置里加上自定义规则。我的做法是签到记录表设置成所有用户不可读写全都走云函数接口权限控制最严格也能避免误开数据。这里有一个不少新手会犯的错在云函数里使用数据库时没有用cloud.database()而是引用了本地wx.cloud.database()的实例导致权限不足或者环境不一致。云函数内部的数据库操作必须使用cloud.database()获取服务端实例这一点非常重要。4.2 安全校验与防作弊机制的设计心得签到系统最怕什么怕替签、怕伪造请求、怕扫码后数据对不上。我在这版系统里做了三层防护。第一层是签名校验。二维码里携带的token不是随便生成的而是服务端用密钥对activityId timestamp做哈希的结果且每次扫码生成的timestamp都有时效限制。请求后端时后端会用相同的密钥重新计算一次不一致就拒绝。这样伪造者即使知道了活动ID也无法算出合法的token。第二层是地理位置校验。有些场景只允许现场签到我可以在云函数里校验签到请求里携带的经纬度与活动地点的经纬度做个距离计算距离超过设置值就视为无效签到。不过这个功能默认是关闭的因为它需要用户授权地理位置体验上有一定损耗。只有在管理端开启了严格定位校验时才启用。第三层是频率控制。用云函数中间件对同一个openid在短时间内的签到请求做限流比如1分钟内最多签到5次防止有人用脚本刷接口。虽然签到本身有幂等保护但限流能减少无效的数据库写入降低服务负载。这三层防护在实际活动中已经足够了。网上很多开源签到项目只做了一层的签名校验甚至有的直接把openid当成参数传进来用户随便改一个值就能帮别人签到这种代码我建议千万不要直接用到生产环境。4.3 高频问题排查速查表与避坑经验开发过程中我记了不少问题这里整理成一张速查表基本都是新手上路最容易卡住的地方。现象原因解决方案扫码后一直提示签到码校验失败前端生成token的密钥和后端不一致检查云函数环境变量中的密钥是否与管理端生成码时使用的是同一个扫码后提示签到码已过期timestamp与服务器时间偏差超过5分钟检查手机系统时间是否准确或把5分钟窗口酌情放宽真机扫码一直无反应开发者工具里无法调用真实相机必须用真机预览调试开发者工具只能模拟扫码结果云函数调用超时数据库索引缺失导致查询慢在云开发控制台给签到记录表加上索引签到成功但统计里看不到记录云函数操作的是另一个环境检查wx.cloud.init中的env是否都指向同一个云环境管理端导入不了签到数据导出Excel的云函数缺少依赖在云函数目录执行云端安装依赖确保node-xlsx被装上再补充几个我个人的避坑心得。真机调试扫码功能第一次一定会遇到请使用真机调试的弹窗这其实是正常的。有些人会用开发者工具自带的模拟扫码功能来测试那个只能模拟不能真实调起相机扫码结果也是手动填的无法验证真实扫码的兼容性。务必要用真机预览。时间窗口的宽容度我试过3分钟结果有用户手机时间慢了1分钟扫码后经常报过期。后来改成5分钟才稳定下来。如果你在校园或其他网络环境不稳定的场景可以放宽到10分钟但再长就不安全了。关于二维码内容长度如果码的内容太复杂二维码图案会变得很密用户用低端手机扫码时要对很久。我试过在二维码里塞了一整段JSON结果现场扫码成功率只有六成。后来改成极简参数串扫码速度快了很多。最后分享一个提升扫码成功率的加分项在扫码页放一个手电筒按钮调用wx.setScreenBrightness或者请求摄像头流时开启闪光灯方便光线不足的会场使用。这个功能很不起眼但用户反馈特别好可以说是投入小见效快的细节优化。我在多个版本的迭代里还发现签到结果页的二次确认操作很重要。用户签到成功后如果不小心退出了结果页又去扫一次码系统应该提示您已签到而不是再次写入重复记录。这个幂等处理在并发情况下尤其考验数据库索引的兜底能力建议你实现时一定要加上唯一索引这道保险。