feat: 开发broker相关代码,开发全局代码

This commit is contained in:
R524809
2025-11-18 18:01:04 +08:00
parent a9d7fc9038
commit 7acadf191f
29 changed files with 3149 additions and 106 deletions

View File

@@ -1,8 +1,9 @@
# 开发环境配置
PORT=3200
DB_HOST=127.0.0.1
DB_HOST=localhost
DB_PORT=5432
DB_USER=joey
DB_PASSWORD=vest_mind_0228
DB_DATABASE=vest_mind
DB_USER=
DB_PASSWORD=
DB_DATABASE=vest_mind_test
# DB_DATABASE_TEST=vest_mind_test

View File

@@ -0,0 +1,714 @@
# NestJS 模块生成指南
## 快速生成完整模块
### 方法一:使用 `resource` 命令(推荐)
一次性生成包含 module、controller、service、entity 的完整 CRUD 资源:
```bash
# 生成 user 模块(会在 src/modules/user 目录下创建所有文件)
pnpm nest g resource modules/user
# 或者使用别名
pnpm nest g res modules/user
```
**交互式选项:**
- `What transport layer do you use?` → 选择 `REST API`
- `Would you like to generate CRUD entry points?` → 选择 `Yes`
**生成的文件结构:**
```
src/modules/user/
├── user.controller.ts # 控制器
├── user.controller.spec.ts # 控制器测试
├── user.service.ts # 服务
├── user.service.spec.ts # 服务测试
├── user.module.ts # 模块
└── entities/
└── user.entity.ts # 实体TypeORM
```
### 方法二:分别生成各个文件
```bash
# 生成模块
pnpm nest g module modules/user
# 生成控制器
pnpm nest g controller modules/user
# 生成服务
pnpm nest g service modules/user
# 生成实体(需要手动创建,或使用 TypeORM CLI
```
### 方法三:使用 TypeORM 生成实体
```bash
# 安装 TypeORM CLI如果还没有
pnpm add -D typeorm
# 生成实体(需要先配置 TypeORM
pnpm typeorm entity:create -n User
```
## 文件组织模式
### 模式一:按功能模块组织(推荐)⭐
```
src/
├── modules/
│ ├── user/
│ │ ├── user.module.ts
│ │ ├── user.controller.ts
│ │ ├── user.service.ts
│ │ ├── user.entity.ts
│ │ ├── user.dto.ts # DTO 文件
│ │ ├── user.controller.spec.ts
│ │ └── user.service.spec.ts
│ ├── order/
│ │ ├── order.module.ts
│ │ ├── order.controller.ts
│ │ ├── order.service.ts
│ │ ├── order.entity.ts
│ │ └── ...
│ └── product/
│ └── ...
├── database/
│ ├── database.module.ts
│ └── database.config.ts
└── app.module.ts
```
**优点:**
- 模块化清晰,每个功能独立
- 易于维护和扩展
- 符合 NestJS 最佳实践
**适用场景:**
- 大型企业级应用
- 需要复杂业务逻辑
- 需要清晰的架构分层
## 推荐的文件组织(当前项目)
基于你的项目结构,推荐使用**模式一**
```
src/
├── modules/ # 业务模块
│ ├── user/
│ │ ├── user.module.ts
│ │ ├── user.controller.ts
│ │ ├── user.service.ts
│ │ ├── user.entity.ts
│ │ ├── dto/ # DTO 文件(可选)
│ │ │ ├── create-user.dto.ts
│ │ │ └── update-user.dto.ts
│ │ └── interfaces/ # 接口定义(可选)
│ │ └── user.interface.ts
│ ├── order/
│ └── product/
├── database/ # 数据库配置
│ ├── database.module.ts
│ └── database.config.ts
├── common/ # 公共模块(可选)
│ ├── filters/
│ ├── guards/
│ ├── interceptors/
│ └── pipes/
└── app.module.ts
```
## 实际生成示例
### 生成 user 模块
```bash
# 1. 生成完整资源
cd /Users/joey-xd/sites/vest-mind/vest-mind-backend/apps/api
pnpm nest g resource modules/user
# 2. 选择选项:
# - REST API
# - Yes (生成 CRUD)
```
### 生成后的文件内容示例
**user.entity.ts:**
```typescript
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
```
**user.service.ts:**
```typescript
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Injectable()
export class UserService {
create(createUserDto: CreateUserDto) {
return 'This action adds a new user';
}
findAll() {
return `This action returns all user`;
}
findOne(id: number) {
return `This action returns a #${id} user`;
}
update(id: number, updateUserDto: UpdateUserDto) {
return `This action updates a #${id} user`;
}
remove(id: number) {
return `This action removes a #${id} user`;
}
}
```
**user.controller.ts:**
```typescript
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
@Get()
findAll() {
return this.userService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.userService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.userService.update(+id, updateUserDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.userService.remove(+id);
}
}
```
## 常用生成命令速查
```bash
# 生成资源(完整 CRUD
pnpm nest g resource modules/user
# 生成模块
pnpm nest g module modules/user
# 生成控制器
pnpm nest g controller modules/user
# 生成服务
pnpm nest g service modules/user
# 生成守卫
pnpm nest g guard modules/user/guards/auth
# 生成拦截器
pnpm nest g interceptor modules/user/interceptors/logging
# 生成过滤器
pnpm nest g filter modules/user/filters/http-exception
# 生成管道
pnpm nest g pipe modules/user/pipes/validation
# 生成装饰器
pnpm nest g decorator modules/user/decorators/roles
```
## 注意事项
1. **路径规范:**
- 使用 `modules/模块名` 作为路径
- 会自动创建目录结构
2. **自动导入:**
- 生成的文件会自动导入到相应的模块中
- 如果不想自动导入,使用 `--skip-import` 选项
3. **测试文件:**
- 默认会生成 `.spec.ts` 测试文件
- 使用 `--no-spec` 可以跳过测试文件生成
4. **扁平结构:**
- 使用 `--flat` 可以生成扁平结构(所有文件在同一目录)
- 不推荐使用,会破坏模块化结构
## DTOData Transfer Object详解
### 什么是 DTO
DTOData Transfer Object是数据传输对象用于在不同层之间传输数据。在 NestJS 中DTO 主要用于:
1. **定义 API 请求和响应的数据结构**
2. **数据验证**(结合 `class-validator`
3. **类型安全**
4. **API 文档生成**(结合 Swagger
### 为什么需要 DTO
#### 1. 数据验证
```typescript
// 没有 DTO - 不安全
@Post()
create(@Body() body: any) {
// body 可能是任何数据,没有验证
return this.userService.create(body);
}
// 使用 DTO - 安全
@Post()
create(@Body() createUserDto: CreateUserDto) {
// 数据已经验证,类型安全
return this.userService.create(createUserDto);
}
```
#### 2. 类型安全
```typescript
// DTO 定义了明确的数据结构
export class CreateUserDto {
username: string;
email: string;
age: number;
}
// TypeScript 会检查类型
const user = new CreateUserDto();
user.username = 'john'; // ✅ 正确
user.age = '25'; // ❌ TypeScript 错误
```
#### 3. API 文档
使用 Swagger 时DTO 会自动生成 API 文档:
```typescript
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({ description: '用户名', example: 'john' })
username: string;
@ApiProperty({ description: '邮箱', example: 'john@example.com' })
email: string;
}
```
### DTO 的类型
#### 1. Create DTO创建数据
用于创建新资源时的数据验证:
```typescript
// dto/create-user.dto.ts
import {
IsString,
IsEmail,
IsOptional,
MinLength,
MaxLength,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({ description: '用户名', example: 'john' })
@IsString()
@MinLength(3)
@MaxLength(20)
username: string;
@ApiProperty({ description: '邮箱', example: 'john@example.com' })
@IsEmail()
email: string;
@ApiProperty({ description: '密码', example: 'password123' })
@IsString()
@MinLength(6)
password: string;
@ApiProperty({ description: '年龄', example: 25, required: false })
@IsOptional()
@IsNumber()
age?: number;
}
```
#### 2. Update DTO更新数据
用于更新资源时的数据验证:
```typescript
// dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
// 方式一:使用 PartialType推荐
export class UpdateUserDto extends PartialType(CreateUserDto) {}
// 方式二:手动定义(更灵活)
// export class UpdateUserDto {
// @IsOptional()
// @IsString()
// username?: string;
//
// @IsOptional()
// @IsEmail()
// email?: string;
// }
```
#### 3. Query DTO查询参数
用于查询列表时的参数验证:
```typescript
// dto/query-user.dto.ts
import { IsOptional, IsNumber, Min, Max } from 'class-validator';
import { Type } from 'class-transformer';
export class QueryUserDto {
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page?: number = 1;
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
@Max(100)
limit?: number = 10;
@IsOptional()
@IsString()
search?: string;
@IsOptional()
@IsString()
sortBy?: string = 'createdAt';
@IsOptional()
@IsString()
sortOrder?: 'ASC' | 'DESC' = 'DESC';
}
```
#### 4. Response DTO响应数据
用于定义 API 响应的数据结构:
```typescript
// dto/user-response.dto.ts
import { ApiProperty } from '@nestjs/swagger';
export class UserResponseDto {
@ApiProperty({ description: '用户ID', example: 1 })
id: number;
@ApiProperty({ description: '用户名', example: 'john' })
username: string;
@ApiProperty({ description: '邮箱', example: 'john@example.com' })
email: string;
@ApiProperty({ description: '创建时间', example: '2024-01-01T00:00:00Z' })
createdAt: Date;
@ApiProperty({ description: '更新时间', example: '2024-01-01T00:00:00Z' })
updatedAt: Date;
}
```
### 在 Controller 中使用 DTO
```typescript
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Query,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { QueryUserDto } from './dto/query-user.dto';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
@Get()
findAll(@Query() queryUserDto: QueryUserDto) {
return this.userService.findAll(queryUserDto);
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.userService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.userService.update(+id, updateUserDto);
}
}
```
### 启用全局验证管道
`main.ts` 中启用全局验证:
```typescript
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 启用全局验证管道
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // 自动删除不在 DTO 中的属性
forbidNonWhitelisted: true, // 如果请求包含未定义的属性,抛出错误
transform: true, // 自动转换类型(如字符串转数字)
transformOptions: {
enableImplicitConversion: true,
},
}),
);
await app.listen(3000);
}
```
### 常用验证装饰器
```typescript
import {
IsString, // 字符串
IsNumber, // 数字
IsBoolean, // 布尔值
IsEmail, // 邮箱
IsUrl, // URL
IsDate, // 日期
IsOptional, // 可选字段
IsNotEmpty, // 非空
MinLength, // 最小长度
MaxLength, // 最大长度
Min, // 最小值
Max, // 最大值
IsEnum, // 枚举
IsArray, // 数组
IsObject, // 对象
ValidateNested, // 嵌套对象验证
IsUUID, // UUID
Matches, // 正则匹配
} from 'class-validator';
```
### DTO 示例:完整的用户模块
```typescript
// dto/create-user.dto.ts
import {
IsString,
IsEmail,
IsOptional,
MinLength,
IsNumber,
Min,
Max,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({ description: '用户名', example: 'john' })
@IsString()
@MinLength(3)
@MaxLength(20)
username: string;
@ApiProperty({ description: '邮箱', example: 'john@example.com' })
@IsEmail()
email: string;
@ApiProperty({ description: '密码', example: 'password123' })
@IsString()
@MinLength(6)
password: string;
@ApiProperty({ description: '年龄', example: 25, required: false })
@IsOptional()
@IsNumber()
@Min(18)
@Max(100)
age?: number;
}
```
```typescript
// dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
```
```typescript
// dto/query-user.dto.ts
import { IsOptional, IsNumber, IsString, Min, Max } from 'class-validator';
import { Type } from 'class-transformer';
export class QueryUserDto {
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page?: number = 1;
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
@Max(100)
limit?: number = 10;
@IsOptional()
@IsString()
search?: string;
}
```
### 安装必要的依赖
```bash
# 安装验证库
pnpm add class-validator class-transformer
# 如果使用 Swagger可选
pnpm add @nestjs/swagger swagger-ui-express
```
### DTO vs Entity
| 特性 | DTO | Entity |
| -------- | ---------------------- | ------------------- |
| **用途** | 数据传输和验证 | 数据库模型 |
| **位置** | `dto/` 目录 | `entities/` 目录 |
| **验证** | 使用 `class-validator` | 使用 TypeORM 装饰器 |
| **暴露** | 暴露给 API 客户端 | 不直接暴露 |
| **示例** | `CreateUserDto` | `User` entity |
**最佳实践:**
- ✅ Entity 包含数据库字段(如 `id`, `createdAt`
- ✅ DTO 只包含客户端需要传递的字段
- ✅ 使用 DTO 转换 Entity避免暴露敏感信息
### DTO 转换示例
```typescript
// service 中使用
@Injectable()
export class UserService {
async create(createUserDto: CreateUserDto): Promise<User> {
// DTO → Entity
const user = this.userRepository.create({
username: createUserDto.username,
email: createUserDto.email,
// 不直接传递 password需要加密
passwordHash: await this.hashPassword(createUserDto.password),
});
return this.userRepository.save(user);
}
async findAll(queryDto: QueryUserDto): Promise<User[]> {
// 使用 DTO 中的查询参数
return this.userRepository.find({
skip: (queryDto.page - 1) * queryDto.limit,
take: queryDto.limit,
});
}
}
```
## 最佳实践
1.**使用 `resource` 命令生成完整模块**
2.**按功能模块组织文件**
3.**每个模块包含module、controller、service、entity**
4.**DTO 文件放在模块目录下的 `dto/` 子目录**
5.**为每个操作创建对应的 DTOCreate、Update、Query、Response**
6.**使用 `class-validator` 进行数据验证**
7.**使用 `PartialType` 创建 Update DTO**
8.**启用全局验证管道**
9.**区分 DTO 和 Entity不要混用**
10.**公共组件放在 `common/` 目录**
11.**避免使用扁平结构**
12.**避免按类型组织文件**
13.**避免在 Entity 中直接暴露给 API**

View File

@@ -28,6 +28,7 @@ export default tseslint.config(
},
{
rules: {
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',

View File

@@ -1,76 +1,90 @@
{
"name": "api",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/typeorm": "^11.0.0",
"pg": "^8.16.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.27"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/pg": "^8.15.6",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^30.0.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
"name": "api",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"test:integration": "jest --config ./test/jest-integration.json"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/mapped-types": "^2.1.0",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.2",
"@nestjs/typeorm": "^11.0.0",
"body-parser": "^2.2.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"compression": "^1.8.1",
"express-rate-limit": "^8.2.1",
"helmet": "^8.1.0",
"pg": "^8.16.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.27"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/body-parser": "^1.19.6",
"@types/compression": "^1.8.1",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/pg": "^8.15.6",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^30.0.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testMatch": [
"**/__tests__/**/*.spec.ts",
"**/*.spec.ts"
],
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

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

View File

@@ -1,6 +1,8 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './database/database.module';
import { CoreModule } from './core/core.module';
import { BrokerModule } from './modules/broker/broker.module';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@@ -13,7 +15,9 @@ import { AppService } from './app.service';
'.env',
],
}),
CoreModule,
DatabaseModule,
BrokerModule,
],
controllers: [AppController],
providers: [AppService],

View File

@@ -0,0 +1,96 @@
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Response } from 'express';
export interface ErrorResponse {
code: number;
message: string;
data: null;
timestamp: string;
errors?: string[] | Record<string, unknown>;
}
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Internal server error';
let code = -1; // 默认错误码
let errors: string[] | Record<string, unknown> | null = null;
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
if (typeof exceptionResponse === 'string') {
message = exceptionResponse;
} else if (
typeof exceptionResponse === 'object' &&
exceptionResponse !== null
) {
const responseObj = exceptionResponse as {
message?: string | string[];
errors?: string[] | Record<string, unknown>;
code?: number;
};
message =
typeof responseObj.message === 'string'
? responseObj.message
: Array.isArray(responseObj.message)
? responseObj.message.join(', ')
: message;
errors = responseObj.errors || null;
// 如果有自定义的 code使用它
if (responseObj.code !== undefined) {
code = responseObj.code;
} else {
// 根据 HTTP 状态码设置业务错误码
code = this.getBusinessCode(status);
}
}
} else if (exception instanceof Error) {
message = exception.message;
}
const errorResponse: ErrorResponse = {
code,
message,
data: null,
timestamp: new Date().toISOString(),
};
// 如果是验证错误,添加详细错误信息
if (errors) {
errorResponse.errors = errors;
}
response.status(status).json(errorResponse);
}
/**
* 根据 HTTP 状态码返回业务错误码
*/
private getBusinessCode(httpStatus: number): number {
const codeMap: Record<number, number> = {
[HttpStatus.BAD_REQUEST]: 400, // 请求参数错误
[HttpStatus.UNAUTHORIZED]: 401, // 未授权
[HttpStatus.FORBIDDEN]: 403, // 禁止访问
[HttpStatus.NOT_FOUND]: 404, // 资源不存在
[HttpStatus.CONFLICT]: 409, // 资源冲突
[HttpStatus.UNPROCESSABLE_ENTITY]: 422, // 验证失败
[HttpStatus.TOO_MANY_REQUESTS]: 429, // 请求过多
[HttpStatus.INTERNAL_SERVER_ERROR]: 500, // 服务器错误
};
return codeMap[httpStatus] ?? -1;
}
}

View File

@@ -0,0 +1,49 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
code: 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) => {
// 如果响应已经是统一格式,直接返回
if (
data &&
typeof data === 'object' &&
'code' in data &&
'message' in data &&
'data' in data
) {
return data as Response<T>;
}
// 统一包装响应格式
// code: 0 表示业务处理成功HTTP 状态码 200 表示请求成功)
return {
code: 0,
message: 'success',
data: (data ?? null) as T,
timestamp: new Date().toISOString(),
};
}),
);
}
}

View File

@@ -0,0 +1,34 @@
import { Module, Global } from '@nestjs/common';
import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { TransformInterceptor } from '../common/interceptors/transform.interceptor';
import { HttpExceptionFilter } from '../common/filters/http-exception.filter';
@Global()
@Module({
providers: [
// 全局异常过滤器
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
// 全局响应拦截器
{
provide: APP_INTERCEPTOR,
useClass: TransformInterceptor,
},
// 全局验证管道
{
provide: APP_PIPE,
useValue: new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
},
],
})
export class CoreModule {}

View File

@@ -1,12 +1,61 @@
import { NestFactory } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module';
import compression from 'compression';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import bodyParser from 'body-parser';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
const configService = new ConfigService();
// const isProduction = configService.get('NODE_ENV') === 'production';
const app = await NestFactory.create(AppModule, {
bodyParser: false, // 禁用默认 bodyParser使用自定义配置
});
// 安全头设置(必须在其他中间件之前)
app.use(helmet());
// 速率限制
app.use(
rateLimit({
windowMs: 60 * 1000, // 1 分钟
max: 100, // 限制每个 IP 在 windowMs 时间内最多 10000 个请求
}),
);
// 请求体解析(设置大小限制为 10mb
app.use(bodyParser.json({ limit: '10mb' }));
app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }));
app.enableCors();
/* app.enableCors({
// 允许的域名
origin: ['http://localhost:3200'],
}); */
app.setGlobalPrefix('api');
// 响应压缩(在 helmet 之后)
app.use(compression());
// Swagger API 文档配置
const swaggerConfig = new DocumentBuilder()
.setTitle('Vest Mind API')
.setDescription('Vest Mind 投资管理系统 API 文档')
.setVersion('1.0')
.addTag('broker', '券商管理相关接口')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('api-docs', app, document, {
swaggerOptions: {
persistAuthorization: true, // 保持授权状态
},
});
const port = configService.get<number>('PORT', 3200);
await app.listen(port);
console.log(`Application is running on: http://localhost:${port}`);
console.log(`Swagger API Docs: http://localhost:${port}/api-docs`);
}
void bootstrap();

