《独立开发者的商业指南·收入与财务工程》13.3 卷一评审与进入卷二

卷一的最后一节用附录 C 的 38 项清单给三个案例打分:LedgerLite 88.2%、Tidecast 67.1%、Nord Studio 53.9%。分数不是目的,目的是把最弱的组映射到卷二要处理的具体条款,并给出进入卷二前的三个动作与基线复用方法。

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

卷一写了 13 章,从三种生意的收入差异一路写到尽调准备。这一节做收口:用附录 C 的清单给三个案例打分,看分数落在哪一档,再把最弱的组映射到卷二的具体章节。收口不是总结陈词,而是给下一卷留一张任务清单。

六组三十八项

附录 C 把评审分成六组,共 38 项,每项 0 到 2 分,满分 76 分。六组分别是收入质量、毛利与成本、单位经济、现金与流动性、集中度与依赖、报表与合规。

组项数满分这一组在问什么
收入质量816收入是不是真的、稳的、能复算的
毛利与成本612成本分类是否一致,毛利率是否稳定
单位经济510一个客户赚不赚钱,多久回本
现金与流动性714未来十三周会不会断粮
集中度与依赖612收入集中在谁身上,人走了还剩多少
报表与合规612三张表能不能勾稽,主体与申报是否一致

评分规则只有一条:拿不出证据的项按 0 分。1 分留给「有证据但不完整」或「口径中途变过」,不留给「感觉还行」。

三案例的评分

把三案例代入附录 C 的标准,得到下表。分数是教学示例,用于演示评分方法,不是对案例的正式评价。

组满分LedgerLiteTidecastNord Studio
收入质量161598
毛利与成本1210128
单位经济10956
现金与流动性1412108
集中度与依赖121064
报表与合规121197
合计76675141
得分率88.2%67.1%53.9%
评级健康需关注高风险

LedgerLite 88.2%,落在健康档。它的失分不在结构,而在细节:退款与拒付没有独立科目,前五大客户占 31% 略高于内部警戒线。这两项买方会追问,但不会因此推翻交易。

Tidecast 67.1%,落在需关注档。它的毛利与成本拿满分,因为成本结构极简;失分集中在收入质量与集中度:没有经常性收入、Steam 占销量 80%、单位经济无法套用订阅口径。这三项都不是「做错了」,而是「生意形态决定的」。

Nord Studio 53.9%,落在高风险档。它的每一组都偏低,最弱的是集中度与依赖:服务收入占 72.7%、前三大客户 48%、交付依赖两位合伙人、没有书面退出条款。这组分数直接解释了它为什么只能按 3.0 倍 SDE 成交。

分数之间的差距不是经营水平的简单排序。Tidecast 的分数低于 LedgerLite,但它的 SDE 只比 LedgerLite 少 9,069 美元;Nord 的分数最低,但它的两位合伙人可能过得很舒服。清单衡量的是「账能不能被第三方复核」,不是「生意好不好」。

最弱的组指向卷二哪一章

卷一只解决「账算不算得清」,卷二解决「钱收不收得回、条款扛不扛得住」。把每组最弱的项映射到卷二,就得到一张进入下一卷的任务表。

最弱的组具体缺口卷二对应主题
收入质量收入确认与合同条款不一致、退款科目缺失合同结构、收入确认条款
毛利与成本供应商成本无合同、价格调整无机制供应商依赖、成本转嫁条款
单位经济客户分层不清、续约与扩张无法归因客户集中、续约与终止
现金与流动性账期长、逾期无催收记录账期设计、应收账龄、坏账
集中度与依赖单一客户或平台占比过高、创始人不可替代责任上限、业务连续性
报表与合规数据责任不清、保险缺口数据责任、保险、争议解决

三案例的映射结果不同。LedgerLite 的缺口主要在「报表与合规」的细节,卷二会帮它把 DPA、责任上限与 SLA 抵扣补上。Tidecast 的缺口在「集中度与依赖」,卷二会讨论平台协议、IP 授权与业务连续性。Nord 的缺口几乎覆盖全部六组,卷二对它是「补合同、补账期、补退出条款」三件事同时进行。

进入卷二前的三个动作

不管读者做的是哪门生意,进入卷二前先完成三个动作。

第一,把卷一的三张表补齐到能勾稽。 利润表、现金流量表、资产负债表三张都要有,且资产等于负债加权益。缺一张,卷二讨论合同与账期时就没有基数。

第二,把五处口径差异写进自己的对照表。 收入是标价还是净得、SDE 是否扣所有者薪酬、毛利率的分母是什么、现金流是经营现金流还是可支配现金、资产负债表是否完整。五条写清楚,跨期比较才不会反复出错。

第三,用附录 C 打一次分,记下最弱的两组。 不要试图一次修完 38 项,先修最弱的两组。分数低不可怕,可怕的是不知道自己低在哪。

三个动作做完,卷二的第一节会从合同结构讲起,读者能把合同条款直接放回自己的三张表里,看它影响哪一行。

这张基线怎么继续用

卷一的数字不会到此结束。卷二讨论账期时,会用 LedgerLite 的 Net 15、Tidecast 的平台月结 30 天、Nord 的 Net 30 去算应收与坏账;讨论责任上限时,会用 12 个月费用、合同金额 100% 去算最坏损失;讨论 SLA 抵扣时,会用 99.5% 与 10%、99.0% 与 5% 去算抵扣金额。

卷三讨论交易时,会用 3.2 倍 ARR、1.8 倍收入、4.5 倍 SDE 去算 headline 价格,再用 earn-out、托管与营运资金调整折算净得。这些条款在卷一都已经出现过,只是当时只做了一行注记。

读者自己的基线也一样。把三张表、五处分歧、六组分数存成一个文件,每季度更新一次。卷二的合同清单、卷三的交易清单,都会在这份基线之上继续加减。

读者自己的收口表

把三案例换成自己的生意,收口只需要一页纸。格式如下:

一、三张表(最近 12 个月)
收入:____   毛利:____   毛利率:____
SDE:____    SDE 率:____
净利润:____  经营现金流:____  转换率:____
资产:____    负债:____        权益:____

二、五处口径
1. 收入是标价、净得还是确认收入:
2. SDE 是否扣除所有者薪酬:
3. 毛利率的分母:
4. 现金流是经营现金流还是可支配现金:
5. 资产负债表是否完整:

三、六组评分(附录 C)
收入质量:__/16   毛利与成本:__/12
单位经济:__/10   现金与流动性:__/14
集中度与依赖:__/12  报表与合规:__/12
合计:__/76   评级:____

四、最弱的两组与对应卷二章节:
五、进入卷二前要完成的三个动作:

一页纸的价值在于可重复。每季度重填一次,把上一季度的表放在旁边对比,就能看出哪些比率在改善、哪些口径在漂移。不要每次换模板,模板一换,历史数据就不可比。

如果某一格填不出来,那一格就是缺口。不要用「大概」填,也不要从别的表里借一个相近的数字。填不出来本身就是结论:这项证据缺失,附录 C 会给它 0 分。

三个交接问题

从卷一进入卷二,有三个问题必须先回答。它们不属于财务计算,但决定了卷二哪些章节对你最重要。

第一,你的收入是「签了合同就稳」还是「每月都要重新赢」? 前者的问题在合同条款与责任上限,后者的问题在续约、流失与客户集中。LedgerLite 属于前者,Tidecast 属于后者,Nord 两条线各占一半。

第二,你的钱是「先收后干」还是「先干后收」? 年付订阅是先收后干,现金充裕但形成递延收入;咨询与平台结算常是先干后收,利润好看但应收占用现金。两种节奏对应卷二的账期、应收与坏账三章。

第三,如果明天你不再参与,收入还能保留多少? 这个比例可以粗略用「不依赖创始人的客户收入 ÷ 总收入」估计。LedgerLite 偏高,Tidecast 中等(IP 可授权但续作依赖个人),Nord 偏低。这个比例直接决定卷三的估值折价与交易结构。

三个问题答完,卷二的阅读顺序也就定了。收入稳、先收后干、依赖低的读者,可以从责任上限与业务连续性读起;收入要靠每月重新赢、先干后收、依赖高的读者,应先读账期、应收与客户集中。

小结

