初始化:职引项目 v1.0

This commit is contained in:
yuzhiran
2026-06-08 16:28:00 +08:00
commit 511f60d0db
111 changed files with 27295 additions and 0 deletions
@@ -0,0 +1,56 @@
import { Controller, Post, Get, Put, Body, Req } from '@nestjs/common'
import { UserService } from './user.service'
import { Public } from '../../common/decorators/public.decorator'
import { CurrentUser } from '../../common/decorators/current-user.decorator'
@Controller('user')
export class UserController {
constructor(private userService: UserService) {}
@Public()
@Post('send-code')
async sendCode(@Body('phone') phone: string) {
return this.userService.sendCode(phone)
}
@Public()
@Post('login')
async login(@Body('phone') phone: string, @Body('code') code: string) {
return this.userService.loginByPhone(phone, code)
}
// 📧 邮箱验证码登录(H5 用)
@Public()
@Post('send-email-code')
async sendEmailCode(@Body('email') email: string) {
return this.userService.sendEmailCode(email)
}
@Public()
@Post('email-login')
async emailLogin(@Body('email') email: string, @Body('code') code: string) {
return this.userService.loginByEmail(email, code)
}
// 微信静默登录
@Public()
@Post('wx-login')
async wxLogin(@Body('code') code: string) {
return this.userService.loginByWx(code)
}
@Get('info')
async getInfo(@CurrentUser('userId') userId: string) {
return this.userService.getInfo(userId)
}
@Put('update')
async update(@CurrentUser('userId') userId: string, @Body() data: { nickname?: string; avatar?: string }) {
return this.userService.update(userId, data)
}
@Get('usage')
async getUsage(@CurrentUser('userId') userId: string) {
return this.userService.getUsage(userId)
}
}
+20
View File
@@ -0,0 +1,20 @@
import { Module } from '@nestjs/common'
import { MongooseModule } from '@nestjs/mongoose'
import { JwtModule } from '@nestjs/jwt'
import { UserController } from './user.controller'
import { UserService } from './user.service'
import { User, UserSchema } from './user.schema'
@Module({
imports: [
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
JwtModule.register({
secret: process.env.JWT_SECRET || 'zhiyin-jwt-secret-2026',
signOptions: { expiresIn: '7d' },
}),
],
controllers: [UserController],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
+42
View File
@@ -0,0 +1,42 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
import { Document } from 'mongoose'
export type UserDocument = User & Document
@Schema({ timestamps: true })
export class User {
@Prop({ unique: true, sparse: true })
phone?: string
@Prop({ unique: true, sparse: true })
wxOpenid?: string
@Prop({ default: '' })
nickname?: string
@Prop({ default: '' })
avatar?: string
@Prop({ default: 0 })
interviewCount: number
@Prop({ default: 3 })
remaining: number
@Prop({ default: 'free' })
plan: string
@Prop()
vipExpireAt?: Date
@Prop({ default: 'user' })
role: string // 'user' | 'admin'
@Prop({ default: false })
isSystemAdmin: boolean
@Prop({ unique: true, sparse: true })
email?: string
}
export const UserSchema = SchemaFactory.createForClass(User)
+165
View File
@@ -0,0 +1,165 @@
import { Injectable, HttpException, HttpStatus } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import { Model } from 'mongoose'
import { JwtService } from '@nestjs/jwt'
import { User, UserDocument } from './user.schema'
import { EmailService } from '../email/email.service'
// In-memory stores
const codeStore = new Map<string, { code: string; expiresAt: number }>()
const emailCodeStore = new Map<string, { code: string; expiresAt: number }>()
@Injectable()
export class UserService {
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>,
private jwtService: JwtService,
private emailService: EmailService,
) {}
async sendCode(phone: string) {
const code = process.env.NODE_ENV === 'production'
? String(Math.floor(100000 + Math.random() * 900000))
: '123456'
codeStore.set(phone, { code, expiresAt: Date.now() + 5 * 60 * 1000 })
if (process.env.NODE_ENV !== 'production') {
console.log(`[DEV] Verification code for ${phone}: ${code}`)
}
return { message: '验证码已发送' }
}
async loginByPhone(phone: string, code: string) {
const record = codeStore.get(phone)
if (!record || record.code !== code) {
throw new HttpException('验证码错误', HttpStatus.UNAUTHORIZED)
}
if (Date.now() > record.expiresAt) {
codeStore.delete(phone)
throw new HttpException('验证码已过期', HttpStatus.UNAUTHORIZED)
}
codeStore.delete(phone)
let user = await this.userModel.findOne({ phone }).exec()
if (!user) {
user = await this.userModel.create({ phone, nickname: `用户${phone.slice(-4)}` })
}
return this.generateAuthResponse(user)
}
async loginByWx(code: string) {
// WeChat silent login - exchange code for openid
const appid = process.env.WX_APPID
const secret = process.env.WX_SECRET
if (!appid || !secret) {
throw new HttpException('微信登录未配置', HttpStatus.SERVICE_UNAVAILABLE)
}
const wxRes = await fetch(
`https://api.weixin.qq.com/sns/jscode2session?appid=${appid}&secret=${secret}&js_code=${code}&grant_type=authorization_code`,
)
const wxData: any = await wxRes.json()
if (wxData.errcode) {
throw new HttpException(`微信登录失败: ${wxData.errmsg}`, HttpStatus.UNAUTHORIZED)
}
const openid = wxData.openid
let user = await this.userModel.findOne({ wxOpenid: openid }).exec()
if (!user) {
user = await this.userModel.create({ wxOpenid: openid, nickname: '微信用户' })
}
return this.generateAuthResponse(user)
}
// 📧 邮箱验证码
async sendEmailCode(email: string) {
if (!email || !email.includes('@')) {
throw new HttpException('请输入正确的邮箱地址', HttpStatus.BAD_REQUEST)
}
const code = String(Math.floor(100000 + Math.random() * 900000))
emailCodeStore.set(email, { code, expiresAt: Date.now() + 10 * 60 * 1000 })
const sent = await this.emailService.sendVerificationCode(email, code)
if (sent) {
return { message: '验证码已发送到邮箱' }
}
// 邮件发送失败时返回 devCode 方便调试
console.log(`[EMAIL] Dev code for ${email}: ${code}`)
return { message: '验证码已发送(邮件服务暂不可用,开发模式)', devCode: code }
}
async loginByEmail(email: string, code: string) {
const record = emailCodeStore.get(email)
if (!record || record.code !== code) {
throw new HttpException('验证码错误', HttpStatus.UNAUTHORIZED)
}
if (Date.now() > record.expiresAt) {
emailCodeStore.delete(email)
throw new HttpException('验证码已过期', HttpStatus.UNAUTHORIZED)
}
emailCodeStore.delete(email)
// 按邮箱查找或创建用户
let user = await this.userModel.findOne({ email }).exec()
if (!user) {
const nick = email.split('@')[0]
user = await this.userModel.create({ email, nickname: nick, remaining: 3 })
}
return this.generateAuthResponse(user)
}
async getInfo(userId: string) {
const user = await this.userModel.findById(userId).exec()
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
return this.safeUser(user)
}
async update(userId: string, data: { nickname?: string; avatar?: string }) {
const user = await this.userModel.findByIdAndUpdate(userId, data, { new: true }).exec()
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
return this.safeUser(user)
}
getModel() { return this.userModel }
async getUsage(userId: string) {
const user = await this.userModel.findById(userId).exec()
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
return { remaining: user.remaining, plan: user.plan, interviewCount: user.interviewCount }
}
async deductRemaining(userId: string) {
const user = await this.userModel.findById(userId).exec()
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
if (user.remaining <= 0) throw new HttpException('使用次数已用完', HttpStatus.FORBIDDEN)
user.remaining -= 1
user.interviewCount += 1
await user.save()
}
private generateAuthResponse(user: UserDocument) {
const payload = { userId: user._id.toString(), phone: user.phone || '' }
return {
token: this.jwtService.sign(payload),
user: this.safeUser(user),
}
}
private safeUser(user: UserDocument) {
return {
id: user._id.toString(),
phone: user.phone || '',
email: user.email || '',
nickname: user.nickname || '',
avatar: user.avatar || '',
plan: user.plan,
role: user.role || 'user',
isSystemAdmin: user.isSystemAdmin || false,
remaining: user.remaining,
interviewCount: user.interviewCount,
}
}
}