《独立开发者的商业指南·收入与财务工程》1.2 收入模型组合:主收入与第二收入线

收入模型不是收入清单。本节把主收入线与第二收入线放进同一个组合,计算经常性收入占比、收入线集中度与加权组合评分,再用 LedgerLite、Tidecast、Nord Studio 三个案例说明什么时候值得加第二收入线,以及加线之前必须先满足哪些条件。

以下情境为教学假设,人物与数字不代表经核实的真实案例。

独立开发者最常见的收入结构不是「一条线」,而是「一条主线加一条补线」:白天做咨询,晚上维护订阅产品;或者一边卖课,一边接定制项目。补线的动机通常是「多一条收入更安全」,但收入线的数量本身不产生安全,结构才产生安全。

这一节把收入组合拆成三个可计算的量:经常性收入占比、收入线集中度、加权组合评分。三个量都不能单独使用,但放在一起,能回答一个具体问题:现在这条第二收入线,是在分散风险,还是在分散注意力?

先定义两个词。主收入线是当前贡献最大、决定现金安全的那条线;第二收入线是除此之外、已经产生真实收入的那条线。只有计划、没有收入的线不算第二收入线,只能算待验证的想法。

三个案例的组合结构完全不同:

项LedgerLiteTidecastNord Studio
主收入线订阅(100%)买断销售(100%)咨询(72.7%)
第二收入线无无产品订阅(27.3%)
年收入$240,240$138,171$154,320
收入性质经常性一次性混合
客户/平台集中度前 5 家 31%,最大一家 9%Steam 80%前 3 家 48%

LedgerLite 和 Tidecast 都只有一条收入线,但风险来源不同。LedgerLite 有 260 家付费客户,前 5 家占 31%,最大一家 9%,客户集中度可控;Tidecast 的 Steam 渠道占 80%,平台规则变化会直接冲击收入。Nord Studio 有两条线,但咨询占 72.7%,第二收入线还不足以改变现金结构。

这说明「有没有第二收入线」和「组合是否健康」是两个问题。前者看收入清单,后者看每条线的性质、占比和相互依赖。

经常性收入占比

经常性收入占比的公式很简单:

经常性收入占比 = 经常性收入 ÷ 总收入 × 100%

按这个口径,三个案例的结果是:

案例经常性收入总收入占比
LedgerLite$240,240$240,240100.0%
Tidecast$0$138,1710.0%
Nord Studio$42,120$154,32027.3%

Nord Studio 的产品订阅是 90 家 × 39 美元 × 12 个月 = 42,120 美元;咨询收入是 110 小时 × 85 美元 × 12 个月 = 112,200 美元。42,120 ÷ 154,320 = 27.3%,与案例事实表一致。

经常性占比高不代表收入一定安全。LedgerLite 的 100% 经常性建立在月流失 1.5%、GRR 83%、NRR 102% 的基础上;如果月流失升到 5%,同样的 100% 经常性会变成一条快速漏水的管道。反过来,Tidecast 的 0% 经常性也不等于没有资产:它的 IP、代码和 9,600 份销量的长尾仍然可以在续作和授权中变现。

经常性占比真正衡量的是「下一期收入有多少已经锁定」。它适合做跨案例对比,不适合单独作为经营目标。

收入线集中度

收入线集中度用赫芬达尔-赫希曼指数(HHI)计算:

HHI = Σ(第 i 条收入线占比)²

结果统一换算成基点,0 表示无限分散,10000 表示只有一条线。三个案例的结果是:

案例收入线占比HHI(bps)解读
LedgerLite订阅 100%10000单线,但客户分散
Tidecast买断 100%10000单线,且平台集中
Nord Studio咨询 72.7%、产品 27.3%6031两条线,但主线仍占七成

Nord Studio 的 HHI 计算是 0.727² + 0.273² = 0.5285 + 0.0745 = 0.6030,换算成基点约 6031。它比单线案例低,但离「分散」还很远:咨询一条线仍贡献近四分之三收入。

