feat: realistic face avatar + voice input + ASR endpoint

This commit is contained in:
yuzhiran
2026-06-12 15:32:04 +08:00
parent 6fe84b6ef8
commit 8191cf4b41
26 changed files with 1934 additions and 228 deletions
+88 -49
View File
@@ -1,4 +1,4 @@
import { Injectable, HttpException, HttpStatus } from '@nestjs/common'
import { Injectable, HttpException, HttpStatus, Logger } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import { Model } from 'mongoose'
import { User, UserDocument } from './user.schema'
@@ -7,6 +7,8 @@ const FREE_OPTIMIZE_LIMIT = 3
@Injectable()
export class QuotaService {
private readonly logger = new Logger(QuotaService.name)
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>,
) {}
@@ -16,12 +18,28 @@ export class QuotaService {
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
if (user.plan !== 'free') return
if ((user.interviewCredits || 0) <= 0) {
throw new HttpException('面试次数已用完,请购买面试次数或开通会员', HttpStatus.FORBIDDEN)
// Backward compat: migrate remaining → interviewCredits
if ((user.interviewCredits ?? 0) <= 0 && (user.remaining ?? 0) > 0) {
await this.userModel.findByIdAndUpdate(userId, {
$set: { interviewCredits: user.remaining, remaining: 0 },
}).exec()
}
user.interviewCredits = (user.interviewCredits || 0) - 1
user.interviewCount = (user.interviewCount || 0) + 1
await user.save()
const result = await this.userModel.findOneAndUpdate(
{ _id: userId, interviewCredits: { $gt: 0 } },
{ $inc: { interviewCredits: -1, interviewCount: 1 } },
{ new: true },
).exec()
if (result) return
// Fallback to share credits
const shareResult = await this.userModel.findOneAndUpdate(
{ _id: userId, shareCredits: { $gt: 0 } },
{ $inc: { shareCredits: -1, interviewCount: 1 } },
).exec()
if (shareResult) return
throw new HttpException('面试次数已用完,请购买面试次数或开通会员', HttpStatus.FORBIDDEN)
}
async checkAndDeductOptimize(userId: string) {
@@ -29,64 +47,85 @@ export class QuotaService {
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
// Backward compat: migrate remaining → freeOptimizeUsed
if ((user.freeOptimizeUsed ?? 0) <= 0 && (user.remaining ?? 0) > 0 && (user.resumeOptimizeCredits ?? 0) <= 0) {
const migrateCount = Math.min(user.remaining, FREE_OPTIMIZE_LIMIT)
await this.userModel.findByIdAndUpdate(userId, {
$set: { freeOptimizeUsed: migrateCount, remaining: Math.max(0, user.remaining - migrateCount) },
}).exec()
this.logger.log(`Migrated remaining=${user.remaining} → freeOptimizeUsed=${migrateCount} for user ${userId}`)
}
// 免费额度
if ((user.freeOptimizeUsed || 0) < FREE_OPTIMIZE_LIMIT) {
user.freeOptimizeUsed = (user.freeOptimizeUsed || 0) + 1
await user.save()
return
}
// Try paid credits first
const paid = await this.userModel.findOneAndUpdate(
{ _id: userId, resumeOptimizeCredits: { $gt: 0 } },
{ $inc: { resumeOptimizeCredits: -1 } },
).exec()
if (paid) return
// Then free limit
const freeResult = await this.userModel.findOneAndUpdate(
{ _id: userId, freeOptimizeUsed: { $lt: FREE_OPTIMIZE_LIMIT } },
{ $inc: { freeOptimizeUsed: 1 } },
).exec()
if (freeResult) return
// Fallback to share credits
const shareResult = await this.userModel.findOneAndUpdate(
{ _id: userId, shareCredits: { $gt: 0 } },
{ $inc: { shareCredits: -1 } },
).exec()
if (shareResult) 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 grantShareCredits(userId: string, amount = 1): Promise<boolean> {
const result = await this.userModel.findByIdAndUpdate(
userId,
{ $inc: { shareCredits: amount } },
).exec()
return !!result
}
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)
async checkAndDeductDownload(userId: string, paidDownload: boolean): Promise<boolean> {
if (paidDownload) return true
if (resume.paidDownload) return
if ((user.resumeDownloadCredits || 0) > 0) {
user.resumeDownloadCredits = (user.resumeDownloadCredits || 0) - 1
await user.save()
}
const result = await this.userModel.findOneAndUpdate(
{ _id: userId, resumeDownloadCredits: { $gt: 0 } },
{ $inc: { resumeDownloadCredits: -1 } },
).exec()
return !!result
}
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 (amount <= 0) throw new HttpException('无效数量', HttpStatus.BAD_REQUEST)
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
const fieldMap: Record<string, string> = {
interview: 'interviewCredits',
optimize: 'resumeOptimizeCredits',
download: 'resumeDownloadCredits',
}
const field = fieldMap[type]
if (!field) throw new HttpException('无效类型', HttpStatus.BAD_REQUEST)
await user.save()
const result = await this.userModel.findByIdAndUpdate(
userId,
{ $inc: { [field]: amount } },
).exec()
if (!result) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
}
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()
async setPlanQuota(userId: string, _plan: string, credits: { interview: number; resumeOptimize: number; resumeDownload: number }) {
const result = await this.userModel.findByIdAndUpdate(userId, {
$set: {
remaining: 999,
interviewCredits: credits.interview,
resumeOptimizeCredits: credits.resumeOptimize,
resumeDownloadCredits: credits.resumeDownload,
freeOptimizeUsed: FREE_OPTIMIZE_LIMIT,
},
}).exec()
if (!result) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
}
}