View File

@@ -0,0 +1,222 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BrokerController } from '../broker.controller';
import { BrokerService } from '../broker.service';
import { Broker } from '../broker.entity';
import { CreateBrokerDto } from '../dto/create-broker.dto';
import { BatchCreateBrokerDto } from '../dto/batch-create-broker.dto';
describe('BrokerController', () => {
let controller: BrokerController;
const mockBrokerService = {
create: jest.fn(),
batchCreate: jest.fn(),
findAll: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [BrokerController],
providers: [
{
provide: BrokerService,
useValue: mockBrokerService,
},
],
}).compile();
controller = module.get<BrokerController>(BrokerController);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('create', () => {
const createBrokerDto: CreateBrokerDto = {
brokerCode: 'HTZQ',
brokerName: '华泰证券',
region: 'CN',
sortOrder: 1,
isActive: true,
};
const mockBroker: Broker = {
brokerId: 1,
...createBrokerDto,
createdAt: new Date(),
updatedAt: new Date(),
};
it('应该成功创建券商并返回 201 状态码', async () => {
mockBrokerService.create.mockResolvedValue(mockBroker);
const result = await controller.create(createBrokerDto);
expect(result).toEqual(mockBroker);
expect(mockBrokerService.create).toHaveBeenCalledWith(
createBrokerDto,
);
expect(mockBrokerService.create).toHaveBeenCalledTimes(1);
});
it('应该正确传递 DTO 到服务层', async () => {
const dto: CreateBrokerDto = {
brokerCode: 'ZSZQ',
brokerName: '招商证券',
region: 'CN',
};
const mockResult: Broker = {
brokerId: 2,
...dto,
sortOrder: 0,
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
};
mockBrokerService.create.mockResolvedValue(mockResult);
await controller.create(dto);
expect(mockBrokerService.create).toHaveBeenCalledWith(dto);
expect(mockBrokerService.create).toHaveBeenCalledWith(
expect.objectContaining({
brokerCode: 'ZSZQ',
brokerName: '招商证券',
region: 'CN',
}),
);
});
});
describe('batchCreate', () => {
const batchCreateDto: BatchCreateBrokerDto = {
brokers: [
{
brokerCode: 'HTZQ',
brokerName: '华泰证券',
region: 'CN',
sortOrder: 1,
},
{
brokerCode: 'ZSZQ',
brokerName: '招商证券',
region: 'CN',
sortOrder: 2,
},
],
};
const mockBrokers: Broker[] = [
{
brokerId: 1,
...batchCreateDto.brokers[0],
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
},
{
brokerId: 2,
...batchCreateDto.brokers[1],
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
},
];
it('应该成功批量创建券商并返回 201 状态码', async () => {
mockBrokerService.batchCreate.mockResolvedValue(mockBrokers);
const result = await controller.batchCreate(batchCreateDto);
expect(result).toEqual(mockBrokers);
expect(result).toHaveLength(2);
expect(mockBrokerService.batchCreate).toHaveBeenCalledWith(
batchCreateDto,
);
expect(mockBrokerService.batchCreate).toHaveBeenCalledTimes(1);
});
it('应该正确传递批量 DTO 到服务层', async () => {
const dto: BatchCreateBrokerDto = {
brokers: [
{
brokerCode: 'HTZQ',
brokerName: '华泰证券',
region: 'CN',
},
],
};
const mockResult: Broker[] = [
{
brokerId: 1,
...dto.brokers[0],
sortOrder: 0,
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
},
];
mockBrokerService.batchCreate.mockResolvedValue(mockResult);
await controller.batchCreate(dto);
expect(mockBrokerService.batchCreate).toHaveBeenCalledWith(dto);
// 验证调用了 batchCreate并且参数包含正确的数据
expect(mockBrokerService.batchCreate).toHaveBeenCalledTimes(1);
});
it('应该处理空数组的情况', async () => {
const emptyDto: BatchCreateBrokerDto = {
brokers: [],
};
mockBrokerService.batchCreate.mockResolvedValue([]);
const result = await controller.batchCreate(emptyDto);
expect(result).toEqual([]);
expect(result).toHaveLength(0);
expect(mockBrokerService.batchCreate).toHaveBeenCalledWith(
emptyDto,
);
});
it('应该处理大量券商批量创建', async () => {
const largeBatchDto: BatchCreateBrokerDto = {
brokers: Array.from({ length: 10 }, (_, i) => ({
brokerCode: `CODE${i}`,
brokerName: `券商${i}`,
region: 'CN',
})),
};
const mockLargeResult: Broker[] = largeBatchDto.brokers.map(
(broker, i) => ({
brokerId: i + 1,
...broker,
sortOrder: 0,
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
}),
);
mockBrokerService.batchCreate.mockResolvedValue(mockLargeResult);
const result = await controller.batchCreate(largeBatchDto);
expect(result).toHaveLength(10);
expect(mockBrokerService.batchCreate).toHaveBeenCalledWith(
largeBatchDto,
);
});
});
});

