小游戏商业化全栈设计:广告聚合、IAP 道具经济与 LTV 预测模型

系统讲解小游戏平台的商业化全栈架构。涵盖广告聚合层设计(Waterfall + Header Bidding 混合出价)、IAP 道具经济体系(消耗品/订阅/战令)、LTV/CAC 预测模型、归因分析(Adjust/AppsFlyer 对接)、防作弊检测与 eCPM 优化策略。提供 AdMediator、IAPManager 的完整 TypeScript 实现与财务模型数据表。

小游戏商业化全栈设计

一、商业化架构总览

MiniPlay Studio 的商业化体系采用**“广告 + IAP + 订阅”**三元混合模型:

graph TB
    subgraph "收入层"
        A[广告变现 IAA] --> B[AdMediator 聚合层]
        C[内购变现 IAP] --> D[IAPManager 商店层]
        E[订阅服务] --> D
    end

    subgraph "分析层"
        F[归因分析 Attribution]
        G[LTC/CAC 预测]
        H[反作弊检测]
    end

    subgraph "运营层"
        I[A/B 测试]
        J[用户分层]
        K[动态定价]
    end

    B --> F
    D --> F
    F --> G
    G --> K
    H --> J
    J --> I
    I --> B
    I --> D

二、广告聚合层:Waterfall + Header Bidding 混合出价

2.1 广告位类型与 eCPM 基准

广告类型典型 eCPM(国内)典型 eCPM(海外)用户侵入性适用场景
激励视频¥80–150$8–15低(用户主动选择)复活、双倍奖励、解锁内容
插屏¥30–60$3–6关卡切换、暂停菜单
Banner¥5–15$0.5–2高(常驻)游戏底部/顶部
全屏视频¥50–100$5–10自然断点
原生广告¥20–40$2–5UI 融入式展示

2.2 AdMediator 混合出价实现

interface AdSource {
  name: string;
  platform: PlatformName;
  adUnitId: string;
  type: 'reward' | 'interstitial' | 'banner' | 'fullscreen';
  priority: number; // Waterfall 优先级
  timeout: number;  // 超时时间 ms
  minECPM?: number; // Header Bidding 最低出价
}

interface BidResponse {
  source: string;
  bidPrice: number; // eCPM
  currency: string;
  ttl: number;      // 出价有效期
  adPayload?: any;  // 广告素材
}

class AdMediator {
  private sources: AdSource[] = [];
  private cache = new Map<string, { ad: any; expiry: number }>();
  private profile: PlatformProfile;

  constructor(profile: PlatformProfile) {
    this.profile = profile;
    this.initDefaultSources();
  }

  private initDefaultSources(): void {
    // 根据平台动态选择广告源
    if (this.profile.name === 'wechat') {
      this.sources = [
        { name: 'wechat_reward', platform: 'wechat', adUnitId: 'ad_xxx1', type: 'reward', priority: 1, timeout: 3000 },
        { name: 'gdt_reward', platform: 'wechat', adUnitId: 'ad_xxx2', type: 'reward', priority: 2, timeout: 3000 },
      ];
    } else if (this.profile.name === 'tiktok') {
      this.sources = [
        { name: 'pangle_reward', platform: 'tiktok', adUnitId: 'ad_yyy1', type: 'reward', priority: 1, timeout: 3000 },
      ];
    }
    // Web 环境使用 AdSense / AdMob
  }

  // 核心:混合出价
  async fetchAd(adType: 'reward' | 'interstitial'): Promise<any> {
    // Phase 1: Header Bidding(并行竞价)
    const hbSources = this.sources.filter(s => s.type === adType && s.minECPM);
    const bids = await Promise.all(
      hbSources.map(s => this.fetchBid(s).catch(() => null))
    );
    const validBids = bids.filter(Boolean) as BidResponse[];

    if (validBids.length > 0) {
      validBids.sort((a, b) => b.bidPrice - a.bidPrice);
      const winner = validBids[0];
      console.log(`[AdMediator] Header Bidding winner: ${winner.source} @ ${winner.bidPrice}`);
      return this.loadAdFromBid(winner);
    }

    // Phase 2: Waterfall(串行回退)
    const wfSources = this.sources
      .filter(s => s.type === adType)
      .sort((a, b) => a.priority - b.priority);

    for (const source of wfSources) {
      try {
        const ad = await this.loadAdWithTimeout(source);
        if (ad) {
          console.log(`[AdMediator] Waterfall filled by: ${source.name}`);
          return ad;
        }
      } catch (e) {
        console.warn(`[AdMediator] ${source.name} failed:`, e);
      }
    }

    throw new Error('No ad available from all sources');
  }

