481 lines
18 KiB
TypeScript
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';
|
|
}
|
|
}
|
|
|
|
}
|