View File

@@ -0,0 +1,349 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ConflictException } from '@nestjs/common';
import { BrokerService } from '../broker.service';
import { Broker } from '../broker.entity';
import { CreateBrokerDto } from '../dto/create-broker.dto';
import { BatchCreateBrokerDto } from '../dto/batch-create-broker.dto';
describe('BrokerService', () => {
let service: BrokerService;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let repository: Repository<Broker>;
const mockRepository = {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
remove: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
BrokerService,
{
provide: getRepositoryToken(Broker),
useValue: mockRepository,
},
],
}).compile();
service = module.get<BrokerService>(BrokerService);
repository = module.get<Repository<Broker>>(getRepositoryToken(Broker));
});
afterEach(() => {
jest.clearAllMocks();
});
describe('create', () => {
const createBrokerDto: CreateBrokerDto = {
brokerCode: 'HTZQ',
brokerName: '华泰证券',
region: 'CN',
sortOrder: 1,
isActive: true,
};
const mockBroker: Broker = {
brokerId: 1,
...createBrokerDto,
createdAt: new Date(),
updatedAt: new Date(),
};
it('应该成功创建一个券商', async () => {
// 模拟数据库中不存在相同 code 和 region 的券商
mockRepository.findOne.mockResolvedValue(null);
mockRepository.create.mockReturnValue(mockBroker);
mockRepository.save.mockResolvedValue(mockBroker);
const result = await service.create(createBrokerDto);
expect(result).toEqual(mockBroker);
expect(mockRepository.findOne).toHaveBeenCalledTimes(2); // 检查 code 和 name
expect(mockRepository.findOne).toHaveBeenCalledWith({
where: {
brokerCode: createBrokerDto.brokerCode,
region: createBrokerDto.region,
},
});
expect(mockRepository.create).toHaveBeenCalledWith({
...createBrokerDto,
sortOrder: 1,
isActive: true,
});
expect(mockRepository.save).toHaveBeenCalledWith(mockBroker);
});
it('应该使用默认值当 sortOrder 和 isActive 未提供时', async () => {
const dtoWithoutDefaults: CreateBrokerDto = {
brokerCode: 'ZSZQ',
brokerName: '招商证券',
region: 'CN',
};
const mockBrokerWithDefaults: Broker = {
brokerId: 2,
...dtoWithoutDefaults,
sortOrder: 0,
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
};
mockRepository.findOne.mockResolvedValue(null);
mockRepository.create.mockReturnValue(mockBrokerWithDefaults);
mockRepository.save.mockResolvedValue(mockBrokerWithDefaults);
const result = await service.create(dtoWithoutDefaults);
expect(result.sortOrder).toBe(0);
expect(result.isActive).toBe(true);
expect(mockRepository.create).toHaveBeenCalledWith({
...dtoWithoutDefaults,
sortOrder: 0,
isActive: true,
});
});
it('应该抛出 ConflictException 当 broker_code 已存在时', async () => {
const existingBroker: Broker = {
brokerId: 1,
brokerCode: 'HTZQ',
brokerName: '华泰证券',
region: 'CN',
sortOrder: 0,
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
};
mockRepository.findOne.mockResolvedValueOnce(existingBroker);
await expect(service.create(createBrokerDto)).rejects.toThrow(
ConflictException,
);
expect(mockRepository.save).not.toHaveBeenCalled();
});
it('应该抛出 ConflictException 当 broker_name 已存在时', async () => {
mockRepository.findOne
.mockResolvedValueOnce(null) // 第一次检查 code不存在
.mockResolvedValueOnce({
// 第二次检查 name已存在
brokerId: 1,
brokerCode: 'OTHER',
brokerName: '华泰证券',
region: 'CN',
sortOrder: 0,
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
});
await expect(service.create(createBrokerDto)).rejects.toThrow(
ConflictException,
);
expect(mockRepository.save).not.toHaveBeenCalled();
});
});
describe('batchCreate', () => {
const batchCreateDto: BatchCreateBrokerDto = {
brokers: [
{
brokerCode: 'HTZQ',
brokerName: '华泰证券',
region: 'CN',
sortOrder: 1,
},
{
brokerCode: 'ZSZQ',
brokerName: '招商证券',
region: 'CN',
sortOrder: 2,
},
],
};
const mockBrokers: Broker[] = [
{
brokerId: 1,
...batchCreateDto.brokers[0],
brokerImage: 'https://example.com/broker1.jpg',
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
},
{
brokerId: 2,
...batchCreateDto.brokers[1],
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
},
];
it('应该成功批量创建券商', async () => {
// 模拟数据库中不存在这些券商
mockRepository.find.mockResolvedValue([]);
mockRepository.create
.mockReturnValueOnce(mockBrokers[0])
.mockReturnValueOnce(mockBrokers[1]);
mockRepository.save.mockResolvedValue(mockBrokers);
const result = await service.batchCreate(batchCreateDto);
expect(result).toEqual(mockBrokers);
expect(result).toHaveLength(2);
expect(mockRepository.create).toHaveBeenCalledTimes(2);
expect(mockRepository.save).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
brokerCode: 'HTZQ',
}),
expect.objectContaining({
brokerCode: 'ZSZQ',
}),
]),
);
});
it('应该为每个券商设置默认值', async () => {
const dtoWithoutDefaults: BatchCreateBrokerDto = {
brokers: [
{
brokerCode: 'HTZQ',
brokerName: '华泰证券',
region: 'CN',
},
],
};
const mockBroker: Broker = {
brokerId: 1,
...dtoWithoutDefaults.brokers[0],
sortOrder: 0,
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
};
mockRepository.find.mockResolvedValue([]);
mockRepository.create.mockReturnValue(mockBroker);
mockRepository.save.mockResolvedValue([mockBroker]);
const result = await service.batchCreate(dtoWithoutDefaults);
expect(result[0].sortOrder).toBe(0);
expect(result[0].isActive).toBe(true);
});
it('应该抛出 ConflictException 当批量数据中有已存在的券商时', async () => {
const existingBroker: Broker = {
brokerId: 1,
brokerCode: 'HTZQ',
brokerName: '华泰证券',
region: 'CN',
sortOrder: 0,
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
};
mockRepository.find.mockResolvedValue([existingBroker]);
await expect(service.batchCreate(batchCreateDto)).rejects.toThrow(
ConflictException,
);
await expect(service.batchCreate(batchCreateDto)).rejects.toThrow(
expect.stringContaining('already exist'),
);
expect(mockRepository.save).not.toHaveBeenCalled();
});
it('应该抛出 ConflictException 当批量数据内部有重复的 code+region 组合时', async () => {
const duplicateDto: BatchCreateBrokerDto = {
brokers: [
{
brokerCode: 'HTZQ',
brokerName: '华泰证券',
region: 'CN',
},
{
brokerCode: 'HTZQ', // 重复的 code
brokerName: '华泰证券2',
region: 'CN', // 相同的 region
},
],
};
mockRepository.find.mockResolvedValue([]);
await expect(service.batchCreate(duplicateDto)).rejects.toThrow(
ConflictException,
);
await expect(service.batchCreate(duplicateDto)).rejects.toThrow(
'Duplicate broker_code and region combinations in batch data',
);
expect(mockRepository.save).not.toHaveBeenCalled();
});
it('应该成功创建不同地区的相同 code', async () => {
const differentRegionDto: BatchCreateBrokerDto = {
brokers: [
{
brokerCode: 'HTZQ',
brokerName: '华泰证券',
region: 'CN',
},
{
brokerCode: 'HTZQ', // 相同的 code
brokerName: 'Huatai Securities',
region: 'US', // 不同的 region
},
],
};
const mockBrokersDifferentRegion: Broker[] = [
{
brokerId: 1,
...differentRegionDto.brokers[0],
sortOrder: 0,
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
},
{
brokerId: 2,
...differentRegionDto.brokers[1],
sortOrder: 0,
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
},
];
mockRepository.find.mockResolvedValue([]);
mockRepository.create
.mockReturnValueOnce(mockBrokersDifferentRegion[0])
.mockReturnValueOnce(mockBrokersDifferentRegion[1]);
mockRepository.save.mockResolvedValue(mockBrokersDifferentRegion);
const result = await service.batchCreate(differentRegionDto);
expect(result).toHaveLength(2);
expect(result[0].region).toBe('CN');
expect(result[1].region).toBe('US');
expect(mockRepository.save).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,141 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
import { BrokerService } from './broker.service';
import { CreateBrokerDto } from './dto/create-broker.dto';
import { UpdateBrokerDto } from './dto/update-broker.dto';
import { QueryBrokerDto } from './dto/query-broker.dto';
import { BatchCreateBrokerDto } from './dto/batch-create-broker.dto';
import { Broker } from './broker.entity';
@ApiTags('broker')
@Controller('broker')
export class BrokerController {
constructor(private readonly brokerService: BrokerService) {}
/**
* 单独创建 broker
*/
@Post()
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: '创建券商', description: '创建单个券商信息' })
@ApiResponse({
status: 201,
description: '创建成功',
type: Broker,
})
@ApiResponse({ status: 400, description: '请求参数错误' })
@ApiResponse({ status: 409, description: '券商代码或名称已存在' })
create(@Body() createBrokerDto: CreateBrokerDto): Promise<Broker> {
return this.brokerService.create(createBrokerDto);
}
/**
* 批量创建 broker
*/
@Post('batch')
@HttpCode(HttpStatus.CREATED)
@ApiOperation({
summary: '批量创建券商',
description: '一次性创建多个券商信息',
})
@ApiResponse({
status: 201,
description: '批量创建成功',
type: [Broker],
})
@ApiResponse({ status: 400, description: '请求参数错误' })
@ApiResponse({ status: 409, description: '存在重复的券商代码或名称' })
batchCreate(
@Body() batchCreateBrokerDto: BatchCreateBrokerDto,
): Promise<Broker[]> {
return this.brokerService.batchCreate(batchCreateBrokerDto);
}
/**
* 查询 broker支持多种查询条件
* 支持按 broker_id、broker_code、broker_name、region 查询
* 返回一个或多个 broker
*/
@Get()
@ApiOperation({
summary: '查询券商列表',
description: '支持按多个条件查询券商,支持分页和排序',
})
@ApiResponse({
status: 200,
description: '查询成功',
type: [Broker],
})
findAll(@Query() queryDto: QueryBrokerDto): Promise<Broker[]> {
return this.brokerService.findAll(queryDto);
}
/**
* 根据 ID 查询单个 broker
*/
@Get(':id')
@ApiOperation({
summary: '根据ID查询券商',
description: '根据券商ID获取详细信息',
})
@ApiParam({ name: 'id', description: '券商ID', type: Number })
@ApiResponse({
status: 200,
description: '查询成功',
type: Broker,
})
@ApiResponse({ status: 404, description: '券商不存在' })
findOne(@Param('id') id: string): Promise<Broker> {
return this.brokerService.findOne(+id);
}
/**
* 更新 broker
*/
@Patch(':id')
@ApiOperation({
summary: '更新券商',
description: '更新券商的部分或全部信息',
})
@ApiParam({ name: 'id', description: '券商ID', type: Number })
@ApiResponse({
status: 200,
description: '更新成功',
type: Broker,
})
@ApiResponse({ status: 404, description: '券商不存在' })
@ApiResponse({ status: 409, description: '更新后的券商代码或名称已存在' })
update(
@Param('id') id: string,
@Body() updateBrokerDto: UpdateBrokerDto,
): Promise<Broker> {
return this.brokerService.update(+id, updateBrokerDto);
}
/**
* 删除 broker
*/
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: '删除券商',
description: '根据券商ID删除券商信息',
})
@ApiParam({ name: 'id', description: '券商ID', type: Number })
@ApiResponse({ status: 204, description: '删除成功' })
@ApiResponse({ status: 404, description: '券商不存在' })
remove(@Param('id') id: string): Promise<void> {
return this.brokerService.remove(+id);
}
}

