以下情境为教学假设,人物与数字不代表经核实的真实案例。
独立开发者最常见的收入结构不是「一条线」,而是「一条主线加一条补线」:白天做咨询,晚上维护订阅产品;或者一边卖课,一边接定制项目。补线的动机通常是「多一条收入更安全」,但收入线的数量本身不产生安全,结构才产生安全。
这一节把收入组合拆成三个可计算的量:经常性收入占比、收入线集中度、加权组合评分。三个量都不能单独使用,但放在一起,能回答一个具体问题:现在这条第二收入线,是在分散风险,还是在分散注意力?
先定义两个词。主收入线是当前贡献最大、决定现金安全的那条线;第二收入线是除此之外、已经产生真实收入的那条线。只有计划、没有收入的线不算第二收入线,只能算待验证的想法。
三个案例的组合结构完全不同:
| 项 | LedgerLite | Tidecast | Nord 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,240 | 100.0% |
| Tidecast | $0 | $138,171 | 0.0% |
| Nord Studio | $42,120 | $154,320 | 27.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) | 加权毛利率 | 组合评分 |
|---|---|---|---|---|
| LedgerLite | 10000 | 0 | 84.2% | 6526 |
| Tidecast | 0 | 0 | 72.0% | 2160 |
| Nord Studio | 2729 | 3969 | 84.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 生命周期转型:从服务到产品、从买断到订阅 。
常见错误与边界
- 把收入线数量当分散度。 两条线各占 50% 才是分散;Nord Studio 两条线,但咨询占 72.7%,HHI 仍有 6031。
- 用收入线 HHI 代替客户 HHI。 LedgerLite 收入线 HHI 是 10000,但前 5 家客户只占 31%,客户风险远低于 Tidecast 的 Steam 80%。
- 把经常性占比当收入质量。 100% 经常性也可能高流失;LedgerLite 的 100% 建立在月流失 1.5% 的基础上,流失翻倍,结论就会变。
- 买断线用利润表毛利率算评分。 利润表的 100% 是平台分成已在收入前扣除;组合评分要用净得率 72.0%,否则会高估买断线的毛利质量。
- 权重不校验。 三个权重相加不等于 10000 时,评分会失真;代码里直接返回错误,不悄悄归一化。
- 把组合评分当排名。 6526 比 4816 高,不代表 LedgerLite 一定比 Nord Studio 好;评分只反映三个维度的结构,不反映增长和风险。
- 主线不稳就加第二线。 主线月流失 5% 时,加线只会稀释注意力;先修主线,再加补线。
- 第二线不复用任何资产。 没有共享客户、数据、品牌或分发,第二线就是一家新公司,独立开发者通常没有第二套获客能力。
- 忽略第二线的支持成本。 产品收入毛利率 84.4%,但每多一个客户就多一份支持;支持成本没有进入毛利率时,第二线可能越卖越亏。
- 用「多一条收入更安全」代替现金测算。 第二线起量前是净现金流出;现金安全垫不足 6 个月时,加线会放大风险,而不是分散风险。
组合不是静态的。今天健康的单线,明年可能因为平台规则变化变成高风险;今天不起眼的第二线,也可能因为客户重复请求而变成主线的补充。第 1.3 节讨论的转型,就是把一条线逐步换成另一条线的过程;第 4 章会从收入确认的角度继续拆解经常性与一次性的差别。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。