515 lines
14 KiB
Markdown
515 lines
14 KiB
Markdown
# 积分系统与广告服务配置指南
|
||
|
||
## 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
|
||
```
|
||
|
||
这个积分和广告系统提供了完整的积分管理、广告观看奖励、每日限制等功能,支持多种广告类型和灵活的奖励机制。
|