v4.2 冲刺版+每日推送+支付修复+全量代码评审
## 新增功能 - 冲刺版 ¥49.9/月:完整支付→激活→权益扣减链路 - 每日一题定时推送(@nestjs/schedule,早8点微信订阅消息) - miniprogram-ci 编译上传脚本(scripts/upload-mp.js) ## Bug修复 - 套餐值统一:vip→growth/sprint(interview轮次限制、analyze次数检查) - member/pay 移除开发绕过:改为订单校验后激活 - progress→report 参数名不匹配:id→interviewId - result.vue resume.create() 参数传错(对象→独立参数) - resume.vue analyze请求缺少Authorization header - bank.vue contribution请求缺少Authorization header - member.vue startPay() 缺少try/catch导致网络错误崩溃 - login.vue 调试面板 v-if="true" 生产泄漏 ## 配置 - 微信支付生产证书就位(商户号1113760598) - .env 清理冗余文件(删除.example/.production) - WX_NOTIFY_URL 更新为 zhiyinwx.yzrcloud.cn ## 文档 - PROJECT-STATUS.md v4.1→v4.2,状态全面更新 - DEPLOYMENT.md 新增小程序编译上传章节、清理检查清单
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import { Controller, Post, Get, Body, HttpException, HttpStatus, UseGuards } from '@nestjs/common'
|
||||
import { Controller, Post, Get, Body, HttpException, HttpStatus, UseGuards, Logger } from '@nestjs/common'
|
||||
import { InjectModel } from '@nestjs/mongoose'
|
||||
import { Model } from 'mongoose'
|
||||
import { User, UserDocument } from '../user/user.schema'
|
||||
import { PaymentOrder, PaymentOrderDocument } from '../payment/payment-order.schema'
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator'
|
||||
import { Public } from '../../common/decorators/public.decorator'
|
||||
|
||||
const GROWTH_PRICE = 1990
|
||||
const SPRINT_PRICE = 4990
|
||||
const DURATION_DAYS = 30
|
||||
const FREE_DAILY_LIMIT = 2
|
||||
|
||||
@@ -20,30 +22,23 @@ interface PlanConfig {
|
||||
|
||||
const PLANS: Record<string, PlanConfig> = {
|
||||
free: {
|
||||
id: 'free',
|
||||
name: '免费版',
|
||||
price: 0,
|
||||
dailyLimit: FREE_DAILY_LIMIT,
|
||||
id: 'free', name: '免费版', price: 0, dailyLimit: FREE_DAILY_LIMIT,
|
||||
features: [
|
||||
'每日 2 次 AI 模拟面试',
|
||||
'基础面试报告',
|
||||
'通用题库随机出题',
|
||||
'简历诊断(限 3 次)',
|
||||
'每日 2 次 AI 模拟面试', '基础面试报告', '通用题库随机出题', '简历诊断(限 3 次)',
|
||||
],
|
||||
},
|
||||
growth: {
|
||||
id: 'growth',
|
||||
name: '成长版',
|
||||
price: GROWTH_PRICE,
|
||||
dailyLimit: 999,
|
||||
id: 'growth', name: '成长版', price: GROWTH_PRICE, dailyLimit: 999,
|
||||
features: [
|
||||
'免费版全部权益',
|
||||
'无限面试次数',
|
||||
'详细面试报告(四维评分)',
|
||||
'进步轨迹雷达图 + 打卡',
|
||||
'每日一题推送',
|
||||
'参考回答思路',
|
||||
'公司真题库',
|
||||
'免费版全部权益', '无限面试次数', '详细面试报告(四维评分)',
|
||||
'进步轨迹雷达图 + 打卡', '每日一题推送', '参考回答思路', '公司真题库',
|
||||
],
|
||||
},
|
||||
sprint: {
|
||||
id: 'sprint', name: '冲刺版', price: SPRINT_PRICE, dailyLimit: 999,
|
||||
features: [
|
||||
'成长版全部权益', 'AI 语音分析(语气词/语速检测)', '技能缺口分析报告',
|
||||
'学习路径推荐', '真人导师 1v1 点评(每月 1 次)', '简历精修(每月 1 次)', '内推优先',
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -51,30 +46,25 @@ const PLANS: Record<string, PlanConfig> = {
|
||||
@Controller('member')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class MemberController {
|
||||
private readonly logger = new Logger(MemberController.name)
|
||||
|
||||
constructor(
|
||||
@InjectModel(User.name) private userModel: Model<UserDocument>,
|
||||
@InjectModel(PaymentOrder.name) private orderModel: Model<PaymentOrderDocument>,
|
||||
) {}
|
||||
|
||||
// 公开的套餐配置(给前端会员页和限制拦截用)
|
||||
@Public()
|
||||
@Get('plans')
|
||||
getPlans() {
|
||||
return {
|
||||
interview: {
|
||||
dailyFreeLimit: FREE_DAILY_LIMIT,
|
||||
maxRoundsFree: 5,
|
||||
maxRoundsVip: 10,
|
||||
},
|
||||
interview: { dailyFreeLimit: FREE_DAILY_LIMIT, maxRoundsFree: 5, maxRoundsVip: 10 },
|
||||
diagnosis: { dailyFreeLimit: 2 },
|
||||
optimize: { dailyFreeLimit: 2 },
|
||||
price: { monthly: GROWTH_PRICE },
|
||||
price: { monthly: GROWTH_PRICE, sprint: SPRINT_PRICE },
|
||||
plans: Object.values(PLANS).map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
price: p.price,
|
||||
id: p.id, name: p.name, price: p.price,
|
||||
priceDisplay: p.price === 0 ? '免费' : `¥${(p.price / 100).toFixed(1)}/月`,
|
||||
dailyLimit: p.dailyLimit,
|
||||
features: p.features,
|
||||
dailyLimit: p.dailyLimit, features: p.features,
|
||||
})),
|
||||
}
|
||||
}
|
||||
@@ -90,41 +80,48 @@ export class MemberController {
|
||||
remaining: user.remaining,
|
||||
dailyLimit: planConfig.dailyLimit,
|
||||
vipExpireAt: user.vipExpireAt,
|
||||
sprintExpireAt: user.sprintExpireAt,
|
||||
sprintRemaining: user.sprintRemaining || 0,
|
||||
isVip: user.plan !== 'free',
|
||||
}
|
||||
}
|
||||
|
||||
@Post('create-order')
|
||||
async createOrder(@CurrentUser('userId') userId: string) {
|
||||
const user = await this.userModel.findById(userId).exec()
|
||||
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
|
||||
const orderId = `ZHI${Date.now()}${userId.slice(-4)}`
|
||||
return {
|
||||
orderId,
|
||||
planId: 'growth',
|
||||
planName: '成长版',
|
||||
amount: GROWTH_PRICE,
|
||||
amountDisplay: `¥${(GROWTH_PRICE / 100).toFixed(1)}`,
|
||||
duration: `${DURATION_DAYS} 天`,
|
||||
}
|
||||
}
|
||||
|
||||
/** 凭订单激活套餐(前端 JSAPI 支付成功后兜底调用) */
|
||||
@Post('pay')
|
||||
async pay(@CurrentUser('userId') userId: string, @Body('orderId') orderId: string) {
|
||||
async pay(@CurrentUser('userId') userId: string, @Body('outTradeNo') outTradeNo: string) {
|
||||
const user = await this.userModel.findById(userId).exec()
|
||||
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
|
||||
if (user.plan !== 'free') return { success: true, plan: user.plan, message: '已是会员' }
|
||||
|
||||
const order = await this.orderModel.findOne({ outTradeNo, userId }).exec()
|
||||
if (!order) throw new HttpException('订单不存在', HttpStatus.NOT_FOUND)
|
||||
if (order.status !== 'success') throw new HttpException('支付未完成', HttpStatus.BAD_REQUEST)
|
||||
|
||||
const expireAt = new Date()
|
||||
expireAt.setDate(expireAt.getDate() + DURATION_DAYS)
|
||||
user.plan = 'growth'
|
||||
user.vipExpireAt = expireAt
|
||||
if (order.plan === 'sprint') {
|
||||
user.plan = 'sprint'
|
||||
user.sprintExpireAt = expireAt
|
||||
user.sprintRemaining = 10
|
||||
} else {
|
||||
user.plan = 'growth'
|
||||
user.vipExpireAt = expireAt
|
||||
}
|
||||
user.remaining = 999
|
||||
await user.save()
|
||||
return {
|
||||
success: true,
|
||||
plan: 'growth',
|
||||
planName: '成长版',
|
||||
expireAt,
|
||||
message: '支付成功!欢迎开通成长版',
|
||||
}
|
||||
return { success: true, plan: user.plan, planName: PLANS[user.plan]?.name, expireAt }
|
||||
}
|
||||
|
||||
/** 扣减冲刺版权益次数 */
|
||||
@Post('sprint/deduct')
|
||||
async deductSprint(@CurrentUser('userId') userId: string) {
|
||||
const user = await this.userModel.findById(userId).exec()
|
||||
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
|
||||
if (user.plan !== 'sprint') throw new HttpException('非冲刺版会员', HttpStatus.FORBIDDEN)
|
||||
if (user.sprintExpireAt && user.sprintExpireAt < new Date()) throw new HttpException('会员已过期', HttpStatus.FORBIDDEN)
|
||||
if ((user.sprintRemaining || 0) <= 0) throw new HttpException('剩余次数不足', HttpStatus.FORBIDDEN)
|
||||
user.sprintRemaining = (user.sprintRemaining || 0) - 1
|
||||
await user.save()
|
||||
return { success: true, sprintRemaining: user.sprintRemaining }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user