feat: 前端:玩家服务平台和运营管理系统后台初始化及框架搭建,后端:完成基础功能搭建。

This commit is contained in:
Stev_Wang
2025-12-27 20:17:20 +08:00
parent 99740da922
commit 2d8566132e
60 changed files with 2330 additions and 5 deletions

View File

@@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

33
backend/src/app.module.ts Normal file
View File

@@ -0,0 +1,33 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { PlayerModule } from './player/player.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
TypeOrmModule.forRoot({
type: 'mysql',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '3306', 10),
username: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_DATABASE || 'jge_rs_sl_web',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: process.env.NODE_ENV === 'development',
logging: process.env.NODE_ENV === 'development',
charset: 'utf8mb4',
}),
AuthModule,
PlayerModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@@ -0,0 +1,21 @@
import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { LoginResponseDto } from './dto/login-response.dto';
@Controller('admin/auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('login')
@HttpCode(HttpStatus.OK)
async login(@Body() loginDto: LoginDto): Promise<LoginResponseDto> {
return this.authService.login(loginDto);
}
@Post('logout')
@HttpCode(HttpStatus.OK)
async logout(): Promise<{ message: string }> {
return { message: '登出成功' };
}
}

View File

@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { AdminUser } from '../entities/admin-user.entity';
@Module({
imports: [
TypeOrmModule.forFeature([AdminUser]),
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET || 'your_jwt_secret_key_change_in_production',
signOptions: { expiresIn: process.env.JWT_EXPIRES_IN || '24h' } as any,
}),
],
controllers: [AuthController],
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}

View File

@@ -0,0 +1,50 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { AdminUser } from '../entities/admin-user.entity';
import { LoginDto } from './dto/login.dto';
import { LoginResponseDto } from './dto/login-response.dto';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(AdminUser)
private adminUserRepository: Repository<AdminUser>,
private jwtService: JwtService,
) {}
async login(loginDto: LoginDto): Promise<LoginResponseDto> {
const { username, password } = loginDto;
const user = await this.adminUserRepository.findOne({
where: { username },
});
if (!user) {
throw new UnauthorizedException('用户名或密码错误');
}
const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
if (!isPasswordValid) {
throw new UnauthorizedException('用户名或密码错误');
}
const payload = { sub: user.id, username: user.username, role: user.role };
return {
accessToken: this.jwtService.sign(payload),
userId: user.id,
username: user.username,
role: user.role,
};
}
async validateUser(userId: number): Promise<AdminUser | null> {
return this.adminUserRepository.findOne({
where: { id: userId },
});
}
}

View File

@@ -0,0 +1,9 @@
export class LoginResponseDto {
accessToken: string;
userId: number;
username: string;
role: string;
}

View File

@@ -0,0 +1,12 @@
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
export class LoginDto {
@IsNotEmpty({ message: '用户名不能为空' })
@IsString({ message: '用户名必须是字符串' })
username: string;
@IsNotEmpty({ message: '密码不能为空' })
@IsString({ message: '密码必须是字符串' })
@MinLength(6, { message: '密码长度至少为6位' })
password: string;
}

View File

@@ -0,0 +1,31 @@
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
const errorResponse = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message:
typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message || exception.message,
};
response.status(status).json(errorResponse);
}
}

View File

@@ -0,0 +1,34 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
statusCode: number;
message: string;
data: T;
timestamp: string;
}
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response<T>> {
return next.handle().pipe(
map((data) => ({
statusCode: context.switchToHttp().getResponse().statusCode,
message: 'success',
data,
timestamp: new Date().toISOString(),
})),
);
}
}

View File

@@ -0,0 +1,22 @@
import { ValidationPipe, ValidationError } from '@nestjs/common';
import { BadRequestException } from '@nestjs/common';
export class CustomValidationPipe extends ValidationPipe {
constructor() {
super({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
exceptionFactory: (errors: ValidationError[]) => {
const messages = errors.map((error) => {
return Object.values(error.constraints || {}).join(', ');
});
return new BadRequestException({
statusCode: 400,
message: messages.join('; '),
error: 'Bad Request',
});
},
});
}
}

View File

