10.1 资质、备案与平台规则

把上线前的资质、备案与平台类目要求整理成一张可被程序读进来做覆盖率与 schema 校验的清单:一条带阶段、等级与类别字段的 D 层资质配置,一段按「已持有集合」计算必办清单覆盖率并校验备案项字段的 S 层对照脚本,以及开发期 / 提审前 / 上线后三阶段的覆盖率与字段残缺对照。所有条目均为示例,具体结论须专业法务判断。

前九章一直在讲「怎么让《合成小岛》跑起来、怎么变现」,第 10 章换一个视角:在把它推到真实平台之前,法律与平台规则先于玩法挡住你。本节只处理最上游的一件事——把「资质、备案、平台类目要求」整理成一张能被程序读进来做校验的清单,并给出可跑的覆盖率与 schema 对照脚本。具体该办哪些证、走什么流程、审核能不能过,需真实业务环境与专业法务判断,命令行不可复现(X),本节不提供任何法律意见或合规结论。

先说分工。站内已有一篇小游戏开发综述 ,那篇 96 行短文里有审核合规口径的一段综述,讲「要准备哪些材料」;本节不重复它的框架,而是把同一类材料变成一份带字段、带状态、可校验的配置——综述告诉你「要办证」,本节告诉你「证办到哪一步、缺哪一项、字段填得对不对」。上线流程本身(提审、灰度、回滚)是第 9 章的事,本节不越界,只把「清单何时补齐」作为交接点。

资质、备案、平台规则是三件事

先分清三种东西,它们的责任主体和补齐时点都不同。资质是主体层面的东西:营业执照、软件著作权登记,开发期就该办。备案是向监管与平台申报的东西:ICP 备案、含内购时的游戏版号、平台类目资质,提审前必须齐。平台规则是平台侧的准入与运营约束:类目要求、支付结算资质、未成年人保护接入,横跨提审与上线两个阶段。三者混在一起,最容易出的错是把「上线后才需要的」当成「提审前必须有」,白白拖慢排期;或者反过来,提审前漏了「含内购才需要」的版号。

本节的工程化思路很朴素:把每一项做成一条带 stage(最迟补齐阶段)、level(must / must-if-paid / should)和 category 的记录,再用一段脚本对「当前已持有集合」算覆盖率。must-if-paid 是关键——它的必要性取决于游戏有没有内购,所以配置里带一个 hasPaid 开关,有内购时才把它计入必办清单。

把清单写成可校验的数据(D 层)

下面这段是 D 层静态配置:资质条目、三阶段的「已持有集合」、备案项的 schema 与样例。所有主体名、编号、状态都是示例占位;真实资质要求与审核结果需真实业务环境与专业法务判断,命令行不可复现(X)。

{
  "version": 1,
  "game": "merge-island",
  "observedAt": "2026-10",
  "note": "资质、备案与平台规则的检查清单(D 层)。条目是对公开可观察机制的归纳,用于工程侧做「清单覆盖率 / 备案项 schema」两类可复现校验;真实主体资质要求、真实备案流程与真实审核结果需真实业务环境与专业法务判断,命令行不可复现(X)。所有编号、主体名与状态均为示例占位。",
  "stages": ["dev", "prereview", "live"],
  "hasPaid": true,
  "items": [
    {"id": "entity-license", "name": "主体营业执照", "stage": "dev", "level": "must", "category": "subject"},
    {"id": "soft-copyright", "name": "软件著作权登记", "stage": "dev", "level": "must", "category": "ip"},
    {"id": "icp-filing", "name": "ICP 备案(域名/主体)", "stage": "prereview", "level": "must", "category": "filing"},
    {"id": "game-approval", "name": "游戏版号(含内购时)", "stage": "prereview", "level": "must-if-paid", "category": "filing"},
    {"id": "category-permit", "name": "平台类目资质", "stage": "prereview", "level": "must", "category": "platform"},
    {"id": "privacy-policy", "name": "隐私政策与用户协议", "stage": "prereview", "level": "must", "category": "privacy"},
    {"id": "realname-antiaddiction", "name": "实名与防沉迷接入", "stage": "prereview", "level": "must", "category": "minor"},
    {"id": "pay-qualify", "name": "支付 / 结算资质", "stage": "live", "level": "must-if-paid", "category": "payment"},
    {"id": "minor-protection-report", "name": "未成年人保护专项报告", "stage": "live", "level": "should", "category": "minor"}
  ],
  "holdings": {
    "dev": ["entity-license"],
    "prereview": ["entity-license", "soft-copyright", "icp-filing"],
    "live": ["entity-license", "soft-copyright", "icp-filing", "game-approval", "category-permit", "privacy-policy", "realname-antiaddiction", "pay-qualify"]
  },
  "filingSchema": {
    "required": ["id", "title", "entity", "category", "status", "evidence"],
    "statusEnum": ["pending", "submitted", "approved", "rejected"],
    "evidenceRequiredUnless": ["pending"]
  },
  "filingItems": [
    {"id": "icp-filing", "title": "ICP 备案", "entity": "示例科技(深圳)有限公司", "category": "filing", "status": "submitted", "evidence": "示例-ICP-0001"},
    {"id": "game-approval", "title": "游戏版号", "entity": "示例科技(深圳)有限公司", "category": "filing", "status": "pending", "evidence": ""},
    {"id": "pay-qualify", "title": "支付资质", "entity": "", "category": "payment", "status": "approved", "evidence": ""}
  ]
}