卷一的收口用三件事完成:三张表能勾稽、五处口径写清楚、六组分数打分并找到最弱的两组。LedgerLite 的 88.2% 说明「账可以被复核」,Tidecast 的 67.1% 说明「结构决定分数」,Nord 的 53.9% 说明「可转移性是最贵的短板」。

volume-review.ts 把评分与映射写成了函数:reviewVolume 按固定阈值给出健康、需关注、高风险三档,nextVolumeFocus 把最弱的组映射到卷二主题。它的一个明确拒绝是:空清单直接报错,而不是返回零分——没有数据不等于零分,而是一个需要补证的空缺。

阅读导航:上一节:13.2 单位经济与估值区间 · 下一节:目录与学习路径 。

常见错误与边界

  1. 把分数当估值。 88.2% 不代表能按 3.2 倍 ARR 成交。评分衡量可复核性,估值还看增长、市场与买方战略。
  2. 只修高分项。 高分项再修也加不了几分,低分项才是折价来源。
  3. 空清单给零分。 没有数据是缺口,不是零分。按 0 分记会让总分虚低,掩盖真正的问题。
  4. 把「不适用」当满分。 买断制没有 NRR,不等于收入质量满分。没有替代证据就给 0 或 1。
  5. 一次修完所有项。 38 项同时动,流程会乱。先修最弱的两组,稳定后再扩。
  6. 分数不动就以为没问题。 分数不动但结构在恶化(经常性收入占比下降、集中度上升),同样要处理。

边界:清单是教学工具,不是审计准则。正式交易中的财务尽调由买方或其会计师执行,标准通常更细。清单的价值是让经营者提前看到买方会问什么。

// file: src/value/volume-review.ts
// tier: L1
// verify: node --test test/value/volume-review.test.ts

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

export type ReviewGroup = {
  key: string;
  label: string;
  maxScore: number;
  score: number;
};

export type ReviewResult = {
  totalScore: number;
  maxScore: number;
  scoreBps: Bps;
  grade: "healthy" | "watch" | "high-risk";
  gaps: ReviewGroup[];
  weakestGroup: ReviewGroup | null;
};

/**
 * Grades are fixed thresholds, not relative ranks: 70% and 55% are the two
 * lines used throughout volume one. A missing group is a data gap, so the
 * function rejects the whole review instead of silently scoring it as zero.
 */
export function reviewVolume(groups: ReviewGroup[]): Result<ReviewResult> {
  if (groups.length === 0) {
    return fail("at least one review group is required");
  }
  let totalScore = 0;
  let maxScore = 0;
  for (const group of groups) {
    if (!Number.isInteger(group.maxScore) || group.maxScore <= 0) {
      return fail(`group ${group.key} must have a positive integer max score`);
    }
    if (
      !Number.isInteger(group.score) ||
      group.score < 0 ||
      group.score > group.maxScore
    ) {
      return fail(`group ${group.key} score must be between 0 and its max score`);
    }
    totalScore += group.score;
    maxScore += group.maxScore;
  }

  const scoreBps = divide(totalScore, maxScore);
  let grade: ReviewResult["grade"] = "healthy";
  if (scoreBps < 5500) {
    grade = "high-risk";
  } else if (scoreBps < 7000) {
    grade = "watch";
  }

  const gaps = groups
    .filter((group) => group.score < group.maxScore)
    .sort((a, b) => a.score / a.maxScore - b.score / b.maxScore);

  return ok({
    totalScore,
    maxScore,
    scoreBps,
    grade,
    gaps,
    weakestGroup: gaps.length > 0 ? gaps[0] : null,
  });
}

/**
 * A gap is only actionable if it points at a chapter. This maps the six review
 * groups to the volume-two themes that continue the work, so the closing
 * chapter can hand the reader a concrete next step.
 */
export function nextVolumeFocus(groupKey: string): string | null {
  const map: Record<string, string> = {
    revenue: "合同与收入确认",
    cost: "供应商与成本条款",
    unit: "客户集中与续约",
    cash: "账期、应收与催收",
    concentration: "责任上限与业务连续性",
    compliance: "数据责任与保险",
  };
  return map[groupKey] ?? null;
}

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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