微信小程序带参二维码获客归因系统设计:scene 参数解析、短码映射与海报合成实战
2026/9/6 8:28:55 网站建设 项目流程

做私域电商的小程序,几乎都绕不开一个需求:每个分销员、每场地推活动、每篇公众号推文、每个门店物料,都要能追踪"这个用户到底从哪个渠道进来的"。微信提供的带参小程序码(wxacode)就是官方归因入口,但 scene 参数只有 32 个字符的限制,直接用会踩一连串坑。本文记录我们在多租户 SaaS 电商系统中落地获客归因链路的完整方案,包含表结构设计、短码映射、海报合成以及 4 个真实踩坑点。

一、需求与整体链路

归因系统要回答三个问题:

  1. 用户扫了谁的码(分销员/门店/渠道)?
  2. 这个码对应什么业务场景(分销绑定、活动落地页、商品详情、拼团)?
  3. 用户后续的注册、下单、复购,如何回溯到最初的渠道?

整体链路如下:

业务后台生成归因任务 → 短码服务生成 6-8 位短码 → 调微信接口生成小程序码 → Canvas/Pillow 合成营销海报 → 用户扫码 → onLoad 解析 scene → 短码反查 → 写入渠道归因关系(带 TTL)→ 注册/下单时读取归因 → 佣金/统计结算

二、scene 参数的 32 字符限制与短码映射

2.1 为什么不能直接把参数塞进 scene

wxacode.getUnlimited接口生成的小程序码,scene 参数最大长度32 个可见字符,且只支持!#$&'()*+,/:;=?@-._~以及字母数字。如果直接拼distributor_id=12345&activity_id=678&scene_type=poster,很容易超长,而且参数里不能出现中文、空格。

我们的方案是:所有归因信息落库,scene 里只放一个短码

2.2 短码表设计

