9.2 COPPA 与儿童向游戏的限制

COPPA(美国,13 岁以下)与儿童专区上架,落到工程里就是一张「年龄 + 平台 → 能力」的矩阵。本节把个性化广告、行为追踪、社交、内购等八项能力做成可跑的能力解析内核,用 P 层 TypeScript 与 Python 参考实现逐字符对拍,并断言 kids 模式必须封禁个性化广告与行为追踪。本节只给工程实现,法律结论请咨询律师。

站内那篇独立游戏出海合规与税务风险清单 已经点过未成年人保护这一条,本节不重复它的法律结论,只把它翻译成一张能力矩阵。边界照旧先说清楚:本节给的是工程实现,法律结论请咨询律师;你的游戏算不算「儿童向」、要不要做可验证的家长同意,都得由法务判断。

儿童向不是「加一条免责声明」

儿童向的合规难点不在于「有没有写声明」,而在于整套能力要整体收紧:个性化广告要关、行为追踪要关、第三方分析要关、社交与内购要么关要么走家长同意。把这些散落在各处的 if 收进一张模式表,才能保证「上架儿童专区」时不会有哪个 SDK 悄悄漏开。下面这张表是公开可观察的平台与法规要求的工程归纳(观察时点 2026-10),不是法条原文:

要求来源公开可观察机制工程动作
COPPA(美国)13 岁以下收集数据需可验证家长同意年龄门 + parentalConsent
GDPR Art.8(欧盟)同意的年龄门槛按成员国定年龄信号 + 分档
家庭 / 儿童专区上架整体按儿童处理familyProgram 强制 kids
平台广告政策儿童向禁个性化广告personalized_ads=disabled

能力矩阵:年龄 + 平台 → 模式

D 层配置把年龄段、受管能力、四种模式、平台家庭标记、测试用例全写成数据。mechanism 记录每项能力「怎么被约束」——是彻底 disabled,还是退成 contextualOnly,还是需要 parentalConsent。注意 platformFlags.familyProgram:一旦在儿童专区上架,无论玩家填几岁都按 kids 处理。

{
  "version": 1,
  "game": "merge-island",
  "observedAt": "2026-10",
  "note": "儿童向与未成年人能力矩阵(D 层)。ageBands 是年龄段;capabilities 是受管能力;profiles 按模式给出每项能力的 allowed 与 mechanism(disabled/contextualOnly/consent/parentalConsent/filtered/nicknameOnly/storeBilling);platformFlags.familyProgram 表示「家庭/儿童专区」上架,一旦为真则整体按 kids 处理。口径为工程建模假设:真实平台家庭政策、真实年龄分级与最终合规判定需真实业务环境与专业法务判断,命令行不可复现(X)。全整数,无浮点。",
  "ageBands": ["under13", "teen13_15", "teen16_17", "adult"],
  "capabilities": ["personalized_ads", "behavioral_tracking", "third_party_analytics", "social_chat", "leaderboard", "iap", "push", "data_sharing"],
  "profiles": [
    {"mode": "kids", "rules": [
      {"capability": "personalized_ads", "allowed": false, "mechanism": "disabled"},
      {"capability": "behavioral_tracking", "allowed": false, "mechanism": "disabled"},
      {"capability": "third_party_analytics", "allowed": false, "mechanism": "disabled"},
      {"capability": "social_chat", "allowed": false, "mechanism": "disabled"},
      {"capability": "leaderboard", "allowed": true, "mechanism": "nicknameOnly"},
      {"capability": "iap", "allowed": false, "mechanism": "parentalConsent"},
      {"capability": "push", "allowed": false, "mechanism": "disabled"},
      {"capability": "data_sharing", "allowed": false, "mechanism": "disabled"}
    ]},
    {"mode": "teen_young", "rules": [
      {"capability": "personalized_ads", "allowed": false, "mechanism": "contextualOnly"},
      {"capability": "behavioral_tracking", "allowed": false, "mechanism": "disabled"},
      {"capability": "third_party_analytics", "allowed": false, "mechanism": "disabled"},
      {"capability": "social_chat", "allowed": false, "mechanism": "disabled"},
      {"capability": "leaderboard", "allowed": true, "mechanism": "nicknameOnly"},
      {"capability": "iap", "allowed": false, "mechanism": "parentalConsent"},
      {"capability": "push", "allowed": true, "mechanism": "consent"},
      {"capability": "data_sharing", "allowed": false, "mechanism": "disabled"}
    ]},
    {"mode": "teen", "rules": [
      {"capability": "personalized_ads", "allowed": false, "mechanism": "contextualOnly"},
      {"capability": "behavioral_tracking", "allowed": false, "mechanism": "disabled"},
      {"capability": "third_party_analytics", "allowed": true, "mechanism": "consent"},
      {"capability": "social_chat", "allowed": true, "mechanism": "filtered"},
      {"capability": "leaderboard", "allowed": true, "mechanism": "nicknameOnly"},
      {"capability": "iap", "allowed": true, "mechanism": "parentalConsent"},
      {"capability": "push", "allowed": true, "mechanism": "consent"},
      {"capability": "data_sharing", "allowed": false, "mechanism": "disabled"}
    ]},
    {"mode": "general", "rules": [
      {"capability": "personalized_ads", "allowed": true, "mechanism": "consent"},
      {"capability": "behavioral_tracking", "allowed": true, "mechanism": "consent"},
      {"capability": "third_party_analytics", "allowed": true, "mechanism": "consent"},
      {"capability": "social_chat", "allowed": true, "mechanism": "filtered"},
      {"capability": "leaderboard", "allowed": true, "mechanism": "nicknameOnly"},
      {"capability": "iap", "allowed": true, "mechanism": "storeBilling"},
      {"capability": "push", "allowed": true, "mechanism": "consent"},
      {"capability": "data_sharing", "allowed": true, "mechanism": "consent"}
    ]}
  ],
  "platformFlags": [
    {"platform": "family_store", "familyProgram": true},
    {"platform": "general_store", "familyProgram": false}
  ],
  "cases": [
    {"id": "c1", "ageBand": "under13", "platform": "general_store"},
    {"id": "c2", "ageBand": "adult", "platform": "family_store"},
    {"id": "c3", "ageBand": "teen13_15", "platform": "general_store"},
    {"id": "c4", "ageBand": "teen16_17", "platform": "general_store"},
    {"id": "c5", "ageBand": "adult", "platform": "general_store"}
  ]
}

