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

2026-09-25 01:20:12 1 次浏览
微信小程序scene场景值渠道追踪二维码数据分析前端开发

9 月 3 日晚上十点半,运营的小蒋在群里甩过来一句:「传单印了两万张,线上核销只有三百多,扫码用户你们到底有没有记渠道?」我翻了一圈数据库,答不上来——因为我们只记了 scene 的原始值,而三种二维码的 scene 拼法不一样,数据早搅成了一锅粥。那天之后我花了两周,把公司的渠道追踪从零到一补齐,踩的坑比想象中多得多。这篇文章把这些坑原样摊开,包括两个到现在还没彻底解决的问题。

场景值机制:小程序凭什么知道用户从哪来

先说底层。微信小程序的启动参数是一套「入口描述」体系:用户从任何入口(扫码、搜一搜、卡片、好友分享)进小程序,微信客户端都会在启动时把入口信息打包成一份 options 交给开发者。这份 options 里有三个关键字段:

小程序场景值渠道追踪

字段 含义 举例
scene 场景值,固定枚举 1047(扫小程序码)、1011(扫二维码)、1035(公众号菜单)
query 启动参数,键值对 ch=dt01&sid=88
path 启动页面路径 pages/index/index

场景值(scene)和启动参数(query)是两码事,很多新手第一次都混了。scene 是微信定义的枚举数字,回答「用户用什么方式进来的」;query 是你自己塞进去的参数,回答「具体是哪张码」。 归因必须两个都看:scene 用来分大类(扫线下物料、扫商品码、分享卡片),query 里的渠道参数用来落到具体的某一批投放。

这份 options 在生命周期里出现两次:App.onLaunch 和 App.onShow 各给一次,冷启动和热切换的值会不同。用户在微信里把小程序切到后台、逛了一圈再切回来,onShow 会拿到一份新的 options。我们第一版就栽在这里——只在 onLaunch 记了一次,结果每天有大约 12% 的访问记录不到入口。后来才换成 wx.getEnterOptionsSync(),这个 API 返回的永远是「本次进入」的入口信息,不受冷热启动影响。

整个归因链路长这样:

flowchart LR
    A[用户扫线下二维码] --> B{二维码类型}
    B -->|小程序码 withScene| C[scene 字段装渠道参数]
    B -->|普通链接二维码| D[query 字段装渠道参数]
    C --> E[客户端取 options]
    D --> E
    E --> F[解析并上报]
    F --> G[服务端归因表落库]
    G --> H[渠道报表]

三种二维码,三种取参方式

线下投放常见的码有三种,取参方式完全不同,这是整套追踪里最容易搞混的部分。

