《独立开发者的商业指南·收入与财务工程》3.1 利润表:收入、成本、费用的口径

同一笔收入放在利润表的不同位置,毛利率和 SDE 会给出完全不同的结论。本节用 LedgerLite 的年度利润表逐项复算收入、变动成本、运营费用与创始人薪酬,对照 Tidecast 的平台分成净得口径和 Nord 的服务成本结构,说明三表建模的第一张表如何定义边界,并为下一张现金流量表留下可勾稽的净利润。

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

利润表最容易让人产生「算对了」的错觉。收入减成本减费用等于利润,看起来只有一条公式;但同一笔钱放进收入、变动成本还是运营费用,会得到完全不同的毛利率和 SDE。三表建模的第一步不是把数字算得更细,而是先把口径钉死,让后面两张表都能接得上。

三个案例里,LedgerLite 的利润表最适合做主线:收入来自 260 家客户、每家有月费,成本费用能拆到具体科目,税额和净利润也与现金流、资产负债表对得上。Tidecast 和 Nord Studio 不提供完整的三表,但它们的收入结构足够说明买断与服务在口径上的差异。

收入的三层口径

同一笔钱在利润表里有三个不同的名字:标价收入、净得收入、确认收入。标价收入是价目表上的金额;净得收入是扣掉平台分成、退款、支付费之后真正属于开发者的金额;确认收入是按履约进度可以记进利润表的金额。三者只有在「售出即收款、无平台、无预收」时才恰好相等。

LedgerLite 的确认收入是 240,240 美元。它的来源是 260 家客户乘以每月 77 美元,得到 MRR 20,020 美元,再乘以 12 得到 ARR 240,240 美元。年付客户占 30%,年付折扣 15%,这部分现金在年初收进来,但利润表只能按月确认,差额进入递延收入。利润表上的 240,240 是十二个月确认额的总和,不是当年收到的现金。

Tidecast 的口径更绕。9,600 份乘以标价 19.99 美元,得到标价收入 191,904 美元;退款 4% 扣 7,676 美元;Steam 的 7,680 份再扣 30% 分成 44,215 美元;官网的 1,920 份扣 5% 支付费 1,842 美元;最后净得 138,171 美元。案例事实表把这 138,171 直接作为利润表的收入,所以它的顶行不是 191,904。

Nord Studio 没有平台分成。咨询收入是 110 小时乘以 85 美元再乘以 12,等于 112,200 美元;产品收入是 90 家乘以 39 美元再乘以 12,等于 42,120 美元;合计 154,320 美元。服务按交付确认,产品按月确认,两者都直接进入收入。

案例标价或合同收入收入前扣减利润表收入口径说明
LedgerLite$240,240$0$240,240订阅按月确认,年付预收进递延收入
Tidecast$191,904退款 $7,676 + 平台 $44,215 + 支付 $1,842 = $53,733$138,171净得口径,平台费用不计入运营费用
Nord Studio$154,320$0$154,320服务按交付、产品按月确认

这张表最重要的不是数字,而是「扣减项放在哪里」。Tidecast 的 53,733 美元可以在收入前扣除,也可以作为变动成本列在毛利之上,还可以塞进运营费用。三种做法都能得到相同的 SDE 121,171,但毛利率和运营费用率会给出不同的画面:收入前扣除时毛利率 100%;作为变动成本时毛利率 72.0%;塞进运营费用时毛利率仍是 100%,但运营费用从 17,000 变成 70,733(17,000 + 53,733)。三表建模必须先声明采用哪一种,否则后面的比率没有可比性。

收入确认的具体时点、预收与递延的拆分,属于第 4 章的内容;本节只约定利润表顶行用确认收入,详见 4.1 预收、递延与确认时点 。

成本与费用的边界

利润表的第二层口径是成本与费用的分界。变动成本随收入或客户数变化,通常放在毛利之上;运营费用是维持生意运转的固定或可调支出,放在毛利之下。分界不是会计教条,而是为了回答不同的问题:毛利率回答「每多一美元收入,先扣掉多少直接成本」,SDE 回答「扣掉全部运营费用后,所有者还能拿到多少」。

