卷三「买量渠道与投放机制」 与「素材设计与创意迭代」 做的是渠道机制与素材设计框架(国内口径)——素材怎么设计、怎么迭代。本节不重做那套素材设计方法论,只做海外素材特有的合规约束。站内那篇独立游戏多语言本地化指南 给的是本地化行业与法务口径(怎么选供应商、合同怎么签),本节也不重写那些,只把「一条素材能不能投到某个地区」变成一个可机读、可对拍的合规判定——口径不对,投出去的素材会被拒审或下架,钱花了、量没起来。
素材合规不是「一条红线」,是一张矩阵
很多人以为素材合规就是「别碰色情暴力」这一条红线。工程上它是一张二维矩阵:行是素材携带的标签(真人、酒类、博彩暗示、儿童吸引、国旗、宗教符号、效果对比、价格承诺),列是目标地区。同一个标签在不同地区处置不同——「酒类」在德国可投、在沙特禁投;「儿童吸引」在巴西需改、在德国禁投。判定不是看素材本身,而是看「素材 × 地区」这个组合。
三档处置与「最严者胜出」
一条素材往往带多个标签,只要其中一个在目标地区是禁投,整条素材在该地区就不能投——这就是最严者胜出。所以处置分三档:
| 处置 | 含义 | 工程动作 |
|---|---|---|
可投 allow | 全部标签在该地区都放行 | 直接投 |
需改 revise | 有标签需调整(换素材元素 / 改文案) | 出本地化变体后再投 |
禁投 block | 有标签触红线且不可低成本改写 | 该地区不投这条 |
这条规则的工程含义是:合规判定必须逐标签做,再取最大值,不能只看「主标签」。一条主打「真人爆分」的素材,真人在德国只是需改、效果对比在德国是禁投——只看主标签会误判成「改一下就行」,实际是整条禁投。
D 层:合规矩阵与 lint
下面这份是 D 层配置 config/g13_creative_compliance.json,把九个素材标签、六个地区规则、五条素材与目标市场写成可机读整数表:
{
"version": 1,
"game": "merge-island",
"observedAt": "2026-10",
"note": "海外素材本地化合规矩阵。口径是「公开可观察机制」的建模假设(观察时点 2026-10),不是任一平台广告政策原文。severity:0=可投、1=需改、2=禁投。regions[].rules 是「素材标签 → 该地区处置」,缺省视为 0(可投)。creatives[] 是一条素材及其携带的敏感标签与目标市场。gate = 该素材在该地区全部标签处置的最大值(最严者胜出);fixes = 处置非 0 的标签(需改或禁投)。真实广告审核结论与平台政策版本需真实业务环境,命令行不可复现(X)。全整数,无浮点。",
"severity": [
{ "v": 0, "id": "allow", "label": "可投" },
{ "v": 1, "id": "revise", "label": "需改" },
{ "v": 2, "id": "block", "label": "禁投" }
],
"tags": [
"gameplay_clip", "real_person", "alcohol", "gambling_hint",
"minor_appeal", "national_flag", "religious_symbol", "before_after_claim", "price_claim"
],
"regions": [
{ "id": "US", "name": "美国", "rules": { "gameplay_clip": 0, "real_person": 0, "alcohol": 1, "gambling_hint": 1, "minor_appeal": 1, "national_flag": 0, "religious_symbol": 0, "before_after_claim": 1, "price_claim": 0 } },
{ "id": "DE", "name": "德国", "rules": { "gameplay_clip": 0, "real_person": 1, "alcohol": 0, "gambling_hint": 2, "minor_appeal": 2, "national_flag": 1, "religious_symbol": 0, "before_after_claim": 2, "price_claim": 1 } },
{ "id": "JP", "name": "日本", "rules": { "gameplay_clip": 0, "real_person": 1, "alcohol": 0, "gambling_hint": 2, "minor_appeal": 1, "national_flag": 0, "religious_symbol": 0, "before_after_claim": 1, "price_claim": 0 } },
{ "id": "KR", "name": "韩国", "rules": { "gameplay_clip": 0, "real_person": 0, "alcohol": 1, "gambling_hint": 2, "minor_appeal": 1, "national_flag": 1, "religious_symbol": 0, "before_after_claim": 1, "price_claim": 1 } },
{ "id": "BR", "name": "巴西", "rules": { "gameplay_clip": 0, "real_person": 0, "alcohol": 1, "gambling_hint": 1, "minor_appeal": 1, "national_flag": 0, "religious_symbol": 0, "before_after_claim": 1, "price_claim": 0 } },
{ "id": "SA", "name": "沙特", "rules": { "gameplay_clip": 0, "real_person": 1, "alcohol": 2, "gambling_hint": 2, "minor_appeal": 0, "national_flag": 1, "religious_symbol": 2, "before_after_claim": 2, "price_claim": 1 } }
],
"creatives": [
{ "id": "hero_burst", "name": "主角爆分", "tags": ["real_person", "before_after_claim"], "markets": ["US", "DE", "JP"] },
{ "id": "coin_rush", "name": "金币雨", "tags": ["gambling_hint", "price_claim"], "markets": ["US", "BR", "KR"] },
{ "id": "family_time", "name": "合家欢", "tags": ["minor_appeal", "national_flag"], "markets": ["US", "JP", "SA"] },
{ "id": "lucky_spin", "name": "幸运转盘", "tags": ["gambling_hint", "real_person", "religious_symbol"], "markets": ["US", "DE", "SA"] },
{ "id": "calm_clip", "name": "纯玩法演示", "tags": ["gameplay_clip"], "markets": ["US", "DE", "JP"] }
]
}
下面这段是 D 层门禁脚本 sim/g13_creative_compliance.py。它读上面那份配置,把每条素材在每个市场的处置(gate)与需改清单(fixes)算出来,并做 schema lint:
#!/usr/bin/env python3
# file: sim/g13_creative_compliance.py
# tier: D
# verify: /opt/homebrew/bin/python3 sim/g13_creative_compliance.py --config config/ --out out/
"""海外素材本地化合规矩阵与门禁(D 层:清单结构 / lint)。
把「素材标签 × 地区规则」解析成每条素材在每个目标市场的处置(gate)与需改清单(fixes):
gate(creative, region) = max(region.rules[t] for t in creative.tags) # 缺省 0(可投)
fixes(creative, region) = sorted(t for t in creative.tags if rules[t] > 0)
severity:0=可投、1=需改、2=禁投;gate 取最严者胜出,所以一条素材只要带一个禁投标签,整条
素材在该地区就是禁投。lint:素材标签必须已声明、目标市场必须是已知地区、地区规则必须覆盖全部
标签。产出 out/g13_creative_compliance.json + .csv。全整数、无浮点、无随机。口径是「公开可观察
机制」的建模假设(观察时点 2026-10);真实广告审核结论与平台政策版本需真实业务环境,命令行
不可复现(X)。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
LABEL = {0: "allow", 1: "revise", 2: "block"}
def evaluate(cfg: dict) -> list[dict]:
"""每条素材在每个目标市场的处置与需改清单。"""
regions = {r["id"]: r["rules"] for r in cfg["regions"]}
rows = []
for c in cfg["creatives"]:
for mkt in c["markets"]:
rules = regions[mkt]
gates = [rules.get(t, 0) for t in c["tags"]]
gate = max(gates) if gates else 0
fixes = sorted(t for t in c["tags"] if rules.get(t, 0) > 0)
rows.append({"creative": c["id"], "region": mkt, "gate": gate,
"gateLabel": LABEL[gate], "fixes": fixes, "fixCount": len(fixes)})
return rows
def lint(cfg: dict) -> int:
"""schema lint:素材标签已声明、目标市场已知、地区规则覆盖全部标签。"""
ok = 1
tagset = set(cfg["tags"])
regionids = {r["id"] for r in cfg["regions"]}
for c in cfg["creatives"]:
if any(t not in tagset for t in c["tags"]) or any(m not in regionids for m in c["markets"]):
ok = 0
for r in cfg["regions"]:
if set(r["rules"].keys()) != tagset:
ok = 0
return ok
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
a = ap.parse_args()
with open(os.path.join(a.config, "g13_creative_compliance.json"), encoding="utf-8") as f:
cfg = json.load(f)
rows = evaluate(cfg)
ok = lint(cfg)
cnt = lambda v: sum(1 for r in rows if r["gate"] == v) # noqa: E731
parts = ";".join(f'{r["creative"]}|{r["region"]}|{r["gate"]}|{",".join(r["fixes"])}' for r in rows)
digest = (f'schemaOk={ok},pairs={len(rows)},allow={cnt(0)},revise={cnt(1)},block={cnt(2)};' + parts)
os.makedirs(a.out, exist_ok=True)
cols = ("creative", "region", "gate", "gateLabel", "fixCount")
with open(os.path.join(a.out, "g13_creative_compliance.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(cols + ("fixes",))
for r in rows:
w.writerow([r[c] for c in cols] + ["|".join(r["fixes"])])
rep = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
"schemaOk": ok, "pairCount": len(rows), "rows": rows, "digest": digest,
"csv": os.path.join(a.out, "g13_creative_compliance.csv")}
with open(os.path.join(a.out, "g13_creative_compliance.json"), "w", encoding="utf-8") as f:
json.dump(rep, f, ensure_ascii=False, indent=2)
print(json.dumps({"digest": digest, "schemaOk": ok,
"rows": [{"creative": r["creative"], "region": r["region"],
"gate": r["gateLabel"], "fixes": r["fixes"]} for r in rows]},
ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
在本机跑 /opt/homebrew/bin/python3 sim/g13_creative_compliance.py --config config/ --out out/,15 个「素材 × 地区」组合的判定(同机相对量级):
| 素材 | 标签 | US | DE | JP | KR | BR | SA |
|---|---|---|---|---|---|---|---|
| 主角爆分 hero_burst | 真人、效果对比 | 需改 | 禁投 | 需改 | — | — | — |
| 金币雨 coin_rush | 博彩暗示、价格承诺 | 需改 | — | — | 禁投 | 需改 | — |
| 合家欢 family_time | 儿童吸引、国旗 | 需改 | — | 需改 | — | — | 需改 |
| 幸运转盘 lucky_spin | 博彩暗示、真人、宗教符号 | 需改 | 禁投 | — | — | — | 禁投 |
| 纯玩法演示 calm_clip | 玩法演示 | 可投 | 可投 | 可投 | — | — | — |
(— 表示该素材未投该市场。)15 个组合里 3 可投 / 8 需改 / 4 禁投。这张表最该看的是 calm_clip:它只带「玩法演示」一个中性标签,在三个市场全部可投——合规成本最低的素材,往往不是最好看的那条,而是标签最少的那条。而 lucky_spin 在沙特是三重违规(博彩 + 真人 + 宗教符号),任何一条都够禁投。摘要行:
digest = schemaOk=1,pairs=15,allow=3,revise=8,block=4;hero_burst|US|1|before_after_claim;hero_burst|DE|2|before_after_claim,real_person;hero_burst|JP|1|before_after_claim,real_person;coin_rush|US|1|gambling_hint;coin_rush|BR|1|gambling_hint;coin_rush|KR|2|gambling_hint,price_claim;family_time|US|1|minor_appeal;family_time|JP|1|minor_appeal;family_time|SA|1|national_flag;lucky_spin|US|1|gambling_hint;lucky_spin|DE|2|gambling_hint,real_person;lucky_spin|SA|2|gambling_hint,real_person,religious_symbol;calm_clip|US|0|;calm_clip|DE|0|;calm_clip|JP|0|
P 层内核:跨语言对拍
下面这段是 P 层 TypeScript 内核 core/src/g13/creative_compliance.ts,复算同一套判定:同样的「最严者胜出」、同样的需改清单排序、同样的 lint。它读同一份配置,产出和 Python 逐字符一致的摘要。
// file: core/src/g13/creative_compliance.ts
// tier: P
// verify: node --test 'core/test/g13_*.test.ts'
//
// 海外素材本地化合规门禁内核:与 sim/g13_creative_compliance.py 逐位等价的 TypeScript 对偶实现。
// 读同一份 config/g13_creative_compliance.json;全整数、无浮点、无随机。
export interface Region { readonly id: string; readonly name: string; readonly rules: Readonly<Record<string, number>> }
export interface Creative { readonly id: string; readonly name: string; readonly tags: readonly string[]; readonly markets: readonly string[] }
export interface ComplianceConfig {
readonly version: number; readonly game: string; readonly observedAt: string;
readonly tags: readonly string[]; readonly regions: readonly Region[]; readonly creatives: readonly Creative[];
}
export interface GateRow {
readonly creative: string; readonly region: string; readonly gate: number;
readonly gateLabel: string; readonly fixes: readonly string[]; readonly fixCount: number;
}
export const LABEL: Record<number, string> = { 0: "allow", 1: "revise", 2: "block" };
/** gate = 该素材在该地区全部标签处置的最大值(最严者胜出);fixes = 处置非 0 的标签。 */
export function evaluate(cfg: ComplianceConfig): GateRow[] {
const regions = new Map(cfg.regions.map((r) => [r.id, r.rules]));
const rows: GateRow[] = [];
for (const c of cfg.creatives) {
for (const mkt of c.markets) {
const rules = regions.get(mkt)!;
const gates = c.tags.map((t) => rules[t] ?? 0);
const gate = gates.length ? Math.max(...gates) : 0;
const fixes = c.tags.filter((t) => (rules[t] ?? 0) > 0).slice().sort();
rows.push({ creative: c.id, region: mkt, gate, gateLabel: LABEL[gate], fixes, fixCount: fixes.length });
}
}
return rows;
}
/** schema lint:素材标签已声明、目标市场已知、地区规则覆盖全部标签。 */
export function lint(cfg: ComplianceConfig): number {
let ok = 1;
const tagset = new Set(cfg.tags);
const regionIds = new Set(cfg.regions.map((r) => r.id));
for (const c of cfg.creatives) {
if (c.tags.some((t) => !tagset.has(t))) ok = 0;
if (c.markets.some((m) => !regionIds.has(m))) ok = 0;
}
for (const r of cfg.regions) {
if (Object.keys(r.rules).length !== tagset.size) ok = 0;
for (const t of tagset) if (!(t in r.rules)) ok = 0;
}
return ok;
}
/** 跨语言对拍摘要:与 sim/g13_creative_compliance.py 的 digest 逐字符一致。 */
export function digest(cfg: ComplianceConfig): string {
const rows = evaluate(cfg);
const cnt = (v: number) => rows.filter((r) => r.gate === v).length;
const parts = rows.map((r) => `${r.creative}|${r.region}|${r.gate}|${r.fixes.join(",")}`).join(";");
return `schemaOk=${lint(cfg)},pairs=${rows.length},allow=${cnt(0)},revise=${cnt(1)},block=${cnt(2)};${parts}`;
}
配套测试 core/test/g13_creative_compliance.test.ts 把 Python 落盘的摘要写成金标准,断言 TS 复算逐字符相等,并验最严者胜出、需改清单、跨地区差异与 lint。在本机跑 node --test 'core/test/g13_creative_compliance.test.ts',本节 6 个用例全过:
✔ 跨语言一致:TS 合规摘要 == Python 金标准
✔ 最严者胜出:一条素材带一个禁投标签,整条在该地区就是禁投
✔ 需改清单:只列出处置非 0 的标签,且按 id 排序
✔ 跨地区差异:同一条素材在 US 可投、在 DE 禁投
✔ lint 通过:标签已声明、市场已知、规则覆盖全部标签
✔ 统计口径:15 个「素材 × 地区」组合里 3 可投 / 8 需改 / 4 禁投
ℹ tests 6
ℹ pass 6
ℹ fail 0
X 层:真实审核结论与政策版本不可复现
上面算的是模型侧:给定标签与规则,判定是什么。而下面这些属于 X 层,需真实业务环境,命令行不可复现:
- 真实的广告审核结论(同一个标签在不同平台、不同时期的判定会变,还可能人工复核翻盘);
- 平台政策版本(本节按观察时点 2026-10 冻结了一组规则,真实政策会更新);
- 改写后的真实通过率(「需改」的素材改完能不能过,只有投了才知道)。
工程上的正确做法:把可复现的判定层与不可复现的政策层分开——判定层固定「逐标签 + 最严者胜出」这套算法;政策层在拿到平台最新政策或被拒审后反标定(把新规则补进 regions,把被误判的标签区间修正,重跑立刻看到新的处置)。这与上一节「海外渠道与投放口径」
是同一个思路:把口径做成可对拍的清单,而不是散落在人脑里的经验。
自检表
| 检查项 | 合格标准 |
|---|---|
| 逐标签判定 | 处置按每个标签算,再取最严者,不只看主标签 |
| 三档处置 | 可投 / 需改 / 禁投 分开,需改给出具体标签 |
| 地区独立 | 同一素材在不同地区判定可不同,规则按地区存 |
| 跨语言一致 | Python 与 TS 摘要逐字符相同 |
| lint 完整 | 标签已声明、市场已知、规则覆盖全部标签 |
| 边界清晰 | 真实审核结论与政策版本标 X,不写成「实测」 |
素材合规最反直觉的一点是:它看起来是「内容好不好」,实际是「标签多不多」——标签越多,命中禁投的概率越高。下一节把口径与合规两条线收束到一件事上:这笔海外投放到底赚不赚、亏到什么程度该停。
阅读导航:上一节:13.1 海外渠道与投放口径 · 下一节:13.3 海外投放 ROI 与止损 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。