diff --git a/backend/services/admin-service/src/api/controllers/user.controller.ts b/backend/services/admin-service/src/api/controllers/user.controller.ts index 42ec4fd9..b93f3de2 100644 --- a/backend/services/admin-service/src/api/controllers/user.controller.ts +++ b/backend/services/admin-service/src/api/controllers/user.controller.ts @@ -17,6 +17,10 @@ import { UserQueryFilters, UserQuerySort, } from '../../domain/repositories/user-query.repository'; +import { + IUserDetailQueryRepository, + USER_DETAIL_QUERY_REPOSITORY, +} from '../../domain/repositories/user-detail-query.repository'; /** * 用户管理控制器 @@ -28,6 +32,8 @@ export class UserController { constructor( @Inject(USER_QUERY_REPOSITORY) private readonly userQueryRepository: IUserQueryRepository, + @Inject(USER_DETAIL_QUERY_REPOSITORY) + private readonly userDetailRepository: IUserDetailQueryRepository, ) {} /** @@ -62,11 +68,18 @@ export class UserController { sort, ); - // 获取所有用户的团队总认种数用于计算百分比 - const totalTeamAdoptions = result.items.reduce((sum, item) => sum + item.teamAdoptionCount, 0); + // 批量获取实时统计数据 + 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)), + items: result.items.map((item) => this.mapToListItem(item, totalTeamAdoptions, statsMap.get(item.accountSequence))), total: result.total, page: result.page, pageSize: result.pageSize, @@ -134,7 +147,16 @@ export class UserController { // ==================== Private Methods ==================== - private mapToListItem(item: UserQueryItem, totalTeamAdoptions: number): UserListItemDto { + private mapToListItem( + item: UserQueryItem, + totalTeamAdoptions: number, + realTimeStats?: { personalAdoptionCount: number; teamAddressCount: number; teamAdoptionCount: number }, + ): UserListItemDto { + // 使用实时统计数据(如果有),否则使用预计算数据 + const personalAdoptions = realTimeStats?.personalAdoptionCount ?? item.personalAdoptionCount; + const teamAddresses = realTimeStats?.teamAddressCount ?? item.teamAddressCount; + const teamAdoptions = realTimeStats?.teamAdoptionCount ?? item.teamAdoptionCount; + // 计算省市认种百分比 const provincePercentage = totalTeamAdoptions > 0 ? Math.round((item.provinceAdoptionCount / totalTeamAdoptions) * 100) @@ -149,9 +171,9 @@ export class UserController { avatar: item.avatarUrl, nickname: item.nickname, phoneNumberMasked: item.phoneNumberMasked, - personalAdoptions: item.personalAdoptionCount, - teamAddresses: item.teamAddressCount, - teamAdoptions: item.teamAdoptionCount, + personalAdoptions, + teamAddresses, + teamAdoptions, provincialAdoptions: { count: item.provinceAdoptionCount, percentage: provincePercentage, diff --git a/backend/services/admin-service/src/domain/repositories/user-detail-query.repository.ts b/backend/services/admin-service/src/domain/repositories/user-detail-query.repository.ts index a17a74d3..f5de5b00 100644 --- a/backend/services/admin-service/src/domain/repositories/user-detail-query.repository.ts +++ b/backend/services/admin-service/src/domain/repositories/user-detail-query.repository.ts @@ -261,4 +261,14 @@ export interface IUserDetailQueryRepository { * 团队包括所有直推和间推用户 */ getTeamStats(accountSequence: string): Promise<{ teamAddressCount: number; teamAdoptionCount: number }>; + + /** + * 批量获取用户实时统计(用于用户列表页面) + * 返回 Map + */ + getBatchUserStats(accountSequences: string[]): Promise>; } diff --git a/backend/services/admin-service/src/infrastructure/persistence/repositories/user-detail-query.repository.impl.ts b/backend/services/admin-service/src/infrastructure/persistence/repositories/user-detail-query.repository.impl.ts index 1fc47857..1accf6ee 100644 --- a/backend/services/admin-service/src/infrastructure/persistence/repositories/user-detail-query.repository.impl.ts +++ b/backend/services/admin-service/src/infrastructure/persistence/repositories/user-detail-query.repository.impl.ts @@ -545,6 +545,78 @@ export class UserDetailQueryRepositoryImpl implements IUserDetailQueryRepository return { teamAddressCount, teamAdoptionCount }; } + async getBatchUserStats(accountSequences: string[]): Promise> { + const result = new Map(); + + if (accountSequences.length === 0) return result; + + // 1. 批量获取个人认种量 + const personalAdoptionCounts = await this.prisma.plantingOrderQueryView.groupBy({ + by: ['accountSequence'], + where: { + accountSequence: { in: accountSequences }, + status: 'MINING_ENABLED', + }, + _count: { id: true }, + }); + const personalAdoptionMap = new Map( + personalAdoptionCounts.map(p => [p.accountSequence, p._count.id]) + ); + + // 2. 批量获取 userId 用于团队统计 + const referrals = await this.prisma.referralQueryView.findMany({ + where: { accountSequence: { in: accountSequences } }, + select: { accountSequence: true, userId: true }, + }); + const userIdMap = new Map(referrals.map(r => [r.accountSequence, r.userId])); + + // 3. 对每个用户计算团队统计(这里需要单独查询,因为 PostgreSQL 数组查询不支持批量) + for (const accountSequence of accountSequences) { + const userId = userIdMap.get(accountSequence); + let teamAddressCount = 0; + let teamAdoptionCount = 0; + + if (userId) { + // 获取团队成员 + const teamMembers = await this.prisma.referralQueryView.findMany({ + where: { + ancestorPath: { has: userId }, + }, + select: { accountSequence: true }, + }); + + teamAddressCount = teamMembers.length; + + // 获取团队认种量 + if (teamMembers.length > 0) { + const count = await this.prisma.plantingOrderQueryView.count({ + where: { + accountSequence: { in: teamMembers.map(m => m.accountSequence) }, + status: 'MINING_ENABLED', + }, + }); + teamAdoptionCount = count; + } + } + + result.set(accountSequence, { + personalAdoptionCount: personalAdoptionMap.get(accountSequence) || 0, + teamAddressCount, + teamAdoptionCount, + }); + } + + return result; + } + // ============================================================================ // 辅助方法 // ============================================================================