2fddd39301
- Add registrationGravity to pricing config (default 50, adjustable via admin) - Inject PricingService into UserService, read config for all 4 registration paths - Expose registrationGravity in /member/plans response for frontend - Login page fetches dynamic value from API, replaces hardcoded '50' - Update user.vue gravity card copy
80 lines
3.8 KiB
TypeScript
80 lines
3.8 KiB
TypeScript
import { Injectable } from '@nestjs/common'
|
|
import { InjectModel } from '@nestjs/mongoose'
|
|
import { Model } from 'mongoose'
|
|
import { SiteConfig, SiteConfigDocument } from './site-config.schema'
|
|
|
|
export interface GravityRates {
|
|
interviewPerUse: number // 每次面试消耗引力值
|
|
optimizePerUse: number // 每次优化消耗引力值
|
|
downloadPerUse: number // 每次下载消耗引力值
|
|
}
|
|
|
|
interface PricingConfig {
|
|
/** 新用户注册赠送引力值 */
|
|
registrationGravity: number
|
|
interview: { pricePerSession: number; creditsPerPurchase: number }
|
|
resumeOptimize: { freeLimit: number; pricePerOptimize: number; creditsPerPurchase: number }
|
|
resumeDownload: { pricePerDownload: number; creditsPerPurchase: number }
|
|
gravityRates: GravityRates
|
|
plans: {
|
|
growth: { price: number; durationDays: number; gravityPerMonth: number; credits: { interview: number; resumeOptimize: number; resumeDownload: number }; features: string[] }
|
|
sprint: { price: number; durationDays: number; gravityPerMonth: number; credits: { interview: number; resumeOptimize: number; resumeDownload: number }; features: string[] }
|
|
}
|
|
}
|
|
|
|
const DEFAULT_PRICING: PricingConfig = {
|
|
registrationGravity: 50,
|
|
interview: { pricePerSession: 600, creditsPerPurchase: 1 },
|
|
resumeOptimize: { freeLimit: 3, pricePerOptimize: 400, creditsPerPurchase: 1 },
|
|
resumeDownload: { pricePerDownload: 200, creditsPerPurchase: 1 },
|
|
gravityRates: { interviewPerUse: 5, optimizePerUse: 3, downloadPerUse: 2 },
|
|
plans: {
|
|
growth: { price: 1900, durationDays: 30, gravityPerMonth: 80, credits: { interview: 999, resumeOptimize: 20, resumeDownload: 10 }, features: ['免费版全部权益', 'AI 模拟面试(每次消耗 5 引力值,无限次)', '详细面试报告(四维评分 + 语音复盘)', '进步轨迹雷达图 + 打卡日历', '每日一题推送 + 参考思路', '公司真题库', '每月赠送 80 引力值,可用于面试/优化/下载'] },
|
|
sprint: { price: 4900, durationDays: 30, gravityPerMonth: 200, credits: { interview: 999, resumeOptimize: 50, resumeDownload: 30 }, features: ['成长版全部权益', 'AI 语音深度分析(语气词/语速/停顿检测)', '技能缺口分析报告', '公司真题库精选', '每月赠送 200 引力值,可用于面试/优化/下载'] },
|
|
},
|
|
}
|
|
|
|
@Injectable()
|
|
export class PricingService {
|
|
private cache: PricingConfig | null = null
|
|
private cacheTime = 0
|
|
|
|
constructor(
|
|
@InjectModel(SiteConfig.name) private configModel: Model<SiteConfigDocument>,
|
|
) {}
|
|
|
|
async getConfig(): Promise<PricingConfig> {
|
|
// Cache for 60s
|
|
if (this.cache && Date.now() - this.cacheTime < 60000) {
|
|
return this.cache
|
|
}
|
|
try {
|
|
const doc = await this.configModel.findOne({ key: 'pricing' }).exec()
|
|
if (doc?.value) {
|
|
this.cache = this.mergeDefaults(doc.value)
|
|
this.cacheTime = Date.now()
|
|
return this.cache
|
|
}
|
|
} catch {}
|
|
return DEFAULT_PRICING
|
|
}
|
|
|
|
invalidateCache() {
|
|
this.cache = null
|
|
}
|
|
|
|
private mergeDefaults(value: any): PricingConfig {
|
|
return {
|
|
registrationGravity: value?.registrationGravity ?? DEFAULT_PRICING.registrationGravity,
|
|
interview: { ...DEFAULT_PRICING.interview, ...value?.interview },
|
|
resumeOptimize: { ...DEFAULT_PRICING.resumeOptimize, ...value?.resumeOptimize },
|
|
resumeDownload: { ...DEFAULT_PRICING.resumeDownload, ...value?.resumeDownload },
|
|
gravityRates: { ...DEFAULT_PRICING.gravityRates, ...value?.gravityRates },
|
|
plans: {
|
|
growth: { ...DEFAULT_PRICING.plans.growth, ...value?.plans?.growth, credits: { ...DEFAULT_PRICING.plans.growth.credits, ...value?.plans?.growth?.credits } },
|
|
sprint: { ...DEFAULT_PRICING.plans.sprint, ...value?.plans?.sprint, credits: { ...DEFAULT_PRICING.plans.sprint.credits, ...value?.plans?.sprint?.credits } },
|
|
},
|
|
}
|
|
}
|
|
}
|