《独立开发者的商业指南·收入与财务工程》8.1 13 周现金流表

13 周现金流表把现金从年度视角拉到每周。本节用 LedgerLite 的订阅回款与年付脉冲、Tidecast 的发行月回款、Nord Studio 的 Net 30 账期,演示滚动周表的字段、口径与最低点识别,并说明 1.5% 月流失在 13 周窗口里为什么几乎看不见。

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

第 3 章把三张历史报表接成了一个闭合的年度模型,第 4 章解释了收入为什么不能按收款确认。这两章回答的都是「过去一年发生了什么」。而一个人经营的公司最先遇到的危机不是年度利润为负,是某个月的某一天账户余额不够付工资、税款或云账单。年度报表看不见这一天,因为它把十二个月折成了一行。

13 周现金流表就是把这一行重新展开。它不预测未来一年,只把未来一个季度拆成 13 个格子,每个格子问两个问题:这一周会进多少钱,这一周必须出多少钱。答案越具体,越能提前发现缺口。本节先建立周表的口径,再用三个案例说明三种收入形态在周表里的不同形状。

为什么是 13 周而不是 12 个月

13 周约等于一个季度,但比「三个月」更适合做现金管理。原因是周与支付周期对齐:工资按周或双周发,平台按周或按月结算,信用卡和云服务按月扣款,税款按季度预缴。用月做格子,会把月末的几笔大额支出压成一个数字;用周做格子,才能看到哪一周同时撞上工资、税款和平台结算延迟。

视角时间粒度回答的问题典型盲区
年度预算12 个月这一年能不能盈利月度内的支付撞车
三张报表季度或年度利润有没有变成现金具体哪一天缺钱
13 周现金流表1 周未来 13 周哪一周会低于水位13 周之后的趋势

13 周还有一层行为上的意义:它足够短,短到每一格都敢写具体数字;又足够长,长到能覆盖一次完整的回款周期。LedgerLite 的账期是 Net 15,Tidecast 的平台月结 30 天,Nord Studio 的咨询是 Net 30。13 周能装下「开票、到期、到账」的完整链条,也能装下一次季度税款的到期日。

年度预算的方法论见 5.1 自上而下与自下而上的预算 ;本节不重复预算编制,只处理预算落地到周之后的现金节奏。两者的关系是:年度预算决定「花多少」,13 周表决定「哪一周花得出来」。

表的五个字段与四条口径

一张周表最少需要五个字段。字段少,才能每周更新;字段多,反而没人维护。

字段含义来源常见错误
周次第 1 到第 13 周固定用日期代替周次,跨月就乱
期初现金上周期末结转银行余额把应收账款当现金
流入本周确定到账的钱合同、平台账单、发票把「应该会付」写成「会付」
流出本周必须付的钱工资、税、供应商账期漏掉季度性税款
期末现金期初 + 流入 − 流出计算不做滚动,只做一次

四条口径决定了这张表能不能用。

第一,只放已签约或已发生的收付。潜在客户、口头承诺、还没开票的项目都不进表,它们属于销售预测,不属于现金预测。第二,按现金实际到账或付出的那一周记录,不按收入确认的那一周。第三,一次性项目单独成行,不和经常性收支混在一起,否则下一周无法判断基线。第四,期初现金必须来自银行对账单,不能来自账面现金,因为账面现金可能包含在途资金。

这四条听起来像会计常识,但它们决定了周表是「管理工具」还是「安慰剂」。一张把所有乐观回款都算进去的表,永远显示绿色,也就永远没用。

LedgerLite:订阅现金流的周表

LedgerLite 的现金结构最简单:260 家客户、ARPA 77 美元、MRR 20,020、ARR 240,240。其中 70% 是月付,30% 是年付并享受 15% 折扣。把年收入除以 52,每周的基础回款是 4,620 美元;再按付款方式拆开,月付部分每周 3,234,年付部分按季度续约、每次 18,018。

