15.3 运营与合规的典型失败

第三节收束本章:前两节的失败伤的是钱,这一节的失败能让一款已经跑起来的游戏一夜归零。资质缺项、内容红线、防沉迷漏放、活动节奏空档、奖励通胀——五类运营与合规失败,各拆成一个可跑体检项,跑「健康配置 vs 失败配置」对照,输出缺陷量。附五组检查的 D 层输入清单。

15.1 和 15.2 的失败,伤的是钱——LTV 掉几个点、ROAS 跌破回本线,你还能改、还能救。15.3 的失败伤的是命:资质缺一项、文案踩一条红线、防沉迷漏放一次,轻则提审被打回、重则上线后被下架整改,一款已经跑起来的游戏一夜归零。这类失败最不像技术问题,却最不该靠「运气」——所以本节把它做成可跑的体检脚本。本站那篇独立开发者的手游广告失败复盘 里也提到素材与文案踩坑的段落,那是事后叙述;本节把「会不会踩坑」变成五个体检项,各跑一遍「健康配置 vs 失败配置」的对照,用缺陷量说话。

运营与合规的失败,为什么该做成「体检」

合规失败有一个反直觉的特点:它在事故前不可见,在事故后不可逆。你不会因为「暂时没被查到」就安全,也不会因为「这次过了审」就一劳永逸——平台规则、内容红线、防沉迷要求都在变。对抗这种不可见性,最务实的办法是把每一条要求翻译成一条可判定的规则,再让脚本对「健康配置」和「失败配置」各跑一遍,看缺陷量差多少。这不是替代法务判断,而是在提审前先把机器能查的部分查干净:真实资质要求、真实审核结果与真实合规判定需真实业务环境与专业法务判断,命令行不可复现(X)。

本节五个体检项覆盖合规与运营两侧:资质覆盖率(缺一项就提审失败)、内容红线(分级扫描 block / review)、防沉迷闸(时段 / 时长 / 消费三闸的漏放)、活动节奏空档(一年内最大空档周数)、奖励通胀(活动奖励相对常规产出的比例)。

S 层:五个体检项,健康 vs 失败对照(S 层)

下面这段是 S 层体检脚本,读 config/l15_ops_compliance.json,对五个体检项各跑一次「健康配置 vs 失败配置」,产出 out/l15_ops_compliance.{json,csv,svg}。每个体检项输出一个缺陷量(资质缺项数 / 红线 block 条数 / 防沉迷漏放次数 / 空档周数 / 通胀超限档),失败配置只要缺陷量 > 0 即判 FAIL。

#!/usr/bin/env python3
# file: sim/l15_ops_compliance.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/l15_ops_compliance.py --config config/ --out out/
"""运营与合规的失败模式体检:读 config/l15_ops_compliance.json。

五个体检项,各自跑「健康配置 vs 失败配置」对照,每个输出一个「缺陷量」:
1) 资质覆盖率  required = must(含 hasPaid 时的 must-if-paid);缺陷 = 提审前缺项数。
2) 内容红线    分级扫描:任一 block 命中即 block,否则 review,最后 pass;缺陷 = block 条数。
3) 防沉迷      时段 / 时长 / 消费三闸;缺陷 = 该被拦却放行的次数(漏放)。
4) 活动节奏    一年内节点最大空档周数;缺陷 = max(0, 最大空档 - 目标空档)。
5) 奖励通胀    活动奖励 / 一周常规产出;缺陷 = max(0, 通胀‰ - 1000) // 100。

清单条目、红线词库、防沉迷规则、节点排期与奖励预算均为示例归纳;真实资质要求、真实审核结果与
真实合规判定需真实业务环境与专业法务判断,命令行不可复现(X)。全整数、地板除,无浮点。
产出 out/l15_ops_compliance.{json,csv,svg}。
"""
from __future__ import annotations
import argparse, csv, json, os, sys, xml.etree.ElementTree as ET

RANK = {"pass": 0, "review": 1, "block": 2}
GROUP_COLOR = {"合规": "#dc2626", "运营": "#f59e0b"}


def required_ids(lic: dict) -> list[str]:
    return [it["id"] for it in lic["items"]
            if it["level"] == "must" or (it["level"] == "must-if-paid" and lic["hasPaid"])]


def coverage(lic: dict, holdings: list[str]) -> dict:
    req, held_set = required_ids(lic), set(holdings)
    held = [i for i in req if i in held_set]
    return {"required": len(req), "held": len(held),
            "coveragePermille": len(held) * 1000 // len(req),
            "missing": [i for i in req if i not in held_set]}


def scan_text(text: str, rules: list[dict]) -> list[dict]:
    hay = text.lower()
    return [{"ruleId": r["id"], "severity": r["severity"], "pattern": p}
            for r in rules for p in r["patterns"] if hay.find(p.lower()) >= 0]


def decide(hits: list[dict]) -> str:
    top = "pass"
    for h in hits:
        if RANK[h["severity"]] > RANK[top]:
            top = h["severity"]
    return top


def redline_defect(redline: dict, texts: list[dict]) -> dict:
    rows = [{"id": t["id"], "kind": t["kind"], "decision": decide(scan_text(t["text"], redline["rules"]))}
            for t in texts]
    blocks = [r["id"] for r in rows if r["decision"] == "block"]
    return {"blocks": blocks, "reviews": [r["id"] for r in rows if r["decision"] == "review"],
            "blockCount": len(blocks)}


def gate_blocked(profile: dict, at: dict) -> bool:
    """返回 True 表示该次被防沉迷拦下(合规);False 表示放行。"""
    if at["dayKind"] not in profile["allowedDays"]:
        return True
    if not (profile["windowStart"] <= at["clock"] < profile["windowEnd"]):
        return True
    if at["durationMin"] > profile["dailyMinCap"]:
        return True
    return at["spendFen"] > profile["singleSpendCapFen"]


def antiediction_defect(cfg: dict, gate_on: bool) -> dict:
    leaked = [at["id"] for at in cfg["attempts"]
              if not (gate_blocked(cfg["profile"], at) if gate_on else False)]
    return {"leakCount": len(leaked), "leaked": leaked}


def max_gap_weeks(nodes: list[int], year_weeks: int) -> int:
    if not nodes:
        return year_weeks
    pts = [0] + sorted(nodes) + [year_weeks]
    return max(pts[i + 1] - pts[i] for i in range(len(pts) - 1))


def inflation_permille(reward_fen: int, daily_faucet: int, event_days: int) -> int:
    return reward_fen * 1000 // (daily_faucet * event_days)


def svg(path: str, checks: list[dict]) -> int:
    """纯 Python SVG:每个体检项一根横条 = 失败配置的缺陷量。"""
    W, PAD, ROW = 760, 220, 42
    H = 78 + ROW * len(checks)
    r = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg", "width": str(W),
                           "height": str(H), "viewBox": f"0 0 {W} {H}"})
    ET.SubElement(r, "rect", {"width": str(W), "height": str(H), "fill": "#ffffff"})
    for x, y, s, sz, fill in [(16, 26, "《合成小岛》运营 / 合规失败体检:失败配置缺陷量", 15, "#111827"),
                              (16, 46, "缺陷量口径:资质缺项 / 红线 block 条数 / 防沉迷漏放次数 / 空档周数 / 通胀超限档", 12, "#6b7280")]:
        ET.SubElement(r, "text", {"x": str(x), "y": str(y), "font-size": str(sz), "fill": fill}).text = s
    span = W - PAD - 120
    hi = max(max(c["failureDefect"] for c in checks), 1)
    for i, c in enumerate(checks):
        y, w = 66 + i * ROW, span * c["failureDefect"] // hi
        ET.SubElement(r, "text", {"x": "16", "y": str(y + 22), "font-size": "12",
                                  "fill": "#374151"}).text = c["name"]
        ET.SubElement(r, "rect", {"x": str(PAD), "y": str(y + 6), "width": str(w), "height": "22",
                                  "rx": "4", "fill": GROUP_COLOR.get(c["group"], "#6b7280")})
        ET.SubElement(r, "text", {"x": str(PAD + w + 8), "y": str(y + 22), "font-size": "12",
                                  "fill": "#6b7280"}).text = f'{c["failureDefect"]} · {c["verdict"]}'
    os.makedirs(os.path.dirname(path), exist_ok=True)
    ET.ElementTree(r).write(path, encoding="utf-8", xml_declaration=True)
    return os.path.getsize(path)


