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

资讯详情

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

微信小程序签到码开发实战:从动态码到token兑换的完整方案

微信小程序签到码开发实战:从动态码到token兑换的完整方案 做签到码这个需求最早是帮朋友做一个线下培训的签到系统现场两百多人排队报手机号效率太低了。当时微信小程序正当红我就想能不能把签到做成“扫一下码”或者“输一下码”就完事。结果真做下来发现“签到码”这三个字背后牵扯的东西比想象中多得多动态码、固定码、兑换token、防重复提交、时间窗校验、机型适配、发布流程……每一环都有坑。这篇就把我实际摸索出来的完整方案写出来重点是微信小程序这一端怎么把“码”这件事做利索同时也把服务端配合的逻辑讲清楚给正要踩坑的朋友一份能直接抄的作业。这个方案适合谁如果你正在做会议签到、培训考勤、活动打卡、门店核销这类功能或者你是刚入门微信小程序、想找一个“输入码扫码状态刷新联调发布”完整链路来练手的开发者这篇文章应该能帮你少走不少弯路。我会把页面怎么写、校验怎么做、token怎么换、真机上容易翻车的几个地方全部拆开讲代码尽量给全思路尽量说透。1. 签到码方案设计先想清楚“码”后面要做什么1.1 签到码的常见形态与选型签到码从形态上分无非两种一种是动态码活动开始前由系统生成、按时段或按人分配用一次就失效另一种是固定码一个活动一个码所有参与者共用核销后标记已签到。两种我都实现过这里说下我的选型经验。如果是小规模课程、内部会议固定码完全够用。管理员把码印在海报上或者投到大屏幕用户在小程序里输入后端判断这个码当前是否有效、有没有被用过。好处是生成成本低、管理简单坏处是码一旦泄露理论上谁都能签所以通常要配合“只能签一次”和“时间窗口”一起用。如果是大型活动、多场次会议或者要求每个人签到记录必须精确到人那就得用动态码。动态码一般是个短码6到8位绑定到具体的用户或订单上可以按手机号发短信也可以在小程序内直接点签到按钮自动带出。用户输入或点击后后端用这个码换取一次性的签到凭证用后即焚。这两种方案在小程序端的实现差别不算大核心都是“拿到码 - 提交校验 - 刷新状态”。重点在于后端怎么设计这个兑换流程我后面会详细讲。这里先记住一个原则小程序端永远不要自己判断码对不对码的有效性、时效性、唯一性全部交给服务端裁决。1.2 小程序端与服务端的职责划分很多新手做签到功能喜欢在小程序本地判断“这个码是不是等于写死的那串字符”省事是省事但一旦要改码、限时、限制人数就得重新发版非常被动。我的做法是小程序端只负责采集“码”和“用户身份”服务端负责所有业务判断。小程序端职责清单大致是这样输入框采集签到码或者调用wx.scanCode扫码获取码值把码值和用户登录态code 或 token一起提交到后端接口接收后端返回的结果成功则展示签到成功页失败则展示失败原因本地缓存签到状态避免重复提交同时支持“我的签到”页面回显服务端职责清单校验签到码是否存在、是否在有效期内绑定用户唯一标识防止同一用户重复签到返回新的用户 token 或更新签到状态后续其他页面以此为依据记录签到时间、签到方式、IP/地理位置按需等扩展字段这个划分想明白之后小程序端的工作就变得很纯粹页面交互 请求封装 状态管理。所有“为什么这个码不能用了”的问题都是后端响应里的一条消息前端只需要把消息原样展示出来再根据错误码决定是否清空输入框、是否跳转结果页。2. 小程序端核心实现输入码、扫码与状态管理2.1 页面结构输入码与扫码两个入口签到页我通常会做成一个独立的页面顶部放标题和活动名称中间放一个大大的输入框下面放“立即签到”按钮右上角或按钮旁边放一个扫码的小图标。实话说扫二维码的体验在微信里已经非常成熟但二维码物料打印、张贴需要成本所以“手动输码”这个入口绝对不能省尤其是临时加人或者二维码被遮挡的时候。页面示意图大概这样输入框用input组件设置typenumber或typetext考虑到签到码可能包含字母比如会议码“MEET2025A”我建议用typetext加上maxlength10不要限制成纯数字。扫码按钮调用wx.scanCode扫码成功后自动填入并直接触发提交省去用户再按一次确认键。WXML 骨架参考view classsign-container view classsign-header text classsign-title{{activityName}}/text text classsign-subtitle请输入签到码完成签到/text /view view classsign-input-wrap input classsign-input placeholder请输入签到码 placeholder-classsign-input-placeholder value{{code}} maxlength10 bindinputhandleCodeInput / view classscan-btn bindtaphandleScan扫码/view /view button classsign-btn bindtaphandleSubmit立即签到/button view classsign-tip wx:if{{tipText}}{{tipText}}/view /view这里有个细节bindinput在 iOS 和安卓上返回值的结构是一样的但用户输入法联想可能会带入空格所以我在handleCodeInput里做了trim()彻底去掉首尾空格再存进 data。签到码这种东西多一个空格少一个空格都会导致校验失败前端能拦截的脏数据一定在前端拦掉。2.2 setData的动态Key与表单校验签到页的 data 结构我习惯这样设计data: { code: , activityName: 2025年产品培训会, tipText: , submitting: false, signed: false }handleCodeInput里除了 trim还可以顺手做“长度达标自动提交”的交互这个看产品需求我在内部工具型小程序里会做但在公开活动上反而故意不做因为容易误触用户还没核对自己的码就发出去了。经验是输码 手动确认虽然多一步但用户安全感强很多。说到 setData有一个高频需求是动态更新对象里的某个字段很多新手会踩坑。比如后端返回的数据结构是{ userInfo: { nickname: 张三, avatar: https://... }, signId: s_20250101_001 }你直接写this.setData({ userInfo.nickname: res.data.userInfo.nickname })在微信小程序里是能用的因为 setData 的 key 支持路径写法但注意这个 key 必须用引号括起来否则会被当成对象解析报语法错误。我之前帮同事排查过一个问题他写的是this.setData({ userInfo.nickname: that.data.nickname })直接报Unexpected number就是因为没加引号。正确的写法是const { nickname } res.data.userInfo; this.setData({ userInfo.nickname: nickname });如果要做更通用的动态更新可以这样写const field userInfo.nickname; this.setData({ [field]: value });这种计算属性名的方式在小程序的基础库 2.x 之后都支持但为了保险我一般还是手动拼好字符串再传。基础库版本这块建议在app.json里用libVersion: latest或者在开发者工具里选一个稳定的基础库版本不要直接依赖最新版因为最新版偶尔会有新特性回归影响生产环境的稳定性。2.3 扫码能力与结果跳转扫码是签到场景里体验最好的方式没有之一。微信原生组件就支持不用额外引入 SDK代码非常轻。handleScan() { wx.scanCode({ scanType: [qrCode, barCode], success: (res) { const code (res.result || ).trim(); if (code) { this.setData({ code }); this.submitSign(code); } else { wx.showToast({ title: 未识别到签到码, icon: none }); } }, fail: (err) { if (err.errMsg err.errMsg.includes(cancel)) { return; // 用户主动取消不用提示 } wx.showToast({ title: 扫码失败请手动输入, icon: none }); } }); }这里有两个容易被忽略的细节。第一扫码结果不一定就是纯签到码。有些签到码会做成 URL 形式比如https://example.com/sign?codeMEET2025A如果你直接把整串 URL 提交给后端后端解析起来会很麻烦。我的做法是后端提供一个规整函数从 URL 中抽取code参数或者前端在handleScan里用正则把参数抠出来二选一但一定要做不能默认扫码结果就是干净码。第二扫码成功后不要立刻wx.navigateTo跳转因为提交是异步的跳转过早会导致 loading 状态丢失、用户反复点击。我习惯先把submitting置为 true等submitSign的回调返回后再根据成功失败决定是wx.navigateTo到成功页还是wx.showToast提示失败并留在原地。2.4 顶部导航栏与页面适配细节签到页如果嵌在需要沉浸式展示的场景里就绕不开导航栏高度适配的问题。微信小程序的胶囊按钮右上角那三个点加圆圈高度在不同机型、不同基础库版本下并不完全一致所以不要写死一个padding-top。比较稳妥的做法是在onLoad里读取系统信息const systemInfo wx.getSystemInfoSync(); const menuRect wx.getMenuButtonBoundingClientRect(); const statusBarHeight systemInfo.statusBarHeight || 20; const navBarHeight (menuRect.top - statusBarHeight) * 2 menuRect.height;然后用navBarHeight撑起自定义导航栏的高度。如果你用原生导航栏可以不用管这些但想追求品牌感自定义导航栏是绕不开的路上面的代码就是我最常用的计算方式。ios 上还有一个经典问题页面内容滚动不畅。有朋友反馈“苹果手机在微信小程序不能进行滑动滚动”我排查过几次最常见的原因是页面根节点设置了height: 100vh而内部内容区没有给flex: 1配合overflow-y: auto导致内容溢出后无处可滚。修复方式很简单给最外层容器设置display: flex; flex-direction: column; height: 100vh;给可滚动区域设置flex: 1; overflow-y: auto;。ios 的橡皮筋回弹特性和安卓略有差异但这样的结构是兼容两种系统的最稳方案。3. 服务端兑换逻辑从签到码到签到成功的闭环3.1 用code换token的设计思路热词里有一句“用code换token”这其实就是签到码系统的核心骨架。前端拿到用户输入的 code 之后不能直接认为“签到成功”而是要用这个 code 去后端换一个“签到凭证”。这个凭证可以是一次性的 token也可以是更新后的用户状态。我设计过这样一组接口接口请求参数返回内容POST /sign/exchangecode、userTokensignToken、活动信息、签到时间GET /sign/statususerToken、活动ID是否已签到、签到时间、签到序号POST /sign/canceluserToken、signToken撤销结果仅限管理员前端在用户输入码后先拿 code 去请求/sign/exchange成功之后拿到 signToken再凭借 signToken 展示成功页、写入本地缓存后续任何需要校验签到资格的接口都带上这个 signToken。这样一来签到码本身不参与业务数据的读取签到码只是“兑换入场资格的一把钥匙”安全性高很多。后端伪代码逻辑Node.js 为例async function exchangeSign(req, res) { const { code, userToken } req.body; // 1. 查签到码记录 const signCode await db.findSignCode(code); if (!signCode) return res.json({ errCode: 1001, msg: 签到码不存在 }); // 2. 检查时间窗口 const now Date.now(); if (now signCode.startTime || now signCode.endTime) { return res.json({ errCode: 1002, msg: 签到码不在有效期内 }); } // 3. 检查是否重复签到 const isSigned await db.checkSigned(userToken, signCode.activityId); if (isSigned) return res.json({ errCode: 1003, msg: 您已签到请勿重复提交 }); // 4. 生成一次性 signToken 并记录 const signToken generateToken(userToken, signCode.id); await db.saveSignRecord({ userToken, activityId: signCode.activityId, signToken, signAt: now }); return res.json({ errCode: 0, data: { signToken, activityName: signCode.activityName, signAt: now } }); }这个流程里第 2、3 步的顺序尽量不要颠倒先查码、再查时间、最后查重复这样的错误提示对用户更友好。如果先查重复再查时间用户可能看到一个“请勿重复提交”但实际上码已经过期了会很困惑。3.2 防重复签到与时间窗校验防重复签到是一开始就必须想好的硬需求否则有人拿同一个码反复刷签到统计就废了。我见过最简单的做法是后端用 Redis 存一个signed:{userId}:{activityId}的 key签到成功后写入再次签到直接命中效率高也天然有过期时间可配置。没有 Redis 的话用数据库表加唯一索引也能实现在签到记录表里给(userToken, activityId)建唯一索引第二次插入直接抛冲突代码里捕获冲突后返回“已签到”。这招比先查后插更稳因为先查后插在高并发下存在竞态两个请求同时查到“未签到”然后同时插入就重复了。时间窗校验也建议放在后端统一处理因为前端的本地时间可以被用户修改而且不同步绝对时间。有些场景还需要区分“提前签到”和“迟到签到”我给活动配置了startTime、endTime、allowEarlyMinutes三个字段allowEarlyMinutes 表示允许提前多少分钟入场默认是 0也就是必须从开始时间起才能签。为什么这么设计因为活动现场经常出现“人都到了但时间没到”的尴尬如果系统一刀切说“不在有效期内”用户体验很差留一个提前量给运营去配比较灵活。3.3 可选的地理围栏校验签到码 token 已经能覆盖绝大多数场景但如果你做的是“门店打卡”“现场会议”可能需要加一个地理围栏校验。微信小程序里获取地理位置需要用户授权同时要在app.json中声明requiredPrivateInfos和permission这两样缺一样真机上wx.getLocation都会直接失败。地理围栏的校验不能在前端做因为前端拿到的经纬度可以被伪造必须在后端拿签到接口里的经纬度参数去和服务端保存的场地中心点做距离计算function distance(lat1, lng1, lat2, lng2) { const rad Math.PI / 180; const dLat (lat2 - lat1) * rad; const dLng (lng2 - lng1) * rad; const a Math.sin(dLat / 2) ** 2 Math.cos(lat1 * rad) * Math.cos(lat2 * rad) * Math.sin(dLng / 2) ** 2; return 2 * Math.asin(Math.sqrt(a)) * 6371000; // 米 }然后判断distance(centerLat, centerLng, userLat, userLng) radiusMeters满足则允许签到否则返回“不在签到范围”。需要提醒一句苹果手机在部分版本上定位返回的经纬度精度偶尔出现漂移几十米内误判属于正常现象所以半径设置建议不低于 200 米否则误报率高用户会来投诉。你要么把半径放开点要么在提示文案里写清楚“请在活动场地内签到”不要写精确的数字免得用户跟你抠字眼。4. 前后端联调与发布避坑实录4.1 联调阶段的调试手段签到码功能写完之后最耗时间的是联调。我常用的调试方式有三种。第一种微信开发者工具里的“模拟器 Network”面板。在 Network 面板里能看到小程序的每个请求、响应、耗时和状态码仔细检查请求参数是否和服务端对得上。很多签到失败的问题其实不是逻辑不对而是前后端对字段名没对齐比如前端传了code后端接口文档写的是signCode这类低级错误在 Network 面板里一眼就能看出来。第二种真机预览。开发者工具模拟器里很多东西不真实比如扫码能力、定位、键盘弹起、胶囊按钮遮挡这些必须真机测。在开发者工具里点“预览”生成二维码用手机微信扫码打开再配合手机上的 vConsole 插件看日志。vConsole 是嵌入在小程序代码里的调试面板可以在页面上直接看到 console 输出和网络请求。我在测试阶段会加一个 URL 参数控制是否加载 vConsole上线时默认不加载这样既不泄露调试信息也不影响包体积。第三种就是最粗暴的“多端自测”一台安卓、一台 iPhone两个系统的表现往往不一样特别是键盘、滚动、定位这些和 WebView 交互相关的功能真的只有真机才能发现。我之前测试时在开发者工具里一切正常到了苹果手机上页面滚不动后来才知道是100vh被刘海屏和工具栏占了空间真实可视区域比100vh小导致底部内容被截断。这类问题必须真机复现才有针对性。调试阶段还有个小技巧在app.json的networkTimeout里把请求超时时间调大一点比如request: 30000防止弱网环境下接口慢被误判失败。正式环境再调回 10000 左右。4.2 HBuilderX发布微信小程序的完整流程如果你的项目是用 uni-app 写的会绕不开 HBuilderX 发行这一步。很多朋友第一次跑的时候对“怎么从 uni-app 项目变成微信小程序包”一头雾水我拆开讲。第一步打开项目确认你用的是 Vue 2 还是 Vue 3 的 uni-app 模板。manifest.json里需要配置微信小程序的appid如果还没有就去微信公众平台注册小程序拿到 AppID。这一步别省一定要填真实 AppID否则后续上传和预览都会受限。第二步选择菜单“发行 - 小程序-微信”HBuilderX 会自动执行编译产出目录通常在项目的dist/build/mp-weixin。编译完成后HBuilderX 会提示你是否打开微信开发者工具选“是”它会自动拉起本地的微信开发者工具并导入产物目录。第三步在微信开发者工具里再次确认 AppID 正确、基础库版本选择合适然后点“上传”按钮。上传后的版本会出现在微信公众平台的“版本管理”里管理员或开发者可以在那里把版本设为“体验版”或者提交审核发布。过程中最常见的报错是Error: mock data is empty or file not found这个多半是编译产物没生成完整清一下 dist 目录重新发行就好。另外 HBuilderX 和微信开发者工具之间有时候会因为端口占用连不上把两个软件都关掉重启一般能解决。uniapp 项目里还会遇到一个问题就是某些 H5 端能跑的代码在小程序端报错。比如直接使用了window、document在小程序中根本不存在。我建议所有操作环境相关的代码都用条件编译包一层// #ifdef MP-WEIXIN const menuBtn wx.getMenuButtonBoundingClientRect(); // #endif // #ifdef H5 const menuBtn { top: 0, height: 44 }; // #endif这样能保证同一套代码在多个端都不会因为环境变量崩溃。注意条件编译是 uni-app 特有的写法注释里的#ifdef一定不能有空格否则编译不过。4.3 经典报错与兼容问题速查开发签到码功能这段时间我陆陆续续踩过不少坑挑几个高频的写出来。一个典型报错是Component pages/index/index does not have a method navigatorClick。这通常是 WXML 里绑定了事件但 JS 的methods里没有定义对应函数。在 Vue 2 风格的 uni-app 里方法写在methods对象里在原生小程序里方法直接写在Page对象下。排查时先看事件名是否写错再看函数作用域是否正确。还有一种是事件名和内部变量重名小程序解析时把方法名当成 data 里的字段了找不到方法就报这个错换个名字就解决了。第二个高频问题是微信小程序 handshake failed due to invalid upgrade header: null这通常出现在本地调试 WebSocket 或某个依赖 WebSocket 的第三方库时。原因是小程序对 WebSocket 的握手校验比浏览器严格服务端没有正确返回 upgrade 响应头。签到码本身用不到 WebSocket但如果你在同一个项目里集成了聊天、消息推送就可能撞上。解决方案是换一个支持小程序协议的 WebSocket 服务端实现或者确认服务器在握手阶段正确返回了Upgrade: websocket和Connection: Upgrade。第三个问题是“基础库版本从哪设置”。微信公众平台后台和开发者工具里都能设置。开发者工具右上角“详情 - 本地设置 - 调试基础库”可以选择版本真机上用户微信的基础库版本由微信自动更新但你的代码可以设置最低基础库版本。我建议最低版本设置在 2.10.0 以上太低的话很多 API 不可用但也不用追求最新太新的 API 在用户群体中覆盖率不够容易导致低版本用户闪退。第四个容易踩的坑是“保存附件 wx.env.user_data_path”。小程序的本地文件路径在不同系统上不一样wx.env.USER_DATA_PATH是获取本地用户目录的推荐方式。但真机上这个目录有时候用户无感知清理微信缓存就没了所以签到记录这种重要数据一定要在拿到签到成功回调后就同步到服务端不要只存在本地。本地路径只适合缓存图片、临时文件不适合做持久化业务数据。第五个安卓机 emoji 输入或特殊字符导致的上传失败。签到码虽然是系统生成的一般不会含特殊字符但如果允许用户自己填备注要特别注意过滤不可见字符。我遇到过用户从 Excel 复制文本里面带了一个\u00A0不间断空格前端 trim 也去不掉最后在后端统一code.replace(/\u00A0/g, )才解决。小程序输入框也建议开启trim属性保持数据干净。4.4 安全与审核注意事项签到码系统涉及用户身份、签到记录、活动状态安全层面的几个基础要求一定要做到不能偷懒。第一个是传输层安全。小程序的请求域名必须配置在微信公众平台的“开发设置 - 服务器域名”里且必须是 HTTPS。正式环境的接口地址不能用 IP不能用自签名证书否则真机请求直接失败。本地开发时可以用开发者工具里“不校验合法域名”的开关但上线前一定要关掉并配置正确的合法域名。第二个是防止码被暴力遍历。签到码如果是纯数字短码理论上有人可以批量尝试。后端必须加上频率限制比如同一个用户一分钟最多请求 5 次交换接口同一个 IP 一小时最多 20 次超限直接拒绝。可以用 Redis 的INCREXPIRE很轻松地实现别偷懒这个不做迟早被刷。第三个是隐私授权。微信对用户隐私的管控越来越严格签到功能如果涉及地理位置、手机信息、昵称头像都需要在小程序后台做好隐私保护指引否则审核会被拒。申请wx.getLocation权限时文案一定要写得具体比如“用于确认你在活动签到范围内”不要写“获取您的位置”理由不充分会被系统驳回。小程序审核这块签到码这类工具类功能只要不涉及虚拟支付、诱导分享一般都比较顺利。需要注意的是如果你在小程序里做了分享拉新之类的操作一定要避开“分享得签到资格”这类诱导逻辑微信审核对诱导分享看得很严一旦判定违规轻则功能下线重则封禁账号。签到就是签到别掺营销这是我踩过坑之后最深刻的感受。5. 上线之后还能怎么扩展签到码做稳定之后后续扩展方向其实挺多的而且每个扩展都顺着同一套“码 token”骨架走改造量不大。一个是多活动管理。后端把签到码与活动 ID 解耦一个小程序就可以同时给多个活动服务每个活动生成一批签到码签到记录按活动维度统计。前端首页做成活动列表点进活动再签到这个扩展大概半天到一天的工时。另一个是签到数据导出。管理员视角加一个 Web 管理后台把签到记录导出成 Excel 或 CSV方便和参会名册对账。技术上就是加一个管理端登录左栏活动列表右栏签到明细表服务端导出时注意数据量超过几万条用异步任务生成文件避免接口超时。再一个是大屏实时统计。活动主办方通常希望现场就能看到“已签到多少人”“签到率多高”那就需要在小程序签到成功后服务端推一条消息到管理端的大屏页面可以用 WebSocket 或者轮询。如果不想引入 WebSocket 的复杂度轮询 5 秒一次在几百人规模下其实也够用。我个人在实际操作中的体会是签到码这种功能听起来小但它是一个完整的用户交互闭环从输入、扫码、校验、反馈到状态展示、后台统计每一环都会遇到意想不到的细节问题。把这些细节逐个解决掉你学到的远远不止“写一个签到页面”这么简单。如果后续还有精力建议把整个项目按“前端小程序 后端服务 管理后台”三层结构重构一遍对理解完整业务系统会很有帮助。
返回列表