  private async fetchBid(source: AdSource): Promise<BidResponse | null> {
    // 调用各平台 bidding SDK
    switch (source.platform) {
      case 'wechat':
        return this.fetchWeChatBid(source);
      case 'tiktok':
        return this.fetchPangleBid(source);
      default:
        return null;
    }
  }

  private async fetchWeChatBid(source: AdSource): Promise<BidResponse | null> {
    // 微信不支持真正的 Header Bidding,模拟基于历史 eCPM 预估
    const historicalECPM = this.getHistoricalECPM(source.name);
    if (historicalECPM < (source.minECPM || 0)) return null;

    return {
      source: source.name,
      bidPrice: historicalECPM,
      currency: 'CNY',
      ttl: 30000,
    };
  }

  private async loadAdWithTimeout(source: AdSource): Promise<any> {
    const adapter = PlatformAdapterFactory.create();
    // 使用统一的 PlatformAdapter 接口加载广告
    switch (source.type) {
      case 'reward':
        return adapter.showRewardAd({ adUnitId: source.adUnitId });
      case 'interstitial':
        return adapter.showInterstitialAd({ adUnitId: source.adUnitId });
      default:
        return null;
    }
  }

  private getHistoricalECPM(sourceName: string): number {
    // 从本地存储或后端获取历史 eCPM
    const history = JSON.parse(localStorage.getItem(`ecpm_${sourceName}`) || '[]');
    if (history.length === 0) return 50;
    return history.reduce((s: number, v: number) => s + v, 0) / history.length;
  }

  // 展示频率控制
  private impressionCounters = new Map<string, { count: number; date: string }>();

  canShow(adType: string, userId: string): boolean {
    const key = `${userId}_${adType}`;
    const today = new Date().toISOString().slice(0, 10);
    const counter = this.impressionCounters.get(key);

    if (!counter || counter.date !== today) {
      this.impressionCounters.set(key, { count: 0, date: today });
      return true;
    }

    // 每日频次上限
    const limits: Record<string, number> = {
      reward: 8,
      interstitial: 12,
      banner: 999,
    };

    return counter.count < (limits[adType] || 10);
  }
}

2.3 展示策略优化

interface AdPlacement {
  id: string;
  type: 'reward' | 'interstitial' | 'banner';
  trigger: 'user_action' | 'level_complete' | 'game_over' | 'natural_break' | 'continuous';
  cooldown: number; // 两次展示间隔(秒)
  dailyCap: number;
  eCPMTarget: number;
}

const AD_PLACEMENTS: AdPlacement[] = [
  { id: 'reward_revive', type: 'reward', trigger: 'user_action', cooldown: 0, dailyCap: 10, eCPMTarget: 120 },
  { id: 'reward_double_coin', type: 'reward', trigger: 'user_action', cooldown: 0, dailyCap: 20, eCPMTarget: 100 },
  { id: 'interstitial_level_complete', type: 'interstitial', trigger: 'level_complete', cooldown: 60, dailyCap: 8, eCPMTarget: 50 },
  { id: 'interstitial_game_over', type: 'interstitial', trigger: 'game_over', cooldown: 30, dailyCap: 6, eCPMTarget: 40 },
  { id: 'banner_bottom', type: 'banner', trigger: 'continuous', cooldown: 0, dailyCap: 999, eCPMTarget: 10 },
];

三、IAP 道具经济体系

3.1 四种道具类型设计

type ProductType = 'consumable' | 'non-consumable' | 'subscription' | 'battle-pass';

interface Product {
  id: string;
  type: ProductType;
  name: string;
  description: string;
  price: number;
  currency: 'CNY' | 'USD';
  icon: string;
  rewards: Reward[];
  // 订阅特有
  subscriptionInfo?: {
    interval: 'week' | 'month' | 'year';
    trialDays: number;
    benefits: string[];
  };
  // 战令特有
  battlePassInfo?: {
    season: number;
    durationDays: number;
    tiers: BattlePassTier[];
  };
}

