《移动游戏产品的架构·商业化与发行工程》1.1 收入结构:内购、广告与混合变现的分界

《潮汐消消》月流水约 987 万元,内购约 609 万元、广告约 378 万元,两条收入线的付款方、结算方与波动来源完全不同。本节把流水拆成到手收入,给出内购、广告与混合变现的分类规则、四条商业化边界,并附一份可直接运行、可与报表对账的收入结构判定代码。

月度经营会上的三个数字

《潮汐消消》的月度经营会上,发行负责人报「本月流水 987 万」,财务负责人报「本月到手 743 万」,增长负责人报「ARPU 3.1 元」。三个数字来自同一款产品、同一个自然月,却没有人能直接拿其中一个解释另外两个。

差异不在计算,而在口径:

  • 987 万是含渠道分成的流水,内购与广告加总。
  • 743 万是扣掉内购渠道分成后的到手收入。
  • 3.1 元是总流水除以 MAU,不是除以 DAU,也不是除以付费用户。

把三件事混在一起讨论,常见的结局是:用流水做预算、用到手做投放、用 ARPU 做版本目标,三套动作互相打架。本节先把收入线分清,再把「流水」与「到手」拆开,最后划出四条商业化边界。1.2 负责分母,1.3 负责把口径固化成可运行的基线。

三条收入线:谁付钱、谁结算、随什么波动

内购、广告、混合变现不是三种「商业模式口号」,而是三条结算链路。先把付款方和结算方写清楚,后面的比例才有意义。

收入线付款方结算方波动来源记账要点
内购玩家应用商店或渠道商代收,再按分成结算版本内容、活动节奏、档位设计、付费深度流水与到手分列,退款与掉单单独扣减
广告广告主广告平台或聚合 SDK,按月结算曝光量、填充率、eCPM、广告主预算季节性通常按平台结算净额入账,不再扣商店分成
混合变现玩家与广告主两套结算链路并行上述因素叠加,且两条线互相影响必须分账,不能用总流水倒推

三条线的区别不只是「钱从哪来」。内购是先有需求再有收入,收入跟版本内容强相关;广告是先有流量再有收入,收入跟用户时长和广告库存强相关。混合变现的产品同时经营两种库存:付费用户的支付意愿,以及免费用户的注意力。两种库存会互相挤压,这是后面所有边界的根源。

按案例设定,消消的月流水约 987 万元,其中内购约 609 万元、广告约 378 万元。这个结构决定了后文所有边界讨论的起点:内购约为广告的 1.6 倍,两条线都不能被忽略。

分界不是比例,是「谁承担增长成本」

判断一款产品是内购驱动、广告驱动还是混合,不能只看某个月的占比。占比会随活动、版本、买量结构变化。更稳的判据是:新增用户带来的边际收入,主要由哪条线承接。

消消的自然量占 55%、买量占 45%,月新增 42 万。新用户里能付费的是少数,月付费率只有 2.8%,大部分新用户的回收要靠广告。这就是「谁承担增长成本」的实际含义:买来的用户先被广告接住,再慢慢转化成付费用户。

判据消消方舟
月流水≈987 万元≈2,820 万元
内购 / 广告≈609 万 / ≈378 万≈2,730 万 / ≈90 万
按绝对额折算占比61.7% / 38.3%96.8% / 3.2%
月付费率2.8%6.5%
ARPPU¥68¥420

这张表说明,「混合变现」不是「两种都做一点」,而是两条线都足以影响月度收入曲线的形状。消消的广告占近四成,任何一个月 eCPM 波动都会直接反映到流水;内购占比高的产品则相反,广告位的主要作用不是收入,而是给免费用户一个继续玩的理由。

渠道分成:流水与到手之间的一道固定折扣

内购流水不是到手收入。案例的结算口径是:Apple App Store 30%,Google Play 30%,国内安卓渠道 30%–50%(案例按平均 40% 计),TapTap 15%。小企业计划 15% 不适用于本案例。

渠道分成率说明
Apple App Store30%按案例口径,不适用小企业计划
Google Play30%海外安卓主渠道
国内安卓渠道30%–50%,按 40% 计不同渠道有差异,案例取平均值
TapTap15%低分成渠道,但量级有限

先按单一渠道算一遍:消消 609 万内购全部走国内安卓 40% 分成,到手 365.4 万;广告 378 万不经过商店,按净额入账;合计 743.4 万。这就是财务口中「743 万」的来源。真实结构是四个平台加权,1.3 会把权重加进去:加权分成 34.3%,到手 778.1 万,比单渠道试算多 34.7 万。差额来自 iOS 与海外 Google Play 的 30%、以及 TapTap 的 15% 拉低了整体分成率。

这里有一个容易犯的错误:把「加权分成 34.3%」和「到手率」混为一谈。34.3% 只作用于内购,广告不参与分成;如果按总流水算,实际扣减比例是 21.2%。两个百分比都正确,但回答的是不同问题。

把分类写成代码

下面这份实现只做三件事:按渠道扣分成、按内购占比分类、按付费率与 ARPPU 判断广告能否承担主收入。它不计算税款,也不处理退款,那两项属于 1.2 的口径范围。

// file: src/biz/revenue-mix.ts
// tier: L1
// verify: node --test test/biz/revenue-mix.test.ts

export type Channel = "app_store" | "google_play" | "android_cn" | "taptap";

export const CHANNEL_CUT: Readonly<Record<Channel, number>> = {
  app_store: 0.30,
  google_play: 0.30,
  android_cn: 0.40,
  taptap: 0.15,
};

export type MixModel = "iap_led" | "hybrid" | "ad_led";

export interface RevenueInput {
  readonly grossIap: number;
  readonly grossAd: number;
  readonly channel: Channel;
}

export interface MixReport {
  readonly grossIap: number;
  readonly grossAd: number;
  readonly grossTotal: number;
  readonly netIap: number;
  readonly netAd: number;
  readonly netTotal: number;
  readonly iapShare: number;
  readonly adShare: number;
  readonly model: MixModel;
  readonly effectiveCut: number;
}

export interface BoundaryInput {
  readonly payRate: number;
  readonly arppu: number;
  readonly adShare: number;
}

export interface BoundaryVerdict {
  readonly adsAsPrimary: boolean;
  readonly reasons: readonly string[];
}

export const MIX_BOUNDARY = {
  payRateMax: 0.03,
  arppuMax: 100,
  adShareCeiling: 0.10,
} as const;

export function channelCut(channel: Channel): number {
  const cut = CHANNEL_CUT[channel];
  if (cut === undefined) {
    throw new RangeError(`unknown channel: ${String(channel)}`);
  }
  return cut;
}

function assertAmount(name: string, value: number): void {
  if (!Number.isFinite(value) || value < 0) {
    throw new RangeError(`${name} must be a finite non-negative number, got ${value}`);
  }
}

export function classifyMix(iapShare: number): MixModel {
  if (!Number.isFinite(iapShare) || iapShare < 0 || iapShare > 1) {
    throw new RangeError(`iapShare must be in [0, 1], got ${iapShare}`);
  }
  if (iapShare >= 0.9) {
    return "iap_led";
  }
  if (iapShare >= 0.2) {
    return "hybrid";
  }
  return "ad_led";
}

export function mixReport(input: RevenueInput): MixReport {
  assertAmount("grossIap", input.grossIap);
  assertAmount("grossAd", input.grossAd);
  const grossTotal = input.grossIap + input.grossAd;
  if (grossTotal <= 0) {
    throw new RangeError("grossTotal must be positive");
  }
  const cut = channelCut(input.channel);
  const netIap = input.grossIap * (1 - cut);
  const netAd = input.grossAd;
  const netTotal = netIap + netAd;
  const iapShare = input.grossIap / grossTotal;
  return {
    grossIap: input.grossIap,
    grossAd: input.grossAd,
    grossTotal,
    netIap,
    netAd,
    netTotal,
    iapShare,
    adShare: 1 - iapShare,
    model: classifyMix(iapShare),
    effectiveCut: (grossTotal - netTotal) / grossTotal,
  };
}

export function boundaryVerdict(input: BoundaryInput): BoundaryVerdict {
  if (!Number.isFinite(input.payRate) || input.payRate < 0 || input.payRate > 1) {
    throw new RangeError(`payRate must be in [0, 1], got ${input.payRate}`);
  }
  if (!Number.isFinite(input.arppu) || input.arppu < 0) {
    throw new RangeError(`arppu must be a non-negative number, got ${input.arppu}`);
  }
  if (!Number.isFinite(input.adShare) || input.adShare < 0 || input.adShare > 1) {
    throw new RangeError(`adShare must be in [0, 1], got ${input.adShare}`);
  }
  const reasons: string[] = [];
  const lowPayRate = input.payRate < MIX_BOUNDARY.payRateMax;
  const lowArppu = input.arppu < MIX_BOUNDARY.arppuMax;
  if (lowPayRate) {
    reasons.push("pay_rate<3%");
  }
  if (lowArppu) {
    reasons.push("arppu<100");
  }
  const adsAsPrimary = lowPayRate && lowArppu;
  if (!adsAsPrimary && input.adShare > MIX_BOUNDARY.adShareCeiling) {
    reasons.push("ad_share>10%");
  }
  return { adsAsPrimary, reasons };
}

