feat: Phase 1-3 全部完成 — 沙盒增强、学情分析、学习路径

This commit is contained in:
yuzhiran-dev
2026-05-18 09:48:51 +08:00
commit 11bb86854c
277 changed files with 37755 additions and 0 deletions
@@ -0,0 +1,33 @@
import { Controller, Post, Get, Body, UseGuards, Req } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { RegisterDto } from './dto/register.dto';
@ApiTags('认证')
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('register')
async register(@Body() body: RegisterDto) {
return this.authService.register(body);
}
@Post('login')
async login(@Body() body: { account: string; password: string }) {
return this.authService.login(body.account, body.password);
}
@Post('refresh')
async refresh(@Body() body: { accessToken: string }) {
return this.authService.refreshAccessToken(body.accessToken);
}
@Get('profile')
@UseGuards(AuthGuard('jwt'))
@ApiBearerAuth()
async profile(@Req() req: any) {
return this.authService.getProfile(req.user.userId);
}
}
+24
View File
@@ -0,0 +1,24 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigService } from '@nestjs/config';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
useFactory: (config: ConfigService) => ({
secret: config.get('JWT_SECRET'),
signOptions: { expiresIn: config.get('JWT_EXPIRES_IN') || '2h' },
}),
inject: [ConfigService],
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService, JwtModule],
})
export class AuthModule {}
+91
View File
@@ -0,0 +1,91 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcryptjs';
import { PrismaService } from '../../prisma/prisma.service';
@Injectable()
export class AuthService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
async register(data: { phone?: string; email?: string; password: string; nickname?: string }) {
const passwordHash = await bcrypt.hash(data.password, 10);
const user = await this.prisma.user.create({
data: {
phone: data.phone,
email: data.email,
passwordHash,
nickname: data.nickname || data.phone || data.email?.split('@')[0],
},
});
return this.generateTokens(user.id);
}
async login(account: string, password: string) {
const user = await this.prisma.user.findFirst({
where: {
OR: [{ phone: account }, { email: account }],
deletedAt: null,
},
});
if (!user || !user.passwordHash) {
throw new UnauthorizedException('账号或密码错误');
}
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
throw new UnauthorizedException('账号或密码错误');
}
await this.prisma.user.update({
where: { id: user.id },
data: { lastLoginAt: new Date() },
});
return this.generateTokens(user.id);
}
async refreshAccessToken(accessToken: string) {
try {
const payload = this.jwtService.verify(accessToken, { ignoreExpiration: true });
const user = await this.prisma.user.findUnique({
where: { id: payload.sub, deletedAt: null },
});
if (!user || user.status !== 'ACTIVE') {
throw new UnauthorizedException('用户不可用');
}
return this.generateTokens(user.id);
} catch {
throw new UnauthorizedException('Token 无效');
}
}
async getProfile(userId: number) {
return this.prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
phone: true,
email: true,
nickname: true,
avatar: true,
status: true,
memberPlan: true,
memberExpire: true,
sandboxDaily: true,
createdAt: true,
},
});
}
private generateTokens(userId: number) {
const payload = { sub: userId };
return {
accessToken: this.jwtService.sign(payload, { expiresIn: '2h' }),
refreshToken: this.jwtService.sign(payload, { expiresIn: '7d' }),
};
}
}
@@ -0,0 +1,20 @@
import { IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator';
export class RegisterDto {
@IsOptional()
@IsString()
phone?: string;
@IsOptional()
@IsString()
email?: string;
@IsNotEmpty({ message: '密码不能为空' })
@IsString()
@MinLength(6, { message: '密码至少6位' })
password: string;
@IsOptional()
@IsString()
nickname?: string;
}
+36
View File
@@ -0,0 +1,36 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from '../../prisma/prisma.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private config: ConfigService,
private prisma: PrismaService,
) {
const secret = config.get<string>('JWT_SECRET') || 'yuzhiran-ai-default-secret';
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: secret,
});
}
async validate(payload: { sub: number; type?: string }) {
if (payload.type === 'admin') {
const admin = await this.prisma.adminUser.findUnique({ where: { id: payload.sub } });
if (!admin || admin.status !== 'ACTIVE') {
throw new UnauthorizedException();
}
return { userId: payload.sub, isAdmin: true };
}
const user = await this.prisma.user.findUnique({ where: { id: payload.sub } });
if (!user || user.deletedAt) {
throw new UnauthorizedException();
}
return { userId: payload.sub, isAdmin: false };
}
}
@@ -0,0 +1,45 @@
// @ts-nocheck
import { Test, TestingModule } from '@nestjs/testing';
import { JwtService } from '@nestjs/jwt';
import { AuthService } from '../auth.service';
import { PrismaService } from '../../../prisma/prisma.service';
jest.mock('bcryptjs', () => ({
compare: jest.fn(),
hash: jest.fn(),
}));
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const mockPrisma = {
user: {
create: jest.fn().mockResolvedValue({ id: 1 }),
findFirst: jest.fn().mockResolvedValue(null),
findUnique: jest.fn().mockResolvedValue(null),
update: jest.fn().mockResolvedValue({}),
},
};
const mockJwtService = {
sign: jest.fn().mockReturnValue('mock-token'),
verify: jest.fn().mockReturnValue({ sub: 1 }),
};
const module = await Test.createTestingModule({
providers: [
AuthService,
{ provide: PrismaService, useValue: mockPrisma },
{ provide: JwtService, useValue: mockJwtService },
],
}).compile();
service = module.get<AuthService>(AuthService);
jest.clearAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});