这里要特别小心:收入线 HHI 不是客户 HHI。LedgerLite 只有一条收入线,HHI 是 10000,但它有 260 家客户,前 5 家只占 31%;Tidecast 也只有一条线,Steam 却占 80%。同样是一个「10000」,风险的含义完全不同。客户集中度要看前五大客户占比、最大客户占比和平台依赖,这些在第 7 章展开,见 7.2 集中度风险 。

加权组合评分

只看经常性占比会高估订阅,只看 HHI 会低估单线但客户分散的生意。把三个维度加权,得到一个 0–10000 的评分:

组合评分 = (4000 × 经常性占比
         + 3000 × (10000 − HHI)
         + 3000 × 加权毛利率) ÷ 10000

三个权重是教学参数,分别代表经常性、分散度和毛利质量;权重之和必须是 10000 bps。加权毛利率按各线年收入加权:

加权毛利率 = Σ(收入线年收入 × 该线毛利率) ÷ 总收入

代入三个案例:

案例经常性占比分散度(10000−HHI)加权毛利率组合评分
LedgerLite10000084.2%6526
Tidecast0072.0%2160
Nord Studio2729396984.4%4816

LedgerLite 的评分最高,因为它在经常性上拿满分;它的分散度是 0,因为只有一条收入线,但客户层面的分散没有进入这个公式。Tidecast 的评分最低,因为经常性和分散度都是 0,毛利率 72.0% 也只拿到部分分数。Nord Studio 居中:经常性 27.3%,分散度 39.7%,毛利率 84.4%,三项都不极端。

评分的作用不是排名,而是暴露结构。LedgerLite 的 6526 告诉你「这条线很经常,但只有一条」;Nord Studio 的 4816 告诉你「有两条线,但主线仍占七成」;Tidecast 的 2160 告诉你「一次性收入 + 单平台,估值倍数低是有原因的」。

买断线的毛利率在评分里用 72.0%,而不是利润表里的 100%。原因是组合评分要比较「标价收入中有多少能留下来」,平台分成和退款必须算进去;利润表里的 100% 是因为这些减项已经在收入前扣除。两种口径都成立,但不能混用。

什么时候值得加第二收入线

加第二收入线不是「多一条收入」那么简单。它至少要同时满足四个条件:

条件判断问题反例
主线稳定主线的流失、续费、回本是否已经可预测?主线月流失 5%,却去开新课
共享基础第二条线是否复用客户、数据、品牌或分发?做会计 SaaS 的人去卖游戏
边际成本可控新增一条线的支持、维护、合规成本是否算过?产品免费引流,支持成本翻倍
需求重叠现有客户是否已经在问同一个问题?为了「看起来更专业」硬加功能

Nord Studio 的产品线符合「共享基础」:咨询客户是品牌官网客户,产品订阅是同一批小企业的工具需求,品牌和关系可以复用。但它的产品线只占 27.3%,说明复用还没有形成规模。要让它成为真正的第二收入线,产品 MRR 需要继续增长,而不是靠咨询补贴。

LedgerLite 不适合现在加第二条线。它的订阅主线还在增长,NRR 102%、月流失 1.5%、回本周期 2.2 个月,单位经济健康;此时分心做第二条线,边际收益低于把现有 260 家客户服务好。第二收入线的信号通常来自现有客户的重复请求,而不是来自「收入不够」的焦虑。相关判断见 第二产品信号 。

Tidecast 的第二收入线更可能是 IP 授权或续作,而不是订阅。它的资产是 IP 组合和 9,600 份销量的受众,授权报价是保底 60,000 美元/年 + 8% 净收入,3 年最低保证 180,000 美元;这比把买断游戏改成订阅更贴近现有能力。服务转产品的路径见 服务 + 产品 。

一个实用的判据是:如果第二收入线在前 6 个月不能复用主线的至少一个核心资产(客户名单、分发渠道、数据、品牌),它大概率是一条新生意,而不是一条补线。新生意需要新的获客、新的支持和新的学习曲线,独立开发者通常没有这么多注意力。

另一个判据是现金。第二收入线在起量前通常是净现金流出;如果主线的现金安全垫不足 6 个月,加线会让整体更脆弱,而不是更安全。现金安全垫的计算见第 8 章,本节只提示这个约束。

把组合写成代码

下面的 revenue-mix.ts 把三个量固化成函数:recurringShare 算经常性占比,lineShares 算各线占比,concentration 算 HHI,portfolioScore 按权重算综合评分。所有金额用整数美分,比率用基点,非法输入返回 Result 错误。

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

import { type Cents, sum } from "../core/money.ts";
import { type Bps, divide } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";

export type RevenueLine = {
  name: string;
  annualCents: Cents;
  recurring: boolean;
  grossMarginBps: Bps;
};

export type LineShare = {
  name: string;
  shareBps: Bps;
};

export type ScoreWeights = {
  recurring: Bps;
  diversification: Bps;
  margin: Bps;
};

export const DEFAULT_WEIGHTS: ScoreWeights = {
  recurring: 4000,
  diversification: 3000,
  margin: 3000,
};

function validate(lines: RevenueLine[]): Result<Cents> {
  if (lines.length === 0) {
    return fail("at least one revenue line is required");
  }
  for (const line of lines) {
    if (line.name.trim() === "") {
      return fail("revenue line name must not be empty");
    }
    if (!Number.isInteger(line.annualCents) || line.annualCents < 0) {
      return fail(`invalid annual amount for ${line.name}`);
    }
    if (
      !Number.isInteger(line.grossMarginBps) ||
      line.grossMarginBps < 0 ||
      line.grossMarginBps > 10000
    ) {
      return fail(`invalid gross margin for ${line.name}`);
    }
  }
  const total = sum(lines.map((line) => line.annualCents));
  if (total <= 0) {
    return fail("total annual revenue must be positive");
  }
  return ok(total);
}

/** Recurring revenue divided by total revenue, in basis points. */
export function recurringShare(lines: RevenueLine[]): Result<Bps> {
  const checked = validate(lines);
  if (!checked.ok) {
    return checked;
  }
  const recurring = sum(
    lines.filter((line) => line.recurring).map((line) => line.annualCents),
  );
  return ok(divide(recurring, checked.value));
}

/** Each line's share of total revenue, in basis points. Rounded shares may not sum to 10000. */
export function lineShares(lines: RevenueLine[]): Result<LineShare[]> {
  const checked = validate(lines);
  if (!checked.ok) {
    return checked;
  }
  return ok(
    lines.map((line) => ({
      name: line.name,
      shareBps: divide(line.annualCents, checked.value),
    })),
  );
}

/** Herfindahl-Hirschman index over revenue lines, normalized to 0-10000. */
export function concentration(lines: RevenueLine[]): Result<Bps> {
  const shares = lineShares(lines);
  if (!shares.ok) {
    return shares;
  }
  const hhi = shares.value.reduce(
    (acc, item) => acc + item.shareBps * item.shareBps,
    0,
  );
  return ok(Math.round(hhi / 10000));
}

/**
 * Weighted portfolio score:
 *   score = (wRecurring x recurringShare
 *          + wDiversification x (10000 - concentration)
 *          + wMargin x weightedGrossMargin) / 10000
 * Weights are basis points and must sum to 10000.
 */
export function portfolioScore(
  lines: RevenueLine[],
  weights: ScoreWeights = DEFAULT_WEIGHTS,
): Result<Bps> {
  const checked = validate(lines);
  if (!checked.ok) {
    return checked;
  }
  const weightSum =
    weights.recurring + weights.diversification + weights.margin;
  if (weightSum !== 10000) {
    return fail(`weights must sum to 10000 bps, got ${weightSum}`);
  }
  const recurring = recurringShare(lines);
  if (!recurring.ok) {
    return recurring;
  }
  const hhi = concentration(lines);
  if (!hhi.ok) {
    return hhi;
  }
  const weightedMargin = Math.round(
    lines.reduce(
      (acc, line) => acc + line.annualCents * line.grossMarginBps,
      0,
    ) / checked.value,
  );
  const diversification = 10000 - hhi.value;
  const score =
    (weights.recurring * recurring.value +
      weights.diversification * diversification +
      weights.margin * weightedMargin) /
    10000;
  return ok(Math.round(score));
}

