小游戏商业化全栈设计
一、商业化架构总览
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–5 | 低 | UI 融入式展示 |
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–30 | 1–3 天 | 40% | 高性价比引导首充 |
| 大额消耗包 | ¥68–198 | 7–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,异常降级 |
| 反作弊 | 多信号评分制,规则可配置,阻断→复核→警告分级 |
延伸阅读
- 小游戏创作者经济生态设计 — 本文广告/IAP 与创作者分润的结合
- 小游戏开发者增长与获客体系 — CAC 优化与获客渠道
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。