feat(authorization): 实现火柴人排名用户详情查看功能

后端:
- identity-service: 新增内部API获取用户详情(手机号、邮箱、KYC等)
- referral-service: 新增内部API获取用户团队统计(直推人数、伞下用户数、认种数量)
- authorization-service:
  - 新增用户公开资料和私密资料API
  - 聚合identity-service和referral-service数据
  - 省团队以上权限可查看私密信息(脱敏处理)

前端:
- 新增UserProfileDialog弹窗组件,支持查看用户详情
- stickman_race_widget: 排名列表项可点击查看用户详情
- authorization_service: 新增getUserProfile/getUserPrivateProfile方法

用户详情包括:
- 基本信息: 用户ID、昵称、头像、注册时间、所在地区
- 团队数据: 推荐人、直推人数、伞下用户数、个人/团队认种数
- 授权信息: 授权类型、权益激活状态
- 联系信息(特权用户可见): 手机号、邮箱、真实姓名(脱敏)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-23 22:42:19 -08:00
parent 53ef1ade42
commit 27a4bbfbef
11 changed files with 1276 additions and 13 deletions

View File

@ -44,6 +44,8 @@ import {
ApplyAuthorizationResponse,
StickmanRankingResponse,
CommunityHierarchyResponse,
UserProfilePublicResponse,
UserProfilePrivateResponse,
} from '@/api/dto/response'
import { CurrentUser } from '@/shared/decorators'
import { JwtAuthGuard } from '@/shared/guards'
@ -215,4 +217,35 @@ export class AuthorizationController {
)
return await this.applicationService.selfApplyAuthorization(command)
}
// ============================================
// 用户资料相关接口
// ============================================
@Get('users/:accountSequence/profile')
@ApiOperation({
summary: '获取用户公开资料',
description: '获取指定用户的公开资料,包括昵称、头像、注册时间、推荐人、直推人数、伞下用户数、认种数量、授权列表等',
})
@ApiParam({ name: 'accountSequence', description: '目标用户的账户序列号' })
@ApiResponse({ status: 200, type: UserProfilePublicResponse })
async getUserProfile(
@Param('accountSequence') accountSequence: string,
): Promise<UserProfilePublicResponse> {
return await this.applicationService.getUserPublicProfile(accountSequence)
}
@Get('users/:accountSequence/profile/private')
@ApiOperation({
summary: '获取用户私密资料',
description: '获取指定用户的私密资料(需要省团队以上权限),包括脱敏的手机号、邮箱、真实姓名等',
})
@ApiParam({ name: 'accountSequence', description: '目标用户的账户序列号' })
@ApiResponse({ status: 200, type: UserProfilePrivateResponse })
async getUserPrivateProfile(
@CurrentUser() user: { userId: string; accountSequence: string },
@Param('accountSequence') accountSequence: string,
): Promise<UserProfilePrivateResponse> {
return await this.applicationService.getUserPrivateProfile(user.accountSequence, accountSequence)
}
}

View File

@ -1,2 +1,3 @@
export * from './authorization.response'
export * from './community-hierarchy.response'
export * from './user-profile.response'

View File

@ -0,0 +1,89 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
import { RoleType } from '@/domain/enums'
/**
*
*/
export class UserAuthorizationInfo {
@ApiProperty({ description: '授权ID' })
authorizationId: string
@ApiProperty({ description: '角色类型', enum: RoleType })
roleType: RoleType
@ApiProperty({ description: '区域代码' })
regionCode: string
@ApiProperty({ description: '区域名称' })
regionName: string
@ApiProperty({ description: '显示标题' })
displayTitle: string
@ApiProperty({ description: '权益是否激活' })
benefitActive: boolean
}
/**
*
*/
export class UserProfilePublicResponse {
@ApiProperty({ description: '用户ID' })
userId: string
@ApiProperty({ description: '账户序列号' })
accountSequence: string
@ApiProperty({ description: '昵称' })
nickname: string
@ApiPropertyOptional({ description: '头像URL' })
avatarUrl?: string
@ApiProperty({ description: '注册时间' })
registeredAt: string
@ApiPropertyOptional({ description: '推荐人序列号' })
inviterSequence?: string
@ApiPropertyOptional({ description: '推荐人昵称' })
inviterNickname?: string
@ApiProperty({ description: '直推人数' })
directReferralCount: number
@ApiProperty({ description: '伞下用户数(团队总人数)' })
umbrellaUserCount: number
@ApiPropertyOptional({ description: '所在省份' })
province?: string
@ApiPropertyOptional({ description: '所在城市' })
city?: string
@ApiProperty({ description: '个人认种数量' })
personalPlantingCount: number
@ApiProperty({ description: '团队认种数量' })
teamPlantingCount: number
@ApiProperty({ description: '授权列表', type: [UserAuthorizationInfo] })
authorizations: UserAuthorizationInfo[]
}
/**
*
*/
export class UserProfilePrivateResponse extends UserProfilePublicResponse {
@ApiPropertyOptional({ description: '手机号(脱敏)' })
phoneNumber?: string
@ApiPropertyOptional({ description: '邮箱(脱敏)' })
email?: string
@ApiPropertyOptional({ description: '真实姓名(脱敏)' })
realName?: string
@ApiProperty({ description: 'KYC状态' })
kycStatus: string
}

