feat: Admin定价管理界面 + 定价DB配置化 (P2)
This commit is contained in:
@@ -3,6 +3,7 @@ import { getModelToken } from '@nestjs/mongoose'
|
||||
import { PaymentController } from './payment.controller'
|
||||
import { WechatPayService } from './wechat-pay.service'
|
||||
import { QuotaService } from '../user/quota.service'
|
||||
import { PricingService } from '../schemas/pricing.service'
|
||||
|
||||
describe('PaymentController', () => {
|
||||
let controller: PaymentController
|
||||
@@ -10,6 +11,7 @@ describe('PaymentController', () => {
|
||||
let mockOrderModel: any
|
||||
let mockWechatPay: any
|
||||
let mockQuotaService: any
|
||||
let mockPricingService: any
|
||||
|
||||
const mockUserId = '507f1f77bcf86cd799439011'
|
||||
|
||||
@@ -32,6 +34,17 @@ describe('PaymentController', () => {
|
||||
grantCredits: jest.fn().mockResolvedValue(undefined),
|
||||
setPlanQuota: jest.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
mockPricingService = {
|
||||
getConfig: jest.fn().mockResolvedValue({
|
||||
interview: { pricePerSession: 500, creditsPerPurchase: 1 },
|
||||
resumeOptimize: { freeLimit: 3, pricePerOptimize: 300, creditsPerPurchase: 1 },
|
||||
resumeDownload: { pricePerDownload: 200, creditsPerPurchase: 1 },
|
||||
plans: {
|
||||
growth: { price: 1990, durationDays: 30, credits: { interview: 999, resumeOptimize: 20, resumeDownload: 10 }, features: [] },
|
||||
sprint: { price: 4990, durationDays: 30, credits: { interview: 999, resumeOptimize: 50, resumeDownload: 30 }, features: [] },
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [PaymentController],
|
||||
@@ -40,6 +53,7 @@ describe('PaymentController', () => {
|
||||
{ provide: getModelToken('PaymentOrder'), useValue: mockOrderModel },
|
||||
{ provide: WechatPayService, useValue: mockWechatPay },
|
||||
{ provide: QuotaService, useValue: mockQuotaService },
|
||||
{ provide: PricingService, useValue: mockPricingService },
|
||||
],
|
||||
}).compile()
|
||||
|
||||
|
||||
@@ -7,24 +7,9 @@ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator'
|
||||
import { WechatPayService } from './wechat-pay.service'
|
||||
import { QuotaService } from '../user/quota.service'
|
||||
import { PricingService } from '../schemas/pricing.service'
|
||||
import { Public } from '../../common/decorators/public.decorator'
|
||||
|
||||
const GROWTH_AMOUNT = 1990
|
||||
const SPRINT_AMOUNT = 4990
|
||||
const VIP_DURATION_DAYS = 30
|
||||
|
||||
const PRODUCT_PRICES: Record<string, number> = {
|
||||
interview: 500,
|
||||
optimize: 300,
|
||||
download: 200,
|
||||
}
|
||||
|
||||
const PRODUCT_CREDITS: Record<string, number> = {
|
||||
interview: 1,
|
||||
optimize: 1,
|
||||
download: 1,
|
||||
}
|
||||
|
||||
@Controller('payment')
|
||||
export class PaymentController {
|
||||
private readonly logger = new Logger(PaymentController.name)
|
||||
@@ -34,6 +19,7 @@ export class PaymentController {
|
||||
@InjectModel(PaymentOrder.name) private orderModel: Model<PaymentOrderDocument>,
|
||||
private wechatPay: WechatPayService,
|
||||
private quotaService: QuotaService,
|
||||
private pricingService: PricingService,
|
||||
) {}
|
||||
|
||||
/** 创建套餐订单(H5:Native 扫码支付) */
|
||||
@@ -45,7 +31,9 @@ export class PaymentController {
|
||||
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
|
||||
if (user.plan !== 'free') throw new HttpException('已是会员', HttpStatus.BAD_REQUEST)
|
||||
|
||||
const amount = plan === 'sprint' ? SPRINT_AMOUNT : GROWTH_AMOUNT
|
||||
const pricing = await this.pricingService.getConfig()
|
||||
const planCfg = pricing.plans[plan === 'sprint' ? 'sprint' : 'growth']
|
||||
const amount = planCfg.price
|
||||
const title = plan === 'sprint' ? '职引冲刺版月度会员' : '职引成长版月度会员'
|
||||
const outTradeNo = `${plan === 'sprint' ? 'SPR' : 'VIP'}${Date.now()}${userId.slice(-6)}`
|
||||
const result = await this.wechatPay.nativePay(title, outTradeNo, amount)
|
||||
@@ -69,7 +57,13 @@ export class PaymentController {
|
||||
const user = await this.userModel.findById(userId).exec()
|
||||
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
|
||||
|
||||
const price = PRODUCT_PRICES[type]
|
||||
const pricing = await this.pricingService.getConfig()
|
||||
const priceMap: Record<string, number> = {
|
||||
interview: pricing.interview.pricePerSession,
|
||||
optimize: pricing.resumeOptimize.pricePerOptimize,
|
||||
download: pricing.resumeDownload.pricePerDownload,
|
||||
}
|
||||
const price = priceMap[type]
|
||||
if (!price) throw new HttpException('价格未配置', HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
|
||||
const titles: Record<string, string> = {
|
||||
@@ -97,7 +91,9 @@ export class PaymentController {
|
||||
const openid = user.wxOpenid
|
||||
if (!openid) throw new HttpException('未绑定微信', HttpStatus.BAD_REQUEST)
|
||||
|
||||
const amount = plan === 'sprint' ? SPRINT_AMOUNT : GROWTH_AMOUNT
|
||||
const pricing = await this.pricingService.getConfig()
|
||||
const planCfg = pricing.plans[plan === 'sprint' ? 'sprint' : 'growth']
|
||||
const amount = planCfg.price
|
||||
const title = plan === 'sprint' ? '职引冲刺版月度会员' : '职引成长版月度会员'
|
||||
const outTradeNo = `${plan === 'sprint' ? 'SPR' : 'VIP'}${Date.now()}${userId.slice(-6)}`
|
||||
const result = await this.wechatPay.jsapiPay(title, outTradeNo, amount, openid)
|
||||
@@ -123,7 +119,13 @@ export class PaymentController {
|
||||
const openid = user.wxOpenid
|
||||
if (!openid) throw new HttpException('未绑定微信', HttpStatus.BAD_REQUEST)
|
||||
|
||||
const price = PRODUCT_PRICES[type]
|
||||
const pricing = await this.pricingService.getConfig()
|
||||
const priceMap: Record<string, number> = {
|
||||
interview: pricing.interview.pricePerSession,
|
||||
optimize: pricing.resumeOptimize.pricePerOptimize,
|
||||
download: pricing.resumeDownload.pricePerDownload,
|
||||
}
|
||||
const price = priceMap[type]
|
||||
if (!price) throw new HttpException('价格未配置', HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
|
||||
const titles: Record<string, string> = {
|
||||
@@ -184,8 +186,10 @@ export class PaymentController {
|
||||
const user = await this.userModel.findById(order.userId).exec()
|
||||
if (!user || user.plan !== 'free') return
|
||||
|
||||
const pricing = await this.pricingService.getConfig()
|
||||
const planCfg = pricing.plans[order.plan === 'sprint' ? 'sprint' : 'growth']
|
||||
const expireAt = new Date()
|
||||
expireAt.setDate(expireAt.getDate() + VIP_DURATION_DAYS)
|
||||
expireAt.setDate(expireAt.getDate() + planCfg.durationDays)
|
||||
const isSprint = order.plan === 'sprint'
|
||||
if (isSprint) {
|
||||
user.plan = 'sprint'
|
||||
@@ -195,9 +199,7 @@ export class PaymentController {
|
||||
user.plan = 'growth'
|
||||
user.vipExpireAt = expireAt
|
||||
}
|
||||
const credits = isSprint
|
||||
? { interview: 999, resumeOptimize: 50, resumeDownload: 30 }
|
||||
: { interview: 999, resumeOptimize: 20, resumeDownload: 10 }
|
||||
const credits = planCfg.credits
|
||||
user.remaining = 999
|
||||
user.interviewCredits = credits.interview
|
||||
user.resumeOptimizeCredits = credits.resumeOptimize
|
||||
@@ -207,7 +209,13 @@ export class PaymentController {
|
||||
}
|
||||
|
||||
private async activateProduct(order: PaymentOrderDocument) {
|
||||
const credits = PRODUCT_CREDITS[order.type]
|
||||
const pricing = await this.pricingService.getConfig()
|
||||
const creditMap: Record<string, number> = {
|
||||
interview: pricing.interview.creditsPerPurchase,
|
||||
optimize: pricing.resumeOptimize.creditsPerPurchase,
|
||||
download: pricing.resumeDownload.creditsPerPurchase,
|
||||
}
|
||||
const credits = creditMap[order.type]
|
||||
if (!credits) return
|
||||
|
||||
const typeMap: Record<string, 'interview' | 'optimize' | 'download'> = {
|
||||
|
||||
Reference in New Issue
Block a user