feat: 付费体系重构 P0 - 配额独立化/简历付费下载/PDF生成
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { Injectable, HttpException, HttpStatus } from '@nestjs/common'
|
||||
import { InjectModel } from '@nestjs/mongoose'
|
||||
import { Model } from 'mongoose'
|
||||
import { User, UserDocument } from './user.schema'
|
||||
|
||||
const FREE_OPTIMIZE_LIMIT = 3
|
||||
|
||||
@Injectable()
|
||||
export class QuotaService {
|
||||
constructor(
|
||||
@InjectModel(User.name) private userModel: Model<UserDocument>,
|
||||
) {}
|
||||
|
||||
async checkAndDeductInterview(userId: string) {
|
||||
const user = await this.userModel.findById(userId).exec()
|
||||
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
|
||||
if (user.plan !== 'free') return
|
||||
|
||||
if ((user.interviewCredits || 0) <= 0) {
|
||||
throw new HttpException('面试次数已用完,请购买面试次数或开通会员', HttpStatus.FORBIDDEN)
|
||||
}
|
||||
user.interviewCredits = (user.interviewCredits || 0) - 1
|
||||
user.interviewCount = (user.interviewCount || 0) + 1
|
||||
await user.save()
|
||||
}
|
||||
|
||||
async checkAndDeductOptimize(userId: string) {
|
||||
const user = await this.userModel.findById(userId).exec()
|
||||
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
|
||||
if (user.plan !== 'free') return
|
||||
|
||||
// 优先扣付费额度
|
||||
if ((user.resumeOptimizeCredits || 0) > 0) {
|
||||
user.resumeOptimizeCredits = (user.resumeOptimizeCredits || 0) - 1
|
||||
await user.save()
|
||||
return
|
||||
}
|
||||
|
||||
// 免费额度
|
||||
if ((user.freeOptimizeUsed || 0) < FREE_OPTIMIZE_LIMIT) {
|
||||
user.freeOptimizeUsed = (user.freeOptimizeUsed || 0) + 1
|
||||
await user.save()
|
||||
return
|
||||
}
|
||||
|
||||
throw new HttpException('简历优化次数已用完,请购买优化次数或开通会员', HttpStatus.FORBIDDEN)
|
||||
}
|
||||
|
||||
async checkDownload(userId: string, resume: { paidDownload?: boolean }): Promise<boolean> {
|
||||
const user = await this.userModel.findById(userId).exec()
|
||||
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
|
||||
|
||||
if (resume.paidDownload) return true
|
||||
if ((user.resumeDownloadCredits || 0) > 0) return true
|
||||
return false
|
||||
}
|
||||
|
||||
async deductDownload(userId: string, resume: { paidDownload?: boolean; _id?: any }) {
|
||||
const user = await this.userModel.findById(userId).exec()
|
||||
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
|
||||
|
||||
if (resume.paidDownload) return
|
||||
if ((user.resumeDownloadCredits || 0) > 0) {
|
||||
user.resumeDownloadCredits = (user.resumeDownloadCredits || 0) - 1
|
||||
await user.save()
|
||||
}
|
||||
}
|
||||
|
||||
async grantCredits(userId: string, type: 'interview' | 'optimize' | 'download', amount: number) {
|
||||
const user = await this.userModel.findById(userId).exec()
|
||||
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
|
||||
|
||||
if (type === 'interview') user.interviewCredits = (user.interviewCredits || 0) + amount
|
||||
else if (type === 'optimize') user.resumeOptimizeCredits = (user.resumeOptimizeCredits || 0) + amount
|
||||
else if (type === 'download') user.resumeDownloadCredits = (user.resumeDownloadCredits || 0) + amount
|
||||
|
||||
await user.save()
|
||||
}
|
||||
|
||||
async setPlanQuota(userId: string, plan: string, credits: { interview: number; resumeOptimize: number; resumeDownload: number }) {
|
||||
const user = await this.userModel.findById(userId).exec()
|
||||
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
|
||||
|
||||
user.remaining = 999
|
||||
user.interviewCredits = credits.interview
|
||||
user.resumeOptimizeCredits = credits.resumeOptimize
|
||||
user.resumeDownloadCredits = credits.resumeDownload
|
||||
user.freeOptimizeUsed = FREE_OPTIMIZE_LIMIT // 会员不再消耗免费次数
|
||||
|
||||
await user.save()
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { MongooseModule } from '@nestjs/mongoose'
|
||||
import { JwtModule } from '@nestjs/jwt'
|
||||
import { UserController } from './user.controller'
|
||||
import { UserService } from './user.service'
|
||||
import { QuotaService } from './quota.service'
|
||||
import { User, UserSchema } from './user.schema'
|
||||
|
||||
@Module({
|
||||
@@ -14,7 +15,7 @@ import { User, UserSchema } from './user.schema'
|
||||
}),
|
||||
],
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
exports: [UserService],
|
||||
providers: [UserService, QuotaService],
|
||||
exports: [UserService, QuotaService],
|
||||
})
|
||||
export class UserModule {}
|
||||
|
||||
@@ -35,6 +35,19 @@ export class User {
|
||||
@Prop({ default: 0 })
|
||||
sprintRemaining: number // 冲刺版剩余次数(语音分析等)
|
||||
|
||||
// --- 新版独立额度(产品改造 v2) ---
|
||||
@Prop({ default: 1 })
|
||||
interviewCredits: number // AI 面试可用次数(含首次免费)
|
||||
|
||||
@Prop({ default: 0 })
|
||||
resumeOptimizeCredits: number // 简历优化可用次数
|
||||
|
||||
@Prop({ default: 0 })
|
||||
resumeDownloadCredits: number // 简历下载可用次数
|
||||
|
||||
@Prop({ default: 0 })
|
||||
freeOptimizeUsed: number // 已使用免费优化次数(上限 3)
|
||||
|
||||
@Prop({ default: 'user' })
|
||||
role: string // 'user' | 'admin'
|
||||
|
||||
|
||||
@@ -211,6 +211,10 @@ export class UserService {
|
||||
isSystemAdmin: user.isSystemAdmin || false,
|
||||
remaining: user.remaining,
|
||||
interviewCount: user.interviewCount,
|
||||
interviewCredits: user.interviewCredits ?? 1,
|
||||
resumeOptimizeCredits: user.resumeOptimizeCredits ?? 0,
|
||||
resumeDownloadCredits: user.resumeDownloadCredits ?? 0,
|
||||
freeOptimizeUsed: user.freeOptimizeUsed ?? 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user