View File

@@ -0,0 +1,96 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
Index,
} from 'typeorm';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
@Entity('broker')
export class Broker {
@ApiProperty({ description: '券商ID', example: 1 })
@PrimaryGeneratedColumn({ name: 'broker_id' })
brokerId: number;
@ApiProperty({
description: '券商代码',
example: 'HTZQ',
maxLength: 50,
})
@Column({ name: 'broker_code', type: 'varchar', length: 50 })
@Index()
brokerCode: string;
@ApiProperty({
description: '券商名称',
example: '华泰证券',
maxLength: 100,
})
@Column({ name: 'broker_name', type: 'varchar', length: 100 })
@Index()
brokerName: string;
@ApiPropertyOptional({
description: '券商图片地址',
example: 'https://example.com/broker-image.jpg',
maxLength: 200,
})
@Column({
name: 'broker_image',
type: 'varchar',
length: 200,
nullable: true,
})
brokerImage?: string;
@ApiProperty({
description: '地区/国家代码',
example: 'CN',
enum: ['CN', 'US', 'HK', 'SG', 'JP', 'UK', 'AU', 'CA', 'OTHER'],
default: 'CN',
})
@Column({ name: 'region', type: 'varchar', length: 50, default: 'CN' })
region: string;
@ApiPropertyOptional({
description: '排序顺序',
example: 1,
default: 0,
})
@Column({
name: 'sort_order',
type: 'integer',
default: 0,
nullable: false,
})
sortOrder?: number; // 可选,数据库有默认值
@ApiPropertyOptional({
description: '是否启用',
example: true,
default: true,
})
@Column({
name: 'is_active',
type: 'boolean',
default: true,
nullable: false,
})
isActive?: boolean; // 可选,数据库有默认值
@ApiProperty({
description: '创建时间',
example: '2024-01-01T00:00:00.000Z',
})
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@ApiProperty({
description: '更新时间',
example: '2024-01-01T00:00:00.000Z',
})
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
}

