feat(admin-service): 用户列表页使用实时统计数据

- 添加 getBatchUserStats 批量查询方法
- user.controller 注入 userDetailRepository
- listUsers 接口使用实时统计替代预计算字段

实时查询的字段:
- personalAdoptionCount: 个人认种量
- teamAddressCount: 团队地址数
- teamAdoptionCount: 团队认种量

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-07 22:56:07 -08:00
parent 6a05150017
commit 23994a23be
3 changed files with 111 additions and 7 deletions

View File

@ -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,

View File

@ -261,4 +261,14 @@ export interface IUserDetailQueryRepository {
*
*/
getTeamStats(accountSequence: string): Promise<{ teamAddressCount: number; teamAdoptionCount: number }>;
/**
*
* Map<accountSequence, stats>
*/
getBatchUserStats(accountSequences: string[]): Promise<Map<string, {
personalAdoptionCount: number;
teamAddressCount: number;
teamAdoptionCount: number;
}>>;
}

View File

@ -545,6 +545,78 @@ export class UserDetailQueryRepositoryImpl implements IUserDetailQueryRepository
return { teamAddressCount, teamAdoptionCount };
}
async getBatchUserStats(accountSequences: string[]): Promise<Map<string, {
personalAdoptionCount: number;
teamAddressCount: number;
teamAdoptionCount: number;
}>> {
const result = new Map<string, {
personalAdoptionCount: number;
teamAddressCount: number;
teamAdoptionCount: number;
}>();
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;
}
// ============================================================================
// 辅助方法
// ============================================================================