feat: 登录页密码+验证码双模式 / 首页岗位优化 / 法律页面 / 后端接口完善

- 前端:登录页重构,支持密码登录、验证码登录、注册三种模式
- 前端:首页热门岗位添加「参考示例」标签,去虚构数据
- 前端:面试页顶部优化,岗位名+状态标签展示
- 前端:新增用户协议、隐私政策页面及免责声明
- 后端:新增 POST /api/user/register 注册接口
- 后端:新增 POST /api/user/set-password 设置密码接口
- 后端:修复 user.schema.ts unique 索引导致 null 冲突问题
- 后端:新增 payment-order.schema、positions.schema、site-config.schema
- 后端:package.json 新增 postbuild 脚本自动复制证书
- 管理后台:新增订单管理 Tab
This commit is contained in:
yuzhiran
2026-06-09 15:39:17 +08:00
parent 511f60d0db
commit 37cfdfe93c
27 changed files with 1045 additions and 195 deletions
+114 -17
View File
@@ -6,12 +6,20 @@ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'
import { CurrentUser } from '../../common/decorators/current-user.decorator'
import { User, UserDocument } from '../user/user.schema'
import { Interview, InterviewDocument } from '../interview/interview.schema'
import { PaymentOrder, PaymentOrderDocument } from '../payment/payment-order.schema'
import { SiteConfig, SiteConfigDocument } from '../schemas/site-config.schema'
import { WechatPayService } from '../payment/wechat-pay.service'
const VIP_DURATION_DAYS = 30
@Controller('admin')
export class AdminController {
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>,
@InjectModel(Interview.name) private interviewModel: Model<InterviewDocument>,
@InjectModel(PaymentOrder.name) private orderModel: Model<PaymentOrderDocument>,
@InjectModel(SiteConfig.name) private configModel: Model<SiteConfigDocument>,
private wechatPay: WechatPayService,
) {}
@Get('check')
@@ -108,26 +116,115 @@ export class AdminController {
return { success: true, message: '已设为管理员' }
}
// ─── 订单管理 ──────────────
@UseGuards(JwtAuthGuard)
@Get('orders')
async getOrders(@Query('page') page = '1', @Query('limit') limit = '20', @Query('status') status: string, @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 (status) filter.status = status
const skip = (Math.max(1, +page) - 1) * +limit
const [orders, total] = await Promise.all([
this.orderModel.find(filter).sort({ createdAt: -1 }).skip(skip).limit(+limit).lean().exec(),
this.orderModel.countDocuments(filter).exec(),
])
return { orders, total, page: +page }
}
@UseGuards(JwtAuthGuard)
@Post('order/sync')
async syncOrder(@Body('outTradeNo') outTradeNo: string, @CurrentUser('userId') adminUserId: string) {
const admin = await this.userModel.findById(adminUserId).exec()
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
const wxResult = await this.wechatPay.queryOrder(outTradeNo)
const tradeState = wxResult?.trade_state
const order = await this.orderModel.findOne({ outTradeNo }).exec()
if (!order) throw new HttpException('订单不存在', HttpStatus.NOT_FOUND)
if (tradeState === 'SUCCESS' && order.status === 'pending') {
order.status = 'success'
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') {
const expireAt = new Date()
expireAt.setDate(expireAt.getDate() + VIP_DURATION_DAYS)
user.plan = 'vip'
user.vipExpireAt = expireAt
user.remaining = 999
await user.save()
}
}
return { order, wxResult }
}
// ─── 系统配置(支持管理后台编辑) ──────
@UseGuards(JwtAuthGuard)
@Get('config')
async getConfig(@CurrentUser('userId') adminUserId: string) {
const admin = await this.userModel.findById(adminUserId).exec()
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
return {
interview: {
maxRoundsFree: 5,
maxRoundsVip: 10,
dailyFreeLimit: 3,
},
diagnosis: {
dailyFreeLimit: 2,
},
optimize: {
dailyFreeLimit: 2,
},
price: {
monthly: 2900, // 分
},
}
// 从数据库读,不存在则返回默认值
const cfg = await this.configModel.findOne({ key: 'site_config' }).exec()
if (cfg) return cfg.value
return DEFAULT_CONFIG
}
}
@UseGuards(JwtAuthGuard)
@Post('config/save')
async saveConfig(@Body() body: any, @CurrentUser('userId') adminUserId: string) {
const admin = await this.userModel.findById(adminUserId).exec()
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
await this.configModel.findOneAndUpdate(
{ key: 'site_config' },
{ key: 'site_config', value: body, description: '站点配置' },
{ upsert: true },
).exec()
return { success: true }
}
// ─── 每日一题管理 ──────────────
@UseGuards(JwtAuthGuard)
@Get('questions')
async getQuestions(@CurrentUser('userId') adminUserId: string) {
const admin = await this.userModel.findById(adminUserId).exec()
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
const cfg = await this.configModel.findOne({ key: 'daily_questions' }).exec()
return cfg?.value || DEFAULT_QUESTIONS
}
@UseGuards(JwtAuthGuard)
@Post('questions/save')
async saveQuestions(@Body() body: any, @CurrentUser('userId') adminUserId: string) {
const admin = await this.userModel.findById(adminUserId).exec()
if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN)
await this.configModel.findOneAndUpdate(
{ key: 'daily_questions' },
{ key: 'daily_questions', value: body, description: '每日一题题库' },
{ upsert: true },
).exec()
return { success: true }
}
}
const DEFAULT_CONFIG = {
interview: { maxRoundsFree: 5, maxRoundsVip: 10, dailyFreeLimit: 3 },
diagnosis: { dailyFreeLimit: 2 },
optimize: { dailyFreeLimit: 2 },
price: { monthly: 1990 },
plans: {
free: { name: '免费版', price: 0, features: ['每日 3 次 AI 模拟面试', '每场最多 5 轮 AI 对话', '基础面试报告', '简历诊断', '简历优化'] },
growth: { name: '成长版', price: 1990, features: ['免费版全部权益', '无限面试次数', '每场最多 10 轮 AI 对话', '详细面试报告(四维评分)', '进步轨迹雷达图 + 打卡', '参考回答思路', '公司真题库'] },
},
}
const DEFAULT_QUESTIONS = [
{ position: '通用', category: 'behavioral', question: '请做一个简单的自我介绍,突出你的核心优势和职业目标。', referenceAnswer: '建议结构:1) 基本信息 2) 教育背景与专业方向 3) 实习/项目经历 4) 核心优势 5) 职业目标' },
{ position: '前端工程师', category: 'technical', question: '请用 JavaScript 实现一个深拷贝函数,并说明可能存在的问题。', referenceAnswer: '可使用递归遍历,注意循环引用用 WeakMap 处理,特殊类型如 Date/RegExp/Map/Set 需单独处理。' },
{ position: '后端工程师', category: 'technical', question: '请说说你对 RESTful API 设计的理解,以及和 GraphQL 的区别。', referenceAnswer: 'RESTful 以资源为核心,使用 HTTP 方法操作;GraphQL 客户端可指定返回字段,减少过度获取。' },
{ position: 'AI 算法工程师', category: 'technical', question: '解释一下 Transformer 架构中的 Self-Attention 机制是如何工作的。', referenceAnswer: 'Self-Attention 通过 QKV 计算注意力权重,公式为 Attention(Q,K,V)=softmax(QK^T/√d)V。' },
]
+11 -2
View File
@@ -3,12 +3,21 @@ import { MongooseModule } from '@nestjs/mongoose'
import { AdminController } from './admin.controller'
import { User, UserSchema } from '../user/user.schema'
import { Interview, InterviewSchema } from '../interview/interview.schema'
import { PaymentOrder, PaymentOrderSchema } from '../payment/payment-order.schema'
import { WechatPayService } from '../payment/wechat-pay.service'
import { SiteConfig, SiteConfigSchema } from '../schemas/site-config.schema'
@Module({
imports: [
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
MongooseModule.forFeature([{ name: Interview.name, schema: InterviewSchema }]),
MongooseModule.forFeature([
{ name: User.name, schema: UserSchema },
{ name: Interview.name, schema: InterviewSchema },
{ name: PaymentOrder.name, schema: PaymentOrderSchema },
{ name: SiteConfig.name, schema: SiteConfigSchema },
]),
],
controllers: [AdminController],
providers: [WechatPayService],
})
export class AdminModule {}