View File

@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BrokerService } from './broker.service';
import { BrokerController } from './broker.controller';
import { Broker } from './broker.entity';
@Module({
imports: [TypeOrmModule.forFeature([Broker])],
controllers: [BrokerController],
providers: [BrokerService],
exports: [BrokerService],
})
export class BrokerModule {}

View File

@@ -0,0 +1,256 @@
import {
Injectable,
NotFoundException,
ConflictException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOptionsWhere } from 'typeorm';
import { Broker } from './broker.entity';
import { CreateBrokerDto } from './dto/create-broker.dto';
import { UpdateBrokerDto } from './dto/update-broker.dto';
import { QueryBrokerDto } from './dto/query-broker.dto';
import { BatchCreateBrokerDto } from './dto/batch-create-broker.dto';
@Injectable()
export class BrokerService {
constructor(
@InjectRepository(Broker)
private readonly brokerRepository: Repository<Broker>,
) {}
/**
* 单独创建 broker
*/
async create(createBrokerDto: CreateBrokerDto): Promise<Broker> {
// 检查同一地区的 broker_code 是否已存在
const existingByCode = await this.brokerRepository.findOne({
where: {
brokerCode: createBrokerDto.brokerCode,
region: createBrokerDto.region,
},
});
if (existingByCode) {
throw new ConflictException(
`Broker with code "${createBrokerDto.brokerCode}" already exists in region "${createBrokerDto.region}"`,
);
}
// 检查同一地区的 broker_name 是否已存在
const existingByName = await this.brokerRepository.findOne({
where: {
brokerName: createBrokerDto.brokerName,
region: createBrokerDto.region,
},
});
if (existingByName) {
throw new ConflictException(
`Broker with name "${createBrokerDto.brokerName}" already exists in region "${createBrokerDto.region}"`,
);
}
const broker = this.brokerRepository.create({
...createBrokerDto,
sortOrder: createBrokerDto.sortOrder ?? 0,
isActive: createBrokerDto.isActive ?? true,
});
return this.brokerRepository.save(broker);
}
/**
* 批量创建 broker
*/
async batchCreate(
batchCreateBrokerDto: BatchCreateBrokerDto,
): Promise<Broker[]> {
const brokers = batchCreateBrokerDto.brokers.map((dto) =>
this.brokerRepository.create({
...dto,
sortOrder: dto.sortOrder ?? 0,
isActive: dto.isActive ?? true,
}),
);
// 检查是否有重复的 broker_code + region 组合
const codeRegionPairs = brokers.map((b) => ({
brokerCode: b.brokerCode,
region: b.region,
}));
const existingBrokers = await this.brokerRepository.find({
where: codeRegionPairs.map((pair) => ({
brokerCode: pair.brokerCode,
region: pair.region,
})),
});
if (existingBrokers.length > 0) {
const conflicts = existingBrokers.map(
(b) => `${b.brokerCode} in ${b.region}`,
);
throw new ConflictException(
`The following brokers already exist: ${conflicts.join(', ')}`,
);
}
// 检查批量数据内部是否有重复
const uniquePairs = new Set(
codeRegionPairs.map((p) => `${p.brokerCode}-${p.region}`),
);
if (uniquePairs.size !== codeRegionPairs.length) {
throw new ConflictException(
'Duplicate broker_code and region combinations in batch data',
);
}
return this.brokerRepository.save(brokers);
}
/**
* 查询 broker支持多种查询条件
*/
async findAll(queryDto: QueryBrokerDto): Promise<Broker[]> {
const where: FindOptionsWhere<Broker> = {};
if (queryDto.brokerId) {
where.brokerId = queryDto.brokerId;
}
if (queryDto.brokerCode) {
where.brokerCode = queryDto.brokerCode;
}
if (queryDto.brokerName) {
where.brokerName = queryDto.brokerName;
}
if (queryDto.region) {
where.region = queryDto.region;
}
if (queryDto.isActive !== undefined) {
where.isActive = queryDto.isActive;
}
return this.brokerRepository.find({
where,
order: {
sortOrder: 'ASC',
brokerId: 'ASC',
},
});
}
/**
* 根据 ID 查询单个 broker
*/
async findOne(id: number): Promise<Broker> {
const broker = await this.brokerRepository.findOne({
where: { brokerId: id },
});
if (!broker) {
throw new NotFoundException(`Broker with ID ${id} not found`);
}
return broker;
}
/**
* 根据条件查询单个 broker返回第一个匹配的
*/
async findOneByCondition(queryDto: QueryBrokerDto): Promise<Broker> {
const where: FindOptionsWhere<Broker> = {};
if (queryDto.brokerId) {
where.brokerId = queryDto.brokerId;
}
if (queryDto.brokerCode) {
where.brokerCode = queryDto.brokerCode;
}
if (queryDto.brokerName) {
where.brokerName = queryDto.brokerName;
}
if (queryDto.region) {
where.region = queryDto.region;
}
if (queryDto.isActive !== undefined) {
where.isActive = queryDto.isActive;
}
const broker = await this.brokerRepository.findOne({ where });
if (!broker) {
throw new NotFoundException(
'Broker not found with the given conditions',
);
}
return broker;
}
/**
* 更新 broker
*/
async update(
id: number,
updateBrokerDto: UpdateBrokerDto,
): Promise<Broker> {
const broker = await this.findOne(id);
// 如果更新 broker_code 或 region检查是否冲突
if ('brokerCode' in updateBrokerDto || 'region' in updateBrokerDto) {
const newCode = updateBrokerDto.brokerCode ?? broker.brokerCode;
const newRegion = updateBrokerDto.region ?? broker.region;
const existing = await this.brokerRepository.findOne({
where: {
brokerCode: newCode,
region: newRegion,
},
});
if (existing && existing.brokerId !== id) {
throw new ConflictException(
`Broker with code "${newCode}" already exists in region "${newRegion}"`,
);
}
}
// 如果更新 broker_name 或 region检查是否冲突
if ('brokerName' in updateBrokerDto || 'region' in updateBrokerDto) {
const newName = updateBrokerDto.brokerName ?? broker.brokerName;
const newRegion = updateBrokerDto.region ?? broker.region;
const existing = await this.brokerRepository.findOne({
where: {
brokerName: newName,
region: newRegion,
},
});
if (existing && existing.brokerId !== id) {
throw new ConflictException(
`Broker with name "${newName}" already exists in region "${newRegion}"`,
);
}
}
Object.assign(broker, updateBrokerDto);
return this.brokerRepository.save(broker);
}
/**
* 删除 broker
*/
async remove(id: number): Promise<void> {
const broker = await this.findOne(id);
await this.brokerRepository.remove(broker);
}
}

View File

@@ -0,0 +1,165 @@
import { CreateBrokerDto } from '../dto/create-broker.dto';
/**
* 主要券商配置数据
* 包含A股、港股和美股的主要券商信息
*/
export const brokersConfig: CreateBrokerDto[] = [
// A股券商
{
brokerCode: 'HTZQ',
brokerName: '华泰证券',
region: 'CN',
sortOrder: 1,
isActive: true,
},
{
brokerCode: 'ZSZQ',
brokerName: '招商证券',
region: 'CN',
sortOrder: 2,
isActive: true,
},
{
brokerCode: 'GTJA',
brokerName: '国泰君安',
region: 'CN',
sortOrder: 3,
isActive: true,
},
{
brokerCode: 'ZXZQ',
brokerName: '中信证券',
region: 'CN',
sortOrder: 4,
isActive: true,
},
{
brokerCode: 'HXZQ',
brokerName: '海通证券',
region: 'CN',
sortOrder: 5,
isActive: true,
},
{
brokerCode: 'GFZQ',
brokerName: '广发证券',
region: 'CN',
sortOrder: 6,
isActive: true,
},
{
brokerCode: 'ZJZQ',
brokerName: '中金公司',
region: 'CN',
sortOrder: 7,
isActive: true,
},
{
brokerCode: 'DFZQ',
brokerName: '东方证券',
region: 'CN',
sortOrder: 8,
isActive: true,
},
{
brokerCode: 'XZQ',
brokerName: '兴业证券',
region: 'CN',
sortOrder: 9,
isActive: true,
},
{
brokerCode: 'SWZQ',
brokerName: '申万宏源',
region: 'CN',
sortOrder: 10,
isActive: true,
},
// 港股券商 从21开始
{
brokerCode: 'FUTU',
brokerName: '富途证券',
region: 'HK',
sortOrder: 21,
isActive: true,
},
{
brokerCode: 'TIGER',
brokerName: '老虎证券',
region: 'HK',
sortOrder: 22,
isActive: true,
},
{
brokerCode: 'HSBC',
brokerName: '汇丰银行',
region: 'HK',
sortOrder: 23,
isActive: true,
},
{
brokerCode: 'CITIC',
brokerName: '中信里昂',
region: 'HK',
sortOrder: 24,
isActive: true,
},
{
brokerCode: 'UBS',
brokerName: '瑞银证券',
region: 'HK',
sortOrder: 25,
isActive: true,
},
// 美股券商 从31开始
{
brokerCode: 'IBKR',
brokerName: 'Interactive Brokers',
region: 'US',
sortOrder: 31,
isActive: true,
},
{
brokerCode: 'SCHWAB',
brokerName: 'Charles Schwab',
region: 'US',
sortOrder: 32,
isActive: true,
},
{
brokerCode: 'FIDELITY',
brokerName: 'Fidelity',
region: 'US',
sortOrder: 33,
isActive: true,
},
{
brokerCode: 'TD',
brokerName: 'TD Ameritrade',
region: 'US',
sortOrder: 34,
isActive: true,
},
{
brokerCode: 'ETRADE',
brokerName: 'E*TRADE',
region: 'US',
sortOrder: 35,
isActive: true,
},
{
brokerCode: 'ROBINHOOD',
brokerName: 'Robinhood',
region: 'US',
sortOrder: 36,
isActive: true,
},
{
brokerCode: 'WEBULL',
brokerName: 'Webull',
region: 'US',
sortOrder: 37,
isActive: true,
},
];

