import { Controller, Get, Param, Query, HttpCode, HttpStatus, NotFoundException, Inject, Logger, } from '@nestjs/common'; import { ReferralTreeQueryDto, LedgerQueryDto, WalletLedgerQueryDto } from '../dto/request/user-detail-query.dto'; import { UserFullDetailDto, ReferralTreeDto, ReferralNodeDto, PlantingLedgerResponseDto, WalletLedgerResponseDto, AuthorizationDetailResponseDto, } from '../dto/response/user-detail.dto'; import { IUserQueryRepository, USER_QUERY_REPOSITORY, } from '../../domain/repositories/user-query.repository'; import { IUserDetailQueryRepository, USER_DETAIL_QUERY_REPOSITORY, } from '../../domain/repositories/user-detail-query.repository'; import { ReferralProxyService } from '../../referral/referral-proxy.service'; /** * 用户详情控制器 * 为 admin-web 用户详情页面提供 API */ @Controller('admin/users') export class UserDetailController { private readonly logger = new Logger(UserDetailController.name); constructor( @Inject(USER_QUERY_REPOSITORY) private readonly userQueryRepository: IUserQueryRepository, @Inject(USER_DETAIL_QUERY_REPOSITORY) private readonly userDetailRepository: IUserDetailQueryRepository, private readonly referralProxyService: ReferralProxyService, ) {} /** * 获取用户完整详情 * GET /admin/users/:accountSequence/full-detail */ @Get(':accountSequence/full-detail') @HttpCode(HttpStatus.OK) async getFullDetail( @Param('accountSequence') accountSequence: string, ): Promise { // 获取基本用户信息 const user = await this.userQueryRepository.findByAccountSequence(accountSequence); if (!user) { throw new NotFoundException(`用户 ${accountSequence} 不存在`); } // 并行获取所有相关数据 const [referralInfo, personalAdoptions, teamStats, directReferralCount, prePlantingStats] = await Promise.all([ this.userDetailRepository.getReferralInfo(accountSequence), this.userDetailRepository.getPersonalAdoptionCount(accountSequence), this.userDetailRepository.getTeamStats(accountSequence), this.userDetailRepository.getDirectReferralCount(accountSequence), this.referralProxyService.getPrePlantingStats(accountSequence), ]); // 获取推荐人昵称 let referrerNickname: string | null = null; let referrerSequence: string | null = null; if (referralInfo?.referrerId) { referrerNickname = await this.userDetailRepository.getReferrerNickname(referralInfo.referrerId); // 获取推荐人的 accountSequence const referrerUser = await this.userQueryRepository.findById(referralInfo.referrerId); referrerSequence = referrerUser?.accountSequence || null; } return { accountId: user.userId.toString(), accountSequence: user.accountSequence, avatar: user.avatarUrl, nickname: user.nickname, phoneNumberMasked: user.phoneNumberMasked, status: this.mapStatus(user.status), kycStatus: user.kycStatus, isOnline: user.isOnline, registeredAt: user.registeredAt.toISOString(), lastActiveAt: user.lastActiveAt?.toISOString() || null, personalAdoptions: personalAdoptions, selfPrePlantingPortions: prePlantingStats.selfPrePlantingPortions, teamPrePlantingPortions: prePlantingStats.teamPrePlantingPortions, teamAddresses: teamStats.teamAddressCount, teamAdoptions: teamStats.teamAdoptionCount, provincialAdoptions: { count: user.provinceAdoptionCount, percentage: teamStats.teamAdoptionCount > 0 ? Math.round((user.provinceAdoptionCount / teamStats.teamAdoptionCount) * 100) : 0, }, cityAdoptions: { count: user.cityAdoptionCount, percentage: teamStats.teamAdoptionCount > 0 ? Math.round((user.cityAdoptionCount / teamStats.teamAdoptionCount) * 100) : 0, }, ranking: user.leaderboardRank, referralInfo: { myReferralCode: referralInfo?.myReferralCode || '', usedReferralCode: referralInfo?.usedReferralCode || null, referrerId: referralInfo?.referrerId?.toString() || null, referrerSequence, referrerNickname, ancestorPath: referralInfo?.ancestorPath?.map((id) => id.toString()).join(',') || null, depth: referralInfo?.depth || 0, directReferralCount: directReferralCount, // 使用实时查询的值 activeDirectCount: referralInfo?.activeDirectCount || 0, }, }; } /** * 获取推荐关系树 * GET /admin/users/:accountSequence/referral-tree */ @Get(':accountSequence/referral-tree') @HttpCode(HttpStatus.OK) async getReferralTree( @Param('accountSequence') accountSequence: string, @Query() query: ReferralTreeQueryDto, ): Promise { // 获取当前用户信息 const user = await this.userQueryRepository.findByAccountSequence(accountSequence); if (!user) { throw new NotFoundException(`用户 ${accountSequence} 不存在`); } // 获取引荐信息和实时统计 const [referralInfo, personalAdoptionCount, directReferralCount, teamStats, prePlantingStats] = await Promise.all([ this.userDetailRepository.getReferralInfo(accountSequence), this.userDetailRepository.getPersonalAdoptionCount(accountSequence), this.userDetailRepository.getDirectReferralCount(accountSequence), this.userDetailRepository.getBatchUserStats([accountSequence]), this.referralProxyService.getPrePlantingStats(accountSequence), ]); const currentUserStats = teamStats.get(accountSequence); const currentUser: ReferralNodeDto = { accountSequence: user.accountSequence, userId: user.userId.toString(), nickname: user.nickname, avatar: user.avatarUrl, personalAdoptions: personalAdoptionCount, teamAdoptions: currentUserStats?.teamAdoptionCount || 0, selfPrePlantingPortions: prePlantingStats.selfPrePlantingPortions, teamPrePlantingPortions: prePlantingStats.teamPrePlantingPortions, depth: referralInfo?.depth || 0, directReferralCount: directReferralCount, isCurrentUser: true, }; let ancestors: ReferralNodeDto[] = []; let directReferrals: ReferralNodeDto[] = []; // 收集所有需要查预种的 accountSequences const allNodeSeqs: string[] = []; // 向上查询 let ancestorNodes: typeof ancestors extends (infer T)[] ? any[] : never = []; if (query.direction === 'up' || query.direction === 'both') { ancestorNodes = await this.userDetailRepository.getAncestors( accountSequence, query.depth || 1, ); allNodeSeqs.push(...ancestorNodes.map((n: any) => n.accountSequence)); } // 向下查询 let referralNodes: typeof directReferrals extends (infer T)[] ? any[] : never = []; if (query.direction === 'down' || query.direction === 'both') { referralNodes = await this.userDetailRepository.getDirectReferrals(accountSequence); allNodeSeqs.push(...referralNodes.map((n: any) => n.accountSequence)); } // 批量获取所有节点的预种统计 const batchPrePlanting = allNodeSeqs.length > 0 ? await this.referralProxyService.batchGetPrePlantingStats(allNodeSeqs) : {}; if (ancestorNodes.length > 0) { ancestors = ancestorNodes.map((node: any) => ({ accountSequence: node.accountSequence, userId: node.userId.toString(), nickname: node.nickname, avatar: node.avatarUrl, personalAdoptions: node.personalAdoptionCount, teamAdoptions: node.teamAdoptionCount, selfPrePlantingPortions: batchPrePlanting[node.accountSequence]?.selfPrePlantingPortions ?? 0, teamPrePlantingPortions: batchPrePlanting[node.accountSequence]?.teamPrePlantingPortions ?? 0, depth: node.depth, directReferralCount: node.directReferralCount, })); } if (referralNodes.length > 0) { directReferrals = referralNodes.map((node: any) => ({ accountSequence: node.accountSequence, userId: node.userId.toString(), nickname: node.nickname, avatar: node.avatarUrl, personalAdoptions: node.personalAdoptionCount, teamAdoptions: node.teamAdoptionCount, selfPrePlantingPortions: batchPrePlanting[node.accountSequence]?.selfPrePlantingPortions ?? 0, teamPrePlantingPortions: batchPrePlanting[node.accountSequence]?.teamPrePlantingPortions ?? 0, depth: node.depth, directReferralCount: node.directReferralCount, })); } return { currentUser, ancestors, directReferrals, }; } /** * 获取认种分类账 * GET /admin/users/:accountSequence/planting-ledger */ @Get(':accountSequence/planting-ledger') @HttpCode(HttpStatus.OK) async getPlantingLedger( @Param('accountSequence') accountSequence: string, @Query() query: LedgerQueryDto, ): Promise { this.logger.log(`[getPlantingLedger] 查询认种分类账, accountSequence=${accountSequence}`); const user = await this.userQueryRepository.findByAccountSequence(accountSequence); this.logger.log(`[getPlantingLedger] 用户查询结果: ${user ? `userId=${user.userId}` : 'null'}`); if (!user) { throw new NotFoundException(`用户 ${accountSequence} 不存在`); } const [summary, ledger] = await Promise.all([ this.userDetailRepository.getPlantingSummary(accountSequence), this.userDetailRepository.getPlantingLedger( accountSequence, query.page || 1, query.pageSize || 20, query.startDate ? new Date(query.startDate) : undefined, query.endDate ? new Date(query.endDate) : undefined, ), ]); this.logger.log(`[getPlantingLedger] 认种汇总: totalOrders=${summary?.totalOrders}, totalTreeCount=${summary?.totalTreeCount}, effectiveTreeCount=${summary?.effectiveTreeCount}`); this.logger.log(`[getPlantingLedger] 认种流水条数: ${ledger.items.length}, total=${ledger.total}`); return { summary: { totalOrders: summary?.totalOrders || 0, totalTreeCount: summary?.totalTreeCount || 0, totalAmount: summary?.totalAmount || '0', effectiveTreeCount: summary?.effectiveTreeCount || 0, pendingTreeCount: summary?.pendingTreeCount || 0, firstPlantingAt: summary?.firstPlantingAt?.toISOString() || null, lastPlantingAt: summary?.lastPlantingAt?.toISOString() || null, }, items: ledger.items.map((item) => ({ orderId: item.orderId.toString(), orderNo: item.orderNo, treeCount: item.treeCount, totalAmount: item.totalAmount, status: item.status, selectedProvince: item.selectedProvince, selectedCity: item.selectedCity, createdAt: item.createdAt.toISOString(), paidAt: item.paidAt?.toISOString() || null, fundAllocatedAt: item.fundAllocatedAt?.toISOString() || null, miningEnabledAt: item.miningEnabledAt?.toISOString() || null, })), total: ledger.total, page: ledger.page, pageSize: ledger.pageSize, totalPages: ledger.totalPages, }; } /** * 获取钱包分类账 * GET /admin/users/:accountSequence/wallet-ledger */ @Get(':accountSequence/wallet-ledger') @HttpCode(HttpStatus.OK) async getWalletLedger( @Param('accountSequence') accountSequence: string, @Query() query: WalletLedgerQueryDto, ): Promise { this.logger.log(`[getWalletLedger] 查询钱包分类账, accountSequence=${accountSequence}`); const user = await this.userQueryRepository.findByAccountSequence(accountSequence); this.logger.log(`[getWalletLedger] 用户查询结果: ${user ? `userId=${user.userId}` : 'null'}`); if (!user) { throw new NotFoundException(`用户 ${accountSequence} 不存在`); } const [summary, ledger] = await Promise.all([ this.userDetailRepository.getWalletSummary(accountSequence), this.userDetailRepository.getWalletLedger( accountSequence, query.page || 1, query.pageSize || 20, { assetType: query.assetType, entryType: query.entryType, startDate: query.startDate ? new Date(query.startDate) : undefined, endDate: query.endDate ? new Date(query.endDate) : undefined, }, ), ]); this.logger.log(`[getWalletLedger] 钱包汇总: usdtAvailable=${summary?.usdtAvailable}, dstAvailable=${summary?.dstAvailable}, hashpower=${summary?.hashpower}`); this.logger.log(`[getWalletLedger] 钱包流水条数: ${ledger.items.length}, total=${ledger.total}`); return { summary: { usdtAvailable: summary?.usdtAvailable || '0', usdtFrozen: summary?.usdtFrozen || '0', dstAvailable: summary?.dstAvailable || '0', dstFrozen: summary?.dstFrozen || '0', bnbAvailable: summary?.bnbAvailable || '0', bnbFrozen: summary?.bnbFrozen || '0', ogAvailable: summary?.ogAvailable || '0', ogFrozen: summary?.ogFrozen || '0', rwadAvailable: summary?.rwadAvailable || '0', rwadFrozen: summary?.rwadFrozen || '0', hashpower: summary?.hashpower || '0', pendingUsdt: summary?.pendingUsdt || '0', pendingHashpower: summary?.pendingHashpower || '0', settleableUsdt: summary?.settleableUsdt || '0', settleableHashpower: summary?.settleableHashpower || '0', settledTotalUsdt: summary?.settledTotalUsdt || '0', settledTotalHashpower: summary?.settledTotalHashpower || '0', expiredTotalUsdt: summary?.expiredTotalUsdt || '0', expiredTotalHashpower: summary?.expiredTotalHashpower || '0', }, items: ledger.items.map((item) => ({ entryId: item.entryId.toString(), entryType: item.entryType, assetType: item.assetType, amount: item.amount, balanceAfter: item.balanceAfter, refOrderId: item.refOrderId, refTxHash: item.refTxHash, memo: item.memo, createdAt: item.createdAt.toISOString(), })), total: ledger.total, page: ledger.page, pageSize: ledger.pageSize, totalPages: ledger.totalPages, }; } /** * 获取授权详情 * GET /admin/users/:accountSequence/authorization-detail */ @Get(':accountSequence/authorization-detail') @HttpCode(HttpStatus.OK) async getAuthorizationDetail( @Param('accountSequence') accountSequence: string, ): Promise { const user = await this.userQueryRepository.findByAccountSequence(accountSequence); if (!user) { throw new NotFoundException(`用户 ${accountSequence} 不存在`); } const [roles, assessments, benefitAssessments, systemLedger] = await Promise.all([ this.userDetailRepository.getAuthorizationRoles(accountSequence), this.userDetailRepository.getMonthlyAssessments(accountSequence), this.userDetailRepository.getBenefitAssessments(accountSequence), this.userDetailRepository.getSystemAccountLedger(accountSequence), ]); return { roles: roles.map((role) => ({ id: role.id, roleType: role.roleType, regionCode: role.regionCode, regionName: role.regionName, displayTitle: role.displayTitle, status: role.status, benefitActive: role.benefitActive, benefitActivatedAt: role.benefitActivatedAt?.toISOString() || null, authorizedAt: role.authorizedAt?.toISOString() || null, authorizedBy: role.authorizedBy, initialTargetTreeCount: role.initialTargetTreeCount, monthlyTargetType: role.monthlyTargetType, lastAssessmentMonth: role.lastAssessmentMonth, monthlyTreesAdded: role.monthlyTreesAdded, officePhotoUrls: role.officePhotoUrls, createdAt: role.createdAt.toISOString(), })), assessments: assessments.map((assessment) => ({ id: assessment.id, authorizationId: assessment.authorizationId, roleType: assessment.roleType, regionCode: assessment.regionCode, assessmentMonth: assessment.assessmentMonth, monthIndex: assessment.monthIndex, monthlyTarget: assessment.monthlyTarget, monthlyCompleted: assessment.monthlyCompleted, cumulativeTarget: assessment.cumulativeTarget, cumulativeCompleted: assessment.cumulativeCompleted, result: assessment.result, rankingInRegion: assessment.rankingInRegion, isFirstPlace: assessment.isFirstPlace, isBypassed: assessment.isBypassed, completedAt: assessment.completedAt?.toISOString() || null, assessedAt: assessment.assessedAt?.toISOString() || null, })), // [2026-01-08] 新增:权益考核记录 benefitAssessments: benefitAssessments.map((ba) => ({ id: ba.id, authorizationId: ba.authorizationId, roleType: ba.roleType, regionCode: ba.regionCode, regionName: ba.regionName, assessmentMonth: ba.assessmentMonth, monthIndex: ba.monthIndex, monthlyTarget: ba.monthlyTarget, cumulativeTarget: ba.cumulativeTarget, treesCompleted: ba.treesCompleted, treesRequired: ba.treesRequired, benefitActionTaken: ba.benefitActionTaken, previousBenefitStatus: ba.previousBenefitStatus, newBenefitStatus: ba.newBenefitStatus, newValidUntil: ba.newValidUntil?.toISOString() || null, result: ba.result, remarks: ba.remarks, assessedAt: ba.assessedAt.toISOString(), createdAt: ba.createdAt.toISOString(), })), systemAccountLedger: systemLedger.map((ledger) => ({ ledgerId: ledger.ledgerId.toString(), accountId: ledger.accountId.toString(), accountType: ledger.accountType, entryType: ledger.entryType, amount: ledger.amount, balanceAfter: ledger.balanceAfter, sourceOrderId: ledger.sourceOrderId?.toString() || null, sourceRewardId: ledger.sourceRewardId?.toString() || null, txHash: ledger.txHash, memo: ledger.memo, createdAt: ledger.createdAt.toISOString(), })), }; } // ============================================================================ // 辅助方法 // ============================================================================ private mapStatus(status: string): 'active' | 'frozen' | 'deactivated' { switch (status.toUpperCase()) { case 'ACTIVE': return 'active'; case 'FROZEN': return 'frozen'; case 'DEACTIVATED': return 'deactivated'; default: return 'active'; } } }