@@ -0,0 +1,22 @@
import { DataSource, DataSourceOptions } from 'typeorm';
import { config } from 'dotenv';
config();
export const dataSourceOptions: DataSourceOptions = {
type: 'mysql',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '3306', 10),
username: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_DATABASE || 'jge_rs_sl_web',
entities: ['dist/**/*.entity{.ts,.js}'],
migrations: ['dist/migrations/*{.ts,.js}'],
synchronize: process.env.NODE_ENV === 'development',
logging: process.env.NODE_ENV === 'development',
charset: 'utf8mb4',
};
const dataSource = new DataSource(dataSourceOptions);
export default dataSource;

View File

@@ -0,0 +1,34 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
export enum AdminRole {
SUPER_ADMIN = 'super_admin',
OPERATOR = 'operator',
VIEWER = 'viewer',
}
@Entity('admin_users')
export class AdminUser {
@PrimaryGeneratedColumn({ type: 'bigint', comment: '用户ID' })
id: number;
@Column({ type: 'varchar', length: 50, unique: true, comment: '用户名' })
@Index()
username: string;
@Column({ type: 'char', length: 60, comment: '密码哈希(bcrypt)' })
passwordHash: string;
@Column({
type: 'enum',
enum: AdminRole,
default: AdminRole.VIEWER,
comment: '角色super_admin-超级管理员, operator-操作员, viewer-查看者',
})
role: AdminRole;
@CreateDateColumn({ type: 'datetime', comment: '创建时间' })
createdAt: Date;
@UpdateDateColumn({ type: 'datetime', comment: '更新时间' })
updatedAt: Date;
}

22
backend/src/main.ts Normal file
View File

@@ -0,0 +1,22 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
import { CustomValidationPipe } from './common/pipes/validation.pipe';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: true,
credentials: true,
});
app.useGlobalPipes(new CustomValidationPipe());
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new TransformInterceptor());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

View File

@@ -0,0 +1,21 @@
import { Controller, Post, Body, HttpCode, HttpStatus, Headers } from '@nestjs/common';
import { PlayerService } from './player.service';
import { LoginDto } from '../auth/dto/login.dto';
@Controller('player/auth')
export class PlayerController {
constructor(private playerService: PlayerService) {}
@Post('login')
@HttpCode(HttpStatus.OK)
async login(@Body() loginDto: LoginDto): Promise<any> {
return this.playerService.login(loginDto);
}
@Post('logout')
@HttpCode(HttpStatus.OK)
async logout(@Headers('authorization') authHeader: string): Promise<any> {
const token = authHeader?.replace('Bearer ', '');
return this.playerService.logout(token);
}
}

View File

@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { PlayerController } from './player.controller';
import { PlayerService } from './player.service';
@Module({
imports: [
HttpModule.register({
proxy: false,
}),
],
controllers: [PlayerController],
providers: [PlayerService],
})
export class PlayerModule {}

View File

@@ -0,0 +1,61 @@
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
import { LoginDto } from '../auth/dto/login.dto';
@Injectable()
export class PlayerService {
private readonly officialApiBaseUrl = process.env.GAME_API_BASE_URL || 'https://api.example.com';
constructor(private readonly httpService: HttpService) {}
async login(loginDto: LoginDto): Promise<any> {
try {
const response = await firstValueFrom(
this.httpService.post(`${this.officialApiBaseUrl}?code=auth/login`, loginDto),
);
const data = (response as any).data;
if (!data.success) {
throw new HttpException(
data.message || '登录失败',
HttpStatus.UNAUTHORIZED,
);
}
return {
accessToken: data.data.token,
userId: data.data.userId,
username: data.data.username,
role: 'player',
};
} catch (error) {
console.error('Player login error:', error);
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
(error as any).response?.data?.message || (error as any).response?.data || '登录失败',
(error as any).response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async logout(token: string): Promise<any> {
try {
const response = await firstValueFrom(
this.httpService.post(`${this.officialApiBaseUrl}?code=auth/out_login`, {}, {
headers: {
Authorization: `Bearer ${token}`,
},
}),
);
return (response as any).data;
} catch (error) {
throw new HttpException(
(error as any).response?.data || '登出失败',
(error as any).response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}