From 227a3c5b7b7002805f91863227ebcc68c0039bed Mon Sep 17 00:00:00 2001 From: lichao <2483469113@qq.com> Date: Tue, 24 Feb 2026 20:17:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E4=BB=A5=E5=8F=8A=E9=83=A8=E7=BD=B2=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 10 ++ .gitea/workflows/deploy.yml | 107 ++++++++++++ Dockerfile | 42 +++++ src/common/common.module.ts | 7 +- src/common/controllers/admin.controller.ts | 180 +++++++++++++++++++++ src/common/dto/set-user-role.dto.ts | 14 ++ src/common/enums/user-role.enum.ts | 11 ++ src/common/guards/super-admin.guard.ts | 33 ++++ src/common/services/admin.service.ts | 77 ++++++--- src/deepseek/deepseek.service.ts | 2 +- src/license/license.controller.ts | 3 +- src/user/entities/user.entity.ts | 9 ++ 12 files changed, 470 insertions(+), 25 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/deploy.yml create mode 100644 Dockerfile create mode 100644 src/common/controllers/admin.controller.ts create mode 100644 src/common/dto/set-user-role.dto.ts create mode 100644 src/common/enums/user-role.enum.ts create mode 100644 src/common/guards/super-admin.guard.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..eed7993 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +dist +.env +.env.local +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..4c8e18b --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,107 @@ +name: Backend CI/CD + +on: + push: + branches: + - main + - master + pull_request: + branches: + - main + - master + +jobs: + # 构建和测试 + build: + runs-on: ubuntu-latest + steps: + - name: 检出代码 + uses: actions/checkout@v3 + + - name: 设置 Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: 安装 pnpm + run: npm install -g pnpm + + - name: 安装依赖 + run: pnpm install --frozen-lockfile + + - name: 代码检查 + run: | + echo "运行代码检查..." + # pnpm run lint + + - name: 运行测试 + run: | + echo "运行单元测试..." + # pnpm run test + + - name: 构建项目 + run: pnpm run build + + - name: 上传构建产物 + uses: actions/upload-artifact@v3 + with: + name: dist + path: dist/ + retention-days: 1 + + # 部署到生产环境 + deploy: + needs: build + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + steps: + - name: 检出代码 + uses: actions/checkout@v3 + + - name: 下载构建产物 + uses: actions/download-artifact@v3 + with: + name: dist + path: dist/ + + - name: 部署到服务器 + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + port: ${{ secrets.SERVER_PORT || 22 }} + script: | + cd /path/to/self_proj + + # 备份数据库 + bash deploy.sh backup + + # 拉取最新代码 + cd oauth_nest_demo + git pull + cd .. + + # 重新构建并启动后端 + docker-compose up -d --build backend + + # 等待服务启动 + sleep 15 + + # 检查服务状态 + docker-compose ps + + - name: 健康检查 + run: | + echo "等待服务启动..." + sleep 10 + curl -f http://${{ secrets.SERVER_HOST }}:3000/api || exit 1 + + - name: 通知部署结果 + if: always() + run: | + if [ ${{ job.status }} == 'success' ]; then + echo "✅ 后端部署成功!" + else + echo "❌ 后端部署失败!" + fi diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a87cb24 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +# 构建阶段 +FROM node:18-alpine AS builder + +WORKDIR /app + +# 安装 pnpm +RUN npm install -g pnpm + +# 复制依赖文件 +COPY package.json pnpm-lock.yaml ./ + +# 安装依赖 +RUN pnpm install --frozen-lockfile + +# 复制源码 +COPY . . + +# 构建 +RUN pnpm run build + +# 生产阶段 +FROM node:18-alpine + +WORKDIR /app + +# 安装 pnpm +RUN npm install -g pnpm + +# 复制依赖文件 +COPY package.json pnpm-lock.yaml ./ + +# 只安装生产依赖 +RUN pnpm install --prod --frozen-lockfile + +# 从构建阶段复制构建产物 +COPY --from=builder /app/dist ./dist + +# 暴露端口 +EXPOSE 3000 + +# 启动应用 +CMD ["node", "dist/main"] diff --git a/src/common/common.module.ts b/src/common/common.module.ts index 58efb0e..3b921bb 100644 --- a/src/common/common.module.ts +++ b/src/common/common.module.ts @@ -3,11 +3,14 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from '../user/entities/user.entity'; import { AdminService } from './services/admin.service'; import { AdminGuard } from './guards/admin.guard'; +import { SuperAdminGuard } from './guards/super-admin.guard'; +import { AdminController } from './controllers/admin.controller'; @Global() @Module({ imports: [TypeOrmModule.forFeature([User])], - providers: [AdminService, AdminGuard], - exports: [AdminService, AdminGuard], + controllers: [AdminController], + providers: [AdminService, AdminGuard, SuperAdminGuard], + exports: [AdminService, AdminGuard, SuperAdminGuard], }) export class CommonModule {} diff --git a/src/common/controllers/admin.controller.ts b/src/common/controllers/admin.controller.ts new file mode 100644 index 0000000..5e9d46a --- /dev/null +++ b/src/common/controllers/admin.controller.ts @@ -0,0 +1,180 @@ +import { + Controller, + Get, + Post, + Body, + Param, + UseGuards, + Req, +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, + ApiParam, + ApiBody, +} from '@nestjs/swagger'; +import { AdminService } from '../services/admin.service'; +import { SuperAdminGuard } from '../guards/super-admin.guard'; +import { AdminGuard } from '../guards/admin.guard'; +import { SetUserRoleDto } from '../dto/set-user-role.dto'; +import { User } from '../../user/entities/user.entity'; +import { UserRole } from '../enums/user-role.enum'; +import { + UnauthorizedResponseDto, + NotFoundResponseDto, +} from '../dto/error-response.dto'; + +@ApiTags('管理员管理模块') +@ApiBearerAuth() +@Controller('admin') +export class AdminController { + constructor(private readonly adminService: AdminService) {} + + @Get('users') + @UseGuards(AdminGuard) + @ApiOperation({ + summary: '获取所有管理员用户', + description: '查询所有管理员和超级管理员用户列表。需要管理员权限', + }) + @ApiResponse({ + status: 200, + description: '查询成功', + type: [User], + }) + @ApiResponse({ + status: 401, + description: '未授权,需要管理员权限', + type: UnauthorizedResponseDto, + }) + async getAdminUsers(): Promise { + return this.adminService.getAdminUsers(); + } + + @Get('check') + @UseGuards(AdminGuard) + @ApiOperation({ + summary: '检查当前用户权限', + description: '返回当前用户的角色和权限信息', + }) + @ApiResponse({ + status: 200, + description: '查询成功', + schema: { + example: { + userId: 1, + role: 'admin', + isAdmin: true, + isSuperAdmin: false, + }, + }, + }) + @ApiResponse({ + status: 401, + description: '未授权', + type: UnauthorizedResponseDto, + }) + async checkPermission(@Req() req: any): Promise<{ + userId: number; + role: UserRole | null; + isAdmin: boolean; + isSuperAdmin: boolean; + }> { + const userId = req.user?.userId || req.user?.id; + const role = await this.adminService.getUserRole(userId); + const isAdmin = await this.adminService.isAdmin(userId); + const isSuperAdmin = await this.adminService.isSuperAdmin(userId); + + return { + userId, + role, + isAdmin, + isSuperAdmin, + }; + } + + @Post('users/:id/role') + @UseGuards(SuperAdminGuard) + @ApiOperation({ + summary: '设置用户角色(超级管理员)', + description: '修改指定用户的角色。仅超级管理员可访问', + }) + @ApiParam({ + name: 'id', + description: '用户ID', + type: Number, + }) + @ApiBody({ type: SetUserRoleDto }) + @ApiResponse({ + status: 200, + description: '角色设置成功', + schema: { + example: { + message: '用户角色已更新', + user: { + id: 1, + username: 'testuser', + email: 'test@example.com', + role: 'admin', + }, + }, + }, + }) + @ApiResponse({ + status: 401, + description: '未授权,需要超级管理员权限', + type: UnauthorizedResponseDto, + }) + @ApiResponse({ + status: 404, + description: '用户不存在', + type: NotFoundResponseDto, + }) + async setUserRole( + @Param('id') id: string, + @Body() dto: SetUserRoleDto, + ): Promise<{ message: string; user: User }> { + const user = await this.adminService.setUserRole(+id, dto.role); + return { + message: '用户角色已更新', + user, + }; + } + + @Get('users/:id/role') + @UseGuards(AdminGuard) + @ApiOperation({ + summary: '查询用户角色', + description: '获取指定用户的角色信息。需要管理员权限', + }) + @ApiParam({ + name: 'id', + description: '用户ID', + type: Number, + }) + @ApiResponse({ + status: 200, + description: '查询成功', + schema: { + example: { + userId: 1, + role: 'admin', + }, + }, + }) + @ApiResponse({ + status: 401, + description: '未授权,需要管理员权限', + type: UnauthorizedResponseDto, + }) + async getUserRole( + @Param('id') id: string, + ): Promise<{ userId: number; role: UserRole | null }> { + const role = await this.adminService.getUserRole(+id); + return { + userId: +id, + role, + }; + } +} diff --git a/src/common/dto/set-user-role.dto.ts b/src/common/dto/set-user-role.dto.ts new file mode 100644 index 0000000..7ce2db8 --- /dev/null +++ b/src/common/dto/set-user-role.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsNotEmpty } from 'class-validator'; +import { UserRole } from '../enums/user-role.enum'; + +export class SetUserRoleDto { + @ApiProperty({ + description: '用户角色', + enum: UserRole, + example: UserRole.ADMIN, + }) + @IsEnum(UserRole, { message: '角色必须是有效的枚举值' }) + @IsNotEmpty({ message: '角色不能为空' }) + role: UserRole; +} diff --git a/src/common/enums/user-role.enum.ts b/src/common/enums/user-role.enum.ts new file mode 100644 index 0000000..5f3cd87 --- /dev/null +++ b/src/common/enums/user-role.enum.ts @@ -0,0 +1,11 @@ +/** + * 用户角色枚举 + */ +export enum UserRole { + /** 普通用户 */ + USER = 'user', + /** 管理员 */ + ADMIN = 'admin', + /** 超级管理员 */ + SUPER_ADMIN = 'super_admin', +} diff --git a/src/common/guards/super-admin.guard.ts b/src/common/guards/super-admin.guard.ts new file mode 100644 index 0000000..fddfb7b --- /dev/null +++ b/src/common/guards/super-admin.guard.ts @@ -0,0 +1,33 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + ForbiddenException, +} from '@nestjs/common'; +import { AdminService } from '../services/admin.service'; + +/** + * 超级管理员守卫 + * 仅允许超级管理员访问 + */ +@Injectable() +export class SuperAdminGuard implements CanActivate { + constructor(private adminService: AdminService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const user = request.user; + + if (!user || !user.id) { + throw new ForbiddenException('请先登录'); + } + + const isSuperAdmin = await this.adminService.isSuperAdmin(user.id); + + if (!isSuperAdmin) { + throw new ForbiddenException('需要超级管理员权限'); + } + + return true; + } +} diff --git a/src/common/services/admin.service.ts b/src/common/services/admin.service.ts index 2ff5196..0b9ff8b 100644 --- a/src/common/services/admin.service.ts +++ b/src/common/services/admin.service.ts @@ -1,7 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Repository, In } from 'typeorm'; import { User } from '../../user/entities/user.entity'; +import { UserRole } from '../enums/user-role.enum'; @Injectable() export class AdminService { @@ -12,43 +13,77 @@ export class AdminService { /** * 检查用户是否是管理员 - * 简单实现:检查用户名是否为 admin 或邮箱包含 admin - * 生产环境应该使用更复杂的角色系统 + * 基于角色的权限判断:ADMIN 或 SUPER_ADMIN */ async isAdmin(userId: number): Promise { const user = await this.userRepository.findOne({ where: { id: userId }, + select: ['id', 'role', 'isActive'], }); - if (!user) { + if (!user || !user.isActive) { return false; } - // 简单的管理员判断逻辑(可根据实际需求调整) - return ( - user.username === 'admin' || - user.email.includes('admin') || - user.username.startsWith('admin_') - ); + return user.role === UserRole.ADMIN || user.role === UserRole.SUPER_ADMIN; + } + + /** + * 检查用户是否是超级管理员 + */ + async isSuperAdmin(userId: number): Promise { + const user = await this.userRepository.findOne({ + where: { id: userId }, + select: ['id', 'role', 'isActive'], + }); + + if (!user || !user.isActive) { + return false; + } + + return user.role === UserRole.SUPER_ADMIN; } /** * 获取所有管理员用户 */ async getAdminUsers(): Promise { - const users = await this.userRepository.find(); - const admins: User[] = []; + return this.userRepository.find({ + where: { + role: In([UserRole.ADMIN, UserRole.SUPER_ADMIN]), + isActive: true, + }, + order: { + role: 'DESC', // SUPER_ADMIN 排在前面 + }, + }); + } - for (const user of users) { - if ( - user.username === 'admin' || - user.email.includes('admin') || - user.username.startsWith('admin_') - ) { - admins.push(user); - } + /** + * 设置用户角色(仅超级管理员可调用) + */ + async setUserRole(userId: number, role: UserRole): Promise { + const user = await this.userRepository.findOne({ + where: { id: userId }, + }); + + if (!user) { + throw new Error('用户不存在'); } - return admins; + user.role = role; + return this.userRepository.save(user); + } + + /** + * 获取用户角色 + */ + async getUserRole(userId: number): Promise { + const user = await this.userRepository.findOne({ + where: { id: userId }, + select: ['id', 'role'], + }); + + return user?.role || null; } } diff --git a/src/deepseek/deepseek.service.ts b/src/deepseek/deepseek.service.ts index fd004ba..28eb034 100644 --- a/src/deepseek/deepseek.service.ts +++ b/src/deepseek/deepseek.service.ts @@ -111,7 +111,7 @@ export class DeepseekService { { headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}`, + Authorization: `Bearer ${apiKey}`, }, }, ); diff --git a/src/license/license.controller.ts b/src/license/license.controller.ts index 96f17d1..bcf068a 100644 --- a/src/license/license.controller.ts +++ b/src/license/license.controller.ts @@ -221,7 +221,8 @@ export class LicenseController { @UseGuards(AdminGuard) @ApiOperation({ summary: '获取卡密统计信息(管理员)', - description: '获取卡密的统计数据,包括总数、已激活、未使用等。仅管理员可访问', + description: + '获取卡密的统计数据,包括总数、已激活、未使用等。仅管理员可访问', }) @ApiResponse({ status: 200, diff --git a/src/user/entities/user.entity.ts b/src/user/entities/user.entity.ts index 4cd0bcb..7ed54b2 100644 --- a/src/user/entities/user.entity.ts +++ b/src/user/entities/user.entity.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; +import { UserRole } from '../../common/enums/user-role.enum'; @Entity() export class User { @@ -19,6 +20,14 @@ export class User { @ApiProperty({ description: '邮箱' }) email: string; + @Column({ type: 'varchar', default: UserRole.USER }) + @ApiProperty({ + description: '用户角色', + enum: UserRole, + default: UserRole.USER, + }) + role: UserRole; + @Column({ default: true }) @ApiProperty({ description: '是否激活', default: true }) isActive: boolean;