interface Reward {
  type: 'coin' | 'gem' | 'item' | 'skin' | 'boost';
  id: string;
  quantity: number;
}

interface BattlePassTier {
  level: number;
  freeReward?: Reward;
  premiumReward: Reward;
}

3.2 经济平衡表

道具类型定价区间复购周期收入占比目标设计要点
小额消耗包¥6–301–3 天40%高性价比引导首充
大额消耗包¥68–1987–14 天25%含 bonus 激励
非消耗品¥12–68一次性15%去广告、永久特权
月卡订阅¥30/月自动续费15%日领奖励 + 特权
战令¥68/赛季45–60 天5%游戏行为驱动付费

3.3 IAPManager 实现

class IAPManager {
  private inventory = new Map<string, number>(); // productId -> 拥有数量
  private subscriptions = new Map<string, { expiry: number }>();
  private battlePassProgress = new Map<number, { tier: number; premium: boolean }>();
  private adapter: PlatformAdapter;

  constructor() {
    this.adapter = PlatformAdapterFactory.create();
    this.loadInventory();
  }

  async purchase(product: Product): Promise<PurchaseResult> {
    // 验证:订阅检查是否已有活跃订阅
    if (product.type === 'subscription') {
      const existing = this.subscriptions.get(product.id);
      if (existing && existing.expiry > Date.now()) {
        return { success: false, error: 'Already subscribed' };
      }
    }

    // 调用平台支付
    const order: IAPOrder = {
      orderId: `ord_${Date.now()}`,
      productId: product.id,
      quantity: 1,
    };

    const receipt = await this.adapter.purchase(order);
    if (!receipt.success) {
      return { success: false, error: receipt.error?.message };
    }

    // 发放奖励
    await this.deliverProduct(product, receipt);

    return { success: true, receipt };
  }

  private async deliverProduct(product: Product, receipt: IAPReceipt): Promise<void> {
    switch (product.type) {
      case 'consumable':
        for (const reward of product.rewards) {
          const current = this.inventory.get(reward.id) || 0;
          this.inventory.set(reward.id, current + reward.quantity);
        }
        break;

      case 'non-consumable':
        this.inventory.set(product.id, 1);
        break;

      case 'subscription':
        const days = product.subscriptionInfo!.interval === 'month' ? 30 : 365;
        this.subscriptions.set(product.id, {
          expiry: Date.now() + days * 24 * 60 * 60 * 1000,
        });
        break;

      case 'battle-pass':
        this.battlePassProgress.set(product.battlePassInfo!.season, {
          tier: 0,
          premium: true,
        });
        break;
    }

    await this.saveInventory();
  }

  hasSubscription(productId: string): boolean {
    const sub = this.subscriptions.get(productId);
    return !!sub && sub.expiry > Date.now();
  }

  getBattlePassProgress(season: number): { tier: number; premium: boolean } | null {
    return this.battlePassProgress.get(season) || null;
  }

  async saveInventory(): Promise<void> {
    const data = {
      inventory: Object.fromEntries(this.inventory),
      subscriptions: Object.fromEntries(this.subscriptions),
      battlePass: Object.fromEntries(this.battlePassProgress),
    };
    localStorage.setItem('iap_inventory', JSON.stringify(data));
  }

  private loadInventory(): void {
    const raw = localStorage.getItem('iap_inventory');
    if (!raw) return;
    try {
      const data = JSON.parse(raw);
      this.inventory = new Map(Object.entries(data.inventory || {}));
      this.subscriptions = new Map(Object.entries(data.subscriptions || {}));
      this.battlePassProgress = new Map(Object.entries(data.battlePass || {}));
    } catch {}
  }
}

interface PurchaseResult {
  success: boolean;
  receipt?: IAPReceipt;
  error?: string;
}

四、LTV / CAC 预测模型

4.1 LTV 预测:群组留存曲线法

interface RetentionCurve {
  day1: number;   // 首日留存率
  day7: number;
  day14: number;
  day30: number;
  day60: number;
  day90: number;
}

class LTVEstimator {
  // 基于留存曲线估算 LTV
  estimateLTV(
    arpdau: number, // 每活跃用户日均收入
    retention: RetentionCurve
  ): number {
    // 简单积分法:假设 180 天生命周期
    const dailyRetentions = this.interpolateRetention(retention);
    let ltv = 0;
    for (let day = 1; day <= 180; day++) {
      const retentionRate = dailyRetentions[day] || 0;
      ltv += arpdau * retentionRate;
    }
    return ltv;
  }