View File

@ -3201,4 +3201,230 @@ export class AuthorizationApplicationService {
}
return mapping[roleType] || roleType
}
// ============================================
// 用户资料相关方法
// ============================================
/**
*
*
*
* @param targetAccountSequence accountSequence
*/
async getUserPublicProfile(targetAccountSequence: string): Promise<{
userId: string
accountSequence: string
nickname: string
avatarUrl?: string
registeredAt: string
inviterSequence?: string
inviterNickname?: string
directReferralCount: number
umbrellaUserCount: number
province?: string
city?: string
personalPlantingCount: number
teamPlantingCount: number
authorizations: Array<{
authorizationId: string
roleType: RoleType
regionCode: string
regionName: string
displayTitle: string
benefitActive: boolean
}>
}> {
this.logger.debug(`[getUserPublicProfile] targetAccountSequence=${targetAccountSequence}`)
// 1. 获取基本用户信息
const userInfo = await this.identityServiceClient.getUserInfoBySequence(targetAccountSequence)
if (!userInfo) {
throw new NotFoundError('用户不存在')
}
// 2. 获取推荐关系统计
const profileStats = await this.referralServiceClient.getUserProfileStats(targetAccountSequence)
// 3. 获取推荐人信息
let inviterNickname: string | undefined
const referralChain = await this.referralServiceClient.getReferralChain(targetAccountSequence)
if (referralChain.length > 0) {
const inviterInfo = await this.identityServiceClient.getUserInfoBySequence(referralChain[0])
inviterNickname = inviterInfo?.nickname
}
// 4. 获取用户的授权列表
const authorizations = await this.authorizationRepository.findByAccountSequence(targetAccountSequence)
const activeAuthorizations = authorizations.filter(
(auth) => auth.status !== AuthorizationStatus.REVOKED,
)
// 5. 获取省市信息(从授权中提取,优先取省团队/市团队授权的区域)
let province: string | undefined
let city: string | undefined
for (const auth of activeAuthorizations) {
if (auth.roleType === RoleType.AUTH_PROVINCE_COMPANY || auth.roleType === RoleType.PROVINCE_COMPANY) {
province = auth.regionName
}
if (auth.roleType === RoleType.AUTH_CITY_COMPANY || auth.roleType === RoleType.CITY_COMPANY) {
city = auth.regionName
}
}
// 6. 从用户详情获取注册时间和推荐人序列号
const userDetail = await this.identityServiceClient.getUserDetailBySequence(targetAccountSequence)
return {
userId: userInfo.userId,
accountSequence: userInfo.accountSequence,
nickname: userInfo.nickname,
avatarUrl: userInfo.avatarUrl,
registeredAt: userDetail?.registeredAt || new Date().toISOString(),
inviterSequence: userDetail?.inviterSequence,
inviterNickname,
directReferralCount: profileStats.directReferralCount,
umbrellaUserCount: profileStats.umbrellaUserCount,
province,
city,
personalPlantingCount: profileStats.personalPlantingCount,
teamPlantingCount: profileStats.teamPlantingCount,
authorizations: activeAuthorizations.map((auth) => ({
authorizationId: auth.authorizationId.value,
roleType: auth.roleType,
regionCode: auth.regionCode.value,
regionName: auth.regionName,
displayTitle: auth.displayTitle,
benefitActive: auth.benefitActive,
})),
}
}
/**
*
*
*
* @param requestAccountSequence accountSequence
* @param targetAccountSequence accountSequence
*/
async getUserPrivateProfile(
requestAccountSequence: string,
targetAccountSequence: string,
): Promise<{
userId: string
accountSequence: string
nickname: string
avatarUrl?: string
phoneNumber?: string
email?: string
realName?: string
kycStatus: string
registeredAt: string
inviterSequence?: string
inviterNickname?: string
directReferralCount: number
umbrellaUserCount: number
province?: string
city?: string
personalPlantingCount: number
teamPlantingCount: number
authorizations: Array<{
authorizationId: string
roleType: RoleType
regionCode: string
regionName: string
displayTitle: string
benefitActive: boolean
}>
}> {
this.logger.debug(
`[getUserPrivateProfile] requestAccountSequence=${requestAccountSequence}, targetAccountSequence=${targetAccountSequence}`,
)
// 1. 验证请求者是否有权限查看私密资料
const hasPrivilege = await this.checkPrivateProfileAccess(requestAccountSequence, targetAccountSequence)
if (!hasPrivilege) {
throw new ApplicationError('您没有权限查看该用户的详细信息')
}
// 2. 获取公开资料
const publicProfile = await this.getUserPublicProfile(targetAccountSequence)
// 3. 获取敏感信息
const userDetail = await this.identityServiceClient.getUserDetailBySequence(targetAccountSequence)
// 4. 脱敏处理手机号和邮箱
const maskedPhoneNumber = userDetail?.phoneNumber
? this.maskPhoneNumber(userDetail.phoneNumber)
: undefined
const maskedEmail = userDetail?.email
? this.maskEmail(userDetail.email)
: undefined
const maskedRealName = userDetail?.realName
? this.maskRealName(userDetail.realName)
: undefined
return {
...publicProfile,
phoneNumber: maskedPhoneNumber,
email: maskedEmail,
realName: maskedRealName,
kycStatus: userDetail?.kycStatus || 'NOT_VERIFIED',
}
}
/**
*
*
* - (PROVINCE_COMPANY)
* - (AUTH_PROVINCE_COMPANY)
* - (CITY_COMPANY)
* -
*/
private async checkPrivateProfileAccess(
requestAccountSequence: string,
_targetAccountSequence: string,
): Promise<boolean> {
// 获取请求者的授权
const requestorAuthorizations = await this.authorizationRepository.findByAccountSequence(requestAccountSequence)
// 检查是否有高级权限
const privilegedRoleTypes = [
RoleType.PROVINCE_COMPANY,
RoleType.AUTH_PROVINCE_COMPANY,
RoleType.CITY_COMPANY,
]
return requestorAuthorizations.some(
(auth) =>
auth.status === AuthorizationStatus.AUTHORIZED &&
privilegedRoleTypes.includes(auth.roleType),
)
}
/**
* 138****1234
*/
private maskPhoneNumber(phone: string): string {
if (phone.length < 7) return phone
return phone.substring(0, 3) + '****' + phone.substring(phone.length - 4)
}
/**
* t***@example.com
*/
private maskEmail(email: string): string {
const atIndex = email.indexOf('@')
if (atIndex < 2) return email
return email.substring(0, 1) + '***' + email.substring(atIndex)
}
/**
* *
*/
private maskRealName(name: string): string {
if (name.length <= 1) return name
if (name.length === 2) return name[0] + '*'
return name[0] + '*'.repeat(name.length - 2) + name[name.length - 1]
}
}