View File

@@ -0,0 +1,29 @@
import { IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
import { CreateBrokerDto } from './create-broker.dto';
export class BatchCreateBrokerDto {
@ApiProperty({
description: '券商列表',
type: [CreateBrokerDto],
example: [
{
brokerCode: 'HTZQ',
brokerName: '华泰证券',
region: 'CN',
sortOrder: 1,
},
{
brokerCode: 'ZSZQ',
brokerName: '招商证券',
region: 'CN',
sortOrder: 2,
},
],
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateBrokerDto)
brokers: CreateBrokerDto[];
}

View File

@@ -0,0 +1,76 @@
import {
IsString,
IsNotEmpty,
IsOptional,
IsNumber,
IsBoolean,
MinLength,
MaxLength,
IsIn,
Min,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateBrokerDto {
@ApiProperty({
description: '券商代码',
example: 'HTZQ',
maxLength: 50,
})
@IsString()
@IsNotEmpty()
@MinLength(1)
@MaxLength(50)
brokerCode: string;
@ApiProperty({
description: '券商名称',
example: '华泰证券',
maxLength: 100,
})
@IsString()
@IsNotEmpty()
@MinLength(1)
@MaxLength(100)
brokerName: string;
@ApiProperty({
description: '地区/国家代码',
example: 'CN',
enum: ['CN', 'US', 'HK', 'SG', 'JP', 'UK', 'AU', 'CA', 'OTHER'],
})
@IsString()
@IsNotEmpty()
@IsIn(['CN', 'US', 'HK', 'SG', 'JP', 'UK', 'AU', 'CA', 'OTHER'])
region: string;
@ApiPropertyOptional({
description: '排序顺序',
example: 1,
minimum: 0,
default: 0,
})
@IsOptional()
@IsNumber()
@Min(0)
sortOrder?: number;
@ApiPropertyOptional({
description: '是否启用',
example: true,
default: true,
})
@IsOptional()
@IsBoolean()
isActive?: boolean;
@ApiPropertyOptional({
description: '券商图片地址',
example: 'https://example.com/broker-image.jpg',
maxLength: 200,
})
@IsOptional()
@IsString()
@MaxLength(200)
brokerImage?: string;
}

View File

@@ -0,0 +1,99 @@
import {
IsOptional,
IsString,
IsNumber,
IsBoolean,
Min,
} from 'class-validator';
import { Type } from 'class-transformer';
import { ApiPropertyOptional } from '@nestjs/swagger';
export class QueryBrokerDto {
@ApiPropertyOptional({
description: '券商ID',
example: 1,
minimum: 1,
})
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
brokerId?: number;
@ApiPropertyOptional({
description: '券商代码',
example: 'HTZQ',
})
@IsOptional()
@IsString()
brokerCode?: string;
@ApiPropertyOptional({
description: '券商名称',
example: '华泰证券',
})
@IsOptional()
@IsString()
brokerName?: string;
@ApiPropertyOptional({
description: '地区/国家代码',
example: 'CN',
enum: ['CN', 'US', 'HK', 'SG', 'JP', 'UK', 'AU', 'CA', 'OTHER'],
})
@IsOptional()
@IsString()
region?: string;
@ApiPropertyOptional({
description: '是否启用',
example: true,
})
@IsOptional()
@Type(() => Boolean)
@IsBoolean()
isActive?: boolean;
@ApiPropertyOptional({
description: '页码',
example: 1,
minimum: 1,
default: 1,
})
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page?: number = 1;
@ApiPropertyOptional({
description: '每页数量',
example: 10,
minimum: 1,
default: 10,
})
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
limit?: number = 10;
@ApiPropertyOptional({
description: '排序字段',
example: 'createdAt',
default: 'createdAt',
})
@IsOptional()
@IsString()
sortBy?: string = 'createdAt';
@ApiPropertyOptional({
description: '排序方向',
example: 'DESC',
enum: ['ASC', 'DESC'],
default: 'DESC',
})
@IsOptional()
@IsString()
sortOrder?: 'ASC' | 'DESC' = 'DESC';
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateBrokerDto } from './create-broker.dto';
export class UpdateBrokerDto extends PartialType(CreateBrokerDto) {}

View File

@@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';
@Module({})
export class UserModule {}

View File

@@ -0,0 +1,7 @@
import { User } from './user';
describe('User', () => {
it('should be defined', () => {
expect(new User()).toBeDefined();
});
});

View File

@@ -0,0 +1 @@
export class User {}

View File

@@ -0,0 +1,12 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": "..",
"testEnvironment": "node",
"testMatch": ["**/test/**/*.integration.spec.ts"],
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/src/$1"
}
}

View File

@@ -0,0 +1,240 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { AppModule } from '../../../src/app.module';
import { Broker } from '../../../src/modules/broker/broker.entity';
import { Response } from '../../../src/common/interceptors/transform.interceptor';
describe('BrokerController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
jest.setTimeout(30000);
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule], // 导入整个应用模块
}).compile();
app = moduleFixture.createNestApplication();
await app.init(); // 初始化应用(包括所有中间件、管道、拦截器等)
});
afterAll(async () => {
await app.close();
});
describe('POST /api/broker', () => {
it('应该成功创建券商(完整流程)', () => {
return request(app.getHttpServer())
.post('/api/broker')
.send({
brokerCode: 'E2E_TEST',
brokerName: 'E2E测试券商',
region: 'CN',
sortOrder: 1,
isActive: true,
})
.expect(201)
.expect((res) => {
// 验证响应格式(经过 TransformInterceptor 处理)
expect(res.body).toHaveProperty('code', 0);
expect(res.body).toHaveProperty('message', 'success');
expect(res.body).toHaveProperty('data');
expect(res.body.data).toHaveProperty(
'brokerCode',
'E2E_TEST',
);
expect(res.body.data).toHaveProperty(
'brokerName',
'E2E测试券商',
);
expect(res.body.data).toHaveProperty('brokerId');
});
});
it('应该返回 400 当数据验证失败时', () => {
return request(app.getHttpServer())
.post('/api/broker')
.send({
// 缺少必需字段
brokerCode: 'TEST',
})
.expect(400)
.expect((res) => {
// 验证错误响应格式(经过 HttpExceptionFilter 处理)
expect(res.body).toHaveProperty('code');
expect(res.body).toHaveProperty('message');
expect(res.body.code).not.toBe(0);
});
});
it('应该返回 409 当券商代码已存在时', async () => {
// 先创建一个券商
await request(app.getHttpServer())
.post('/api/broker')
.send({
brokerCode: 'DUPLICATE_TEST',
brokerName: '重复测试',
region: 'CN',
})
.expect(201);
// 尝试创建重复的券商
return request(app.getHttpServer())
.post('/api/broker')
.send({
brokerCode: 'DUPLICATE_TEST',
brokerName: '重复测试2',
region: 'CN',
})
.expect(409)
.expect((res) => {
expect(res.body.code).toBe(409);
expect(res.body.message).toContain('already exists');
});
});
});
describe('GET /api/broker', () => {
it('应该返回券商列表', async () => {
// 先创建一个券商
const createResponse = await request(app.getHttpServer())
.post('/api/broker')
.send({
brokerCode: 'GET_TEST',
brokerName: '查询测试',
region: 'CN',
})
.expect(201);
// 查询券商列表
return request(app.getHttpServer())
.get('/api/broker')
.expect(200)
.expect((res) => {
expect(res.body).toHaveProperty('code', 0);
expect(res.body).toHaveProperty('data');
expect(Array.isArray(res.body.data)).toBe(true);
// 验证创建的券商在列表中
const found = res.body.data.find(
(b: { brokerCode: string }) =>
b.brokerCode === 'GET_TEST',
);
expect(found).toBeDefined();
});
});
it('应该支持按条件查询', async () => {
// 先创建一个券商
await request(app.getHttpServer())
.post('/api/broker')
.send({
brokerCode: 'QUERY_TEST',
brokerName: '条件查询测试',
region: 'CN',
})
.expect(201);
// 按 brokerCode 查询
return request(app.getHttpServer())
.get('/api/broker?brokerCode=QUERY_TEST')
.expect(200)
.expect((res) => {
expect(res.body.code).toBe(0);
expect(res.body.data).toHaveLength(1);
expect(res.body.data[0].brokerCode).toBe('QUERY_TEST');
});
});
});
describe('GET /api/broker/:id', () => {
it('应该返回单个券商', async () => {
// 先创建一个券商
const createResponse = await request(app.getHttpServer())
.post('/api/broker')
.send({
brokerCode: 'GET_ONE_TEST',
brokerName: '单个查询测试',
region: 'CN',
})
.expect(201);
const brokerId = (createResponse.body as Response<Broker>).data
.brokerId;
// 查询单个券商
return request(app.getHttpServer())
.get(`/api/broker/${brokerId}`)
.expect(200)
.expect((res) => {
expect(res.body.code).toBe(0);
expect(res.body.data.brokerId).toBe(brokerId);
expect(res.body.data.brokerCode).toBe('GET_ONE_TEST');
});
});
it('应该返回 404 当券商不存在时', () => {
return request(app.getHttpServer())
.get('/api/broker/99999')
.expect(404)
.expect((res) => {
expect(res.body.code).toBe(404);
});
});
});
describe('PATCH /api/broker/:id', () => {
it('应该成功更新券商', async () => {
// 先创建一个券商
const createResponse = await request(app.getHttpServer())
.post('/api/broker')
.send({
brokerCode: 'UPDATE_TEST',
brokerName: '更新测试',
region: 'CN',
})
.expect(201);
const brokerId = (createResponse.body as Response<Broker>).data
.brokerId;
// 更新券商
return request(app.getHttpServer())
.patch(`/api/broker/${brokerId}`)
.send({
brokerName: '更新后的名称',
})
.expect(200)
.expect((res) => {
expect(res.body.code).toBe(0);
expect(res.body.data.brokerName).toBe('更新后的名称');
});
});
});
describe('DELETE /api/broker/:id', () => {
it('应该成功删除券商', async () => {
// 先创建一个券商
const createResponse = await request(app.getHttpServer())
.post('/api/broker')
.send({
brokerCode: 'DELETE_TEST',
brokerName: '删除测试',
region: 'CN',
})
.expect(201);
const brokerId = (createResponse.body as Response<Broker>).data
.brokerId;
// 删除券商
await request(app.getHttpServer())
.delete(`/api/broker/${brokerId}`)
.expect(204);
// 验证已删除
await request(app.getHttpServer())
.get(`/api/broker/${brokerId}`)
.expect(404);
});
});
});