代码里有两个细节值得注意。第一,lineShares 对每条线单独四舍五入,所以各线占比之和可能是 9999 或 10001 bps,不保证精确等于 10000;这是展示层的舍入,不是逻辑错误。第二,portfolioScore 的权重之和必须精确等于 10000,否则直接返回错误;权重是口径的一部分,不能悄悄归一化。

三个案例的调用结果与上面的表一致:LedgerLite 6526、Tidecast 2160、Nord Studio 4816。如果要调整权重,比如更看重经常性,可以把 recurring 调到 6000、diversification 和 margin 各 2000,重新跑一遍;评分会变,但结构问题不会因为权重变化而消失。

小结

  • 收入组合的健康度不等于收入线的数量,而等于每条线的性质、占比和相互依赖。
  • 经常性收入占比衡量「下一期收入有多少已经锁定」:LedgerLite 100.0%、Tidecast 0.0%、Nord Studio 27.3%。
  • 收入线 HHI 衡量集中度:单线案例 10000,Nord Studio 6031;但收入线 HHI 不是客户 HHI。
  • 组合评分按 4000/3000/3000 加权经常性、分散度和毛利率,三案例分别为 6526、2160、4816。
  • 加第二收入线要同时满足主线稳定、共享基础、边际成本可控、需求重叠四个条件。
  • 第二收入线在前 6 个月不能复用主线核心资产时,它大概率是一条新生意,不是补线。

阅读导航:上一节:1.1 三种生意:订阅、买断、服务的财务差异 · 下一节:1.3 生命周期转型:从服务到产品、从买断到订阅 。

常见错误与边界

  1. 把收入线数量当分散度。 两条线各占 50% 才是分散;Nord Studio 两条线,但咨询占 72.7%,HHI 仍有 6031。
  2. 用收入线 HHI 代替客户 HHI。 LedgerLite 收入线 HHI 是 10000,但前 5 家客户只占 31%,客户风险远低于 Tidecast 的 Steam 80%。
  3. 把经常性占比当收入质量。 100% 经常性也可能高流失;LedgerLite 的 100% 建立在月流失 1.5% 的基础上,流失翻倍,结论就会变。
  4. 买断线用利润表毛利率算评分。 利润表的 100% 是平台分成已在收入前扣除;组合评分要用净得率 72.0%,否则会高估买断线的毛利质量。
  5. 权重不校验。 三个权重相加不等于 10000 时,评分会失真;代码里直接返回错误,不悄悄归一化。
  6. 把组合评分当排名。 6526 比 4816 高,不代表 LedgerLite 一定比 Nord Studio 好;评分只反映三个维度的结构,不反映增长和风险。
  7. 主线不稳就加第二线。 主线月流失 5% 时,加线只会稀释注意力;先修主线,再加补线。
  8. 第二线不复用任何资产。 没有共享客户、数据、品牌或分发,第二线就是一家新公司,独立开发者通常没有第二套获客能力。
  9. 忽略第二线的支持成本。 产品收入毛利率 84.4%,但每多一个客户就多一份支持;支持成本没有进入毛利率时,第二线可能越卖越亏。
  10. 用「多一条收入更安全」代替现金测算。 第二线起量前是净现金流出;现金安全垫不足 6 个月时,加线会放大风险,而不是分散风险。

组合不是静态的。今天健康的单线,明年可能因为平台规则变化变成高风险;今天不起眼的第二线,也可能因为客户重复请求而变成主线的补充。第 1.3 节讨论的转型,就是把一条线逐步换成另一条线的过程;第 4 章会从收入确认的角度继续拆解经常性与一次性的差别。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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