def build_checks(cfg: dict) -> list[dict]:
    lic, red, cad, inf = cfg["license"], cfg["redline"], cfg["cadence"], cfg["eventInflation"]
    hc, fc = coverage(lic, lic["healthyHoldings"]), coverage(lic, lic["failureHoldings"])
    hr, fr = redline_defect(red, red["healthyTexts"]), redline_defect(red, red["failureTexts"])
    ha, fa = antiediction_defect(cfg["antiediction"], True), antiediction_defect(cfg["antiediction"], False)
    hg, fg = max_gap_weeks(cad["healthyNodes"], cad["yearWeeks"]), max_gap_weeks(cad["failureNodes"], cad["yearWeeks"])
    hd, fd = max(0, hg - cad["targetGapWeeks"]), max(0, fg - cad["targetGapWeeks"])
    hi = inflation_permille(inf["healthyEventRewardFen"], inf["dailyFaucetFen"], inf["eventDays"])
    fi = inflation_permille(inf["failureEventRewardFen"], inf["dailyFaucetFen"], inf["eventDays"])
    return [
        {"id": "license", "group": "合规", "name": "资质覆盖率",
         "healthy": f'{hc["coveragePermille"]}‰', "failure": f'{fc["coveragePermille"]}‰',
         "healthyDefect": hc["required"] - hc["held"], "failureDefect": fc["required"] - fc["held"],
         "verdict": "PASS" if fc["coveragePermille"] == 1000 else "FAIL",
         "detail": f'提审前缺 {fc["required"] - fc["held"]} 项:{",".join(fc["missing"])}'},
        {"id": "redline", "group": "合规", "name": "内容红线",
         "healthy": f'{hr["blockCount"]} block', "failure": f'{fr["blockCount"]} block',
         "healthyDefect": hr["blockCount"], "failureDefect": fr["blockCount"],
         "verdict": "PASS" if fr["blockCount"] == 0 else "FAIL",
         "detail": f'命中 block 的文案:{",".join(fr["blocks"]) or "无"};review:{",".join(fr["reviews"]) or "无"}'},
        {"id": "antiediction", "group": "合规", "name": "防沉迷闸",
         "healthy": f'{ha["leakCount"]} 漏放', "failure": f'{fa["leakCount"]} 漏放',
         "healthyDefect": ha["leakCount"], "failureDefect": fa["leakCount"],
         "verdict": "PASS" if fa["leakCount"] == 0 else "FAIL",
         "detail": f'未接入防沉迷时放行的违规尝试:{",".join(fa["leaked"])}'},
        {"id": "cadence", "group": "运营", "name": "活动节奏空档",
         "healthy": f'{hg} 周', "failure": f'{fg} 周',
         "healthyDefect": hd, "failureDefect": fd, "verdict": "PASS" if fd == 0 else "FAIL",
         "detail": f'最大空档 {fg} 周 > 目标 {cad["targetGapWeeks"]} 周,超出 {fd} 周'},
        {"id": "inflation", "group": "运营", "name": "活动奖励通胀",
         "healthy": f'{hi}‰', "failure": f'{fi}‰',
         "healthyDefect": max(0, hi - 1000) // 100, "failureDefect": max(0, fi - 1000) // 100,
         "verdict": "PASS" if fi <= 1000 else "FAIL",
         "detail": f'活动奖励 = 一周常规产出的 {fi}‰,超过 1000‰ 即奖励通胀'}]


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, "l15_ops_compliance.json"), encoding="utf-8") as f:
        cfg = json.load(f)
    checks = build_checks(cfg)
    fails = [c for c in checks if c["verdict"] == "FAIL"]
    os.makedirs(a.out, exist_ok=True)
    sb = svg(os.path.join(a.out, "l15_ops_compliance.svg"), checks)
    report = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
              "note": cfg["note"], "checkCount": len(checks), "checks": checks,
              "failCount": len(fails), "failIds": [c["id"] for c in fails], "svgBytes": sb}
    with open(os.path.join(a.out, "l15_ops_compliance.json"), "w", encoding="utf-8") as f:
        json.dump(report, f, ensure_ascii=False, indent=2)
    cols = ("id", "group", "name", "healthy", "failure", "healthyDefect", "failureDefect", "verdict", "detail")
    with open(os.path.join(a.out, "l15_ops_compliance.csv"), "w", encoding="utf-8", newline="") as f:
        w = csv.writer(f)
        w.writerow(cols)
        for c in checks:
            w.writerow([c[k] for k in cols])
    for c in checks:
        print(f'[{c["verdict"]}] {c["group"]} {c["name"]:<8} 健康 {c["healthy"]:<10} '
              f'失败 {c["failure"]:<10} 缺陷 {c["healthyDefect"]}→{c["failureDefect"]}')
    print(f'体检结论:{len(fails)}/{len(checks)} 项 FAIL  {report["failIds"]}')
    return 0