  // 留存曲线插值
  private interpolateRetention(curve: RetentionCurve): number[] {
    const points = [
      { day: 1, val: curve.day1 },
      { day: 7, val: curve.day7 },
      { day: 14, val: curve.day14 },
      { day: 30, val: curve.day30 },
      { day: 60, val: curve.day60 },
      { day: 90, val: curve.day90 },
    ];

    const result: number[] = [0]; // day 0
    for (let i = 0; i < points.length - 1; i++) {
      const start = points[i];
      const end = points[i + 1];
      for (let day = start.day; day < end.day; day++) {
        const t = (day - start.day) / (end.day - start.day);
        // 对数衰减插值
        const logStart = Math.log(start.val + 0.01);
        const logEnd = Math.log(end.val + 0.01);
        result[day] = Math.exp(logStart + t * (logEnd - logStart)) - 0.01;
      }
    }
    // 90 天后指数衰减
    for (let day = 90; day <= 180; day++) {
      result[day] = result[day - 1] * 0.995;
    }

    return result;
  }

  // 基于首日行为的分群 LTV 预测
  estimateByCohort(cohort: UserCohort): number {
    const baseARPDAU = 0.5; // 基准
    const multiplier =
      (cohort.day1SessionCount * 0.1) +
      (cohort.day1AdViews * 0.02) +
      (cohort.hasIAP ? 2.0 : 0) +
      (cohort.socialShares * 0.15);

    const arpdau = baseARPDAU * (1 + multiplier);
    return this.estimateLTV(arpdau, cohort.retention);
  }
}

interface UserCohort {
  cohortDate: string;
  size: number;
  day1SessionCount: number;
  day1AdViews: number;
  hasIAP: boolean;
  socialShares: number;
  retention: RetentionCurve;
}

4.2 CAC 计算与盈亏平衡

class CACCalculator {
  // CAC = 总营销支出 / 新增用户数
  calculateCAC(spend: MarketingSpend, newUsers: number): number {
    const totalSpend =
      spend.paidAds +
      spend.influencer +
      spend.contentMarketing +
      spend.events +
      spend.other;
    return totalSpend / newUsers;
  }

  // LTV / CAC 比率
  getUnitEconomics(ltv: number, cac: number): {
    ratio: number;
    paybackPeriod: number; // 月
    isHealthy: boolean;
  } {
    const ratio = ltv / cac;
    // 假设 ARPU 线性增长,回收期 = CAC / 月 ARPU
    const monthlyARPU = ltv / 6; // 简化:LTV 约等于 6 个月 ARPU
    const paybackPeriod = cac / monthlyARPU;

    return {
      ratio,
      paybackPeriod,
      isHealthy: ratio >= 3 && paybackPeriod <= 12,
    };
  }
}

interface MarketingSpend {
  paidAds: number;
  influencer: number;
  contentMarketing: number;
  events: number;
  other: number;
}

单位经济健康标准

指标健康值说明
LTV / CAC≥ 3每获客成本产出至少 3 倍价值
CAC Payback≤ 12 个月12 个月内回本
月流失率≤ 10%月留存 ≥ 90%
付费率≥ 2%每 100 用户有 2 人付费

五、归因分析与数据追踪

5.1 事件追踪体系

interface AnalyticsEvent {
  event: string;
  userId: string;
  timestamp: number;
  properties: Record<string, any>;
}

class MonetizationAnalytics {
  private events: AnalyticsEvent[] = [];
  private flushThreshold = 50;

  track(event: string, properties: Record<string, any>): void {
    this.events.push({
      event,
      userId: this.getUserId(),
      timestamp: Date.now(),
      properties,
    });

    if (this.events.length >= this.flushThreshold) {
      this.flush();
    }
  }

  // 预置追踪事件
  trackAdImpression(adType: string, source: string, eCPM: number): void {
    this.track('ad_impression', { adType, source, eCPM });
  }

  trackAdClick(adType: string, source: string): void {
    this.track('ad_click', { adType, source });
  }

  trackPurchase(productId: string, revenue: number, currency: string): void {
    this.track('purchase', { productId, revenue, currency });
  }

