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

515 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 积分系统与广告服务配置指南
## 1. 积分系统设计
### 1.1 积分获取方式
- **观看广告**: 每次完整观看激励视频广告获得5-20积分
- **订阅赠送**: 包月用户每月自动获得积分
- **新用户奖励**: 注册即送100积分
- **每日签到**: 连续签到获得递增积分奖励
- **分享奖励**: 分享作品获得积分
### 1.2 积分消耗规则
- **图片生成**: 基础10积分高质量15积分
- **视频生成**: 基础50积分高质量75积分
- **高级功能**: 特殊滤镜、风格转换等
## 2. 积分服务实现
### 2.1 积分管理服务
```typescript
// 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 广告服务实现
```typescript
// 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 积分相关接口
```typescript
// 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 广告相关接口
```typescript
// 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. 环境配置
```env
# 积分系统配置
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
```
这个积分和广告系统提供了完整的积分管理、广告观看奖励、每日限制等功能,支持多种广告类型和灵活的奖励机制。