2026-03-27 22:20:25 +08:00
|
|
|
|
---
|
|
|
|
|
|
description: NestJS 后端架构规范和模块结构
|
|
|
|
|
|
globs:
|
|
|
|
|
|
- "backend/**/*.ts"
|
|
|
|
|
|
alwaysApply: false
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# 后端架构规范
|
|
|
|
|
|
|
|
|
|
|
|
## 模块结构
|
|
|
|
|
|
|
|
|
|
|
|
每个功能模块应包含:
|
|
|
|
|
|
|
|
|
|
|
|
- `module.ts` - 模块定义
|
|
|
|
|
|
- `controller.ts` - 控制器
|
|
|
|
|
|
- `service.ts` - 服务层
|
|
|
|
|
|
- `dto/` - 数据传输对象目录
|
|
|
|
|
|
|
|
|
|
|
|
### 命名规范
|
|
|
|
|
|
|
|
|
|
|
|
- 模块命名使用复数形式:`users`, `roles`, `contests`
|
|
|
|
|
|
- 子模块放在父模块目录下:`contests/works/`, `contests/teams/`
|
|
|
|
|
|
|
|
|
|
|
|
### 目录结构示例
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
src/
|
|
|
|
|
|
├── contests/
|
|
|
|
|
|
│ ├── contests.module.ts
|
|
|
|
|
|
│ ├── contests/
|
|
|
|
|
|
│ │ ├── contests.module.ts
|
|
|
|
|
|
│ │ ├── contests.controller.ts
|
|
|
|
|
|
│ │ ├── contests.service.ts
|
|
|
|
|
|
│ │ └── dto/
|
|
|
|
|
|
│ ├── works/
|
|
|
|
|
|
│ │ ├── works.module.ts
|
|
|
|
|
|
│ │ ├── works.controller.ts
|
|
|
|
|
|
│ │ ├── works.service.ts
|
|
|
|
|
|
│ │ └── dto/
|
|
|
|
|
|
│ └── teams/
|
|
|
|
|
|
│ └── ...
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 服务层 (Service)
|
|
|
|
|
|
|
|
|
|
|
|
### 基本规范
|
|
|
|
|
|
|
|
|
|
|
|
- 使用 `@Injectable()` 装饰器
|
|
|
|
|
|
- 构造函数注入依赖,使用 `private readonly`
|
|
|
|
|
|
- 所有数据库操作通过 PrismaService
|
|
|
|
|
|
- **禁止直接使用 SQL**
|
|
|
|
|
|
|
|
|
|
|
|
### 标准方法命名
|
|
|
|
|
|
|
|
|
|
|
|
- `create` - 创建
|
|
|
|
|
|
- `findAll` - 查询列表(支持分页)
|
|
|
|
|
|
- `findOne` - 查询单个
|
|
|
|
|
|
- `update` - 更新
|
|
|
|
|
|
- `remove` - 删除(软删除或级联)
|
|
|
|
|
|
|
|
|
|
|
|
### 示例
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
|
export class UsersService {
|
|
|
|
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
|
|
|
|
|
|
|
|
async create(createDto: CreateUserDto, tenantId: number) {
|
|
|
|
|
|
return this.prisma.user.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
...createDto,
|
|
|
|
|
|
tenantId,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async findAll(tenantId: number, skip?: number, take?: number) {
|
|
|
|
|
|
return this.prisma.user.findMany({
|
|
|
|
|
|
where: { tenantId, validState: 1 },
|
|
|
|
|
|
skip,
|
|
|
|
|
|
take,
|
|
|
|
|
|
include: {
|
|
|
|
|
|
roles: {
|
|
|
|
|
|
include: { role: true },
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 控制器层 (Controller)
|
|
|
|
|
|
|
|
|
|
|
|
### 基本规范
|
|
|
|
|
|
|
|
|
|
|
|
- 使用 `@Controller()` 装饰器,路径使用复数形式
|
|
|
|
|
|
- 所有路由默认需要认证(除非使用 `@Public()` 装饰器)
|
|
|
|
|
|
- 使用 REST 风格的 HTTP 方法装饰器
|
|
|
|
|
|
|
|
|
|
|
|
### 装饰器使用
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
@Controller("users")
|
|
|
|
|
|
export class UsersController {
|
|
|
|
|
|
constructor(private readonly usersService: UsersService) {}
|
|
|
|
|
|
|
|
|
|
|
|
@Post()
|
|
|
|
|
|
@RequirePermission("user:create")
|
|
|
|
|
|
async create(
|
|
|
|
|
|
@Body() createDto: CreateUserDto,
|
|
|
|
|
|
@CurrentTenantId() tenantId: number,
|
|
|
|
|
|
@CurrentUser() user: any
|
|
|
|
|
|
) {
|
|
|
|
|
|
return this.usersService.create(createDto, tenantId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Get()
|
|
|
|
|
|
@RequirePermission("user:read")
|
|
|
|
|
|
async findAll(
|
|
|
|
|
|
@CurrentTenantId() tenantId: number,
|
|
|
|
|
|
@Query("skip") skip?: number,
|
|
|
|
|
|
@Query("take") take?: number
|
|
|
|
|
|
) {
|
|
|
|
|
|
return this.usersService.findAll(tenantId, skip, take);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Public()
|
|
|
|
|
|
@Get("public-info")
|
|
|
|
|
|
async getPublicInfo() {
|
|
|
|
|
|
return { version: "1.0.0" };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 常用装饰器
|
|
|
|
|
|
|
|
|
|
|
|
- `@CurrentTenantId()` - 获取当前租户ID
|
|
|
|
|
|
- `@CurrentUser()` - 获取当前用户信息
|
|
|
|
|
|
- `@RequirePermission('module:action')` - 权限检查
|
|
|
|
|
|
- `@Public()` - 公开接口,无需认证
|
|
|
|
|
|
|
|
|
|
|
|
## DTO 规范
|
|
|
|
|
|
|
|
|
|
|
|
### 命名规范
|
|
|
|
|
|
|
|
|
|
|
|
- 创建:`CreateXxxDto`
|
|
|
|
|
|
- 更新:`UpdateXxxDto`
|
|
|
|
|
|
- 查询:`QueryXxxDto`
|
|
|
|
|
|
|
|
|
|
|
|
### 验证规则
|
|
|
|
|
|
|
|
|
|
|
|
使用 `class-validator` 装饰器:
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
import {
|
|
|
|
|
|
IsString,
|
|
|
|
|
|
IsEmail,
|
|
|
|
|
|
IsOptional,
|
|
|
|
|
|
IsArray,
|
|
|
|
|
|
IsNumber,
|
|
|
|
|
|
} from "class-validator";
|
|
|
|
|
|
|
|
|
|
|
|
export class CreateUserDto {
|
|
|
|
|
|
@IsString()
|
|
|
|
|
|
username: string;
|
|
|
|
|
|
|
|
|
|
|
|
@IsString()
|
|
|
|
|
|
password: string;
|
|
|
|
|
|
|
|
|
|
|
|
@IsString()
|
|
|
|
|
|
nickname: string;
|
|
|
|
|
|
|
|
|
|
|
|
@IsEmail()
|
|
|
|
|
|
@IsOptional()
|
|
|
|
|
|
email?: string;
|
|
|
|
|
|
|
|
|
|
|
|
@IsArray()
|
|
|
|
|
|
@IsNumber({}, { each: true })
|
|
|
|
|
|
@IsOptional()
|
|
|
|
|
|
roleIds?: number[];
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 错误处理
|
|
|
|
|
|
|
|
|
|
|
|
使用 NestJS 内置异常,消息使用中文:
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
import {
|
|
|
|
|
|
NotFoundException,
|
|
|
|
|
|
BadRequestException,
|
|
|
|
|
|
UnauthorizedException,
|
|
|
|
|
|
ForbiddenException,
|
|
|
|
|
|
} from "@nestjs/common";
|
|
|
|
|
|
|
|
|
|
|
|
// 示例
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
|
throw new NotFoundException("用户不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!isValid) {
|
|
|
|
|
|
throw new BadRequestException("数据验证失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 权限控制
|
|
|
|
|
|
|
|
|
|
|
|
权限字符串格式:`模块:操作`
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
@RequirePermission('contest:create') // 创建竞赛
|
|
|
|
|
|
@RequirePermission('user:update') // 更新用户
|
|
|
|
|
|
@RequirePermission('role:delete') // 删除角色
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 代码风格
|
|
|
|
|
|
|
|
|
|
|
|
- 导入顺序:NestJS 核心 → 第三方库 → 本地模块
|
|
|
|
|
|
- 使用 async/await,避免 Promise.then()
|
|
|
|
|
|
- 使用解构赋值提高代码可读性
|
|
|
|
|
|
- 复杂逻辑必须添加注释
|