用 S 层脚本做覆盖率与 schema 对照

下面这段是 S 层仿真脚本,读上面那份配置,做两件事:一是按「已持有集合」算每个阶段的必办清单覆盖率(required = must 加上有内购时的 must-if-paid,should 不进分母),列出缺失项与最迟补齐阶段;二是对备案项做 schema 校验——必填字段是否齐、status 是否落在枚举内、非 pending 状态的凭证号是否为空。产出 JSON + CSV + SVG,三阶段一对照,「开发期缺得多、上线后应齐」应当一眼可见。

#!/usr/bin/env python3
# file: sim/l10_license.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/l10_license.py --config config/ --out out/
"""资质备案检查:清单覆盖率 + 备案项 schema 校验(整数、可复算)。

读 config/l10_license.json:资质条目 + 各阶段「已持有」集合 + 备案项 schema 与样例。
1) 覆盖率:required = level 以 "must" 开头(含 must-if-paid,当 hasPaid 为真时计入);
   coveragePermille = |held ∩ required| * 1000 // |required|;
2) 缺失清单:required - held,按条目 stage 标注「最迟补齐阶段」;
3) 备案项 schema 校验:required 字段齐全、status 落在枚举内、
   status 不在 evidenceRequiredUnless 时 evidence 非空;
4) 三阶段对照(dev / prereview / live)——同一份清单,只换「已持有」集合。
全程整数,比例用千分比。产出 out/l10_license.json + out/l10_license.csv + out/l10_license.svg。

清单条目与 schema 均为示例;真实资质要求、真实备案流程与审核结果需真实业务环境与
专业法务判断,命令行不可复现(X)。
"""

from __future__ import annotations

import argparse
import csv
import json
import os
import sys
import xml.etree.ElementTree as ET

STAGE_LABEL = {"dev": "开发期", "prereview": "提审前", "live": "上线后"}


def load(cfg_dir: str, name: str) -> dict:
    with open(os.path.join(cfg_dir, f"{name}.json"), encoding="utf-8") as f:
        return json.load(f)


def required_ids(cfg: dict) -> list[str]:
    """required:must / must-if-paid(有内购时计入)。should 不计入覆盖率分母。"""
    out = []
    for it in cfg["items"]:
        if it["level"] == "must" or (it["level"] == "must-if-paid" and cfg["hasPaid"]):
            out.append(it["id"])
    return out


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


def validate_filing(cfg: dict) -> list[dict]:
    """备案项 schema 校验:返回违规列表(itemId + 字段 + 原因)。"""
    schema = cfg["filingSchema"]
    enum = set(schema["statusEnum"])
    allow_empty = set(schema["evidenceRequiredUnless"])
    bad = []
    for row in cfg["filingItems"]:
        iid = row.get("id", "?")
        for key in schema["required"]:
            if key not in row:
                bad.append({"itemId": iid, "field": key, "reason": "missingField"})
        if row.get("status") not in enum:
            bad.append({"itemId": iid, "field": "status", "reason": "badEnum"})
        if row.get("status") not in allow_empty and not str(row.get("evidence", "")).strip():
            bad.append({"itemId": iid, "field": "evidence", "reason": "emptyEvidence"})
        if not str(row.get("entity", "")).strip():
            bad.append({"itemId": iid, "field": "entity", "reason": "emptyEntity"})
    return bad


def txt(parent: ET.Element, x: float, y: float, s: str, size: int = 12,
        fill: str = "#374151", anchor: str | None = None) -> None:
    a = {"x": str(x), "y": str(y), "font-size": str(size), "fill": fill}
    if anchor:
        a["text-anchor"] = anchor
    ET.SubElement(parent, "text", a).text = s


