rwadurian/backend/services/admin-service/src/api/controllers/user.controller.ts

225 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<UserListResponseDto> {
// 构建筛选条件
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<UserDetailDto> {
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';
}
}
}