export function formatMixReport(report: MixReport): string {
  const iapPct = (report.iapShare * 100).toFixed(1);
  const cutPct = (report.effectiveCut * 100).toFixed(1);
  return `model=${report.model} iap=${iapPct}% gross=${report.grossTotal} net=${report.netTotal.toFixed(0)} cut=${cutPct}%`;
}

读一段输出

把消消的 609 万内购与 378 万广告、按国内安卓 40% 分成喂进去,输出是:

model=hybrid iap=61.7% gross=9870000 net=7434000 cut=24.7%

四个字段各有明确含义:

  • model=hybrid 表示内购占比落在 20% 到 90% 之间。分类阈值是工程约定,不是行业标准;换产品要重新定义。
  • iap=61.7% 是按绝对额折算的内购占比,不是事实表里单独一行的比例。
  • net=7434000 是扣掉内购分成、保留广告净额后的到手收入。
  • cut=24.7% 是「总流水减到手」除以总流水,只反映内购部分的分成,不要拿它去乘广告收入。

这段输出的价值在于可对账:同一个月换任何渠道,只有 net 与 cut 变化,iap 与 model 不变。如果两个月的 model 突然从 hybrid 跳到 ad_led,先查内购收入是否漏记,而不是先改广告策略。

边界一:广告能不能成为主收入

广告能否承担主收入,取决于用户盘的付费能力,而不是当月的收入比例。案例采用两条判据:

条件判据结论
月付费率 < 3% 且 ARPPU < ¥100广告具备承担主收入的用户基础可以提高广告位密度,但要单独监控内购转化
月付费率 ≥ 3% 或 ARPPU ≥ ¥100广告只能是补充广告收入占比越过 10% 就要重新评估
广告位进入核心玩法决策点与占比无关,直接越界用广告替代关键奖励,会把付费用户训练成等待免费

消消的 2.8% 与 ¥68 同时满足两条,所以广告可以承担更大的收入份额;反过来,付费率与 ARPPU 都高的产品一旦把广告扩到 10% 以上,就会被用户理解为「付了钱还要看广告」。这里的「可以」是可行性判断,不是建议把广告做成第一大收入线。

边界二:档位与付费深度

指标消消方舟
商品档位¥6 / ¥18 / ¥30 / ¥68 / ¥128¥30 / ¥68 / ¥128 / ¥328 / ¥648
主力档位¥18(占订单 46%)¥128(占订单 38%)
平均客单价¥14.5¥182
月订单量42 万笔15 万笔
客单价 / ARPPU≈0.21≈0.43

档位数量不等于付费深度。判断边界看客单价除以 ARPPU:0.21 意味着收入被大量小额订单摊薄,0.43 意味着订单更集中在高价位段。比值越低,新增中低档位越可能只是把同一批付费用户拆成更多订单。

这个比值决定加档位的效果。如果客单价远低于 ARPPU,说明收入依赖少数大额订单,新增中低档位只是把同一批付费用户拆成更多订单,流水不涨、订单量涨;如果客单价接近 ARPPU,新增档位才可能真正改变付费结构。

边界三:退款、掉单与欺诈的记账位置

指标消消方舟
掉单率0.35%0.20%
退款率1.2%0.8%
支付欺诈率0.08%0.03%

这三项不是收入,是收入的减项。常见错误是把掉单计入流水,再单独列一笔「损失」,同一笔钱被算两次。消消 609 万内购按 1.2% 退款率折算约 7.3 万元/月,按 0.08% 欺诈率折算约 0.49 万元/月;这两笔应该从到手收入里扣,而不是从流水里再加回去。

掉单与退款的发生环节也不同。掉单在支付回调,用户可能已经扣款但订单未确认;退款在售后,是已经确认的收入被退回;欺诈在风控,是订单本身不该被确认。三者合并成一个「损耗率」,就无法判断该修支付链路、客服流程还是风控规则。

边界四:未成年人充值的硬上限

案例背景采用国内防沉迷口径:未成年人仅可在周五、周六、周日及法定节假日 20:00–21:00 游玩 1 小时;8–16 周岁单次充值不超过 ¥50、每月不超过 ¥200,16–18 周岁单次不超过 ¥100、每月不超过 ¥400。

