一、流量主开通与门槛
1.1 开通条件
| 条件 | 要求 |
|---|---|
| 主体类型 | 个人、企业、政府、媒体等均可申请 |
| 累计独立访客 | 开通前累计 UV 达到 1000 |
| 违规记录 | 无严重违规,未处于封禁期 |
| 类目限制 | 医疗、金融等特殊类目需额外资质 |
流量主入口在「微信公众平台 - 流量主」。UV 的统计口径是去重后的访问用户数,同一用户多次访问只算一次,且不同小程序之间的 UV 不能合并计算。也就是说,靠刷量凑 1000 UV 走不通,必须是真实用户。
1.2 收益与结算
| 项目 | 说明 |
|---|---|
| 结算周期 | 次月结算上月收益 |
| 打款门槛 | 单次结算满 100 元才打款 |
| 结算方式 | 微信零钱或银行卡,企业主体需对公账户 |
| 扣税 | 个人主体按劳务报酬代扣代缴 |
| 数据延迟 | 收益数据 T+1 更新,结算数据延迟更久 |
收益数据的两个口径要分清:流量主后台展示的是预估收益,结算单才是最终金额,两者因无效流量过滤会有差异,做财务核算时以后者为准。
二、广告位类型与选型
2.1 组件式广告
组件式广告由基础库托管加载,开发者只需在 WXML 中声明并处理回调:
<!-- banner:页面内固定位置 -->
<ad
unit-id="adunit-xxxxxxxxxxxx"
ad-type="banner"
ad-theme="white"
binderror="onAdError"
bindload="onAdLoad"
/>
<!-- 视频广告:ad-intervals 为轮播间隔秒数,取值 30 到 120 -->
<ad
unit-id="adunit-yyyyyyyyyyyy"
ad-type="video"
ad-intervals="60"
binderror="onAdError"
/>
<!-- 格子广告:grid-count 控制格子数量 -->
<ad
unit-id="adunit-zzzzzzzzzzzz"
ad-type="grid"
grid-count="5"
grid-opacity="0.9"
/>
unit-id 必须在流量主后台创建广告位后替换,测试时使用官方提供的测试广告位 ID,不要用线上 ID 联调,否则会污染真实数据。
2.2 广告位规划
| 位置 | 类型 | 频次 | 说明 |
|---|---|---|---|
| 首页底部 | banner | 常驻 | 曝光高但单价低,适合打底 |
| 列表插入 | 原生模板 | 每 8 到 10 条插一条 | 与内容融合,点击率通常高于 banner |
| 功能解锁 | 激励视频 | 用户主动触发 | eCPM 最高,必须给明确奖励 |
| 页面切换 | 插屏 | 每 2 到 3 次切换一次 | 打扰感强,慎用 |
| 结算页 | 格子广告 | 常驻 | 展示多个广告位,适合工具类 |
2.3 数量与限制
| 限制项 | 说明 |
|---|---|
| 同屏数量 | 同一页面建议不超过 1 个 banner |
| 插屏频次 | 官方建议单用户每日不超过 3 次 |
| 激励视频 | 必须用户主动点击,禁止自动播放 |
| 位置要求 | 不得遮挡功能按钮,不得做诱导点击 |
| 未成年保护 | 不得向未成年人投放不适宜广告 |
三、激励视频接入与异常处理
3.1 单例封装
激励视频是变现效率最高的广告类型,但它的 API 是回调式的,必须封装成 Promise 才好用:
// utils/rewarded-ad.js
let adInstance = null;
function getAd(adUnitId) {
// 同一 adUnitId 复用实例,重复创建会报错
if (adInstance) return adInstance;
if (!wx.createRewardedVideoAd) return null;
adInstance = wx.createRewardedVideoAd({ adUnitId });
adInstance.onError((err) => {
console.error('激励视频错误', err.errCode, err.errMsg);
});
return adInstance;
}
function showRewardedAd(adUnitId) {
const ad = getAd(adUnitId);
if (!ad) return Promise.reject(new Error('当前基础库不支持激励视频'));
return new Promise((resolve, reject) => {
const onClose = (res) => {
ad.offClose(onClose);
// isEnded 为 true 表示完整观看,此时才能发放奖励
resolve({ rewarded: !!(res && res.isEnded) });
};
ad.onClose(onClose);
ad.show().catch(() => {
// 首次加载失败时先 load 再重试一次
ad.load()
.then(() => ad.show())
.catch((err) => {
ad.offClose(onClose);
reject(err);
});
});
});
}
onClose 回调必须用 offClose 解绑,否则页面反复进入会累积多个监听,一次关闭触发多次发奖。
3.2 错误码处理
| errCode | 含义 | 处理方式 |
|---|---|---|
| 1000 | 后端错误 | 稍后重试,提示用户 |
| 1001 | 参数错误 | 检查 adUnitId 是否正确 |
| 1002 | 广告单元已关闭 | 去流量主后台开启 |
| 1003 | 触发频次限制 | 降低展示频率 |
| 1004 | 广告无填充 | 稍后重试,不阻塞业务 |
| 1005 | 未开通流量主或广告位 | 检查开通状态 |
| 1006 | 广告组件已销毁 | 重新创建实例 |
| 1007 | 广告不可用 | 降级为无奖励路径 |
| 1008 | 系统错误 | 稍后重试 |
所有失败分支都必须有降级路径:广告加载失败时,要么直接发放奖励,要么明确告诉用户「当前暂无广告,请稍后再试」,不能卡在加载动画上。
3.3 频次控制
const SHOW_LIMIT = 5;
const KEY = 'rewarded_count';
function canShow() {
const today = new Date().toDateString();
const cache = wx.getStorageSync(KEY) || {};
if (cache.date !== today) return true;
return cache.count < SHOW_LIMIT;
}
function markShown() {
const today = new Date().toDateString();
const cache = wx.getStorageSync(KEY) || {};
const count = cache.date === today ? cache.count + 1 : 1;
wx.setStorageSync(KEY, { date: today, count });
}
除了本地限制,还要接受平台侧返回的 1003 错误,两者结合才不会出现「用户点了没反应」的情况。
四、banner 与插屏的工程细节
4.1 插屏广告
let interstitial = null;
function showInterstitial(adUnitId) {
// 同一 adUnitId 在页面生命周期内只创建一次实例
if (!interstitial) {
interstitial = wx.createInterstitialAd({ adUnitId });
interstitial.onError((err) => console.warn('插屏广告错误', err));
}
interstitial.show().catch(() => {
// 未加载成功时静默忽略,不阻塞业务流程
});
}
插屏广告要选择自然节点触发:关卡结束、表单提交成功、页面返回。在页面 onShow 里无条件弹插屏是留存杀手。
4.2 布局注意
| 问题 | 处理方式 |
|---|---|
| 广告未加载时留白 | 容器给固定高度,避免布局跳动 |
| 广告高度变化 | 在 bindload 回调后再展示下方内容 |
| 底部安全区 | banner 上方预留 env(safe-area-inset-bottom) |
| 随页面滚动 | 需要常驻时用 position: fixed 定位 |
| 深色模式 | ad-theme 随 wx.getAppBaseInfo().theme 切换 |
4.3 原生模板广告
<ad-custom unit-id="adunit-xxx" /> 的样式由后台模板决定,容器必须给定高度。它比 banner 更贴合内容流,点击率通常更高,但加载失败时不会自动收起,需要自行做兜底隐藏。
五、广告与体验的平衡
5.1 设计原则
- 激励视频只在用户有明确动机时出现:复活、解锁、加速、领券
- 不遮挡主要操作按钮,不做误触设计
- 插屏只在自然节点出现,不在启动页和首屏出现
- 提供「关闭广告」的替代路径,付费去广告是最自然的转化钩子
- 同时监控收益与留存,收益上升而留存下降时必须立即回退
5.2 A/B 实验
// 按用户维度稳定分组,避免同一用户频繁切换策略
function getAdGroup(openid) {
const hash = [...openid].reduce((acc, ch) => acc + ch.charCodeAt(0), 0);
return hash % 100 < 50 ? 'A' : 'B'; // A 组对照组,B 组实验组
}
分组必须稳定:同一个用户每次都进同一组,否则实验数据会互相污染。观察指标如下:
| 指标 | 含义 | 观察方式 |
|---|---|---|
| eCPM | 千次曝光收益 | 流量主后台 |
| 人均曝光 | 曝光次数除以活跃用户数 | 自建埋点 |
| 广告点击率 | 点击除以曝光 | 后台加埋点 |
| 次日留存 | 是否因广告下降 | 留存看板 |
| 人均时长 | 是否因广告下降 | 使用时长看板 |
六、CPS 联盟与分销体系
6.1 跳转实现
// 跳转到合作小程序并携带归因参数
function jumpToPartner(item, extra) {
wx.navigateToMiniProgram({
appId: item.appId,
path: item.path,
extraData: {
// 归因信息会透传给目标小程序
source: 'plumephp',
uid: extra.uid,
channel: extra.channel
},
envVersion: 'release',
fail: () => wx.showToast({ title: '跳转失败', icon: 'none' })
});
}
wx.navigateToMiniProgram 每次跳转都会弹确认框,同一会话内免确认的次数有限,不要在一个页面里连续做多次跳转。
6.2 归因与结算
| 环节 | 要点 |
|---|---|
| 归因参数 | 通过 extraData 透传 uid 与渠道号 |
| 订单回流 | 由合作方回传订单与佣金,需提供对账接口 |
| 结算周期 | 常见 T+1 到 T+30,取决于联盟规则 |
| 风控 | 禁止自买自返与刷单,会被扣量甚至封号 |
| 选品 | 优先高佣、低退货、与用户画像匹配的品类 |
CPS 的本质是把流量卖给转化能力更强的合作方,必须自建对账:合作方回传的订单号要与自己的跳转记录逐条比对,否则扣量、丢单都无从发现。
七、付费会员与虚拟支付合规
7.1 合规红线
| 场景 | iOS | Android |
|---|---|---|
| 虚拟商品(会员、道具、充值) | 禁止在小程序内购买 | 需开通虚拟支付权限 |
| 实物商品 | 允许(微信支付) | 允许 |
| 内容付费(课程、专栏) | 视为虚拟,禁止 | 需虚拟支付 |
| 引导外部支付 | 禁止诱导至 App 或 H5 支付 | 同样限制 |
违规代价是功能下架甚至封禁。不要用「跳转客服发链接」「引导加群付款」这类方式绕过,风控识别能力远超预期。
7.2 端能力判断
function canUseVirtualPay() {
const { platform } = wx.getDeviceInfo();
// 虚拟支付仅在 Android 且已开通权限时可用
return platform === 'android' && hasVirtualPayPermission();
}
function renderPayEntry() {
if (canUseVirtualPay()) {
this.setData({ payType: 'virtual' });
} else {
// iOS 上隐藏支付入口,仅保留会员权益说明
this.setData({ payType: 'none' });
}
}
7.3 会员定价
| 策略 | 说明 |
|---|---|
| 连续包月 | 转化最高,需明确自动续费规则与取消入口 |
| 阶梯年卡 | 提升客单价,配合首月优惠 |
| 权益分层 | 免费看广告、付费去广告是最自然的转化钩子 |
| 试看试用 | 降低决策门槛,但要把到期提醒做足 |
八、变现数据看板与 ROI 分析
8.1 核心指标
| 指标 | 公式 | 用途 |
|---|---|---|
| eCPM | 广告收入除以曝光次数再乘 1000 | 衡量广告位质量 |
| ARPU | 总收入除以活跃用户数 | 判断用户价值 |
| ARPPU | 付费收入除以付费用户数 | 衡量付费深度 |
| LTV | ARPU 乘以生命周期 | 与获客成本比较 |
| ROI | LTV 除以 CAC | 大于 1 才可持续 |
| 填充率 | 有广告返回的请求除以总请求 | 判断流量是否被浪费 |
8.2 埋点上报
function trackAd(event, payload) {
wx.reportEvent('ad_event', {
event, // show / click / close / error
adUnitId: payload.adUnitId,
position: payload.position,
errCode: payload.errCode || 0,
ts: Date.now()
});
}
上报字段要能对齐流量主后台的口径,否则两端数据永远对不上:后台按「广告位」统计,自建埋点也必须带 adUnitId 与位置标识,否则无法定位是哪个位置拉低了 eCPM。
8.3 优化闭环
每周同时看 eCPM 与留存两条曲线:eCPM 涨而留存跌,说明变现过度,要减少插屏或降低频次;eCPM 跌而留存稳,说明广告位或类型需要调整,可以尝试原生模板替换 banner。每次只改一个变量并留足观察期,否则无法归因。完整的指标口径与埋点设计可以对照小程序数据分析与埋点 ;如果变现依赖分享裂变带来的新用户,还需要结合小程序分享与增长 设计分享路径,让获客成本真正被广告收入覆盖。
九、总结
小程序的变现体系可以概括成三层:广告打底、CPS 增量、会员收口。广告是最容易启动的一层,流量主 1000 UV 即可开通,但要清楚 banner 只适合打底、原生模板的点击率通常更高、激励视频的 eCPM 最高且必须给明确奖励。接入时最大的工程坑在于实例复用与降级:激励视频与插屏都要按 adUnitId 单例复用,onClose 必须解绑,所有失败分支都要有可用的替代路径。
CPS 与会员是提高单用户价值的两条路。CPS 要把归因参数透传清楚并自建对账,会员则要严守合规红线:iOS 上的虚拟支付是绝对禁区,任何绕过方式都会带来下架风险。无论走哪条路,衡量标准都是同一组指标:eCPM、ARPU、ARPPU、LTV 与 ROI,其中 ROI 大于 1 才是可持续的生意。
最后强调两件事。第一,广告与体验的平衡靠数据而不是感觉,A/B 分组要按用户稳定划分,并同时监控收益与留存。第二,涉及交易与权益的环节都要回到服务端复算,端上只负责展示与交互,具体支付链路可参考小程序微信支付实战 。变现不是把广告塞满页面,而是在用户价值与商业价值之间找到可持续的那个平衡点。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。