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:
yuzhiran
2026-06-09 20:03:05 +08:00
parent 37cfdfe93c
commit 9276ab9028
44 changed files with 15205 additions and 2062 deletions
+21 -16
View File
@@ -1,7 +1,6 @@
import { Controller, Get, Post, Body, Query, HttpException, HttpStatus, UseGuards } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import { Model } from 'mongoose'
import { Public } from '../../common/decorators/public.decorator'
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'
import { CurrentUser } from '../../common/decorators/current-user.decorator'
import { User, UserDocument } from '../user/user.schema'
@@ -29,19 +28,21 @@ export class AdminController {
return { isAdmin: user?.role === 'admin' }
}
@Public()
@UseGuards(JwtAuthGuard)
@Post('verify')
async verify(@Body('adminId') adminId: string) {
const user = await this.userModel.findById(adminId).exec()
async verify(@CurrentUser('userId') userId: string) {
const user = await this.userModel.findById(userId).exec()
if (!user || user.role !== 'admin') {
throw new HttpException('无权限访问', HttpStatus.FORBIDDEN)
}
return { ok: true, nickname: user.nickname || '管理员' }
}
@Public()
@UseGuards(JwtAuthGuard)
@Get('overview')
async overview() {
async overview(@CurrentUser('userId') adminUserId: string) {
const admin = await this.userModel.findById(adminUserId).exec()
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
const [userCount, interviewCount, todayUsers, todayInterviews] = await Promise.all([
this.userModel.countDocuments().exec(),
this.interviewModel.countDocuments().exec(),
@@ -51,9 +52,11 @@ export class AdminController {
return { userCount, interviewCount, todayUsers, todayInterviews }
}
@Public()
@UseGuards(JwtAuthGuard)
@Get('users')
async getUsers(@Query('keyword') keyword: string, @Query('page') page = '1', @Query('limit') limit = '20') {
async getUsers(@Query('keyword') keyword: string, @Query('page') page = '1', @Query('limit') limit = '20', @CurrentUser('userId') adminUserId: string) {
const admin = await this.userModel.findById(adminUserId).exec()
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
const filter: any = {}
if (keyword) filter.$or = [
{ phone: { $regex: keyword, $options: 'i' } },
@@ -67,9 +70,11 @@ export class AdminController {
return { users, total, page: +page }
}
@Public()
@UseGuards(JwtAuthGuard)
@Get('interviews')
async getInterviews(@Query('page') page = '1', @Query('limit') limit = '20') {
async getInterviews(@Query('page') page = '1', @Query('limit') limit = '20', @CurrentUser('userId') adminUserId: string) {
const admin = await this.userModel.findById(adminUserId).exec()
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
const skip = (Math.max(1, +page) - 1) * +limit
const [interviews, total] = await Promise.all([
this.interviewModel.find().sort({ createdAt: -1 }).skip(skip).limit(+limit).populate('userId', 'phone nickname').lean().exec(),
@@ -86,12 +91,12 @@ export class AdminController {
const user = await this.userModel.findById(targetUserId).exec()
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
const expireAt = new Date()
expireAt.setDate(expireAt.getDate() + 30)
user.plan = 'vip'
expireAt.setDate(expireAt.getDate() + VIP_DURATION_DAYS)
user.plan = 'growth'
user.vipExpireAt = expireAt
user.remaining = 999
await user.save()
return { success: true, plan: 'vip', expireAt }
return { success: true, plan: 'growth', expireAt }
}
@UseGuards(JwtAuthGuard)
@@ -144,14 +149,14 @@ export class AdminController {
if (!order) throw new HttpException('订单不存在', HttpStatus.NOT_FOUND)
if (tradeState === 'SUCCESS' && order.status === 'pending') {
order.status = 'success'
order.wxTransactionId = wxResult.transaction_id
order.wxTransactionId = wxResult?.transaction_id || ''
order.paidAt = new Date()
await order.save()
const user = await this.userModel.findById(order.userId).exec()
if (user && user.plan !== 'vip') {
if (user && user.plan === 'free') {
const expireAt = new Date()
expireAt.setDate(expireAt.getDate() + VIP_DURATION_DAYS)
user.plan = 'vip'
user.plan = 'growth'
user.vipExpireAt = expireAt
user.remaining = 999
await user.save()