feat(admin): 实现用户详情页面
前端 (admin-web): - 新增用户详情页面 /users/[id] - 实现推荐关系树可视化,支持点击节点切换视角 - 添加认种分类账Tab,显示汇总和订单明细 - 添加钱包分类账Tab,显示余额汇总和流水明细 - 添加授权信息Tab,显示角色、月度考核和系统账户流水 - 用户列表"查看详情"改为 Link 导航到详情页 后端 (admin-service): - 新增 UserDetailController 提供详情页API - 新增 UserDetailQueryRepository 查询CDC同步的数据 - API: GET /admin/users/:seq/full-detail - API: GET /admin/users/:seq/referral-tree - API: GET /admin/users/:seq/planting-ledger - API: GET /admin/users/:seq/wallet-ledger - API: GET /admin/users/:seq/authorization-detail 🤖 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
d293ec10e4
commit
3d31e8beb9
|
|
@ -0,0 +1,402 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Query,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
NotFoundException,
|
||||
Inject,
|
||||
} 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';
|
||||
|
||||
/**
|
||||
* 用户详情控制器
|
||||
* 为 admin-web 用户详情页面提供 API
|
||||
*/
|
||||
@Controller('admin/users')
|
||||
export class UserDetailController {
|
||||
constructor(
|
||||
@Inject(USER_QUERY_REPOSITORY)
|
||||
private readonly userQueryRepository: IUserQueryRepository,
|
||||
@Inject(USER_DETAIL_QUERY_REPOSITORY)
|
||||
private readonly userDetailRepository: IUserDetailQueryRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 获取用户完整详情
|
||||
* 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 = await this.userDetailRepository.getReferralInfo(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: user.personalAdoptionCount,
|
||||
teamAddresses: user.teamAddressCount,
|
||||
teamAdoptions: user.teamAdoptionCount,
|
||||
provincialAdoptions: {
|
||||
count: user.provinceAdoptionCount,
|
||||
percentage: user.teamAdoptionCount > 0
|
||||
? Math.round((user.provinceAdoptionCount / user.teamAdoptionCount) * 100)
|
||||
: 0,
|
||||
},
|
||||
cityAdoptions: {
|
||||
count: user.cityAdoptionCount,
|
||||
percentage: user.teamAdoptionCount > 0
|
||||
? Math.round((user.cityAdoptionCount / user.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: referralInfo?.directReferralCount || 0,
|
||||
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 = await this.userDetailRepository.getReferralInfo(accountSequence);
|
||||
|
||||
const currentUser: ReferralNodeDto = {
|
||||
accountSequence: user.accountSequence,
|
||||
userId: user.userId.toString(),
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatarUrl,
|
||||
personalAdoptions: user.personalAdoptionCount,
|
||||
depth: referralInfo?.depth || 0,
|
||||
directReferralCount: referralInfo?.directReferralCount || 0,
|
||||
isCurrentUser: true,
|
||||
};
|
||||
|
||||
let ancestors: ReferralNodeDto[] = [];
|
||||
let directReferrals: ReferralNodeDto[] = [];
|
||||
|
||||
// 向上查询
|
||||
if (query.direction === 'up' || query.direction === 'both') {
|
||||
const ancestorNodes = await this.userDetailRepository.getAncestors(
|
||||
accountSequence,
|
||||
query.depth || 1,
|
||||
);
|
||||
ancestors = ancestorNodes.map((node) => ({
|
||||
accountSequence: node.accountSequence,
|
||||
userId: node.userId.toString(),
|
||||
nickname: node.nickname,
|
||||
avatar: node.avatarUrl,
|
||||
personalAdoptions: node.personalAdoptionCount,
|
||||
depth: node.depth,
|
||||
directReferralCount: node.directReferralCount,
|
||||
}));
|
||||
}
|
||||
|
||||
// 向下查询
|
||||
if (query.direction === 'down' || query.direction === 'both') {
|
||||
const referralNodes = await this.userDetailRepository.getDirectReferrals(accountSequence);
|
||||
directReferrals = referralNodes.map((node) => ({
|
||||
accountSequence: node.accountSequence,
|
||||
userId: node.userId.toString(),
|
||||
nickname: node.nickname,
|
||||
avatar: node.avatarUrl,
|
||||
personalAdoptions: node.personalAdoptionCount,
|
||||
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> {
|
||||
const user = await this.userQueryRepository.findByAccountSequence(accountSequence);
|
||||
if (!user) {
|
||||
throw new NotFoundException(`用户 ${accountSequence} 不存在`);
|
||||
}
|
||||
|
||||
const [summary, ledger] = await Promise.all([
|
||||
this.userDetailRepository.getPlantingSummary(user.userId),
|
||||
this.userDetailRepository.getPlantingLedger(
|
||||
user.userId,
|
||||
query.page || 1,
|
||||
query.pageSize || 20,
|
||||
query.startDate ? new Date(query.startDate) : undefined,
|
||||
query.endDate ? new Date(query.endDate) : undefined,
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
summary: {
|
||||
totalOrders: summary?.totalOrders || 0,
|
||||
totalTreeCount: summary?.totalTreeCount || 0,
|
||||
totalAmount: this.formatDecimal(summary?.totalAmount),
|
||||
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: this.formatDecimal(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> {
|
||||
const user = await this.userQueryRepository.findByAccountSequence(accountSequence);
|
||||
if (!user) {
|
||||
throw new NotFoundException(`用户 ${accountSequence} 不存在`);
|
||||
}
|
||||
|
||||
const [summary, ledger] = await Promise.all([
|
||||
this.userDetailRepository.getWalletSummary(user.userId),
|
||||
this.userDetailRepository.getWalletLedger(
|
||||
user.userId,
|
||||
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,
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
summary: {
|
||||
usdtAvailable: this.formatDecimal(summary?.usdtAvailable),
|
||||
usdtFrozen: this.formatDecimal(summary?.usdtFrozen),
|
||||
dstAvailable: this.formatDecimal(summary?.dstAvailable),
|
||||
dstFrozen: this.formatDecimal(summary?.dstFrozen),
|
||||
bnbAvailable: this.formatDecimal(summary?.bnbAvailable),
|
||||
bnbFrozen: this.formatDecimal(summary?.bnbFrozen),
|
||||
ogAvailable: this.formatDecimal(summary?.ogAvailable),
|
||||
ogFrozen: this.formatDecimal(summary?.ogFrozen),
|
||||
rwadAvailable: this.formatDecimal(summary?.rwadAvailable),
|
||||
rwadFrozen: this.formatDecimal(summary?.rwadFrozen),
|
||||
hashpower: this.formatDecimal(summary?.hashpower),
|
||||
pendingUsdt: this.formatDecimal(summary?.pendingUsdt),
|
||||
pendingHashpower: this.formatDecimal(summary?.pendingHashpower),
|
||||
settleableUsdt: this.formatDecimal(summary?.settleableUsdt),
|
||||
settleableHashpower: this.formatDecimal(summary?.settleableHashpower),
|
||||
settledTotalUsdt: this.formatDecimal(summary?.settledTotalUsdt),
|
||||
settledTotalHashpower: this.formatDecimal(summary?.settledTotalHashpower),
|
||||
expiredTotalUsdt: this.formatDecimal(summary?.expiredTotalUsdt),
|
||||
expiredTotalHashpower: this.formatDecimal(summary?.expiredTotalHashpower),
|
||||
},
|
||||
items: ledger.items.map((item) => ({
|
||||
entryId: item.entryId.toString(),
|
||||
entryType: item.entryType,
|
||||
assetType: item.assetType,
|
||||
amount: this.formatDecimal(item.amount),
|
||||
balanceAfter: item.balanceAfter ? this.formatDecimal(item.balanceAfter) : null,
|
||||
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, systemLedger] = await Promise.all([
|
||||
this.userDetailRepository.getAuthorizationRoles(accountSequence),
|
||||
this.userDetailRepository.getMonthlyAssessments(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,
|
||||
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,
|
||||
})),
|
||||
systemAccountLedger: systemLedger.map((ledger) => ({
|
||||
ledgerId: ledger.ledgerId.toString(),
|
||||
accountId: ledger.accountId.toString(),
|
||||
accountType: ledger.accountType,
|
||||
entryType: ledger.entryType,
|
||||
amount: this.formatDecimal(ledger.amount),
|
||||
balanceAfter: this.formatDecimal(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';
|
||||
}
|
||||
}
|
||||
|
||||
private formatDecimal(value: bigint | null | undefined): string {
|
||||
if (!value) return '0';
|
||||
// bigint 已经乘以 1e8,需要转回小数
|
||||
const num = Number(value) / 1e8;
|
||||
return num.toFixed(8).replace(/\.?0+$/, '');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { IsOptional, IsInt, Min, Max, IsIn, IsString, IsDateString } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
/**
|
||||
* 推荐关系树查询参数
|
||||
*/
|
||||
export class ReferralTreeQueryDto {
|
||||
@IsOptional()
|
||||
@IsIn(['up', 'down', 'both'])
|
||||
direction?: 'up' | 'down' | 'both' = 'both';
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => parseInt(value, 10))
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(10)
|
||||
depth?: number = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类账查询参数
|
||||
*/
|
||||
export class LedgerQueryDto {
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => parseInt(value, 10))
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number = 1;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => parseInt(value, 10))
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
pageSize?: number = 20;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
startDate?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 钱包分类账查询参数
|
||||
*/
|
||||
export class WalletLedgerQueryDto extends LedgerQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
assetType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
entryType?: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
/**
|
||||
* 用户详情页面响应 DTO
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// 用户完整信息
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 推荐信息
|
||||
*/
|
||||
export class ReferralInfoDto {
|
||||
myReferralCode!: string;
|
||||
usedReferralCode!: string | null;
|
||||
referrerId!: string | null;
|
||||
referrerSequence!: string | null;
|
||||
referrerNickname!: string | null;
|
||||
ancestorPath!: string | null;
|
||||
depth!: number;
|
||||
directReferralCount!: number;
|
||||
activeDirectCount!: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户完整详情响应
|
||||
*/
|
||||
export class UserFullDetailDto {
|
||||
accountId!: string;
|
||||
accountSequence!: string;
|
||||
avatar!: string | null;
|
||||
nickname!: string | null;
|
||||
phoneNumberMasked!: string | null;
|
||||
status!: 'active' | 'frozen' | 'deactivated';
|
||||
kycStatus!: string;
|
||||
isOnline!: boolean;
|
||||
registeredAt!: string;
|
||||
lastActiveAt!: string | null;
|
||||
|
||||
// 认种统计
|
||||
personalAdoptions!: number;
|
||||
teamAddresses!: number;
|
||||
teamAdoptions!: number;
|
||||
provincialAdoptions!: {
|
||||
count: number;
|
||||
percentage: number;
|
||||
};
|
||||
cityAdoptions!: {
|
||||
count: number;
|
||||
percentage: number;
|
||||
};
|
||||
|
||||
// 排名
|
||||
ranking!: number | null;
|
||||
|
||||
// 推荐信息
|
||||
referralInfo!: ReferralInfoDto;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 推荐关系树
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 推荐关系节点
|
||||
*/
|
||||
export class ReferralNodeDto {
|
||||
accountSequence!: string;
|
||||
userId!: string;
|
||||
nickname!: string | null;
|
||||
avatar!: string | null;
|
||||
personalAdoptions!: number;
|
||||
depth!: number;
|
||||
directReferralCount!: number;
|
||||
isCurrentUser?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推荐关系树响应
|
||||
*/
|
||||
export class ReferralTreeDto {
|
||||
currentUser!: ReferralNodeDto;
|
||||
ancestors!: ReferralNodeDto[]; // 向上的推荐人链
|
||||
directReferrals!: ReferralNodeDto[]; // 直推用户列表
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 认种分类账
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 认种汇总
|
||||
*/
|
||||
export class PlantingSummaryDto {
|
||||
totalOrders!: number;
|
||||
totalTreeCount!: number;
|
||||
totalAmount!: string;
|
||||
effectiveTreeCount!: number;
|
||||
pendingTreeCount!: number;
|
||||
firstPlantingAt!: string | null;
|
||||
lastPlantingAt!: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认种分类账项
|
||||
*/
|
||||
export class PlantingLedgerItemDto {
|
||||
orderId!: string;
|
||||
orderNo!: string;
|
||||
treeCount!: number;
|
||||
totalAmount!: string;
|
||||
status!: string;
|
||||
selectedProvince!: string | null;
|
||||
selectedCity!: string | null;
|
||||
createdAt!: string;
|
||||
paidAt!: string | null;
|
||||
fundAllocatedAt!: string | null;
|
||||
miningEnabledAt!: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认种分类账响应
|
||||
*/
|
||||
export class PlantingLedgerResponseDto {
|
||||
summary!: PlantingSummaryDto;
|
||||
items!: PlantingLedgerItemDto[];
|
||||
total!: number;
|
||||
page!: number;
|
||||
pageSize!: number;
|
||||
totalPages!: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 钱包分类账
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 钱包汇总
|
||||
*/
|
||||
export class WalletSummaryDto {
|
||||
// USDT
|
||||
usdtAvailable!: string;
|
||||
usdtFrozen!: string;
|
||||
// DST
|
||||
dstAvailable!: string;
|
||||
dstFrozen!: string;
|
||||
// BNB
|
||||
bnbAvailable!: string;
|
||||
bnbFrozen!: string;
|
||||
// OG
|
||||
ogAvailable!: string;
|
||||
ogFrozen!: string;
|
||||
// RWAD
|
||||
rwadAvailable!: string;
|
||||
rwadFrozen!: string;
|
||||
// 算力
|
||||
hashpower!: string;
|
||||
// 收益
|
||||
pendingUsdt!: string;
|
||||
pendingHashpower!: string;
|
||||
settleableUsdt!: string;
|
||||
settleableHashpower!: string;
|
||||
settledTotalUsdt!: string;
|
||||
settledTotalHashpower!: string;
|
||||
expiredTotalUsdt!: string;
|
||||
expiredTotalHashpower!: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 钱包分类账项
|
||||
*/
|
||||
export class WalletLedgerItemDto {
|
||||
entryId!: string;
|
||||
entryType!: string;
|
||||
assetType!: string;
|
||||
amount!: string;
|
||||
balanceAfter!: string | null;
|
||||
refOrderId!: string | null;
|
||||
refTxHash!: string | null;
|
||||
memo!: string | null;
|
||||
createdAt!: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 钱包分类账响应
|
||||
*/
|
||||
export class WalletLedgerResponseDto {
|
||||
summary!: WalletSummaryDto;
|
||||
items!: WalletLedgerItemDto[];
|
||||
total!: number;
|
||||
page!: number;
|
||||
pageSize!: number;
|
||||
totalPages!: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 授权信息
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 授权角色
|
||||
*/
|
||||
export class AuthorizationRoleDto {
|
||||
id!: string;
|
||||
roleType!: string;
|
||||
regionCode!: string;
|
||||
regionName!: string;
|
||||
displayTitle!: string;
|
||||
status!: string;
|
||||
benefitActive!: boolean;
|
||||
benefitActivatedAt!: string | null;
|
||||
authorizedAt!: string | null;
|
||||
authorizedBy!: string | null;
|
||||
initialTargetTreeCount!: number;
|
||||
monthlyTargetType!: string;
|
||||
lastAssessmentMonth!: string | null;
|
||||
monthlyTreesAdded!: number;
|
||||
createdAt!: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 月度考核
|
||||
*/
|
||||
export class MonthlyAssessmentDto {
|
||||
id!: string;
|
||||
authorizationId!: string;
|
||||
roleType!: string;
|
||||
regionCode!: string;
|
||||
assessmentMonth!: string;
|
||||
monthIndex!: number;
|
||||
monthlyTarget!: number;
|
||||
monthlyCompleted!: number;
|
||||
cumulativeTarget!: number;
|
||||
cumulativeCompleted!: number;
|
||||
result!: string;
|
||||
rankingInRegion!: number | null;
|
||||
isFirstPlace!: boolean;
|
||||
isBypassed!: boolean;
|
||||
completedAt!: string | null;
|
||||
assessedAt!: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统账户流水项
|
||||
*/
|
||||
export class SystemAccountLedgerItemDto {
|
||||
ledgerId!: string;
|
||||
accountId!: string;
|
||||
accountType!: string;
|
||||
entryType!: string;
|
||||
amount!: string;
|
||||
balanceAfter!: string;
|
||||
sourceOrderId!: string | null;
|
||||
sourceRewardId!: string | null;
|
||||
txHash!: string | null;
|
||||
memo!: string | null;
|
||||
createdAt!: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 授权详情响应
|
||||
*/
|
||||
export class AuthorizationDetailResponseDto {
|
||||
roles!: AuthorizationRoleDto[];
|
||||
assessments!: MonthlyAssessmentDto[];
|
||||
systemAccountLedger!: SystemAccountLedgerItemDto[];
|
||||
}
|
||||
|
|
@ -32,7 +32,11 @@ import { AdminNotificationController, MobileNotificationController } from './api
|
|||
import { UserQueryRepositoryImpl } from './infrastructure/persistence/repositories/user-query.repository.impl';
|
||||
import { USER_QUERY_REPOSITORY } from './domain/repositories/user-query.repository';
|
||||
import { UserController } from './api/controllers/user.controller';
|
||||
import { UserDetailController } from './api/controllers/user-detail.controller';
|
||||
import { UserEventConsumerService } from './infrastructure/kafka/user-event-consumer.service';
|
||||
// User Detail Query imports
|
||||
import { UserDetailQueryRepositoryImpl } from './infrastructure/persistence/repositories/user-detail-query.repository.impl';
|
||||
import { USER_DETAIL_QUERY_REPOSITORY } from './domain/repositories/user-detail-query.repository';
|
||||
// System Config imports
|
||||
import { SystemConfigRepositoryImpl } from './infrastructure/persistence/repositories/system-config.repository.impl';
|
||||
import { SYSTEM_CONFIG_REPOSITORY } from './domain/repositories/system-config.repository';
|
||||
|
|
@ -89,6 +93,7 @@ import { MaintenanceInterceptor } from './api/interceptors/maintenance.intercept
|
|||
AdminNotificationController,
|
||||
MobileNotificationController,
|
||||
UserController,
|
||||
UserDetailController,
|
||||
AdminSystemConfigController,
|
||||
PublicSystemConfigController,
|
||||
// User Profile System Controllers
|
||||
|
|
@ -131,6 +136,10 @@ import { MaintenanceInterceptor } from './api/interceptors/maintenance.intercept
|
|||
provide: USER_QUERY_REPOSITORY,
|
||||
useClass: UserQueryRepositoryImpl,
|
||||
},
|
||||
{
|
||||
provide: USER_DETAIL_QUERY_REPOSITORY,
|
||||
useClass: UserDetailQueryRepositoryImpl,
|
||||
},
|
||||
UserEventConsumerService,
|
||||
// System Config
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,248 @@
|
|||
/**
|
||||
* 用户详情查询仓储接口
|
||||
* 用于 admin-web 用户详情页面的数据查询
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// 推荐关系相关
|
||||
// ============================================================================
|
||||
|
||||
export interface ReferralInfo {
|
||||
userId: bigint;
|
||||
accountSequence: string;
|
||||
myReferralCode: string;
|
||||
usedReferralCode: string | null;
|
||||
referrerId: bigint | null;
|
||||
ancestorPath: bigint[];
|
||||
depth: number;
|
||||
directReferralCount: number;
|
||||
activeDirectCount: number;
|
||||
}
|
||||
|
||||
export interface ReferralNode {
|
||||
userId: bigint;
|
||||
accountSequence: string;
|
||||
nickname: string | null;
|
||||
avatarUrl: string | null;
|
||||
personalAdoptionCount: number;
|
||||
depth: number;
|
||||
directReferralCount: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 认种相关
|
||||
// ============================================================================
|
||||
|
||||
export interface PlantingSummary {
|
||||
totalOrders: number;
|
||||
totalTreeCount: number;
|
||||
totalAmount: bigint; // Decimal as bigint for precision
|
||||
effectiveTreeCount: number;
|
||||
pendingTreeCount: number;
|
||||
firstPlantingAt: Date | null;
|
||||
lastPlantingAt: Date | null;
|
||||
}
|
||||
|
||||
export interface PlantingLedgerItem {
|
||||
orderId: bigint;
|
||||
orderNo: string;
|
||||
treeCount: number;
|
||||
totalAmount: bigint;
|
||||
status: string;
|
||||
selectedProvince: string | null;
|
||||
selectedCity: string | null;
|
||||
createdAt: Date;
|
||||
paidAt: Date | null;
|
||||
fundAllocatedAt: Date | null;
|
||||
miningEnabledAt: Date | null;
|
||||
}
|
||||
|
||||
export interface PlantingLedgerResult {
|
||||
items: PlantingLedgerItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 钱包相关
|
||||
// ============================================================================
|
||||
|
||||
export interface WalletSummary {
|
||||
usdtAvailable: bigint;
|
||||
usdtFrozen: bigint;
|
||||
dstAvailable: bigint;
|
||||
dstFrozen: bigint;
|
||||
bnbAvailable: bigint;
|
||||
bnbFrozen: bigint;
|
||||
ogAvailable: bigint;
|
||||
ogFrozen: bigint;
|
||||
rwadAvailable: bigint;
|
||||
rwadFrozen: bigint;
|
||||
hashpower: bigint;
|
||||
pendingUsdt: bigint;
|
||||
pendingHashpower: bigint;
|
||||
settleableUsdt: bigint;
|
||||
settleableHashpower: bigint;
|
||||
settledTotalUsdt: bigint;
|
||||
settledTotalHashpower: bigint;
|
||||
expiredTotalUsdt: bigint;
|
||||
expiredTotalHashpower: bigint;
|
||||
}
|
||||
|
||||
export interface WalletLedgerItem {
|
||||
entryId: bigint;
|
||||
entryType: string;
|
||||
assetType: string;
|
||||
amount: bigint;
|
||||
balanceAfter: bigint | null;
|
||||
refOrderId: string | null;
|
||||
refTxHash: string | null;
|
||||
memo: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface WalletLedgerResult {
|
||||
items: WalletLedgerItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface WalletLedgerFilters {
|
||||
assetType?: string;
|
||||
entryType?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 授权相关
|
||||
// ============================================================================
|
||||
|
||||
export interface AuthorizationRole {
|
||||
id: string;
|
||||
roleType: string;
|
||||
regionCode: string;
|
||||
regionName: string;
|
||||
displayTitle: string;
|
||||
status: string;
|
||||
benefitActive: boolean;
|
||||
benefitActivatedAt: Date | null;
|
||||
authorizedAt: Date | null;
|
||||
authorizedBy: string | null;
|
||||
initialTargetTreeCount: number;
|
||||
monthlyTargetType: string;
|
||||
lastAssessmentMonth: string | null;
|
||||
monthlyTreesAdded: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface MonthlyAssessment {
|
||||
id: string;
|
||||
authorizationId: string;
|
||||
roleType: string;
|
||||
regionCode: string;
|
||||
assessmentMonth: string;
|
||||
monthIndex: number;
|
||||
monthlyTarget: number;
|
||||
monthlyCompleted: number;
|
||||
cumulativeTarget: number;
|
||||
cumulativeCompleted: number;
|
||||
result: string;
|
||||
rankingInRegion: number | null;
|
||||
isFirstPlace: boolean;
|
||||
isBypassed: boolean;
|
||||
completedAt: Date | null;
|
||||
assessedAt: Date | null;
|
||||
}
|
||||
|
||||
export interface SystemAccountLedger {
|
||||
ledgerId: bigint;
|
||||
accountId: bigint;
|
||||
accountType: string;
|
||||
entryType: string;
|
||||
amount: bigint;
|
||||
balanceAfter: bigint;
|
||||
sourceOrderId: bigint | null;
|
||||
sourceRewardId: bigint | null;
|
||||
txHash: string | null;
|
||||
memo: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 仓储接口
|
||||
// ============================================================================
|
||||
|
||||
export const USER_DETAIL_QUERY_REPOSITORY = Symbol('USER_DETAIL_QUERY_REPOSITORY');
|
||||
|
||||
export interface IUserDetailQueryRepository {
|
||||
/**
|
||||
* 获取用户推荐关系信息
|
||||
*/
|
||||
getReferralInfo(accountSequence: string): Promise<ReferralInfo | null>;
|
||||
|
||||
/**
|
||||
* 获取用户推荐人链(向上)
|
||||
*/
|
||||
getAncestors(accountSequence: string, depth: number): Promise<ReferralNode[]>;
|
||||
|
||||
/**
|
||||
* 获取用户直推列表(向下)
|
||||
*/
|
||||
getDirectReferrals(accountSequence: string): Promise<ReferralNode[]>;
|
||||
|
||||
/**
|
||||
* 获取推荐人昵称
|
||||
*/
|
||||
getReferrerNickname(referrerId: bigint): Promise<string | null>;
|
||||
|
||||
/**
|
||||
* 获取认种汇总
|
||||
*/
|
||||
getPlantingSummary(userId: bigint): Promise<PlantingSummary | null>;
|
||||
|
||||
/**
|
||||
* 获取认种分类账
|
||||
*/
|
||||
getPlantingLedger(
|
||||
userId: bigint,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
startDate?: Date,
|
||||
endDate?: Date,
|
||||
): Promise<PlantingLedgerResult>;
|
||||
|
||||
/**
|
||||
* 获取钱包汇总
|
||||
*/
|
||||
getWalletSummary(userId: bigint): Promise<WalletSummary | null>;
|
||||
|
||||
/**
|
||||
* 获取钱包分类账
|
||||
*/
|
||||
getWalletLedger(
|
||||
userId: bigint,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
filters?: WalletLedgerFilters,
|
||||
): Promise<WalletLedgerResult>;
|
||||
|
||||
/**
|
||||
* 获取授权角色列表
|
||||
*/
|
||||
getAuthorizationRoles(accountSequence: string): Promise<AuthorizationRole[]>;
|
||||
|
||||
/**
|
||||
* 获取月度考核记录
|
||||
*/
|
||||
getMonthlyAssessments(accountSequence: string): Promise<MonthlyAssessment[]>;
|
||||
|
||||
/**
|
||||
* 获取系统账户流水(用户相关的授权角色账户流水)
|
||||
*/
|
||||
getSystemAccountLedger(accountSequence: string): Promise<SystemAccountLedger[]>;
|
||||
}
|
||||
|
|
@ -0,0 +1,425 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import {
|
||||
IUserDetailQueryRepository,
|
||||
ReferralInfo,
|
||||
ReferralNode,
|
||||
PlantingSummary,
|
||||
PlantingLedgerItem,
|
||||
PlantingLedgerResult,
|
||||
WalletSummary,
|
||||
WalletLedgerItem,
|
||||
WalletLedgerResult,
|
||||
WalletLedgerFilters,
|
||||
AuthorizationRole,
|
||||
MonthlyAssessment,
|
||||
SystemAccountLedger,
|
||||
} from '../../../domain/repositories/user-detail-query.repository';
|
||||
|
||||
@Injectable()
|
||||
export class UserDetailQueryRepositoryImpl implements IUserDetailQueryRepository {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
// ============================================================================
|
||||
// 推荐关系相关
|
||||
// ============================================================================
|
||||
|
||||
async getReferralInfo(accountSequence: string): Promise<ReferralInfo | null> {
|
||||
const referral = await this.prisma.referralQueryView.findUnique({
|
||||
where: { accountSequence },
|
||||
});
|
||||
|
||||
if (!referral) return null;
|
||||
|
||||
return {
|
||||
userId: referral.userId,
|
||||
accountSequence: referral.accountSequence,
|
||||
myReferralCode: referral.myReferralCode,
|
||||
usedReferralCode: referral.usedReferralCode,
|
||||
referrerId: referral.referrerId,
|
||||
ancestorPath: referral.ancestorPath,
|
||||
depth: referral.depth,
|
||||
directReferralCount: referral.directReferralCount,
|
||||
activeDirectCount: referral.activeDirectCount,
|
||||
};
|
||||
}
|
||||
|
||||
async getAncestors(accountSequence: string, depth: number): Promise<ReferralNode[]> {
|
||||
const referral = await this.prisma.referralQueryView.findUnique({
|
||||
where: { accountSequence },
|
||||
});
|
||||
|
||||
if (!referral || referral.ancestorPath.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 获取祖先列表(从直接推荐人开始,最多 depth 层)
|
||||
const ancestorIds = referral.ancestorPath.slice(0, depth);
|
||||
if (ancestorIds.length === 0) return [];
|
||||
|
||||
// 获取祖先用户信息
|
||||
const [users, referrals] = await Promise.all([
|
||||
this.prisma.userQueryView.findMany({
|
||||
where: { userId: { in: ancestorIds } },
|
||||
select: {
|
||||
userId: true,
|
||||
accountSequence: true,
|
||||
nickname: true,
|
||||
avatarUrl: true,
|
||||
personalAdoptionCount: true,
|
||||
},
|
||||
}),
|
||||
this.prisma.referralQueryView.findMany({
|
||||
where: { userId: { in: ancestorIds } },
|
||||
select: {
|
||||
userId: true,
|
||||
depth: true,
|
||||
directReferralCount: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// 合并数据
|
||||
const referralMap = new Map(referrals.map((r) => [r.userId.toString(), r]));
|
||||
const userMap = new Map(users.map((u) => [u.userId.toString(), u]));
|
||||
|
||||
return ancestorIds.map((id, index) => {
|
||||
const user = userMap.get(id.toString());
|
||||
const ref = referralMap.get(id.toString());
|
||||
return {
|
||||
userId: id,
|
||||
accountSequence: user?.accountSequence || '',
|
||||
nickname: user?.nickname || null,
|
||||
avatarUrl: user?.avatarUrl || null,
|
||||
personalAdoptionCount: user?.personalAdoptionCount || 0,
|
||||
depth: ref?.depth || index,
|
||||
directReferralCount: ref?.directReferralCount || 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getDirectReferrals(accountSequence: string): Promise<ReferralNode[]> {
|
||||
const referral = await this.prisma.referralQueryView.findUnique({
|
||||
where: { accountSequence },
|
||||
});
|
||||
|
||||
if (!referral) return [];
|
||||
|
||||
// 查找直接推荐的用户(referrerId = 当前用户的 userId)
|
||||
const directReferrals = await this.prisma.referralQueryView.findMany({
|
||||
where: { referrerId: referral.userId },
|
||||
select: {
|
||||
userId: true,
|
||||
accountSequence: true,
|
||||
depth: true,
|
||||
directReferralCount: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (directReferrals.length === 0) return [];
|
||||
|
||||
// 获取用户信息
|
||||
const users = await this.prisma.userQueryView.findMany({
|
||||
where: { userId: { in: directReferrals.map((r) => r.userId) } },
|
||||
select: {
|
||||
userId: true,
|
||||
accountSequence: true,
|
||||
nickname: true,
|
||||
avatarUrl: true,
|
||||
personalAdoptionCount: true,
|
||||
},
|
||||
});
|
||||
|
||||
const userMap = new Map(users.map((u) => [u.userId.toString(), u]));
|
||||
|
||||
return directReferrals.map((ref) => {
|
||||
const user = userMap.get(ref.userId.toString());
|
||||
return {
|
||||
userId: ref.userId,
|
||||
accountSequence: ref.accountSequence,
|
||||
nickname: user?.nickname || null,
|
||||
avatarUrl: user?.avatarUrl || null,
|
||||
personalAdoptionCount: user?.personalAdoptionCount || 0,
|
||||
depth: ref.depth,
|
||||
directReferralCount: ref.directReferralCount,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getReferrerNickname(referrerId: bigint): Promise<string | null> {
|
||||
const user = await this.prisma.userQueryView.findUnique({
|
||||
where: { userId: referrerId },
|
||||
select: { nickname: true },
|
||||
});
|
||||
return user?.nickname || null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 认种相关
|
||||
// ============================================================================
|
||||
|
||||
async getPlantingSummary(userId: bigint): Promise<PlantingSummary | null> {
|
||||
// 获取持仓信息
|
||||
const position = await this.prisma.plantingPositionQueryView.findUnique({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
// 获取订单统计
|
||||
const [orderStats, firstOrder, lastOrder] = await Promise.all([
|
||||
this.prisma.plantingOrderQueryView.aggregate({
|
||||
where: { userId },
|
||||
_count: true,
|
||||
_sum: {
|
||||
treeCount: true,
|
||||
totalAmount: true,
|
||||
},
|
||||
}),
|
||||
this.prisma.plantingOrderQueryView.findFirst({
|
||||
where: { userId, paidAt: { not: null } },
|
||||
orderBy: { paidAt: 'asc' },
|
||||
select: { paidAt: true },
|
||||
}),
|
||||
this.prisma.plantingOrderQueryView.findFirst({
|
||||
where: { userId, paidAt: { not: null } },
|
||||
orderBy: { paidAt: 'desc' },
|
||||
select: { paidAt: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
totalOrders: orderStats._count,
|
||||
totalTreeCount: orderStats._sum.treeCount || 0,
|
||||
totalAmount: this.decimalToBigint(orderStats._sum.totalAmount),
|
||||
effectiveTreeCount: position?.effectiveTreeCount || 0,
|
||||
pendingTreeCount: position?.pendingTreeCount || 0,
|
||||
firstPlantingAt: firstOrder?.paidAt || null,
|
||||
lastPlantingAt: lastOrder?.paidAt || null,
|
||||
};
|
||||
}
|
||||
|
||||
async getPlantingLedger(
|
||||
userId: bigint,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
startDate?: Date,
|
||||
endDate?: Date,
|
||||
): Promise<PlantingLedgerResult> {
|
||||
const where: any = { userId };
|
||||
|
||||
if (startDate || endDate) {
|
||||
where.createdAt = {};
|
||||
if (startDate) where.createdAt.gte = startDate;
|
||||
if (endDate) where.createdAt.lte = endDate;
|
||||
}
|
||||
|
||||
const [total, items] = await Promise.all([
|
||||
this.prisma.plantingOrderQueryView.count({ where }),
|
||||
this.prisma.plantingOrderQueryView.findMany({
|
||||
where,
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
items: items.map((item) => ({
|
||||
orderId: item.id,
|
||||
orderNo: item.orderNo,
|
||||
treeCount: item.treeCount,
|
||||
totalAmount: this.decimalToBigint(item.totalAmount),
|
||||
status: item.status,
|
||||
selectedProvince: item.selectedProvince,
|
||||
selectedCity: item.selectedCity,
|
||||
createdAt: item.createdAt,
|
||||
paidAt: item.paidAt,
|
||||
fundAllocatedAt: item.fundAllocatedAt,
|
||||
miningEnabledAt: item.miningEnabledAt,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 钱包相关
|
||||
// ============================================================================
|
||||
|
||||
async getWalletSummary(userId: bigint): Promise<WalletSummary | null> {
|
||||
const wallet = await this.prisma.walletAccountQueryView.findUnique({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
if (!wallet) return null;
|
||||
|
||||
return {
|
||||
usdtAvailable: this.decimalToBigint(wallet.usdtAvailable),
|
||||
usdtFrozen: this.decimalToBigint(wallet.usdtFrozen),
|
||||
dstAvailable: this.decimalToBigint(wallet.dstAvailable),
|
||||
dstFrozen: this.decimalToBigint(wallet.dstFrozen),
|
||||
bnbAvailable: this.decimalToBigint(wallet.bnbAvailable),
|
||||
bnbFrozen: this.decimalToBigint(wallet.bnbFrozen),
|
||||
ogAvailable: this.decimalToBigint(wallet.ogAvailable),
|
||||
ogFrozen: this.decimalToBigint(wallet.ogFrozen),
|
||||
rwadAvailable: this.decimalToBigint(wallet.rwadAvailable),
|
||||
rwadFrozen: this.decimalToBigint(wallet.rwadFrozen),
|
||||
hashpower: this.decimalToBigint(wallet.hashpower),
|
||||
pendingUsdt: this.decimalToBigint(wallet.pendingUsdt),
|
||||
pendingHashpower: this.decimalToBigint(wallet.pendingHashpower),
|
||||
settleableUsdt: this.decimalToBigint(wallet.settleableUsdt),
|
||||
settleableHashpower: this.decimalToBigint(wallet.settleableHashpower),
|
||||
settledTotalUsdt: this.decimalToBigint(wallet.settledTotalUsdt),
|
||||
settledTotalHashpower: this.decimalToBigint(wallet.settledTotalHashpower),
|
||||
expiredTotalUsdt: this.decimalToBigint(wallet.expiredTotalUsdt),
|
||||
expiredTotalHashpower: this.decimalToBigint(wallet.expiredTotalHashpower),
|
||||
};
|
||||
}
|
||||
|
||||
async getWalletLedger(
|
||||
userId: bigint,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
filters?: WalletLedgerFilters,
|
||||
): Promise<WalletLedgerResult> {
|
||||
const where: any = { userId };
|
||||
|
||||
if (filters?.assetType) {
|
||||
where.assetType = filters.assetType;
|
||||
}
|
||||
if (filters?.entryType) {
|
||||
where.entryType = filters.entryType;
|
||||
}
|
||||
if (filters?.startDate || filters?.endDate) {
|
||||
where.createdAt = {};
|
||||
if (filters.startDate) where.createdAt.gte = filters.startDate;
|
||||
if (filters.endDate) where.createdAt.lte = filters.endDate;
|
||||
}
|
||||
|
||||
const [total, items] = await Promise.all([
|
||||
this.prisma.walletLedgerEntryView.count({ where }),
|
||||
this.prisma.walletLedgerEntryView.findMany({
|
||||
where,
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
items: items.map((item) => ({
|
||||
entryId: item.id,
|
||||
entryType: item.entryType,
|
||||
assetType: item.assetType,
|
||||
amount: this.decimalToBigint(item.amount),
|
||||
balanceAfter: item.balanceAfter ? this.decimalToBigint(item.balanceAfter) : null,
|
||||
refOrderId: item.refOrderId,
|
||||
refTxHash: item.refTxHash,
|
||||
memo: item.memo,
|
||||
createdAt: item.createdAt,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 授权相关
|
||||
// ============================================================================
|
||||
|
||||
async getAuthorizationRoles(accountSequence: string): Promise<AuthorizationRole[]> {
|
||||
const roles = await this.prisma.authorizationRoleQueryView.findMany({
|
||||
where: {
|
||||
accountSequence,
|
||||
deletedAt: null,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
return 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,
|
||||
authorizedAt: role.authorizedAt,
|
||||
authorizedBy: role.authorizedBy,
|
||||
initialTargetTreeCount: role.initialTargetTreeCount,
|
||||
monthlyTargetType: role.monthlyTargetType,
|
||||
lastAssessmentMonth: role.lastAssessmentMonth,
|
||||
monthlyTreesAdded: role.monthlyTreesAdded,
|
||||
createdAt: role.createdAt,
|
||||
}));
|
||||
}
|
||||
|
||||
async getMonthlyAssessments(accountSequence: string): Promise<MonthlyAssessment[]> {
|
||||
const assessments = await this.prisma.monthlyAssessmentQueryView.findMany({
|
||||
where: { accountSequence },
|
||||
orderBy: [{ assessmentMonth: 'desc' }, { createdAt: 'desc' }],
|
||||
});
|
||||
|
||||
return 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,
|
||||
assessedAt: assessment.assessedAt,
|
||||
}));
|
||||
}
|
||||
|
||||
async getSystemAccountLedger(accountSequence: string): Promise<SystemAccountLedger[]> {
|
||||
// 先获取用户的授权角色
|
||||
const roles = await this.prisma.authorizationRoleQueryView.findMany({
|
||||
where: {
|
||||
accountSequence,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (roles.length === 0) return [];
|
||||
|
||||
// 注:SystemAccountLedgerView 不直接关联用户,这里暂时返回空
|
||||
// 实际业务中可能需要根据授权角色ID或区域来查询相关流水
|
||||
// 这里简化处理,如果需要可以通过其他方式关联
|
||||
return [];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 辅助方法
|
||||
// ============================================================================
|
||||
|
||||
private decimalToBigint(decimal: Decimal | null | undefined): bigint {
|
||||
if (!decimal) return BigInt(0);
|
||||
// 转换为字符串后解析,保留精度
|
||||
const str = decimal.toString();
|
||||
// 移除小数点,按整数处理
|
||||
const parts = str.split('.');
|
||||
if (parts.length === 1) {
|
||||
return BigInt(parts[0]);
|
||||
}
|
||||
// 有小数部分,乘以 10^小数位数
|
||||
const scale = parts[1].length;
|
||||
const intPart = parts[0] + parts[1];
|
||||
// 返回原始数值(不做缩放,保持 decimal 格式)
|
||||
return BigInt(Math.round(parseFloat(str) * 1e8));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,342 @@
|
|||
# 用户详情页面实现计划
|
||||
|
||||
## 1. 页面概述
|
||||
|
||||
创建独立的用户详情页面 `/users/[id]`,展示用户的完整信息。
|
||||
|
||||
## 2. 页面结构
|
||||
|
||||
```
|
||||
/users/[id]/page.tsx
|
||||
├── 用户基本信息卡片
|
||||
│ ├── 头像、昵称、账户序号
|
||||
│ ├── 手机号(脱敏)
|
||||
│ ├── KYC 状态
|
||||
│ ├── 注册时间、最后活跃时间
|
||||
│ └── 用户状态(正常/冻结)
|
||||
│
|
||||
├── 推荐关系树(倒树形结构)
|
||||
│ ├── 当前用户为根节点
|
||||
│ ├── 向上显示推荐人链路(ancestor_path)
|
||||
│ ├── 向下显示直推用户列表
|
||||
│ ├── 点击任意节点可切换查看该节点的关系树
|
||||
│ └── 显示每个节点的基本信息(序号、昵称、认种量)
|
||||
│
|
||||
├── 认种信息 Tab
|
||||
│ ├── 认种汇总
|
||||
│ │ ├── 个人认种总量
|
||||
│ │ ├── 团队认种总量
|
||||
│ │ ├── 首次认种时间
|
||||
│ │ └── 最近认种时间
|
||||
│ └── 认种分类账明细(表格)
|
||||
│ ├── 订单号
|
||||
│ ├── 认种数量
|
||||
│ ├── 金额
|
||||
│ ├── 状态
|
||||
│ ├── 省市选择
|
||||
│ └── 时间
|
||||
│
|
||||
├── 钱包信息 Tab
|
||||
│ ├── 钱包汇总
|
||||
│ │ ├── USDT 余额(可用/冻结)
|
||||
│ │ ├── DST 余额
|
||||
│ │ ├── 算力余额
|
||||
│ │ ├── 待领取收益
|
||||
│ │ ├── 可结算收益
|
||||
│ │ └── 已结算收益
|
||||
│ └── 钱包分类账明细(表格)
|
||||
│ ├── 流水ID
|
||||
│ ├── 类型(充值/提现/收益/扣款等)
|
||||
│ ├── 资产类型
|
||||
│ ├── 金额
|
||||
│ ├── 余额快照
|
||||
│ ├── 关联订单
|
||||
│ └── 时间
|
||||
│
|
||||
└── 授权信息 Tab
|
||||
├── 授权汇总
|
||||
│ ├── 授权角色列表(社区合伙人/省公司/市公司等)
|
||||
│ ├── 授权状态
|
||||
│ ├── 授权区域
|
||||
│ └── 权益激活状态
|
||||
└── 授权分类账明细(表格)
|
||||
├── 月度考核记录
|
||||
│ ├── 考核月份
|
||||
│ ├── 月度目标/完成
|
||||
│ ├── 累计目标/完成
|
||||
│ ├── 考核结果
|
||||
│ └── 区域排名
|
||||
└── 系统账户流水(如果是省/市公司)
|
||||
├── 流水ID
|
||||
├── 账户类型
|
||||
├── 流水类型
|
||||
├── 金额
|
||||
├── 余额
|
||||
└── 时间
|
||||
```
|
||||
|
||||
## 3. 需要创建的文件
|
||||
|
||||
### 3.1 前端文件
|
||||
|
||||
```
|
||||
frontend/admin-web/src/
|
||||
├── app/(dashboard)/users/[id]/
|
||||
│ ├── page.tsx # 用户详情页面
|
||||
│ └── user-detail.module.scss # 页面样式
|
||||
│
|
||||
├── components/features/users/
|
||||
│ ├── ReferralTree/
|
||||
│ │ ├── ReferralTree.tsx # 推荐关系树组件
|
||||
│ │ ├── ReferralTree.module.scss
|
||||
│ │ └── index.ts
|
||||
│ ├── PlantingTab/
|
||||
│ │ ├── PlantingTab.tsx # 认种信息 Tab
|
||||
│ │ ├── PlantingTab.module.scss
|
||||
│ │ └── index.ts
|
||||
│ ├── WalletTab/
|
||||
│ │ ├── WalletTab.tsx # 钱包信息 Tab
|
||||
│ │ ├── WalletTab.module.scss
|
||||
│ │ └── index.ts
|
||||
│ └── AuthorizationTab/
|
||||
│ ├── AuthorizationTab.tsx # 授权信息 Tab
|
||||
│ ├── AuthorizationTab.module.scss
|
||||
│ └── index.ts
|
||||
│
|
||||
├── services/
|
||||
│ └── userDetailService.ts # 用户详情相关 API
|
||||
│
|
||||
├── hooks/
|
||||
│ └── useUserDetail.ts # 用户详情相关 hooks(扩展)
|
||||
│
|
||||
└── types/
|
||||
└── userDetail.types.ts # 用户详情相关类型
|
||||
```
|
||||
|
||||
### 3.2 后端文件(admin-service)
|
||||
|
||||
```
|
||||
backend/services/admin-service/src/
|
||||
├── application/
|
||||
│ ├── queries/
|
||||
│ │ ├── get-user-full-detail.query.ts
|
||||
│ │ ├── get-user-referral-tree.query.ts
|
||||
│ │ ├── get-user-planting-ledger.query.ts
|
||||
│ │ ├── get-user-wallet-ledger.query.ts
|
||||
│ │ └── get-user-authorization-detail.query.ts
|
||||
│ └── handlers/
|
||||
│ └── (对应的 handler 文件)
|
||||
│
|
||||
├── interfaces/http/controllers/
|
||||
│ └── user-detail.controller.ts # 用户详情 API 控制器
|
||||
│
|
||||
└── infrastructure/persistence/repositories/
|
||||
└── user-detail-query.repository.impl.ts
|
||||
```
|
||||
|
||||
## 4. API 端点设计
|
||||
|
||||
### 4.1 获取用户完整信息
|
||||
```
|
||||
GET /v1/admin/users/:accountSequence/full-detail
|
||||
Response: {
|
||||
basicInfo: { ... },
|
||||
referralInfo: {
|
||||
myReferralCode: string,
|
||||
usedReferralCode: string,
|
||||
referrerId: string,
|
||||
referrerSequence: string,
|
||||
ancestorPath: string,
|
||||
depth: number,
|
||||
directReferralCount: number,
|
||||
activeDirectCount: number
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 获取推荐关系树
|
||||
```
|
||||
GET /v1/admin/users/:accountSequence/referral-tree
|
||||
Query: { direction: 'up' | 'down', depth?: number }
|
||||
Response: {
|
||||
currentUser: { accountSequence, nickname, personalAdoptions },
|
||||
ancestors: [...], // 向上的推荐人链
|
||||
directReferrals: [...] // 直推用户列表
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 获取认种分类账
|
||||
```
|
||||
GET /v1/admin/users/:accountSequence/planting-ledger
|
||||
Query: { page, pageSize, startDate?, endDate? }
|
||||
Response: {
|
||||
summary: {
|
||||
totalOrders: number,
|
||||
totalTreeCount: number,
|
||||
totalAmount: string,
|
||||
firstPlantingAt: string,
|
||||
lastPlantingAt: string
|
||||
},
|
||||
items: [...],
|
||||
total: number,
|
||||
page: number,
|
||||
pageSize: number
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 获取钱包分类账
|
||||
```
|
||||
GET /v1/admin/users/:accountSequence/wallet-ledger
|
||||
Query: { page, pageSize, assetType?, entryType?, startDate?, endDate? }
|
||||
Response: {
|
||||
summary: {
|
||||
usdtAvailable: string,
|
||||
usdtFrozen: string,
|
||||
dstAvailable: string,
|
||||
hashpower: string,
|
||||
pendingUsdt: string,
|
||||
settleableUsdt: string,
|
||||
settledTotalUsdt: string
|
||||
},
|
||||
items: [...],
|
||||
total: number,
|
||||
page: number,
|
||||
pageSize: number
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 获取授权信息
|
||||
```
|
||||
GET /v1/admin/users/:accountSequence/authorization-detail
|
||||
Response: {
|
||||
roles: [{
|
||||
id: string,
|
||||
roleType: string,
|
||||
regionCode: string,
|
||||
regionName: string,
|
||||
displayTitle: string,
|
||||
status: string,
|
||||
benefitActive: boolean,
|
||||
authorizedAt: string
|
||||
}],
|
||||
assessments: [{
|
||||
assessmentMonth: string,
|
||||
monthlyTarget: number,
|
||||
monthlyCompleted: number,
|
||||
cumulativeTarget: number,
|
||||
cumulativeCompleted: number,
|
||||
result: string,
|
||||
rankingInRegion: number
|
||||
}],
|
||||
systemAccountLedger: [...] // 如果是省/市公司
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 数据来源(CDC 同步表)
|
||||
|
||||
| 数据 | 来源表 | admin-service 视图表 |
|
||||
|------|--------|---------------------|
|
||||
| 用户基本信息 | identity.user_accounts | user_query_view |
|
||||
| 推荐关系 | referral.referral_relationships | referral_query_view |
|
||||
| 认种订单 | planting.planting_orders | planting_order_query_view |
|
||||
| 认种资金分配 | planting.fund_allocations | fund_allocation_view |
|
||||
| 钱包余额 | wallet.wallet_accounts | wallet_account_query_view |
|
||||
| 钱包流水 | wallet.wallet_ledger_entries | wallet_ledger_entry_view |
|
||||
| 授权角色 | authorization.authorization_roles | authorization_role_query_view |
|
||||
| 月度考核 | authorization.monthly_assessments | monthly_assessment_query_view |
|
||||
| 系统账户流水 | authorization.system_account_ledgers | system_account_ledger_view |
|
||||
|
||||
## 6. 推荐关系树组件设计
|
||||
|
||||
### 6.1 数据结构
|
||||
```typescript
|
||||
interface ReferralNode {
|
||||
accountSequence: string;
|
||||
nickname: string | null;
|
||||
avatar: string | null;
|
||||
personalAdoptions: number;
|
||||
depth: number;
|
||||
isCurrentUser: boolean;
|
||||
children?: ReferralNode[];
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 交互设计
|
||||
- 初始显示当前用户及其直接推荐人(向上1级)和直推用户(向下1级)
|
||||
- 点击任意节点,该节点变为中心,重新加载其上下级关系
|
||||
- 使用树形布局,父节点在上,子节点在下
|
||||
- 节点显示:头像 + 序号 + 昵称 + 认种量
|
||||
- 当前查看的用户节点高亮显示
|
||||
|
||||
### 6.3 实现方案
|
||||
- 使用 CSS Flexbox/Grid 实现树形布局
|
||||
- 或使用 react-d3-tree 库
|
||||
- 或使用 @ant-design/charts 的组织架构图
|
||||
|
||||
## 7. Tab 切换设计
|
||||
|
||||
```typescript
|
||||
type TabType = 'planting' | 'wallet' | 'authorization';
|
||||
|
||||
const tabs = [
|
||||
{ key: 'planting', label: '认种信息' },
|
||||
{ key: 'wallet', label: '钱包信息' },
|
||||
{ key: 'authorization', label: '授权信息' },
|
||||
];
|
||||
```
|
||||
|
||||
## 8. 实现顺序
|
||||
|
||||
1. **Phase 1: 基础结构**
|
||||
- 创建页面路由和基本布局
|
||||
- 实现用户基本信息卡片
|
||||
- 更新用户列表页面的"查看详情"链接
|
||||
|
||||
2. **Phase 2: 后端 API**
|
||||
- 实现 user-detail.controller.ts
|
||||
- 添加查询推荐关系的 repository 方法
|
||||
- 添加查询分类账的 repository 方法
|
||||
|
||||
3. **Phase 3: 推荐关系树**
|
||||
- 实现 ReferralTree 组件
|
||||
- 实现节点点击切换功能
|
||||
|
||||
4. **Phase 4: 认种信息**
|
||||
- 实现 PlantingTab 组件
|
||||
- 显示汇总和明细表格
|
||||
|
||||
5. **Phase 5: 钱包信息**
|
||||
- 实现 WalletTab 组件
|
||||
- 显示汇总和明细表格
|
||||
|
||||
6. **Phase 6: 授权信息**
|
||||
- 实现 AuthorizationTab 组件
|
||||
- 显示角色列表、考核记录
|
||||
|
||||
## 9. 样式规范
|
||||
|
||||
- 使用现有的设计系统颜色变量
|
||||
- 卡片间距: 24px
|
||||
- 表格使用现有的 Table 组件样式
|
||||
- Tab 使用自定义样式,与现有风格保持一致
|
||||
- 树形结构使用清晰的连接线
|
||||
|
||||
## 10. 注意事项
|
||||
|
||||
1. **性能优化**
|
||||
- 分类账明细使用分页加载
|
||||
- 推荐关系树按需加载(点击时才加载子节点)
|
||||
- 使用 React Query 缓存数据
|
||||
|
||||
2. **错误处理**
|
||||
- 用户不存在时显示友好提示
|
||||
- 网络错误时显示重试按钮
|
||||
- 数据为空时显示空状态提示
|
||||
|
||||
3. **权限控制**
|
||||
- 确保只有登录的管理员可以访问
|
||||
- 敏感信息(如完整手机号)不显示
|
||||
|
||||
4. **响应式设计**
|
||||
- 表格在小屏幕上可横向滚动
|
||||
- 推荐关系树在小屏幕上可缩放
|
||||
|
|
@ -0,0 +1,803 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { Button, toast } from '@/components/common';
|
||||
import { PageContainer } from '@/components/layout';
|
||||
import { cn } from '@/utils/helpers';
|
||||
import { formatNumber, formatRanking } from '@/utils/formatters';
|
||||
import {
|
||||
useUserFullDetail,
|
||||
useReferralTree,
|
||||
usePlantingLedger,
|
||||
useWalletLedger,
|
||||
useAuthorizationDetail,
|
||||
} from '@/hooks/useUserDetailPage';
|
||||
import type {
|
||||
ReferralNode,
|
||||
PlantingLedgerItem,
|
||||
WalletLedgerItem,
|
||||
WALLET_ENTRY_TYPE_LABELS,
|
||||
ASSET_TYPE_LABELS,
|
||||
PLANTING_STATUS_LABELS,
|
||||
AUTHORIZATION_ROLE_LABELS,
|
||||
AUTHORIZATION_STATUS_LABELS,
|
||||
ASSESSMENT_RESULT_LABELS,
|
||||
} from '@/types/userDetail.types';
|
||||
import styles from './user-detail.module.scss';
|
||||
|
||||
// Tab 类型
|
||||
type TabType = 'referral' | 'planting' | 'wallet' | 'authorization';
|
||||
|
||||
const tabs: { key: TabType; label: string }[] = [
|
||||
{ key: 'referral', label: '推荐关系' },
|
||||
{ key: 'planting', label: '认种信息' },
|
||||
{ key: 'wallet', label: '钱包信息' },
|
||||
{ key: 'authorization', label: '授权信息' },
|
||||
];
|
||||
|
||||
// 流水类型标签
|
||||
const entryTypeLabels: Record<string, string> = {
|
||||
DEPOSIT: '充值',
|
||||
DEPOSIT_USDT: 'USDT充值',
|
||||
DEPOSIT_BNB: 'BNB充值',
|
||||
WITHDRAW: '提现',
|
||||
WITHDRAW_FROZEN: '提现冻结',
|
||||
WITHDRAW_CONFIRMED: '提现确认',
|
||||
WITHDRAW_CANCELLED: '提现取消',
|
||||
PLANTING_PAYMENT: '认种支付',
|
||||
PLANTING_FROZEN: '认种冻结',
|
||||
PLANTING_DEDUCT: '认种扣款',
|
||||
REWARD_PENDING: '收益待领取',
|
||||
REWARD_SETTLED: '收益结算',
|
||||
REWARD_EXPIRED: '收益过期',
|
||||
TRANSFER_OUT: '转出',
|
||||
TRANSFER_IN: '转入',
|
||||
INTERNAL_TRANSFER: '内部转账',
|
||||
ADMIN_ADJUSTMENT: '管理员调整',
|
||||
SYSTEM_DEDUCT: '系统扣款',
|
||||
FEE: '手续费',
|
||||
};
|
||||
|
||||
const assetTypeLabels: Record<string, string> = {
|
||||
USDT: 'USDT',
|
||||
DST: 'DST',
|
||||
BNB: 'BNB',
|
||||
OG: 'OG',
|
||||
RWAD: 'RWAD',
|
||||
HASHPOWER: '算力',
|
||||
};
|
||||
|
||||
const plantingStatusLabels: Record<string, string> = {
|
||||
CREATED: '已创建',
|
||||
PAID: '已支付',
|
||||
FUND_ALLOCATED: '资金已分配',
|
||||
MINING_ENABLED: '已开始挖矿',
|
||||
CANCELLED: '已取消',
|
||||
EXPIRED: '已过期',
|
||||
};
|
||||
|
||||
const roleTypeLabels: Record<string, string> = {
|
||||
COMMUNITY_PARTNER: '社区合伙人',
|
||||
PROVINCE_COMPANY: '省公司',
|
||||
CITY_COMPANY: '市公司',
|
||||
AUTH_PROVINCE_COMPANY: '授权省公司',
|
||||
AUTH_CITY_COMPANY: '授权市公司',
|
||||
};
|
||||
|
||||
const authStatusLabels: Record<string, string> = {
|
||||
PENDING: '待授权',
|
||||
AUTHORIZED: '已授权',
|
||||
REVOKED: '已撤销',
|
||||
EXPIRED: '已过期',
|
||||
};
|
||||
|
||||
const assessmentResultLabels: Record<string, string> = {
|
||||
NOT_ASSESSED: '未考核',
|
||||
PASSED: '通过',
|
||||
FAILED: '未通过',
|
||||
BYPASSED: '豁免',
|
||||
};
|
||||
|
||||
/**
|
||||
* 用户详情页面
|
||||
*/
|
||||
export default function UserDetailPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const accountSequence = params.id as string;
|
||||
|
||||
const [activeTab, setActiveTab] = useState<TabType>('referral');
|
||||
const [treeRootUser, setTreeRootUser] = useState<string>(accountSequence);
|
||||
const [plantingPage, setPlantingPage] = useState(1);
|
||||
const [walletPage, setWalletPage] = useState(1);
|
||||
|
||||
// 获取用户完整信息
|
||||
const { data: userDetail, isLoading: detailLoading, error: detailError } = useUserFullDetail(accountSequence);
|
||||
|
||||
// 获取推荐关系树(以当前选中的用户为根)
|
||||
const { data: referralTree, isLoading: treeLoading } = useReferralTree(treeRootUser, 'both', 1);
|
||||
|
||||
// 获取认种分类账
|
||||
const { data: plantingData, isLoading: plantingLoading } = usePlantingLedger(accountSequence, {
|
||||
page: plantingPage,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
// 获取钱包分类账
|
||||
const { data: walletData, isLoading: walletLoading } = useWalletLedger(accountSequence, {
|
||||
page: walletPage,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
// 获取授权信息
|
||||
const { data: authData, isLoading: authLoading } = useAuthorizationDetail(accountSequence);
|
||||
|
||||
// 切换推荐关系树的根节点
|
||||
const handleTreeNodeClick = useCallback((node: ReferralNode) => {
|
||||
setTreeRootUser(node.accountSequence);
|
||||
}, []);
|
||||
|
||||
// 返回列表
|
||||
const handleBack = useCallback(() => {
|
||||
router.push('/users');
|
||||
}, [router]);
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string | null) => {
|
||||
if (!dateStr) return '-';
|
||||
return new Date(dateStr).toLocaleString('zh-CN');
|
||||
};
|
||||
|
||||
// 格式化金额
|
||||
const formatAmount = (amount: string | null) => {
|
||||
if (!amount) return '-';
|
||||
const num = parseFloat(amount);
|
||||
return num.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 8 });
|
||||
};
|
||||
|
||||
if (detailLoading) {
|
||||
return (
|
||||
<PageContainer title="用户详情">
|
||||
<div className={styles.loading}>加载中...</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (detailError || !userDetail) {
|
||||
return (
|
||||
<PageContainer title="用户详情">
|
||||
<div className={styles.error}>
|
||||
<p>加载失败: {(detailError as Error)?.message || '用户不存在'}</p>
|
||||
<Button onClick={handleBack}>返回列表</Button>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer title={`用户详情 - ${userDetail.accountSequence}`}>
|
||||
<div className={styles.userDetail}>
|
||||
{/* 返回按钮 */}
|
||||
<div className={styles.userDetail__backBar}>
|
||||
<button className={styles.userDetail__backBtn} onClick={handleBack}>
|
||||
<span className={styles.userDetail__backIcon}>←</span>
|
||||
返回用户列表
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 用户基本信息卡片 */}
|
||||
<div className={styles.userDetail__basicCard}>
|
||||
<div className={styles.userDetail__basicHeader}>
|
||||
<div
|
||||
className={styles.userDetail__avatar}
|
||||
style={{ backgroundImage: `url(${userDetail.avatar || '/images/Data@2x.png'})` }}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
styles.userDetail__status,
|
||||
userDetail.isOnline ? styles['userDetail__status--online'] : styles['userDetail__status--offline']
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.userDetail__basicInfo}>
|
||||
<h1 className={styles.userDetail__nickname}>
|
||||
{userDetail.nickname || '未设置昵称'}
|
||||
<span className={cn(
|
||||
styles.userDetail__statusBadge,
|
||||
styles[`userDetail__statusBadge--${userDetail.status}`]
|
||||
)}>
|
||||
{userDetail.status === 'active' ? '正常' : userDetail.status === 'frozen' ? '冻结' : '停用'}
|
||||
</span>
|
||||
</h1>
|
||||
<div className={styles.userDetail__basicMeta}>
|
||||
<span>账户序号: <strong>{userDetail.accountSequence}</strong></span>
|
||||
<span>手机号: {userDetail.phoneNumberMasked || '未绑定'}</span>
|
||||
<span>KYC: {userDetail.kycStatus}</span>
|
||||
</div>
|
||||
<div className={styles.userDetail__basicMeta}>
|
||||
<span>注册时间: {formatDate(userDetail.registeredAt)}</span>
|
||||
<span>最后活跃: {formatDate(userDetail.lastActiveAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className={styles.userDetail__statsGrid}>
|
||||
<div className={styles.userDetail__statCard}>
|
||||
<span className={styles.userDetail__statLabel}>个人认种量</span>
|
||||
<span className={styles.userDetail__statValue}>{formatNumber(userDetail.personalAdoptions)}</span>
|
||||
</div>
|
||||
<div className={styles.userDetail__statCard}>
|
||||
<span className={styles.userDetail__statLabel}>团队认种量</span>
|
||||
<span className={styles.userDetail__statValue}>{formatNumber(userDetail.teamAdoptions)}</span>
|
||||
</div>
|
||||
<div className={styles.userDetail__statCard}>
|
||||
<span className={styles.userDetail__statLabel}>团队地址数</span>
|
||||
<span className={styles.userDetail__statValue}>{formatNumber(userDetail.teamAddresses)}</span>
|
||||
</div>
|
||||
<div className={styles.userDetail__statCard}>
|
||||
<span className={styles.userDetail__statLabel}>龙虎榜排名</span>
|
||||
<span className={cn(
|
||||
styles.userDetail__statValue,
|
||||
userDetail.ranking && userDetail.ranking <= 10 && styles['userDetail__statValue--gold']
|
||||
)}>
|
||||
{userDetail.ranking ? formatRanking(userDetail.ranking) : '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.userDetail__statCard}>
|
||||
<span className={styles.userDetail__statLabel}>直推人数</span>
|
||||
<span className={styles.userDetail__statValue}>
|
||||
{formatNumber(userDetail.referralInfo.directReferralCount)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.userDetail__statCard}>
|
||||
<span className={styles.userDetail__statLabel}>活跃直推</span>
|
||||
<span className={styles.userDetail__statValue}>
|
||||
{formatNumber(userDetail.referralInfo.activeDirectCount)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 推荐人信息 */}
|
||||
{userDetail.referralInfo.referrerSequence && (
|
||||
<div className={styles.userDetail__referrerInfo}>
|
||||
<span className={styles.userDetail__referrerLabel}>推荐人:</span>
|
||||
<Link
|
||||
href={`/users/${userDetail.referralInfo.referrerSequence}`}
|
||||
className={styles.userDetail__referrerLink}
|
||||
>
|
||||
{userDetail.referralInfo.referrerSequence}
|
||||
{userDetail.referralInfo.referrerNickname && ` (${userDetail.referralInfo.referrerNickname})`}
|
||||
</Link>
|
||||
<span className={styles.userDetail__referrerMeta}>
|
||||
邀请码: {userDetail.referralInfo.usedReferralCode || '-'}
|
||||
</span>
|
||||
<span className={styles.userDetail__referrerMeta}>
|
||||
层级深度: {userDetail.referralInfo.depth}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tab 切换 */}
|
||||
<div className={styles.userDetail__tabs}>
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
className={cn(
|
||||
styles.userDetail__tab,
|
||||
activeTab === tab.key && styles['userDetail__tab--active']
|
||||
)}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab 内容 */}
|
||||
<div className={styles.userDetail__tabContent}>
|
||||
{/* 推荐关系 Tab */}
|
||||
{activeTab === 'referral' && (
|
||||
<div className={styles.referralTab}>
|
||||
<div className={styles.referralTab__header}>
|
||||
<h3>推荐关系树</h3>
|
||||
{treeRootUser !== accountSequence && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setTreeRootUser(accountSequence)}
|
||||
>
|
||||
返回当前用户
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{treeLoading ? (
|
||||
<div className={styles.referralTab__loading}>加载中...</div>
|
||||
) : referralTree ? (
|
||||
<div className={styles.referralTree}>
|
||||
{/* 向上的推荐人链 */}
|
||||
{referralTree.ancestors.length > 0 && (
|
||||
<div className={styles.referralTree__ancestors}>
|
||||
<div className={styles.referralTree__label}>推荐人链 (向上)</div>
|
||||
<div className={styles.referralTree__nodeList}>
|
||||
{referralTree.ancestors.map((ancestor, index) => (
|
||||
<div key={ancestor.accountSequence} className={styles.referralTree__nodeWrapper}>
|
||||
<button
|
||||
className={styles.referralTree__node}
|
||||
onClick={() => handleTreeNodeClick(ancestor)}
|
||||
>
|
||||
<span className={styles.referralTree__nodeSeq}>{ancestor.accountSequence}</span>
|
||||
<span className={styles.referralTree__nodeNickname}>
|
||||
{ancestor.nickname || '未设置'}
|
||||
</span>
|
||||
<span className={styles.referralTree__nodeAdoptions}>
|
||||
认种: {formatNumber(ancestor.personalAdoptions)}
|
||||
</span>
|
||||
</button>
|
||||
{index < referralTree.ancestors.length - 1 && (
|
||||
<div className={styles.referralTree__connector}>↑</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.referralTree__connector}>↑</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 当前用户 */}
|
||||
<div className={styles.referralTree__current}>
|
||||
<div
|
||||
className={cn(
|
||||
styles.referralTree__node,
|
||||
styles['referralTree__node--current'],
|
||||
referralTree.currentUser.accountSequence === accountSequence &&
|
||||
styles['referralTree__node--highlight']
|
||||
)}
|
||||
>
|
||||
<span className={styles.referralTree__nodeSeq}>
|
||||
{referralTree.currentUser.accountSequence}
|
||||
</span>
|
||||
<span className={styles.referralTree__nodeNickname}>
|
||||
{referralTree.currentUser.nickname || '未设置'}
|
||||
</span>
|
||||
<span className={styles.referralTree__nodeAdoptions}>
|
||||
认种: {formatNumber(referralTree.currentUser.personalAdoptions)}
|
||||
</span>
|
||||
<span className={styles.referralTree__nodeCount}>
|
||||
直推: {formatNumber(referralTree.currentUser.directReferralCount)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 直推用户 */}
|
||||
{referralTree.directReferrals.length > 0 && (
|
||||
<div className={styles.referralTree__directReferrals}>
|
||||
<div className={styles.referralTree__connector}>↓</div>
|
||||
<div className={styles.referralTree__label}>
|
||||
直推用户 ({referralTree.directReferrals.length})
|
||||
</div>
|
||||
<div className={styles.referralTree__nodeGrid}>
|
||||
{referralTree.directReferrals.map((referral) => (
|
||||
<button
|
||||
key={referral.accountSequence}
|
||||
className={styles.referralTree__node}
|
||||
onClick={() => handleTreeNodeClick(referral)}
|
||||
>
|
||||
<span className={styles.referralTree__nodeSeq}>{referral.accountSequence}</span>
|
||||
<span className={styles.referralTree__nodeNickname}>
|
||||
{referral.nickname || '未设置'}
|
||||
</span>
|
||||
<span className={styles.referralTree__nodeAdoptions}>
|
||||
认种: {formatNumber(referral.personalAdoptions)}
|
||||
</span>
|
||||
{referral.directReferralCount > 0 && (
|
||||
<span className={styles.referralTree__nodeCount}>
|
||||
直推: {formatNumber(referral.directReferralCount)}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{referralTree.directReferrals.length === 0 && referralTree.ancestors.length === 0 && (
|
||||
<div className={styles.referralTree__empty}>暂无推荐关系</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.referralTab__empty}>暂无推荐关系数据</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 认种信息 Tab */}
|
||||
{activeTab === 'planting' && (
|
||||
<div className={styles.plantingTab}>
|
||||
{plantingLoading ? (
|
||||
<div className={styles.plantingTab__loading}>加载中...</div>
|
||||
) : plantingData ? (
|
||||
<>
|
||||
{/* 认种汇总 */}
|
||||
<div className={styles.plantingTab__summary}>
|
||||
<h3>认种汇总</h3>
|
||||
<div className={styles.plantingTab__summaryGrid}>
|
||||
<div className={styles.plantingTab__summaryItem}>
|
||||
<span className={styles.plantingTab__summaryLabel}>总订单数</span>
|
||||
<span className={styles.plantingTab__summaryValue}>
|
||||
{formatNumber(plantingData.summary.totalOrders)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.plantingTab__summaryItem}>
|
||||
<span className={styles.plantingTab__summaryLabel}>总认种量</span>
|
||||
<span className={styles.plantingTab__summaryValue}>
|
||||
{formatNumber(plantingData.summary.totalTreeCount)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.plantingTab__summaryItem}>
|
||||
<span className={styles.plantingTab__summaryLabel}>总金额 (USDT)</span>
|
||||
<span className={styles.plantingTab__summaryValue}>
|
||||
{formatAmount(plantingData.summary.totalAmount)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.plantingTab__summaryItem}>
|
||||
<span className={styles.plantingTab__summaryLabel}>有效认种量</span>
|
||||
<span className={styles.plantingTab__summaryValue}>
|
||||
{formatNumber(plantingData.summary.effectiveTreeCount)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.plantingTab__summaryItem}>
|
||||
<span className={styles.plantingTab__summaryLabel}>首次认种</span>
|
||||
<span className={styles.plantingTab__summaryValue}>
|
||||
{formatDate(plantingData.summary.firstPlantingAt)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.plantingTab__summaryItem}>
|
||||
<span className={styles.plantingTab__summaryLabel}>最近认种</span>
|
||||
<span className={styles.plantingTab__summaryValue}>
|
||||
{formatDate(plantingData.summary.lastPlantingAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 认种分类账 */}
|
||||
<div className={styles.plantingTab__ledger}>
|
||||
<h3>认种分类账明细</h3>
|
||||
<div className={styles.ledgerTable}>
|
||||
<div className={styles.ledgerTable__header}>
|
||||
<div className={styles.ledgerTable__cell}>订单号</div>
|
||||
<div className={styles.ledgerTable__cell}>认种数量</div>
|
||||
<div className={styles.ledgerTable__cell}>金额</div>
|
||||
<div className={styles.ledgerTable__cell}>状态</div>
|
||||
<div className={styles.ledgerTable__cell}>省市</div>
|
||||
<div className={styles.ledgerTable__cell}>创建时间</div>
|
||||
<div className={styles.ledgerTable__cell}>支付时间</div>
|
||||
</div>
|
||||
{plantingData.items.length === 0 ? (
|
||||
<div className={styles.ledgerTable__empty}>暂无认种记录</div>
|
||||
) : (
|
||||
plantingData.items.map((item) => (
|
||||
<div key={item.orderId} className={styles.ledgerTable__row}>
|
||||
<div className={styles.ledgerTable__cell}>{item.orderNo}</div>
|
||||
<div className={styles.ledgerTable__cell}>{formatNumber(item.treeCount)}</div>
|
||||
<div className={styles.ledgerTable__cell}>{formatAmount(item.totalAmount)}</div>
|
||||
<div className={styles.ledgerTable__cell}>
|
||||
<span className={cn(
|
||||
styles.ledgerTable__status,
|
||||
styles[`ledgerTable__status--${item.status.toLowerCase()}`]
|
||||
)}>
|
||||
{plantingStatusLabels[item.status] || item.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.ledgerTable__cell}>
|
||||
{item.selectedProvince || '-'} / {item.selectedCity || '-'}
|
||||
</div>
|
||||
<div className={styles.ledgerTable__cell}>{formatDate(item.createdAt)}</div>
|
||||
<div className={styles.ledgerTable__cell}>{formatDate(item.paidAt)}</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分页 */}
|
||||
{plantingData.totalPages > 1 && (
|
||||
<div className={styles.pagination}>
|
||||
<button
|
||||
disabled={plantingPage === 1}
|
||||
onClick={() => setPlantingPage((p) => p - 1)}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span>第 {plantingPage} / {plantingData.totalPages} 页</span>
|
||||
<button
|
||||
disabled={plantingPage === plantingData.totalPages}
|
||||
onClick={() => setPlantingPage((p) => p + 1)}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.plantingTab__empty}>暂无认种数据</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 钱包信息 Tab */}
|
||||
{activeTab === 'wallet' && (
|
||||
<div className={styles.walletTab}>
|
||||
{walletLoading ? (
|
||||
<div className={styles.walletTab__loading}>加载中...</div>
|
||||
) : walletData ? (
|
||||
<>
|
||||
{/* 钱包汇总 */}
|
||||
<div className={styles.walletTab__summary}>
|
||||
<h3>钱包汇总</h3>
|
||||
<div className={styles.walletTab__summaryGrid}>
|
||||
<div className={styles.walletTab__summaryItem}>
|
||||
<span className={styles.walletTab__summaryLabel}>USDT 可用</span>
|
||||
<span className={styles.walletTab__summaryValue}>
|
||||
{formatAmount(walletData.summary.usdtAvailable)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.walletTab__summaryItem}>
|
||||
<span className={styles.walletTab__summaryLabel}>USDT 冻结</span>
|
||||
<span className={styles.walletTab__summaryValue}>
|
||||
{formatAmount(walletData.summary.usdtFrozen)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.walletTab__summaryItem}>
|
||||
<span className={styles.walletTab__summaryLabel}>DST 可用</span>
|
||||
<span className={styles.walletTab__summaryValue}>
|
||||
{formatAmount(walletData.summary.dstAvailable)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.walletTab__summaryItem}>
|
||||
<span className={styles.walletTab__summaryLabel}>算力</span>
|
||||
<span className={styles.walletTab__summaryValue}>
|
||||
{formatAmount(walletData.summary.hashpower)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.walletTab__summaryItem}>
|
||||
<span className={styles.walletTab__summaryLabel}>待领取收益</span>
|
||||
<span className={styles.walletTab__summaryValue}>
|
||||
{formatAmount(walletData.summary.pendingUsdt)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.walletTab__summaryItem}>
|
||||
<span className={styles.walletTab__summaryLabel}>可结算收益</span>
|
||||
<span className={styles.walletTab__summaryValue}>
|
||||
{formatAmount(walletData.summary.settleableUsdt)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.walletTab__summaryItem}>
|
||||
<span className={styles.walletTab__summaryLabel}>已结算收益</span>
|
||||
<span className={styles.walletTab__summaryValue}>
|
||||
{formatAmount(walletData.summary.settledTotalUsdt)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.walletTab__summaryItem}>
|
||||
<span className={styles.walletTab__summaryLabel}>过期收益</span>
|
||||
<span className={styles.walletTab__summaryValue}>
|
||||
{formatAmount(walletData.summary.expiredTotalUsdt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 钱包分类账 */}
|
||||
<div className={styles.walletTab__ledger}>
|
||||
<h3>钱包分类账明细</h3>
|
||||
<div className={styles.ledgerTable}>
|
||||
<div className={styles.ledgerTable__header}>
|
||||
<div className={styles.ledgerTable__cell}>流水ID</div>
|
||||
<div className={styles.ledgerTable__cell}>类型</div>
|
||||
<div className={styles.ledgerTable__cell}>资产</div>
|
||||
<div className={styles.ledgerTable__cell}>金额</div>
|
||||
<div className={styles.ledgerTable__cell}>余额快照</div>
|
||||
<div className={styles.ledgerTable__cell}>关联订单</div>
|
||||
<div className={styles.ledgerTable__cell}>时间</div>
|
||||
</div>
|
||||
{walletData.items.length === 0 ? (
|
||||
<div className={styles.ledgerTable__empty}>暂无钱包流水</div>
|
||||
) : (
|
||||
walletData.items.map((item) => (
|
||||
<div key={item.entryId} className={styles.ledgerTable__row}>
|
||||
<div className={styles.ledgerTable__cell}>{item.entryId}</div>
|
||||
<div className={styles.ledgerTable__cell}>
|
||||
{entryTypeLabels[item.entryType] || item.entryType}
|
||||
</div>
|
||||
<div className={styles.ledgerTable__cell}>
|
||||
{assetTypeLabels[item.assetType] || item.assetType}
|
||||
</div>
|
||||
<div className={cn(
|
||||
styles.ledgerTable__cell,
|
||||
parseFloat(item.amount) >= 0
|
||||
? styles['ledgerTable__cell--positive']
|
||||
: styles['ledgerTable__cell--negative']
|
||||
)}>
|
||||
{parseFloat(item.amount) >= 0 ? '+' : ''}{formatAmount(item.amount)}
|
||||
</div>
|
||||
<div className={styles.ledgerTable__cell}>
|
||||
{formatAmount(item.balanceAfter)}
|
||||
</div>
|
||||
<div className={styles.ledgerTable__cell}>
|
||||
{item.refOrderId || item.refTxHash || '-'}
|
||||
</div>
|
||||
<div className={styles.ledgerTable__cell}>{formatDate(item.createdAt)}</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分页 */}
|
||||
{walletData.totalPages > 1 && (
|
||||
<div className={styles.pagination}>
|
||||
<button
|
||||
disabled={walletPage === 1}
|
||||
onClick={() => setWalletPage((p) => p - 1)}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span>第 {walletPage} / {walletData.totalPages} 页</span>
|
||||
<button
|
||||
disabled={walletPage === walletData.totalPages}
|
||||
onClick={() => setWalletPage((p) => p + 1)}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.walletTab__empty}>暂无钱包数据</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 授权信息 Tab */}
|
||||
{activeTab === 'authorization' && (
|
||||
<div className={styles.authTab}>
|
||||
{authLoading ? (
|
||||
<div className={styles.authTab__loading}>加载中...</div>
|
||||
) : authData ? (
|
||||
<>
|
||||
{/* 授权角色列表 */}
|
||||
<div className={styles.authTab__roles}>
|
||||
<h3>授权角色</h3>
|
||||
{authData.roles.length === 0 ? (
|
||||
<div className={styles.authTab__empty}>暂无授权角色</div>
|
||||
) : (
|
||||
<div className={styles.authTab__roleGrid}>
|
||||
{authData.roles.map((role) => (
|
||||
<div key={role.id} className={styles.authTab__roleCard}>
|
||||
<div className={styles.authTab__roleHeader}>
|
||||
<span className={styles.authTab__roleType}>
|
||||
{roleTypeLabels[role.roleType] || role.roleType}
|
||||
</span>
|
||||
<span className={cn(
|
||||
styles.authTab__roleStatus,
|
||||
styles[`authTab__roleStatus--${role.status.toLowerCase()}`]
|
||||
)}>
|
||||
{authStatusLabels[role.status] || role.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.authTab__roleInfo}>
|
||||
<p><strong>区域:</strong> {role.regionName} ({role.regionCode})</p>
|
||||
<p><strong>显示头衔:</strong> {role.displayTitle}</p>
|
||||
<p>
|
||||
<strong>权益状态:</strong>
|
||||
{role.benefitActive ? (
|
||||
<span className={styles.authTab__benefitActive}>已激活</span>
|
||||
) : (
|
||||
<span className={styles.authTab__benefitInactive}>未激活</span>
|
||||
)}
|
||||
</p>
|
||||
<p><strong>初始目标:</strong> {formatNumber(role.initialTargetTreeCount)} 棵</p>
|
||||
<p><strong>月度目标类型:</strong> {role.monthlyTargetType}</p>
|
||||
<p><strong>授权时间:</strong> {formatDate(role.authorizedAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 月度考核记录 */}
|
||||
<div className={styles.authTab__assessments}>
|
||||
<h3>月度考核记录</h3>
|
||||
{authData.assessments.length === 0 ? (
|
||||
<div className={styles.authTab__empty}>暂无考核记录</div>
|
||||
) : (
|
||||
<div className={styles.ledgerTable}>
|
||||
<div className={styles.ledgerTable__header}>
|
||||
<div className={styles.ledgerTable__cell}>考核月份</div>
|
||||
<div className={styles.ledgerTable__cell}>角色</div>
|
||||
<div className={styles.ledgerTable__cell}>月度目标/完成</div>
|
||||
<div className={styles.ledgerTable__cell}>累计目标/完成</div>
|
||||
<div className={styles.ledgerTable__cell}>结果</div>
|
||||
<div className={styles.ledgerTable__cell}>区域排名</div>
|
||||
</div>
|
||||
{authData.assessments.map((assessment) => (
|
||||
<div key={assessment.id} className={styles.ledgerTable__row}>
|
||||
<div className={styles.ledgerTable__cell}>{assessment.assessmentMonth}</div>
|
||||
<div className={styles.ledgerTable__cell}>
|
||||
{roleTypeLabels[assessment.roleType] || assessment.roleType}
|
||||
</div>
|
||||
<div className={styles.ledgerTable__cell}>
|
||||
{formatNumber(assessment.monthlyCompleted)} / {formatNumber(assessment.monthlyTarget)}
|
||||
</div>
|
||||
<div className={styles.ledgerTable__cell}>
|
||||
{formatNumber(assessment.cumulativeCompleted)} / {formatNumber(assessment.cumulativeTarget)}
|
||||
</div>
|
||||
<div className={styles.ledgerTable__cell}>
|
||||
<span className={cn(
|
||||
styles.ledgerTable__result,
|
||||
styles[`ledgerTable__result--${assessment.result.toLowerCase()}`]
|
||||
)}>
|
||||
{assessmentResultLabels[assessment.result] || assessment.result}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.ledgerTable__cell}>
|
||||
{assessment.rankingInRegion || '-'}
|
||||
{assessment.isFirstPlace && ' 🥇'}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 系统账户流水(如果有) */}
|
||||
{authData.systemAccountLedger.length > 0 && (
|
||||
<div className={styles.authTab__systemLedger}>
|
||||
<h3>系统账户流水</h3>
|
||||
<div className={styles.ledgerTable}>
|
||||
<div className={styles.ledgerTable__header}>
|
||||
<div className={styles.ledgerTable__cell}>流水ID</div>
|
||||
<div className={styles.ledgerTable__cell}>账户类型</div>
|
||||
<div className={styles.ledgerTable__cell}>流水类型</div>
|
||||
<div className={styles.ledgerTable__cell}>金额</div>
|
||||
<div className={styles.ledgerTable__cell}>余额</div>
|
||||
<div className={styles.ledgerTable__cell}>时间</div>
|
||||
</div>
|
||||
{authData.systemAccountLedger.map((ledger) => (
|
||||
<div key={ledger.ledgerId} className={styles.ledgerTable__row}>
|
||||
<div className={styles.ledgerTable__cell}>{ledger.ledgerId}</div>
|
||||
<div className={styles.ledgerTable__cell}>{ledger.accountType}</div>
|
||||
<div className={styles.ledgerTable__cell}>{ledger.entryType}</div>
|
||||
<div className={cn(
|
||||
styles.ledgerTable__cell,
|
||||
parseFloat(ledger.amount) >= 0
|
||||
? styles['ledgerTable__cell--positive']
|
||||
: styles['ledgerTable__cell--negative']
|
||||
)}>
|
||||
{parseFloat(ledger.amount) >= 0 ? '+' : ''}{formatAmount(ledger.amount)}
|
||||
</div>
|
||||
<div className={styles.ledgerTable__cell}>{formatAmount(ledger.balanceAfter)}</div>
|
||||
<div className={styles.ledgerTable__cell}>{formatDate(ledger.createdAt)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.authTab__empty}>暂无授权数据</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,826 @@
|
|||
/* 用户详情页面样式 */
|
||||
@use '@/styles/variables' as *;
|
||||
@use '@/styles/mixins' as *;
|
||||
|
||||
// ============================================================================
|
||||
// 基础布局
|
||||
// ============================================================================
|
||||
|
||||
.loading,
|
||||
.error {
|
||||
@include flex-center;
|
||||
flex-direction: column;
|
||||
min-height: 300px;
|
||||
gap: $spacing-lg;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.error {
|
||||
p {
|
||||
color: $error-color;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.userDetail {
|
||||
@include flex-column;
|
||||
gap: $spacing-xl;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 返回按钮
|
||||
// ============================================================================
|
||||
|
||||
.userDetail__backBar {
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.userDetail__backBtn {
|
||||
@include flex-start;
|
||||
gap: $spacing-sm;
|
||||
padding: $spacing-sm $spacing-base;
|
||||
border: none;
|
||||
border-radius: $border-radius-base;
|
||||
background: transparent;
|
||||
color: $text-secondary;
|
||||
font-size: $font-size-sm;
|
||||
cursor: pointer;
|
||||
@include transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: $background-color;
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.userDetail__backIcon {
|
||||
font-size: $font-size-md;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 基本信息卡片
|
||||
// ============================================================================
|
||||
|
||||
.userDetail__basicCard {
|
||||
@include card-base;
|
||||
padding: $padding-card;
|
||||
}
|
||||
|
||||
.userDetail__basicHeader {
|
||||
@include flex-start;
|
||||
gap: $spacing-xl;
|
||||
margin-bottom: $spacing-xl;
|
||||
padding-bottom: $spacing-xl;
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.userDetail__avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: $border-radius-round;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-color: $background-color;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.userDetail__status {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
border-radius: $border-radius-round;
|
||||
box-shadow: 0 0 0 3px $card-background;
|
||||
|
||||
&--online {
|
||||
background-color: $success-color;
|
||||
}
|
||||
|
||||
&--offline {
|
||||
background-color: $text-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.userDetail__basicInfo {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.userDetail__nickname {
|
||||
@include flex-start;
|
||||
gap: $spacing-md;
|
||||
font-size: $font-size-xxl;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-primary;
|
||||
margin: 0 0 $spacing-sm;
|
||||
}
|
||||
|
||||
.userDetail__statusBadge {
|
||||
display: inline-block;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
border-radius: $border-radius-sm;
|
||||
font-size: $font-size-xs;
|
||||
font-weight: $font-weight-medium;
|
||||
|
||||
&--active {
|
||||
background-color: rgba($success-color, 0.1);
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&--frozen {
|
||||
background-color: rgba($warning-color, 0.1);
|
||||
color: $warning-color;
|
||||
}
|
||||
|
||||
&--deactivated {
|
||||
background-color: rgba($error-color, 0.1);
|
||||
color: $error-color;
|
||||
}
|
||||
}
|
||||
|
||||
.userDetail__basicMeta {
|
||||
@include flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-lg;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-secondary;
|
||||
margin-bottom: $spacing-xs;
|
||||
|
||||
strong {
|
||||
color: $text-primary;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 统计卡片
|
||||
// ============================================================================
|
||||
|
||||
.userDetail__statsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: $spacing-base;
|
||||
margin-bottom: $spacing-xl;
|
||||
|
||||
@include respond-below(xl) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@include respond-below(md) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.userDetail__statCard {
|
||||
@include flex-column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-lg;
|
||||
background-color: $background-color;
|
||||
border-radius: $border-radius-base;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.userDetail__statLabel {
|
||||
font-size: $font-size-xs;
|
||||
color: $text-secondary;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.userDetail__statValue {
|
||||
font-family: $font-family-number;
|
||||
font-size: $font-size-xl;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-primary;
|
||||
|
||||
&--gold {
|
||||
color: #d4a537;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 推荐人信息
|
||||
// ============================================================================
|
||||
|
||||
.userDetail__referrerInfo {
|
||||
@include flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-md;
|
||||
padding: $spacing-base;
|
||||
background-color: rgba($primary-color, 0.05);
|
||||
border-radius: $border-radius-base;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
||||
.userDetail__referrerLabel {
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.userDetail__referrerLink {
|
||||
color: $primary-color;
|
||||
font-weight: $font-weight-medium;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.userDetail__referrerMeta {
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tab 导航
|
||||
// ============================================================================
|
||||
|
||||
.userDetail__tabs {
|
||||
@include flex-start;
|
||||
gap: $spacing-xs;
|
||||
padding: $spacing-xs;
|
||||
background-color: $background-color;
|
||||
border-radius: $border-radius-base;
|
||||
}
|
||||
|
||||
.userDetail__tab {
|
||||
padding: $spacing-sm $spacing-lg;
|
||||
border: none;
|
||||
border-radius: $border-radius-sm;
|
||||
background: transparent;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $text-secondary;
|
||||
cursor: pointer;
|
||||
@include transition-fast;
|
||||
|
||||
&:hover {
|
||||
color: $text-primary;
|
||||
background-color: rgba($white, 0.5);
|
||||
}
|
||||
|
||||
&--active {
|
||||
background-color: $card-background;
|
||||
color: $primary-color;
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.userDetail__tabContent {
|
||||
@include card-base;
|
||||
padding: $padding-card;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 推荐关系 Tab
|
||||
// ============================================================================
|
||||
|
||||
.referralTab {
|
||||
@include flex-column;
|
||||
gap: $spacing-xl;
|
||||
}
|
||||
|
||||
.referralTab__header {
|
||||
@include flex-between;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.referralTab__loading,
|
||||
.referralTab__empty {
|
||||
@include flex-center;
|
||||
min-height: 200px;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 推荐关系树
|
||||
// ============================================================================
|
||||
|
||||
.referralTree {
|
||||
@include flex-column;
|
||||
align-items: center;
|
||||
gap: $spacing-lg;
|
||||
padding: $spacing-xl;
|
||||
background-color: $background-color;
|
||||
border-radius: $border-radius-lg;
|
||||
}
|
||||
|
||||
.referralTree__ancestors {
|
||||
@include flex-column;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
|
||||
.referralTree__label {
|
||||
font-size: $font-size-xs;
|
||||
color: $text-secondary;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.referralTree__nodeList {
|
||||
@include flex-column;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
|
||||
.referralTree__nodeWrapper {
|
||||
@include flex-column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.referralTree__connector {
|
||||
color: $text-disabled;
|
||||
font-size: $font-size-lg;
|
||||
padding: $spacing-xs 0;
|
||||
}
|
||||
|
||||
.referralTree__current {
|
||||
@include flex-center;
|
||||
}
|
||||
|
||||
.referralTree__node {
|
||||
@include flex-column;
|
||||
align-items: center;
|
||||
padding: $spacing-base $spacing-xl;
|
||||
background-color: $card-background;
|
||||
border: 2px solid $border-color;
|
||||
border-radius: $border-radius-base;
|
||||
cursor: pointer;
|
||||
@include transition-fast;
|
||||
|
||||
&:hover {
|
||||
border-color: $primary-color;
|
||||
box-shadow: $shadow-md;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&--current {
|
||||
border-color: $primary-color;
|
||||
background-color: rgba($primary-color, 0.05);
|
||||
}
|
||||
|
||||
&--highlight {
|
||||
border-width: 3px;
|
||||
box-shadow: 0 0 0 4px rgba($primary-color, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.referralTree__nodeSeq {
|
||||
font-family: $font-family-number;
|
||||
font-size: $font-size-md;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.referralTree__nodeNickname {
|
||||
font-size: $font-size-xs;
|
||||
color: $text-secondary;
|
||||
margin-top: $spacing-xs;
|
||||
}
|
||||
|
||||
.referralTree__nodeAdoptions {
|
||||
font-size: $font-size-xs;
|
||||
color: $text-secondary;
|
||||
margin-top: $spacing-xs;
|
||||
}
|
||||
|
||||
.referralTree__nodeCount {
|
||||
font-size: $font-size-xs;
|
||||
color: $primary-color;
|
||||
margin-top: $spacing-xs;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
.referralTree__directReferrals {
|
||||
@include flex-column;
|
||||
align-items: center;
|
||||
gap: $spacing-md;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.referralTree__nodeGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: $spacing-md;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.referralTree__empty {
|
||||
@include flex-center;
|
||||
min-height: 100px;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 认种信息 Tab
|
||||
// ============================================================================
|
||||
|
||||
.plantingTab {
|
||||
@include flex-column;
|
||||
gap: $spacing-xl;
|
||||
}
|
||||
|
||||
.plantingTab__loading,
|
||||
.plantingTab__empty {
|
||||
@include flex-center;
|
||||
min-height: 200px;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.plantingTab__summary,
|
||||
.plantingTab__ledger {
|
||||
@include flex-column;
|
||||
gap: $spacing-lg;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.plantingTab__summaryGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: $spacing-base;
|
||||
|
||||
@include respond-below(xl) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@include respond-below(md) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.plantingTab__summaryItem {
|
||||
@include flex-column;
|
||||
padding: $spacing-base;
|
||||
background-color: $background-color;
|
||||
border-radius: $border-radius-base;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.plantingTab__summaryLabel {
|
||||
font-size: $font-size-xs;
|
||||
color: $text-secondary;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.plantingTab__summaryValue {
|
||||
font-family: $font-family-number;
|
||||
font-size: $font-size-md;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 钱包信息 Tab
|
||||
// ============================================================================
|
||||
|
||||
.walletTab {
|
||||
@include flex-column;
|
||||
gap: $spacing-xl;
|
||||
}
|
||||
|
||||
.walletTab__loading,
|
||||
.walletTab__empty {
|
||||
@include flex-center;
|
||||
min-height: 200px;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.walletTab__summary,
|
||||
.walletTab__ledger {
|
||||
@include flex-column;
|
||||
gap: $spacing-lg;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.walletTab__summaryGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: $spacing-base;
|
||||
|
||||
@include respond-below(lg) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@include respond-below(sm) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.walletTab__summaryItem {
|
||||
@include flex-column;
|
||||
padding: $spacing-base;
|
||||
background-color: $background-color;
|
||||
border-radius: $border-radius-base;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.walletTab__summaryLabel {
|
||||
font-size: $font-size-xs;
|
||||
color: $text-secondary;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.walletTab__summaryValue {
|
||||
font-family: $font-family-number;
|
||||
font-size: $font-size-md;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 授权信息 Tab
|
||||
// ============================================================================
|
||||
|
||||
.authTab {
|
||||
@include flex-column;
|
||||
gap: $spacing-xl;
|
||||
}
|
||||
|
||||
.authTab__loading,
|
||||
.authTab__empty {
|
||||
@include flex-center;
|
||||
min-height: 200px;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.authTab__roles,
|
||||
.authTab__assessments,
|
||||
.authTab__systemLedger {
|
||||
@include flex-column;
|
||||
gap: $spacing-lg;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.authTab__roleGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: $spacing-base;
|
||||
}
|
||||
|
||||
.authTab__roleCard {
|
||||
@include flex-column;
|
||||
padding: $spacing-lg;
|
||||
background-color: $background-color;
|
||||
border-radius: $border-radius-base;
|
||||
border: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.authTab__roleHeader {
|
||||
@include flex-between;
|
||||
margin-bottom: $spacing-md;
|
||||
padding-bottom: $spacing-md;
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.authTab__roleType {
|
||||
font-size: $font-size-md;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.authTab__roleStatus {
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
border-radius: $border-radius-sm;
|
||||
font-size: $font-size-xs;
|
||||
font-weight: $font-weight-medium;
|
||||
|
||||
&--pending {
|
||||
background-color: rgba($warning-color, 0.1);
|
||||
color: $warning-color;
|
||||
}
|
||||
|
||||
&--authorized {
|
||||
background-color: rgba($success-color, 0.1);
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&--revoked {
|
||||
background-color: rgba($error-color, 0.1);
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
&--expired {
|
||||
background-color: rgba($text-disabled, 0.2);
|
||||
color: $text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.authTab__roleInfo {
|
||||
@include flex-column;
|
||||
gap: $spacing-xs;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-secondary;
|
||||
|
||||
strong {
|
||||
color: $text-primary;
|
||||
margin-right: $spacing-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.authTab__benefitActive {
|
||||
color: $success-color;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
.authTab__benefitInactive {
|
||||
color: $text-disabled;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 通用分类账表格
|
||||
// ============================================================================
|
||||
|
||||
.ledgerTable {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
@include custom-scrollbar;
|
||||
}
|
||||
|
||||
.ledgerTable__header {
|
||||
display: flex;
|
||||
background-color: $background-color;
|
||||
border-bottom: 2px solid $border-color;
|
||||
min-width: 900px;
|
||||
}
|
||||
|
||||
.ledgerTable__row {
|
||||
display: flex;
|
||||
border-bottom: 1px solid $border-color;
|
||||
min-width: 900px;
|
||||
@include transition-fast;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($primary-color, 0.02);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ledgerTable__cell {
|
||||
flex: 1;
|
||||
padding: $spacing-md $spacing-base;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-primary;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.ledgerTable__header & {
|
||||
font-weight: $font-weight-medium;
|
||||
color: $text-secondary;
|
||||
text-transform: uppercase;
|
||||
font-size: $font-size-xs;
|
||||
}
|
||||
|
||||
&--positive {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&--negative {
|
||||
color: $error-color;
|
||||
}
|
||||
}
|
||||
|
||||
.ledgerTable__empty {
|
||||
@include flex-center;
|
||||
min-height: 120px;
|
||||
color: $text-secondary;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
||||
.ledgerTable__status {
|
||||
display: inline-block;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
border-radius: $border-radius-sm;
|
||||
font-size: $font-size-xs;
|
||||
font-weight: $font-weight-medium;
|
||||
|
||||
&--created {
|
||||
background-color: rgba($info-color, 0.1);
|
||||
color: $info-color;
|
||||
}
|
||||
|
||||
&--paid {
|
||||
background-color: rgba($primary-color, 0.1);
|
||||
color: $primary-color;
|
||||
}
|
||||
|
||||
&--fund_allocated {
|
||||
background-color: rgba($warning-color, 0.1);
|
||||
color: $warning-color;
|
||||
}
|
||||
|
||||
&--mining_enabled {
|
||||
background-color: rgba($success-color, 0.1);
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&--cancelled {
|
||||
background-color: rgba($error-color, 0.1);
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
&--expired {
|
||||
background-color: rgba($text-disabled, 0.2);
|
||||
color: $text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.ledgerTable__result {
|
||||
display: inline-block;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
border-radius: $border-radius-sm;
|
||||
font-size: $font-size-xs;
|
||||
font-weight: $font-weight-medium;
|
||||
|
||||
&--not_assessed {
|
||||
background-color: rgba($text-disabled, 0.2);
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
&--passed {
|
||||
background-color: rgba($success-color, 0.1);
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&--failed {
|
||||
background-color: rgba($error-color, 0.1);
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
&--bypassed {
|
||||
background-color: rgba($warning-color, 0.1);
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 分页
|
||||
// ============================================================================
|
||||
|
||||
.pagination {
|
||||
@include flex-center;
|
||||
gap: $spacing-md;
|
||||
padding-top: $spacing-lg;
|
||||
|
||||
button {
|
||||
padding: $spacing-sm $spacing-base;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: $border-radius-sm;
|
||||
background-color: $card-background;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-primary;
|
||||
cursor: pointer;
|
||||
@include transition-fast;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: $primary-color;
|
||||
color: $primary-color;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: $text-disabled;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-secondary;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,12 @@
|
|||
|
||||
import { useState, useCallback } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { Modal, toast, Button } from '@/components/common';
|
||||
import Link from 'next/link';
|
||||
import { toast, Button } from '@/components/common';
|
||||
import { PageContainer } from '@/components/layout';
|
||||
import { cn } from '@/utils/helpers';
|
||||
import { formatNumber, formatRanking } from '@/utils/formatters';
|
||||
import { useUsers, useUserDetail } from '@/hooks';
|
||||
import { useUsers } from '@/hooks';
|
||||
import type { UserListItem } from '@/services/userService';
|
||||
import styles from './users.module.scss';
|
||||
|
||||
|
|
@ -48,7 +49,6 @@ export default function UsersPage() {
|
|||
const [keyword, setKeyword] = useState('');
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState<string[]>([]);
|
||||
const [detailUserId, setDetailUserId] = useState<string | null>(null);
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
|
|
@ -68,12 +68,6 @@ export default function UsersPage() {
|
|||
sortOrder: 'desc',
|
||||
});
|
||||
|
||||
// 获取用户详情
|
||||
const {
|
||||
data: userDetail,
|
||||
isLoading: detailLoading,
|
||||
} = useUserDetail(detailUserId || '');
|
||||
|
||||
const users = usersData?.items ?? [];
|
||||
const total = usersData?.total ?? 0;
|
||||
const totalPages = usersData?.totalPages ?? 1;
|
||||
|
|
@ -107,16 +101,6 @@ export default function UsersPage() {
|
|||
toast.success('导出功能开发中');
|
||||
}, []);
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = useCallback((user: UserListItem) => {
|
||||
setDetailUserId(user.accountId);
|
||||
}, []);
|
||||
|
||||
// 关闭详情弹窗
|
||||
const handleCloseDetail = useCallback(() => {
|
||||
setDetailUserId(null);
|
||||
}, []);
|
||||
|
||||
// 生成分页按钮
|
||||
const renderPaginationButtons = () => {
|
||||
const buttons = [];
|
||||
|
|
@ -483,12 +467,12 @@ export default function UsersPage() {
|
|||
|
||||
{/* 操作 */}
|
||||
<div className={cn(styles.users__tableCell, styles['users__tableCell--actions'])}>
|
||||
<button
|
||||
<Link
|
||||
href={`/users/${user.accountSequence || user.accountId}`}
|
||||
className={styles.users__rowAction}
|
||||
onClick={() => handleViewDetail(user)}
|
||||
>
|
||||
查看详情
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
|
@ -518,66 +502,6 @@ export default function UsersPage() {
|
|||
<div className={styles.users__paginationList}>{renderPaginationButtons()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 用户详情弹窗 */}
|
||||
<Modal
|
||||
visible={!!detailUserId}
|
||||
title="用户详情"
|
||||
onClose={handleCloseDetail}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
{detailLoading ? (
|
||||
<div className={styles.userDetail__loading}>加载中...</div>
|
||||
) : userDetail ? (
|
||||
<div className={styles.userDetail}>
|
||||
<div className={styles.userDetail__header}>
|
||||
<div
|
||||
className={styles.users__avatar}
|
||||
style={{
|
||||
backgroundImage: `url(${userDetail.avatar || '/images/Data@2x.png'})`,
|
||||
width: 64,
|
||||
height: 64,
|
||||
}}
|
||||
/>
|
||||
<div className={styles.userDetail__info}>
|
||||
<h3>{userDetail.nickname || '未设置昵称'}</h3>
|
||||
<p>账户序号: {userDetail.accountSequence}</p>
|
||||
<p>手机号: {userDetail.phoneNumberMasked || '未绑定'}</p>
|
||||
<p>KYC状态: {userDetail.kycStatus}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.userDetail__stats}>
|
||||
<div className={styles.userDetail__statItem}>
|
||||
<span className={styles.userDetail__statLabel}>个人认种量</span>
|
||||
<span className={styles.userDetail__statValue}>
|
||||
{formatNumber(userDetail.personalAdoptions)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.userDetail__statItem}>
|
||||
<span className={styles.userDetail__statLabel}>团队认种量</span>
|
||||
<span className={styles.userDetail__statValue}>
|
||||
{formatNumber(userDetail.teamAdoptions)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.userDetail__statItem}>
|
||||
<span className={styles.userDetail__statLabel}>龙虎榜排名</span>
|
||||
<span className={styles.userDetail__statValue}>
|
||||
{formatRanking(userDetail.ranking)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.userDetail__meta}>
|
||||
<p>注册时间: {new Date(userDetail.registeredAt).toLocaleString()}</p>
|
||||
{userDetail.lastActiveAt && (
|
||||
<p>最后活跃: {new Date(userDetail.lastActiveAt).toLocaleString()}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.userDetail__empty}>未找到用户信息</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,5 +2,6 @@
|
|||
|
||||
export * from './useDashboard';
|
||||
export * from './useUsers';
|
||||
export * from './useUserDetailPage';
|
||||
export * from './useAuthorizations';
|
||||
export * from './useSystemWithdrawal';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* 用户详情页面 Hooks
|
||||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { userDetailService } from '@/services/userDetailService';
|
||||
import type {
|
||||
LedgerQueryParams,
|
||||
WalletLedgerQueryParams,
|
||||
} from '@/types/userDetail.types';
|
||||
|
||||
// Query Keys
|
||||
export const userDetailKeys = {
|
||||
all: ['userDetail'] as const,
|
||||
fullDetail: (accountSequence: string) => [...userDetailKeys.all, 'fullDetail', accountSequence] as const,
|
||||
referralTree: (accountSequence: string, direction: string, depth: number) =>
|
||||
[...userDetailKeys.all, 'referralTree', accountSequence, direction, depth] as const,
|
||||
plantingLedger: (accountSequence: string, params: LedgerQueryParams) =>
|
||||
[...userDetailKeys.all, 'plantingLedger', accountSequence, params] as const,
|
||||
walletLedger: (accountSequence: string, params: WalletLedgerQueryParams) =>
|
||||
[...userDetailKeys.all, 'walletLedger', accountSequence, params] as const,
|
||||
authorizationDetail: (accountSequence: string) =>
|
||||
[...userDetailKeys.all, 'authorizationDetail', accountSequence] as const,
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户完整信息
|
||||
*/
|
||||
export function useUserFullDetail(accountSequence: string) {
|
||||
return useQuery({
|
||||
queryKey: userDetailKeys.fullDetail(accountSequence),
|
||||
queryFn: () => userDetailService.getFullDetail(accountSequence),
|
||||
enabled: !!accountSequence,
|
||||
staleTime: 60 * 1000, // 1分钟
|
||||
gcTime: 5 * 60 * 1000, // 5分钟
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取推荐关系树
|
||||
*/
|
||||
export function useReferralTree(
|
||||
accountSequence: string,
|
||||
direction: 'up' | 'down' | 'both' = 'both',
|
||||
depth: number = 1
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: userDetailKeys.referralTree(accountSequence, direction, depth),
|
||||
queryFn: () => userDetailService.getReferralTree(accountSequence, direction, depth),
|
||||
enabled: !!accountSequence,
|
||||
staleTime: 60 * 1000,
|
||||
gcTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取认种分类账
|
||||
*/
|
||||
export function usePlantingLedger(
|
||||
accountSequence: string,
|
||||
params: LedgerQueryParams = {}
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: userDetailKeys.plantingLedger(accountSequence, params),
|
||||
queryFn: () => userDetailService.getPlantingLedger(accountSequence, params),
|
||||
enabled: !!accountSequence,
|
||||
staleTime: 30 * 1000,
|
||||
gcTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取钱包分类账
|
||||
*/
|
||||
export function useWalletLedger(
|
||||
accountSequence: string,
|
||||
params: WalletLedgerQueryParams = {}
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: userDetailKeys.walletLedger(accountSequence, params),
|
||||
queryFn: () => userDetailService.getWalletLedger(accountSequence, params),
|
||||
enabled: !!accountSequence,
|
||||
staleTime: 30 * 1000,
|
||||
gcTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取授权信息
|
||||
*/
|
||||
export function useAuthorizationDetail(accountSequence: string) {
|
||||
return useQuery({
|
||||
queryKey: userDetailKeys.authorizationDetail(accountSequence),
|
||||
queryFn: () => userDetailService.getAuthorizationDetail(accountSequence),
|
||||
enabled: !!accountSequence,
|
||||
staleTime: 60 * 1000,
|
||||
gcTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
|
@ -21,6 +21,15 @@ export const API_ENDPOINTS = {
|
|||
EXPORT: '/v1/admin/users/export',
|
||||
},
|
||||
|
||||
// 用户详情页面 (admin-service) - 完整用户信息、推荐关系、分类账等
|
||||
USER_DETAIL: {
|
||||
FULL_DETAIL: (accountSequence: string) => `/v1/admin/users/${accountSequence}/full-detail`,
|
||||
REFERRAL_TREE: (accountSequence: string) => `/v1/admin/users/${accountSequence}/referral-tree`,
|
||||
PLANTING_LEDGER: (accountSequence: string) => `/v1/admin/users/${accountSequence}/planting-ledger`,
|
||||
WALLET_LEDGER: (accountSequence: string) => `/v1/admin/users/${accountSequence}/wallet-ledger`,
|
||||
AUTHORIZATION_DETAIL: (accountSequence: string) => `/v1/admin/users/${accountSequence}/authorization-detail`,
|
||||
},
|
||||
|
||||
// 龙虎榜 (leaderboard-service)
|
||||
LEADERBOARD: {
|
||||
RANKINGS: '/v1/leaderboard/rankings',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
* 用户详情服务
|
||||
* 负责用户详情页面的 API 调用
|
||||
*/
|
||||
|
||||
import apiClient from '@/infrastructure/api/client';
|
||||
import { API_ENDPOINTS } from '@/infrastructure/api/endpoints';
|
||||
import type {
|
||||
UserFullDetail,
|
||||
ReferralTreeData,
|
||||
PlantingLedgerResponse,
|
||||
WalletLedgerResponse,
|
||||
AuthorizationDetailResponse,
|
||||
LedgerQueryParams,
|
||||
WalletLedgerQueryParams,
|
||||
} from '@/types/userDetail.types';
|
||||
|
||||
/**
|
||||
* 用户详情服务
|
||||
*/
|
||||
export const userDetailService = {
|
||||
/**
|
||||
* 获取用户完整信息
|
||||
*/
|
||||
async getFullDetail(accountSequence: string): Promise<UserFullDetail> {
|
||||
return apiClient.get(API_ENDPOINTS.USER_DETAIL.FULL_DETAIL(accountSequence));
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取推荐关系树
|
||||
* @param accountSequence 用户账户序号
|
||||
* @param direction 方向:up-向上查推荐人链,down-向下查直推用户,both-双向
|
||||
* @param depth 深度,默认1级
|
||||
*/
|
||||
async getReferralTree(
|
||||
accountSequence: string,
|
||||
direction: 'up' | 'down' | 'both' = 'both',
|
||||
depth: number = 1
|
||||
): Promise<ReferralTreeData> {
|
||||
return apiClient.get(API_ENDPOINTS.USER_DETAIL.REFERRAL_TREE(accountSequence), {
|
||||
params: { direction, depth },
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取认种分类账
|
||||
*/
|
||||
async getPlantingLedger(
|
||||
accountSequence: string,
|
||||
params: LedgerQueryParams = {}
|
||||
): Promise<PlantingLedgerResponse> {
|
||||
return apiClient.get(API_ENDPOINTS.USER_DETAIL.PLANTING_LEDGER(accountSequence), {
|
||||
params: {
|
||||
page: params.page || 1,
|
||||
pageSize: params.pageSize || 20,
|
||||
startDate: params.startDate,
|
||||
endDate: params.endDate,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取钱包分类账
|
||||
*/
|
||||
async getWalletLedger(
|
||||
accountSequence: string,
|
||||
params: WalletLedgerQueryParams = {}
|
||||
): Promise<WalletLedgerResponse> {
|
||||
return apiClient.get(API_ENDPOINTS.USER_DETAIL.WALLET_LEDGER(accountSequence), {
|
||||
params: {
|
||||
page: params.page || 1,
|
||||
pageSize: params.pageSize || 20,
|
||||
assetType: params.assetType,
|
||||
entryType: params.entryType,
|
||||
startDate: params.startDate,
|
||||
endDate: params.endDate,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取授权信息
|
||||
*/
|
||||
async getAuthorizationDetail(accountSequence: string): Promise<AuthorizationDetailResponse> {
|
||||
return apiClient.get(API_ENDPOINTS.USER_DETAIL.AUTHORIZATION_DETAIL(accountSequence));
|
||||
},
|
||||
};
|
||||
|
||||
export default userDetailService;
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
/**
|
||||
* 用户详情页面类型定义
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// 用户基本信息
|
||||
// ============================================================================
|
||||
|
||||
export interface UserFullDetail {
|
||||
// 基本信息
|
||||
accountId: string;
|
||||
accountSequence: string;
|
||||
avatar: string | null;
|
||||
nickname: string | null;
|
||||
phoneNumberMasked: string | null;
|
||||
status: 'active' | 'frozen' | 'deactivated';
|
||||
kycStatus: string;
|
||||
isOnline: boolean;
|
||||
registeredAt: string;
|
||||
lastActiveAt: string | null;
|
||||
|
||||
// 认种统计
|
||||
personalAdoptions: number;
|
||||
teamAddresses: number;
|
||||
teamAdoptions: number;
|
||||
provincialAdoptions: {
|
||||
count: number;
|
||||
percentage: number;
|
||||
};
|
||||
cityAdoptions: {
|
||||
count: number;
|
||||
percentage: number;
|
||||
};
|
||||
|
||||
// 排名
|
||||
ranking: number | null;
|
||||
|
||||
// 推荐信息
|
||||
referralInfo: {
|
||||
myReferralCode: string;
|
||||
usedReferralCode: string | null;
|
||||
referrerId: string | null;
|
||||
referrerSequence: string | null;
|
||||
referrerNickname: string | null;
|
||||
ancestorPath: string | null;
|
||||
depth: number;
|
||||
directReferralCount: number;
|
||||
activeDirectCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 推荐关系树
|
||||
// ============================================================================
|
||||
|
||||
export interface ReferralNode {
|
||||
accountSequence: string;
|
||||
userId: string;
|
||||
nickname: string | null;
|
||||
avatar: string | null;
|
||||
personalAdoptions: number;
|
||||
depth: number;
|
||||
directReferralCount: number;
|
||||
isCurrentUser?: boolean;
|
||||
}
|
||||
|
||||
export interface ReferralTreeData {
|
||||
currentUser: ReferralNode;
|
||||
ancestors: ReferralNode[]; // 向上的推荐人链(从直接推荐人到最顶层)
|
||||
directReferrals: ReferralNode[]; // 直推用户列表
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 认种信息
|
||||
// ============================================================================
|
||||
|
||||
export interface PlantingSummary {
|
||||
totalOrders: number;
|
||||
totalTreeCount: number;
|
||||
totalAmount: string;
|
||||
effectiveTreeCount: number;
|
||||
pendingTreeCount: number;
|
||||
firstPlantingAt: string | null;
|
||||
lastPlantingAt: string | null;
|
||||
}
|
||||
|
||||
export interface PlantingLedgerItem {
|
||||
orderId: string;
|
||||
orderNo: string;
|
||||
treeCount: number;
|
||||
totalAmount: string;
|
||||
status: string;
|
||||
selectedProvince: string | null;
|
||||
selectedCity: string | null;
|
||||
createdAt: string;
|
||||
paidAt: string | null;
|
||||
fundAllocatedAt: string | null;
|
||||
miningEnabledAt: string | null;
|
||||
}
|
||||
|
||||
export interface PlantingLedgerResponse {
|
||||
summary: PlantingSummary;
|
||||
items: PlantingLedgerItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
// 资金分配明细
|
||||
export interface FundAllocationItem {
|
||||
allocationId: string;
|
||||
orderId: string;
|
||||
orderNo: string;
|
||||
targetType: string;
|
||||
amount: string;
|
||||
targetAccountId: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 钱包信息
|
||||
// ============================================================================
|
||||
|
||||
export interface WalletSummary {
|
||||
// USDT
|
||||
usdtAvailable: string;
|
||||
usdtFrozen: string;
|
||||
// DST
|
||||
dstAvailable: string;
|
||||
dstFrozen: string;
|
||||
// BNB
|
||||
bnbAvailable: string;
|
||||
bnbFrozen: string;
|
||||
// OG
|
||||
ogAvailable: string;
|
||||
ogFrozen: string;
|
||||
// RWAD
|
||||
rwadAvailable: string;
|
||||
rwadFrozen: string;
|
||||
// 算力
|
||||
hashpower: string;
|
||||
// 收益
|
||||
pendingUsdt: string;
|
||||
pendingHashpower: string;
|
||||
settleableUsdt: string;
|
||||
settleableHashpower: string;
|
||||
settledTotalUsdt: string;
|
||||
settledTotalHashpower: string;
|
||||
expiredTotalUsdt: string;
|
||||
expiredTotalHashpower: string;
|
||||
}
|
||||
|
||||
export interface WalletLedgerItem {
|
||||
entryId: string;
|
||||
entryType: string;
|
||||
assetType: string;
|
||||
amount: string;
|
||||
balanceAfter: string | null;
|
||||
refOrderId: string | null;
|
||||
refTxHash: string | null;
|
||||
memo: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface WalletLedgerResponse {
|
||||
summary: WalletSummary;
|
||||
items: WalletLedgerItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 授权信息
|
||||
// ============================================================================
|
||||
|
||||
export interface AuthorizationRole {
|
||||
id: string;
|
||||
roleType: string;
|
||||
regionCode: string;
|
||||
regionName: string;
|
||||
displayTitle: string;
|
||||
status: string;
|
||||
benefitActive: boolean;
|
||||
benefitActivatedAt: string | null;
|
||||
authorizedAt: string | null;
|
||||
authorizedBy: string | null;
|
||||
initialTargetTreeCount: number;
|
||||
monthlyTargetType: string;
|
||||
lastAssessmentMonth: string | null;
|
||||
monthlyTreesAdded: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface MonthlyAssessment {
|
||||
id: string;
|
||||
authorizationId: string;
|
||||
roleType: string;
|
||||
regionCode: string;
|
||||
assessmentMonth: string;
|
||||
monthIndex: number;
|
||||
monthlyTarget: number;
|
||||
monthlyCompleted: number;
|
||||
cumulativeTarget: number;
|
||||
cumulativeCompleted: number;
|
||||
result: string;
|
||||
rankingInRegion: number | null;
|
||||
isFirstPlace: boolean;
|
||||
isBypassed: boolean;
|
||||
completedAt: string | null;
|
||||
assessedAt: string | null;
|
||||
}
|
||||
|
||||
export interface SystemAccountLedgerItem {
|
||||
ledgerId: string;
|
||||
accountId: string;
|
||||
accountType: string;
|
||||
entryType: string;
|
||||
amount: string;
|
||||
balanceAfter: string;
|
||||
sourceOrderId: string | null;
|
||||
sourceRewardId: string | null;
|
||||
txHash: string | null;
|
||||
memo: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface AuthorizationDetailResponse {
|
||||
roles: AuthorizationRole[];
|
||||
assessments: MonthlyAssessment[];
|
||||
systemAccountLedger: SystemAccountLedgerItem[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API 查询参数
|
||||
// ============================================================================
|
||||
|
||||
export interface LedgerQueryParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
export interface WalletLedgerQueryParams extends LedgerQueryParams {
|
||||
assetType?: string;
|
||||
entryType?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 流水类型映射
|
||||
// ============================================================================
|
||||
|
||||
export const WALLET_ENTRY_TYPE_LABELS: Record<string, string> = {
|
||||
// 充值
|
||||
DEPOSIT: '充值',
|
||||
DEPOSIT_USDT: 'USDT充值',
|
||||
DEPOSIT_BNB: 'BNB充值',
|
||||
// 提现
|
||||
WITHDRAW: '提现',
|
||||
WITHDRAW_FROZEN: '提现冻结',
|
||||
WITHDRAW_CONFIRMED: '提现确认',
|
||||
WITHDRAW_CANCELLED: '提现取消',
|
||||
// 认种
|
||||
PLANTING_PAYMENT: '认种支付',
|
||||
PLANTING_FROZEN: '认种冻结',
|
||||
PLANTING_DEDUCT: '认种扣款',
|
||||
// 收益
|
||||
REWARD_PENDING: '收益待领取',
|
||||
REWARD_SETTLED: '收益结算',
|
||||
REWARD_EXPIRED: '收益过期',
|
||||
// 转账
|
||||
TRANSFER_OUT: '转出',
|
||||
TRANSFER_IN: '转入',
|
||||
INTERNAL_TRANSFER: '内部转账',
|
||||
// 其他
|
||||
ADMIN_ADJUSTMENT: '管理员调整',
|
||||
SYSTEM_DEDUCT: '系统扣款',
|
||||
FEE: '手续费',
|
||||
};
|
||||
|
||||
export const ASSET_TYPE_LABELS: Record<string, string> = {
|
||||
USDT: 'USDT',
|
||||
DST: 'DST',
|
||||
BNB: 'BNB',
|
||||
OG: 'OG',
|
||||
RWAD: 'RWAD',
|
||||
HASHPOWER: '算力',
|
||||
};
|
||||
|
||||
export const PLANTING_STATUS_LABELS: Record<string, string> = {
|
||||
CREATED: '已创建',
|
||||
PAID: '已支付',
|
||||
FUND_ALLOCATED: '资金已分配',
|
||||
MINING_ENABLED: '已开始挖矿',
|
||||
CANCELLED: '已取消',
|
||||
EXPIRED: '已过期',
|
||||
};
|
||||
|
||||
export const AUTHORIZATION_ROLE_LABELS: Record<string, string> = {
|
||||
COMMUNITY_PARTNER: '社区合伙人',
|
||||
PROVINCE_COMPANY: '省公司',
|
||||
CITY_COMPANY: '市公司',
|
||||
AUTH_PROVINCE_COMPANY: '授权省公司',
|
||||
AUTH_CITY_COMPANY: '授权市公司',
|
||||
};
|
||||
|
||||
export const AUTHORIZATION_STATUS_LABELS: Record<string, string> = {
|
||||
PENDING: '待授权',
|
||||
AUTHORIZED: '已授权',
|
||||
REVOKED: '已撤销',
|
||||
EXPIRED: '已过期',
|
||||
};
|
||||
|
||||
export const ASSESSMENT_RESULT_LABELS: Record<string, string> = {
|
||||
NOT_ASSESSED: '未考核',
|
||||
PASSED: '通过',
|
||||
FAILED: '未通过',
|
||||
BYPASSED: '豁免',
|
||||
};
|
||||
Loading…
Reference in New Issue