Files
zhiyin/backend/src/modules/user/quota.service.ts
T

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()
}
}