From 3d31e8beb901974ab10dd95226ccce0fc8d3421f Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 7 Jan 2026 20:10:01 -0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E5=AE=9E=E7=8E=B0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=AF=A6=E6=83=85=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前端 (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 --- .../api/controllers/user-detail.controller.ts | 402 +++++++++ .../api/dto/request/user-detail-query.dto.ts | 57 ++ .../src/api/dto/response/user-detail.dto.ts | 266 ++++++ .../services/admin-service/src/app.module.ts | 9 + .../user-detail-query.repository.ts | 248 ++++++ .../user-detail-query.repository.impl.ts | 425 +++++++++ .../users/[id]/IMPLEMENTATION_PLAN.md | 342 ++++++++ .../src/app/(dashboard)/users/[id]/page.tsx | 803 +++++++++++++++++ .../users/[id]/user-detail.module.scss | 826 ++++++++++++++++++ .../src/app/(dashboard)/users/page.tsx | 88 +- frontend/admin-web/src/hooks/index.ts | 1 + .../admin-web/src/hooks/useUserDetailPage.ts | 99 +++ .../src/infrastructure/api/endpoints.ts | 9 + .../src/services/userDetailService.ts | 89 ++ .../admin-web/src/types/userDetail.types.ts | 322 +++++++ 15 files changed, 3904 insertions(+), 82 deletions(-) create mode 100644 backend/services/admin-service/src/api/controllers/user-detail.controller.ts create mode 100644 backend/services/admin-service/src/api/dto/request/user-detail-query.dto.ts create mode 100644 backend/services/admin-service/src/api/dto/response/user-detail.dto.ts create mode 100644 backend/services/admin-service/src/domain/repositories/user-detail-query.repository.ts create mode 100644 backend/services/admin-service/src/infrastructure/persistence/repositories/user-detail-query.repository.impl.ts create mode 100644 frontend/admin-web/src/app/(dashboard)/users/[id]/IMPLEMENTATION_PLAN.md create mode 100644 frontend/admin-web/src/app/(dashboard)/users/[id]/page.tsx create mode 100644 frontend/admin-web/src/app/(dashboard)/users/[id]/user-detail.module.scss create mode 100644 frontend/admin-web/src/hooks/useUserDetailPage.ts create mode 100644 frontend/admin-web/src/services/userDetailService.ts create mode 100644 frontend/admin-web/src/types/userDetail.types.ts diff --git a/backend/services/admin-service/src/api/controllers/user-detail.controller.ts b/backend/services/admin-service/src/api/controllers/user-detail.controller.ts new file mode 100644 index 00000000..74321e28 --- /dev/null +++ b/backend/services/admin-service/src/api/controllers/user-detail.controller.ts @@ -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 { + // 获取基本用户信息 + 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 { + // 获取当前用户信息 + 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 { + 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 { + 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 { + 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+$/, ''); + } +} diff --git a/backend/services/admin-service/src/api/dto/request/user-detail-query.dto.ts b/backend/services/admin-service/src/api/dto/request/user-detail-query.dto.ts new file mode 100644 index 00000000..6a9ffe51 --- /dev/null +++ b/backend/services/admin-service/src/api/dto/request/user-detail-query.dto.ts @@ -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; +} diff --git a/backend/services/admin-service/src/api/dto/response/user-detail.dto.ts b/backend/services/admin-service/src/api/dto/response/user-detail.dto.ts new file mode 100644 index 00000000..49045348 --- /dev/null +++ b/backend/services/admin-service/src/api/dto/response/user-detail.dto.ts @@ -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[]; +} diff --git a/backend/services/admin-service/src/app.module.ts b/backend/services/admin-service/src/app.module.ts index 710e8ceb..9b9f82c8 100644 --- a/backend/services/admin-service/src/app.module.ts +++ b/backend/services/admin-service/src/app.module.ts @@ -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 { diff --git a/backend/services/admin-service/src/domain/repositories/user-detail-query.repository.ts b/backend/services/admin-service/src/domain/repositories/user-detail-query.repository.ts new file mode 100644 index 00000000..7d2a8cad --- /dev/null +++ b/backend/services/admin-service/src/domain/repositories/user-detail-query.repository.ts @@ -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; + + /** + * 获取用户推荐人链(向上) + */ + getAncestors(accountSequence: string, depth: number): Promise; + + /** + * 获取用户直推列表(向下) + */ + getDirectReferrals(accountSequence: string): Promise; + + /** + * 获取推荐人昵称 + */ + getReferrerNickname(referrerId: bigint): Promise; + + /** + * 获取认种汇总 + */ + getPlantingSummary(userId: bigint): Promise; + + /** + * 获取认种分类账 + */ + getPlantingLedger( + userId: bigint, + page: number, + pageSize: number, + startDate?: Date, + endDate?: Date, + ): Promise; + + /** + * 获取钱包汇总 + */ + getWalletSummary(userId: bigint): Promise; + + /** + * 获取钱包分类账 + */ + getWalletLedger( + userId: bigint, + page: number, + pageSize: number, + filters?: WalletLedgerFilters, + ): Promise; + + /** + * 获取授权角色列表 + */ + getAuthorizationRoles(accountSequence: string): Promise; + + /** + * 获取月度考核记录 + */ + getMonthlyAssessments(accountSequence: string): Promise; + + /** + * 获取系统账户流水(用户相关的授权角色账户流水) + */ + getSystemAccountLedger(accountSequence: string): Promise; +} diff --git a/backend/services/admin-service/src/infrastructure/persistence/repositories/user-detail-query.repository.impl.ts b/backend/services/admin-service/src/infrastructure/persistence/repositories/user-detail-query.repository.impl.ts new file mode 100644 index 00000000..153c2be6 --- /dev/null +++ b/backend/services/admin-service/src/infrastructure/persistence/repositories/user-detail-query.repository.impl.ts @@ -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 { + 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 { + 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 { + 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 { + const user = await this.prisma.userQueryView.findUnique({ + where: { userId: referrerId }, + select: { nickname: true }, + }); + return user?.nickname || null; + } + + // ============================================================================ + // 认种相关 + // ============================================================================ + + async getPlantingSummary(userId: bigint): Promise { + // 获取持仓信息 + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + // 先获取用户的授权角色 + 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)); + } +} diff --git a/frontend/admin-web/src/app/(dashboard)/users/[id]/IMPLEMENTATION_PLAN.md b/frontend/admin-web/src/app/(dashboard)/users/[id]/IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..5a911ca8 --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/users/[id]/IMPLEMENTATION_PLAN.md @@ -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. **响应式设计** + - 表格在小屏幕上可横向滚动 + - 推荐关系树在小屏幕上可缩放 diff --git a/frontend/admin-web/src/app/(dashboard)/users/[id]/page.tsx b/frontend/admin-web/src/app/(dashboard)/users/[id]/page.tsx new file mode 100644 index 00000000..573c5caf --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/users/[id]/page.tsx @@ -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 = { + 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 = { + USDT: 'USDT', + DST: 'DST', + BNB: 'BNB', + OG: 'OG', + RWAD: 'RWAD', + HASHPOWER: '算力', +}; + +const plantingStatusLabels: Record = { + CREATED: '已创建', + PAID: '已支付', + FUND_ALLOCATED: '资金已分配', + MINING_ENABLED: '已开始挖矿', + CANCELLED: '已取消', + EXPIRED: '已过期', +}; + +const roleTypeLabels: Record = { + COMMUNITY_PARTNER: '社区合伙人', + PROVINCE_COMPANY: '省公司', + CITY_COMPANY: '市公司', + AUTH_PROVINCE_COMPANY: '授权省公司', + AUTH_CITY_COMPANY: '授权市公司', +}; + +const authStatusLabels: Record = { + PENDING: '待授权', + AUTHORIZED: '已授权', + REVOKED: '已撤销', + EXPIRED: '已过期', +}; + +const assessmentResultLabels: Record = { + 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('referral'); + const [treeRootUser, setTreeRootUser] = useState(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 ( + +
加载中...
+
+ ); + } + + if (detailError || !userDetail) { + return ( + +
+

加载失败: {(detailError as Error)?.message || '用户不存在'}

+ +
+
+ ); + } + + return ( + +
+ {/* 返回按钮 */} +
+ +
+ + {/* 用户基本信息卡片 */} +
+
+
+
+
+
+

+ {userDetail.nickname || '未设置昵称'} + + {userDetail.status === 'active' ? '正常' : userDetail.status === 'frozen' ? '冻结' : '停用'} + +

+
+ 账户序号: {userDetail.accountSequence} + 手机号: {userDetail.phoneNumberMasked || '未绑定'} + KYC: {userDetail.kycStatus} +
+
+ 注册时间: {formatDate(userDetail.registeredAt)} + 最后活跃: {formatDate(userDetail.lastActiveAt)} +
+
+
+ + {/* 统计卡片 */} +
+
+ 个人认种量 + {formatNumber(userDetail.personalAdoptions)} +
+
+ 团队认种量 + {formatNumber(userDetail.teamAdoptions)} +
+
+ 团队地址数 + {formatNumber(userDetail.teamAddresses)} +
+
+ 龙虎榜排名 + + {userDetail.ranking ? formatRanking(userDetail.ranking) : '-'} + +
+
+ 直推人数 + + {formatNumber(userDetail.referralInfo.directReferralCount)} + +
+
+ 活跃直推 + + {formatNumber(userDetail.referralInfo.activeDirectCount)} + +
+
+ + {/* 推荐人信息 */} + {userDetail.referralInfo.referrerSequence && ( +
+ 推荐人: + + {userDetail.referralInfo.referrerSequence} + {userDetail.referralInfo.referrerNickname && ` (${userDetail.referralInfo.referrerNickname})`} + + + 邀请码: {userDetail.referralInfo.usedReferralCode || '-'} + + + 层级深度: {userDetail.referralInfo.depth} + +
+ )} +
+ + {/* Tab 切换 */} +
+ {tabs.map((tab) => ( + + ))} +
+ + {/* Tab 内容 */} +
+ {/* 推荐关系 Tab */} + {activeTab === 'referral' && ( +
+
+

推荐关系树

+ {treeRootUser !== accountSequence && ( + + )} +
+ + {treeLoading ? ( +
加载中...
+ ) : referralTree ? ( +
+ {/* 向上的推荐人链 */} + {referralTree.ancestors.length > 0 && ( +
+
推荐人链 (向上)
+
+ {referralTree.ancestors.map((ancestor, index) => ( +
+ + {index < referralTree.ancestors.length - 1 && ( +
+ )} +
+ ))} +
+
+
+ )} + + {/* 当前用户 */} +
+
+ + {referralTree.currentUser.accountSequence} + + + {referralTree.currentUser.nickname || '未设置'} + + + 认种: {formatNumber(referralTree.currentUser.personalAdoptions)} + + + 直推: {formatNumber(referralTree.currentUser.directReferralCount)} + +
+
+ + {/* 直推用户 */} + {referralTree.directReferrals.length > 0 && ( +
+
+
+ 直推用户 ({referralTree.directReferrals.length}) +
+
+ {referralTree.directReferrals.map((referral) => ( + + ))} +
+
+ )} + + {referralTree.directReferrals.length === 0 && referralTree.ancestors.length === 0 && ( +
暂无推荐关系
+ )} +
+ ) : ( +
暂无推荐关系数据
+ )} +
+ )} + + {/* 认种信息 Tab */} + {activeTab === 'planting' && ( +
+ {plantingLoading ? ( +
加载中...
+ ) : plantingData ? ( + <> + {/* 认种汇总 */} +
+

认种汇总

+
+
+ 总订单数 + + {formatNumber(plantingData.summary.totalOrders)} + +
+
+ 总认种量 + + {formatNumber(plantingData.summary.totalTreeCount)} + +
+
+ 总金额 (USDT) + + {formatAmount(plantingData.summary.totalAmount)} + +
+
+ 有效认种量 + + {formatNumber(plantingData.summary.effectiveTreeCount)} + +
+
+ 首次认种 + + {formatDate(plantingData.summary.firstPlantingAt)} + +
+
+ 最近认种 + + {formatDate(plantingData.summary.lastPlantingAt)} + +
+
+
+ + {/* 认种分类账 */} +
+

认种分类账明细

+
+
+
订单号
+
认种数量
+
金额
+
状态
+
省市
+
创建时间
+
支付时间
+
+ {plantingData.items.length === 0 ? ( +
暂无认种记录
+ ) : ( + plantingData.items.map((item) => ( +
+
{item.orderNo}
+
{formatNumber(item.treeCount)}
+
{formatAmount(item.totalAmount)}
+
+ + {plantingStatusLabels[item.status] || item.status} + +
+
+ {item.selectedProvince || '-'} / {item.selectedCity || '-'} +
+
{formatDate(item.createdAt)}
+
{formatDate(item.paidAt)}
+
+ )) + )} +
+ + {/* 分页 */} + {plantingData.totalPages > 1 && ( +
+ + 第 {plantingPage} / {plantingData.totalPages} 页 + +
+ )} +
+ + ) : ( +
暂无认种数据
+ )} +
+ )} + + {/* 钱包信息 Tab */} + {activeTab === 'wallet' && ( +
+ {walletLoading ? ( +
加载中...
+ ) : walletData ? ( + <> + {/* 钱包汇总 */} +
+

钱包汇总

+
+
+ USDT 可用 + + {formatAmount(walletData.summary.usdtAvailable)} + +
+
+ USDT 冻结 + + {formatAmount(walletData.summary.usdtFrozen)} + +
+
+ DST 可用 + + {formatAmount(walletData.summary.dstAvailable)} + +
+
+ 算力 + + {formatAmount(walletData.summary.hashpower)} + +
+
+ 待领取收益 + + {formatAmount(walletData.summary.pendingUsdt)} + +
+
+ 可结算收益 + + {formatAmount(walletData.summary.settleableUsdt)} + +
+
+ 已结算收益 + + {formatAmount(walletData.summary.settledTotalUsdt)} + +
+
+ 过期收益 + + {formatAmount(walletData.summary.expiredTotalUsdt)} + +
+
+
+ + {/* 钱包分类账 */} +
+

钱包分类账明细

+
+
+
流水ID
+
类型
+
资产
+
金额
+
余额快照
+
关联订单
+
时间
+
+ {walletData.items.length === 0 ? ( +
暂无钱包流水
+ ) : ( + walletData.items.map((item) => ( +
+
{item.entryId}
+
+ {entryTypeLabels[item.entryType] || item.entryType} +
+
+ {assetTypeLabels[item.assetType] || item.assetType} +
+
= 0 + ? styles['ledgerTable__cell--positive'] + : styles['ledgerTable__cell--negative'] + )}> + {parseFloat(item.amount) >= 0 ? '+' : ''}{formatAmount(item.amount)} +
+
+ {formatAmount(item.balanceAfter)} +
+
+ {item.refOrderId || item.refTxHash || '-'} +
+
{formatDate(item.createdAt)}
+
+ )) + )} +
+ + {/* 分页 */} + {walletData.totalPages > 1 && ( +
+ + 第 {walletPage} / {walletData.totalPages} 页 + +
+ )} +
+ + ) : ( +
暂无钱包数据
+ )} +
+ )} + + {/* 授权信息 Tab */} + {activeTab === 'authorization' && ( +
+ {authLoading ? ( +
加载中...
+ ) : authData ? ( + <> + {/* 授权角色列表 */} +
+

授权角色

+ {authData.roles.length === 0 ? ( +
暂无授权角色
+ ) : ( +
+ {authData.roles.map((role) => ( +
+
+ + {roleTypeLabels[role.roleType] || role.roleType} + + + {authStatusLabels[role.status] || role.status} + +
+
+

区域: {role.regionName} ({role.regionCode})

+

显示头衔: {role.displayTitle}

+

+ 权益状态: + {role.benefitActive ? ( + 已激活 + ) : ( + 未激活 + )} +

+

初始目标: {formatNumber(role.initialTargetTreeCount)} 棵

+

月度目标类型: {role.monthlyTargetType}

+

授权时间: {formatDate(role.authorizedAt)}

+
+
+ ))} +
+ )} +
+ + {/* 月度考核记录 */} +
+

月度考核记录

+ {authData.assessments.length === 0 ? ( +
暂无考核记录
+ ) : ( +
+
+
考核月份
+
角色
+
月度目标/完成
+
累计目标/完成
+
结果
+
区域排名
+
+ {authData.assessments.map((assessment) => ( +
+
{assessment.assessmentMonth}
+
+ {roleTypeLabels[assessment.roleType] || assessment.roleType} +
+
+ {formatNumber(assessment.monthlyCompleted)} / {formatNumber(assessment.monthlyTarget)} +
+
+ {formatNumber(assessment.cumulativeCompleted)} / {formatNumber(assessment.cumulativeTarget)} +
+
+ + {assessmentResultLabels[assessment.result] || assessment.result} + +
+
+ {assessment.rankingInRegion || '-'} + {assessment.isFirstPlace && ' 🥇'} +
+
+ ))} +
+ )} +
+ + {/* 系统账户流水(如果有) */} + {authData.systemAccountLedger.length > 0 && ( +
+

系统账户流水

+
+
+
流水ID
+
账户类型
+
流水类型
+
金额
+
余额
+
时间
+
+ {authData.systemAccountLedger.map((ledger) => ( +
+
{ledger.ledgerId}
+
{ledger.accountType}
+
{ledger.entryType}
+
= 0 + ? styles['ledgerTable__cell--positive'] + : styles['ledgerTable__cell--negative'] + )}> + {parseFloat(ledger.amount) >= 0 ? '+' : ''}{formatAmount(ledger.amount)} +
+
{formatAmount(ledger.balanceAfter)}
+
{formatDate(ledger.createdAt)}
+
+ ))} +
+
+ )} + + ) : ( +
暂无授权数据
+ )} +
+ )} +
+
+ + ); +} diff --git a/frontend/admin-web/src/app/(dashboard)/users/[id]/user-detail.module.scss b/frontend/admin-web/src/app/(dashboard)/users/[id]/user-detail.module.scss new file mode 100644 index 00000000..afac0e4e --- /dev/null +++ b/frontend/admin-web/src/app/(dashboard)/users/[id]/user-detail.module.scss @@ -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; + } +} diff --git a/frontend/admin-web/src/app/(dashboard)/users/page.tsx b/frontend/admin-web/src/app/(dashboard)/users/page.tsx index a66314a6..6dcf9c72 100644 --- a/frontend/admin-web/src/app/(dashboard)/users/page.tsx +++ b/frontend/admin-web/src/app/(dashboard)/users/page.tsx @@ -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([]); - const [detailUserId, setDetailUserId] = useState(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() { {/* 操作 */}
- +
)) @@ -518,66 +502,6 @@ export default function UsersPage() {
{renderPaginationButtons()}
- - {/* 用户详情弹窗 */} - - {detailLoading ? ( -
加载中...
- ) : userDetail ? ( -
-
-
-
-

{userDetail.nickname || '未设置昵称'}

-

账户序号: {userDetail.accountSequence}

-

手机号: {userDetail.phoneNumberMasked || '未绑定'}

-

KYC状态: {userDetail.kycStatus}

-
-
-
-
- 个人认种量 - - {formatNumber(userDetail.personalAdoptions)} - -
-
- 团队认种量 - - {formatNumber(userDetail.teamAdoptions)} - -
-
- 龙虎榜排名 - - {formatRanking(userDetail.ranking)} - -
-
-
-

注册时间: {new Date(userDetail.registeredAt).toLocaleString()}

- {userDetail.lastActiveAt && ( -

最后活跃: {new Date(userDetail.lastActiveAt).toLocaleString()}

- )} -
-
- ) : ( -
未找到用户信息
- )} -
); diff --git a/frontend/admin-web/src/hooks/index.ts b/frontend/admin-web/src/hooks/index.ts index 856bfd6b..d6135e64 100644 --- a/frontend/admin-web/src/hooks/index.ts +++ b/frontend/admin-web/src/hooks/index.ts @@ -2,5 +2,6 @@ export * from './useDashboard'; export * from './useUsers'; +export * from './useUserDetailPage'; export * from './useAuthorizations'; export * from './useSystemWithdrawal'; diff --git a/frontend/admin-web/src/hooks/useUserDetailPage.ts b/frontend/admin-web/src/hooks/useUserDetailPage.ts new file mode 100644 index 00000000..5408f5c1 --- /dev/null +++ b/frontend/admin-web/src/hooks/useUserDetailPage.ts @@ -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, + }); +} diff --git a/frontend/admin-web/src/infrastructure/api/endpoints.ts b/frontend/admin-web/src/infrastructure/api/endpoints.ts index 5500267b..fb7ee61d 100644 --- a/frontend/admin-web/src/infrastructure/api/endpoints.ts +++ b/frontend/admin-web/src/infrastructure/api/endpoints.ts @@ -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', diff --git a/frontend/admin-web/src/services/userDetailService.ts b/frontend/admin-web/src/services/userDetailService.ts new file mode 100644 index 00000000..13aa4f2d --- /dev/null +++ b/frontend/admin-web/src/services/userDetailService.ts @@ -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 { + 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 { + return apiClient.get(API_ENDPOINTS.USER_DETAIL.REFERRAL_TREE(accountSequence), { + params: { direction, depth }, + }); + }, + + /** + * 获取认种分类账 + */ + async getPlantingLedger( + accountSequence: string, + params: LedgerQueryParams = {} + ): Promise { + 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 { + 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 { + return apiClient.get(API_ENDPOINTS.USER_DETAIL.AUTHORIZATION_DETAIL(accountSequence)); + }, +}; + +export default userDetailService; diff --git a/frontend/admin-web/src/types/userDetail.types.ts b/frontend/admin-web/src/types/userDetail.types.ts new file mode 100644 index 00000000..ec866ece --- /dev/null +++ b/frontend/admin-web/src/types/userDetail.types.ts @@ -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 = { + // 充值 + 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 = { + USDT: 'USDT', + DST: 'DST', + BNB: 'BNB', + OG: 'OG', + RWAD: 'RWAD', + HASHPOWER: '算力', +}; + +export const PLANTING_STATUS_LABELS: Record = { + CREATED: '已创建', + PAID: '已支付', + FUND_ALLOCATED: '资金已分配', + MINING_ENABLED: '已开始挖矿', + CANCELLED: '已取消', + EXPIRED: '已过期', +}; + +export const AUTHORIZATION_ROLE_LABELS: Record = { + COMMUNITY_PARTNER: '社区合伙人', + PROVINCE_COMPANY: '省公司', + CITY_COMPANY: '市公司', + AUTH_PROVINCE_COMPANY: '授权省公司', + AUTH_CITY_COMPANY: '授权市公司', +}; + +export const AUTHORIZATION_STATUS_LABELS: Record = { + PENDING: '待授权', + AUTHORIZED: '已授权', + REVOKED: '已撤销', + EXPIRED: '已过期', +}; + +export const ASSESSMENT_RESULT_LABELS: Record = { + NOT_ASSESSED: '未考核', + PASSED: '通过', + FAILED: '未通过', + BYPASSED: '豁免', +};