十六节走到这里,前面的每一章都交了一份东西:投放配置集、归因与聚合配置、内购合规清单、渠道商务表、资质财务表、运营 SOP、单位账与月度经营账、操盘手册、对账台账。但「交了很多东西」和「能交付了」是两回事——没有人把这些散件列成一张可勾选、带权重的清单。本节就做这件事,并顺手把与卷五《跨平台发行与出海》的衔接点交代清楚。
先说分工:本卷与《上线变现与长线运营》 是同一套台账的上下游——卷三管从上线到长线运营的链路(留存、活动、版本节奏),卷四管商业运营的账与交付(投放、结算、单位账、交付清单),两卷不重叠;运营协作与团队侧的展开交给站内游戏行业运营 专题,本节只做交付视角的收束。
交付物清单:把 16 章拆成可勾选的项
清单是「装配」的反向操作。第 16.1 节的手册回答怎么用,本节清单回答交了什么、还差什么。每一项给一个 id、名称、归属章、权重与完成态;权重不是拍脑袋,而是按这一项在真实操盘里的缺了会有多痛来分——投放配置集与归因聚合各占 15,因为它们一旦缺,整条投放链路都跑不起来;附录 E 只占 5,缺了不影响操盘。
下面是 D 层清单 config/o16_delivery.json:
{
"version": 1,
"game": "merge-island",
"observedAt": "2026-10",
"readyThresholdPermille": 900,
"note": "卷四交付物清单与完成度(D 层)。口径是「交付清单装配」的建模,不是任何真实项目的验收记录。每项 item:weight=权重(整数),status=完成态(done/partial/todo,对应 1000/500/0 千分比)。整体完成度 = Σ(weight×statusPermille) // Σweight(千分比),达到 readyThresholdPermille 即判可交付。真实交付物内容与验收结论需真实业务环境,命令行不可复现(X)。全整数,无浮点。",
"items": [
{ "id": "toc", "name": "卷四目录篇(16 章 48 节导览)", "chapter": 16, "weight": 5, "status": "done" },
{ "id": "ua-config", "name": "投放配置集(账户/出价/素材,第 1–3 章)", "chapter": 3, "weight": 15, "status": "done" },
{ "id": "attr-agg", "name": "归因与聚合配置集(第 4–6 章)", "chapter": 6, "weight": 15, "status": "done" },
{ "id": "iap-comp", "name": "内购与合规清单(第 7–8 章)", "chapter": 8, "weight": 12, "status": "done" },
{ "id": "channel-bd", "name": "渠道与商务表(第 9–10 章)", "chapter": 10, "weight": 12, "status": "done" },
{ "id": "lic-fin", "name": "资质与财务表(第 11–12 章)", "chapter": 12, "weight": 12, "status": "done" },
{ "id": "ops-sop", "name": "运营 SOP 与例会编排(第 13 章)", "chapter": 13, "weight": 8, "status": "done" },
{ "id": "unit-pnl", "name": "单位账与月度经营账(第 14 章)", "chapter": 14, "weight": 12, "status": "done" },
{ "id": "retro", "name": "案例复盘与决策对照表(第 15 章)", "chapter": 15, "weight": 6, "status": "partial" },
{ "id": "handbook", "name": "操盘手册(第 16 章)", "chapter": 16, "weight": 8, "status": "done" },
{ "id": "ledger", "name": "全链路对账台账(第 16 章)", "chapter": 16, "weight": 5, "status": "done" },
{ "id": "appendix-e", "name": "附录 E(配置与清单索引)", "chapter": 16, "weight": 5, "status": "todo" }
]
}
清单里有三处刻意留下的非完成态,它们不是失误,而是这套建模要暴露的东西:retro 是 partial(案例复盘的决策对照表只填了骨架),appendix-e 是 todo(配置与清单索引还没建)。保留缺口、并让缺口可见,比把清单刷成一片绿更有用。
S 层:交付完整度打分
有了清单,下一个问题自然是:现在到底能不能交? 下面这段 S 层脚本 sim/o16_delivery.py 把每个完成态折算成完成度千分比(done→1000、partial→500、todo→0),再按权重加权求和,得到一个整体完成度,与可交付线比较。产出 JSON + CSV + SVG。
#!/usr/bin/env python3
# file: sim/o16_delivery.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/o16_delivery.py --config config/ --out out/
"""卷四交付物完整性打分(S 层):把交付清单按权重折算成整体完成度,判定是否达可交付线。
读 config/o16_delivery.json 的交付项。每项 status 映射成完成度千分比:
done → 1000 partial → 500 todo → 0
整体完成度 = Σ(weight × statusPermille) // Σweight(千分比),达到 readyThresholdPermille 即判可交付。
另列出「缺口项」(status != done)与「待办权重」。产出 JSON + CSV + SVG。全整数、无浮点。
真实交付物内容与验收结论需真实业务环境,命令行不可复现(X)。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET
STATUS_PERMILLE = {"done": 1000, "partial": 500, "todo": 0}
STATUS_COLOR = {"done": "#16a34a", "partial": "#f59e0b", "todo": "#dc2626"}
def load(d: str) -> dict:
with open(os.path.join(d, "o16_delivery.json"), encoding="utf-8") as f:
return json.load(f)
def draw_svg(path: str, items: list, score: int, threshold: int) -> int:
"""纯 Python SVG:每个交付项一根权重条(按完成度着色)+ 一条可交付线。"""
W, PAD, ROW = 760, 300, 34
H = 74 + ROW * len(items)
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"卷四交付物完整性:整体 {score}‰(可交付线 {threshold}‰)")
span = W - PAD - 24
total_w = sum(it["weight"] for it in items) or 1
for i, it in enumerate(items):
y = 52 + i * ROW
w = span * it["weight"] // total_w
ET.SubElement(root, "text", {"x": "16", "y": str(y + 18), "font-size": "11",
"fill": "#374151"}).text = it["name"][:26]
ET.SubElement(root, "rect", {"x": str(PAD), "y": str(y + 4), "width": str(w),
"height": "18", "rx": "3",
"fill": STATUS_COLOR[it["status"]]})
ET.SubElement(root, "text", {"x": str(PAD + w + 6), "y": str(y + 17), "font-size": "10",
"fill": "#6b7280"}).text = (
f'{it["status"]} · 权重 {it["weight"]} · {it["statusPermille"]}‰')
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)
items = []
for it in cfg["items"]:
sp = STATUS_PERMILLE[it["status"]]
items.append(dict(it, statusPermille=sp, weighted=it["weight"] * sp))
total_w = sum(it["weight"] for it in items)
total_weighted = sum(it["weighted"] for it in items)
score = total_weighted // total_w
threshold = cfg["readyThresholdPermille"]
gaps = [it for it in items if it["status"] != "done"]
digest = f"items={len(items)},weight={total_w},score={score},ready={str(score >= threshold).lower()}" \
+ ";gaps=" + ",".join(f'{g["id"]}:{g["status"]}' for g in gaps)
os.makedirs(a.out, exist_ok=True)
svg_bytes = draw_svg(os.path.join(a.out, "o16_delivery.svg"), items, score, threshold)
with open(os.path.join(a.out, "o16_delivery.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
cols = ("id", "name", "chapter", "weight", "status", "statusPermille", "weighted")
w.writerow(cols)
for it in items:
w.writerow([it[k] for k in cols])
rep = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
"itemCount": len(items), "totalWeight": total_w, "totalWeighted": total_weighted,
"scorePermille": score, "readyThresholdPermille": threshold,
"ready": score >= threshold, "gaps": [{"id": g["id"], "name": g["name"],
"status": g["status"], "weight": g["weight"]} for g in gaps],
"pendingWeight": sum(g["weight"] for g in gaps), "items": items, "digest": digest,
"svg": os.path.join(a.out, "o16_delivery.svg"), "svgBytes": svg_bytes}
with open(os.path.join(a.out, "o16_delivery.json"), "w", encoding="utf-8") as f:
json.dump(rep, f, ensure_ascii=False, indent=2)
print(json.dumps({"itemCount": len(items), "totalWeight": total_w, "scorePermille": score,
"readyThresholdPermille": threshold, "ready": score >= threshold,
"gaps": rep["gaps"], "pendingWeight": rep["pendingWeight"],
"digest": digest, "svgBytes": svg_bytes}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
本机跑 for f in sim/o16_*.py; do /opt/homebrew/bin/python3 "$f" --config config/ --out out/; done,交付完整度打分的真实输出(同机相对量级,绝对数值不可移植):
{
"itemCount": 12,
"totalWeight": 115,
"scorePermille": 930,
"readyThresholdPermille": 900,
"ready": true,
"gaps": [
{
"id": "retro",
"name": "案例复盘与决策对照表(第 15 章)",
"status": "partial",
"weight": 6
},
{
"id": "appendix-e",
"name": "附录 E(配置与清单索引)",
"status": "todo",
"weight": 5
}
],
"pendingWeight": 11,
"digest": "items=12,weight=115,score=930,ready=true;gaps=retro:partial,appendix-e:todo",
"svgBytes": 4375
}
十二项、总权重 115,整体完成度 930‰,高于可交付线 900‰——判定可交付。别被 ready=true 冲昏头:它只说「按这套权重够了」,缺口项 retro(部分)与 appendix-e(未办)仍然躺在那里,待办权重合计 11。
交付物总表
| id | 交付物 | 归属章 | 权重 | 状态 | 完成度 |
|---|---|---|---|---|---|
| toc | 卷四目录篇 | 16 | 5 | done | 1000‰ |
| ua-config | 投放配置集 | 1–3 | 15 | done | 1000‰ |
| attr-agg | 归因与聚合配置集 | 4–6 | 15 | done | 1000‰ |
| iap-comp | 内购与合规清单 | 7–8 | 12 | done | 1000‰ |
| channel-bd | 渠道与商务表 | 9–10 | 12 | done | 1000‰ |
| lic-fin | 资质与财务表 | 11–12 | 12 | done | 1000‰ |
| ops-sop | 运营 SOP 与例会编排 | 13 | 8 | done | 1000‰ |
| unit-pnl | 单位账与月度经营账 | 14 | 12 | done | 1000‰ |
| retro | 案例复盘与决策对照表 | 15 | 6 | partial | 500‰ |
| handbook | 操盘手册 | 16 | 8 | done | 1000‰ |
| ledger | 全链路对账台账 | 16 | 5 | done | 1000‰ |
| appendix-e | 附录 E(配置与清单索引) | 16 | 5 | todo | 0‰ |
为什么按权重而不是按篇数
用「交了 10 篇 / 共 12 篇」也能算完成度,但那会得出 833‰——这个数会骗人。因为交付物的价值从来不均等:缺了投放配置集,整条买量链路直接停摆;缺了附录 E,操盘手只是翻文档时多花两分钟。按篇数计数把这两者等量齐观,按权重加权才还原了「缺哪一项更痛」。
权重本身也是可争议的口径,不是真理。这套权重是建模者按「缺了有多痛」给的,换一个操盘团队、换一个品类,权重会不同。所以清单里的 weight 字段是数据、是可改的输入,而不是写死在代码里的常量——改权重只需要改 config/o16_delivery.json,打分逻辑一行不用动。这正是「一份 config,多处消费」在交付管理上的复用。
与卷五《跨平台发行与出海》的衔接
本卷把国内市场的账跑通了:买量投放、归因聚合、内购合规、渠道结算、单位账与月度经营账,全部从一份共享 economy.json 复算,并且能对账、能打分。卷五要接的正是这套交付物的输出端——把国内跑通的账搬到多地区、多币种的发行环境里。
具体衔接点有四个。其一,货币与汇率:本卷所有金额都是「分」,单币种;卷五要引入多币种与汇率折算,economy.json 的整分口径需要扩展成「以某个基准币记账 + 各币种汇率」。其二,渠道分成:本卷的综合扣点 373‰ 是国内渠道加权折叠出来的一个数;卷五的每个海外渠道分成比例、税务预扣(withholding)规则都不同,第 12 章的结算台账要按地区重算。其三,合规差异:本卷第 7–8 章的内购与合规清单是中国大陆口径(版号、实名、未成年人保护);卷五面对的是 GDPR、COPPA、各地区的年龄分级与隐私同意,同一份合规清单要分叉成多套。其四,单位账的换算:第 14 章的单位账以「分」计,卷五要面对美元、欧元、日元、韩元等,LTV/CAC 的回本判断必须在统一基准币下做,否则汇率波动会把结论带偏。
需要说明的是:卷五《跨平台发行与出海》目前尚未落盘,其目录页与全部章节都还不存在,所以本节不给任何指向卷五的站内链接——现在链过去只会得到死链。以上衔接点先以文字交代,等卷五落地后再由那一卷的引言反向链回本节。本卷自身的入口与全部书稿可从书架全部书稿 进入;卷三《上线变现与长线运营》见《上线变现与长线运营》 ,两卷的差别是:卷三管从上线到长线运营的链路,卷四管商业运营的账与交付,卷五管跨平台与出海。
缺口处置:两项怎么办
清单里剩两个缺口,处置方式不同。appendix-e(附录 E:配置与清单索引,todo)是纯工作量缺口——它只是把全卷散落的 config/*.json 与各章清单汇成一张索引表,没有不确定性,补齐即可,权重 5 也印证它「缺了不致命」。补齐它能把完成度再抬一档。
retro(案例复盘与决策对照表,partial)则是认知缺口,不能靠堆时间解决。它的骨架已经填了,缺的是把真实操盘里的关键决策(为什么这版素材被砍、为什么这个出价策略中止)与当时的判断依据对照起来——这依赖真实业务环境,命令行不可复现(X),故本节只能把它标成 partial 而不是 done。
对这两个缺口的正确态度是:不要为了把分数刷到 1000‰ 而把 partial 改成 done。清单的价值在于如实,一旦为了好看而虚报完成态,整张清单就失去了「看清缺口」的功能——这比缺口本身更危险。
自检表
| 检查项 | 合格标准 |
|---|---|
| 清单完整 | 16 章每章至少一项交付物,权重为正整数 |
| 状态合法 | status 只取 done / partial / todo,映射 1000/500/0 |
| 缺口可见 | 非完成态不被抹平,缺口与待办权重单独列出 |
| 口径可改 | 权重是配置数据,打分逻辑不写死权重 |
| 判定有据 | 整体完成度与可交付线都来自同一份配置 |
| 边界 | 真实交付物内容与验收结论标 X |
至此卷四收束。十六节从「账户与出价」一路走到「交付与出海」,留下了一套能跑、能对账、能打分的商业运营台账——它不保证你赚钱,但能让你在每一步都算得清账、看得见缺口。
阅读导航:上一节:16.2 全链路对账与仿真验收 · 下一节:回到目录 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。