系列教程微信小程序投票系统完整开发 上一章第十章 - 结果页 result 开发 下一章第十二章 - 后端接口设计与数据库建表11.1 为什么 openid 如此重要openid 是微信对用户在某个特定小程序下的唯一标识具有以下特性特性说明唯一性同一个用户在同一个小程序的 openid 永远不变隔离性同一个用户在不同小程序的 openid 不同安全性不暴露用户真实微信号隐私友好用途识别用户身份、防重复投票、关联用户数据11.2 openid 获取的完整时序图小程序端 后端服务 微信服务器 │ │ │ │── 1. wx.login() ───────►│ │ │◄─ 2. 返回临时 code ──────│ │ │ │ │ │── 3. POST /wx/user/login │ │ │ { code: xxx } ──►│ │ │ │── 4. GET code2Session ──►│ │ │ appidsecretcode │ │ │◄─ 5. { openid, session } │ │ │ │ │◄─ 6. { code:200, │ │ │ data: openid } ──│ │ │ │ │ │─ 7. 缓存到 Storage ──────┤ │ │─ 8. 通知所有等待回调 ──────┤ │关键点code有效期5分钟且只能使用一次AppSecret只能在后端使用绝不能放在小程序代码中后端调用微信 API 的地址https://api.weixin.qq.com/sns/jscode2session11.3 当前 app.js 完整代码解析// app.jsApp({globalData:{userInfo:null,openid:,baseUrl:https://www.chinahanwucun.cn,_openidReady:false,// openid 是否已就绪_openidCallbacks:[]// 等待 openid 的回调队列},/** * 对外暴露的 openid 获取方法 * 如果 openid 已就绪 → 立即回调 * 如果未就绪 → 加入队列就绪后统一回调 * * 使用方式在页面 js 中 * const app getApp() * app.getOpenid(openid { ... }) */getOpenid(callback){if(this.globalData._openidReady){callback(this.globalData.openid)}else{this.globalData._openidCallbacks.push(callback)}},/** * 内部方法openid 就绪通知所有等待的回调 */_resolveOpenid(openid){this.globalData.openidopenidthis.globalData._openidReadytrue// 清空队列依次执行所有等待的回调constcbsthis.globalData._openidCallbacksthis.globalData._openidCallbacks[]cbs.forEach(cbcb(openid))},onLaunch(){// 步骤1优先读取缓存让页面请求能立即发出不用等待登录完成constcachedwx.getStorageSync(openid)if(cached){this._resolveOpenid(cached)}// 步骤2每次启动都重新登录刷新 session微信 session 有效期约7天wx.login({success:res{if(res.code){wx.request({url:${this.globalData.baseUrl}/wx/user/login,method:POST,header:{content-type:application/json},data:{code:res.code},success:r{if(r.datar.data.data){constopenidr.data.data wx.setStorageSync(openid,openid)// 更新缓存// 如果之前没有缓存首次使用现在通知等待的回调if(!cached){this._resolveOpenid(openid)}else{// 已经通知过了只更新全局变量即可this.globalData.openidopenid}}elseif(!cached){this._fallbackOpenid()}},fail:(){if(!cached)this._fallbackOpenid()}})}},fail:(){if(!cached)this._fallbackOpenid()}})},/** * 降级策略当网络异常或接口异常时生成一个临时 ID * 用 tmp_ 前缀区分后端可做相应处理 */_fallbackOpenid(){letopenidwx.getStorageSync(openid)if(!openid){openidtmp_Date.now()_Math.random().toString(36).substr(2,8)wx.setStorageSync(openid,openid)}this._resolveOpenid(openid)}})11.4 回调队列机制详解这是一个经典的异步初始化 等待队列模式// 场景页面 A 和页面 B 同时加载都需要 openid// 页面 A onLoadopenid 还没好app.getOpenid(openid{// 此回调被推入 _openidCallbacks 队列fetchVoteList(openid)})// 页面 B onLoadopenid 还没好app.getOpenid(openid{// 此回调也被推入 _openidCallbacks 队列fetchUserInfo(openid)})// 此时 _openidCallbacks [fetchVoteList回调, fetchUserInfo回调]// ... 等待 wx.login 后端接口 ...// openid 获取成功_resolveOpenid 被调用// → 依次执行队列中所有回调// → fetchVoteList 和 fetchUserInfo 同时收到 openid 并执行11.5 常见错误与解决方案错误1直接读取 globalData时序问题// ❌ 错误app.js 还在异步获取 openid这里可能拿到空字符串onLoad(){constopenidapp.globalData.openid// 可能是 wx.request({data:{openid}})// 请求带了空 openid}// ✅ 正确使用回调确保 openid 就绪后再发请求onLoad(){app.getOpenid(openid{wx.request({data:{openid}})// openid 一定有值})}错误2code 被多次使用// ❌ 错误每次需要 openid 都调用 wx.login// wx.login 返回的 code 只能用一次多次调用会导致之前的 code 失效// ✅ 正确只在 app.js onLaunch 中调用一次 wx.login// 后续通过 getOpenid() 方法复用已获取的 openid错误3AppSecret 放在前端// ❌ 严重错误小程序代码可以被反编译AppSecret 会泄露wx.request({url:https://api.weixin.qq.com/sns/jscode2session,data:{appid:xxx,secret:AppSecret泄露了,js_code:code}})// ✅ 正确code 发给自己的后端由后端持有 AppSecret 去换 openidwx.request({url:https://your-server.com/wx/user/login,data:{code:code}// AppSecret 在后端的 application.properties 中配置})11.6 全局状态管理扩展除了 openidapp.js 还可以管理其他全局状态App({globalData:{openid:,baseUrl:https://www.chinahanwucun.cn,// 扩展用户信息缓存userInfo:null,// 扩展全局配置从后端拉取config:{maxOptionsPerVote:10,voteTitleMaxLength:50}},// 扩展检查网络状态checkNetwork(callback){wx.getNetworkType({success(res){if(res.networkTypenone){wx.showToast({title:当前无网络连接,icon:none})callback(false)}else{callback(true)}}})}})本章小结✅ 深入理解了 openid 的唯一性和获取时序✅ 完全理解了回调队列机制异步初始化模式✅ 掌握了正确使用getOpenid()的方式✅ 知道了 3 个典型错误场景及避坑方法下一章后端接口设计与完整的数据库建表语句。