import { Controller, Get, Param, Query, HttpCode, HttpStatus, NotFoundException, Inject, } from '@nestjs/common'; import { ListUsersDto } from '../dto/request/user-query.dto'; import { UserListResponseDto, UserListItemDto, UserDetailDto } from '../dto/response/user.dto'; import { IUserQueryRepository, USER_QUERY_REPOSITORY, UserQueryItem, UserQueryFilters, UserQuerySort, } from '../../domain/repositories/user-query.repository'; import { IUserDetailQueryRepository, USER_DETAIL_QUERY_REPOSITORY, } from '../../domain/repositories/user-detail-query.repository'; /** * 用户管理控制器 * * 为 admin-web 提供用户查询接口 */ @Controller('admin/users') export class UserController { constructor( @Inject(USER_QUERY_REPOSITORY) private readonly userQueryRepository: IUserQueryRepository, @Inject(USER_DETAIL_QUERY_REPOSITORY) private readonly userDetailRepository: IUserDetailQueryRepository, ) {} /** * 获取用户列表 * GET /admin/users */ @Get() @HttpCode(HttpStatus.OK) async listUsers(@Query() query: ListUsersDto): Promise { // 构建筛选条件 const filters: UserQueryFilters = { keyword: query.keyword, status: query.status, kycStatus: query.kycStatus, hasInviter: query.hasInviter, minAdoptions: query.minAdoptions, maxAdoptions: query.maxAdoptions, registeredAfter: query.registeredAfter ? new Date(query.registeredAfter) : undefined, registeredBefore: query.registeredBefore ? new Date(query.registeredBefore) : undefined, }; // 构建排序条件 const sort: UserQuerySort | undefined = query.sortBy ? { field: query.sortBy as UserQuerySort['field'], order: query.sortOrder || 'desc', } : undefined; // 查询数据 const result = await this.userQueryRepository.findMany( filters, { page: query.page || 1, pageSize: query.pageSize || 10 }, sort, ); // 批量获取实时统计数据 const accountSequences = result.items.map(item => item.accountSequence); const statsMap = await this.userDetailRepository.getBatchUserStats(accountSequences); // 获取所有用户的团队总认种数用于计算百分比(使用实时数据) let totalTeamAdoptions = 0; for (const stats of statsMap.values()) { totalTeamAdoptions += stats.teamAdoptionCount; } return { items: result.items.map((item) => this.mapToListItem(item, totalTeamAdoptions, statsMap.get(item.accountSequence))), total: result.total, page: result.page, pageSize: result.pageSize, totalPages: result.totalPages, }; } /** * 获取用户详情 * GET /admin/users/:id */ @Get(':id') @HttpCode(HttpStatus.OK) async getUserDetail(@Param('id') id: string): Promise { let user: UserQueryItem | null = null; // 尝试作为账号序列号查询 if (id.startsWith('D')) { user = await this.userQueryRepository.findByAccountSequence(id); } // 如果不是序列号或未找到,尝试作为 userId 查询 if (!user) { try { const userId = BigInt(id); user = await this.userQueryRepository.findById(userId); } catch { // 无法转换为 BigInt,忽略 } } if (!user) { throw new NotFoundException(`用户 ${id} 不存在`); } return this.mapToDetail(user); } /** * 获取用户统计信息 * GET /admin/users/stats/summary */ @Get('stats/summary') @HttpCode(HttpStatus.OK) async getUserStats(): Promise<{ totalUsers: number; activeUsers: number; frozenUsers: number; verifiedUsers: number; }> { const [total, active, frozen, verified] = await Promise.all([ this.userQueryRepository.count(), this.userQueryRepository.count({ status: 'ACTIVE' }), this.userQueryRepository.count({ status: 'FROZEN' }), this.userQueryRepository.count({ kycStatus: 'VERIFIED' }), ]); return { totalUsers: total, activeUsers: active, frozenUsers: frozen, verifiedUsers: verified, }; } // ==================== Private Methods ==================== private mapToListItem( item: UserQueryItem, totalTeamAdoptions: number, realTimeStats?: { personalAdoptionCount: number; teamAddressCount: number; teamAdoptionCount: number; provinceAdoptionCount: number; cityAdoptionCount: number; }, ): UserListItemDto { // 使用实时统计数据(如果有),否则使用预计算数据 const personalAdoptions = realTimeStats?.personalAdoptionCount ?? item.personalAdoptionCount; const teamAddresses = realTimeStats?.teamAddressCount ?? item.teamAddressCount; const teamAdoptions = realTimeStats?.teamAdoptionCount ?? item.teamAdoptionCount; const provinceAdoptions = realTimeStats?.provinceAdoptionCount ?? item.provinceAdoptionCount; const cityAdoptions = realTimeStats?.cityAdoptionCount ?? item.cityAdoptionCount; // 计算省市认种百分比(相对于该用户的团队总认种量) const userTeamAdoptions = teamAdoptions > 0 ? teamAdoptions : 1; const provincePercentage = teamAdoptions > 0 ? Math.round((provinceAdoptions / userTeamAdoptions) * 100) : 0; const cityPercentage = teamAdoptions > 0 ? Math.round((cityAdoptions / userTeamAdoptions) * 100) : 0; return { accountId: item.userId.toString(), accountSequence: item.accountSequence, avatar: item.avatarUrl, nickname: item.nickname, phoneNumberMasked: item.phoneNumberMasked, personalAdoptions, teamAddresses, teamAdoptions, provincialAdoptions: { count: provinceAdoptions, percentage: provincePercentage, }, cityAdoptions: { count: cityAdoptions, percentage: cityPercentage, }, referrerId: item.inviterSequence, ranking: item.leaderboardRank, status: this.mapStatus(item.status), isOnline: item.isOnline, }; } private mapToDetail(item: UserQueryItem): UserDetailDto { const listItem = this.mapToListItem(item, 0); return { ...listItem, kycStatus: item.kycStatus, registeredAt: item.registeredAt.toISOString(), lastActiveAt: item.lastActiveAt?.toISOString() || null, }; } 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'; } } }