feat: 开发broker相关代码,开发全局代码
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
+51
-2
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateBrokerDto } from './create-broker.dto';
|
||||
|
||||
export class UpdateBrokerDto extends PartialType(CreateBrokerDto) {}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
@Module({})
|
||||
export class UserModule {}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { User } from './user';
|
||||
|
||||
describe('User', () => {
|
||||
it('should be defined', () => {
|
||||
expect(new User()).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export class User {}
|
||||
Reference in New Issue
Block a user