P 层:能力解析内核

内核只做一件事:把「年龄 + 平台」解析成一个模式,再读出该模式的能力表。isBlocked 遵循「最保守」原则——清单里没显式放行的能力一律视为阻断,防止新增能力时默认放行。全整数、无随机:

// file: core/src/g09/kids.ts
// tier: P
// verify: node --test 'core/test/g09_*.test.ts'
//
// 儿童向 / 未成年人能力解析内核:与 sim/g09_kids_gate.py 逐位等价的 TypeScript 对偶实现。
// 读同一份 config/g09_kids_policy.json;全整数、无浮点、无随机、禁 Math.random(纯确定性回放)。
// 只用可擦除语法(禁 enum / namespace / 参数属性 / 装饰器)。

export interface CapRule { readonly capability: string; readonly allowed: boolean; readonly mechanism: string; }
export interface Profile { readonly mode: string; readonly rules: readonly CapRule[]; }
export interface PlatformFlag { readonly platform: string; readonly familyProgram: boolean; }
export interface KidsCase { readonly id: string; readonly ageBand: string; readonly platform: string; }
export interface KidsConfig {
  readonly version: number; readonly game: string; readonly ageBands: readonly string[];
  readonly capabilities: readonly string[]; readonly profiles: readonly Profile[];
  readonly platformFlags: readonly PlatformFlag[]; readonly cases: readonly KidsCase[];
}
export interface Effective { readonly id: string; readonly mode: string; readonly rules: readonly CapRule[]; }

/** 年龄 + 平台家庭专区 → 能力模式。家庭专区优先,其次按年龄段。 */
export function resolveMode(ageBand: string, familyProgram: boolean): string {
  if (familyProgram) return "kids";
  if (ageBand === "under13") return "kids";
  if (ageBand === "teen13_15") return "teen_young";
  if (ageBand === "teen16_17") return "teen";
  return "general";
}

export function resolveCase(cfg: KidsConfig, c: KidsCase): Effective {
  const flag = cfg.platformFlags.find((p) => p.platform === c.platform)!;
  const mode = resolveMode(c.ageBand, flag.familyProgram);
  const prof = cfg.profiles.find((p) => p.mode === mode)!;
  return { id: c.id, mode, rules: prof.rules };
}

export function allowedCount(e: Effective): number {
  return e.rules.filter((r) => r.allowed).length;
}

/** 缺规则按「最保守」处理:未显式放行即视为阻断。 */
export function isBlocked(e: Effective, cap: string): boolean {
  const r = e.rules.find((x) => x.capability === cap);
  return r === undefined ? true : !r.allowed;
}

/** 跨语言对拍摘要:与 sim/g09_kids_gate.py 的 digest 逐字符一致。 */
export function digest(cfg: KidsConfig): string {
  const eff = cfg.cases.map((c) => resolveCase(cfg, c));
  const head = eff.map((e) => `${e.id}:${e.mode}:${allowedCount(e)}`).join(",");
  const total = eff.length * cfg.capabilities.length;
  const allowed = eff.reduce((s, e) => s + allowedCount(e), 0);
  return `${head};allowed=${allowed},blocked=${total - allowed},total=${total}`;
}

硬断言与跨语言对拍

测试测的是跨语言逐字符一致,外加三条不变量:家庭专区强制 kids、未满 13 岁强制 kids、kids 模式必须封禁个性化广告与行为追踪。

