站内有一篇独立游戏全球财务与多币种结构 ,那是资金架构层面的顶层设计:要不要开境外主体、多币种账户怎么搭、利润怎么回流。本节不重写那套架构,只做 H5 小游戏单款口径的最后一段净额算术:一笔待结算的毛额,扣掉增值税、预提/所得税、汇兑点差、手续费之后,实际到你账户的净额是多少。分工很清楚:那篇回答「架构怎么搭」,本节回答「这一笔钱到手多少」。本节也不提供任何具体税务意见——只给一台能把整数费率算成净额的机器。
前两节把「平台扣多少、账期多久、缺口多大」算完了。钱到了平台的结算系统,还不算完——它还要过税务与收款这一关。这一关的特点是:规则不完全由你定,且各地不同。所以本节的做法和前面一样——把可复现的算术留在模型里,把不可复现的税率与合规结论标成边界。
从毛额到净额:四道扣减
一笔待结算毛额到你的账户,通常经过四道扣减,顺序固定:
| 扣减 | 作用对象 | 说明 |
|---|---|---|
| 增值税 | 毛额 | 按销售额计征,先扣 |
| 预提 / 所得税 | 增值税后净额 | 部分主体 / 跨境场景下预扣 |
| 汇兑点差 | 税后净额 | 跨境收款换汇时的买卖价差 |
| 手续费 | 税后净额 | 固定 + 比例,电汇常有固定费 |
写成整数公式(单位统一用「分」,费率用整数百分比或万分比):
vat = gross × vatPct // 100
afterVat = gross − vat
incomeTax = afterVat × incomeTaxPct // 100
afterTax = afterVat − incomeTax
fx = afterTax × fxSpreadPermyriad // 10000
transferFee= fixedFee + afterTax × transferFeePct // 100
net = afterTax − fx − transferFee
ratioPermille = net × 1000 // gross
最后一个 ratioPermille 是净额占毛额的比例——运营需要记住的就是这个数。它把四条收款路线的全部差异折叠成一个可直接比较的量。注意 vatPct 是整数百分比:写 6 表示 6%,不是 6‰。这个坑很常见,一旦单位写错,整张表都会错一个数量级。
三条收款路线,一个净额口径
H5 小游戏常见的收款路线有四条,差异不在「扣多少」而在「扣哪些」:
| 路线 | 增值税 | 预提/所得税 | 汇兑点差 | 手续费 | 特点 |
|---|---|---|---|---|---|
| 境内对公 | 有 | 通常无 | 无 | 固定小额 | 最省,但只适用境内主体 |
| 跨境电汇 | 有 | 常有 | 有 | 固定较大 | 环节最多,净额最低 |
| 平台钱包 | 无 | 无 | 无 | 无 | 留在平台内消费,不提现 |
| 代理发行代收 | 有 | 视约定 | 无 | 比例代收服务费 | 代理抽成,本质是分成 |
四条路线的费率结构完全不同,所以不能只比一个「总扣点」,必须逐项拆开。这也是本节用仿真而非拍脑袋的原因:同样是「跨境」,点差 1.5% 和手续费 0.3% 对不同毛额的影响不一样(固定手续费在大额时占比会下降)。
S 层仿真:净额模型
下面这段是 S 层仿真脚本 sim/o12_netpayout.py。它读 config/o12_netpayout.json 的四条路线,按上面的整数公式算出逐项扣减与到账净额,产出 JSON + CSV + SVG。
#!/usr/bin/env python3
# file: sim/o12_netpayout.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/o12_netpayout.py --config config/ --out out/
"""税务、发票与跨境收款净额仿真:把「待结算毛额 → 增值税 → 预提/所得税 → 汇兑点差 → 手续费
→ 到账净额」算成整数,并给出净额占毛额比例(netToGrossPermille)。
读 config/o12_netpayout.json。对每条收款路线 r(毛额 G = grossPayoutFen):
vatFen = G * vatPct // 100 # 增值税
afterVat = G - vatFen
incomeTaxFen = afterVat * incomeTaxPct // 100 # 预提 / 所得税
afterTax = afterVat - incomeTaxFen
fxFen = afterTax * fxSpreadPermyriad // 10000 # 汇率点差
transferFen = transferFeeFen + afterTax * transferFeePct // 100 # 固定 + 比例手续费
netFen = afterTax - fxFen - transferFen
ratioPermille= netFen * 1000 // G
口径:真实税率、真实合规结论、真实到账金额需真实业务环境,命令行不可复现(X)。
本脚本只做「给定整数费率」的算术,**不提供任何具体税务意见**。
产出 out/o12_netpayout.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_netpayout.json"), encoding="utf-8") as f:
return json.load(f)
def evaluate(r: dict, gross: int) -> dict:
vat = gross * r["vatPct"] // 100
after_vat = gross - vat
income_tax = after_vat * r["incomeTaxPct"] // 100
after_tax = after_vat - income_tax
fx = after_tax * r["fxSpreadPermyriad"] // 10000
transfer = r["transferFeeFen"] + after_tax * r["transferFeePct"] // 100
net = after_tax - fx - transfer
ratio = net * 1000 // gross if gross else 0
total_deduction = gross - net
return dict(r, vatFen=vat, incomeTaxFen=income_tax, fxFen=fx, transferFeeTotalFen=transfer,
netFen=net, netToGrossPermille=ratio, totalDeductionFen=total_deduction)
def draw_svg(path: str, rows: list, gross: int) -> int:
"""每条路线一根堆叠条:增值税 / 所得税 / 汇兑 / 手续费 / 到账净额(占毛额比例)。"""
W, H, PAD, ROW = 760, 74 + 62 * len(rows), 160, 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"待结算毛额 {gross} 分的逐项扣减与到账净额"
span = W - PAD - 150
segs = [("vatFen", "#fca5a5"), ("incomeTaxFen", "#fdba74"), ("fxFen", "#a5b4fc"),
("transferFeeTotalFen", "#94a3b8"), ("netFen", "#16a34a")]
for i, r in enumerate(rows):
y = 54 + i * ROW
ET.SubElement(root, "text", {"x": "16", "y": str(y + 18), "font-size": "12",
"fill": "#374151"}).text = r["name"]
ET.SubElement(root, "text", {"x": "16", "y": str(y + 34), "font-size": "11",
"fill": "#6b7280"}).text = f'到账 {r["netToGrossPermille"]}‰'
x = PAD
for key, color in segs:
w = span * r[key] // gross if gross else 0
if w > 0:
ET.SubElement(root, "rect", {"x": str(x), "y": str(y + 6), "width": str(w),
"height": "18", "fill": color})
x += w
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)
gross = cfg["grossPayoutFen"]
rows = [evaluate(r, gross) for r in cfg["routes"]]
best = max(rows, key=lambda r: r["netFen"])["id"]
worst = min(rows, key=lambda r: r["netFen"])["id"]
digest = ",".join(f'{r["id"]}:{r["netFen"]}:{r["netToGrossPermille"]}' for r in rows) + \
f";best={best},worst={worst}"
os.makedirs(a.out, exist_ok=True)
svg = draw_svg(os.path.join(a.out, "o12_netpayout.svg"), rows, gross)
with open(os.path.join(a.out, "o12_netpayout.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
cols = ("id", "name", "grossFen", "vatFen", "incomeTaxFen", "fxFen",
"transferFeeTotalFen", "totalDeductionFen", "netFen", "netToGrossPermille")
w.writerow(cols)
for r in rows:
w.writerow([gross if k == "grossFen" else r[k] for k in cols])
rep = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
"grossPayoutFen": gross, "routeCount": len(rows), "routes": rows,
"bestRoute": best, "worstRoute": worst, "digest": digest,
"csv": os.path.join(a.out, "o12_netpayout.csv"),
"svg": os.path.join(a.out, "o12_netpayout.svg"), "svgBytes": svg}
with open(os.path.join(a.out, "o12_netpayout.json"), "w", encoding="utf-8") as f:
json.dump(rep, f, ensure_ascii=False, indent=2)
print(json.dumps({"grossPayoutFen": gross,
"routes": [{"id": r["id"], "vatFen": r["vatFen"], "incomeTaxFen": r["incomeTaxFen"],
"fxFen": r["fxFen"], "transferFeeTotalFen": r["transferFeeTotalFen"],
"netFen": r["netFen"], "netToGrossPermille": r["netToGrossPermille"]}
for r in rows],
"bestRoute": best, "worstRoute": worst, "digest": digest},
ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
费率参数放在 config/o12_netpayout.json。这里的所有税率都是「公开可观察机制」口径的建模假设,不是任何税务机关或银行的实测结论:
{
"version": 1,
"game": "merge-island",
"observedAt": "2026-10",
"grossPayoutFen": 1000000,
"note": "税务、发票与跨境收款的到账净额参数。口径是「公开可观察机制」的建模假设,不是任何税务机关或银行的实测结论,也不构成任何税务意见。grossPayoutFen=待结算毛额(分);vatPct=增值税率(整数百分比,建模假设);incomeTaxPct=预提/所得税率(整数百分比,建模假设);fxSpreadPermyriad=汇率点差(万分比);transferFeeFen=固定手续费(分);transferFeePct=比例手续费(整数百分比)。真实税率、真实合规结论、真实到账金额需真实业务环境,命令行不可复现(X)。全整数,无浮点。",
"routes": [
{ "id": "domestic", "name": "境内对公", "vatPct": 6, "incomeTaxPct": 0, "fxSpreadPermyriad": 0, "transferFeeFen": 200, "transferFeePct": 0 },
{ "id": "crossborder", "name": "跨境电汇", "vatPct": 6, "incomeTaxPct": 10, "fxSpreadPermyriad": 150, "transferFeeFen": 1500, "transferFeePct": 0 },
{ "id": "wallet", "name": "平台钱包", "vatPct": 0, "incomeTaxPct": 0, "fxSpreadPermyriad": 0, "transferFeeFen": 0, "transferFeePct": 0 },
{ "id": "agent", "name": "代理发行代收", "vatPct": 6, "incomeTaxPct": 0, "fxSpreadPermyriad": 0, "transferFeeFen": 0, "transferFeePct": 3 }
]
}
在本机跑 for f in sim/o12_*.py; do /opt/homebrew/bin/python3 "$f" --config config/ --out out/; done,以 1000000 分(1 万元)待结算毛额为例,结果是(同机相对量级,绝对数值不可移植):
| 路线 | 增值税(分) | 所得税(分) | 汇兑(分) | 手续费(分) | 到账净额(分) | 净额占比‰ |
|---|---|---|---|---|---|---|
| 境内对公 | 60000 | 0 | 0 | 200 | 939800 | 939 |
| 跨境电汇 | 60000 | 94000 | 12690 | 1500 | 831810 | 831 |
| 平台钱包 | 0 | 0 | 0 | 0 | 1000000 | 1000 |
| 代理发行代收 | 60000 | 0 | 0 | 28200 | 911800 | 911 |
这张表最有用的读法是看「扣在哪」而不是「扣多少」。境内对公净额 939‰,几乎只被增值税吃掉;跨境电汇净额掉到 831‰,多出来的损失主要来自预提所得税(94000 分)与汇兑点差(12690 分),固定手续费 1500 分相对是小头。代理发行代收的 28200 分手续费,本质是代理的分成,它和增值税叠加后净额 911‰。平台钱包 1000‰ 看着最漂亮——但那笔钱留在平台内,要么用于平台内投放,要么仍要另走一道提现扣减,不能直接当现金花。「净额最高」的路线未必是「现金最好」的路线。
发票与合规:只做工程落点,不做法律意见
上面算的是「钱到手多少」。和钱一起走的还有票据:增值税发票怎么开、进项怎么抵、跨境怎么备案。这些不属于本节可复现的范围——它们是税务与法务的专业判断,本节只给出工程上要留的落点:
| 落点 | 工程上要做什么 |
|---|---|
| 发票台账 | 每笔结算记录对应发票号、开票主体、税率,可回溯 |
| 进项归集 | 投放、服务器、外包等进项发票按主体归集,供抵扣 |
| 跨境申报 | 收款路线、币种、到账日逐笔留痕,供申报与对账 |
| 主体归属 | 收入记在哪个主体、按哪个税率,与资金架构一致 |
这张表是检查清单,不是税务建议。真正的税率适用、发票开具口径、跨境合规结论,必须由具备资质的专业人士结合主体与地区判断。本节的作用是确保你的数据留得下来、对得上,而不是替你下结论。
X 层:真实税率与合规结论不可复现
边界必须划清。上面算的是模型侧:给定整数费率,净额是多少。下面这些属于 X 层,需真实业务环境,命令行不可复现:
- 真实的适用税率(由主体性质、地区、业务类型共同决定,且会随政策调整);
- 真实的合规结论(发票能不能开、进项能不能抵、跨境要不要备案,都是专业判断);
- 真实的到账金额与换汇价(银行实际成交价、到账日汇率、中间行扣费,逐笔不同)。
工程上正确的做法是:把可复现的净额算术与不可复现的税务判断分开——算术层回答「如果费率是 X,净额是多少」,判断层交给专业人士。任何时候都不要把模型算出来的净额当成「税务结论」——它只是一个给定假设下的算术结果。
自检表
| 检查项 | 合格标准 |
|---|---|
| 扣减顺序 | 增值税 → 所得税 → 汇兑 → 手续费,顺序固定 |
| 单位一致 | 百分比与万分比分清,不混用 |
| 逐项留痕 | 每笔扣减单独列出,不只给净额 |
| 路线可比 | 用净额占比统一比较,不用总扣点糊弄 |
| 边界清晰 | 真实税率与合规结论标 X,不写成「实测」 |
| 不给意见 | 只做工程落点清单,不下税务结论 |
第 12 章到这里把「平台 → 你」这条资金链走完了:12.1 算清扣点与账期,12.2 看清现金缺口,12.3 落到净额与票据。三节合起来,就是一款 H5 小游戏从「后台显示收入」到「银行到账」的完整路径。下一章我们换到运营侧——不再算钱,而是把运营节奏与团队协作机制化。
阅读导航:上一节:12.2 现金流与收入确认 · 下一节:13.1 运营 SOP 与例会机制 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。