155 lines
5.3 KiB
TypeScript
155 lines
5.3 KiB
TypeScript
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<UserDocument>,
|
|
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<boolean> {
|
|
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<boolean> {
|
|
// 主路径: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()
|
|
}
|
|
}
|