bw-mini-app-server/docs/credit-and-ad-system.md

14 KiB
Raw Permalink Blame History

积分系统与广告服务配置指南

1. 积分系统设计

1.1 积分获取方式

  • 观看广告: 每次完整观看激励视频广告获得5-20积分
  • 订阅赠送: 包月用户每月自动获得积分
  • 新用户奖励: 注册即送100积分
  • 每日签到: 连续签到获得递增积分奖励
  • 分享奖励: 分享作品获得积分

1.2 积分消耗规则

  • 图片生成: 基础10积分高质量15积分
  • 视频生成: 基础50积分高质量75积分
  • 高级功能: 特殊滤镜、风格转换等

2. 积分服务实现

2.1 积分管理服务

// src/services/credit.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserCredit, CreditType, CreditSource } from '../entities/user-credit.entity';
import { User } from '../entities/user.entity';
import { PlatformType } from '../entities/platform-user.entity';

@Injectable()
export class CreditService {
  constructor(
    @InjectRepository(UserCredit)
    private readonly creditRepository: Repository<UserCredit>,
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {}

  // 获取用户当前积分余额
  async getUserBalance(userId: string, platform: PlatformType): Promise<number> {
    const latestRecord = await this.creditRepository.findOne({
      where: { userId, platform },
      order: { createdAt: 'DESC' },
    });
    
    return latestRecord?.balance || 0;
  }

  // 检查用户积分是否足够
  async checkBalance(userId: string, platform: PlatformType, requiredAmount: number): Promise<boolean> {
    const currentBalance = await this.getUserBalance(userId, platform);
    return currentBalance >= requiredAmount;
  }

  // 增加积分
  async addCredits(
    userId: string,
    platform: PlatformType,
    amount: number,
    source: CreditSource,
    description?: string,
    referenceId?: string
  ): Promise<UserCredit> {
    const currentBalance = await this.getUserBalance(userId, platform);
    const newBalance = currentBalance + amount;

    const creditRecord = this.creditRepository.create({
      userId,
      platform,
      type: CreditType.REWARD,
      source,
      amount,
      balance: newBalance,
      description,
      referenceId,
    });

    return this.creditRepository.save(creditRecord);
  }

  // 消耗积分
  async consumeCredits(
    userId: string,
    platform: PlatformType,
    amount: number,
    source: CreditSource,
    referenceId?: string
  ): Promise<UserCredit> {
    const currentBalance = await this.getUserBalance(userId, platform);
    
    if (currentBalance < amount) {
      throw new Error('积分不足');
    }

    const newBalance = currentBalance - amount;

    const creditRecord = this.creditRepository.create({
      userId,
      platform,
      type: CreditType.CONSUME,
      source,
      amount: -amount, // 负数表示扣除
      balance: newBalance,
      description: `消耗积分: ${source}`,
      referenceId,
    });

    return this.creditRepository.save(creditRecord);
  }

  // 退还积分
  async refundCredits(
    userId: string,
    platform: PlatformType,
    amount: number,
    source: CreditSource,
    referenceId?: string
  ): Promise<UserCredit> {
    const currentBalance = await this.getUserBalance(userId, platform);
    const newBalance = currentBalance + amount;

    const creditRecord = this.creditRepository.create({
      userId,
      platform,
      type: CreditType.REFUND,
      source,
      amount,
      balance: newBalance,
      description: `积分退还: ${source}`,
      referenceId,
    });

    return this.creditRepository.save(creditRecord);
  }

  // 获取积分历史记录
  async getCreditHistory(
    userId: string,
    platform: PlatformType,
    page: number = 1,
    limit: number = 20
  ) {
    const [records, total] = await this.creditRepository.findAndCount({
      where: { userId, platform },
      order: { createdAt: 'DESC' },
      skip: (page - 1) * limit,
      take: limit,
    });

    return {
      records,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  // 新用户注册奖励
  async grantNewUserBonus(userId: string, platform: PlatformType): Promise<UserCredit> {
    const bonusAmount = 100; // 新用户奖励100积分
    
    return this.addCredits(
      userId,
      platform,
      bonusAmount,
      CreditSource.MANUAL,
      '新用户注册奖励',
      'new_user_bonus'
    );
  }

  // 每日签到奖励
  async grantDailyCheckIn(userId: string, platform: PlatformType, consecutiveDays: number): Promise<UserCredit> {
    // 连续签到奖励递增: 第1天5积分第2天10积分第7天及以上20积分
    let bonusAmount = 5;
    if (consecutiveDays >= 7) {
      bonusAmount = 20;
    } else if (consecutiveDays >= 3) {
      bonusAmount = 15;
    } else if (consecutiveDays >= 2) {
      bonusAmount = 10;
    }

    return this.addCredits(
      userId,
      platform,
      bonusAmount,
      CreditSource.MANUAL,
      `每日签到奖励 (连续${consecutiveDays}天)`,
      `daily_checkin_${consecutiveDays}`
    );
  }
}

2.2 广告服务实现

// src/services/ad.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AdWatch, AdType, AdStatus } from '../entities/ad-watch.entity';
import { CreditService } from './credit.service';
import { PlatformType } from '../entities/platform-user.entity';
import { CreditSource } from '../entities/user-credit.entity';

@Injectable()
export class AdService {
  constructor(
    @InjectRepository(AdWatch)
    private readonly adWatchRepository: Repository<AdWatch>,
    private readonly creditService: CreditService,
  ) {}

  // 开始观看广告
  async startAdWatch(
    userId: string,
    platform: PlatformType,
    adType: AdType,
    adId: string,
    adUnitId?: string
  ): Promise<AdWatch> {
    const adWatch = this.adWatchRepository.create({
      userId,
      platform,
      adType,
      adId,
      adUnitId,
      status: AdStatus.STARTED,
    });

    return this.adWatchRepository.save(adWatch);
  }

  // 完成观看广告
  async completeAdWatch(
    adWatchId: string,
    watchDuration: number,
    adData?: any
  ): Promise<{ adWatch: AdWatch; creditReward: number }> {
    const adWatch = await this.adWatchRepository.findOne({
      where: { id: adWatchId },
    });

    if (!adWatch) {
      throw new Error('广告观看记录不存在');
    }

    if (adWatch.status !== AdStatus.STARTED) {
      throw new Error('广告状态异常');
    }

    // 计算奖励积分
    const creditReward = this.calculateAdReward(adWatch.adType, watchDuration);

    // 更新广告观看记录
    await this.adWatchRepository.update(adWatchId, {
      status: AdStatus.COMPLETED,
      watchDuration,
      rewardCredits: creditReward,
      adData,
    });

    // 发放积分奖励
    if (creditReward > 0) {
      await this.creditService.addCredits(
        adWatch.userId,
        adWatch.platform,
        creditReward,
        CreditSource.AD_WATCH,
        `观看${adWatch.adType}广告奖励`,
        adWatchId
      );
    }

    const updatedAdWatch = await this.adWatchRepository.findOne({
      where: { id: adWatchId },
    });

    return {
      adWatch: updatedAdWatch,
      creditReward,
    };
  }

  // 跳过广告
  async skipAdWatch(adWatchId: string, watchDuration: number): Promise<AdWatch> {
    await this.adWatchRepository.update(adWatchId, {
      status: AdStatus.SKIPPED,
      watchDuration,
      rewardCredits: 0, // 跳过广告无奖励
    });

    return this.adWatchRepository.findOne({ where: { id: adWatchId } });
  }

  // 广告观看失败
  async failAdWatch(adWatchId: string, errorMessage?: string): Promise<AdWatch> {
    await this.adWatchRepository.update(adWatchId, {
      status: AdStatus.FAILED,
      adData: { error: errorMessage },
    });

    return this.adWatchRepository.findOne({ where: { id: adWatchId } });
  }

  // 计算广告奖励积分
  private calculateAdReward(adType: AdType, watchDuration: number): number {
    // 不同类型广告的基础奖励
    const baseRewards = {
      [AdType.REWARDED]: 15,      // 激励视频广告奖励最高
      [AdType.INTERSTITIAL]: 8,  // 插屏广告中等奖励
      [AdType.BANNER]: 3,         // 横幅广告奖励较低
      [AdType.NATIVE]: 5,         // 原生广告奖励较低
    };

    const baseReward = baseRewards[adType] || 5;

    // 根据观看时长调整奖励
    if (adType === AdType.REWARDED) {
      // 激励视频需要观看完整才有奖励
      const minWatchTime = 15; // 最少观看15秒
      if (watchDuration < minWatchTime) {
        return 0;
      }
    }

    return baseReward;
  }

  // 获取用户广告观看历史
  async getUserAdHistory(
    userId: string,
    platform: PlatformType,
    page: number = 1,
    limit: number = 20
  ) {
    const [records, total] = await this.adWatchRepository.findAndCount({
      where: { userId, platform },
      order: { createdAt: 'DESC' },
      skip: (page - 1) * limit,
      take: limit,
    });

    return {
      records,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  // 获取今日广告观看统计
  async getTodayAdStats(userId: string, platform: PlatformType) {
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const tomorrow = new Date(today);
    tomorrow.setDate(tomorrow.getDate() + 1);

    const stats = await this.adWatchRepository
      .createQueryBuilder('ad_watch')
      .select('ad_watch.adType', 'adType')
      .addSelect('ad_watch.status', 'status')
      .addSelect('COUNT(*)', 'count')
      .addSelect('SUM(ad_watch.rewardCredits)', 'totalRewards')
      .where('ad_watch.userId = :userId', { userId })
      .andWhere('ad_watch.platform = :platform', { platform })
      .andWhere('ad_watch.createdAt >= :today', { today })
      .andWhere('ad_watch.createdAt < :tomorrow', { tomorrow })
      .groupBy('ad_watch.adType, ad_watch.status')
      .getRawMany();

    return stats;
  }

  // 检查广告观看限制
  async checkAdWatchLimit(userId: string, platform: PlatformType, adType: AdType): Promise<boolean> {
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const tomorrow = new Date(today);
    tomorrow.setDate(tomorrow.getDate() + 1);

    // 每日观看限制
    const dailyLimits = {
      [AdType.REWARDED]: 20,      // 激励视频每日最多20次
      [AdType.INTERSTITIAL]: 10, // 插屏广告每日最多10次
      [AdType.BANNER]: 50,        // 横幅广告每日最多50次
      [AdType.NATIVE]: 30,        // 原生广告每日最多30次
    };

    const todayCount = await this.adWatchRepository.count({
      where: {
        userId,
        platform,
        adType,
        status: AdStatus.COMPLETED,
        createdAt: {
          $gte: today,
          $lt: tomorrow,
        } as any,
      },
    });

    return todayCount < (dailyLimits[adType] || 10);
  }
}

3. API接口实现

3.1 积分相关接口

// src/controllers/credit.controller.ts
@ApiTags('💰 积分系统')
@Controller('credits')
export class CreditController {
  constructor(private readonly creditService: CreditService) {}

  @Get('balance')
  @ApiOperation({ summary: '获取用户积分余额' })
  async getBalance(@CurrentUser() user: any) {
    const balance = await this.creditService.getUserBalance(user.id, user.platform);
    return { balance };
  }

  @Get('history')
  @ApiOperation({ summary: '获取积分历史记录' })
  async getHistory(
    @CurrentUser() user: any,
    @Query('page') page: number = 1,
    @Query('limit') limit: number = 20
  ) {
    return this.creditService.getCreditHistory(user.id, user.platform, page, limit);
  }

  @Post('daily-checkin')
  @ApiOperation({ summary: '每日签到' })
  async dailyCheckIn(@CurrentUser() user: any) {
    // 这里需要实现签到逻辑,计算连续签到天数
    const consecutiveDays = 1; // 简化示例
    return this.creditService.grantDailyCheckIn(user.id, user.platform, consecutiveDays);
  }
}

3.2 广告相关接口

// src/controllers/ad.controller.ts
@ApiTags('📺 广告系统')
@Controller('ads')
export class AdController {
  constructor(private readonly adService: AdService) {}

  @Post('watch/start')
  @ApiOperation({ summary: '开始观看广告' })
  async startWatch(@CurrentUser() user: any, @Body() startAdDto: StartAdWatchDto) {
    return this.adService.startAdWatch(
      user.id,
      user.platform,
      startAdDto.adType,
      startAdDto.adId,
      startAdDto.adUnitId
    );
  }

  @Post('watch/:adWatchId/complete')
  @ApiOperation({ summary: '完成观看广告' })
  async completeWatch(
    @Param('adWatchId') adWatchId: string,
    @Body() completeAdDto: CompleteAdWatchDto
  ) {
    return this.adService.completeAdWatch(
      adWatchId,
      completeAdDto.watchDuration,
      completeAdDto.adData
    );
  }

  @Get('stats/today')
  @ApiOperation({ summary: '获取今日广告观看统计' })
  async getTodayStats(@CurrentUser() user: any) {
    return this.adService.getTodayAdStats(user.id, user.platform);
  }
}

4. 环境配置

# 积分系统配置
NEW_USER_BONUS_CREDITS=100
DAILY_CHECKIN_BASE_CREDITS=5
MAX_DAILY_CHECKIN_CREDITS=20

# 广告奖励配置
REWARDED_AD_CREDITS=15
INTERSTITIAL_AD_CREDITS=8
BANNER_AD_CREDITS=3
NATIVE_AD_CREDITS=5

# 广告观看限制
DAILY_REWARDED_AD_LIMIT=20
DAILY_INTERSTITIAL_AD_LIMIT=10
DAILY_BANNER_AD_LIMIT=50
DAILY_NATIVE_AD_LIMIT=30

# AI生成积分消耗
IMAGE_GENERATION_CREDITS=10
VIDEO_GENERATION_CREDITS=50
HIGH_QUALITY_MULTIPLIER=1.5

这个积分和广告系统提供了完整的积分管理、广告观看奖励、每日限制等功能,支持多种广告类型和灵活的奖励机制。