这不是转化优化问题,而是支付链路必须在创单前拦截的硬约束。消消的主力档位 ¥18 在限额内;案例中的 ¥648 档位同时超过 16–18 周岁的 ¥100 与 8–16 周岁的 ¥50 上限,未成年人场景必须先做实名与年龄判定,再决定是否允许该档位。高客单价产品还应加入人脸抽检。把限额交给客服事后退款,既不合规,也会把退款率推高。

对照:方舟为什么不能照抄

方舟内购占 96.8%,广告只占 3.2%;付费率 6.5%、ARPPU ¥420,一条广告位对月流水的边际贡献远小于一次版本活动。

消消相反:付费率 2.8%、ARPPU ¥68,单靠内购撑不起 987 万月流水,378 万广告是结构性收入,不是边角料。

代价与取舍

混合变现的代价是两套系统。消消接入 14 个第三方 SDK,其中广告聚合、广告归因与激励视频服务至少占三个。每一个 SDK 都带来隐私合规、包体、初始化耗时与崩溃面的增量。另一个代价是决策复杂度:广告位密度提高会压低内购转化,内购档位加深会压低广告观看意愿,两条线的最优点不在同一处。选择混合变现,等于接受「每个月都要重新找平衡」,而不是找到一个一劳永逸的比例。

与同章的关系

本节建立了三条收入线的分类、流水与到手的换算,以及四条边界。1.2 会把本节反复出现的 MAU、付费用户、ARPPU 与 eCPM 定义清楚,说明为什么同一个 3.1 元可以被三个部门算出三个版本。1.3 把本节的三条收入线、渠道分成与两个案例固化成一个可运行的基线,任何一次口径改动都会在基线里留下差异。

小结

  • 内购、广告、混合变现是三条结算链路,付款方、结算方与波动来源都不同。
  • 消消月流水约 987 万元,内购约 609 万元、广告约 378 万元;对照案例约为 2,820 万元,内购占 96.8%。
  • 分类看增量由谁承接,不看单月比例;20% 与 90% 是工程约定,不是行业标准。
  • 渠道分成只作用于内购,广告按净额入账;34.3% 的分成率不等于 34.3% 的到手折扣。
  • 掉单、退款、欺诈是收入的减项,不能先计入流水再单独列损失。
  • 广告能否承担主收入,看付费率与 ARPPU 两条判据,不看当月广告占比。
  • 未成年人充值是支付链路的硬约束,案例中的 ¥648 档位必须在创单前拦截。

阅读导航:上一节:目录与学习路径 · 下一节:1.2 ARPU、ARPPU、付费率与 eCPM 的口径 。

常见错误与边界

  1. 把流水当收入做预算。 987 万流水扣掉渠道分成后到手约 743 万,按流水排投放预算会高估可支配现金。
  2. 用总流水倒推内购与广告占比。 两条线的结算周期与扣减项不同,倒推出来的占比既不能对账,也不能指导决策。
  3. 把广告收入再扣一次商店分成。 广告由广告平台结算,不经过应用商店,重复扣减会低估到手收入。
  4. 用单渠道分成率估算所有平台。 消消的平台结构里 iOS 30%、TapTap 15%,按 40% 一刀切会低估到手约 34.7 万元/月。
  5. 认为广告占比高就一定是广告驱动。 消消广告占 38.3%,但内购仍是第一大收入线;分类要看增量由谁承接。
  6. 在高 ARPPU 产品里提高广告密度。 付费率 6.5%、ARPPU ¥420 的产品,广告只占 3.2%,强行加广告位会把高价值用户推向竞品。
  7. 把掉单、退款、欺诈混成一个「损耗率」。 三者分别发生在支付回调、售后与风控环节,合并后无法定位。
  8. 让广告位进入核心玩法决策点。 用看广告替代关键奖励,会把本来愿意付费的用户训练成等待免费。
  9. 按档位面值判断付费深度。 案例中有 ¥648 档位,但主力是 ¥128;只看最高档会误判用户的实际支付结构。
  10. 把未成年人限额当成运营策略。 限额是支付链路的硬约束,¥648 单笔在未成年人场景必须拦截,不能靠客服事后退款。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《移动游戏产品的架构·商业化与发行工程》附录 C 商业化与发行评审清单
  2. 《移动游戏产品的架构·商业化与发行工程》附录 B 双案例商业化事实表
  3. 《移动游戏产品的架构·商业化与发行工程》附录 A 商业化与发行术语速查