9.1 渠道分级与对接清单

渠道分级决定有限的人力该投到哪个渠道:把每个渠道的「预期新增 × 累计活跃 × ARPDAU → 毛收入 → 扣渠道分成 → 扣对接成本 → 净贡献」整条链算成整数,按净贡献阈值分出 S/A/B/C 四级,再按级展开对接清单;配可跑的 Python 仿真,净贡献为负的渠道直接不接。

站内有一篇《H5 游戏产业全景》 ,它从产业视角讲清了联运、分成与多渠道冗余的格局与风险——那是「为什么要做多渠道」的问题;而《上线变现与长线运营》 第 5 章给的是买量渠道的机制框架(见买量渠道与投放机制 )。本节与两者分工不同:它只处理联运渠道的分级与对接清单——当一款游戏要同时上十几个渠道时,人力有限,哪些渠道值得单独排活动位与人力、哪些只做最低成本接入、哪些直接不接,需要一个可复算的判据。上一节8.3 广告数据与隐私 收尾了变现合规,本章开始处理渠道合作。

渠道不是越多越好,先分级

一个渠道从「谈」到「上」要花的钱和人力是实打实的:商务对接、SDK 联调、渠道包制作与提审、结算账户与发票、专属活动位排期、数据回传对账。一个渠道走完这套流程,轻则几天、重则几周。如果每个渠道都用同一套规格对待,有限的运营人力会被摊薄到每个渠道都不痛不痒。

所以第一件事是分级:把每个渠道按「接进来能带来多少净贡献」排序,分成 S/A/B/C 四级,再按级配资源。级别不是渠道名气的排序,而是净贡献的排序——这决定了后面对接清单的深度。

净贡献公式:从新增到净收入

一个渠道值不值得接,看的是净贡献——扣掉渠道分成与对接成本之后,还剩多少。写成整数公式(金额统一用「分」,避免浮点):

ltvDeciFen = arpdauDeciFen × activeDaysPermille // 1000    # 每安装累计 LTV(0.1 分)
grossFen   = newUsers × ltvDeciFen // 10                   # 毛收入(分)
netFen     = grossFen × (1000 − sharePermille) // 1000 − integrationCostFen

activeDaysPermille 是每个安装在整个生命周期里的累计活跃日(千分比日),它把「这个渠道来的用户留多久」折成了一个乘数;sharePermille 是渠道拿走的分成(300 = 30%);integrationCostFen 是一次性对接成本。三个因子决定净贡献:量(newUsers)、质(LTV)、价(分成)。分成高的渠道不一定差——如果它带来的用户留存与 ARPDAU 都高,扣完成成之后仍然赚;反过来,量大的渠道也可能因为分成太高而净贡献平庸。

S 层仿真:六个渠道的净贡献与分级

下面这段是 S 层仿真脚本 sim/o09_channel_tier.py。它读 config/o09_channel_tier.json 的六个渠道,用整数公式算出每个渠道的净贡献,再按阈值分级,产出 JSON + CSV + SVG。

#!/usr/bin/env python3
# file: sim/o09_channel_tier.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/o09_channel_tier.py --config config/ --out out/
"""渠道分级仿真:读 config/o09_channel_tier.json,把每个渠道的「新增 × 累计活跃 × ARPDAU
→ 毛收入 → 扣渠道分成 → 扣对接成本 → 净贡献」整条链算成整数,再按净贡献阈值分级。

对每个渠道:

    ltvDeciFen   = arpdauDeciFen * activeDaysPermille // 1000     # 每安装累计 LTV(0.1 分)
    grossFen     = newUsers * ltvDeciFen // 10                    # 毛收入(分)
    netFen       = grossFen * (1000 - sharePermille) // 1000 - integrationCostFen
    tier         = 净贡献 >= 阈值 S → S;否则 >= A → A;否则 >= B → B;否则 C
    netNegative  = netFen < 0  (净贡献为负,属于「暂缓接入」)

分级是渠道运营的资源分配依据:S 级渠道值得单独排活动位与人力,C 级渠道只做最低成本
接入,净贡献为负的渠道直接不接。产出 out/o09_channel_tier.json + .csv + .svg。
全程整数、地板除,无浮点。渠道分成、结算单属真实业务环境,命令行不可复现(X)。
"""
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, "o09_channel_tier.json"), encoding="utf-8") as f:
        return json.load(f)