  trackLevelStart(level: number): void {
    this.track('level_start', { level });
  }

  trackLevelComplete(level: number, score: number): void {
    this.track('level_complete', { level, score });
  }

  private flush(): void {
    // 发送到分析后端(如 Mixpanel、Amplitude、自建)
    const batch = this.events.splice(0, this.flushThreshold);
    fetch('/api/analytics/events', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(batch),
      keepalive: true,
    }).catch(() => {
      // 失败时重新入队
      this.events.unshift(...batch);
    });
  }

  private getUserId(): string {
    let id = localStorage.getItem('user_id');
    if (!id) {
      id = 'u_' + Math.random().toString(36).slice(2) + Date.now().toString(36);
      localStorage.setItem('user_id', id);
    }
    return id;
  }
}

六、防作弊检测

interface FraudSignals {
  deviceId: string;
  ip: string;
  adViewsPerHour: number;
  adCompletionRate: number;
  timeToComplete: number; // 激励视频从 show 到 close 的时间
  deviceFarmIndicators: boolean;
  vpnUsage: boolean;
}

class FraudDetector {
  private rules: FraudRule[] = [
    {
      name: 'excessive_ad_views',
      check: (s: FraudSignals) => s.adViewsPerHour > 20,
      score: 30,
    },
    {
      name: 'impossible_completion_time',
      check: (s: FraudSignals) => s.timeToComplete < 3000, // < 3 秒不可能看完
      score: 50,
    },
    {
      name: 'abnormal_completion_rate',
      check: (s: FraudSignals) => s.adCompletionRate > 0.98 || s.adCompletionRate < 0.3,
      score: 20,
    },
    {
      name: 'device_farm',
      check: (s: FraudSignals) => s.deviceFarmIndicators,
      score: 80,
    },
    {
      name: 'vpn_spoofing',
      check: (s: FraudSignals) => s.vpnUsage,
      score: 15,
    },
  ];

  evaluate(signals: FraudSignals): FraudVerdict {
    let score = 0;
    const triggered: string[] = [];

    for (const rule of this.rules) {
      if (rule.check(signals)) {
        score += rule.score;
        triggered.push(rule.name);
      }
    }

    if (score >= 80) return { action: 'block', score, triggered };
    if (score >= 50) return { action: 'review', score, triggered };
    if (score >= 20) return { action: 'warn', score, triggered };
    return { action: 'allow', score, triggered };
  }
}

interface FraudRule {
  name: string;
  check: (signals: FraudSignals) => boolean;
  score: number;
}

interface FraudVerdict {
  action: 'allow' | 'warn' | 'review' | 'block';
  score: number;
  triggered: string[];
}

七、商业化数据仪表板关键指标

const DASHBOARD_METRICS = {
  // 广告
  adRequests: '广告请求数',
  adFillRate: '填充率',
  adImpressions: '展示数',
  adClicks: '点击数',
  adCTR: '点击率',
  adECPM: '千次展示收入',
  adRevenue: '广告收入',

  // IAP
  iapTransactions: '内购笔数',
  iapRevenue: '内购收入',
  iapARPU: '每用户平均收入',
  iapARPPU: '每付费用户平均收入',
  iapConversionRate: '付费转化率',

  // 综合
  dau: '日活跃用户',
  mau: '月活跃用户',
  retention: '留存率',
  ltv: '生命周期价值',
  cac: '获客成本',
  ltvCacRatio: 'LTV/CAC',
};

八、总结与最佳实践

维度关键设计
广告Waterfall + Header Bidding 混合,频次控制,精准展示时机
IAP四类型道具体系,首充引导,订阅自动续费,战令赛季制
预测留存曲线积分 LTV,分群预测,LTV/CAC ≥ 3 为健康线
归因全链路事件追踪,实时 flush,异常降级
反作弊多信号评分制,规则可配置,阻断→复核→警告分级

延伸阅读

继续阅读

探索更多技术文章

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

全部文章 返回首页

「games」更多文章

  1. 小游戏开发者增长与获客体系:裂变邀请、Game Jam 与开发者社区运营
  2. 小游戏创作者经济生态设计:插件商店、收益分润与创作者成长体系
  3. 小游戏性能剖析与内存管理:Chrome DevTools 实战 + 引擎级 Profiler