View File

@ -12,6 +12,22 @@ export interface UserInfo {
avatarUrl?: string;
}
/**
*
*/
export interface UserDetailInfo {
userId: string;
accountSequence: string;
nickname: string;
avatarUrl?: string;
phoneNumber?: string; // 敏感信息
email?: string; // 敏感信息
registeredAt: string;
inviterSequence?: string; // 推荐人序列号
kycStatus: string;
realName?: string; // 敏感信息
}
/**
* Identity Service HTTP
* identity-service
@ -99,4 +115,31 @@ export class IdentityServiceClient implements OnModuleInit {
return null;
}
/**
* accountSequence
*/
async getUserDetailBySequence(accountSequence: string): Promise<UserDetailInfo | null> {
if (!this.enabled) {
return null;
}
try {
this.logger.debug(`[HTTP] GET /internal/users/${accountSequence}/detail`);
const response = await this.httpClient.get<{ success: boolean; data: UserDetailInfo } | UserDetailInfo>(
`/api/v1/internal/users/${accountSequence}/detail`,
);
// 处理可能的两种响应格式
const data = (response.data as any)?.data || response.data;
if (data) {
return data as UserDetailInfo;
}
} catch (error) {
this.logger.error(`[HTTP] Failed to get user detail for ${accountSequence}:`, error);
}
return null;
}
}

View File

@ -35,6 +35,17 @@ interface TeamMembersResponse {
teamMembers: string[];
}
/**
*
*/
export interface UserProfileStatsResponse {
accountSequence: string;
directReferralCount: number;
umbrellaUserCount: number;
personalPlantingCount: number;
teamPlantingCount: number;
}
/**
* referral-service authorization-service
*/
@ -245,4 +256,39 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul
return [];
}
}
/**
*
*/
async getUserProfileStats(accountSequence: string): Promise<UserProfileStatsResponse> {
const emptyStats: UserProfileStatsResponse = {
accountSequence,
directReferralCount: 0,
umbrellaUserCount: 0,
personalPlantingCount: 0,
teamPlantingCount: 0,
};
if (!this.enabled) {
this.logger.debug('[DISABLED] Referral service integration is disabled');
return emptyStats;
}
try {
this.logger.debug(`[HTTP] GET /internal/referral-chain/${accountSequence}/profile-stats`);
const response = await this.httpClient.get<UserProfileStatsResponse>(
`/api/v1/internal/referral-chain/${accountSequence}/profile-stats`,
);
if (!response.data) {
return emptyStats;
}
return response.data;
} catch (error) {
this.logger.error(`[HTTP] Failed to get profile stats for accountSequence ${accountSequence}:`, error);
return emptyStats;
}
}
}

View File

@ -25,6 +25,22 @@ interface UserBasicInfo {
avatarUrl?: string;
}
/**
*
*/
interface UserDetailInfo {
userId: string;
accountSequence: string;
nickname: string;
avatarUrl?: string;
phoneNumber?: string; // 敏感信息
email?: string; // 敏感信息
registeredAt: string;
inviterSequence?: string; // 推荐人序列号
kycStatus: string;
realName?: string; // 敏感信息
}
/**
*
* JWT认证
@ -94,4 +110,37 @@ export class InternalController {
return null;
}
}
@Public()
@Get('users/:accountSequence/detail')
@ApiOperation({ summary: '获取用户详情(内部调用,包含敏感信息)' })
@ApiResponse({ status: 200, description: '返回用户详情信息' })
async getUserDetailBySequence(@Param('accountSequence') accountSequence: string): Promise<UserDetailInfo | null> {
this.logger.debug(`[getUserDetailBySequence] 查询用户详情: ${accountSequence}`);
try {
const sequence = AccountSequence.create(accountSequence);
const user = await this.userRepository.findByAccountSequence(sequence);
if (!user) {
return null;
}
return {
userId: user.userId.value.toString(),
accountSequence: user.accountSequence.value,
nickname: user.nickname,
avatarUrl: user.avatarUrl || undefined,
phoneNumber: user.phoneNumber?.value || undefined,
email: user.email || undefined,
registeredAt: user.registeredAt.toISOString(),
inviterSequence: user.inviterSequence?.value || undefined,
kycStatus: user.kycStatus,
realName: user.realName || undefined,
};
} catch (error) {
this.logger.error(`[getUserDetailBySequence] 查询失败:`, error);
return null;
}
}
}

View File

@ -3,6 +3,8 @@ import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
import {
REFERRAL_RELATIONSHIP_REPOSITORY,
IReferralRelationshipRepository,
TEAM_STATISTICS_REPOSITORY,
ITeamStatisticsRepository,
} from '../../domain';
/**
@ -17,6 +19,8 @@ export class InternalReferralChainController {
constructor(
@Inject(REFERRAL_RELATIONSHIP_REPOSITORY)
private readonly referralRepo: IReferralRelationshipRepository,
@Inject(TEAM_STATISTICS_REPOSITORY)
private readonly teamStatsRepo: ITeamStatisticsRepository,
) {}
/**
@ -182,4 +186,68 @@ export class InternalReferralChainController {
teamMembers,
};
}
/**
* API
*
*/
@Get(':accountSequence/profile-stats')
@ApiOperation({ summary: '获取用户团队统计内部API' })
@ApiParam({ name: 'accountSequence', description: '账户序列号' })
@ApiResponse({
status: 200,
description: '用户团队统计',
schema: {
type: 'object',
properties: {
accountSequence: { type: 'string' },
directReferralCount: { type: 'number', description: '直推人数' },
umbrellaUserCount: { type: 'number', description: '伞下用户数(团队总人数)' },
personalPlantingCount: { type: 'number', description: '个人认种数量' },
teamPlantingCount: { type: 'number', description: '团队认种数量' },
},
},
})
async getProfileStats(@Param('accountSequence') accountSequence: string) {
this.logger.debug(`[INTERNAL] getProfileStats: accountSequence=${accountSequence}`);
const relationship = await this.referralRepo.findByAccountSequence(accountSequence);
if (!relationship) {
return {
accountSequence: accountSequence,
directReferralCount: 0,
umbrellaUserCount: 0,
personalPlantingCount: 0,
teamPlantingCount: 0,
};
}
// 获取直推人数
const directReferrals = await this.referralRepo.findDirectReferrals(relationship.userId);
const directReferralCount = directReferrals.length;
// 获取伞下用户数(递归获取所有下级)
let umbrellaUserCount = 0;
const queue = [...directReferrals];
while (queue.length > 0) {
umbrellaUserCount++;
const current = queue.shift()!;
const subReferrals = await this.referralRepo.findDirectReferrals(current.userId);
queue.push(...subReferrals);
}
// 获取团队统计
const teamStats = await this.teamStatsRepo.findByUserId(relationship.userId);
const personalPlantingCount = teamStats?.personalPlantingCount ?? 0;
const teamPlantingCount = teamStats?.teamPlantingCount ?? 0;
return {
accountSequence: accountSequence,
directReferralCount,
umbrellaUserCount,
personalPlantingCount,
teamPlantingCount,
};
}
}

View File

@ -460,33 +460,147 @@ class SelfApplyAuthorizationResponse {
///
class StickmanRankingResponse {
final String id;
final String userId;
final String authorizationId;
final String nickname;
final String? avatarUrl;
final int completedCount;
final double monthlyEarnings;
final bool isCurrentUser;
final String? accountSequence;
StickmanRankingResponse({
required this.id,
required this.userId,
required this.authorizationId,
required this.nickname,
this.avatarUrl,
required this.completedCount,
required this.monthlyEarnings,
required this.isCurrentUser,
this.accountSequence,
});
factory StickmanRankingResponse.fromJson(Map<String, dynamic> json) {
return StickmanRankingResponse(
id: json['id']?.toString() ?? '',
userId: json['userId']?.toString() ?? '',
authorizationId: json['authorizationId']?.toString() ?? '',
nickname: json['nickname'] ?? '',
avatarUrl: json['avatarUrl'],
completedCount: json['completedCount'] ?? 0,
monthlyEarnings: (json['monthlyEarnings'] ?? 0).toDouble(),
isCurrentUser: json['isCurrentUser'] ?? false,
accountSequence: json['accountSequence']?.toString(),
);
}
}
///
class UserAuthorizationInfo {
final String authorizationId;
final RoleType roleType;
final String regionCode;
final String regionName;
final String displayTitle;
final bool benefitActive;
UserAuthorizationInfo({
required this.authorizationId,
required this.roleType,
required this.regionCode,
required this.regionName,
required this.displayTitle,
required this.benefitActive,
});
factory UserAuthorizationInfo.fromJson(Map<String, dynamic> json) {
return UserAuthorizationInfo(
authorizationId: json['authorizationId'] ?? '',
roleType: RoleTypeExtension.fromString(json['roleType']) ?? RoleType.community,
regionCode: json['regionCode'] ?? '',
regionName: json['regionName'] ?? '',
displayTitle: json['displayTitle'] ?? '',
benefitActive: json['benefitActive'] ?? false,
);
}
}
///
class UserProfileResponse {
final String userId;
final String accountSequence;
final String nickname;
final String? avatarUrl;
final String registeredAt;
final String? inviterSequence;
final String? inviterNickname;
final int directReferralCount;
final int umbrellaUserCount;
final String? province;
final String? city;
final int personalPlantingCount;
final int teamPlantingCount;
final List<UserAuthorizationInfo> authorizations;
//
final String? phoneNumber;
final String? email;
final String? realName;
final String? kycStatus;
UserProfileResponse({
required this.userId,
required this.accountSequence,
required this.nickname,
this.avatarUrl,
required this.registeredAt,
this.inviterSequence,
this.inviterNickname,
required this.directReferralCount,
required this.umbrellaUserCount,
this.province,
this.city,
required this.personalPlantingCount,
required this.teamPlantingCount,
required this.authorizations,
this.phoneNumber,
this.email,
this.realName,
this.kycStatus,
});
factory UserProfileResponse.fromJson(Map<String, dynamic> json) {
return UserProfileResponse(
userId: json['userId']?.toString() ?? '',
accountSequence: json['accountSequence']?.toString() ?? '',
nickname: json['nickname'] ?? '',
avatarUrl: json['avatarUrl'],
registeredAt: json['registeredAt'] ?? '',
inviterSequence: json['inviterSequence'],
inviterNickname: json['inviterNickname'],
directReferralCount: json['directReferralCount'] ?? 0,
umbrellaUserCount: json['umbrellaUserCount'] ?? 0,
province: json['province'],
city: json['city'],
personalPlantingCount: json['personalPlantingCount'] ?? 0,
teamPlantingCount: json['teamPlantingCount'] ?? 0,
authorizations: (json['authorizations'] as List<dynamic>?)
?.map((e) => UserAuthorizationInfo.fromJson(e as Map<String, dynamic>))
.toList() ?? [],
phoneNumber: json['phoneNumber'],
email: json['email'],
realName: json['realName'],
kycStatus: json['kycStatus'],
);
}
///
DateTime get registeredAtDateTime => DateTime.tryParse(registeredAt) ?? DateTime.now();
///
bool get hasPrivateInfo => phoneNumber != null || email != null || realName != null;
}
///
///
/// :
@ -730,4 +844,78 @@ class AuthorizationService {
rethrow;
}
}
///
///
/// [accountSequence]
///
Future<UserProfileResponse> getUserProfile(String accountSequence) async {
try {
debugPrint('获取用户资料: accountSequence=$accountSequence');
final response = await _apiClient.get(
'/authorizations/users/$accountSequence/profile',
);
if (response.statusCode == 200) {
final responseData = response.data;
Map<String, dynamic>? data;
if (responseData is Map<String, dynamic>) {
if (responseData.containsKey('data')) {
data = responseData['data'] as Map<String, dynamic>?;
} else {
data = responseData;
}
}
if (data != null) {
final profile = UserProfileResponse.fromJson(data);
debugPrint('用户资料获取成功: ${profile.nickname}');
return profile;
}
throw Exception('用户资料数据格式错误');
}
throw Exception('获取用户资料失败');
} catch (e) {
debugPrint('获取用户资料失败: $e');
rethrow;
}
}
///
///
/// [accountSequence]
///
Future<UserProfileResponse> getUserPrivateProfile(String accountSequence) async {
try {
debugPrint('获取用户私密资料: accountSequence=$accountSequence');
final response = await _apiClient.get(
'/authorizations/users/$accountSequence/profile/private',
);
if (response.statusCode == 200) {
final responseData = response.data;
Map<String, dynamic>? data;
if (responseData is Map<String, dynamic>) {
if (responseData.containsKey('data')) {
data = responseData['data'] as Map<String, dynamic>?;
} else {
data = responseData;
}
}
if (data != null) {
final profile = UserProfileResponse.fromJson(data);
debugPrint('用户私密资料获取成功: ${profile.nickname}');
return profile;
}
throw Exception('用户私密资料数据格式错误');
}
throw Exception('获取用户私密资料失败');
} catch (e) {
debugPrint('获取用户私密资料失败: $e');
rethrow;
}
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
import 'user_profile_dialog.dart';
///
class StickmanRankingData {
@ -10,6 +11,7 @@ class StickmanRankingData {
final int targetCount; // (: 50000, : 10000)
final double monthlyEarnings; //
final bool isCurrentUser; //
final String? accountSequence; //
StickmanRankingData({
required this.id,
@ -19,6 +21,7 @@ class StickmanRankingData {
required this.targetCount,
required this.monthlyEarnings,
this.isCurrentUser = false,
this.accountSequence,
});
/// (0.0 - 1.0)
@ -406,19 +409,21 @@ class _StickmanRaceWidgetState extends State<StickmanRaceWidget>
///
Widget _buildRankingItem(int rank, StickmanRankingData data) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: data.isCurrentUser
? const Color(0xFFD4AF37).withValues(alpha: 0.1)
: Colors.white.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
border: data.isCurrentUser
? Border.all(color: const Color(0xFFD4AF37), width: 1)
: null,
),
child: Row(
return GestureDetector(
onTap: () => _showUserProfile(data),
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: data.isCurrentUser
? const Color(0xFFD4AF37).withValues(alpha: 0.1)
: Colors.white.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
border: data.isCurrentUser
? Border.all(color: const Color(0xFFD4AF37), width: 1)
: null,
),
child: Row(
children: [
//
Container(
@ -536,8 +541,35 @@ class _StickmanRaceWidgetState extends State<StickmanRaceWidget>
],
),
),
//
const SizedBox(width: 8),
const Icon(
Icons.chevron_right,
color: Color(0xFF8B5A2B),
size: 20,
),
],
),
),
);
}
///
void _showUserProfile(StickmanRankingData data) {
if (data.accountSequence == null || data.accountSequence!.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('暂无该用户的详细信息'),
duration: Duration(seconds: 2),
),
);
return;
}
UserProfileDialog.show(
context,
accountSequence: data.accountSequence!,
nickname: data.nickname,
);
}

View File

@ -0,0 +1,488 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import '../../../../core/services/authorization_service.dart';
import '../../../../core/di/injection_container.dart';
///
class UserProfileDialog extends ConsumerStatefulWidget {
///
final String accountSequence;
///
final String? nickname;
const UserProfileDialog({
super.key,
required this.accountSequence,
this.nickname,
});
///
static Future<void> show(
BuildContext context, {
required String accountSequence,
String? nickname,
}) {
return showDialog(
context: context,
builder: (context) => UserProfileDialog(
accountSequence: accountSequence,
nickname: nickname,
),
);
}
@override
ConsumerState<UserProfileDialog> createState() => _UserProfileDialogState();
}
class _UserProfileDialogState extends ConsumerState<UserProfileDialog> {
UserProfileResponse? _profile;
bool _isLoading = true;
String? _error;
bool _hasPrivilege = false;
@override
void initState() {
super.initState();
_loadProfile();
}
Future<void> _loadProfile() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final authService = ref.read(authorizationServiceProvider);
//
try {
final privateProfile = await authService.getUserPrivateProfile(widget.accountSequence);
if (mounted) {
setState(() {
_profile = privateProfile;
_hasPrivilege = true;
_isLoading = false;
});
}
return;
} catch (e) {
//
debugPrint('无权限获取私密资料,尝试获取公开资料');
}
//
final profile = await authService.getUserProfile(widget.accountSequence);
if (mounted) {
setState(() {
_profile = profile;
_hasPrivilege = false;
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_error = e.toString().replaceAll('Exception: ', '');
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40),
child: Container(
constraints: const BoxConstraints(maxWidth: 400, maxHeight: 600),
decoration: BoxDecoration(
color: const Color(0xFFF5F0E6),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildHeader(),
Flexible(
child: _isLoading
? _buildLoading()
: _error != null
? _buildError()
: _buildContent(),
),
],
),
),
);
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFFD4AF37), Color(0xFFB8860B)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Row(
children: [
const Icon(Icons.person, color: Colors.white, size: 24),
const SizedBox(width: 12),
Expanded(
child: Text(
widget.nickname ?? '用户详情',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
overflow: TextOverflow.ellipsis,
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close, color: Colors.white),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
);
}
Widget _buildLoading() {
return const Center(
child: Padding(
padding: EdgeInsets.all(40),
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
),
),
);
}
Widget _buildError() {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 48,
),
const SizedBox(height: 16),
Text(
_error ?? '加载失败',
style: const TextStyle(
fontSize: 14,
color: Color(0xFF5D4037),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
TextButton(
onPressed: _loadProfile,
child: const Text('重试'),
),
],
),
),
);
}
Widget _buildContent() {
final profile = _profile!;
return SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
_buildUserHeader(profile),
const SizedBox(height: 20),
//
_buildInfoCard(
title: '基本信息',
children: [
_buildInfoRow('用户ID', profile.accountSequence),
_buildInfoRow('注册时间', _formatDate(profile.registeredAtDateTime)),
if (profile.inviterNickname != null)
_buildInfoRow('推荐人', profile.inviterNickname!),
if (profile.province != null || profile.city != null)
_buildInfoRow('所在地区', [profile.province, profile.city].whereType<String>().join(' ')),
],
),
const SizedBox(height: 16),
//
_buildInfoCard(
title: '团队数据',
children: [
_buildInfoRow('直推人数', '${profile.directReferralCount}'),
_buildInfoRow('伞下用户', '${profile.umbrellaUserCount}'),
_buildInfoRow('个人认种', '${profile.personalPlantingCount}'),
_buildInfoRow('团队认种', '${profile.teamPlantingCount}'),
],
),
const SizedBox(height: 16),
//
if (profile.authorizations.isNotEmpty) ...[
_buildInfoCard(
title: '授权信息',
children: profile.authorizations.map((auth) {
return _buildAuthorizationRow(auth);
}).toList(),
),
const SizedBox(height: 16),
],
//
if (_hasPrivilege && profile.hasPrivateInfo)
_buildInfoCard(
title: '联系信息',
icon: Icons.lock,
children: [
if (profile.phoneNumber != null)
_buildInfoRow('手机号', profile.phoneNumber!),
if (profile.email != null)
_buildInfoRow('邮箱', profile.email!),
if (profile.realName != null)
_buildInfoRow('真实姓名', profile.realName!),
if (profile.kycStatus != null)
_buildInfoRow('KYC状态', _formatKycStatus(profile.kycStatus!)),
],
),
],
),
);
}
Widget _buildUserHeader(UserProfileResponse profile) {
return Row(
children: [
//
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFFD4AF37),
width: 2,
),
),
child: ClipOval(
child: profile.avatarUrl != null
? Image.network(
profile.avatarUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => _buildDefaultAvatar(),
)
: _buildDefaultAvatar(),
),
),
const SizedBox(width: 16),
//
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
profile.nickname,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF5D4037),
),
),
const SizedBox(height: 4),
Wrap(
spacing: 6,
runSpacing: 4,
children: profile.authorizations.map((auth) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: auth.benefitActive
? const Color(0xFFD4AF37).withValues(alpha: 0.2)
: Colors.grey.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(10),
),
child: Text(
auth.displayTitle,
style: TextStyle(
fontSize: 11,
color: auth.benefitActive
? const Color(0xFFD4AF37)
: Colors.grey,
fontWeight: FontWeight.w500,
),
),
);
}).toList(),
),
],
),
),
],
);
}
Widget _buildDefaultAvatar() {
return Container(
color: const Color(0xFFD4AF37).withValues(alpha: 0.3),
child: const Icon(
Icons.person,
size: 32,
color: Color(0xFFD4AF37),
),
);
}
Widget _buildInfoCard({
required String title,
required List<Widget> children,
IconData? icon,
}) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFD4AF37).withValues(alpha: 0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (icon != null) ...[
Icon(icon, size: 16, color: const Color(0xFFD4AF37)),
const SizedBox(width: 6),
],
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF5D4037),
),
),
],
),
const SizedBox(height: 12),
...children,
],
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80,
child: Text(
label,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF8B7355),
),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF5D4037),
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
Widget _buildAuthorizationRow(UserAuthorizationInfo auth) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: auth.benefitActive ? Colors.green : Colors.grey,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
auth.displayTitle,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF5D4037),
),
),
),
Text(
auth.benefitActive ? '权益激活' : '权益未激活',
style: TextStyle(
fontSize: 12,
color: auth.benefitActive ? Colors.green : Colors.grey,
),
),
],
),
);
}
String _formatDate(DateTime date) {
return DateFormat('yyyy-MM-dd').format(date);
}
String _formatKycStatus(String status) {
switch (status.toUpperCase()) {
case 'VERIFIED':
return '已认证';
case 'PENDING':
return '审核中';
case 'REJECTED':
return '已拒绝';
default:
return '未认证';
}
}
}