CREATETABLE`qrcode_scene`(`id`BIGINTUNSIGNEDNOTNULLAUTO_INCREMENT,`scene_code`CHAR(8)NOTNULLCOMMENT'短码,base62,8位可容纳3.5万亿组合',`tenant_id`INTUNSIGNEDNOTNULLCOMMENT'商户ID(多租户隔离)',`biz_type`TINYINTNOTNULLCOMMENT'业务类型:1分销员 2活动 3商品 4拼团 5门店',`biz_id`INTUNSIGNEDNOTNULLCOMMENT'业务主键,如分销员ID',`page`VARCHAR(128)NOTNULLDEFAULT'pages/index/index'COMMENT'落地页路径',`expire_at`INTUNSIGNEDNOTNULLDEFAULT0COMMENT'过期时间戳,0为永久',`scan_count`INTUNSIGNEDNOTNULLDEFAULT0,`created_at`INTUNSIGNEDNOTNULL,PRIMARYKEY(`id`),UNIQUEKEY`uk_scene_code`(`scene_code`),KEY`idx_tenant_biz`(`tenant_id`,`biz_type`,`biz_id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='小程序码scene短码映射';

短码用 base62(0-9a-zA-Z)生成,8 位空间足够,且全是合法字符。生成时要注意防碰撞

importrandom,string ALPHABET=string.digits+string.ascii_letters# 62 charsdefgen_scene_code(length=8):# 用密码学随机源,避免被枚举遍历whileTrue:code=''.join(random.SystemRandom().choice(ALPHABET)for_inrange(length))# INSERT ... 唯一键冲突则重试,最多 5 次try:db.execute("INSERT INTO qrcode_scene(scene_code, tenant_id, biz_type, biz_id, page, created_at) ""VALUES (%s,%s,%s,%s,%s,%s)",(code,tenant_id,biz_type,biz_id,page,now_ts))returncodeexceptDuplicateKeyError:continue

踩坑点 1:不要用自增 ID 转 62 进制当短码。自增 ID 可预测,竞品或黑产遍历 scene 就能爬光你所有分销员的推广码,甚至伪造扫码关系。随机短码 + 唯一键重试才是稳妥做法。

2.3 小程序端解析

// app.js 或落地页 onLoadonLoad(options){// 扫码进入时 scene 是 encodeURIComponent 编码过的constsceneStr=options.scene?decodeURIComponent(options.scene):'';if(sceneStr){wx.request({url:`${API_BASE}/qrcode/resolve`,data:{scene:sceneStr},success:(res)=>{const{biz_type,biz_id,page,redirect_params}=res.data;// 1. 异步上报扫码事件(不阻塞跳转)this.reportScan(res.data);// 2. 写入本地归因缓存wx.setStorageSync('attr_scene',{biz_type,biz_id,ts:Date.now()});// 3. 按业务类型跳转/绑定this.handleAttribution(biz_type,biz_id,redirect_params);}});}}

后端 resolve 接口做三件事:短码反查、扫码计数 +1、返回业务上下文。反查走uk_scene_code唯一索引,单行查询,QPS 压力很小。

三、归因关系的绑定与 TTL 策略

扫码不等于转化。用户今天扫了分销员 A 的码,可能三天后才下单。归因关系需要持久化,但又不能"一次扫码终身绑定",否则分销员之间会恶意抢人。

CREATETABLE`user_attribution`(`id`BIGINTUNSIGNEDNOTNULLAUTO_INCREMENT,`user_id`INTUNSIGNEDNOTNULL,`tenant_id`INTUNSIGNEDNOTNULL,`biz_type`TINYINTNOTNULL,`biz_id`INTUNSIGNEDNOTNULLCOMMENT'归因目标,如分销员ID',`scene_code`CHAR(8)NOTNULL,`source`TINYINTNOTNULLDEFAULT1COMMENT'1扫码 2分享卡片 3搜索',`expire_at`INTUNSIGNEDNOTNULLCOMMENT'归因有效期截止时间',`created_at`INTUNSIGNEDNOTNULL,PRIMARYKEY(`id`),UNIQUEKEY`uk_user_biztype`(`user_id`,`biz_type`),KEY`idx_expire`(`expire_at`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;

关键规则:

  • 唯一键(user_id, biz_type):同一用户在同一业务类型下只保留一条归因,新扫码按"覆盖规则"决定是否更新(我们的策略是:未产生过订单的归因允许新渠道覆盖,已产生订单的锁定 30 天);
  • TTL 字段 + 定时任务:分销归因有效期设 30 天(可按商户配置),每天凌晨扫idx_expire清理过期记录,避免无限堆积;
  • 下单时实时判断:订单结算佣金时不直接信任历史归因,而是重新查user_attribution并校验expire_at > NOW(),防止用过期关系结算佣金。

踩坑点 2:扫码时用户可能还没注册(无 user_id)。小程序wx.login是静默的,但授权手机号/注册是后置的。正确做法是先把归因写本地 Storage + 以匿名 openid 落库,等用户注册/授权成功时,用 openid 把匿名归因"转正"到 user_id。漏掉这一步,未注册扫码用户的归因会全部丢失。

四、小程序码生成与海报合成

4.1 调微信接口的注意事项

getUnlimited返回的是图片二进制流(image/jpeg),不是 JSON:

importrequestsdefget_wxacode(access_token,scene,page,env_version='release'):url=f"https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}"resp=requests.post(url,json={"scene":scene,"page":page,"check_path":False,# 开发版页面未发布时设 False,否则报错"env_version":env_version,"width":430,"line_color":{"r":31,"g":58,"b":104},"is_hyaline":False},timeout=10)content_type=resp.headers.get("Content-Type","")if"image"notincontent_type:# 出错时返回 JSON,如 {"errcode":41030,"errmsg":"invalid page"}raiseRuntimeError(f"wxacode error:{resp.text}")returnresp.content# jpeg bytes

踩坑点 3:access_token 必须集中管理。多实例服务各自刷新 token 会互相顶掉(微信只认最新的),导致偶发 40001 错误。正确做法是把 token 放 Redis 集中缓存,提前 5 分钟过期,用分布式锁保证全局只有一个实例刷新。

4.2 海报合成:服务端 Pillow 方案

分销海报一般是"背景图 + 小程序码 + 分销员昵称/头像"。我们在服务端用 Pillow 合成,避免各手机端 Canvas 兼容性问题:

fromPILimportImage,ImageDraw,ImageFontimportiodefcompose_poster(bg_path,qrcode_bytes,nickname,avatar_bytes=None):poster=Image.open(bg_path).convert("RGB")qr=Image.open(io.BytesIO(qrcode_bytes)).convert("RGBA")qr=qr.resize((280,280),Image.LANCZOS)# 小程序码贴到底部居中位置(设计稿固定坐标)poster.paste(qr,(235,1080),qr)draw=ImageDraw.Draw(poster)font=ImageFont.truetype("fonts/SourceHanSansCN-Bold.otf",36)# 昵称超长省略name=nicknameiflen(nickname)<=10elsenickname[:9]+"…"draw.text((60,1390),f"{name}邀请您进店选购",font=font,fill=(51,51,51))buf=io.BytesIO()poster.save(buf,format="JPEG",quality=90)returnbuf.getvalue()

踩坑点 4:中文字体和 emoji。Linux 服务器默认字体不含中文,不手动加载.ttf/.otf字体会画成方框;昵称里的 emoji 在 Pillow 里基本无法渲染,要在写入前用正则过滤掉 emoji 和特殊符号,否则海报上出现乱码方块。

五、数据统计与佣金结算的口径

归因数据最终要服务两件事:渠道效果统计和分销佣金。

  • 统计口径分离:扫码数(scan_count)、访问 UV、注册数、下单数、GMV 是漏斗的五层,必须分层记录,不能只存最终订单。我们用一张qrcode_scan_log(scene_code、openid、ts、ip 脱敏)做扫码明细,注册和下单事件再各自关联归因,漏斗转化率才能算准;
  • 佣金只认真实支付:佣金结算以"已支付且过售后期"的订单为准,归因关系在订单创建时快照(存 scene_code + biz_id 到订单表),后续归因关系变化不影响历史订单;
  • 防刷:同一 openid 短时间内对同一 scene 的大量扫码只计一次 UV;异常高频扫码(如单码日扫数千次且无注册)进风控队列。

六、小结

带参小程序码归因系统的核心就三句话:scene 里只放短码,业务信息全部落库;归因关系带 TTL、按事件快照;token 集中管理、海报服务端合成。这套方案在我们多租户电商系统里支撑了分销推广、地推物料、活动海报三类场景,单表千万级短码、日均百万级扫码下查询和反查都很稳。后续如果要做公众号文章、视频号直播间的跨渠道归因,可以在biz_type上继续扩展,链路不用动。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询