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:
parent
6a05150017
commit
23994a23be
|
|
@ -17,6 +17,10 @@ import {
|
||||||
UserQueryFilters,
|
UserQueryFilters,
|
||||||
UserQuerySort,
|
UserQuerySort,
|
||||||
} from '../../domain/repositories/user-query.repository';
|
} 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(
|
constructor(
|
||||||
@Inject(USER_QUERY_REPOSITORY)
|
@Inject(USER_QUERY_REPOSITORY)
|
||||||
private readonly userQueryRepository: IUserQueryRepository,
|
private readonly userQueryRepository: IUserQueryRepository,
|
||||||
|
@Inject(USER_DETAIL_QUERY_REPOSITORY)
|
||||||
|
private readonly userDetailRepository: IUserDetailQueryRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -62,11 +68,18 @@ export class UserController {
|
||||||
sort,
|
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 {
|
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,
|
total: result.total,
|
||||||
page: result.page,
|
page: result.page,
|
||||||
pageSize: result.pageSize,
|
pageSize: result.pageSize,
|
||||||
|
|
@ -134,7 +147,16 @@ export class UserController {
|
||||||
|
|
||||||
// ==================== Private Methods ====================
|
// ==================== 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
|
const provincePercentage = totalTeamAdoptions > 0
|
||||||
? Math.round((item.provinceAdoptionCount / totalTeamAdoptions) * 100)
|
? Math.round((item.provinceAdoptionCount / totalTeamAdoptions) * 100)
|
||||||
|
|
@ -149,9 +171,9 @@ export class UserController {
|
||||||
avatar: item.avatarUrl,
|
avatar: item.avatarUrl,
|
||||||
nickname: item.nickname,
|
nickname: item.nickname,
|
||||||
phoneNumberMasked: item.phoneNumberMasked,
|
phoneNumberMasked: item.phoneNumberMasked,
|
||||||
personalAdoptions: item.personalAdoptionCount,
|
personalAdoptions,
|
||||||
teamAddresses: item.teamAddressCount,
|
teamAddresses,
|
||||||
teamAdoptions: item.teamAdoptionCount,
|
teamAdoptions,
|
||||||
provincialAdoptions: {
|
provincialAdoptions: {
|
||||||
count: item.provinceAdoptionCount,
|
count: item.provinceAdoptionCount,
|
||||||
percentage: provincePercentage,
|
percentage: provincePercentage,
|
||||||
|
|
|
||||||
|
|
@ -261,4 +261,14 @@ export interface IUserDetailQueryRepository {
|
||||||
* 团队包括所有直推和间推用户
|
* 团队包括所有直推和间推用户
|
||||||
*/
|
*/
|
||||||
getTeamStats(accountSequence: string): Promise<{ teamAddressCount: number; teamAdoptionCount: number }>;
|
getTeamStats(accountSequence: string): Promise<{ teamAddressCount: number; teamAdoptionCount: number }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量获取用户实时统计(用于用户列表页面)
|
||||||
|
* 返回 Map<accountSequence, stats>
|
||||||
|
*/
|
||||||
|
getBatchUserStats(accountSequences: string[]): Promise<Map<string, {
|
||||||
|
personalAdoptionCount: number;
|
||||||
|
teamAddressCount: number;
|
||||||
|
teamAdoptionCount: number;
|
||||||
|
}>>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -545,6 +545,78 @@ export class UserDetailQueryRepositoryImpl implements IUserDetailQueryRepository
|
||||||
return { teamAddressCount, teamAdoptionCount };
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 辅助方法
|
// 辅助方法
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue