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:
parent
53ef1ade42
commit
27a4bbfbef
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export * from './authorization.response'
|
||||
export * from './community-hierarchy.response'
|
||||
export * from './user-profile.response'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 '未认证';
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue