站内有一篇独立游戏全球财务与多币种结构 ,那是资金架构层面的顶层设计:境外主体、多币种账户、利润回流路径怎么搭。本节不碰那套架构,只做H5 小游戏单款产品口径的结算算术:一笔广告收入或内购流水,经过平台、渠道、代扣几道手,最后落到你账上的净额是多少、隔多少天到账。站内《上线变现与长线运营》 把 eCPM、LTV、回本建过模,但那是「赚多少」;本节接上最后一环——「赚到的钱什么时候、以什么净额到账」。两者的分工很清楚:前两者回答钱从哪来、架构怎么搭,本节回答钱怎么落袋。
前面十一章我们把「怎么投、怎么接、怎么审」走了一遍。到这一章要面对一个更朴素的问题:收入不是现金。后台显示的「本月收入」是毛流水,真正进你银行账户的是一个更小的数,而且要等。搞清楚这两件事——扣多少、等多久——是运营操盘手必须有的账感。
分成这件事,拆成三个扣点
一个 H5 小游戏平台上的收入,通常来自两条通道:广告(IAA)与内购(IAP)。两条通道的扣点不是同一个数:
| 扣点 | 作用对象 | 谁在收 | 口径 |
|---|---|---|---|
| 广告分成 | IAA 毛收入 | 平台(含聚合网络) | 千分比 |
| 内购分成 | IAP 毛流水 | 平台 / 支付通道 | 千分比 |
| 代扣代缴 | 扣点后的净额 | 税务 / 平台代扣 | 千分比 |
顺序很重要:扣点是先扣的,代扣是在扣点之后的净额上再扣。把顺序写反,账就对不上。用整数表达就是:
iaaNet = grossIaa × (1000 − iaaShare) // 1000
iapNet = grossIap × (1000 − iapShare) // 1000
subtotal = iaaNet + iapNet
withhold = subtotal × withholdPermille // 1000
netSettle = subtotal − withhold
takePermille= (gross − netSettle) × 1000 // gross
最后一个 takePermille 是综合扣点率——它把两三条通道、两三层扣点折叠成一个数。操盘手口头说的「这个平台抽多少」,指的通常就是它。注意它不是把各档扣点简单相加:因为广告和内购的毛额不同,加权之后才是真实综合扣点。
结算周期:钱从赚到到到手的时间
扣点决定到手多少,结算周期决定多久到手。这两件事合起来才是完整的账。结算周期本身又分两段:
- 结算周期:平台按月结还是半月结,账单周期多长;
- 冻结/账期:结算之后还要压多久才打款,部分平台会留一段冻结期防退款与作弊。
两段相加才是「钱从赚到那天到真正到账那天」的天数。这个天数直接决定你放量时要垫多少营运资金——账期越长,你越像一个给平台垫资的银行。
把净额和账期合起来,可以定义一个粗略的「平均在途资金」:假设结算额按月均匀产生,账期 T 天,则平均有 netSettle × T / 30 的钱卡在平台上。这个数不是精确的财务口径,但它给你一个量级感:账期从 30 天拉到 60 天,你被占用的现金大致翻倍。
S 层仿真:把每个平台算到底
下面这段是 S 层仿真脚本 sim/o12_settlement.py。它读 config/o12_settlement.json 的四个平台,按上面的整数公式算出每个平台的综合扣点率、账期天数、平均在途资金,并汇总全网口径,产出 JSON + CSV + SVG。
#!/usr/bin/env python3
# file: sim/o12_settlement.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/o12_settlement.py --config config/ --out out/
"""平台分成与结算周期仿真:把 H5 小游戏每个平台/渠道的「毛收入 → 扣点 → 代扣 → 到账」
算成整数,并给出综合扣点率、账期天数与平均在途资金。
对 config/o12_settlement.json 的每个平台:
iaaNetFen = grossIaaFen * (1000 - iaaSharePermille) // 1000 # 广告净额(分)
iapNetFen = grossIapFen * (1000 - iapSharePermille) // 1000 # 内购净额(分)
subtotalFen = iaaNetFen + iapNetFen
withholdFen = subtotalFen * withholdPermille // 1000 # 代扣代缴
netSettleFen= subtotalFen - withholdFen # 实际到账
takePermille= (grossFen - netSettleFen) * 1000 // grossFen # 综合扣点(千分比)
cashDays = settleCycleDays + holdDays # 结算周期 + 冻结期
floatFen = netSettleFen * cashDays // periodDays # 平均在途资金
口径:真实分成比例、真实结算单、真实账期需真实业务环境,命令行不可复现(X)。
产出 out/o12_settlement.json + .csv + .svg。全整数、地板除,无浮点。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET
def load(d: str) -> dict:
with open(os.path.join(d, "o12_settlement.json"), encoding="utf-8") as f:
return json.load(f)
def evaluate(p: dict, period_days: int) -> dict:
iaa_net = p["grossIaaFen"] * (1000 - p["iaaSharePermille"]) // 1000
iap_net = p["grossIapFen"] * (1000 - p["iapSharePermille"]) // 1000
subtotal = iaa_net + iap_net
withhold = subtotal * p["withholdPermille"] // 1000
net = subtotal - withhold
gross = p["grossIaaFen"] + p["grossIapFen"]
take = (gross - net) * 1000 // gross if gross else 0
cash_days = p["settleCycleDays"] + p["holdDays"]
float_fen = net * cash_days // period_days
return dict(p, grossFen=gross, iaaNetFen=iaa_net, iapNetFen=iap_net, subtotalFen=subtotal,
withholdFen=withhold, netSettleFen=net, effectiveTakePermille=take,
cashDays=cash_days, floatFen=float_fen)
def draw_svg(path: str, rows: list, total_take: int) -> int:
"""每个平台两根横条:综合扣点(‰)与到账净额(相对最大值),右侧标注账期。"""
W, H, PAD, ROW = 760, 70 + 62 * len(rows), 180, 62
root = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg", "width": str(W),
"height": str(H), "viewBox": f"0 0 {W} {H}"})
ET.SubElement(root, "rect", {"width": str(W), "height": str(H), "fill": "#ffffff"})
ET.SubElement(root, "text", {"x": "16", "y": "28", "font-size": "15",
"fill": "#111827"}).text = (
f"H5 小游戏平台分成与账期(综合扣点‰ / 到账净额,全网综合 {total_take}‰)")
span = W - PAD - 190
max_net = max(r["netSettleFen"] for r in rows) or 1
for i, r in enumerate(rows):
y = 54 + i * ROW
ET.SubElement(root, "text", {"x": "16", "y": str(y + 16), "font-size": "12",
"fill": "#374151"}).text = r["name"]
ET.SubElement(root, "text", {"x": "16", "y": str(y + 32), "font-size": "11",
"fill": "#6b7280"}).text = f'账期 {r["cashDays"]} 天'
take_w = span * min(r["effectiveTakePermille"], 600) // 600
ET.SubElement(root, "rect", {"x": str(PAD), "y": str(y + 6), "width": str(take_w),
"height": "16", "rx": "3", "fill": "#f97316"})
ET.SubElement(root, "text", {"x": str(PAD + take_w + 6), "y": str(y + 18), "font-size": "11",
"fill": "#6b7280"}).text = f'扣点 {r["effectiveTakePermille"]}‰'
net_w = span * r["netSettleFen"] // max_net
ET.SubElement(root, "rect", {"x": str(PAD), "y": str(y + 28), "width": str(net_w),
"height": "16", "rx": "3", "fill": "#2563eb"})
ET.SubElement(root, "text", {"x": str(PAD + net_w + 6), "y": str(y + 40), "font-size": "11",
"fill": "#6b7280"}).text = f'到账 {r["netSettleFen"]}分'
os.makedirs(os.path.dirname(path), exist_ok=True)
ET.ElementTree(root).write(path, encoding="utf-8", xml_declaration=True)
return len(ET.tostring(root))
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
a = ap.parse_args()
cfg = load(a.config)
period = cfg["periodDays"]
rows = [evaluate(p, period) for p in cfg["platforms"]]
total_gross = sum(r["grossFen"] for r in rows)
total_net = sum(r["netSettleFen"] for r in rows)
total_take = (total_gross - total_net) * 1000 // total_gross if total_gross else 0
total_float = sum(r["floatFen"] for r in rows)
# 跨语言对拍摘要:core/test/o12_settlement.test.ts 复算同一串数字,逐位比对。
digest = ",".join(
f'{r["id"]}:{r["effectiveTakePermille"]}:{r["cashDays"]}:{r["netSettleFen"]}'
for r in rows) + f";totalNet={total_net},totalTake={total_take}"
os.makedirs(a.out, exist_ok=True)
svg = draw_svg(os.path.join(a.out, "o12_settlement.svg"), rows, total_take)
with open(os.path.join(a.out, "o12_settlement.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
cols = ("id", "name", "grossFen", "iaaNetFen", "iapNetFen", "withholdFen",
"netSettleFen", "effectiveTakePermille", "settleCycleDays", "holdDays",
"cashDays", "floatFen")
w.writerow(cols)
for r in rows:
w.writerow([r[k] for k in cols])
rep = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
"periodDays": period, "platformCount": len(rows), "platforms": rows,
"totalGrossFen": total_gross, "totalNetFen": total_net,
"totalTakePermille": total_take, "totalFloatFen": total_float,
"worstTake": max(rows, key=lambda r: r["effectiveTakePermille"])["id"],
"longestCash": max(rows, key=lambda r: r["cashDays"])["id"],
"digest": digest, "csv": os.path.join(a.out, "o12_settlement.csv"),
"svg": os.path.join(a.out, "o12_settlement.svg"), "svgBytes": svg}
with open(os.path.join(a.out, "o12_settlement.json"), "w", encoding="utf-8") as f:
json.dump(rep, f, ensure_ascii=False, indent=2)
print(json.dumps({"platforms": [{"id": r["id"], "grossFen": r["grossFen"],
"netSettleFen": r["netSettleFen"],
"effectiveTakePermille": r["effectiveTakePermille"],
"cashDays": r["cashDays"], "floatFen": r["floatFen"]}
for r in rows],
"totalGrossFen": total_gross, "totalNetFen": total_net,
"totalTakePermille": total_take, "totalFloatFen": total_float,
"digest": digest, "worstTake": rep["worstTake"],
"longestCash": rep["longestCash"]}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
平台参数放在 config/o12_settlement.json。注意这里的比例是「公开可观察机制」口径的建模假设,不是任何平台结算单的实测:真实分成比例、真实账期属于 X 层——需真实业务环境,命令行不可复现。
{
"version": 1,
"game": "merge-island",
"observedAt": "2026-10",
"periodDays": 30,
"note": "H5 小游戏平台分成与结算周期参数。口径是「公开可观察机制」的建模假设,不是任何平台后台结算单实测。grossIaaFen=广告毛收入(分/月);grossIapFen=内购毛流水(分/月);iaaSharePermille/iapSharePermille=平台(含渠道/支付通道)扣点千分比;withholdPermille=代扣代缴千分比(在扣点之后);settleCycleDays=结算周期(天);holdDays=冻结/账期(天)。真实分成比例、真实结算单、真实账期需真实业务环境,命令行不可复现(X)。全整数,无浮点。",
"platforms": [
{ "id": "wechat", "name": "微信小游戏", "grossIaaFen": 5000000, "grossIapFen": 2000000, "iaaSharePermille": 400, "iapSharePermille": 300, "withholdPermille": 0, "settleCycleDays": 30, "holdDays": 15 },
{ "id": "douyin", "name": "抖音小游戏", "grossIaaFen": 3000000, "grossIapFen": 1000000, "iaaSharePermille": 350, "iapSharePermille": 300, "withholdPermille": 0, "settleCycleDays": 15, "holdDays": 7 },
{ "id": "h5_web", "name": "海外 H5 平台", "grossIaaFen": 1200000, "grossIapFen": 400000, "iaaSharePermille": 300, "iapSharePermille": 300, "withholdPermille": 0, "settleCycleDays": 30, "holdDays": 0 },
{ "id": "union", "name": "联运渠道", "grossIaaFen": 800000, "grossIapFen": 300000, "iaaSharePermille": 500, "iapSharePermille": 500, "withholdPermille": 30, "settleCycleDays": 45, "holdDays": 30 }
]
}
在本机跑 for f in sim/o12_*.py; do /opt/homebrew/bin/python3 "$f" --config config/ --out out/; done,这一条链跑出来的结果(同机相对量级,绝对数值不可移植):
| 平台 | 毛收入(分) | 到账净额(分) | 综合扣点‰ | 账期(天) | 平均在途(分) |
|---|---|---|---|---|---|
| 微信小游戏 | 7000000 | 4400000 | 371 | 45 | 6600000 |
| 抖音小游戏 | 4000000 | 2650000 | 337 | 22 | 1943333 |
| 海外 H5 平台 | 1600000 | 1120000 | 300 | 30 | 1120000 |
| 联运渠道 | 1100000 | 533500 | 515 | 75 | 1333750 |
| 全网 | 13700000 | 8703500 | 364 | — | 10997083 |
这张表把「哪个平台更划算」这件事拆成了两个独立维度。看综合扣点:联运渠道 515‰ 最狠,海外 H5 300‰ 最轻。看账期:联运渠道 75 天最长,抖音 22 天最短。真正麻烦的是两个维度同时差的平台——联运渠道扣点最高、账期最长,正是「又贵又慢」。反之抖音扣点 337‰ 不算最低,但 22 天账期让它成为现金流最友好的平台。只看扣点不看账期,是运营最常见的账盲。
P 层内核:跨语言对拍,证明公式没写错
一张表你没法确认「是模型对」还是「算错了碰巧看着对」。本套用一份 P 层 TypeScript 内核 core/src/o12/settlement.ts 复算同一条链:同样的整数地板除、同样的扣点顺序、同样的账期定义。它读同一份 config/o12_settlement.json,产出和 Python 逐字符一致的摘要。
// file: core/src/o12/settlement.ts
// tier: P
// verify: node --test 'core/test/o12_*.test.ts'
//
// 平台分成与结算周期内核:与 sim/o12_settlement.py 逐位等价的 TypeScript 对偶实现。
// 读同一份 config/o12_settlement.json;全整数、地板除、无浮点、无 Math.random。
export interface Platform {
readonly id: string; readonly name: string;
readonly grossIaaFen: number; readonly grossIapFen: number;
readonly iaaSharePermille: number; readonly iapSharePermille: number;
readonly withholdPermille: number;
readonly settleCycleDays: number; readonly holdDays: number;
}
export interface SettlementConfig {
readonly version: number; readonly game: string; readonly periodDays: number;
readonly platforms: readonly Platform[];
}
export interface PlatformResult {
readonly id: string; readonly grossFen: number;
readonly iaaNetFen: number; readonly iapNetFen: number;
readonly withholdFen: number; readonly netSettleFen: number;
readonly effectiveTakePermille: number; readonly cashDays: number; readonly floatFen: number;
}
/** 单个平台:毛收入 → 扣点 → 代扣 → 到账净额、综合扣点率、账期、平均在途。 */
export function evaluate(p: Platform, periodDays: number): PlatformResult {
const iaaNetFen = Math.floor((p.grossIaaFen * (1000 - p.iaaSharePermille)) / 1000);
const iapNetFen = Math.floor((p.grossIapFen * (1000 - p.iapSharePermille)) / 1000);
const subtotalFen = iaaNetFen + iapNetFen;
const withholdFen = Math.floor((subtotalFen * p.withholdPermille) / 1000);
const netSettleFen = subtotalFen - withholdFen;
const grossFen = p.grossIaaFen + p.grossIapFen;
const effectiveTakePermille = grossFen ? Math.floor(((grossFen - netSettleFen) * 1000) / grossFen) : 0;
const cashDays = p.settleCycleDays + p.holdDays;
const floatFen = Math.floor((netSettleFen * cashDays) / periodDays);
return { id: p.id, grossFen, iaaNetFen, iapNetFen, withholdFen, netSettleFen,
effectiveTakePermille, cashDays, floatFen };
}
export function evaluateAll(cfg: SettlementConfig): PlatformResult[] {
return cfg.platforms.map((p) => evaluate(p, cfg.periodDays));
}
export function totals(rows: readonly PlatformResult[]): { grossFen: number; netFen: number; takePermille: number; floatFen: number } {
const grossFen = rows.reduce((s, r) => s + r.grossFen, 0);
const netFen = rows.reduce((s, r) => s + r.netSettleFen, 0);
const floatFen = rows.reduce((s, r) => s + r.floatFen, 0);
const takePermille = grossFen ? Math.floor(((grossFen - netFen) * 1000) / grossFen) : 0;
return { grossFen, netFen, takePermille, floatFen };
}
/** 跨语言对拍摘要:与 sim/o12_settlement.py 的 digest 逐字符一致。 */
export function digest(rows: readonly PlatformResult[]): string {
const t = totals(rows);
return rows.map((r) => `${r.id}:${r.effectiveTakePermille}:${r.cashDays}:${r.netSettleFen}`)
.join(",") + `;totalNet=${t.netFen},totalTake=${t.takePermille}`;
}
配套测试 core/test/o12_settlement.test.ts 把 Python 落盘的摘要写成金标准,断言 TS 内核复算结果逐字符相等,并顺手验扣点顺序与账期定义。在本机跑 node --test 'core/test/o12_*.test.ts',5 个用例全过;两套独立实现给出同一个摘要:
✔ 跨语言一致:TS 结算摘要 == Python 金标准 (0.573541ms)
✔ 综合扣点 = (毛收入 - 到账净额) × 1000 // 毛收入 (0.087708ms)
✔ 代扣代缴在扣点之后:union 有 3% 代扣 (0.07275ms)
✔ 账期 = 结算周期 + 冻结期;平均在途 = 净额 × 账期 // 月天数 (0.068791ms)
✔ 全网口径:综合扣点与在途资金汇总 (0.079166ms)
ℹ tests 5 ℹ pass 5 ℹ fail 0
digest = wechat:371:45:4400000,douyin:337:22:2650000,h5_web:300:30:1120000,union:515:75:533500;totalNet=8703500,totalTake=364
只要有一边的扣点顺序、整数语义、账期定义写错,摘要立刻分叉。这和卷二「一份逻辑、两种宿主」 是同一个信念:同一个数,在 Python 仿真、TS 内核、浏览器原型里必须长得一模一样。
X 层:真实结算单不可复现
边界必须划清。上面算的是模型侧:给定扣点比例与账期,到手多少、多久到手。而下面这些属于 X 层,需真实业务环境,命令行不可复现:
- 真实的平台分成比例与结算条款(各家不同、会调整、常有阶梯与激励返点,公开渠道只能看到大致区间);
- 真实的结算单与账期(含对账差异、退款扣减、作弊扣减,平台侧逐笔核算);
- 真实的代扣代缴规则(由税务政策与主体性质共同决定,不是平台单方面说了算)。
这些都不是「跑个脚本就能知道」的。工程上正确的做法是:把可复现的模型层与不可复现的真实数据层分开——模型层用来做「如果分成是 X、账期是 Y,结果如何」,真实数据层用来在拿到结算单后反标定模型参数(把实际扣点与账期填回 config,模型立刻反映现实)。这也是第 16 章全链路对账要跟这些字段一一对应的原因。
自检表
| 检查项 | 合格标准 |
|---|---|
| 扣点顺序 | 代扣在扣点之后,不颠倒 |
| 双通道 | 广告与内购分别算,不混为一谈 |
| 综合扣点 | 用加权口径,不做简单相加 |
| 账期拆解 | 结算周期与冻结期分开列 |
| 在途量化 | 给出平均在途资金,不只看净额 |
| 边界清晰 | 真实结算单与代扣规则标 X,不写成「实测」 |
分成和账期算清楚了,只是把「收入」这一列填满。但收入不等于现金——你账上确认了 13.1 万收入,不代表这 13.1 万现在就能花。下一节我们就把这条时间差单独拉出来,看现金流缺口到底有多大、峰值出现在哪一天。
阅读导航:上一节:11.3 资质倒排与上线前置 · 下一节:12.2 现金流与收入确认 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。