def svg(path: str, rows: list[dict]) -> int:
    """纯 Python SVG:三阶段覆盖率横条。"""
    W, PAD, ROW = 720, 120, 46
    H = 70 + ROW * len(rows)
    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"})
    txt(r, 16, 26, "《合成小岛》资质清单覆盖率(示例清单,‰)", 15, "#111827")
    bmax = W - PAD - 90
    for i, row in enumerate(rows):
        y = 70 + i * ROW
        txt(r, 16, y + 20, f'{STAGE_LABEL[row["stage"]]} ({row["held"]}/{row["required"]})')
        w = bmax * row["coveragePermille"] // 1000
        color = "#dc2626" if row["coveragePermille"] < 500 else (
            "#f59e0b" if row["coveragePermille"] < 1000 else "#10b981")
        ET.SubElement(r, "rect", {"x": str(PAD), "y": str(y + 6), "width": str(w),
                                  "height": "20", "rx": "4", "fill": color})
        txt(r, PAD + w + 8, y + 21, f'{row["coveragePermille"]}‰', 12, "#6b7280")
    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 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, "l10_license")
    name_of = {it["id"]: it["name"] for it in cfg["items"]}
    stage_of = {it["id"]: it["stage"] for it in cfg["items"]}
    rows = [coverage(cfg, s) for s in cfg["stages"]]
    for row in rows:
        row["missingDetail"] = [
            {"id": m, "name": name_of[m], "dueStage": stage_of[m]} for m in row["missing"]]
    violations = validate_filing(cfg)
    os.makedirs(a.out, exist_ok=True)
    sb = svg(os.path.join(a.out, "l10_license.svg"), rows)
    report = {"source": "sim/l10_license.py", "tier": "S", "game": cfg["game"],
              "observedAt": cfg["observedAt"], "note": cfg["note"],
              "requiredCount": len(required_ids(cfg)), "hasPaid": cfg["hasPaid"],
              "stages": rows, "filingViolations": violations, "svgBytes": sb}
    with open(os.path.join(a.out, "l10_license.json"), "w", encoding="utf-8") as f:
        json.dump(report, f, ensure_ascii=False, indent=2)
    with open(os.path.join(a.out, "l10_license.csv"), "w", encoding="utf-8", newline="") as f:
        w = csv.writer(f)
        w.writerow(["stage", "required", "held", "coveragePermille", "missing"])
        for row in rows:
            w.writerow([row["stage"], row["required"], row["held"],
                        row["coveragePermille"], "|".join(row["missing"])])
    for row in rows:
        print(f'{STAGE_LABEL[row["stage"]]:<4} 覆盖 {row["held"]}/{row["required"]} '
              f'= {row["coveragePermille"]}‰  缺: {",".join(row["missing"]) or "无"}')
    print(f"备案项 schema 违规 {len(violations)} 条:")
    for v in violations:
        print(f'  {v["itemId"]}.{v["field"]} -> {v["reason"]}')
    return 0


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

本机跑出来的输出(同机相对量级,绝对数值不可移植):

开发期  覆盖 1/8 = 125‰  缺: soft-copyright,icp-filing,game-approval,category-permit,privacy-policy,realname-antiaddiction,pay-qualify
提审前  覆盖 3/8 = 375‰  缺: game-approval,category-permit,privacy-policy,realname-antiaddiction,pay-qualify
上线后  覆盖 8/8 = 1000‰  缺: 无
备案项 schema 违规 2 条:
  pay-qualify.evidence -> emptyEvidence
  pay-qualify.entity -> emptyEntity

三阶段的覆盖率对照(125‰ → 375‰ → 1000‰)把「清单何时补齐」变成一条可见的曲线。更值得注意的是最后两条 schema 违规:pay-qualify 这条记录主体名空、且 approved 状态却没有凭证号——它在覆盖率里被算作「已持有」,但字段是残缺的。这正是覆盖率会骗人的地方:「有没有办」和「办得对不对」是两件事,只有 schema 校验能把后者揪出来。

平台规则的引用纪律(X)

上面所有条目都是对公开可观察机制的归纳,观察时点为 2026-10。真实平台上,具体要办哪些证、类目资质如何核验、版号审批要多久,都属于不可复现的真实业务过程,命令行跑不出来,标 X。本节的做法不是给你一份「标准答案清单」,而是给你一套装清单的容器:把真实法务给出的结论填进 items 与 filingSchema,脚本负责校验一致性与完整性。任何具体资质结论都必须由专业法务判断,脚本只保证「清单内部自洽、字段不残缺」。

把清单接到上线流程(交接点)

本节只负责把清单做对,不负责流程编排。清单里每一条的 stage 字段就是与第 9 章上线流程的交接点:开发期该办的在开工前办、提审前该办的在提审材料冻结前办、上线后该办的在上线后一个月内补齐。第 9 章讲提审、灰度与回滚的流程本身,本节讲清单的内容与校验,两者在「提审材料冻结」这一刻对接,不重叠。

阅读导航:上一节:目录 · 下一节:10.2 内容安全与审核红线 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「minigame」更多文章

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