成本侧同样按年除以 52:变动成本 38,000 除以 52 是 731,运营费用 72,000 除以 52 是 1,385,创始人薪酬 60,000 除以 52 是 1,154,合计每周 3,270。所得税 11,941 不按周摊,而是作为一次性支出落在预缴周。

周期初流入流出净额期末
1$95,0003,2343,270−$36$94,964
294,9643,2343,270−$36$94,928
394,9283,2343,270−$36$94,892
494,8923,2343,270−$36$94,856
594,8563,2343,270−$36$94,820
694,82021,2523,270$17,982$112,802
7112,8023,2343,270−$36$112,766
8112,7663,2343,270−$36$112,730
9112,7303,23415,211−$11,977$100,753
10100,7533,2343,270−$36$100,717
11100,7173,2343,270−$36$100,681
12100,6813,2343,270−$36$100,645
13100,6453,2343,270−$36$100,609

这张表最值得看的是三个数。第一,常规周的净额是 −36,也就是说,只靠月付客户,LedgerLite 每周略微失血,全年靠年付续约的脉冲补回来。第二,第 6 周流入 21,252,是 3,234 的月付加 18,018 的年付续约,这一周把现金推到 112,802。第三,第 9 周流出 15,211,是常规 3,270 加税款 11,941,这一周把现金拉回 100,753。

最低点出现在第 5 周,94,820,正好在年付续约到账之前。这不是巧合:年付客户的续约日决定了订阅公司的现金节奏,续约周之前的几周永远是最紧的。如果年付比例从 30% 升到 40%,月付部分从 14,014 降到 12,012,常规周流入从 3,234 降到 2,772,周净额从 −36 变成 −498,最低点会降到 92,510;第 6 周的脉冲则从 18,018 升到 24,024。年付比例越高,续约前的坑越深,续约那周的峰越高。

1.5% 月流失在 13 周里几乎看不见

LedgerLite 的月流失率是 1.5%,收入口径,年化约 16.6%。把这个流失率折成周,每周约 0.35%。如果三个月不做任何新签,月付回款从 3,234 开始逐周衰减:

口径13 周累计回款与持平口径的差
假设零流失$42,042—
按 1.5% 月流失逐周衰减$41,174−$868
第三个月的单月收入$19,133比期初少 $887

13 周只少 868 美元,占累计回款的 2.1%。这正是周表的局限:它对「一次性撞车」极其敏感,对「缓慢侵蚀」几乎无感。1.5% 的月流失在一年里会吃掉 16.6% 的收入,但在一个季度里只留下 2% 的痕迹。所以周表必须和年度趋势一起看:周表负责发现「哪一周会断」,趋势负责发现「一年后还剩多少」。

Tidecast 与 Nord:脉冲与账期

同样是 13 周,Tidecast 和 Nord Studio 的表形状完全不同。Tidecast 是买断制,收入集中在发行月和 Q4;Nord Studio 是服务加产品,收入被账期切成两段。

Tidecast 的 13 周窗口从 11 月中旬到次年 2 月中旬,正好覆盖 Q4 高峰和 1 月的低谷。单份净得按标价 19.99、退款 4%、Steam 分成 30%、官网支付费 5% 计算,加权后每份约 14.39 美元。12 月销量 2,400 份,折每周 554 份;1 月 480 份,折每周 111 份。平台月结 30 天,所以 12 月的销售要到 1 月才变成现金。案例没有给出 Tidecast 的资产负债表,本节把可支配现金 92,090 当作期初水位,只用于演示 13 周的节奏。

周期初流入流出净额期末
1$92,0902,073327$1,746$93,836
293,8362,073327$1,746$95,582
395,5822,073327$1,746$97,328
497,3282,073327$1,746$99,074
599,0742,073327$1,746$100,820
6100,8202,073327$1,746$102,566
7102,5667,974327$7,647$110,213
8110,2137,974327$7,647$117,860
9117,8607,974327$7,647$125,507
10125,5077,974327$7,647$133,154
11133,1547,974327$7,647$140,801
12140,8011,598327$1,271$142,072
13142,0721,598327$1,271$143,343

Tidecast 的表里,现金在前 6 周缓慢上升,第 7 到 11 周出现五个 7,974 的峰值,第 12 周开始回落到 1,598。峰值不是 12 月卖的,是 12 月卖、1 月到账的。13 周里现金从 92,090 涨到 143,343,增加 51,253,看起来很好;但第 13 周的周净额只有 1,271,年化后远低于 Q4 的水平。买断生意的周表永远在回答同一个问题:这一次脉冲之后,下一次脉冲在哪里。

Nord Studio 的表是另一种形状。咨询 110 小时、每小时 85 美元,月收入 9,350;产品 90 家、每月 39 美元,月收入 3,510;合计月收入 12,860。咨询按 Net 30 开票,产品按月收款,所以稳态下每周流入约 2,968。直接成本 24,000 和两人工资 96,000 按年除以 52,每周流出 2,308。Nord 同样没有给出期初现金,本节把可支配现金 25,740 当作起点。

周期初流入流出净额期末
1$25,7402,9682,308$660$26,400
226,4002,9682,308$660$27,060
327,0602,9682,308$660$27,720
427,7202,9682,308$660$28,380
528,3802,9682,308$660$29,040
629,0402,9682,308$660$29,700
729,7002,9682,308$660$30,360
830,3602,96810,888−$7,920$22,440
922,4402,9682,308$660$23,100
1023,1002,9682,308$660$23,760
1123,7602,9682,308$660$24,420
1224,4202,9682,308$660$25,080
1325,0802,9682,308$660$25,740

Nord 的周表几乎是一条直线:每周 +660,第 8 周因为 8,580 的税款掉到 22,440,然后继续以每周 660 爬回 25,740。13 周结束时现金回到起点,等于说这个季度赚的钱刚好等于税。账期在稳态下看不出来,因为「本月开票、下月收款」的延迟被连续的项目抵消了;一旦停止接新单,第 13 周会少收一个月的咨询费 9,350。

案例13 周现金变化周净额形状最低点对周表的敏感点
LedgerLite+$5,609平线加两次脉冲第 5 周 $94,820年付续约日
Tidecast+$51,253长尾加五周峰值期初 $92,090平台结算延迟
Nord Studio$0直线加一次税第 8 周 $22,440咨询项目连续性

三张表放在一起,13 周现金流表的价值就清楚了:它不是把年度利润除以 52,而是把每个案例最脆弱的那个时间点暴露出来。LedgerLite 脆弱在续约前,Tidecast 脆弱在脉冲后,Nord 脆弱在税周到账前。

把周表写成代码

下面的 cash13.ts 把周表固化成一条可复算的链路:输入期初现金和 13 组流入流出,输出每一行的期初、净额、期末,以及最低点所在的周。金额用整数美分,周数固定为 13,任何一周的流入流出不是整数都会被拒绝。

// file: src/cash/cash13.ts
// tier: L1
// verify: node --test test/cash/cash13.test.ts

import { type Cents, add, sub } from "../core/money.ts";
import { type Result, fail, ok } from "../core/result.ts";

export const WEEKS_PER_QUARTER = 13;

/** One week of planned receipts and payments, both in cents. */
export type WeekFlow = {
  inflow: Cents;
  outflow: Cents;
};

export type WeekRow = {
  week: number;
  opening: Cents;
  inflow: Cents;
  outflow: Cents;
  net: Cents;
  closing: Cents;
};

export type Cash13 = {
  rows: WeekRow[];
  totalInflow: Cents;
  totalOutflow: Cents;
  netChange: Cents;
  openingCash: Cents;
  closingCash: Cents;
  minimumCash: Cents;
  minimumWeek: number;
  negativeFromWeek: number | null;
};

/**
 * A 52-week year keeps the arithmetic reproducible from the case facts:
 * LedgerLite ARR 240,240 / 52 = 4,620 per week exactly.
 */
export function weeklyFromAnnual(annual: Cents): Cents {
  return Math.round(annual / 52);
}

export function weeklyFromMonthly(monthly: Cents): Cents {
  return Math.round((monthly * 12) / 52);
}

function validate(openingCash: Cents, weeks: WeekFlow[]): Result<true> {
  if (!Number.isInteger(openingCash)) {
    return fail("opening cash must be an integer number of cents");
  }
  if (weeks.length !== WEEKS_PER_QUARTER) {
    return fail(`a rolling cash table needs exactly ${WEEKS_PER_QUARTER} weeks`);
  }
  for (let i = 0; i < weeks.length; i += 1) {
    const row = weeks[i];
    if (!Number.isInteger(row.inflow)) {
      return fail(`week ${i + 1} inflow must be an integer`);
    }
    if (!Number.isInteger(row.outflow)) {
      return fail(`week ${i + 1} outflow must be an integer`);
    }
  }
  return ok(true);
}

/**
 * Rolling 13-week table. Each row closes into the next row's opening, so the
 * closing balance is the only state that has to be carried forward. The
 * minimum is tracked with the week it occurs: a table that never dips is not
 * the same as a table that dips in week 13, even when both end positive.
 */
export function buildCash13(
  openingCash: Cents,
  weeks: WeekFlow[],
): Result<Cash13> {
  const checked = validate(openingCash, weeks);
  if (!checked.ok) {
    return checked;
  }
  const rows: WeekRow[] = [];
  let balance = openingCash;
  let totalInflow = 0;
  let totalOutflow = 0;
  let minimumCash = openingCash;
  let minimumWeek = 0;
  let negativeFromWeek: number | null = null;
  for (let i = 0; i < weeks.length; i += 1) {
    const flow = weeks[i];
    const net = sub(flow.inflow, flow.outflow);
    const closing = add(balance, net);
    rows.push({
      week: i + 1,
      opening: balance,
      inflow: flow.inflow,
      outflow: flow.outflow,
      net,
      closing,
    });
    totalInflow = add(totalInflow, flow.inflow);
    totalOutflow = add(totalOutflow, flow.outflow);
    if (closing < minimumCash) {
      minimumCash = closing;
      minimumWeek = i + 1;
    }
    if (closing < 0 && negativeFromWeek === null) {
      negativeFromWeek = i + 1;
    }
    balance = closing;
  }
  return ok({
    rows,
    totalInflow,
    totalOutflow,
    netChange: sub(totalInflow, totalOutflow),
    openingCash,
    closingCash: balance,
    minimumCash,
    minimumWeek,
    negativeFromWeek,
  });
}

/**
 * First week whose closing balance sits below the floor. Returns null when the
 * whole horizon stays at or above the floor.
 */
export function firstWeekBelow(
  rows: WeekRow[],
  floor: Cents,
): number | null {
  for (const row of rows) {
    if (row.closing < floor) {
      return row.week;
    }
  }
  return null;
}

/** Weeks in the horizon that end below the floor. */
export function weeksBelow(rows: WeekRow[], floor: Cents): number {
  return rows.filter((row) => row.closing < floor).length;
}

代码里有三个刻意的设计。第一,周数固定为 13,不做参数化:一旦允许 4 周或 26 周,口径就会在团队里分叉,表格之间不再可比。第二,最低点和最低点所在的周一起返回。只看期末现金会漏掉中途的深坑,而现金危机几乎总是发生在中途。第三,negativeFromWeek 记录第一次余额为负的周;只要它不为空,这张表就已经不是「紧不紧」的问题,而是「哪一周付不出来」的问题。

LedgerLite 的输入是期初 9,500,000 美分、13 组流入 323,400(第 6 周 2,125,200)、流出 327,000(第 9 周 1,521,100),输出期末 10,060,900、最低 9,482,000、最低周第 5 周。Tidecast 的输入是期初 9,209,000、13 组流入按发行节奏排列、流出 32,700,输出期末 14,334,300,最低点就是期初。这些数字与上面的表逐行对应,只是单位从美元换成了美分。

周表本身不解决「钱不够怎么办」,它只负责提前发现。发现之后的动作属于情景模拟与 runway 计算:8.2 节会把大客户流失、平台封禁、退款潮代入这张表,8.3 节会把期末现金换算成还能撑几个月。操作层的周表模板、银行流水导入与每周复盘节奏,见 13 周现金流 。

小结

  • 13 周现金流表按周记录期初、流入、流出、净额、期末五个字段,只放已签约或已发生的收付,按现金日期而不是确认日期入表。
  • LedgerLite 的常规周净额是 −36,靠第 6 周年付续约 18,018 补回,第 9 周税款 11,941 又把现金拉回;最低点在第 5 周,94,820。
  • 1.5% 月流失在 13 周里只造成 868 美元的累计回款差,周表对它几乎无感;流失要看年度趋势,撞车要看周表。
  • Tidecast 的 13 周现金增加 51,253,但峰值来自 12 月销售在 1 月到账,第 13 周的周净额只剩 1,271,脉冲之后必须问下一次脉冲在哪里。
  • Nord Studio 的周表是一条每周 +660 的直线,第 8 周的 8,580 税款把低点压到 22,440;账期在稳态下被连续项目抵消,断单才会暴露。
  • 三张表的共同结论:周表的价值不是期末余额,而是最低点出现在第几周、为什么出现在那一周。

阅读导航:上一节:7.3 续约、扩张与收缩收入 · 下一节:8.2 情景模拟:断供、退款潮、平台封禁 。

常见错误与边界

  1. 把年度利润除以 52 当周现金流。 LedgerLite 年净利 58,299,除以 52 是 1,121,但常规周净额是 −36;差额来自年付脉冲和税款集中到期。
  2. 把应收账款当现金。 LedgerLite 年末应收 12,000,账期 Net 15;这张应收要等到账才进周表,提前记入会把最低点整体抬高约 12,000。
  3. 把平台销售额当到账额。 Tidecast 12 月卖 2,400 份,现金要 30 天后才到;按销售周记账会让第 3 到第 7 周凭空多出五周峰值。
  4. 漏掉季度税款。 LedgerLite 的 11,941、Nord 的 8,580 都不是均匀支出;摊到 13 周会把第 8、第 9 周的深坑抹平。
  5. 只做一次,不滚动。 13 周表每周要往前滚一格;一个月不更新,表里的「未来」就已经是过去。
  6. 把最乐观的回款写进表。 只放已签约或已发生的收付;口头承诺和未开票项目属于销售预测,不属于现金预测。
  7. 只看期末现金。 Tidecast 期末 143,343 很漂亮,但第 13 周周净额只有 1,271;期末余额不告诉你脉冲已经结束。
  8. 把 13 周当全年。 一个季度覆盖不了完整的年度税季和续约季;周表要和 12 个月趋势一起读。
  9. 用账面现金当期初。 在途资金、未清算的平台款、被冻结的余额都不等于可用现金;期初必须来自银行对账单。
  10. 忽略周表之外的承诺。 已签但未开始的合同、已承诺但未下单的采购,都不在这 13 周的流入流出里,却会在第 14 周出现。

本节的边界是「13 周、单一情景、稳态参数」的现金模型:它假设回款节奏和历史一致,不含新签带来的增量,也不含融资、借款和分红。它足够回答「未来一个季度哪一周最紧」,但不足以回答「如果最大客户走了会怎样」——后者需要把冲击写成参数,代入同一张表重算。下一节做这件事:把大客户流失、平台封禁、退款潮和工资税费集中到期,分别变成可复算的情景。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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