View File

@@ -0,0 +1,150 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BrokerService } from '../../../src/modules/broker/broker.service';
import { Broker } from '../../../src/modules/broker/broker.entity';
import { BrokerModule } from '../../../src/modules/broker/broker.module';
import { CreateBrokerDto } from '../../../src/modules/broker/dto/create-broker.dto';
import { BatchCreateBrokerDto } from '../../../src/modules/broker/dto/batch-create-broker.dto';
import { getDatabaseConfig } from '../../../src/database/database.config';
import { brokersConfig } from '../../../src/modules/broker/config/brokers.config';
describe('BrokerService (集成测试)', () => {
let service: BrokerService;
let repository: Repository<Broker>;
let module: TestingModule;
beforeAll(async () => {
jest.setTimeout(30000); // 设置超时为 30 秒
// 创建测试模块,使用真实数据库连接
module = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.development', '.env'],
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => {
const config = getDatabaseConfig(configService);
// 使用测试数据库(如果配置了),否则使用开发数据库
const testDatabase =
configService.get<string>('DB_DATABASE') ||
(config.database as string);
return {
...config,
database: testDatabase,
synchronize: true, // 测试环境允许同步
dropSchema: false, // 不删除现有数据
} as typeof config;
},
inject: [ConfigService],
}),
BrokerModule,
],
}).compile();
service = module.get<BrokerService>(BrokerService);
repository = module.get<Repository<Broker>>(getRepositoryToken(Broker));
// 清理测试数据(可选)
await repository.clear();
});
afterAll(async () => {
// 测试结束后清理
// await repository.clear();
await module.close();
});
describe.skip('create - 集成测试', () => {
it('应该成功在数据库中创建券商', async () => {
const createBrokerDto: CreateBrokerDto = {
brokerCode: 'HTZQ_TEST',
brokerName: '华泰证券测试',
region: 'CN',
sortOrder: 1,
isActive: true,
};
const result = await service.create(createBrokerDto);
console.log(result);
expect(result).toBeDefined();
expect(result.brokerId).toBeDefined();
expect(result.brokerCode).toBe(createBrokerDto.brokerCode);
expect(result.brokerName).toBe(createBrokerDto.brokerName);
// 验证数据确实保存到数据库
const savedBroker = await repository.findOne({
where: { brokerId: result.brokerId },
});
expect(savedBroker).toBeDefined();
expect(savedBroker?.brokerCode).toBe(createBrokerDto.brokerCode);
// 清理
// await repository.remove(result);
});
it('应该使用数据库默认值', async () => {
const createBrokerDto: CreateBrokerDto = {
brokerCode: 'ZSZQ_TEST',
brokerName: '招商证券测试',
region: 'CN',
// 不提供 sortOrder 和 isActive
};
const result = await service.create(createBrokerDto);
expect(result.sortOrder).toBe(0); // 数据库默认值
expect(result.isActive).toBe(true); // 数据库默认值
// 清理
// await repository.remove(result);
});
});
describe.only('batchCreate - 集成测试', () => {
it('应该成功从配置文件批量创建券商', async () => {
// 从配置文件读取券商数据
const batchCreateDto: BatchCreateBrokerDto = {
brokers: brokersConfig,
};
const result = await service.batchCreate(batchCreateDto);
// 验证创建成功
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBeGreaterThan(0);
// 验证数据确实保存到数据库
const savedBrokers = await repository.find({
where: brokersConfig.map((broker) => ({
brokerCode: broker.brokerCode,
region: broker.region,
})),
});
expect(savedBrokers.length).toBe(brokersConfig.length);
// 验证部分数据
const cnBrokers = savedBrokers.filter((b) => b.region === 'CN');
const hkBrokers = savedBrokers.filter((b) => b.region === 'HK');
const usBrokers = savedBrokers.filter((b) => b.region === 'US');
expect(cnBrokers.length).toBeGreaterThan(0);
expect(hkBrokers.length).toBeGreaterThan(0);
expect(usBrokers.length).toBeGreaterThan(0);
// 验证 brokerImage 为 null 或 undefined
savedBrokers.forEach((broker) => {
expect(broker.brokerImage).toBeFalsy(); // null 或 undefined 都通过
});
// 注意:不删除数据,保留在数据库中
});
});
});

261
pnpm-lock.yaml generated
View File