LedgerLite 的 38,000 美元变动成本可以拆成三块:云 18,000、支付 12,000、支持 8,000。云和支付随客户数与收款流水变化,支持随客户数增长但有一定阶梯,属于半变动。72,000 美元运营费用拆成五块:营销 16,800、工具 9,000、外包 20,000、法律保险 6,000、其他 20,200。营销可以按年获客计划调整,工具是固定订阅,外包是产品开发投入,法律保险是合规固定成本。

科目金额归类判断理由是否随收入变化
云$18,000变动成本随客户与用量增长是
支付$12,000变动成本随收款流水增长是
支持$8,000变动成本随客户数增长,有阶梯半变动
营销$16,800运营费用获客投入,不随当期收入可调
工具$9,000运营费用固定订阅否
外包$20,000运营费用产品开发投入可调
法律保险$6,000运营费用合规固定支出否
其他$20,200运营费用杂项,混合性质混合

创始人薪酬 60,000 美元不在这 72,000 里,而是放在 SDE 之下。SDE 的定义是「扣除运营费用、但尚未扣除所有者薪酬」的收益;如果把 60,000 放进运营费用,算出来的 70,240 就不是 SDE,而是税前利润。这个顺序不是格式问题,而是估值口径问题:SDE 130,240 和税前利润 70,240 会被套上不同的倍数。

Nord Studio 提供了一个更明显的分类例子。案例把两位合伙人的 96,000 美元工资放在毛利之下,所以毛利是 130,320,毛利率 84.4%,SDE 是 34,320。如果把合伙人工资视为交付服务的直接人力成本,毛利会降到 34,320,毛利率变成 22.2%,运营费用变成 0,SDE 仍然是 34,320。分类改变了毛利率,没有改变 SDE;但如果拿毛利率去和纯产品公司比较,22.2% 和 84.4% 会给出完全不同的印象。

Tidecast 的 100% 毛利率也是分类的产物。它的平台分成、退款和支付费已经在收入前扣掉,利润表上只剩 138,171 的净得,变动成本为 0,所以毛利率是 100%。如果改用总额口径,收入 191,904,扣减项 53,733 作为变动成本,毛利率是 72.0%。这个 72.0% 与净得率 138,171 ÷ 191,904 是同一个比值,只是在利润表里叫毛利率,在渠道分析里叫净得率;净得口径下的 100% 才是不同的分母。

口径收入毛利毛利率SDE说明
LedgerLite 案例口径$240,240$202,24084.2%$130,240变动成本 $38,000,运营费用 $72,000
Tidecast 净得口径$138,171$138,171100.0%$121,171平台费与退款已在收入前扣除
Tidecast 总额口径$191,904$138,17172.0%$121,171扣减项 $53,733 作为变动成本
Nord 案例口径$154,320$130,32084.4%$34,320合伙人工资在毛利之下
Nord 工资进成本$154,320$34,32022.2%$34,320运营费用变为 $0,SDE 不变

本节采用案例事实表的分类:LedgerLite 的创始人薪酬在 SDE 之下,Tidecast 用净得口径,Nord 的合伙人工资在毛利之下。简化假设是:没有折旧、没有利息、没有汇兑损益;税只用单一税率,不处理亏损结转和税收抵免。这些假设会在第 3.2 节和第 3.3 节继续沿用,直到三张表能闭合。

LedgerLite 利润表逐项复算

把 LedgerLite 从客户数一路算到净利润,一共八步。每一步只用一个加减乘除,派生值都写明公式。

步骤公式结果
MRR260 家 × $77$20,020
ARR$20,020 × 12$240,240
毛利$240,240 − $38,000$202,240
毛利率$202,240 ÷ $240,24084.2%
SDE$202,240 − $72,000$130,240
税前利润$130,240 − $60,000$70,240
所得税17% × $70,240$11,940.80,案例表取整为 $11,941
净利润$70,240 − $11,941$58,299

这里有一个容易被忽略的舍入差。案例事实表按整数美元展示,所以税额写 11,941,净利润写 58,299;如果按美分精确计算,17% 乘以 70,240 是 11,940.80,净利润是 58,299.20。两者相差 0.20 美元,不影响任何结论,但代码和正文必须使用同一套舍入口径。本节的表格用案例表的整数美元,代码用精确到分的美分。