小程序码(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 里只做初始化,取入口统一放到独立函数
// 每次进入页面都调一次,别只挂在启动回调里
function reportEntry() {
  // getEnterOptionsSync 拿到的永远是「本次进入」的 options
  const opts = wx.getEnterOptionsSync();
  const scene = opts.query && opts.query.scene ? decodeURIComponent(opts.query.scene) : "";

  if (scene) {
    // 小程序码的 scene 是我们自己拼的 k=v&k2=v2,URL 编码后塞进去的
    // 解析前必须先 decodeURIComponent,否则连 & 号都是 %26
    const params = parseScene(scene);
    if (params.ch) {
      // 渠道号存在才上报,减少垃圾数据
      reportChannel(params.ch, opts.scene, "qrcode");
    }
  } else if (opts.query && opts.query.ch) {
    // 普通链接二维码走的是 query 原样透传,不用再解析
    reportChannel(opts.query.ch, opts.scene, "link");
  } else {
    // 没有 ch 参数的,记一个「未知入口」,别直接丢掉
    // 后面排查数据缺失时,这个兜底记录帮过我们大忙
    reportChannel("_none", opts.scene, "unknown");
  }
}

// scene 解析器:按 & 拆键值对,比 decode 全串再 split 更稳
function parseScene(raw) {
  const out = {};
  // scene 里理论上不该有额外空白,trim 一下防御物料端手误
  raw.split("&").forEach(function (kv) {
    if (!kv) return;
    const idx = kv.indexOf("=");
    // 没有等号的片段直接忽略,避免 key 为 undefined
    if (idx < 1) return;
    out[kv.slice(0, idx)] = kv.slice(idx + 1);
  });
  return out;
}

有个细节必须强调:opts.query.scene 在部分入口下是已经被微信解码过的,你如果再 decode 一次,遇到参数值里本来就有 % 的场景会解出乱码。稳妥做法是先判断字符串里有没有 % 再决定要不要 decode,或者干脆约定渠道参数值只用字母数字,绕开整个编码问题。我们选了后者,省事。

编码代码:生成小程序码那一半

环境:Node.js 18,axios 调微信接口,access_token 走缓存中间件。

// 渠道号白名单,防止拼 scene 时被塞进脏字符
// 白名单比黑名单省心,正则一行搞定
const CH_PATTERN = /^[A-Za-z0-9]{3,8}$/;

// 拼小程序码的 scene,总长不能超 32 个可见字符
function buildScene(params) {
  const pairs = [];
  for (const k of Object.keys(params)) {
    // 值做 encodeURIComponent,键名我们自己保证是纯字母
    // 编码后长度可能变长,必须用编码后的长度去算总量
    const v = encodeURIComponent(String(params[k]));
    pairs.push(k + "=" + v);
  }
  const scene = pairs.join("&");
  // 超长直接抛错,宁可在生成时失败,不要等印出去才发现截断
  if (scene.length > 32) {
    throw new Error("scene 超长: " + scene.length + " 字符,需压缩参数");
  }
  return scene;
}

async function makeQrcode(ch) {
  // 渠道号先过白名单,这段校验在上线第二周拦下过一次脏数据
  if (!CH_PATTERN.test(ch)) throw new Error("非法渠道号 " + ch);
  const scene = buildScene({ ch: ch });
  // page 必须是已发布的页面路径,不能带 / 开头,也不能带参数
  const resp = await wxApi.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 日那次翻车值得记一笔。当时有个渠道想带活动编号,ch=dt01&act=zhuanti0901 拼出来 27 个字符,看着没超,但 act 的值里有中文,encodeURIComponent 之后膨胀到 40 多个字符。接口当时没做长度断言,微信直接返回了错误码,打包脚本卡了两小时。所以上面代码里那个超长抛错,是拿一次真金白银的加班换来的。

渠道归因表怎么设计

服务端这边一张表就够了,关键是把「入口大类」和「渠道明细」拆成两个字段,报表才好做。

字段 类型 说明
open_id_hash varchar(64) open_id 做哈希后存,避免敏感信息直存
channel_code varchar(16) query 里的 ch 参数,归因的主键
scene_type int 微信场景值枚举,1047/1011/1035 等
entry_type varchar(8) qrcode/link/share/unknown 四类
entered_at datetime 进入时间,建索引
is_new_user tinyint 当天是否新用户,报表要用

上报链路的时序:

sequenceDiagram
    participant U as 用户
    participant M as 小程序客户端
    participant S as 归因服务
    participant D as 数据库
    U->>M: 扫描带参小程序码
    M->>M: getEnterOptionsSync 取 scene 并解析
    M->>S: POST /report 上报 ch 与场景值
    S->>S: 校验渠道号白名单与去重
    S->>D: 写入 entry_log 并更新渠道日汇总
    S-->>M: 200 返回

两个设计取舍说一下。去重窗口我们定的是同一用户 30 分钟内重复上报只记一次,这个数字是拿 9 月上旬三天的日志回放试出来的——太短会把「扫码进店→退出→再进」记成两次,太长又会漏掉真实的多次访问。另一个是 channel_code 建了唯一索引,配合渠道字典表做外键校验,字典里不存在的渠道号进库时会被标脏,第二天人工核对,而不是直接拒掉。

上线两周的实测数据

以下均为我们自建监测口径(entry_log 表统计,样本约 3.1 万次进入),不代表任何第三方平台数据。

上线后的报表大概是这样:

渠道 扫码进入 新用户占比 次日留存
传单 dt01 4120 61% 18%
店内立牌 dp02 1870 34% 26%
异业合作 yh03 940 72% 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 个百分点,查了一下午才定位到。

还没解决的问题

坦白说有两个。一是 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

场景值 · 渠道追踪 · 小程序码 · 二维码 · 数据分析 · 归因

🤖
本内容由 AI 辅助生成,经人工校对审核;部分素材、资料来源于公开网络,仅作个人观点分享与交流使用,无任何商业侵权意图。若内容、图片、文字涉及您的合法著作权、版权权益,请联系本人,核实后将第一时间删除、修改相关内容。