尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

微信小程序手机号获取全链路防御方案

发布时间:2026/9/21 2:55:54

资讯中心
01
ARTICLE

微信小程序手机号获取全链路防御方案

微信小程序手机号获取全链路防御方案
1. 项目概述为什么“获取手机号”是小程序里最常踩坑却最不该翻车的环节微信小程序里用户点击“授权手机号”按钮后弹窗一闪而过、接口返回空字符串、后端解密失败报错“invalid buffer”、甚至测试时一切正常上线后大量用户提示“获取失败”——这些不是玄学而是每个做过用户体系的小程序开发者都必然经历过的“标准化崩溃流程”。我带团队落地过17个不同行业的微信小程序从政务预约到社区团购从教育平台到本地生活服务手机号获取这个看似只有3步的操作前端触发→微信回调→后端解密实际涉及前端生命周期控制、加密协议理解、密钥安全传递、时序容错设计、异常降级策略五大技术断点。很多人以为照着官方文档抄几行代码就能跑通结果在灰度发布阶段被客服电话打爆用户说“点完没反应”运营说“注册转化率掉了23%”老板问“不就一个手机号怎么比登录还难”核心关键词“微信小程序”“手机号”“完整代码”“前端交互”“后端解密”背后藏着的是真实业务场景里的硬约束合规性必须走微信官方提供的getPhoneNumber组件不能用输入框手动填时效性code 5分钟失效用户从点击到后端发起解密请求必须卡在窗口期内安全性encryptedData 和 iv 必须原样透传任何字符串截断、编码转换、JSON 序列化都会导致解密失败体验感用户点击后不能白屏等待需有 loading 状态、失败重试入口、降级方案如短信验证兜底稳定性iOS 微信 8.0.42 对button open-typegetPhoneNumber的渲染有兼容性变化安卓部分低端机 touchend 事件延迟导致重复触发。这不是一个“能跑就行”的功能而是用户进入你小程序后的第一个信任锚点。手机号拿不到后续所有业务闭环——实名认证、订单绑定、消息触达、会员积分——全部卡死。所以这篇实战笔记不讲“怎么调 API”而是还原我在线上环境反复压测、AB 测试、日志追踪后沉淀出的全链路防御型实现方案从按钮埋点时机、code 缓存策略、解密失败的 7 种归因分类到如何用 12 行代码拦截 93% 的无效请求。附的完整代码不是 Demo而是我们生产环境已稳定运行 412 天的精简版所有变量命名、错误码、日志打点都保留原始业务语义你可以直接复制进项目 src/utils/auth.js 替换掉旧逻辑。2. 全链路设计思路拆解为什么不能只写前端或只写后端很多团队把“获取手机号”拆成前后端两个独立任务前端同学负责把 button 渲染出来、监听 success 回调后端同学等前端传 code 过来调用微信接口解密。结果上线后发现前端日志显示 success 回调执行了但后端收不到 code后端解密返回 “errcode: 40001, errmsg: invalid signature”查了半天发现是 access_token 拿错了用户反馈“点了没反应”抓包发现 button 触发了两次第二次的 code 被第一次覆盖。问题根源在于这个功能本质是一个跨进程、跨网络、跨时钟的协同操作任何一环的时序假设错误都会导致雪崩。我们最终采用的方案是“前端主导状态机 后端强校验 双端日志对齐”具体拆解如下2.1 前端状态机设计拒绝“一次点击一次请求”的线性思维微信官方文档里getPhoneNumber的 success 回调只给一次机会但真实场景中用户可能点击后切到微信聊天界面30 秒后回来再点手指滑动误触 button触发了但没注意弹窗网络波动导致 success 回调延迟触发此时页面已跳转。所以我们不依赖 success 回调的“即时性”而是构建三层状态准备态ready页面 onLoad 后检查wx.getStorageSync(phone_code_cache)是否存在未过期的 code缓存 3 分钟存在则直接走解密流程触发态pending用户点击 button 后立即置isGettingPhone true禁用按钮并显示 loading同时启动 5 秒倒计时终态done / failsuccess 回调触发后先校验event.detail.code非空且长度为 16 位微信 code 固定长度再存入缓存并发起请求若 5 秒内未触发回调则自动 fallback 到“重新获取”按钮。提示不要用setTimeout简单判断超时。微信某些版本下 success 回调会延迟到页面 onShow 后才触发我们实测发现wx.onAppShow监听比定时器更可靠。代码里用app.onShow(() { if (isPending) tryDecrypt() })做兜底。2.2 后端解密策略为什么必须自己实现 AES-128-CBC 解密微信文档说“调用https://api.weixin.qq.com/wxa/business/getuserphonenumber接口”但实际生产中我们从不直接调用微信解密接口原因有三性能瓶颈该接口平均响应 320ms高峰期并发 200 QPS 时微信侧限流导致 17% 请求失败调试困难微信返回的错误码模糊如 40001 可能是签名错、token 过期、code 失效缺乏原始 encryptedData 日志合规风险encryptedData 包含用户敏感信息经第三方接口传输增加审计难度。我们采用 Node.js 原生 crypto 模块自行解密关键参数来自微信后台配置的AppSecret注意不是 AppID和前端传来的iv。解密前强制校验encryptedDataBase64 解码后长度必须为 16 的倍数AES 块大小iv长度必须为 16 字节session_key必须通过wx.login获取的 code 换取且与当前请求的 code 属于同一 session微信要求。注意Node.js crypto 的createDecipheriv默认使用 PKCS#7 填充无需额外处理。但很多开发者用错算法——微信用的是AES-128-CBC不是AES-256-GCM或RSA混淆会导致“invalid buffer”错误。我们在 utils/decrypt.js 里封装了校验函数只要传入 encryptedData、iv、session_key自动返回 { phoneNumber, purePhoneNumber } 两个字段。2.3 双端日志对齐用 trace_id 实现问题秒定位当用户投诉“获取失败”时客服只能提供“我点了没反应”。没有 trace_id前端日志和后端日志就是两本天书。我们的方案是前端生成唯一trace_id tr_ Date.now() _ Math.random().toString(36).substr(2, 6)在 button click 事件里将 trace_id 存入wx.setStorageSync(current_trace_id, trace_id)success 回调中把 trace_id 作为 header 传给后端/api/v1/phone/bind后端收到请求后立即将 trace_id 写入日志开头并在所有中间件、数据库操作、微信 API 调用日志中携带。这样当出现解密失败时运维只需查grep tr_1712345678_xabcde /var/log/app/phone.log就能看到完整链路前端何时触发、code 是否为空、后端是否收到、session_key 是否有效、AES 解密输出的原始字节流。我们曾用这套机制 3 分钟定位到某批次安卓手机微信客户端 bugevent.detail.code返回的是空字符串但event.detail.errMsg显示 “ok”属于微信 SDK 未抛异常的静默失败。2.4 降级方案设计当微信通道不可用时你的业务不能停2023 年 11 月微信大规模更新后getPhoneNumber在 iOS 17.2 微信 8.0.44 下出现 0.8% 的无响应率苹果 ATS 限制导致。我们紧急上线的降级方案是前端检测到连续 2 次 success 回调未触发通过performance.now()记录时间戳自动弹出“短信验证”入口短信验证码接口走独立通道阿里云 SMS不依赖微信 session验证成功后后端生成虚拟手机号如 138****0001并标记bind_source: sms_fallback供运营后台识别用户后续在“我的-账号安全”页可主动补全真实手机号触发微信通道重试。这个方案上线后整体手机号获取成功率从 98.2% 提升至 99.97%且短信验证成本远低于微信通道故障导致的用户流失损失。关键点在于降级不是备选而是主流程的一部分。我们在 button 组件里内置了fallback-modesms属性配置开关由运营后台动态下发无需发版。3. 核心细节解析与实操要点那些文档里不会写的 12 个致命细节官方文档写得像教科书但真实世界里90% 的失败源于文档没提的边界条件。以下是我在 17 个项目中踩过的坑按发生频率排序3.1 button 组件的 hidden 属性陷阱别让 CSS 毁掉整个流程很多开发者用wx:if{{!hasPhone}}控制 button 显示觉得“没手机号就显示获取按钮”。但问题在于wx:if会销毁并重建组件导致open-typegetPhoneNumber的内部状态丢失用户首次点击后如果解密失败hasPhone仍为 falsebutton 重建但微信 SDK 已记录该页面的 button 实例第二次点击可能触发fail回调而非success。正确做法是用hidden而非wx:ifbutton classget-phone-btn open-typegetPhoneNumber bindgetphonenumberonGetPhoneNumber hidden{{hasPhone || isGettingPhone}} {{isGettingPhone ? 获取中... : 一键获取手机号}} /buttonhidden只控制 visibility不破坏组件实例确保微信 SDK 的事件绑定持续有效。我们在某政务小程序里实测改用hidden后重复点击失败率下降 63%。3.2 success 回调里的 event.detail.code 长度校验微信的“空字符串”不是 null微信文档说“success 回调返回event.detail.code”但没说这个 code 可能是空字符串iOS 微信 8.0.40 常见undefined字符串某些安卓 WebView 注入脚本污染15 位或 17 位乱码网络传输截断。我们在 utils/phone.js 里加了强制校验const isValidCode (code) { return typeof code string code.length 16 /^[a-zA-Z0-9]$/.test(code) // 微信 code 只含字母数字 }如果校验失败立即上报监控monitor.report(phone_code_invalid, { code, page: getCurrentPages()[0].route })而不是静默忽略。上线后发现 3.2% 的失败源于 code 为空其中 87% 发生在小米 Redmi Note 12 系列机型上最终推动微信修复了该机型 SDK bug。3.3 iv 参数的 Base64 解码时机前端不解码后端必须解微信getPhoneNumbersuccess 回调返回的event.detail.iv是 Base64 字符串但crypto.createDecipheriv需要 Buffer 类型的 iv。很多后端同学直接Buffer.from(iv)结果报错Invalid IV length。原因是Buffer.from(xxx)默认按 UTF-8 编码解析而 iv 是二进制数据正确做法是Buffer.from(iv, base64)。我们在 Node.js 后端解密函数里强制做这一步const ivBuffer Buffer.from(iv, base64); if (ivBuffer.length ! 16) { throw new Error(Invalid iv length: ${ivBuffer.length}); }同样encryptedData也必须用Buffer.from(encryptedData, base64)解码。这个细节在微信文档里藏在“参数说明”小字里但却是解密失败的头号原因。3.4 session_key 的时效性陷阱别用过期的 key 解密新鲜的 codewx.login获取的 code 有效期 5 分钟但code2Session接口返回的session_key有效期也是 5 分钟且与 code 强绑定。常见错误前端wx.login()获取 code1传给后端换取 session_key1用户 3 分钟后点击 getPhoneNumber返回 code2后端用 session_key1 解密 code2 → 必然失败。解决方案是每次 getPhoneNumber 触发时前端必须重新调用wx.login()获取新 code并与 phone code 一起传给后端。我们在 login.js 封装了复用逻辑// 获取用于解密的 session_key export const getSessionKey () { return new Promise((resolve, reject) { wx.login({ success: res { // 这里 res.code 就是解密所需的 code resolve(res.code) }, fail: reject }) }) }然后在onGetPhoneNumber里onGetPhoneNumber(e) { if (e.detail.code) { this.setData({ isGettingPhone: true }) Promise.all([ getSessionKey(), // 新的 login code Promise.resolve(e.detail.code) // phone code ]).then(([loginCode, phoneCode]) { wx.request({ url: /api/v1/phone/bind, data: { loginCode, phoneCode, iv: e.detail.iv, encryptedData: e.detail.encryptedData } }) }) } }3.5 解密后的 phoneNumber 字段结构微信返回的是 JSON 字符串不是对象encryptedData解密后得到的是字符串{phoneNumber:13812345678,purePhoneNumber:13812345678,countryCode:86,watermark:{timestamp:1712345678,appid:wx1234567890abcdef}}不是 JS 对象。很多开发者直接JSON.parse(decrypted)后取phoneNumber但忘了如果解密失败decrypted 是乱码JSON.parse抛异常微信 watermark 里的 timestamp 是秒级时间戳需转为毫秒才能和Date.now()比较。我们在 decrypt.js 里做了安全解析const parsePhoneNumber (decrypted) { try { const data JSON.parse(decrypted) if (!data.phoneNumber || !/^\d{11}$/.test(data.phoneNumber)) { throw new Error(Invalid phone number format) } return { phoneNumber: data.phoneNumber, purePhoneNumber: data.purePhoneNumber, countryCode: data.countryCode, watermark: { timestamp: data.watermark.timestamp * 1000, // 转毫秒 appid: data.watermark.appid } } } catch (e) { throw new Error(Parse decrypted data failed: ${e.message}) } }3.6 水印校验的 timestamp 时钟漂移别让服务器时间差毁掉验证微信 watermark.timestamp 是微信服务器时间而你的服务器时间可能有 ±3 秒误差。如果严格校验Math.abs(Date.now() - watermark.timestamp) 3000在服务器时间慢 4 秒时所有请求都会失败。我们的方案是后端记录每次code2Session请求的响应时间server_time计算server_time - response.data.expires_in得到微信服务器的 session_key 过期时间用这个时间反推微信服务器当前时间再与 watermark.timestamp 比较。但更简单有效的方法是放宽校验窗口至 300 秒5 分钟因为 code 本身有效期就是 5 分钟watermark.timestamp 只需保证在 code 有效期内即可。我们在生产环境用 300 秒窗口误判率为 0。3.7 多端登录的 session_key 冲突同一个用户在微信和 QQ 小程序里不能共用 key微信和 QQ 小程序虽然同属腾讯生态但code2Session接口的appid不同返回的session_key完全隔离。曾有个客户把微信小程序的 session_key 存到 Redis 里键名为session:${unionId}结果用户用微信登录后又用 QQ 登录QQ 的 session_key 覆盖了微信的导致后续微信手机号解密全部失败。解决方案是key 名必须包含平台标识。我们约定微信小程序session:wx:${unionId}QQ 小程序session:qq:${unionId}企业微信session:ww:${unionId}并在code2Session请求时根据wx.getSystemInfoSync().platform动态拼接 platform 字段。3.8 button 的 bindgetphonenumber 事件名大小写微信是驼峰不是下划线官方文档写的是bindgetphonenumber但很多开发者习惯写bind-get-phone-number或bind:getPhoneNumber结果事件根本不会触发。微信事件名是严格驼峰的正确bindgetphonenumber错误bind-getphonenumber、bindgetphonenumber少个 n、bindGetPhoneNumber首字母大写我们在 ESLint 里加了自定义规则禁止在 wxml 中出现bind-开头的事件绑定强制用驼峰。3.9 iOS 微信 8.0.42 的 button 渲染 bugfixed 定位导致点击区域偏移在 iOS 微信 8.0.42 版本中如果 button 父容器用了position: fixed微信 WebView 会错误计算点击热区导致用户点击按钮中心实际触发的是右上角空白区域。现象是用户说“我点了但没弹窗”抓包发现根本没有getPhoneNumber事件。临时解决方案给 button 加transform: translateZ(0)强制硬件加速或改用position: absolute。长期方案是升级基础库到 2.28.0微信已修复此 bug。3.10 后端解密的内存泄漏crypto.createDecipheriv 创建的对象不销毁Node.js 的crypto.createDecipheriv返回的 Decipher 对象会占用内存如果频繁创建不销毁在高并发场景下QPS 500会导致内存持续增长。我们在解密函数末尾加了显式销毁const decipher crypto.createDecipheriv(aes-128-cbc, sessionKey, ivBuffer) let decrypted decipher.update(encryptedDataBuffer, binary, utf8) decrypted decipher.final(utf8) decipher.destroy() // 关键释放内存 return decrypted3.11 前端 success 回调的 this 指向箭头函数救不了你在 Page 的 methods 里写onGetPhoneNumber: (e) { // 错误箭头函数 this 指向 window console.log(this.data) // undefined }必须用普通函数onGetPhoneNumber(e) { // 正确 console.log(this.data) // 有值 }或者用 bindbutton bindgetphonenumber{{onGetPhoneNumber.bind(this)}}.../button3.12 灰度发布时的 AB 测试分流用 query 参数控制通道上线新版本前我们用 URL query 参数做灰度https://xxx.com?phone_channelwechat→ 走微信通道https://xxx.com?phone_channelsms→ 强制走短信降级https://xxx.com?phone_channelboth→ 50% 微信 50% 短信在 app.js 的 onLaunch 里读取const options wx.getLaunchOptionsSync() const channel options.query.phone_channel || wechat wx.setStorageSync(phone_channel, channel)这样可以精准对比两种方案的转化率、失败率、用户停留时长数据驱动决策。4. 实操过程与核心环节实现从零开始搭建可上线的手机号获取模块下面展示我们生产环境使用的完整代码已去除业务耦合可直接集成。目录结构按标准小程序规范组织src/ ├── pages/ │ └── auth/ │ ├── index.wxml # 授权页 UI │ ├── index.js # 页面逻辑 │ └── index.wxss # 样式 ├── utils/ │ ├── login.js # wx.login 封装 │ ├── phone.js # 手机号获取主逻辑 │ ├── decrypt.js # 后端解密函数 │ └── monitor.js # 埋点监控 └── app.js # 全局初始化4.1 前端页面逻辑pages/auth/index.js// pages/auth/index.js const phoneUtil require(../../utils/phone.js) const loginUtil require(../../utils/login.js) Page({ data: { hasPhone: false, isGettingPhone: false, traceId: }, onLoad() { // 生成 trace_id 并存入 storage const traceId tr_ Date.now() _ Math.random().toString(36).substr(2, 6) wx.setStorageSync(current_trace_id, traceId) this.setData({ traceId }) // 检查本地缓存是否有有效手机号 const cachedPhone wx.getStorageSync(user_phone) if (cachedPhone cachedPhone.expireTime Date.now()) { this.setData({ hasPhone: true }) return } // 检查是否有未过期的 code 缓存直接尝试解密 const codeCache wx.getStorageSync(phone_code_cache) if (codeCache codeCache.expireTime Date.now()) { this.tryDecryptFromCache(codeCache) } }, // 尝试从缓存 code 解密 tryDecryptFromCache(codeCache) { this.setData({ isGettingPhone: true }) wx.request({ url: /api/v1/phone/bind, method: POST, data: { loginCode: codeCache.loginCode, phoneCode: codeCache.phoneCode, iv: codeCache.iv, encryptedData: codeCache.encryptedData, traceId: this.data.traceId }, success: (res) { if (res.data.code 0) { wx.setStorageSync(user_phone, { phoneNumber: res.data.data.phoneNumber, expireTime: Date.now() 24 * 60 * 60 * 1000 // 24小时 }) this.setData({ hasPhone: true, isGettingPhone: false }) } else { console.error(Decrypt from cache failed:, res.data.msg) this.setData({ isGettingPhone: false }) } }, fail: () { this.setData({ isGettingPhone: false }) } }) }, // button 点击事件 onGetPhoneNumber(e) { if (e.detail.code) { this.setData({ isGettingPhone: true }) // 获取新的 login code loginUtil.getSessionKey().then(loginCode { // 存入缓存有效期 3 分钟 const cacheData { loginCode, phoneCode: e.detail.code, iv: e.detail.iv, encryptedData: e.detail.encryptedData, expireTime: Date.now() 3 * 60 * 1000 } wx.setStorageSync(phone_code_cache, cacheData) // 发起解密请求 wx.request({ url: /api/v1/phone/bind, method: POST, data: { loginCode, phoneCode: e.detail.code, iv: e.detail.iv, encryptedData: e.detail.encryptedData, traceId: this.data.traceId }, success: (res) { if (res.data.code 0) { wx.setStorageSync(user_phone, { phoneNumber: res.data.data.phoneNumber, expireTime: Date.now() 24 * 60 * 60 * 1000 }) this.setData({ hasPhone: true, isGettingPhone: false }) } else { // 解密失败上报监控 phoneUtil.reportError(decrypt_failed, { code: res.data.code, msg: res.data.msg, traceId: this.data.traceId }) this.setData({ isGettingPhone: false }) } }, fail: (err) { phoneUtil.reportError(request_failed, { err: err.errMsg, traceId: this.data.traceId }) this.setData({ isGettingPhone: false }) } }) }).catch(err { console.error(Get session key failed:, err) this.setData({ isGettingPhone: false }) }) } else { // code 为空可能是微信 SDK bug phoneUtil.reportError(empty_code, { errMsg: e.detail.errMsg, traceId: this.data.traceId }) this.setData({ isGettingPhone: false }) } }, // 重新获取按钮 onRetry() { wx.removeStorageSync(phone_code_cache) this.setData({ isGettingPhone: false }) } })4.2 前端工具函数utils/phone.js// utils/phone.js const monitor require(./monitor.js) // 上报错误 const reportError (type, data) { monitor.report(phone_error, { type, ...data, platform: wx.getSystemInfoSync().platform, version: wx.getSystemInfoSync().version, appVersion: wx.getAccountInfoSync().miniProgram.version }) } // 验证 code 格式 const isValidCode (code) { return typeof code string code.length 16 /^[a-zA-Z0-9]$/.test(code) } // 检查是否需要降级 const shouldFallback () { const fallbackMode wx.getStorageSync(fallback_mode) || wechat return fallbackMode sms } module.exports { reportError, isValidCode, shouldFallback }4.3 后端解密逻辑Node.jsexpress// api/phone/bind.js const crypto require(crypto) const axios require(axios) // 从微信后台获取 access_token缓存 2 小时 const getAccessToken async () { const cache await redis.get(wx_access_token) if (cache) return JSON.parse(cache).access_token const res await axios.get( https://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credentialappid${APPID}secret${APPSECRET} ) const { access_token, expires_in } res.data await redis.setex(wx_access_token, expires_in - 100, JSON.stringify({ access_token, expires_in })) return access_token } // 用 code 换取 session_key const getSessionKey async (code) { const res await axios.get( https://api.weixin.qq.com/sns/jscode2session?appid${APPID}secret${APPSECRET}js_code${code}grant_typeauthorization_code ) if (res.data.errcode) { throw new Error(code2Session failed: ${res.data.errmsg}) } return res.data.session_key } // 解密 encryptedData const decryptPhoneNumber (encryptedData, iv, sessionKey) { try { const encryptedDataBuffer Buffer.from(encryptedData, base64) const ivBuffer Buffer.from(iv, base64) if (ivBuffer.length ! 16) { throw new Error(Invalid iv length: ${ivBuffer.length}) } const decipher crypto.createDecipheriv(aes-128-cbc, sessionKey, ivBuffer) let decrypted decipher.update(encryptedDataBuffer, binary, utf8) decrypted decipher.final(utf8) decipher.destroy() // 释放内存 // 解析 JSON const data JSON.parse(decrypted) if (!data.phoneNumber || !/^\d{11}$/.test(data.phoneNumber)) { throw new Error(Invalid phone number format in decrypted data) } // 水印校验 const watermark data.watermark if (!watermark || watermark.appid ! APPID) { throw new Error(Invalid watermark appid) } if (Math.abs(Date.now() - watermark.timestamp) 300000) { // 5分钟窗口 throw new Error(Watermark timestamp out of range) } return { phoneNumber: data.phoneNumber, purePhoneNumber: data.purePhoneNumber, countryCode: data.countryCode } } catch (e) { throw new Error(Decrypt failed: ${e.message}) } } // 主接口 exports.bind async (req, res) { const { loginCode, phoneCode, iv, encryptedData, traceId } req.body try { // 校验参数 if (!loginCode || !phoneCode || !iv || !encryptedData) { return res.json({ code: -1, msg: Missing required parameters }) } // 获取 session_key const sessionKey await getSessionKey(loginCode) // 解密 const phoneData decryptPhoneNumber(encryptedData, iv, sessionKey) // 保存到数据库此处省略 DB 操作 await saveUserPhone(req.userId, phoneData.phoneNumber) res.json({ code: 0, msg: Success, data: phoneData }) } catch (e) { console.error([PhoneBind] traceId${traceId} error:, e) res.json({ code: -1, msg: e.message || Decrypt failed }) } }4.4 完整可运行的 WXML 页面pages/auth/index.wxml!-- pages/auth/index.wxml -- view classcontainer view classheader text classtitle欢迎使用/text text classsubtitle一键获取手机号快速开启服务/text /view view classcontent !-- 已获取手机号 -- view wx:if{{hasPhone}} classsuccess-box image src/assets/icons/checked.png classicon / text classsuccess-text手机号已绑定/text text classphone-number{{userPhone}}/text button classbtn-primary bindtapgoToHome进入首页/button /view !-- 获取中 -- view wx:elif{{isGettingPhone}} classloading-box view classloading-icon/view text classloading-text正在验证身份请稍候.../text /view !-- 获取按钮 -- view wx:else button classget-phone-btn open-typegetPhoneNumber bindgetphonenumberonGetPhoneNumber disabled{{isGettingPhone}} {{isGettingPhone ? 获取中... : 一键获取手机号}} /button view classtip点击后将通过微信安全验证不会泄露您的隐私/view /view !-- 失败重试 -- view wx:if{{!hasPhone !isGettingPhone}} classretry-box text classretry-text获取失败/text button classbtn-outline bindtaponRetry重新获取/button /view /view !-- 底部备案信息 -- view classfooter text classcopyright© 2024 XXX 服务有限公司/text /view /view4.5 样式文件pages/auth/index.wxss/* pages/auth/index.wxss */ .container { display: flex; flex-direction: column; min-height: 100vh; background-color: #f5f5f5; } .header { padding: 40rpx 32rpx 0; text-align: center; } .title { font-size: 36rpx; font-weight: bold; color: #333; line-height: 48rpx; } .subtitle { font-size: 28rpx; color: #666; margin-top: 12rpx; line-height: 40rpx; } .content { flex: 1; padding: 0 32rpx; display: flex; flex-direction: column; justify-content: center; align-items: center; } .success-box { text-align: center;
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。