把利润表的各项换成占收入的比率,可以看出费用结构:

指标金额占收入
收入$240,240100.0%
变动成本$38,00015.8%
毛利$202,24084.2%
运营费用$72,00030.0%
SDE$130,24054.2%
创始人薪酬$60,00025.0%
税前利润$70,24029.2%
所得税$11,9415.0%
净利润$58,29924.3%

毛利率 84.2% 减运营费用率 30.0% 等于 SDE 率 54.2%;再减创始人薪酬率 25.0% 得到 29.2% 的税前利润率;再减 5.0% 的税,得到 24.3% 的净利率。按精确金额计算,这条链能闭合;表中四舍五入到一位小数,逐项相加可能有 0.1 个百分点的舍入差。分类没有重复也没有遗漏。

但 130,240 的 SDE 不是现金。它还没有扣营运资金变化、资本开支和债务偿还;净利润 58,299 也不是现金,因为收入里有一部分是应收,成本里有一部分是预收。利润表只回答「这一年赚没赚」,不回答「现金有没有增加」。下一节的现金流量表会从净利润出发,把这两件事拆开。

买断与服务的对照

把三个案例的利润表放在一起,差异很快显现。

指标LedgerLiteTidecastNord Studio
收入$240,240$138,171$154,320
收入性质经常性订阅一次性买断72.7% 服务 + 27.3% 产品
变动成本$38,000$0(净得口径)$24,000
毛利$202,240$138,171$130,320
毛利率84.2%100.0%84.4%
运营费用$72,000$17,000$96,000
SDE$130,240$121,171$34,320
SDE 率54.2%87.7%22.2%
所得税$11,941$29,081$8,580
净利润$58,299$92,090$25,740
净利率24.3%66.6%16.7%

表中 SDE 率 = SDE ÷ 收入,净利率 = 净利润 ÷ 收入。Tidecast 的净利润是 121,171 − 29,081 = 92,090,Nord 的净利润是 34,320 − 8,580 = 25,740;LedgerLite 的净利润 58,299 已在上一节复算。

Tidecast 的 SDE 率 87.7%、净利率 66.6%,看起来是三个案例里最赚钱的。这组数字没有错,但它只描述「已经卖出去的 9,600 份」。买断收入是一次性的,明年不保证还有 9,600 份;12 月卖 2,400 份、1 月卖 480 份,Q4 占全年销量 38%,现金高度集中在假日季。利润表上的高净利率,是用一次性收入换来的。

Nord Studio 的 SDE 率只有 22.2%,因为 96,000 美元工资是刚性的。服务收入占 72.7%,咨询续约率 70%,意味着每年有 30% 的服务收入需要重新赢得;产品收入占 27.3%,月流失 3.0%。毛利率 84.4% 看起来健康,但毛利要先养活两个人,剩下的才是 SDE。这也是为什么它的标准估值锚 4.5 倍 SDE 只有 154,440 美元,而且因为服务收入占比高、依赖合伙人,买方报价还会下调到 3.0 倍 SDE = 102,960 美元。

LedgerLite 处在中间:SDE 率 54.2%,净利率 24.3%,收入是经常性的,月流失 1.5%、NRR 102%、GRR 83%。它的运营费用 72,000 不是浪费,而是维持下一年的收入基础;营销 16,800 对应年新签 120 家,CAC 140 美元,回本周期 2.2 个月。利润表上的费用,要和单位经济一起看才有意义;单位经济的四个数见 2.1 单位经济的四个数:ARPA、毛利、CAC、回本 。

三种生意的利润表差异,最终落在三个位置:

  • 订阅的差异在留存和获客成本:毛利率高,运营费用也高,但收入可以续。
  • 买断的差异在销量和平台分成:单笔净得高,收入不经常,季节性强。
  • 服务的差异在人的成本:毛利率看起来健康,SDE 被工资吃掉,收入依赖合伙人。

