月度经营会上的三个数字
《潮汐消消》的月度经营会上,发行负责人报「本月流水 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 Store | 30% | 按案例口径,不适用小企业计划 |
| Google Play | 30% | 海外安卓主渠道 |
| 国内安卓渠道 | 30%–50%,按 40% 计 | 不同渠道有差异,案例取平均值 |
| TapTap | 15% | 低分成渠道,但量级有限 |
先按单一渠道算一遍:消消 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 的口径 。
常见错误与边界
- 把流水当收入做预算。 987 万流水扣掉渠道分成后到手约 743 万,按流水排投放预算会高估可支配现金。
- 用总流水倒推内购与广告占比。 两条线的结算周期与扣减项不同,倒推出来的占比既不能对账,也不能指导决策。
- 把广告收入再扣一次商店分成。 广告由广告平台结算,不经过应用商店,重复扣减会低估到手收入。
- 用单渠道分成率估算所有平台。 消消的平台结构里 iOS 30%、TapTap 15%,按 40% 一刀切会低估到手约 34.7 万元/月。
- 认为广告占比高就一定是广告驱动。 消消广告占 38.3%,但内购仍是第一大收入线;分类要看增量由谁承接。
- 在高 ARPPU 产品里提高广告密度。 付费率 6.5%、ARPPU ¥420 的产品,广告只占 3.2%,强行加广告位会把高价值用户推向竞品。
- 把掉单、退款、欺诈混成一个「损耗率」。 三者分别发生在支付回调、售后与风控环节,合并后无法定位。
- 让广告位进入核心玩法决策点。 用看广告替代关键奖励,会把本来愿意付费的用户训练成等待免费。
- 按档位面值判断付费深度。 案例中有 ¥648 档位,但主力是 ¥128;只看最高档会误判用户的实际支付结构。
- 把未成年人限额当成运营策略。 限额是支付链路的硬约束,¥648 单笔在未成年人场景必须拦截,不能靠客服事后退款。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。