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

481 lines
18 KiB
TypeScript

import {
Controller,
Get,
Param,
Query,
HttpCode,
HttpStatus,
NotFoundException,
Inject,
Logger,
} from '@nestjs/common';
import { ReferralTreeQueryDto, LedgerQueryDto, WalletLedgerQueryDto } from '../dto/request/user-detail-query.dto';
import {
UserFullDetailDto,
ReferralTreeDto,
ReferralNodeDto,
PlantingLedgerResponseDto,
WalletLedgerResponseDto,
AuthorizationDetailResponseDto,
} from '../dto/response/user-detail.dto';
import {
IUserQueryRepository,
USER_QUERY_REPOSITORY,
} from '../../domain/repositories/user-query.repository';
import {
IUserDetailQueryRepository,
USER_DETAIL_QUERY_REPOSITORY,
} from '../../domain/repositories/user-detail-query.repository';
import { ReferralProxyService } from '../../referral/referral-proxy.service';
/**
* 用户详情控制器
* 为 admin-web 用户详情页面提供 API
*/
@Controller('admin/users')
export class UserDetailController {
private readonly logger = new Logger(UserDetailController.name);
constructor(
@Inject(USER_QUERY_REPOSITORY)
private readonly userQueryRepository: IUserQueryRepository,
@Inject(USER_DETAIL_QUERY_REPOSITORY)
private readonly userDetailRepository: IUserDetailQueryRepository,
private readonly referralProxyService: ReferralProxyService,
) {}
/**
* 获取用户完整详情
* GET /admin/users/:accountSequence/full-detail
*/
@Get(':accountSequence/full-detail')
@HttpCode(HttpStatus.OK)
async getFullDetail(
@Param('accountSequence') accountSequence: string,
): Promise<UserFullDetailDto> {
// 获取基本用户信息
const user = await this.userQueryRepository.findByAccountSequence(accountSequence);
if (!user) {
throw new NotFoundException(`用户 ${accountSequence} 不存在`);
}
// 并行获取所有相关数据
const [referralInfo, personalAdoptions, teamStats, directReferralCount, prePlantingStats] = await Promise.all([
this.userDetailRepository.getReferralInfo(accountSequence),
this.userDetailRepository.getPersonalAdoptionCount(accountSequence),
this.userDetailRepository.getTeamStats(accountSequence),
this.userDetailRepository.getDirectReferralCount(accountSequence),
this.referralProxyService.getPrePlantingStats(accountSequence),
]);
// 获取推荐人昵称
let referrerNickname: string | null = null;
let referrerSequence: string | null = null;
if (referralInfo?.referrerId) {
referrerNickname = await this.userDetailRepository.getReferrerNickname(referralInfo.referrerId);
// 获取推荐人的 accountSequence
const referrerUser = await this.userQueryRepository.findById(referralInfo.referrerId);
referrerSequence = referrerUser?.accountSequence || null;
}
return {
accountId: user.userId.toString(),
accountSequence: user.accountSequence,
avatar: user.avatarUrl,
nickname: user.nickname,
phoneNumberMasked: user.phoneNumberMasked,
status: this.mapStatus(user.status),
kycStatus: user.kycStatus,
isOnline: user.isOnline,
registeredAt: user.registeredAt.toISOString(),
lastActiveAt: user.lastActiveAt?.toISOString() || null,
personalAdoptions: personalAdoptions,
selfPrePlantingPortions: prePlantingStats.selfPrePlantingPortions,
teamPrePlantingPortions: prePlantingStats.teamPrePlantingPortions,
teamAddresses: teamStats.teamAddressCount,
teamAdoptions: teamStats.teamAdoptionCount,
provincialAdoptions: {
count: user.provinceAdoptionCount,
percentage: teamStats.teamAdoptionCount > 0
? Math.round((user.provinceAdoptionCount / teamStats.teamAdoptionCount) * 100)
: 0,
},
cityAdoptions: {
count: user.cityAdoptionCount,
percentage: teamStats.teamAdoptionCount > 0
? Math.round((user.cityAdoptionCount / teamStats.teamAdoptionCount) * 100)
: 0,
},
ranking: user.leaderboardRank,
referralInfo: {
myReferralCode: referralInfo?.myReferralCode || '',
usedReferralCode: referralInfo?.usedReferralCode || null,
referrerId: referralInfo?.referrerId?.toString() || null,
referrerSequence,
referrerNickname,
ancestorPath: referralInfo?.ancestorPath?.map((id) => id.toString()).join(',') || null,
depth: referralInfo?.depth || 0,
directReferralCount: directReferralCount, // 使用实时查询的值
activeDirectCount: referralInfo?.activeDirectCount || 0,
},
};
}
/**
* 获取推荐关系树
* GET /admin/users/:accountSequence/referral-tree
*/
@Get(':accountSequence/referral-tree')
@HttpCode(HttpStatus.OK)
async getReferralTree(
@Param('accountSequence') accountSequence: string,
@Query() query: ReferralTreeQueryDto,
): Promise<ReferralTreeDto> {
// 获取当前用户信息
const user = await this.userQueryRepository.findByAccountSequence(accountSequence);
if (!user) {
throw new NotFoundException(`用户 ${accountSequence} 不存在`);
}
// 获取引荐信息和实时统计
const [referralInfo, personalAdoptionCount, directReferralCount, teamStats, prePlantingStats] = await Promise.all([
this.userDetailRepository.getReferralInfo(accountSequence),
this.userDetailRepository.getPersonalAdoptionCount(accountSequence),
this.userDetailRepository.getDirectReferralCount(accountSequence),
this.userDetailRepository.getBatchUserStats([accountSequence]),
this.referralProxyService.getPrePlantingStats(accountSequence),
]);
const currentUserStats = teamStats.get(accountSequence);
const currentUser: ReferralNodeDto = {
accountSequence: user.accountSequence,
userId: user.userId.toString(),
nickname: user.nickname,
avatar: user.avatarUrl,
personalAdoptions: personalAdoptionCount,
teamAdoptions: currentUserStats?.teamAdoptionCount || 0,
selfPrePlantingPortions: prePlantingStats.selfPrePlantingPortions,
teamPrePlantingPortions: prePlantingStats.teamPrePlantingPortions,
depth: referralInfo?.depth || 0,
directReferralCount: directReferralCount,
isCurrentUser: true,
};
let ancestors: ReferralNodeDto[] = [];
let directReferrals: ReferralNodeDto[] = [];
// 收集所有需要查预种的 accountSequences
const allNodeSeqs: string[] = [];
// 向上查询
let ancestorNodes: typeof ancestors extends (infer T)[] ? any[] : never = [];
if (query.direction === 'up' || query.direction === 'both') {
ancestorNodes = await this.userDetailRepository.getAncestors(
accountSequence,
query.depth || 1,
);
allNodeSeqs.push(...ancestorNodes.map((n: any) => n.accountSequence));
}
// 向下查询
let referralNodes: typeof directReferrals extends (infer T)[] ? any[] : never = [];
if (query.direction === 'down' || query.direction === 'both') {
referralNodes = await this.userDetailRepository.getDirectReferrals(accountSequence);
allNodeSeqs.push(...referralNodes.map((n: any) => n.accountSequence));
}
// 批量获取所有节点的预种统计
const batchPrePlanting = allNodeSeqs.length > 0
? await this.referralProxyService.batchGetPrePlantingStats(allNodeSeqs)
: {};
if (ancestorNodes.length > 0) {
ancestors = ancestorNodes.map((node: any) => ({
accountSequence: node.accountSequence,
userId: node.userId.toString(),
nickname: node.nickname,
avatar: node.avatarUrl,
personalAdoptions: node.personalAdoptionCount,
teamAdoptions: node.teamAdoptionCount,
selfPrePlantingPortions: batchPrePlanting[node.accountSequence]?.selfPrePlantingPortions ?? 0,
teamPrePlantingPortions: batchPrePlanting[node.accountSequence]?.teamPrePlantingPortions ?? 0,
depth: node.depth,
directReferralCount: node.directReferralCount,
}));
}
if (referralNodes.length > 0) {
directReferrals = referralNodes.map((node: any) => ({
accountSequence: node.accountSequence,
userId: node.userId.toString(),
nickname: node.nickname,
avatar: node.avatarUrl,
personalAdoptions: node.personalAdoptionCount,
teamAdoptions: node.teamAdoptionCount,
selfPrePlantingPortions: batchPrePlanting[node.accountSequence]?.selfPrePlantingPortions ?? 0,
teamPrePlantingPortions: batchPrePlanting[node.accountSequence]?.teamPrePlantingPortions ?? 0,
depth: node.depth,
directReferralCount: node.directReferralCount,
}));
}
return {
currentUser,
ancestors,
directReferrals,
};
}
/**
* 获取认种分类账
* GET /admin/users/:accountSequence/planting-ledger
*/
@Get(':accountSequence/planting-ledger')
@HttpCode(HttpStatus.OK)
async getPlantingLedger(
@Param('accountSequence') accountSequence: string,
@Query() query: LedgerQueryDto,
): Promise<PlantingLedgerResponseDto> {
this.logger.log(`[getPlantingLedger] 查询认种分类账, accountSequence=${accountSequence}`);
const user = await this.userQueryRepository.findByAccountSequence(accountSequence);
this.logger.log(`[getPlantingLedger] 用户查询结果: ${user ? `userId=${user.userId}` : 'null'}`);
if (!user) {
throw new NotFoundException(`用户 ${accountSequence} 不存在`);
}
const [summary, ledger] = await Promise.all([
this.userDetailRepository.getPlantingSummary(accountSequence),
this.userDetailRepository.getPlantingLedger(
accountSequence,
query.page || 1,
query.pageSize || 20,
query.startDate ? new Date(query.startDate) : undefined,
query.endDate ? new Date(query.endDate) : undefined,
),
]);
this.logger.log(`[getPlantingLedger] 认种汇总: totalOrders=${summary?.totalOrders}, totalTreeCount=${summary?.totalTreeCount}, effectiveTreeCount=${summary?.effectiveTreeCount}`);
this.logger.log(`[getPlantingLedger] 认种流水条数: ${ledger.items.length}, total=${ledger.total}`);
return {
summary: {
totalOrders: summary?.totalOrders || 0,
totalTreeCount: summary?.totalTreeCount || 0,
totalAmount: summary?.totalAmount || '0',
effectiveTreeCount: summary?.effectiveTreeCount || 0,
pendingTreeCount: summary?.pendingTreeCount || 0,
firstPlantingAt: summary?.firstPlantingAt?.toISOString() || null,
lastPlantingAt: summary?.lastPlantingAt?.toISOString() || null,
},
items: ledger.items.map((item) => ({
orderId: item.orderId.toString(),
orderNo: item.orderNo,
treeCount: item.treeCount,
totalAmount: item.totalAmount,
status: item.status,
selectedProvince: item.selectedProvince,
selectedCity: item.selectedCity,
createdAt: item.createdAt.toISOString(),
paidAt: item.paidAt?.toISOString() || null,
fundAllocatedAt: item.fundAllocatedAt?.toISOString() || null,
miningEnabledAt: item.miningEnabledAt?.toISOString() || null,
})),
total: ledger.total,
page: ledger.page,
pageSize: ledger.pageSize,
totalPages: ledger.totalPages,
};
}
/**
* 获取钱包分类账
* GET /admin/users/:accountSequence/wallet-ledger
*/
@Get(':accountSequence/wallet-ledger')
@HttpCode(HttpStatus.OK)
async getWalletLedger(
@Param('accountSequence') accountSequence: string,
@Query() query: WalletLedgerQueryDto,
): Promise<WalletLedgerResponseDto> {
this.logger.log(`[getWalletLedger] 查询钱包分类账, accountSequence=${accountSequence}`);
const user = await this.userQueryRepository.findByAccountSequence(accountSequence);
this.logger.log(`[getWalletLedger] 用户查询结果: ${user ? `userId=${user.userId}` : 'null'}`);
if (!user) {
throw new NotFoundException(`用户 ${accountSequence} 不存在`);
}
const [summary, ledger] = await Promise.all([
this.userDetailRepository.getWalletSummary(accountSequence),
this.userDetailRepository.getWalletLedger(
accountSequence,
query.page || 1,
query.pageSize || 20,
{
assetType: query.assetType,
entryType: query.entryType,
startDate: query.startDate ? new Date(query.startDate) : undefined,
endDate: query.endDate ? new Date(query.endDate) : undefined,
},
),
]);
this.logger.log(`[getWalletLedger] 钱包汇总: usdtAvailable=${summary?.usdtAvailable}, dstAvailable=${summary?.dstAvailable}, hashpower=${summary?.hashpower}`);
this.logger.log(`[getWalletLedger] 钱包流水条数: ${ledger.items.length}, total=${ledger.total}`);
return {
summary: {
usdtAvailable: summary?.usdtAvailable || '0',
usdtFrozen: summary?.usdtFrozen || '0',
dstAvailable: summary?.dstAvailable || '0',
dstFrozen: summary?.dstFrozen || '0',
bnbAvailable: summary?.bnbAvailable || '0',
bnbFrozen: summary?.bnbFrozen || '0',
ogAvailable: summary?.ogAvailable || '0',
ogFrozen: summary?.ogFrozen || '0',
rwadAvailable: summary?.rwadAvailable || '0',
rwadFrozen: summary?.rwadFrozen || '0',
hashpower: summary?.hashpower || '0',
pendingUsdt: summary?.pendingUsdt || '0',
pendingHashpower: summary?.pendingHashpower || '0',
settleableUsdt: summary?.settleableUsdt || '0',
settleableHashpower: summary?.settleableHashpower || '0',
settledTotalUsdt: summary?.settledTotalUsdt || '0',
settledTotalHashpower: summary?.settledTotalHashpower || '0',
expiredTotalUsdt: summary?.expiredTotalUsdt || '0',
expiredTotalHashpower: summary?.expiredTotalHashpower || '0',
},
items: ledger.items.map((item) => ({
entryId: item.entryId.toString(),
entryType: item.entryType,
assetType: item.assetType,
amount: item.amount,
balanceAfter: item.balanceAfter,
refOrderId: item.refOrderId,
refTxHash: item.refTxHash,
memo: item.memo,
createdAt: item.createdAt.toISOString(),
})),
total: ledger.total,
page: ledger.page,
pageSize: ledger.pageSize,
totalPages: ledger.totalPages,
};
}
/**
* 获取授权详情
* GET /admin/users/:accountSequence/authorization-detail
*/
@Get(':accountSequence/authorization-detail')
@HttpCode(HttpStatus.OK)
async getAuthorizationDetail(
@Param('accountSequence') accountSequence: string,
): Promise<AuthorizationDetailResponseDto> {
const user = await this.userQueryRepository.findByAccountSequence(accountSequence);
if (!user) {
throw new NotFoundException(`用户 ${accountSequence} 不存在`);
}
const [roles, assessments, benefitAssessments, systemLedger] = await Promise.all([
this.userDetailRepository.getAuthorizationRoles(accountSequence),
this.userDetailRepository.getMonthlyAssessments(accountSequence),
this.userDetailRepository.getBenefitAssessments(accountSequence),
this.userDetailRepository.getSystemAccountLedger(accountSequence),
]);
return {
roles: roles.map((role) => ({
id: role.id,
roleType: role.roleType,
regionCode: role.regionCode,
regionName: role.regionName,
displayTitle: role.displayTitle,
status: role.status,
benefitActive: role.benefitActive,
benefitActivatedAt: role.benefitActivatedAt?.toISOString() || null,
authorizedAt: role.authorizedAt?.toISOString() || null,
authorizedBy: role.authorizedBy,
initialTargetTreeCount: role.initialTargetTreeCount,
monthlyTargetType: role.monthlyTargetType,
lastAssessmentMonth: role.lastAssessmentMonth,
monthlyTreesAdded: role.monthlyTreesAdded,
officePhotoUrls: role.officePhotoUrls,
createdAt: role.createdAt.toISOString(),
})),
assessments: assessments.map((assessment) => ({
id: assessment.id,
authorizationId: assessment.authorizationId,
roleType: assessment.roleType,
regionCode: assessment.regionCode,
assessmentMonth: assessment.assessmentMonth,
monthIndex: assessment.monthIndex,
monthlyTarget: assessment.monthlyTarget,
monthlyCompleted: assessment.monthlyCompleted,
cumulativeTarget: assessment.cumulativeTarget,
cumulativeCompleted: assessment.cumulativeCompleted,
result: assessment.result,
rankingInRegion: assessment.rankingInRegion,
isFirstPlace: assessment.isFirstPlace,
isBypassed: assessment.isBypassed,
completedAt: assessment.completedAt?.toISOString() || null,
assessedAt: assessment.assessedAt?.toISOString() || null,
})),
// [2026-01-08] 新增:权益考核记录
benefitAssessments: benefitAssessments.map((ba) => ({
id: ba.id,
authorizationId: ba.authorizationId,
roleType: ba.roleType,
regionCode: ba.regionCode,
regionName: ba.regionName,
assessmentMonth: ba.assessmentMonth,
monthIndex: ba.monthIndex,
monthlyTarget: ba.monthlyTarget,
cumulativeTarget: ba.cumulativeTarget,
treesCompleted: ba.treesCompleted,
treesRequired: ba.treesRequired,
benefitActionTaken: ba.benefitActionTaken,
previousBenefitStatus: ba.previousBenefitStatus,
newBenefitStatus: ba.newBenefitStatus,
newValidUntil: ba.newValidUntil?.toISOString() || null,
result: ba.result,
remarks: ba.remarks,
assessedAt: ba.assessedAt.toISOString(),
createdAt: ba.createdAt.toISOString(),
})),
systemAccountLedger: systemLedger.map((ledger) => ({
ledgerId: ledger.ledgerId.toString(),
accountId: ledger.accountId.toString(),
accountType: ledger.accountType,
entryType: ledger.entryType,
amount: ledger.amount,
balanceAfter: ledger.balanceAfter,
sourceOrderId: ledger.sourceOrderId?.toString() || null,
sourceRewardId: ledger.sourceRewardId?.toString() || null,
txHash: ledger.txHash,
memo: ledger.memo,
createdAt: ledger.createdAt.toISOString(),
})),
};
}
// ============================================================================
// 辅助方法
// ============================================================================
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';
}
}
}