@@ -12,19 +12,43 @@ importers:
dependencies:
'@nestjs/common':
specifier: ^11.0.1
version: 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2)
version: 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/config':
specifier: ^4.0.2
version: 4.0.2(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)
version: 4.0.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)
'@nestjs/core':
specifier: ^11.0.1
version: 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/mapped-types':
specifier: ^2.1.0
version: 2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)
'@nestjs/platform-express':
specifier: ^11.0.1
version: 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)
version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)
'@nestjs/swagger':
specifier: ^11.2.2
version: 11.2.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)
'@nestjs/typeorm':
specifier: ^11.0.0
version: 11.0.0(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))
version: 11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))
body-parser:
specifier: ^2.2.0
version: 2.2.0
class-transformer:
specifier: ^0.5.1
version: 0.5.1
class-validator:
specifier: ^0.14.2
version: 0.14.2
compression:
specifier: ^1.8.1
version: 1.8.1
express-rate-limit:
specifier: ^8.2.1
version: 8.2.1(express@5.1.0)
helmet:
specifier: ^8.1.0
version: 8.1.0
pg:
specifier: ^8.16.3
version: 8.16.3
@@ -52,7 +76,13 @@ importers:
version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3)
'@nestjs/testing':
specifier: ^11.0.1
version: 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)
version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)
'@types/body-parser':
specifier: ^1.19.6
version: 1.19.6
'@types/compression':
specifier: ^1.8.1
version: 1.8.1
'@types/express':
specifier: ^5.0.0
version: 5.0.5
@@ -654,6 +684,9 @@ packages:
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
engines: {node: '>=8'}
'@microsoft/tsdoc@0.16.0':
resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==}
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
@@ -707,6 +740,19 @@ packages:
'@nestjs/websockets':
optional: true
'@nestjs/mapped-types@2.1.0':
resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==}
peerDependencies:
'@nestjs/common': ^10.0.0 || ^11.0.0
class-transformer: ^0.4.0 || ^0.5.0
class-validator: ^0.13.0 || ^0.14.0
reflect-metadata: ^0.1.12 || ^0.2.0
peerDependenciesMeta:
class-transformer:
optional: true
class-validator:
optional: true
'@nestjs/platform-express@11.1.9':
resolution: {integrity: sha512-GVd3+0lO0mJq2m1kl9hDDnVrX3Nd4oH3oDfklz0pZEVEVS0KVSp63ufHq2Lu9cyPdSBuelJr9iPm2QQ1yX+Kmw==}
peerDependencies:
@@ -718,6 +764,23 @@ packages:
peerDependencies:
typescript: '>=4.8.2'
'@nestjs/swagger@11.2.2':
resolution: {integrity: sha512-i16GRaZ7vlTHIqk8C1UvV/WwQYbWwQymocTvU8mr6QIUBZ6fJc1uGEsw0Mu/JWC0kaV3nbsTj1hZbXrc5Ui4NA==}
peerDependencies:
'@fastify/static': ^8.0.0
'@nestjs/common': ^11.0.1
'@nestjs/core': ^11.0.1
class-transformer: '*'
class-validator: '*'
reflect-metadata: ^0.1.12 || ^0.2.0
peerDependenciesMeta:
'@fastify/static':
optional: true
class-transformer:
optional: true
class-validator:
optional: true
'@nestjs/testing@11.1.9':
resolution: {integrity: sha512-UFxerBDdb0RUNxQNj25pvkvNE7/vxKhXYWBt3QuwBFnYISzRIzhVlyIqLfoV5YI3zV0m0Nn4QAn1KM0zzwfEng==}
peerDependencies:
@@ -772,6 +835,9 @@ packages:
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
'@scarf/scarf@1.4.0':
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
'@sinclair/typebox@0.34.41':
resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==}
@@ -821,6 +887,9 @@ packages:
'@types/body-parser@1.19.6':
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
'@types/compression@1.8.1':
resolution: {integrity: sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==}
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
@@ -896,6 +965,9 @@ packages:
'@types/supertest@6.0.3':
resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==}
'@types/validator@13.15.10':
resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
'@types/yargs-parser@21.0.3':
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
@@ -1369,6 +1441,12 @@ packages:
cjs-module-lexer@2.1.1:
resolution: {integrity: sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==}
class-transformer@0.5.1:
resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==}
class-validator@0.14.2:
resolution: {integrity: sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==}
cli-cursor@3.1.0:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
engines: {node: '>=8'}
@@ -1425,6 +1503,14 @@ packages:
component-emitter@1.3.1:
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
compressible@2.0.18:
resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
engines: {node: '>= 0.6'}
compression@1.8.1:
resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==}
engines: {node: '>= 0.8.0'}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -1484,6 +1570,14 @@ packages:
dayjs@1.11.19:
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@@ -1705,6 +1799,12 @@ packages:
resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
express-rate-limit@8.2.1:
resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==}
engines: {node: '>= 16'}
peerDependencies:
express: '>= 4.11'
express@5.1.0:
resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
engines: {node: '>= 18'}
@@ -1910,6 +2010,10 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
helmet@8.1.0:
resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==}
engines: {node: '>=18.0.0'}
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
@@ -1960,6 +2064,10 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ip-address@10.0.1:
resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==}
engines: {node: '>= 12'}
ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
@@ -2232,6 +2340,9 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
libphonenumber-js@1.12.26:
resolution: {integrity: sha512-MagMOuqEXB2Pa90cWE+BoCmcKJx+de5uBIicaUkQ+uiEslZ0OBMNOkSZT/36syXNHu68UeayTxPm3DYM2IHoLQ==}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@@ -2369,6 +2480,9 @@ packages:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -2388,6 +2502,10 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
negotiator@0.6.4:
resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
engines: {node: '>= 0.6'}
negotiator@1.0.0:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
@@ -2427,6 +2545,10 @@ packages:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
on-headers@1.1.0:
resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==}
engines: {node: '>= 0.8'}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
@@ -2871,6 +2993,9 @@ packages:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'}
swagger-ui-dist@5.30.2:
resolution: {integrity: sha512-HWCg1DTNE/Nmapt+0m2EPXFwNKNeKK4PwMjkwveN/zn1cV2Kxi9SURd+m0SpdcSgWEK/O64sf8bzXdtUhigtHA==}
symbol-observable@4.0.0:
resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
engines: {node: '>=0.10'}
@@ -3150,6 +3275,10 @@ packages:
resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
engines: {node: '>=10.12.0'}
validator@13.15.23:
resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==}
engines: {node: '>= 0.10'}
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
@@ -3950,6 +4079,8 @@ snapshots:
'@lukeed/csprng@1.1.0': {}
'@microsoft/tsdoc@0.16.0': {}
'@napi-rs/wasm-runtime@0.2.12':
dependencies:
'@emnapi/core': 1.7.1
@@ -3984,7 +4115,7 @@ snapshots:
- uglify-js
- webpack-cli
'@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2)':
'@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
dependencies:
file-type: 21.1.0
iterare: 1.2.1
@@ -3993,20 +4124,23 @@ snapshots:
rxjs: 7.8.2
tslib: 2.8.1
uid: 2.0.2
optionalDependencies:
class-transformer: 0.5.1
class-validator: 0.14.2
transitivePeerDependencies:
- supports-color
'@nestjs/config@4.0.2(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)':
'@nestjs/config@4.0.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)':
dependencies:
'@nestjs/common': 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
dotenv: 16.4.7
dotenv-expand: 12.0.1
lodash: 4.17.21
rxjs: 7.8.2
'@nestjs/core@11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
'@nestjs/core@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
dependencies:
'@nestjs/common': 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nuxt/opencollective': 0.4.1
fast-safe-stringify: 2.1.1
iterare: 1.2.1
@@ -4016,12 +4150,20 @@ snapshots:
tslib: 2.8.1
uid: 2.0.2
optionalDependencies:
'@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)
'@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)
'@nestjs/platform-express@11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)':
'@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)':
dependencies:
'@nestjs/common': 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
reflect-metadata: 0.2.2
optionalDependencies:
class-transformer: 0.5.1
class-validator: 0.14.2
'@nestjs/platform-express@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
cors: 2.8.5
express: 5.1.0
multer: 2.0.2
@@ -4052,18 +4194,33 @@ snapshots:
transitivePeerDependencies:
- chokidar
'@nestjs/testing@11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)':
'@nestjs/swagger@11.2.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)':
dependencies:
'@nestjs/common': 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@microsoft/tsdoc': 0.16.0
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)
js-yaml: 4.1.1
lodash: 4.17.21
path-to-regexp: 8.3.0
reflect-metadata: 0.2.2
swagger-ui-dist: 5.30.2
optionalDependencies:
class-transformer: 0.5.1
class-validator: 0.14.2
'@nestjs/testing@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
tslib: 2.8.1
optionalDependencies:
'@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)
'@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)
'@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))':
'@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))':
dependencies:
'@nestjs/common': 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
reflect-metadata: 0.2.2
rxjs: 7.8.2
typeorm: 0.3.27(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
@@ -4095,6 +4252,8 @@ snapshots:
'@pkgr/core@0.2.9': {}
'@scarf/scarf@1.4.0': {}
'@sinclair/typebox@0.34.41': {}
'@sinonjs/commons@3.0.1':
@@ -4156,6 +4315,11 @@ snapshots:
'@types/connect': 3.4.38
'@types/node': 22.19.1
'@types/compression@1.8.1':
dependencies:
'@types/express': 5.0.5
'@types/node': 22.19.1
'@types/connect@3.4.38':
dependencies:
'@types/node': 22.19.1
@@ -4253,6 +4417,8 @@ snapshots:
'@types/methods': 1.1.4
'@types/superagent': 8.1.9
'@types/validator@13.15.10': {}
'@types/yargs-parser@21.0.3': {}
'@types/yargs@17.0.35':
@@ -4761,6 +4927,14 @@ snapshots:
cjs-module-lexer@2.1.1: {}
class-transformer@0.5.1: {}
class-validator@0.14.2:
dependencies:
'@types/validator': 13.15.10
libphonenumber-js: 1.12.26
validator: 13.15.23
cli-cursor@3.1.0:
dependencies:
restore-cursor: 3.1.0
@@ -4809,6 +4983,22 @@ snapshots:
component-emitter@1.3.1: {}
compressible@2.0.18:
dependencies:
mime-db: 1.54.0
compression@1.8.1:
dependencies:
bytes: 3.1.2
compressible: 2.0.18
debug: 2.6.9
negotiator: 0.6.4
on-headers: 1.1.0
safe-buffer: 5.2.1
vary: 1.1.2
transitivePeerDependencies:
- supports-color
concat-map@0.0.1: {}
concat-stream@2.0.0:
@@ -4860,6 +5050,10 @@ snapshots:
dayjs@1.11.19: {}
debug@2.6.9:
dependencies:
ms: 2.0.0
debug@4.4.3:
dependencies:
ms: 2.1.3
@@ -5069,6 +5263,11 @@ snapshots:
jest-mock: 30.2.0
jest-util: 30.2.0
express-rate-limit@8.2.1(express@5.1.0):
dependencies:
express: 5.1.0
ip-address: 10.0.1
express@5.1.0:
dependencies:
accepts: 2.0.0
@@ -5333,6 +5532,8 @@ snapshots:
dependencies:
function-bind: 1.1.2
helmet@8.1.0: {}
html-escaper@2.0.2: {}
http-errors@2.0.0:
@@ -5378,6 +5579,8 @@ snapshots:
inherits@2.0.4: {}
ip-address@10.0.1: {}
ipaddr.js@1.9.1: {}
is-arrayish@0.2.1: {}
@@ -5817,6 +6020,8 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
libphonenumber-js@1.12.26: {}
lines-and-columns@1.2.4: {}
load-esm@1.0.3: {}
@@ -5923,6 +6128,8 @@ snapshots:
dependencies:
minimist: 1.2.8
ms@2.0.0: {}
ms@2.1.3: {}
multer@2.0.2:
@@ -5941,6 +6148,8 @@ snapshots:
natural-compare@1.4.0: {}
negotiator@0.6.4: {}
negotiator@1.0.0: {}
neo-async@2.6.2: {}
@@ -5969,6 +6178,8 @@ snapshots:
dependencies:
ee-first: 1.1.1
on-headers@1.1.0: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
@@ -6418,6 +6629,10 @@ snapshots:
dependencies:
has-flag: 4.0.0
swagger-ui-dist@5.30.2:
dependencies:
'@scarf/scarf': 1.4.0
symbol-observable@4.0.0: {}
synckit@0.11.11:
@@ -6666,6 +6881,8 @@ snapshots:
'@types/istanbul-lib-coverage': 2.0.6
convert-source-map: 2.0.0
validator@13.15.23: {}
vary@1.1.2: {}
walker@1.0.8: