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,
|
ApplyAuthorizationResponse,
|
||||||
StickmanRankingResponse,
|
StickmanRankingResponse,
|
||||||
CommunityHierarchyResponse,
|
CommunityHierarchyResponse,
|
||||||
|
UserProfilePublicResponse,
|
||||||
|
UserProfilePrivateResponse,
|
||||||
} from '@/api/dto/response'
|
} from '@/api/dto/response'
|
||||||
import { CurrentUser } from '@/shared/decorators'
|
import { CurrentUser } from '@/shared/decorators'
|
||||||
import { JwtAuthGuard } from '@/shared/guards'
|
import { JwtAuthGuard } from '@/shared/guards'
|
||||||
|
|
@ -215,4 +217,35 @@ export class AuthorizationController {
|
||||||
)
|
)
|
||||||
return await this.applicationService.selfApplyAuthorization(command)
|
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 './authorization.response'
|
||||||
export * from './community-hierarchy.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
|
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;
|
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 HTTP 客户端
|
||||||
* 用于从 identity-service 获取用户信息
|
* 用于从 identity-service 获取用户信息
|
||||||
|
|
@ -99,4 +115,31 @@ export class IdentityServiceClient implements OnModuleInit {
|
||||||
|
|
||||||
return null;
|
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[];
|
teamMembers: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户资料统计接口
|
||||||
|
*/
|
||||||
|
export interface UserProfileStatsResponse {
|
||||||
|
accountSequence: string;
|
||||||
|
directReferralCount: number;
|
||||||
|
umbrellaUserCount: number;
|
||||||
|
personalPlantingCount: number;
|
||||||
|
teamPlantingCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 适配器类:将 referral-service 返回的数据转换为 authorization-service 需要的格式
|
* 适配器类:将 referral-service 返回的数据转换为 authorization-service 需要的格式
|
||||||
*/
|
*/
|
||||||
|
|
@ -245,4 +256,39 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul
|
||||||
return [];
|
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;
|
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认证
|
* 用于微服务间的内部通信,不需要JWT认证
|
||||||
|
|
@ -94,4 +110,37 @@ export class InternalController {
|
||||||
return null;
|
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 {
|
import {
|
||||||
REFERRAL_RELATIONSHIP_REPOSITORY,
|
REFERRAL_RELATIONSHIP_REPOSITORY,
|
||||||
IReferralRelationshipRepository,
|
IReferralRelationshipRepository,
|
||||||
|
TEAM_STATISTICS_REPOSITORY,
|
||||||
|
ITeamStatisticsRepository,
|
||||||
} from '../../domain';
|
} from '../../domain';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -17,6 +19,8 @@ export class InternalReferralChainController {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(REFERRAL_RELATIONSHIP_REPOSITORY)
|
@Inject(REFERRAL_RELATIONSHIP_REPOSITORY)
|
||||||
private readonly referralRepo: IReferralRelationshipRepository,
|
private readonly referralRepo: IReferralRelationshipRepository,
|
||||||
|
@Inject(TEAM_STATISTICS_REPOSITORY)
|
||||||
|
private readonly teamStatsRepo: ITeamStatisticsRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -182,4 +186,68 @@ export class InternalReferralChainController {
|
||||||
teamMembers,
|
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 {
|
class StickmanRankingResponse {
|
||||||
final String id;
|
final String id;
|
||||||
|
final String userId;
|
||||||
|
final String authorizationId;
|
||||||
final String nickname;
|
final String nickname;
|
||||||
final String? avatarUrl;
|
final String? avatarUrl;
|
||||||
final int completedCount;
|
final int completedCount;
|
||||||
final double monthlyEarnings;
|
final double monthlyEarnings;
|
||||||
final bool isCurrentUser;
|
final bool isCurrentUser;
|
||||||
|
final String? accountSequence;
|
||||||
|
|
||||||
StickmanRankingResponse({
|
StickmanRankingResponse({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.userId,
|
||||||
|
required this.authorizationId,
|
||||||
required this.nickname,
|
required this.nickname,
|
||||||
this.avatarUrl,
|
this.avatarUrl,
|
||||||
required this.completedCount,
|
required this.completedCount,
|
||||||
required this.monthlyEarnings,
|
required this.monthlyEarnings,
|
||||||
required this.isCurrentUser,
|
required this.isCurrentUser,
|
||||||
|
this.accountSequence,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory StickmanRankingResponse.fromJson(Map<String, dynamic> json) {
|
factory StickmanRankingResponse.fromJson(Map<String, dynamic> json) {
|
||||||
return StickmanRankingResponse(
|
return StickmanRankingResponse(
|
||||||
id: json['id']?.toString() ?? '',
|
id: json['id']?.toString() ?? '',
|
||||||
|
userId: json['userId']?.toString() ?? '',
|
||||||
|
authorizationId: json['authorizationId']?.toString() ?? '',
|
||||||
nickname: json['nickname'] ?? '',
|
nickname: json['nickname'] ?? '',
|
||||||
avatarUrl: json['avatarUrl'],
|
avatarUrl: json['avatarUrl'],
|
||||||
completedCount: json['completedCount'] ?? 0,
|
completedCount: json['completedCount'] ?? 0,
|
||||||
monthlyEarnings: (json['monthlyEarnings'] ?? 0).toDouble(),
|
monthlyEarnings: (json['monthlyEarnings'] ?? 0).toDouble(),
|
||||||
isCurrentUser: json['isCurrentUser'] ?? false,
|
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;
|
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:flutter/material.dart';
|
||||||
import 'package:lottie/lottie.dart';
|
import 'package:lottie/lottie.dart';
|
||||||
|
import 'user_profile_dialog.dart';
|
||||||
|
|
||||||
/// 火柴人排名数据模型
|
/// 火柴人排名数据模型
|
||||||
class StickmanRankingData {
|
class StickmanRankingData {
|
||||||
|
|
@ -10,6 +11,7 @@ class StickmanRankingData {
|
||||||
final int targetCount; // 目标数量 (省: 50000, 市: 10000)
|
final int targetCount; // 目标数量 (省: 50000, 市: 10000)
|
||||||
final double monthlyEarnings; // 本月可结算收益
|
final double monthlyEarnings; // 本月可结算收益
|
||||||
final bool isCurrentUser; // 是否是当前用户
|
final bool isCurrentUser; // 是否是当前用户
|
||||||
|
final String? accountSequence; // 账户序列号(用于查看详情)
|
||||||
|
|
||||||
StickmanRankingData({
|
StickmanRankingData({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
|
@ -19,6 +21,7 @@ class StickmanRankingData {
|
||||||
required this.targetCount,
|
required this.targetCount,
|
||||||
required this.monthlyEarnings,
|
required this.monthlyEarnings,
|
||||||
this.isCurrentUser = false,
|
this.isCurrentUser = false,
|
||||||
|
this.accountSequence,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 完成进度 (0.0 - 1.0)
|
/// 完成进度 (0.0 - 1.0)
|
||||||
|
|
@ -406,19 +409,21 @@ class _StickmanRaceWidgetState extends State<StickmanRaceWidget>
|
||||||
|
|
||||||
/// 构建排名项
|
/// 构建排名项
|
||||||
Widget _buildRankingItem(int rank, StickmanRankingData data) {
|
Widget _buildRankingItem(int rank, StickmanRankingData data) {
|
||||||
return Container(
|
return GestureDetector(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
onTap: () => _showUserProfile(data),
|
||||||
padding: const EdgeInsets.all(12),
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
color: data.isCurrentUser
|
padding: const EdgeInsets.all(12),
|
||||||
? const Color(0xFFD4AF37).withValues(alpha: 0.1)
|
decoration: BoxDecoration(
|
||||||
: Colors.white.withValues(alpha: 0.5),
|
color: data.isCurrentUser
|
||||||
borderRadius: BorderRadius.circular(8),
|
? const Color(0xFFD4AF37).withValues(alpha: 0.1)
|
||||||
border: data.isCurrentUser
|
: Colors.white.withValues(alpha: 0.5),
|
||||||
? Border.all(color: const Color(0xFFD4AF37), width: 1)
|
borderRadius: BorderRadius.circular(8),
|
||||||
: null,
|
border: data.isCurrentUser
|
||||||
),
|
? Border.all(color: const Color(0xFFD4AF37), width: 1)
|
||||||
child: Row(
|
: null,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// 排名
|
// 排名
|
||||||
Container(
|
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