import { Injectable, HttpException, HttpStatus, Logger } from '@nestjs/common' import { InjectModel } from '@nestjs/mongoose' import { Model } from 'mongoose' import { User, UserDocument } from './user.schema' import { PricingService } from '../schemas/pricing.service' const FREE_OPTIMIZE_LIMIT = 3 @Injectable() export class QuotaService { private readonly logger = new Logger(QuotaService.name) constructor( @InjectModel(User.name) private userModel: Model, private pricingService: PricingService, ) {} /** 检查并扣除面试引力值(所有计划统一走引力值) */ async checkAndDeductInterview(userId: string) { const user = await this.userModel.findById(userId).exec() if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND) // 迁移旧字段到 gravity if ((user.gravity ?? 0) <= 0 && this.hasOldCredits(user)) { await this.migrateOldCredits(userId) } const rates = (await this.pricingService.getConfig()).gravityRates const cost = rates.interviewPerUse // 用 gravity 支付,后备 shareCredits const result = await this.deductGravityOrFallback(userId, cost) if (result) return throw new HttpException('引力值不足,请充值或分享获取', HttpStatus.FORBIDDEN) } /** 检查并扣除优化引力值(所有计划统一走引力值) */ async checkAndDeductOptimize(userId: string) { const user = await this.userModel.findById(userId).exec() if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND) // 迁移旧字段 if ((user.gravity ?? 0) <= 0 && this.hasOldCredits(user)) { await this.migrateOldCredits(userId) } // 免费优化次数 const freeResult = await this.userModel.findOneAndUpdate( { _id: userId, freeOptimizeUsed: { $lt: FREE_OPTIMIZE_LIMIT } }, { $inc: { freeOptimizeUsed: 1 } }, ).exec() if (freeResult) return const rates = (await this.pricingService.getConfig()).gravityRates const cost = rates.optimizePerUse const result = await this.deductGravityOrFallback(userId, cost) if (result) return throw new HttpException('引力值不足,请充值或分享获取', HttpStatus.FORBIDDEN) } /** 检查并扣除下载引力值 */ async checkAndDeductDownload(userId: string, paidDownload: boolean): Promise { if (paidDownload) return true const rates = (await this.pricingService.getConfig()).gravityRates const cost = rates.downloadPerUse const result = await this.deductGravityOrFallback(userId, cost) if (result) return true return false } /** 从 gravity 扣除,后备从 shareCredits 扣除(兼容旧数据) */ private async deductGravityOrFallback(userId: string, cost: number): Promise { // 主路径:gravity const gravResult = await this.userModel.findOneAndUpdate( { _id: userId, gravity: { $gte: cost } }, { $inc: { gravity: -cost } }, ).exec() if (gravResult) return true // 后备:旧 shareCredits const shareResult = await this.userModel.findOneAndUpdate( { _id: userId, shareCredits: { $gt: 0 } }, { $inc: { shareCredits: -1 } }, ).exec() if (shareResult) return true return false } /** 增加引力值 */ async grantGravity(userId: string, amount: number) { if (amount <= 0) throw new HttpException('无效数量', HttpStatus.BAD_REQUEST) const result = await this.userModel.findByIdAndUpdate( userId, { $inc: { gravity: amount } }, ).exec() if (!result) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND) } /** 设置 VIP 套餐引力值额度 */ async setPlanQuota(userId: string, gravityAmount: number) { const result = await this.userModel.findByIdAndUpdate(userId, { $set: { gravity: gravityAmount, freeOptimizeUsed: FREE_OPTIMIZE_LIMIT, }, }).exec() if (!result) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND) } /** 是否为非会员用户授予初始引力值 */ async grantFreeGravity(userId: string) { await this.userModel.findByIdAndUpdate(userId, { $set: { interviewCredits: 1, gravity: 5 }, }).exec() } /** 判断是否有旧额度需要迁移 */ private hasOldCredits(user: UserDocument): boolean { return (user.interviewCredits ?? 0) > 0 || (user.resumeOptimizeCredits ?? 0) > 0 || (user.resumeDownloadCredits ?? 0) > 0 || (user.remaining ?? 0) > 0 } /** 迁移旧额度到 gravity */ private async migrateOldCredits(userId: string) { const user = await this.userModel.findById(userId).exec() if (!user) return const interviewVal = (user.interviewCredits ?? 0) * 5 const optimizeVal = (user.resumeOptimizeCredits ?? 0) * 3 const downloadVal = (user.resumeDownloadCredits ?? 0) * 2 const oldRemainVal = (user.remaining ?? 0) * 5 const shareVal = (user.shareCredits ?? 0) * 1 const total = interviewVal + optimizeVal + downloadVal + oldRemainVal + shareVal if (total <= 0) return await this.userModel.findByIdAndUpdate(userId, { $inc: { gravity: total }, $set: { interviewCredits: 0, resumeOptimizeCredits: 0, resumeDownloadCredits: 0, remaining: 0, shareCredits: 0, }, }).exec() } }