12.3 税务、发票与跨境收款

税务、发票与跨境收款是《合成小岛》收入落袋的最后一道关:把待结算毛额按增值税、预提所得税、汇兑点差与手续费四道扣减算成整数,得到每条收款路线的到账净额与净额占比,并说明哪些税率、发票口径与合规结论属于不可复现、只能观察、不能当作税务意见的边界。

站内有一篇独立游戏全球财务与多币种结构 ,那是资金架构层面的顶层设计:要不要开境外主体、多币种账户怎么搭、利润怎么回流。本节不重写那套架构,只做 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 万元)待结算毛额为例,结果是(同机相对量级,绝对数值不可移植):

路线增值税(分)所得税(分)汇兑(分)手续费(分)到账净额(分)净额占比‰
境内对公6000000200939800939
跨境电汇6000094000126901500831810831
平台钱包000010000001000
代理发行代收600000028200911800911

这张表最有用的读法是看「扣在哪」而不是「扣多少」。境内对公净额 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 与例会机制 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「minigame」更多文章

  1. 附录 F:全书公式、参数与数据集索引
  2. 9.3 同意管理与数据出境
  3. 9.2 COPPA 与儿童向游戏的限制