拓冰建站拓冰建站
首页 / 资讯中心 / 正文

二维码扫进来不知道用户从哪来:小程序场景值与渠道参数追踪实战

二维码扫进来不知道用户从哪来小程序场景值与渠道参数追踪实战适用读者做过带参二维码投放、被运营追着问「这批扫码用户到底从哪个渠道来的」的小程序开发者正在设计渠道归因表的后端以及所有被scene参数坑过的同行。TL;DRscene 与 query 是两套独立参数归因必须同时记录。小程序码 scene 有 32 字符编码后长度限制生成端必须校验编码后长度。统一用wx.getEnterOptionsSync()取入口信息避免 onLaunch 漏记热启动。归因表将入口大类与渠道明细拆分为 scene_type 和 channel_code 两个字段。9 月 3 日晚上十点半运营的小蒋在群里甩过来一句「传单印了两万张线上核销只有三百多扫码用户你们到底有没有记渠道」我翻了一圈数据库答不上来——因为我们只记了scene的原始值而三种二维码的 scene 拼法不一样数据早搅成了一锅粥。那天之后我花了两周把公司的渠道追踪从零到一补齐踩的坑比想象中多得多。这篇文章把这些坑原样摊开包括两个到现在还没彻底解决的问题。场景值机制小程序凭什么知道用户从哪来先说底层。微信小程序的启动参数是一套「入口描述」体系用户从任何入口扫码、搜一搜、卡片、好友分享进小程序微信客户端都会在启动时把入口信息打包成一份options交给开发者。这份options里有三个关键字段字段含义举例scene场景值固定枚举1047扫小程序码、1011扫二维码、1035公众号菜单query启动参数键值对chdt01sid88path启动页面路径pages/index/index场景值scene和启动参数query是两码事很多新手第一次都混了。scene 是微信定义的枚举数字回答「用户用什么方式进来的」query 是你自己塞进去的参数回答「具体是哪张码」。归因必须两个都看scene 用来分大类扫线下物料、扫商品码、分享卡片query 里的渠道参数用来落到具体的某一批投放。这份 options 在生命周期里出现两次App.onLaunch和App.onShow各给一次冷启动和热切换的值会不同。用户在微信里把小程序切到后台、逛了一圈再切回来onShow会拿到一份新的 options。我们第一版就栽在这里——只在onLaunch记了一次结果每天有大约 12% 的访问记录不到入口。后来才换成wx.getEnterOptionsSync()这个 API 返回的永远是「本次进入」的入口信息不受冷热启动影响。整个归因链路长这样小程序码 withScene普通链接二维码用户扫线下二维码二维码类型scene 字段装渠道参数query 字段装渠道参数客户端取 options解析并上报服务端归因表落库渠道报表三种二维码三种取参方式线下投放常见的码有三种取参方式完全不同这是整套追踪里最容易搞混的部分。小程序码withScene。走wxacode.getUnlimited接口生成参数通过scene字段携带进小程序后出现在 options 的query.scene里是 URL 编码过的字符串。限制scene 最大 32 个可见字符只支持数字、大小写英文以及!#$()*,/:;?-._~这几个符号。中文直接传不了等号拼接要自己解析。普通带参二维码。你自己生成一个指向已配置域名 H5 的二维码用户扫了以后经由微信的「扫普通链接二维码打开小程序」能力跳进小程序原始 URL 的查询串会进query不用编码也不用截断能装的东西多得多。公众号/文章里的码。这种要靠 scene 值区分入口大类渠道参数同样走自己的 query。三种方式的对比维度小程序码 scene普通链接二维码 query分享卡片容量32 字符上限受 URL 长度限制很宽松可自定义 path 和参数编码必须 URL 编码 自定义解析原样透传直接是对象适合线下物料、批量化投放已有 H5 体系的项目线上裂变生成方式服务端调微信接口任意二维码库前端拼 path我们的传单投放最后选了小程序码一版物料印了 7 个渠道每个渠道 5 万张scene里只装了一个ch参数加三位渠道号。解析代码客户端这一半环境微信小程序基础库 2.21原生框架TypeScript 也行这里用 JS 方便看。// App.onLaunch 里只做初始化取入口统一放到独立函数// 每次进入页面都调一次别只挂在启动回调里functionreportEntry(){// getEnterOptionsSync 拿到的永远是「本次进入」的 optionsconstoptswx.getEnterOptionsSync();constsceneopts.queryopts.query.scene?decodeURIComponent(opts.query.scene):;if(scene){// 小程序码的 scene 是我们自己拼的 kvk2v2URL 编码后塞进去的// 解析前必须先 decodeURIComponent否则连 号都是 %26constparamsparseScene(scene);if(params.ch){// 渠道号存在才上报减少垃圾数据reportChannel(params.ch,opts.scene,qrcode);}}elseif(opts.queryopts.query.ch){// 普通链接二维码走的是 query 原样透传不用再解析reportChannel(opts.query.ch,opts.scene,link);}else{// 没有 ch 参数的记一个「未知入口」别直接丢掉// 后面排查数据缺失时这个兜底记录帮过我们大忙reportChannel(_none,opts.scene,unknown);}}// scene 解析器按 拆键值对比 decode 全串再 split 更稳functionparseScene(raw){constout{};// scene 里理论上不该有额外空白trim 一下防御物料端手误raw.split().forEach(function(kv){if(!kv)return;constidxkv.indexOf();// 没有等号的片段直接忽略避免 key 为 undefinedif(idx1)return;out[kv.slice(0,idx)]kv.slice(idx1);});returnout;}有个细节必须强调opts.query.scene在部分入口下是已经被微信解码过的你如果再 decode 一次遇到参数值里本来就有%的场景会解出乱码。稳妥做法是先判断字符串里有没有%再决定要不要 decode或者干脆约定渠道参数值只用字母数字绕开整个编码问题。我们选了后者省事。编码代码生成小程序码那一半环境Node.js 18axios调微信接口access_token 走缓存中间件。// 渠道号白名单防止拼 scene 时被塞进脏字符// 白名单比黑名单省心正则一行搞定constCH_PATTERN/^[A-Za-z0-9]{3,8}$/;// 拼小程序码的 scene总长不能超 32 个可见字符functionbuildScene(params){constpairs[];for(constkofObject.keys(params)){// 值做 encodeURIComponent键名我们自己保证是纯字母// 编码后长度可能变长必须用编码后的长度去算总量constvencodeURIComponent(String(params[k]));pairs.push(kv);}constscenepairs.join();// 超长直接抛错宁可在生成时失败不要等印出去才发现截断if(scene.length32){thrownewError(scene 超长: scene.length 字符需压缩参数);}returnscene;}asyncfunctionmakeQrcode(ch){// 渠道号先过白名单这段校验在上线第二周拦下过一次脏数据if(!CH_PATTERN.test(ch))thrownewError(非法渠道号 ch);constscenebuildScene({ch:ch});// page 必须是已发布的页面路径不能带 / 开头也不能带参数constrespawaitwxApi.post(/wxa/getwxacodeunlimit,{scene:scene,page:pages/index/index,check_path:false,});// 返回的是图片二进制流落 OSS 后把 ch 存进文件名方便对账return{buffer:resp.data,key:qr/ch/Date.now().png};}8 月 20 日那次翻车值得记一笔。当时有个渠道想带活动编号chdt01actzhuanti0901拼出来 27 个字符看着没超但act的值里有中文encodeURIComponent之后膨胀到 40 多个字符。接口当时没做长度断言微信直接返回了错误码打包脚本卡了两小时。所以上面代码里那个超长抛错是拿一次真金白银的加班换来的。渠道归因表怎么设计服务端这边一张表就够了关键是把「入口大类」和「渠道明细」拆成两个字段报表才好做。字段类型说明open_id_hashvarchar(64)open_id 做哈希后存避免敏感信息直存channel_codevarchar(16)query 里的 ch 参数归因的主键scene_typeint微信场景值枚举1047/1011/1035 等entry_typevarchar(8)qrcode/link/share/unknown 四类entered_atdatetime进入时间建索引is_new_usertinyint当天是否新用户报表要用上报链路的时序数据库归因服务小程序客户端用户数据库归因服务小程序客户端用户扫描带参小程序码getEnterOptionsSync 取 scene 并解析POST /report 上报 ch 与场景值校验渠道号白名单与去重写入 entry_log 并更新渠道日汇总200 返回两个设计取舍说一下。去重窗口我们定的是同一用户 30 分钟内重复上报只记一次这个数字是拿 9 月上旬三天的日志回放试出来的——太短会把「扫码进店→退出→再进」记成两次太长又会漏掉真实的多次访问。另一个是channel_code建了唯一索引配合渠道字典表做外键校验字典里不存在的渠道号进库时会被标脏第二天人工核对而不是直接拒掉。上线两周的实测数据以下均为我们自建监测口径entry_log 表统计样本约 3.1 万次进入不代表任何第三方平台数据。上线后的报表大概是这样渠道扫码进入新用户占比次日留存传单 dt01412061%18%店内立牌 dp02187034%26%异业合作 yh0394072%12%三个数字推翻了运营原来的两个假设传单量大但留存垫底的是异业合作那批用户是冲合作方奖品来的店内立牌量不大留存反而不错。9 月 18 日的运营会上小蒋原话是「早半年有这张表上季度的预算就不会那么分了」。追踪这件事的价值不在技术在于让投放决策有依据。踩坑清单全是眼泪scene 的 32 位是「编码后」的长度。中文、、经encodeURIComponent后一个字符能膨胀到 9 个字节。生成端必须拿编码后的串算长度而不是拼完的原始参数。onLaunch里的 options 不是万能的。热启动场景下用户可能从别的入口再次进入只记 onLaunch 会漏。统一用wx.getEnterOptionsSync()并且在onShow里也调一次、按时间戳取更新的那份。小程序码和普通二维码的 scene 值不同。扫小程序码场景值是 1047扫普通链接二维码跳小程序是 1011报表上如果只按渠道号分组不区分类别两类数据会缠在一起排查起来非常费劲。check_path: false的坑。调试期用check_path: false生成码指向的页面还没发布用户扫码会提示页面不存在。我们 8 月 28 日放过一批测试码到门店被店长拍照发群里质疑「码是假的」。发布页面之前任何码都不要流出。测试码和正式码要分渠道号段。我们划了test前缀做测试段报表 SQL 里直接过滤。有一次测试数据混进正式报表把当周新用户占比拉高了 9 个百分点查了一下午才定位到。排查实战一次渠道数据对不上的完整定位过程光有归因表还不够数据对不上时怎么查才是这套系统真正值钱的地方。分享一次真实的排查过程正好把前面踩的坑串起来。某天上午十点运营小蒋又来了「传单渠道今天进入量怎么比昨天同期少了 40%」我第一反应是别慌先分三步走。第一步先看上报曲线定位异常时间点。直接查 entry_log 表按小时分组看传单渠道的进入量对比前三天同时段。SELECTDATE_FORMAT(entered_at,%Y-%m-%d %H:00)AShour_bucket,COUNT(*)ASenter_cntFROMentry_logWHEREchannel_codedt01ANDentered_atDATE_SUB(NOW(),INTERVAL3DAY)GROUPBYhour_bucketORDERBYhour_bucket;跑出来发现异常不是从零点开始的而是从当天早上 7 点整突然断崖式下跌前一天同时段每小时还有 300 左右今天直接掉到 180。时间点很干净说明不是全天性的投放问题更像某个时刻之后上报链路出了问题。第二步对比微信公众平台「访问分析」判断是上报缺失还是真实下降。打开公众平台的「统计 → 访问分析」看「扫小程序码」这个场景的实时趋势。如果微信侧也同步下跌那是真实流量少了如果微信侧正常、只有我们报表跌那就是上报丢了。当时对比下来微信侧「扫小程序码」的实时曲线是平稳的只有我们 entry_log 在跌。结论明确流量没少是上报丢了问题出在我们自己这一侧。第三步检查当天是否有代码发布重点看 getEnterOptionsSync 的调用时机。翻发布记录当天凌晨 1 点确实上线了一个版本改动里恰好动了入口上报逻辑。回看代码问题出在热启动场景// 有问题的写法只在 onLaunch 里取一次App({onLaunch(){// 冷启动时能拿到 query但热启动时这里可能是旧的或空的this.globalData.entrywx.getEnterOptionsSync();},});// 修正后的写法每次 onShow 都重新取按时间戳取最新App({onShow(){constoptswx.getEnterOptionsSync();// 热启动时基础库可能返回空 query必须做兜底constsceneopts.queryopts.query.scene?decodeURIComponent(opts.query.scene):;if(scene){reportChannel(parseScene(scene).ch,opts.scene,qrcode);}else{// 空 query 一律走 _none 兜底别静默丢弃reportChannel(_none,opts.scene,unknown);}},});第四步定位根因——某版本基础库热启动返回空 query。光改代码还不够得搞清楚为什么之前没暴露。查了微信基础库的更新日志和社区帖子发现某个版本的基础库在热启动小程序从后台切回前台时getEnterOptionsSync()返回的query是空的只有scene有值。而我们新版代码恰好把「取入口」从onShow挪到了onLaunch只取一次冷启动没问题热启动就全走了_none兜底分支——报表里传单渠道的_none占比从平时的 3% 飙到了 43%。-- 验证看异常时段 _none 兜底占比是否异常升高SELECTDATE_FORMAT(entered_at,%Y-%m-%d %H:00)AShour_bucket,entry_type,COUNT(*)ASenter_cntFROMentry_logWHEREchannel_codedt01ANDentered_at2026-09-24 06:00:00ANDentered_at2026-09-24 10:00:00GROUPBYhour_bucket,entry_typeORDERBYhour_bucket,entry_type;结果_none占比确实异常。修复方案是双保险一是onShow里每次重新取getEnterOptionsSync()二是对空query但scene有值的情况尝试用scene反查渠道虽然拿不到ch参数但至少能归到「扫小程序码」这个大类不至于全丢。上线后观察两天传单渠道上报量恢复正常_none占比回落到 3% 以内。这次排查最大的收获是数据对不上时先分「上报缺失」和「真实下降」再查代码变更最后落到基础库行为顺序不能乱。而_none兜底记录就是为这种时刻准备的——没有它你连「丢了多少」都说不清。还没解决的问题坦白说有两个。一是 iOS 微信部分版本下从相册识别二维码进入时偶发 scene 解析出空串复现率大概千分之三报了微信开放社区没人回目前只能靠_none兜底记录观察。二是普通链接二维码依赖「已配置的域名跳转规则」改一次规则要全量重发物料这对线下投放太不友好了还没想到更好的办法。这两个坑如果有同行踩过并有解法评论区求指点。参考与延伸wx.getEnterOptionsSync 官方文档https://developers.weixin.qq.com/miniprogram/dev/api/base/app/wx.getEnterOptionsSync.html小程序码 getUnlimited 接口文档含 scene 限制说明https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/qr-code/getUnlimitedQRCode.html场景值枚举清单https://developers.weixin.qq.com/miniprogram/dev/framework/app-service/scene.html扫普通链接二维码打开小程序配置指引https://developers.weixin.qq.com/miniprogram/introduction/qrcode.html场景值 · 渠道追踪 · 小程序码 · 二维码 · 数据分析 · 归因
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门