// file: core/test/g09_kids.test.ts
// tier: P
// verify: node --test 'core/test/g09_*.test.ts'
//
// 硬断言:TS 能力解析内核复算的摘要,与 Python 侧 sim/g09_kids_gate.py 落盘的金标准逐字符一致;
// 并验家庭专区强制 kids、未满 13 岁强制 kids、kids 模式封禁个性化广告与行为追踪。

import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

import { resolveMode, resolveCase, allowedCount, isBlocked, digest,
  type KidsConfig } from "../src/g09/kids.ts";

const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
const cfg = JSON.parse(readFileSync(join(ROOT, "config", "g09_kids_policy.json"), "utf8")) as KidsConfig;

// Python 侧同机实跑落盘的金标准摘要
const EXPECTED = "c1:kids:1,c2:kids:1,c3:teen_young:2,c4:teen:5,c5:general:8"
  + ";allowed=17,blocked=23,total=40";

test("跨语言一致:TS 能力摘要 == Python 金标准", () => {
  assert.equal(digest(cfg), EXPECTED);
});

test("家庭专区优先:成人账号上架儿童专区也强制 kids", () => {
  assert.equal(resolveMode("adult", true), "kids");
  assert.equal(resolveMode("adult", false), "general");
});

test("未满 13 岁强制 kids,无论平台", () => {
  assert.equal(resolveMode("under13", false), "kids");
  assert.equal(resolveCase(cfg, cfg.cases.find((c) => c.id === "c1")!).mode, "kids");
});

test("kids 模式封禁个性化广告与行为追踪(不变量)", () => {
  const e = resolveCase(cfg, cfg.cases.find((c) => c.id === "c1")!);
  assert.equal(isBlocked(e, "personalized_ads"), true);
  assert.equal(isBlocked(e, "behavioral_tracking"), true);
  assert.equal(allowedCount(e), 1);
});

test("成人一般模式八项全开", () => {
  const e = resolveCase(cfg, cfg.cases.find((c) => c.id === "c5")!);
  assert.equal(e.mode, "general");
  assert.equal(allowedCount(e), 8);
});

同机实跑(Node 26,strip-only 直跑 .ts):

$ node --test core/test/g09_kids.test.ts
✔ 跨语言一致:TS 能力摘要 == Python 金标准
✔ 家庭专区优先:成人账号上架儿童专区也强制 kids
✔ 未满 13 岁强制 kids,无论平台
✔ kids 模式封禁个性化广告与行为追踪(不变量)
✔ 成人一般模式八项全开
ℹ tests 5
ℹ pass 5
ℹ fail 0

Python 参考实现(sim/g09_kids_gate.py)对同一份配置产出的金标准摘要:

digest=c1:kids:1,c2:kids:1,c3:teen_young:2,c4:teen:5,c5:general:8;allowed=17,blocked=23,total=40

为什么用「模式」而不是散落的年龄判断

五个用例的摘要能读出这条线的形状:c1(未满 13 岁)与 c2(成人但在儿童专区)解析到同一个 kids 模式,各只放行 1 项能力(leaderboard 也只允许昵称);c3(13–15 岁)是 teen_young,放行 2 项;c4(16–17 岁)是 teen,放行 5 项;c5(成人)全开 8 项。如果把这套逻辑写成散落各处的 if (age < 13),那么「家庭专区里一个填了成年的账号」就是最容易漏的洞——而 resolveMode 把它变成一条优先级规则:familyProgram 先于年龄段。40 个「用例 × 能力」格子中 23 个被阻断,这个数字本身就是给产品看的「儿童向要砍掉多少功能」。

这些数不能靠命令行复现(X)

年龄分级、平台家庭政策、真实审核结果与最终合规判定都需要真实业务环境与专业法务判断,命令行无法复现,标为 X。真正能复现的,是给定这张模式表之后的解析结果——那部分 TS 内核与 Python 参考实现逐字符一致(5/5 通过)。

自检

检查项本节结论
是否重复合规清单的法律结论否,只做能力矩阵,首段外链既有长文
家庭专区是否强制 kids是,familyProgram 优先级最高
未满 13 岁是否强制 kids是,resolveMode 单测断言
未知能力默认放行还是阻断阻断(最保守),isBlocked 返回 true
跨语言是否一致是,TS 摘要 == Python 金标准,5/5 通过
是否提供法律意见否,法律结论请咨询律师

本节把「儿童向」从「一条免责声明」变成了一张可机读、可断言的能力矩阵。下一节进入 9.3,处理两个连在一起的问题:同意怎么被记录与传播,以及数据要出境时,传输门怎么判。

阅读导航:上一节:9.1 GDPR / CCPA 的工程落点 · 下一节:9.3 同意管理与数据出境 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「minigame」更多文章

  1. 附录 F:全书公式、参数与数据集索引
  2. 9.3 同意管理与数据出境
  3. 9.1 GDPR / CCPA 的工程落点