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 { // 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 }; } }