231 lines
7.9 KiB
TypeScript
231 lines
7.9 KiB
TypeScript
import {
|
||
Controller,
|
||
Get,
|
||
Post,
|
||
Put,
|
||
Body,
|
||
Query,
|
||
HttpCode,
|
||
HttpStatus,
|
||
Logger,
|
||
} from '@nestjs/common';
|
||
import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger';
|
||
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||
import { PrePlantingConfigService } from './pre-planting-config.service';
|
||
import { PrePlantingProxyService } from './pre-planting-proxy.service';
|
||
import { PrismaService } from '../infrastructure/persistence/prisma/prisma.service';
|
||
|
||
class UpdatePrePlantingConfigDto {
|
||
@IsBoolean()
|
||
isActive: boolean;
|
||
|
||
@IsOptional()
|
||
@IsString()
|
||
updatedBy?: string;
|
||
}
|
||
|
||
class TogglePrePlantingConfigDto {
|
||
@IsBoolean()
|
||
isActive: boolean;
|
||
}
|
||
|
||
class UpdatePrePlantingAgreementDto {
|
||
@IsString()
|
||
text: string;
|
||
}
|
||
|
||
@ApiTags('预种计划配置')
|
||
@Controller('admin/pre-planting')
|
||
export class PrePlantingConfigController {
|
||
private readonly logger = new Logger(PrePlantingConfigController.name);
|
||
|
||
constructor(
|
||
private readonly configService: PrePlantingConfigService,
|
||
private readonly proxyService: PrePlantingProxyService,
|
||
private readonly prisma: PrismaService,
|
||
) {}
|
||
|
||
/**
|
||
* 根据 accountSequence 查找该用户的团队成员 accountSequence 列表
|
||
* 逻辑:找到该用户的 userId,然后查找 ancestorPath 包含该 userId 的所有用户
|
||
* 返回的列表包含该用户本人
|
||
*/
|
||
private async resolveTeamAccountSequences(teamOfAccountSeq: string): Promise<string[]> {
|
||
// 1. 找到 teamOf 用户的 userId
|
||
const leader = await this.prisma.referralQueryView.findUnique({
|
||
where: { accountSequence: teamOfAccountSeq },
|
||
select: { userId: true, accountSequence: true },
|
||
});
|
||
if (!leader) {
|
||
this.logger.warn(`[resolveTeamAccountSequences] 未找到用户: ${teamOfAccountSeq}`);
|
||
return [];
|
||
}
|
||
|
||
// 2. 查找 ancestorPath 包含该 userId 的所有下级用户(PostgreSQL array contains)
|
||
const teamMembers = await this.prisma.referralQueryView.findMany({
|
||
where: {
|
||
ancestorPath: { has: leader.userId },
|
||
},
|
||
select: { accountSequence: true },
|
||
});
|
||
|
||
// 3. 包含团队领导本人
|
||
const sequences = [leader.accountSequence, ...teamMembers.map((m) => m.accountSequence)];
|
||
this.logger.debug(`[resolveTeamAccountSequences] ${teamOfAccountSeq} 团队成员数: ${sequences.length}`);
|
||
return sequences;
|
||
}
|
||
|
||
@Get('config')
|
||
@ApiOperation({ summary: '获取预种计划开关状态(含协议文本)' })
|
||
@ApiResponse({ status: HttpStatus.OK, description: '开关状态' })
|
||
async getConfig() {
|
||
const config = await this.configService.getConfig();
|
||
const agreementText = await this.configService.getAgreement();
|
||
return { ...config, agreementText };
|
||
}
|
||
|
||
@Post('config')
|
||
@HttpCode(HttpStatus.OK)
|
||
@ApiOperation({ summary: '更新预种计划开关状态' })
|
||
@ApiResponse({ status: HttpStatus.OK, description: '更新成功' })
|
||
async updateConfig(@Body() dto: UpdatePrePlantingConfigDto) {
|
||
return this.configService.updateConfig(dto.isActive, dto.updatedBy);
|
||
}
|
||
|
||
// ============================================
|
||
// [2026-02-27] 新增:预种管理端点(toggle + 数据查询代理)
|
||
// ============================================
|
||
|
||
@Put('config/toggle')
|
||
@HttpCode(HttpStatus.OK)
|
||
@ApiOperation({ summary: '切换预种计划开关' })
|
||
@ApiResponse({ status: HttpStatus.OK, description: '切换成功' })
|
||
async toggleConfig(@Body() dto: TogglePrePlantingConfigDto) {
|
||
return this.configService.updateConfig(dto.isActive);
|
||
}
|
||
|
||
@Get('orders')
|
||
@ApiOperation({ summary: '预种订单列表(管理员视角)' })
|
||
@ApiQuery({ name: 'page', required: false })
|
||
@ApiQuery({ name: 'pageSize', required: false })
|
||
@ApiQuery({ name: 'keyword', required: false })
|
||
@ApiQuery({ name: 'status', required: false })
|
||
@ApiQuery({ name: 'teamOf', required: false, description: '团队筛选:指定用户 accountSequence,只显示其团队成员的订单' })
|
||
async getOrders(
|
||
@Query('page') page?: string,
|
||
@Query('pageSize') pageSize?: string,
|
||
@Query('keyword') keyword?: string,
|
||
@Query('status') status?: string,
|
||
@Query('teamOf') teamOf?: string,
|
||
) {
|
||
let accountSequences: string[] | undefined;
|
||
if (teamOf) {
|
||
accountSequences = await this.resolveTeamAccountSequences(teamOf);
|
||
if (accountSequences.length === 0) {
|
||
return { items: [], total: 0, page: page ? parseInt(page, 10) : 1, pageSize: pageSize ? parseInt(pageSize, 10) : 20 };
|
||
}
|
||
}
|
||
|
||
return this.proxyService.getOrders({
|
||
page: page ? parseInt(page, 10) : undefined,
|
||
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
|
||
keyword: keyword || undefined,
|
||
status: status || undefined,
|
||
accountSequences,
|
||
});
|
||
}
|
||
|
||
@Get('positions')
|
||
@ApiOperation({ summary: '预种持仓列表(管理员视角)' })
|
||
@ApiQuery({ name: 'page', required: false })
|
||
@ApiQuery({ name: 'pageSize', required: false })
|
||
@ApiQuery({ name: 'keyword', required: false })
|
||
@ApiQuery({ name: 'teamOf', required: false, description: '团队筛选:指定用户 accountSequence,只显示其团队成员的持仓' })
|
||
async getPositions(
|
||
@Query('page') page?: string,
|
||
@Query('pageSize') pageSize?: string,
|
||
@Query('keyword') keyword?: string,
|
||
@Query('teamOf') teamOf?: string,
|
||
) {
|
||
let accountSequences: string[] | undefined;
|
||
if (teamOf) {
|
||
accountSequences = await this.resolveTeamAccountSequences(teamOf);
|
||
if (accountSequences.length === 0) {
|
||
return { items: [], total: 0, page: page ? parseInt(page, 10) : 1, pageSize: pageSize ? parseInt(pageSize, 10) : 20 };
|
||
}
|
||
}
|
||
|
||
return this.proxyService.getPositions({
|
||
page: page ? parseInt(page, 10) : undefined,
|
||
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
|
||
keyword: keyword || undefined,
|
||
accountSequences,
|
||
});
|
||
}
|
||
|
||
@Get('merges')
|
||
@ApiOperation({ summary: '预种合并记录列表(管理员视角)' })
|
||
@ApiQuery({ name: 'page', required: false })
|
||
@ApiQuery({ name: 'pageSize', required: false })
|
||
@ApiQuery({ name: 'keyword', required: false })
|
||
@ApiQuery({ name: 'status', required: false })
|
||
async getMerges(
|
||
@Query('page') page?: string,
|
||
@Query('pageSize') pageSize?: string,
|
||
@Query('keyword') keyword?: string,
|
||
@Query('status') status?: string,
|
||
) {
|
||
return this.proxyService.getMerges({
|
||
page: page ? parseInt(page, 10) : undefined,
|
||
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
|
||
keyword: keyword || undefined,
|
||
status: status || undefined,
|
||
});
|
||
}
|
||
|
||
@Get('stats')
|
||
@ApiOperation({ summary: '预种统计汇总' })
|
||
async getStats() {
|
||
return this.proxyService.getStats();
|
||
}
|
||
|
||
// ============================================
|
||
// [2026-02-28] 新增:预种协议管理
|
||
// ============================================
|
||
|
||
@Get('agreement')
|
||
@ApiOperation({ summary: '获取预种协议文本' })
|
||
@ApiResponse({ status: HttpStatus.OK, description: '协议文本' })
|
||
async getAgreement() {
|
||
const text = await this.configService.getAgreement();
|
||
return { text };
|
||
}
|
||
|
||
@Put('agreement')
|
||
@HttpCode(HttpStatus.OK)
|
||
@ApiOperation({ summary: '更新预种协议文本' })
|
||
@ApiResponse({ status: HttpStatus.OK, description: '更新成功' })
|
||
async updateAgreement(@Body() dto: UpdatePrePlantingAgreementDto) {
|
||
return this.configService.updateAgreement(dto.text);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 公开 API(供 planting-service 调用)
|
||
*/
|
||
@ApiTags('预种计划配置-内部API')
|
||
@Controller('api/v1/admin/pre-planting')
|
||
export class PublicPrePlantingConfigController {
|
||
constructor(
|
||
private readonly configService: PrePlantingConfigService,
|
||
) {}
|
||
|
||
@Get('config')
|
||
@ApiOperation({ summary: '获取预种计划开关状态(内部API,含协议文本)' })
|
||
async getConfig() {
|
||
const config = await this.configService.getConfig();
|
||
const agreementText = await this.configService.getAgreement();
|
||
return { ...config, agreementText };
|
||
}
|
||
}
|