if __name__ == "__main__":
    sys.exit(main())

真跑输出(同机):

[FAIL] 合规 资质覆盖率    健康 1000‰      失败 250‰       缺陷 0→6
[FAIL] 合规 内容红线     健康 0 block    失败 2 block    缺陷 0→2
[FAIL] 合规 防沉迷闸     健康 0 漏放       失败 3 漏放       缺陷 0→3
[FAIL] 运营 活动节奏空档   健康 6 周        失败 26 周       缺陷 0→20
[FAIL] 运营 活动奖励通胀   健康 285‰       失败 2142‰      缺陷 0→11
体检结论:5/5 项 FAIL  ['license', 'redline', 'antiediction', 'cadence', 'inflation']

D 层:五组检查的输入清单(D 层)

脚本的输入是一份 D 层配置:九项资质清单(标 must / must-if-paid / should)、八条内容红线(分 block / review 两级)、一份防沉迷画像与三条违规尝试、一张 52 周节点排期表、以及活动奖励预算。这些条目都是示例归纳,真实平台清单会因品类与地区而异——但结构是通用的:把「要求」写成条目,把「违规样本」写成输入,让脚本替你数缺陷。

{
  "version": 1,
  "game": "merge-island",
  "observedAt": "2026-10",
  "note": "运营与合规的失败模式体检(D 层)。把「资质覆盖率 / 内容红线 / 防沉迷 / 活动节奏 / 奖励通胀」五个体检项,各自跑一遍「健康配置 vs 失败配置」的对照。清单条目、红线词库、防沉迷规则、节点排期与奖励预算均为示例归纳;真实资质要求、真实审核结果与真实合规判定需真实业务环境与专业法务判断,命令行不可复现(X)。全整数、禁浮点。",
  "license": {
    "hasPaid": true,
    "items": [
      { "id": "entity-license", "name": "主体营业执照", "level": "must" },
      { "id": "soft-copyright", "name": "软件著作权登记", "level": "must" },
      { "id": "icp-filing", "name": "ICP 备案", "level": "must" },
      { "id": "game-approval", "name": "游戏版号(含内购时)", "level": "must-if-paid" },
      { "id": "category-permit", "name": "平台类目资质", "level": "must" },
      { "id": "privacy-policy", "name": "隐私政策与用户协议", "level": "must" },
      { "id": "realname-antiaddiction", "name": "实名与防沉迷接入", "level": "must" },
      { "id": "pay-qualify", "name": "支付 / 结算资质", "level": "must-if-paid" },
      { "id": "minor-protection-report", "name": "未成年人保护专项报告", "level": "should" }
    ],
    "healthyHoldings": ["entity-license", "soft-copyright", "icp-filing", "game-approval",
      "category-permit", "privacy-policy", "realname-antiaddiction", "pay-qualify"],
    "failureHoldings": ["entity-license", "soft-copyright"]
  },
  "redline": {
    "rules": [
      { "id": "r-political", "category": "涉政", "severity": "block", "patterns": ["领导人", "敏感事件"] },
      { "id": "r-porn", "category": "涉黄", "severity": "block", "patterns": ["色情", "裸聊"] },
      { "id": "r-violence", "category": "涉暴", "severity": "block", "patterns": ["血腥", "恐怖袭击"] },
      { "id": "r-gambling", "category": "赌博", "severity": "block", "patterns": ["现金红包", "押注", "真钱"] },
      { "id": "r-ip", "category": "侵权", "severity": "block", "patterns": ["宝可梦", "马里奥", "迪士尼"] },
      { "id": "r-superstition", "category": "迷信", "severity": "review", "patterns": ["转运", "开光"] },
      { "id": "r-induce-share", "category": "诱导分享", "severity": "review", "patterns": ["分享给", "转发到"] },
      { "id": "r-false-ad", "category": "虚假宣传", "severity": "review", "patterns": ["稳赚不赔", "一键暴富"] }
    ],
    "healthyTexts": [
      { "id": "h1", "kind": "游戏名", "text": "合成小岛" },
      { "id": "h2", "kind": "买量文案", "text": "拖动相同岛屿即可合成更大的岛屿" },
      { "id": "h3", "kind": "活动文案", "text": "登录领取限定装扮" }
    ],
    "failureTexts": [
      { "id": "f1", "kind": "游戏名", "text": "合成小岛:一键暴富,稳赚不赔" },
      { "id": "f2", "kind": "素材标题", "text": "官方授权宝可梦合成玩法" },
      { "id": "f3", "kind": "活动文案", "text": "幸运大转盘,赢取现金红包和转运符" }
    ]
  },
  "antiediction": {
    "profile": { "band": "8-16 周岁", "allowedDays": ["fri", "sat", "sun"],
      "windowStart": "20:00", "windowEnd": "21:00", "dailyMinCap": 60,
      "singleSpendCapFen": 5000, "monthlySpendCapFen": 20000 },
    "attempts": [
      { "id": "a-school-night", "clock": "20:30", "dayKind": "school", "durationMin": 10, "spendFen": 0 },
      { "id": "a-over-daily", "clock": "20:30", "dayKind": "sat", "durationMin": 120, "spendFen": 0 },
      { "id": "a-over-spend", "clock": "20:30", "dayKind": "sat", "durationMin": 5, "spendFen": 6000 }
    ]
  },
  "cadence": {
    "yearWeeks": 52,
    "targetGapWeeks": 6,
    "healthyNodes": [2, 8, 14, 20, 26, 32, 38, 44, 50],
    "failureNodes": [2, 3, 4, 5, 6, 24, 25, 26]
  },
  "eventInflation": {
    "dailyFaucetFen": 200,
    "eventDays": 7,
    "healthyEventRewardFen": 400,
    "failureEventRewardFen": 3000
  }
}

