From 9b1c92464e699eb85a38d4da61a3735d9fb4e433 Mon Sep 17 00:00:00 2001 From: yuzhiran Date: Thu, 25 Jun 2026 08:36:57 +0800 Subject: [PATCH] fix: gravity deducted before AI call, causing deduction on failure - Split checkAndDeductInterview into checkInterview (read-only) and deductInterview (deduction only) - Restructure interview.create(): check gravity -> AI call -> deduct on success - If AI call fails, gravity is never deducted - Keep old checkAndDeductInterview for backward compatibility --- .../modules/interview/interview.service.ts | 7 ++++- backend/src/modules/user/quota.service.ts | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/backend/src/modules/interview/interview.service.ts b/backend/src/modules/interview/interview.service.ts index 342d85b..57bc085 100644 --- a/backend/src/modules/interview/interview.service.ts +++ b/backend/src/modules/interview/interview.service.ts @@ -21,14 +21,19 @@ export class InterviewService { ) {} async create(userId: string, position: string) { - await this.quotaService.checkAndDeductInterview(userId) + // Step 1: 先检查引力值是否足够(不扣除) + const cost = await this.quotaService.checkInterview(userId) + // Step 2: AI 生成第一个问题 const firstQuestion = await this.aiService.call({ systemPrompt: `你是一位专业的${position}面试官。请针对校招该岗位提出第一个面试问题,要求具体且有针对性。直接输出问题,不要多余内容。`, userMessage: `请为${position}岗位的校招候选人提出第一个面试问题。`, temperature: 0.8, }) + // Step 3: AI 调用成功后,扣除引力值并创建面试记录 + await this.quotaService.deductInterview(userId, cost) + const interview = await this.interviewModel.create({ userId, position, diff --git a/backend/src/modules/user/quota.service.ts b/backend/src/modules/user/quota.service.ts index 6f1d4ff..592c7a0 100644 --- a/backend/src/modules/user/quota.service.ts +++ b/backend/src/modules/user/quota.service.ts @@ -15,6 +15,36 @@ export class QuotaService { private pricingService: PricingService, ) {} + /** 仅检查面试引力值是否充足,不扣除(用于先 AI 后扣款模式) */ + async checkInterview(userId: string): Promise { + 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 gravOk = (user.gravity ?? 0) >= cost + const shareOk = (user.shareCredits ?? 0) > 0 + if (!gravOk && !shareOk) { + throw new HttpException('引力值不足,请充值或分享获取', HttpStatus.FORBIDDEN) + } + return cost + } + + /** 扣除面试引力值(在 AI 调用成功后调用) */ + async deductInterview(userId: string, cost: number) { + const result = await this.deductGravityOrFallback(userId, cost) + if (!result) { + throw new HttpException('引力值不足', HttpStatus.FORBIDDEN) + } + } + /** 检查并扣除面试引力值(所有计划统一走引力值) */ async checkAndDeductInterview(userId: string) { const user = await this.userModel.findById(userId).exec()