利润表能解释「为什么 SDE 不同」,但不能解释「为什么现金不同」。Tidecast 的 92,090 净利润和 LedgerLite 的 58,299 净利润,哪一个更能变成手里的现金,要看应收、递延收入和资本开支。这正是下一张表要回答的问题。

把利润表写成代码

下面的 income-statement.ts 把收入拆成经常性和一次性两桶,再复用第 1 章的 pnl.ts 计算毛利、SDE、税前利润、税和净利润。金额统一用整数美分,比率统一用基点,非法输入返回 Result 错误,而不是静默算出一个看起来合理的数。

// file: src/fin/income-statement.ts
// tier: L1
// verify: node --test test/fin/income-statement.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";
import { type Pnl, buildPnl } from "./pnl.ts";

export type RevenueKind = "recurring" | "one-time";

export type RevenueLine = {
  name: string;
  kind: RevenueKind;
  amountCents: Cents;
};

export type StatementLine = RevenueLine & {
  shareBps: Bps;
};

export type RevenueSplit = {
  recurringCents: Cents;
  oneTimeCents: Cents;
  totalCents: Cents;
  recurringShareBps: Bps;
};

export type IncomeStatement = {
  lines: StatementLine[];
  split: RevenueSplit;
  grossProfit: Cents;
  grossMarginBps: Bps;
  sde: Cents;
  pretax: Cents;
  tax: Cents;
  net: Cents;
};

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 (line.kind !== "recurring" && line.kind !== "one-time") {
      return fail(`unknown revenue kind for ${line.name}`);
    }
    if (!Number.isInteger(line.amountCents) || line.amountCents < 0) {
      return fail(`invalid amount for ${line.name}`);
    }
  }
  const total = sum(lines.map((line) => line.amountCents));
  if (total <= 0) {
    return fail("total revenue must be positive");
  }
  return ok(total);
}

/**
 * Splits revenue into recurring and one-time buckets. The classification is
 * supplied by the caller: the same contract can be recurring or one-time
 * depending on the question being asked.
 */
export function splitRevenue(lines: RevenueLine[]): Result<RevenueSplit> {
  const checked = validate(lines);
  if (!checked.ok) {
    return checked;
  }
  const recurringCents = sum(
    lines
      .filter((line) => line.kind === "recurring")
      .map((line) => line.amountCents),
  );
  return ok({
    recurringCents,
    oneTimeCents: checked.value - recurringCents,
    totalCents: checked.value,
    recurringShareBps: divide(recurringCents, checked.value),
  });
}

/**
 * Builds an income statement from revenue lines and the same four cost and
 * tax inputs used by pnl.ts. Line shares are reported in basis points.
 */
export function buildIncomeStatement(input: {
  lines: RevenueLine[];
  variableCosts: Cents;
  operatingExpenses: Cents;
  founderSalary: Cents;
  taxRate: Bps;
}): Result<IncomeStatement> {
  const split = splitRevenue(input.lines);
  if (!split.ok) {
    return split;
  }
  const pnl: Result<Pnl> = buildPnl({
    revenue: split.value.totalCents,
    variableCosts: input.variableCosts,
    operatingExpenses: input.operatingExpenses,
    founderSalary: input.founderSalary,
    taxRate: input.taxRate,
  });
  if (!pnl.ok) {
    return pnl;
  }
  return ok({
    lines: input.lines.map((line) => ({
      ...line,
      shareBps: divide(line.amountCents, split.value.totalCents),
    })),
    split: split.value,
    grossProfit: pnl.value.grossProfit,
    grossMarginBps: pnl.value.grossMargin,
    sde: pnl.value.sde,
    pretax: pnl.value.pretax,
    tax: pnl.value.tax,
    net: pnl.value.net,
  });
}

代码只做三件事:校验收入行、拆分经常性与一次性、把总额交给 buildPnl。RevenueLine.kind 由调用方决定:LedgerLite 的订阅是 recurring,Tidecast 的 Steam 和官网都是 one-time,Nord 的咨询在本节按 one-time、产品按 recurring,因为案例事实表把产品收入单独列为 27.3%。这个分类不是永久标签,而是服务于当前问题的视角。