五个体检项分别意味着什么

资质覆盖率:健康配置八项齐全、覆盖率 1000‰;失败配置只持有营业执照与软著两项,覆盖率掉到 250‰,提审前缺六项。这不是「慢慢补」的事——缺 game-approval(版号)会让含内购的版本直接无法上架,缺 realname-antiaddiction(实名与防沉迷)会被平台强制下架。

内容红线:失败配置三条文案里两条命中 block——「一键暴富、稳赚不赔」踩虚假宣传、「官方授权宝可梦」踩 IP 侵权;还有一条命中 review(现金红包 + 转运符)。红线扫描的价值在于分级:block 是必须改掉的硬伤,review 是提审前人工复核的灰区。

防沉迷闸:健康配置下三条违规尝试(工作日登录、超时长、超单笔消费)全被拦;一旦未接入防沉迷,三次全部放行——3 漏放。防沉迷不是「可选优化」,是提审硬门槛,漏放一次就是合规事故。

活动节奏空档:健康排期九场活动把最大空档压到 6 周(正好等于目标);失败排期前五场挤在第 2–6 周、之后一路空到第 24 周——最大空档 26 周,玩家有半年没新内容,直接掉线。

奖励通胀:健康活动的奖励等于一周常规产出的 285‰(约三成,合理);失败活动是 2142‰(两周多),奖励通胀 11 档——玩家拿活动奖励就能跳过成长曲线,长期产出与付费动机被一起摧毁。

本章小结

三节下来,失败被拆成了三层:玩法 / 数值 / 变现(15.1,伤钱)、买量 / 留存(15.2,伤钱且难救)、运营 / 合规(15.3,要命)。它们的共同点是——都能被写成参数、喂进模型、跑出坏数字。避坑的正确姿势从来不是背一份「不要做什么」的清单,而是把每条失败做成一个可复现的对照:改一个数,跑一遍,看它掉几个点。这样你在真正投放、真正提审之前,就已经见过它翻车的样子。

下一章是全书的收口:把三卷的所有模型装配成一份可交付的上线方案,跑一次全链路仿真验收。

阅读导航:上一节:15.2 买量与留存的典型失败 · 下一节:16.1 装配完整上线方案 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「minigame」更多文章

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