def evaluate(ch: dict, th: dict) -> dict:
    ltv = ch["arpdauDeciFen"] * ch["activeDaysPermille"] // 1000
    gross = ch["newUsers"] * ltv // 10
    net = gross * (1000 - ch["sharePermille"]) // 1000 - ch["integrationCostFen"]
    if net >= th["S"]:
        tier = "S"
    elif net >= th["A"]:
        tier = "A"
    elif net >= th["B"]:
        tier = "B"
    else:
        tier = "C"
    return {**ch, "ltvDeciFen": ltv, "grossFen": gross, "netFen": net,
            "tier": tier, "netNegative": net < 0,
            "netPerCostPermille": net * 1000 // ch["integrationCostFen"] if ch["integrationCostFen"] else 0}


def draw_svg(path: str, rows: list) -> int:
    """每个渠道一根横条:净贡献(分),阈值线标出 S/A/B 边界,负值标红。"""
    W, PAD, ROW = 760, 220, 40
    H = 100 + ROW * len(rows)
    lo = min([r["netFen"] for r in rows] + [0])
    hi = max([r["netFen"] for r in rows] + [1])
    span = W - PAD - 120
    scale = lambda v: PAD + span * (v - lo) // (hi - lo) if hi > lo else PAD
    svg = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg", "width": str(W), "height": str(H),
                             "viewBox": f"0 0 {W} {H}"})
    ET.SubElement(svg, "rect", {"width": str(W), "height": str(H), "fill": "#ffffff"})
    ET.SubElement(svg, "text", {"x": "16", "y": "28", "font-size": "15",
                                "fill": "#111827"}).text = "各渠道净贡献(分)· 负值=暂缓接入"
    x0 = scale(0)
    ET.SubElement(svg, "line", {"x1": str(x0), "y1": "44", "x2": str(x0), "y2": str(H - 16),
                                "stroke": "#ef4444", "stroke-dasharray": "4 4"})
    for i, r in enumerate(rows):
        y = 56 + i * ROW
        color = "#ef4444" if r["netFen"] < 0 else ("#16a34a" if r["tier"] in ("S", "A") else "#94a3b8")
        ET.SubElement(svg, "text", {"x": "16", "y": str(y + 18), "font-size": "12",
                                    "fill": "#374151"}).text = f'{r["tier"]} · {r["name"]}'
        xs, xe = sorted((x0, scale(r["netFen"])))
        ET.SubElement(svg, "rect", {"x": str(xs), "y": str(y + 4), "width": str(max(xe - xs, 1)),
                                    "height": "18", "rx": "3", "fill": color})
        ET.SubElement(svg, "text", {"x": str(xe + 6), "y": str(y + 18), "font-size": "11",
                                    "fill": "#6b7280"}).text = f'{r["netFen"]} 分'
    os.makedirs(os.path.dirname(path), exist_ok=True)
    ET.ElementTree(svg).write(path, encoding="utf-8", xml_declaration=True)
    return len(ET.tostring(svg))


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)
    th = cfg["tierThresholdsFen"]
    rows = [evaluate(ch, th) for ch in cfg["channels"]]

    hist = {t: 0 for t in ("S", "A", "B", "C")}
    for r in rows:
        hist[r["tier"]] += 1
    neg = [r["id"] for r in rows if r["netFen"] < 0]
    # 分级后取每级的推荐渠道清单(净贡献降序),供「对接清单」按级展开
    by_tier = {t: [r["id"] for r in sorted(rows, key=lambda x: -x["netFen"]) if r["tier"] == t]
               for t in ("S", "A", "B", "C")}
    digest = ",".join(f'{r["id"]}:{r["ltvDeciFen"]}:{r["grossFen"]}:{r["netFen"]}:{r["tier"]}'
                      for r in rows) + ";" + ",".join(f"{t}={hist[t]}" for t in ("S", "A", "B", "C"))

    os.makedirs(a.out, exist_ok=True)
    svg = draw_svg(os.path.join(a.out, "o09_channel_tier.svg"), rows)
    with open(os.path.join(a.out, "o09_channel_tier.csv"), "w", encoding="utf-8", newline="") as f:
        w = csv.writer(f)
        w.writerow(["id", "name", "kind", "newUsers", "arpdauDeciFen", "activeDaysPermille",
                    "sharePermille", "integrationCostFen", "ltvDeciFen", "grossFen", "netFen",
                    "netPerCostPermille", "tier", "netNegative"])
        for r in rows:
            w.writerow([r["id"], r["name"], r["kind"], r["newUsers"], r["arpdauDeciFen"],
                        r["activeDaysPermille"], r["sharePermille"], r["integrationCostFen"],
                        r["ltvDeciFen"], r["grossFen"], r["netFen"], r["netPerCostPermille"],
                        r["tier"], r["netNegative"]])
    rep = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
           "tierThresholdsFen": th, "channelCount": len(rows), "channels": rows,
           "histogram": hist, "netNegativeIds": neg, "byTier": by_tier,
           "onboardingChecklist": cfg["onboardingChecklist"], "digest": digest,
           "csv": os.path.join(a.out, "o09_channel_tier.csv"),
           "svg": os.path.join(a.out, "o09_channel_tier.svg"), "svgBytes": svg}
    with open(os.path.join(a.out, "o09_channel_tier.json"), "w", encoding="utf-8") as f:
        json.dump(rep, f, ensure_ascii=False, indent=2)
    print(json.dumps({"digest": digest, "histogram": hist, "netNegativeIds": neg, "byTier": by_tier,
                      "channels": [(r["id"], r["netFen"], r["tier"]) for r in rows]},
                     ensure_ascii=False, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

渠道参数放在 config/o09_channel_tier.json。这里的数字是「公开可观察机制」口径的建模假设,不是任何渠道后台或结算单的实测——真实分成比例与真实结算单属于 X 层:

{
  "version": 1,
  "game": "merge-island",
  "observedAt": "2026-10",
  "note": "渠道分级参数。口径是「公开可观察机制」的建模假设,不是任何渠道后台或结算单的实测。newUsers=该渠道预期月新增(公开可观察机制假设);arpdauDeciFen=0.1 分/DAU/日;activeDaysPermille=每安装累计活跃日千分比;sharePermille=渠道分成千分比(300=30%);integrationCostFen=一次性对接成本(分,含 SDK 联调与渠道包制作)。真实分成比例、真实结算单需真实业务环境,命令行不可复现(X)。全整数,无浮点。",
  "tierThresholdsFen": { "S": 5000000, "A": 3000000, "B": 500000 },
  "channels": [
    { "id": "douyin", "name": "抖音小游戏", "kind": "minigame", "newUsers": 90000, "arpdauDeciFen": 380, "activeDaysPermille": 2800, "sharePermille": 200, "integrationCostFen": 800000 },
    { "id": "wechat", "name": "微信小游戏", "kind": "minigame", "newUsers": 120000, "arpdauDeciFen": 350, "activeDaysPermille": 2600, "sharePermille": 400, "integrationCostFen": 600000 },
    { "id": "appstore", "name": "应用宝联运", "kind": "store", "newUsers": 60000, "arpdauDeciFen": 420, "activeDaysPermille": 3200, "sharePermille": 300, "integrationCostFen": 1200000 },
    { "id": "huawei", "name": "华为应用市场", "kind": "store", "newUsers": 30000, "arpdauDeciFen": 460, "activeDaysPermille": 3400, "sharePermille": 500, "integrationCostFen": 1500000 },
    { "id": "union", "name": "第三方联运联盟", "kind": "union", "newUsers": 40000, "arpdauDeciFen": 180, "activeDaysPermille": 1600, "sharePermille": 600, "integrationCostFen": 300000 },
    { "id": "oppo", "name": "OPPO 软件商店", "kind": "store", "newUsers": 20000, "arpdauDeciFen": 300, "activeDaysPermille": 2200, "sharePermille": 450, "integrationCostFen": 900000 }
  ],
  "onboardingChecklist": {
    "S": ["商务对接与合同签署", "SDK 接入与联调", "渠道包制作与提审", "结算账户与发票信息", "渠道专属活动位排期", "数据回传与对账口径"],
    "A": ["商务对接与合同签署", "SDK 接入与联调", "渠道包制作与提审", "结算账户与发票信息"],
    "B": ["SDK 接入与联调", "渠道包制作与提审", "结算账户与发票信息"],
    "C": ["渠道包制作与提审", "结算账户与发票信息"]
  }
}

在本机跑 for f in sim/o09_*.py; do /opt/homebrew/bin/python3 "$f" --config config/ --out out/; done,六个渠道的净贡献与分级是(同机相对量级,绝对数值不可移植):

渠道新增每安装 LTV(0.1分)毛收入(分)分成对接成本(分)净贡献(分)级别
抖音小游戏900001064957600020%8000006860800S
微信小游戏1200009101092000040%6000005952000S
应用宝联运600001344806400030%12000004444800A
华为应用市场300001564469200050%1500000846000B
第三方联运联盟40000288115200060%300000160800C
OPPO 软件商店20000660132000045%900000−174000C

规律很干净:决定级别的是净贡献,不是渠道名气。微信的毛收入最高(10920000 分),但 40% 的分成把它压到了第二;华为每安装 LTV 最高(1564),但只有 30000 新增、50% 分成加 1500000 对接成本,净贡献掉到 B 级。最刺眼的是 OPPO——净贡献 −174000 分,毛收入还不够覆盖分成与对接成本,属于净贡献为负,直接不接。这就是分级的价值:它把「这个渠道要不要接」从拍脑袋变成了一个能复算的数。

分级之后:按级展开对接清单

分级不是终点,是资源分配的依据。同一套对接动作,对不同级别应该有不同的深度:

级别对接深度对接清单
S专人跟进、单独排活动位商务对接与合同、SDK 接入与联调、渠道包制作与提审、结算账户与发票、渠道专属活动位、数据回传与对账口径
A常规对接、纳入统一排期商务对接与合同、SDK 接入与联调、渠道包制作与提审、结算账户与发票
B最低成本接入SDK 接入与联调、渠道包制作与提审、结算账户与发票
C只做技术接入渠道包制作与提审、结算账户与发票

清单本身是 D 层配置(config/o09_channel_tier.json 的 onboardingChecklist),S 脚本会把它随分级结果一起落盘——级别一变,清单自动跟着变,不需要人工维护一张对不上的表。注意清单的最底层(C 级)也保留了「渠道包制作与提审」和「结算账户与发票」两项:再小的渠道,包和账也是必须的;能省的只是商务、活动位与对账深度。

X 层:真实分成与结算单不可复现

边界必须划清。上面算的是模型侧:给定新增、LTV、分成与对接成本,净贡献是多少。下面这些属于 X 层,需真实业务环境,命令行不可复现:

  • 真实的渠道分成比例(同一渠道对不同产品、不同量级能给到不同的点位,且常带保底与阶梯);
  • 真实的渠道新增与留存(由渠道流量质量决定,且渠道侧不透明);
  • 真实的对接成本(SDK 联调工时、渠道包提审往返、活动位排期冲突都是变量)。

工程上正确的做法是:把可复现的分级模型与不可复现的真实条款分开——模型回答「在假设下这个渠道值多少」,真实条款靠商务谈判后反标定(把谈定的分成与对接成本填回 config,分级立刻反映现实)。

自检表

检查项合格标准
净贡献可复算给定新增、LTV、分成与成本,净贡献逐位可复算
分级有阈值每级的分界写死在 config,不是口头约定
负贡献显式净贡献为负的渠道被单独标出,不进接入队列
清单跟着级别走对接清单由级别推导,不是两张对不上的表
全程整数LTV、毛收入、净贡献全为整数地板除,无浮点
边界清晰真实分成与结算单标 X,不写成「实测」

分级最危险的错觉,是把「模型算出的净贡献」当成「渠道一定会给的结果」。模型只回答「在假设成立时值多少」,假设是否成立要靠谈判与接入后的真实数据去证伪。下一节我们把视角从「接哪个渠道」转到「接进来之后钱怎么分、什么时候到账」——联运分成与结算周期。

阅读导航:上一节:8.3 广告数据与隐私 · 下一节:9.2 联运分成与结算周期 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「minigame」更多文章

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