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

@@ -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 {}