feat(admin-web): 用户详情页引荐关系树优化

- 引荐关系节点显示团队认种量:本人认种 / 团队认种
- 无上级引荐人时显示"总部"节点
- 角色标签优化:社区权益→部门权益,区域→部门名称/城市名称
- 后端 ReferralNode 添加 teamAdoptionCount 字段

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-08 05:12:28 -08:00
parent f55ac7e9cb
commit e2b2c17d38
7 changed files with 104 additions and 41 deletions

View File

@ -133,18 +133,21 @@ export class UserDetailController {
} }
// 获取引荐信息和实时统计 // 获取引荐信息和实时统计
const [referralInfo, personalAdoptionCount, directReferralCount] = await Promise.all([ const [referralInfo, personalAdoptionCount, directReferralCount, teamStats] = await Promise.all([
this.userDetailRepository.getReferralInfo(accountSequence), this.userDetailRepository.getReferralInfo(accountSequence),
this.userDetailRepository.getPersonalAdoptionCount(accountSequence), this.userDetailRepository.getPersonalAdoptionCount(accountSequence),
this.userDetailRepository.getDirectReferralCount(accountSequence), this.userDetailRepository.getDirectReferralCount(accountSequence),
this.userDetailRepository.getBatchUserStats([accountSequence]),
]); ]);
const currentUserStats = teamStats.get(accountSequence);
const currentUser: ReferralNodeDto = { const currentUser: ReferralNodeDto = {
accountSequence: user.accountSequence, accountSequence: user.accountSequence,
userId: user.userId.toString(), userId: user.userId.toString(),
nickname: user.nickname, nickname: user.nickname,
avatar: user.avatarUrl, avatar: user.avatarUrl,
personalAdoptions: personalAdoptionCount, personalAdoptions: personalAdoptionCount,
teamAdoptions: currentUserStats?.teamAdoptionCount || 0,
depth: referralInfo?.depth || 0, depth: referralInfo?.depth || 0,
directReferralCount: directReferralCount, directReferralCount: directReferralCount,
isCurrentUser: true, isCurrentUser: true,
@ -165,6 +168,7 @@ export class UserDetailController {
nickname: node.nickname, nickname: node.nickname,
avatar: node.avatarUrl, avatar: node.avatarUrl,
personalAdoptions: node.personalAdoptionCount, personalAdoptions: node.personalAdoptionCount,
teamAdoptions: node.teamAdoptionCount,
depth: node.depth, depth: node.depth,
directReferralCount: node.directReferralCount, directReferralCount: node.directReferralCount,
})); }));
@ -179,6 +183,7 @@ export class UserDetailController {
nickname: node.nickname, nickname: node.nickname,
avatar: node.avatarUrl, avatar: node.avatarUrl,
personalAdoptions: node.personalAdoptionCount, personalAdoptions: node.personalAdoptionCount,
teamAdoptions: node.teamAdoptionCount,
depth: node.depth, depth: node.depth,
directReferralCount: node.directReferralCount, directReferralCount: node.directReferralCount,
})); }));

View File

@ -69,6 +69,7 @@ export class ReferralNodeDto {
nickname!: string | null; nickname!: string | null;
avatar!: string | null; avatar!: string | null;
personalAdoptions!: number; personalAdoptions!: number;
teamAdoptions!: number; // 团队认种量
depth!: number; depth!: number;
directReferralCount!: number; directReferralCount!: number;
isCurrentUser?: boolean; isCurrentUser?: boolean;

View File

@ -25,6 +25,7 @@ export interface ReferralNode {
nickname: string | null; nickname: string | null;
avatarUrl: string | null; avatarUrl: string | null;
personalAdoptionCount: number; personalAdoptionCount: number;
teamAdoptionCount: number; // 团队认种量(包括本人和所有下级)
depth: number; depth: number;
directReferralCount: number; directReferralCount: number;
} }

View File

@ -82,14 +82,14 @@ export class UserDetailQueryRepositoryImpl implements IUserDetailQueryRepository
}), }),
]); ]);
// 实时统计:获取每个祖先的认种数量和直推数量 // 实时统计:获取每个祖先的认种数量、团队认种量和直推数量
const accountSequences = users.map(u => u.accountSequence); const userAccountSequences = users.map(u => u.accountSequence);
const [adoptionCounts, directReferralCounts] = await Promise.all([ const [adoptionCounts, directReferralCounts, teamStats] = await Promise.all([
// 统计每个用户的认种订单数量(状态为 MINING_ENABLED // 统计每个用户的认种订单数量(状态为 MINING_ENABLED
this.prisma.plantingOrderQueryView.groupBy({ this.prisma.plantingOrderQueryView.groupBy({
by: ['accountSequence'], by: ['accountSequence'],
where: { where: {
accountSequence: { in: accountSequences }, accountSequence: { in: userAccountSequences },
status: 'MINING_ENABLED', status: 'MINING_ENABLED',
}, },
_count: { id: true }, _count: { id: true },
@ -100,6 +100,8 @@ export class UserDetailQueryRepositoryImpl implements IUserDetailQueryRepository
where: { referrerId: { in: ancestorIds } }, where: { referrerId: { in: ancestorIds } },
_count: { userId: true }, _count: { userId: true },
}), }),
// 获取团队认种量
this.getBatchUserStats(userAccountSequences),
]); ]);
const adoptionCountMap = new Map(adoptionCounts.map(a => [a.accountSequence, a._count.id])); const adoptionCountMap = new Map(adoptionCounts.map(a => [a.accountSequence, a._count.id]));
@ -116,12 +118,14 @@ export class UserDetailQueryRepositoryImpl implements IUserDetailQueryRepository
return ancestorIds.map((id, index) => { return ancestorIds.map((id, index) => {
const user = userMap.get(id.toString()); const user = userMap.get(id.toString());
const ref = referralMap.get(id.toString()); const ref = referralMap.get(id.toString());
const stats = teamStats.get(user?.accountSequence || '');
return { return {
userId: id, userId: id,
accountSequence: user?.accountSequence || '', accountSequence: user?.accountSequence || '',
nickname: user?.nickname || null, nickname: user?.nickname || null,
avatarUrl: user?.avatarUrl || null, avatarUrl: user?.avatarUrl || null,
personalAdoptionCount: adoptionCountMap.get(user?.accountSequence || '') || 0, personalAdoptionCount: adoptionCountMap.get(user?.accountSequence || '') || 0,
teamAdoptionCount: stats?.teamAdoptionCount || 0,
depth: ref?.depth || index, depth: ref?.depth || index,
directReferralCount: directCountMap.get(id.toString()) || 0, directReferralCount: directCountMap.get(id.toString()) || 0,
}; };
@ -159,9 +163,9 @@ export class UserDetailQueryRepositoryImpl implements IUserDetailQueryRepository
}, },
}); });
// 实时统计:获取每个用户的认种数量和直推数量 // 实时统计:获取每个用户的认种数量、团队认种量和直推数量
const userAccountSequences = directReferrals.map(r => r.accountSequence); const userAccountSequences = directReferrals.map(r => r.accountSequence);
const [adoptionCounts, directReferralCounts] = await Promise.all([ const [adoptionCounts, directReferralCounts, teamStats] = await Promise.all([
// 统计每个用户的认种订单数量(状态为 MINING_ENABLED // 统计每个用户的认种订单数量(状态为 MINING_ENABLED
this.prisma.plantingOrderQueryView.groupBy({ this.prisma.plantingOrderQueryView.groupBy({
by: ['accountSequence'], by: ['accountSequence'],
@ -177,6 +181,8 @@ export class UserDetailQueryRepositoryImpl implements IUserDetailQueryRepository
where: { referrerId: { in: userIds } }, where: { referrerId: { in: userIds } },
_count: { userId: true }, _count: { userId: true },
}), }),
// 获取团队认种量
this.getBatchUserStats(userAccountSequences),
]); ]);
const adoptionCountMap = new Map(adoptionCounts.map(a => [a.accountSequence, a._count.id])); const adoptionCountMap = new Map(adoptionCounts.map(a => [a.accountSequence, a._count.id]));
@ -189,12 +195,14 @@ export class UserDetailQueryRepositoryImpl implements IUserDetailQueryRepository
return directReferrals.map((ref) => { return directReferrals.map((ref) => {
const user = userMap.get(ref.userId.toString()); const user = userMap.get(ref.userId.toString());
const stats = teamStats.get(ref.accountSequence);
return { return {
userId: ref.userId, userId: ref.userId,
accountSequence: ref.accountSequence, accountSequence: ref.accountSequence,
nickname: user?.nickname || null, nickname: user?.nickname || null,
avatarUrl: user?.avatarUrl || null, avatarUrl: user?.avatarUrl || null,
personalAdoptionCount: adoptionCountMap.get(ref.accountSequence) || 0, personalAdoptionCount: adoptionCountMap.get(ref.accountSequence) || 0,
teamAdoptionCount: stats?.teamAdoptionCount || 0,
depth: ref.depth, depth: ref.depth,
directReferralCount: directCountMap.get(ref.userId.toString()) || 0, directReferralCount: directCountMap.get(ref.userId.toString()) || 0,
}; };

View File

@ -82,14 +82,31 @@ const plantingStatusLabels: Record<string, string> = {
}; };
const roleTypeLabels: Record<string, string> = { const roleTypeLabels: Record<string, string> = {
COMMUNITY: '社区权益', COMMUNITY: '部门权益',
COMMUNITY_PARTNER: '社区权益', COMMUNITY_PARTNER: '部门权益',
AUTH_PROVINCE_COMPANY: '省团队', // 授权省公司 = 省团队权益 AUTH_PROVINCE_COMPANY: '省团队', // 授权省公司 = 省团队权益
PROVINCE_COMPANY: '省区域', // 正式省公司 = 省区域权益 PROVINCE_COMPANY: '省区域', // 正式省公司 = 省区域权益
AUTH_CITY_COMPANY: '市团队', // 授权市公司 = 市团队权益(40U) AUTH_CITY_COMPANY: '市团队', // 授权市公司 = 市团队权益(40U)
CITY_COMPANY: '市区域', // 正式市公司 = 市区域权益(35U+2%算力) CITY_COMPANY: '市区域', // 正式市公司 = 市区域权益(35U+2%算力)
}; };
// 判断是否为社区/部门类型角色
const isCommunityRole = (roleType: string): boolean => {
return roleType === 'COMMUNITY' || roleType === 'COMMUNITY_PARTNER';
};
// 判断是否为市级角色
const isCityRole = (roleType: string): boolean => {
return roleType === 'AUTH_CITY_COMPANY' || roleType === 'CITY_COMPANY';
};
// 获取区域标签名称
const getRegionLabel = (roleType: string): string => {
if (isCommunityRole(roleType)) return '部门名称:';
if (isCityRole(roleType)) return '城市名称:';
return '区域:';
};
const authStatusLabels: Record<string, string> = { const authStatusLabels: Record<string, string> = {
PENDING: '待授权', PENDING: '待授权',
AUTHORIZED: '已授权', AUTHORIZED: '已授权',
@ -394,33 +411,43 @@ export default function UserDetailPage() {
) : referralTree ? ( ) : referralTree ? (
<div className={styles.referralTree}> <div className={styles.referralTree}>
{/* 向上的引荐人链 */} {/* 向上的引荐人链 */}
{referralTree.ancestors.length > 0 && ( <div className={styles.referralTree__ancestors}>
<div className={styles.referralTree__ancestors}> <div className={styles.referralTree__label}> ()</div>
<div className={styles.referralTree__label}> ()</div> {referralTree.ancestors.length > 0 ? (
<div className={styles.referralTree__nodeList}> <>
{referralTree.ancestors.map((ancestor, index) => ( <div className={styles.referralTree__nodeList}>
<div key={ancestor.accountSequence} className={styles.referralTree__nodeWrapper}> {referralTree.ancestors.map((ancestor, index) => (
<button <div key={ancestor.accountSequence} className={styles.referralTree__nodeWrapper}>
className={styles.referralTree__node} <button
onClick={() => handleTreeNodeClick(ancestor)} className={styles.referralTree__node}
> onClick={() => handleTreeNodeClick(ancestor)}
<span className={styles.referralTree__nodeSeq}>{ancestor.accountSequence}</span> >
<span className={styles.referralTree__nodeNickname}> <span className={styles.referralTree__nodeSeq}>{ancestor.accountSequence}</span>
{ancestor.nickname || '未设置'} <span className={styles.referralTree__nodeNickname}>
</span> {ancestor.nickname || '未设置'}
<span className={styles.referralTree__nodeAdoptions}> </span>
: {formatNumber(ancestor.personalAdoptions)} <span className={styles.referralTree__nodeAdoptions}>
</span> : {formatNumber(ancestor.personalAdoptions)} / : {formatNumber(ancestor.teamAdoptions)}
</button> </span>
{index < referralTree.ancestors.length - 1 && ( </button>
<div className={styles.referralTree__connector}></div> {index < referralTree.ancestors.length - 1 && (
)} <div className={styles.referralTree__connector}></div>
</div> )}
))} </div>
</div> ))}
<div className={styles.referralTree__connector}></div> </div>
</div> <div className={styles.referralTree__connector}></div>
)} </>
) : (
<>
<div className={styles.referralTree__headquarters}>
<span className={styles.referralTree__headquartersLabel}></span>
<span className={styles.referralTree__headquartersDesc}></span>
</div>
<div className={styles.referralTree__connector}></div>
</>
)}
</div>
{/* 当前用户及其递归下级 */} {/* 当前用户及其递归下级 */}
<div className={styles.referralTree__currentWrapper}> <div className={styles.referralTree__currentWrapper}>
@ -434,9 +461,6 @@ export default function UserDetailPage() {
/> />
</div> </div>
{referralTree.directReferrals.length === 0 && referralTree.ancestors.length === 0 && (
<div className={styles.referralTree__empty}></div>
)}
</div> </div>
) : ( ) : (
<div className={styles.referralTab__empty}></div> <div className={styles.referralTab__empty}></div>
@ -699,7 +723,7 @@ export default function UserDetailPage() {
</span> </span>
</div> </div>
<div className={styles.authTab__roleInfo}> <div className={styles.authTab__roleInfo}>
<p><strong>:</strong> {role.regionName} ({role.regionCode})</p> <p><strong>{getRegionLabel(role.roleType)}</strong> {role.regionName} ({role.regionCode})</p>
<p><strong>:</strong> {role.displayTitle}</p> <p><strong>:</strong> {role.displayTitle}</p>
<p> <p>
<strong>:</strong> <strong>:</strong>
@ -945,7 +969,7 @@ function ReferralNodeItem({
{node.nickname || '未设置'} {node.nickname || '未设置'}
</span> </span>
<span className={styles.referralTree__nodeAdoptions}> <span className={styles.referralTree__nodeAdoptions}>
: {formatNumber(node.personalAdoptions)} : {formatNumber(node.personalAdoptions)} / : {formatNumber(node.teamAdoptions)}
</span> </span>
{node.directReferralCount > 0 && ( {node.directReferralCount > 0 && (
<span className={styles.referralTree__nodeCount}> <span className={styles.referralTree__nodeCount}>

View File

@ -480,6 +480,29 @@
color: $text-secondary; color: $text-secondary;
} }
// 总部节点样式
.referralTree__headquarters {
@include flex-column;
align-items: center;
padding: $spacing-base $spacing-xl;
background: linear-gradient(135deg, rgba($primary-color, 0.1) 0%, rgba($success-color, 0.1) 100%);
border: 2px solid $primary-color;
border-radius: $border-radius-base;
min-width: 140px;
}
.referralTree__headquartersLabel {
font-size: $font-size-lg;
font-weight: $font-weight-bold;
color: $primary-color;
}
.referralTree__headquartersDesc {
font-size: $font-size-xs;
color: $text-secondary;
margin-top: $spacing-xs;
}
// ============================================================================ // ============================================================================
// 认种信息 Tab // 认种信息 Tab
// ============================================================================ // ============================================================================

View File

@ -59,6 +59,7 @@ export interface ReferralNode {
nickname: string | null; nickname: string | null;
avatar: string | null; avatar: string | null;
personalAdoptions: number; personalAdoptions: number;
teamAdoptions: number; // 团队认种量
depth: number; depth: number;
directReferralCount: number; directReferralCount: number;
isCurrentUser?: boolean; isCurrentUser?: boolean;