feat: Admin定价管理界面 + 定价DB配置化 (P2)

This commit is contained in:
yuzhiran
2026-06-12 09:52:04 +08:00
parent a55cb56be2
commit d379d181e4
10 changed files with 361 additions and 104 deletions
+37 -55
View File
@@ -6,46 +6,11 @@ import { PaymentOrder, PaymentOrderDocument } from '../payment/payment-order.sch
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'
import { CurrentUser } from '../../common/decorators/current-user.decorator'
import { QuotaService } from '../user/quota.service'
import { PricingService } from '../schemas/pricing.service'
import { Public } from '../../common/decorators/public.decorator'
const GROWTH_PRICE = 1990
const SPRINT_PRICE = 4990
const DURATION_DAYS = 30
const FREE_DAILY_LIMIT = 2
interface PlanConfig {
id: string
name: string
price: number
dailyLimit: number
features: string[]
}
const PLANS: Record<string, PlanConfig> = {
free: {
id: 'free', name: '免费版', price: 0, dailyLimit: FREE_DAILY_LIMIT,
features: [
'AI 模拟面试 1 次(体验)', '基础面试报告', '通用题库随机出题', '简历优化(限 3 次免费)',
],
},
growth: {
id: 'growth', name: '成长版', price: GROWTH_PRICE, dailyLimit: 999,
features: [
'免费版全部权益', 'AI 数字人面试无限次', '详细面试报告(四维评分)',
'进步轨迹雷达图 + 打卡', '每日一题推送', '参考回答思路', '公司真题库',
'简历优化 20 次/月', '简历下载 10 次/月',
],
},
sprint: {
id: 'sprint', name: '冲刺版', price: SPRINT_PRICE, dailyLimit: 999,
features: [
'成长版全部权益', 'AI 语音分析(语气词/语速检测)', '技能缺口分析报告',
'学习路径推荐', '真人导师 1v1 点评(每月 1 次)', '简历精修(每月 1 次)', '内推优先',
'简历优化 50 次/月', '简历下载 30 次/月',
],
},
}
@Controller('member')
@UseGuards(JwtAuthGuard)
export class MemberController {
@@ -55,21 +20,41 @@ export class MemberController {
@InjectModel(User.name) private userModel: Model<UserDocument>,
@InjectModel(PaymentOrder.name) private orderModel: Model<PaymentOrderDocument>,
private quotaService: QuotaService,
private pricingService: PricingService,
) {}
@Public()
@Get('plans')
getPlans() {
async getPlans() {
const pricing = await this.pricingService.getConfig()
const plans = [
{
id: 'free', name: '免费版', price: 0,
priceDisplay: '免费',
dailyLimit: FREE_DAILY_LIMIT,
features: ['AI 模拟面试 1 次(体验)', '基础面试报告', '通用题库随机出题', '简历优化(限 3 次免费)'],
credits: { interview: 1, resumeOptimize: 0, resumeDownload: 0 },
},
]
for (const planId of ['growth', 'sprint'] as const) {
const cfg = pricing.plans[planId]
if (cfg) {
plans.push({
id: planId,
name: planId === 'growth' ? '成长版' : '冲刺版',
price: cfg.price,
priceDisplay: `¥${(cfg.price / 100).toFixed(1)}/月`,
dailyLimit: 999,
features: cfg.features,
credits: cfg.credits,
})
}
}
return {
interview: { dailyFreeLimit: FREE_DAILY_LIMIT, maxRoundsFree: 5, maxRoundsVip: 10 },
diagnosis: { dailyFreeLimit: 2 },
optimize: { dailyFreeLimit: 2 },
price: { monthly: GROWTH_PRICE, sprint: SPRINT_PRICE },
plans: Object.values(PLANS).map(p => ({
id: p.id, name: p.name, price: p.price,
priceDisplay: p.price === 0 ? '免费' : `¥${(p.price / 100).toFixed(1)}/月`,
dailyLimit: p.dailyLimit, features: p.features,
})),
plans,
}
}
@@ -77,12 +62,11 @@ export class MemberController {
async getStatus(@CurrentUser('userId') userId: string) {
const user = await this.userModel.findById(userId).exec()
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
const planConfig = PLANS[user.plan] || PLANS.free
return {
plan: user.plan,
planName: planConfig.name,
planName: user.plan === 'growth' ? '成长版' : user.plan === 'sprint' ? '冲刺版' : '免费版',
remaining: user.remaining,
dailyLimit: planConfig.dailyLimit,
dailyLimit: user.plan !== 'free' ? 999 : FREE_DAILY_LIMIT,
vipExpireAt: user.vipExpireAt,
sprintExpireAt: user.sprintExpireAt,
sprintRemaining: user.sprintRemaining || 0,
@@ -105,22 +89,20 @@ export class MemberController {
if (!order) throw new HttpException('订单不存在', HttpStatus.NOT_FOUND)
if (order.status !== 'success') throw new HttpException('支付未完成', HttpStatus.BAD_REQUEST)
const pricing = await this.pricingService.getConfig()
const planCfg = pricing.plans[order.plan === 'sprint' ? 'sprint' : 'growth']
const expireAt = new Date()
expireAt.setDate(expireAt.getDate() + DURATION_DAYS)
const credits = { interview: 999, resumeOptimize: 20, resumeDownload: 10 }
expireAt.setDate(expireAt.getDate() + planCfg.durationDays)
if (order.plan === 'sprint') {
user.plan = 'sprint'
user.sprintExpireAt = expireAt
user.sprintRemaining = 10
credits.interview = 999
credits.resumeOptimize = 50
credits.resumeDownload = 30
} else {
user.plan = 'growth'
user.vipExpireAt = expireAt
}
await this.quotaService.setPlanQuota(userId, order.plan, credits)
return { success: true, plan: user.plan, planName: PLANS[user.plan]?.name, expireAt }
await this.quotaService.setPlanQuota(userId, order.plan, planCfg.credits)
return { success: true, plan: user.plan, planName: user.plan === 'growth' ? '成长版' : '冲刺版', expireAt }
}
/** 扣减冲刺版权益次数 */