LedgerLite 的输入可以直接从事实表抄进夹具:收入行 24,024,000 美分、变动成本 3,800,000、运营费用 7,200,000、创始人薪酬 6,000,000、税率 1,700 bps。输出是毛利 20,224,000、毛利率 8,418 bps、SDE 13,024,000、税前 7,024,000、税 1,194,080、净利润 5,829,920。Tidecast 和 Nord 的输入也能得到对应结果,测试文件里各有断言。

这套代码不处理折旧、摊销、利息、汇兑和一次性项目,也不处理多币种换算。它只覆盖本章的简化口径;真实账目里的科目映射、月结和审计调整,属于记账与月结流程,见 记账与月结 。

小结

  • 利润表的第一口径是收入:标价、净得、确认收入三者不能混用。Tidecast 从 191,904 到 138,171 是净得口径,净得率 72.0%。
  • 第二口径是成本与费用:LedgerLite 变动成本 38,000、运营费用 72,000;创始人薪酬 60,000 在 SDE 之下,不在运营费用里。
  • 第三口径是分类:Nord 的 96,000 工资放在毛利下还是成本里,毛利率从 84.4% 变成 22.2%,SDE 仍然是 34,320。
  • LedgerLite 从 MRR 20,020 到净利润 58,299,八步全部可复算;SDE 130,240 不是现金,净利润 58,299 也不是现金。
  • 三案例 SDE 分别为 130,240、121,171、34,320,差异来自收入性质与固定成本,不是会计技巧。
  • 利润表只回答「赚没赚」;下一张现金流量表回答「现金有没有增加」,再下一张资产负债表回答「期末还剩什么」。

阅读导航:上一节:2.3 单位经济失效的五个信号 · 下一节:3.2 现金流量表:经营、投资、筹资 。

常见错误与边界

  1. 把标价收入当利润表收入。 Tidecast 的标价收入是 191,904,利润表收入是净得 138,171;用 191,904 算毛利率会得到 72.0%,但分母已经不是利润表的顶行。
  2. 把平台分成放进运营费用。 净得口径下,平台分成和退款已经在收入前扣除;再放进运营费用会重复扣减,SDE 会被低估。
  3. 把创始人薪酬放进运营费用。 LedgerLite 的 60,000 如果放进运营费用,SDE 会从 130,240 变成 70,240;这个数字是税前利润,不是 SDE,套估值倍数会错位。
  4. 用毛利率比较不同收入口径。 Tidecast 的 100% 和 72.0% 是同一笔生意的两种展示;不声明口径就横向比较,结论没有意义。
  5. 把 SDE 当净利润或现金。 SDE 还没扣创始人薪酬、所得税、营运资金变化和资本开支;LedgerLite 的 SDE 130,240 与净利润 58,299 相差 71,941(130,240 − 58,299)。
  6. 忽略税额舍入。 案例表写 11,941,精确值是 11,940.80;正文与代码要用同一套舍入口径,否则三表会出现几分钱的差。
  7. 把一次性买断收入按订阅倍数估值。 Tidecast 的估值锚是 1.8 倍收入,LedgerLite 是 3.2 倍 ARR;倍数差异来自收入性质,不是谈判技巧。
  8. 认为科目分类只有一种正确答案。 Nord 的工资进成本后毛利率变成 22.2%,SDE 不变;分类服务于问题,但必须在表头声明。
  9. 忘记递延收入。 LedgerLite 的年付预收不是当期收入,递延收入增加 8,000 会在现金流量表里加回;只看利润表会低估订阅业务的现金。
  10. 把利润表当成现金表。 利润表按权责发生制确认,现金按收付实现制流动;两者之间的桥,就是下一节的现金流量表。

本节的边界是「利润表内部的口径」:收入怎么定义、成本费用怎么分、SDE 和净利润怎么算。它不处理收入确认的具体时点、不处理记账软件操作、不处理税务申报流程;这些分别属于第 4 章、记账与月结流程和税务章节。税务申报流程不在本节展开,税率只作为利润表参数;所得税与常设机构的口径见 9.2 所得税与常设机构 。把这三层口径固定下来,3.2 节才能从净利润出发,逐项解释现金为什么增加或减少。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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