feat: 开发我的持仓列表
This commit is contained in:
@@ -9,6 +9,7 @@ import { StorageModule } from './modules/storage/storage.module';
|
||||
import { StockInfoModule } from './modules/stock-info/stock-info.module';
|
||||
import { StockDailyPriceModule } from './modules/stock-daily-price/stock-daily-price.module';
|
||||
import { PositionModule } from './modules/position/position.module';
|
||||
import { PositionChangeModule } from './modules/position-change/position-change.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -28,6 +29,7 @@ import { PositionModule } from './modules/position/position.module';
|
||||
StockInfoModule,
|
||||
StockDailyPriceModule,
|
||||
PositionModule,
|
||||
PositionChangeModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsNumber,
|
||||
IsDateString,
|
||||
IsIn,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class CreatePositionChangeDto {
|
||||
@ApiProperty({
|
||||
description: '持仓ID',
|
||||
example: 1,
|
||||
})
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@IsNotEmpty()
|
||||
positionId: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '变更日期',
|
||||
example: '2024-01-01',
|
||||
})
|
||||
@IsDateString()
|
||||
@IsNotEmpty()
|
||||
changeDate: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '变更类型',
|
||||
example: 'buy',
|
||||
enum: ['buy', 'sell', 'auto'],
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsIn(['buy', 'sell', 'auto'])
|
||||
changeType: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '变更前份额/数量',
|
||||
example: 100,
|
||||
})
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
beforeShares: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '变更前成本价',
|
||||
example: 1600.0,
|
||||
})
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(0.0001)
|
||||
beforeCostPrice: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '变更后份额/数量',
|
||||
example: 150,
|
||||
})
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
afterShares: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '变更后成本价',
|
||||
example: 1650.0,
|
||||
})
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(0.0001)
|
||||
afterCostPrice: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: '备注/思考',
|
||||
example: '加仓买入,看好长期走势',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { PositionChange } from '../position-change.entity';
|
||||
import { PaginationInfo } from '@/common/dto/pagination.dto';
|
||||
|
||||
/**
|
||||
* 持仓变更记录分页响应数据
|
||||
*/
|
||||
export class PaginatedPositionChangeData {
|
||||
@ApiProperty({
|
||||
description: '持仓变更记录列表',
|
||||
type: [PositionChange],
|
||||
})
|
||||
list: PositionChange[];
|
||||
|
||||
@ApiProperty({ description: '分页信息', type: PaginationInfo })
|
||||
pagination: PaginationInfo;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { IsOptional, IsNumber, Min } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class QueryPositionChangeDto {
|
||||
@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;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class UpdatePositionChangeDto {
|
||||
@ApiPropertyOptional({
|
||||
description: '备注/思考',
|
||||
example: '加仓买入,看好长期走势',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Query,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiParam,
|
||||
ApiBearerAuth,
|
||||
} from '@nestjs/swagger';
|
||||
import { PositionChangeService } from './position-change.service';
|
||||
import { CreatePositionChangeDto } from './dto/create-position-change.dto';
|
||||
import { UpdatePositionChangeDto } from './dto/update-position-change.dto';
|
||||
import { QueryPositionChangeDto } from './dto/query-position-change.dto';
|
||||
import { PositionChange } from './position-change.entity';
|
||||
import { PaginatedPositionChangeData } from './dto/paginated-response.dto';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { User } from '../user/user.entity';
|
||||
|
||||
@ApiTags('position-change')
|
||||
@Controller('position-change')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
export class PositionChangeController {
|
||||
constructor(
|
||||
private readonly positionChangeService: PositionChangeService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 分页查询单个持仓的所有变更记录
|
||||
*/
|
||||
@Get('position/:positionId')
|
||||
@ApiOperation({
|
||||
summary: '查询单个持仓的所有变更记录',
|
||||
description: '分页查询指定持仓的所有变更记录,按变更日期倒序',
|
||||
})
|
||||
@ApiParam({ name: 'positionId', description: '持仓ID', type: Number })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '查询成功',
|
||||
type: PaginatedPositionChangeData,
|
||||
})
|
||||
@ApiResponse({ status: 401, description: '未授权' })
|
||||
@ApiResponse({ status: 404, description: '持仓不存在' })
|
||||
findAllByPositionId(
|
||||
@Request() req: { user: User },
|
||||
@Param('positionId') positionId: string,
|
||||
@Query() queryDto: QueryPositionChangeDto,
|
||||
): Promise<PaginatedPositionChangeData> {
|
||||
return this.positionChangeService.findAllByPositionId(
|
||||
+positionId,
|
||||
req.user.userId,
|
||||
queryDto,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建持仓变更记录
|
||||
*/
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({
|
||||
summary: '创建持仓变更记录',
|
||||
description: '为指定持仓创建新的变更记录',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: '创建成功',
|
||||
type: PositionChange,
|
||||
})
|
||||
@ApiResponse({ status: 400, description: '请求参数错误' })
|
||||
@ApiResponse({ status: 404, description: '持仓不存在' })
|
||||
create(
|
||||
@Request() req: { user: User },
|
||||
@Body() createPositionChangeDto: CreatePositionChangeDto,
|
||||
): Promise<PositionChange> {
|
||||
return this.positionChangeService.create(
|
||||
req.user.userId,
|
||||
createPositionChangeDto,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新持仓变更记录的备注/思考
|
||||
*/
|
||||
@Patch(':id')
|
||||
@ApiOperation({
|
||||
summary: '更新持仓变更记录的备注',
|
||||
description: '更新指定变更记录的备注/思考内容',
|
||||
})
|
||||
@ApiParam({ name: 'id', description: '变更记录ID', type: Number })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '更新成功',
|
||||
type: PositionChange,
|
||||
})
|
||||
@ApiResponse({ status: 404, description: '变更记录不存在' })
|
||||
@ApiResponse({ status: 403, description: '无权访问' })
|
||||
update(
|
||||
@Request() req: { user: User },
|
||||
@Param('id') id: string,
|
||||
@Body() updatePositionChangeDto: UpdatePositionChangeDto,
|
||||
): Promise<PositionChange> {
|
||||
return this.positionChangeService.update(
|
||||
+id,
|
||||
req.user.userId,
|
||||
updatePositionChangeDto,
|
||||
);
|
||||
}
|
||||
}
|
||||
105
apps/api/src/modules/position-change/position-change.entity.ts
Normal file
105
apps/api/src/modules/position-change/position-change.entity.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
@Entity('position_changes')
|
||||
export class PositionChange {
|
||||
@ApiProperty({ description: '变更记录ID', example: 1 })
|
||||
@PrimaryGeneratedColumn({ name: 'change_id' })
|
||||
changeId: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '持仓ID',
|
||||
example: 1,
|
||||
})
|
||||
@Column({ name: 'position_id', type: 'bigint' })
|
||||
@Index()
|
||||
positionId: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '变更日期',
|
||||
example: '2024-01-01',
|
||||
})
|
||||
@Column({ name: 'change_date', type: 'date' })
|
||||
@Index()
|
||||
changeDate: Date;
|
||||
|
||||
@ApiProperty({
|
||||
description: '变更类型',
|
||||
example: 'buy',
|
||||
enum: ['buy', 'sell', 'auto'],
|
||||
})
|
||||
@Column({
|
||||
name: 'change_type',
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
})
|
||||
changeType: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '变更前份额/数量',
|
||||
example: 100,
|
||||
})
|
||||
@Column({
|
||||
name: 'before_shares',
|
||||
type: 'decimal',
|
||||
precision: 18,
|
||||
scale: 4,
|
||||
})
|
||||
beforeShares: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '变更前成本价',
|
||||
example: 1600.0,
|
||||
})
|
||||
@Column({
|
||||
name: 'before_cost_price',
|
||||
type: 'decimal',
|
||||
precision: 18,
|
||||
scale: 4,
|
||||
})
|
||||
beforeCostPrice: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '变更后份额/数量',
|
||||
example: 150,
|
||||
})
|
||||
@Column({
|
||||
name: 'after_shares',
|
||||
type: 'decimal',
|
||||
precision: 18,
|
||||
scale: 4,
|
||||
})
|
||||
afterShares: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '变更后成本价',
|
||||
example: 1650.0,
|
||||
})
|
||||
@Column({
|
||||
name: 'after_cost_price',
|
||||
type: 'decimal',
|
||||
precision: 18,
|
||||
scale: 4,
|
||||
})
|
||||
afterCostPrice: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: '备注/思考',
|
||||
example: '加仓买入,看好长期走势',
|
||||
})
|
||||
@Column({ name: 'notes', type: 'text', nullable: true })
|
||||
notes?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '创建时间',
|
||||
example: '2024-01-01T00:00:00.000Z',
|
||||
})
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { PositionChangeService } from './position-change.service';
|
||||
import { PositionChangeController } from './position-change.controller';
|
||||
import { PositionChange } from './position-change.entity';
|
||||
import { Position } from '../position/position.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([PositionChange, Position])],
|
||||
controllers: [PositionChangeController],
|
||||
providers: [PositionChangeService],
|
||||
exports: [PositionChangeService],
|
||||
})
|
||||
export class PositionChangeModule {}
|
||||
149
apps/api/src/modules/position-change/position-change.service.ts
Normal file
149
apps/api/src/modules/position-change/position-change.service.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ForbiddenException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PositionChange } from './position-change.entity';
|
||||
import { CreatePositionChangeDto } from './dto/create-position-change.dto';
|
||||
import { UpdatePositionChangeDto } from './dto/update-position-change.dto';
|
||||
import { QueryPositionChangeDto } from './dto/query-position-change.dto';
|
||||
import { PaginationInfo } from '@/common/dto/pagination.dto';
|
||||
import { Position } from '../position/position.entity';
|
||||
|
||||
@Injectable()
|
||||
export class PositionChangeService {
|
||||
private readonly logger = new Logger(PositionChangeService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(PositionChange)
|
||||
private readonly positionChangeRepository: Repository<PositionChange>,
|
||||
@InjectRepository(Position)
|
||||
private readonly positionRepository: Repository<Position>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 分页查询单个持仓的所有变更记录
|
||||
*/
|
||||
async findAllByPositionId(
|
||||
positionId: number,
|
||||
userId: number,
|
||||
queryDto: QueryPositionChangeDto,
|
||||
): Promise<{
|
||||
list: PositionChange[];
|
||||
pagination: PaginationInfo;
|
||||
}> {
|
||||
// 验证持仓是否属于当前用户
|
||||
const position = await this.positionRepository.findOne({
|
||||
where: { positionId, userId },
|
||||
});
|
||||
|
||||
if (!position) {
|
||||
throw new NotFoundException(`持仓不存在:ID ${positionId}`);
|
||||
}
|
||||
|
||||
// 分页参数
|
||||
const page = queryDto.page || 1;
|
||||
const limit = queryDto.limit || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// 查询总数
|
||||
const total = await this.positionChangeRepository.count({
|
||||
where: { positionId },
|
||||
});
|
||||
|
||||
// 查询分页数据(按变更日期和创建时间倒序)
|
||||
const list = await this.positionChangeRepository.find({
|
||||
where: { positionId },
|
||||
order: {
|
||||
changeDate: 'DESC',
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
});
|
||||
|
||||
// 计算总页数
|
||||
const total_page = Math.ceil(total / limit);
|
||||
|
||||
return {
|
||||
list,
|
||||
pagination: {
|
||||
total,
|
||||
total_page,
|
||||
page_size: limit,
|
||||
current_page: page,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建持仓变更记录
|
||||
*/
|
||||
async create(
|
||||
userId: number,
|
||||
createPositionChangeDto: CreatePositionChangeDto,
|
||||
): Promise<PositionChange> {
|
||||
// 验证持仓是否属于当前用户
|
||||
const position = await this.positionRepository.findOne({
|
||||
where: {
|
||||
positionId: createPositionChangeDto.positionId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!position) {
|
||||
throw new NotFoundException(
|
||||
`持仓不存在:ID ${createPositionChangeDto.positionId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 创建变更记录
|
||||
const positionChange = this.positionChangeRepository.create({
|
||||
...createPositionChangeDto,
|
||||
changeDate: new Date(createPositionChangeDto.changeDate),
|
||||
});
|
||||
|
||||
return this.positionChangeRepository.save(positionChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新持仓变更记录的备注/思考
|
||||
*/
|
||||
async update(
|
||||
changeId: number,
|
||||
userId: number,
|
||||
updatePositionChangeDto: UpdatePositionChangeDto,
|
||||
): Promise<PositionChange> {
|
||||
// 查找变更记录
|
||||
const positionChange = await this.positionChangeRepository.findOne({
|
||||
where: { changeId },
|
||||
relations: [], // 不加载关联,后面手动验证
|
||||
});
|
||||
|
||||
if (!positionChange) {
|
||||
throw new NotFoundException(`变更记录不存在:ID ${changeId}`);
|
||||
}
|
||||
|
||||
// 验证持仓是否属于当前用户
|
||||
const position = await this.positionRepository.findOne({
|
||||
where: {
|
||||
positionId: positionChange.positionId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!position) {
|
||||
throw new ForbiddenException('无权访问该持仓的变更记录');
|
||||
}
|
||||
|
||||
// 只更新备注字段
|
||||
if (updatePositionChangeDto.notes !== undefined) {
|
||||
positionChange.notes = updatePositionChangeDto.notes;
|
||||
}
|
||||
|
||||
return this.positionChangeRepository.save(positionChange);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,14 @@ export class Position {
|
||||
description: '用户ID',
|
||||
example: 1,
|
||||
})
|
||||
@Column({ name: 'user_id', type: 'bigint' })
|
||||
@Column({
|
||||
name: 'user_id',
|
||||
type: 'bigint',
|
||||
transformer: {
|
||||
to: (value: number) => value,
|
||||
from: (value: string) => (value ? parseInt(value, 10) : null),
|
||||
},
|
||||
})
|
||||
@Index()
|
||||
userId: number;
|
||||
|
||||
@@ -28,7 +35,14 @@ export class Position {
|
||||
description: '券商ID',
|
||||
example: 1,
|
||||
})
|
||||
@Column({ name: 'broker_id', type: 'bigint' })
|
||||
@Column({
|
||||
name: 'broker_id',
|
||||
type: 'bigint',
|
||||
transformer: {
|
||||
to: (value: number) => value,
|
||||
from: (value: string) => (value ? parseInt(value, 10) : null),
|
||||
},
|
||||
})
|
||||
@Index()
|
||||
brokerId: number;
|
||||
|
||||
@@ -80,6 +94,10 @@ export class Position {
|
||||
precision: 18,
|
||||
scale: 4,
|
||||
default: 0,
|
||||
transformer: {
|
||||
to: (value: number) => value,
|
||||
from: (value: string) => (value ? parseFloat(value) : 0),
|
||||
},
|
||||
})
|
||||
shares: number;
|
||||
|
||||
@@ -92,6 +110,10 @@ export class Position {
|
||||
type: 'decimal',
|
||||
precision: 18,
|
||||
scale: 4,
|
||||
transformer: {
|
||||
to: (value: number) => value,
|
||||
from: (value: string) => (value ? parseFloat(value) : null),
|
||||
},
|
||||
})
|
||||
costPrice: number;
|
||||
|
||||
@@ -105,9 +127,30 @@ export class Position {
|
||||
precision: 18,
|
||||
scale: 4,
|
||||
nullable: true,
|
||||
transformer: {
|
||||
to: (value: number | undefined) => value,
|
||||
from: (value: string | null) => (value ? parseFloat(value) : null),
|
||||
},
|
||||
})
|
||||
currentPrice?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: '上一次的价格(用于对比显示红绿色)',
|
||||
example: 1800.0,
|
||||
})
|
||||
@Column({
|
||||
name: 'previous_price',
|
||||
type: 'decimal',
|
||||
precision: 18,
|
||||
scale: 4,
|
||||
nullable: true,
|
||||
transformer: {
|
||||
to: (value: number | undefined) => value,
|
||||
from: (value: string | null) => (value ? parseFloat(value) : null),
|
||||
},
|
||||
})
|
||||
previousPrice?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '货币类型',
|
||||
example: 'CNY',
|
||||
@@ -133,6 +176,10 @@ export class Position {
|
||||
precision: 10,
|
||||
scale: 6,
|
||||
default: 1,
|
||||
transformer: {
|
||||
to: (value: number | undefined) => value,
|
||||
from: (value: string | null) => (value ? parseFloat(value) : 1),
|
||||
},
|
||||
})
|
||||
exchangeRate?: number;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Repository, IsNull, FindOptionsWhere } from 'typeorm';
|
||||
import { Position } from './position.entity';
|
||||
import { CreatePositionDto } from './dto/create-position.dto';
|
||||
import { UpdatePositionDto } from './dto/update-position.dto';
|
||||
@@ -35,10 +35,9 @@ export class PositionService {
|
||||
|
||||
// 计算每个持仓的额外字段
|
||||
const result: PositionResponseDto[] = positions.map((position) => {
|
||||
const costValue =
|
||||
Number(position.shares) * Number(position.costPrice);
|
||||
const costValue = position.shares * position.costPrice;
|
||||
const marketValue = position.currentPrice
|
||||
? Number(position.shares) * Number(position.currentPrice)
|
||||
? position.shares * position.currentPrice
|
||||
: 0;
|
||||
const profit = marketValue - costValue;
|
||||
const profitPercent =
|
||||
@@ -75,8 +74,7 @@ export class PositionService {
|
||||
let totalAsset = 0;
|
||||
for (const position of positions) {
|
||||
if (position.currentPrice) {
|
||||
const marketValue =
|
||||
Number(position.shares) * Number(position.currentPrice);
|
||||
const marketValue = position.shares * position.currentPrice;
|
||||
totalAsset += marketValue;
|
||||
}
|
||||
}
|
||||
@@ -92,14 +90,22 @@ export class PositionService {
|
||||
createPositionDto: CreatePositionDto,
|
||||
): Promise<Position> {
|
||||
// 检查唯一性约束:同一用户同一券商同一资产只能有一条持仓
|
||||
const whereCondition: FindOptionsWhere<Position> = {
|
||||
userId,
|
||||
brokerId: createPositionDto.brokerId,
|
||||
symbol: createPositionDto.symbol,
|
||||
assetType: createPositionDto.assetType,
|
||||
};
|
||||
|
||||
// 处理 market 字段:如果为 undefined 或空字符串,查询 null 值
|
||||
if (createPositionDto.market) {
|
||||
whereCondition.market = createPositionDto.market;
|
||||
} else {
|
||||
whereCondition.market = IsNull();
|
||||
}
|
||||
|
||||
const existing = await this.positionRepository.findOne({
|
||||
where: {
|
||||
userId,
|
||||
brokerId: createPositionDto.brokerId,
|
||||
symbol: createPositionDto.symbol,
|
||||
market: createPositionDto.market || null,
|
||||
assetType: createPositionDto.assetType,
|
||||
},
|
||||
where: whereCondition,
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
@@ -146,6 +152,13 @@ export class PositionService {
|
||||
position.costPrice = updatePositionDto.costPrice;
|
||||
}
|
||||
if (updatePositionDto.currentPrice !== undefined) {
|
||||
// 更新价格时,将当前价格保存到 previous_price
|
||||
if (
|
||||
position.currentPrice !== null &&
|
||||
position.currentPrice !== undefined
|
||||
) {
|
||||
position.previousPrice = position.currentPrice;
|
||||
}
|
||||
position.currentPrice = updatePositionDto.currentPrice;
|
||||
}
|
||||
if (updatePositionDto.currency !== undefined) {
|
||||
|
||||
Reference in New Issue
Block a user