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.getPersonalAdoptionCount(accountSequence),
this.userDetailRepository.getDirectReferralCount(accountSequence),
this.userDetailRepository.getBatchUserStats([accountSequence]),
]);
const currentUserStats = teamStats.get(accountSequence);
const currentUser: ReferralNodeDto = {
accountSequence: user.accountSequence,
userId: user.userId.toString(),
nickname: user.nickname,
avatar: user.avatarUrl,
personalAdoptions: personalAdoptionCount,
teamAdoptions: currentUserStats?.teamAdoptionCount || 0,
depth: referralInfo?.depth || 0,
directReferralCount: directReferralCount,
isCurrentUser: true,
@ -165,6 +168,7 @@ export class UserDetailController {
nickname: node.nickname,
avatar: node.avatarUrl,
personalAdoptions: node.personalAdoptionCount,
teamAdoptions: node.teamAdoptionCount,
depth: node.depth,
directReferralCount: node.directReferralCount,
}));
@ -179,6 +183,7 @@ export class UserDetailController {
nickname: node.nickname,
avatar: node.avatarUrl,
personalAdoptions: node.personalAdoptionCount,
teamAdoptions: node.teamAdoptionCount,
depth: node.depth,
directReferralCount: node.directReferralCount,
}));

View File

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

View File

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

View File

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

View File

@ -82,14 +82,31 @@ const plantingStatusLabels: Record<string, string> = {
};
const roleTypeLabels: Record<string, string> = {
COMMUNITY: '社区权益',
COMMUNITY_PARTNER: '社区权益',
COMMUNITY: '部门权益',
COMMUNITY_PARTNER: '部门权益',
AUTH_PROVINCE_COMPANY: '省团队', // 授权省公司 = 省团队权益
PROVINCE_COMPANY: '省区域', // 正式省公司 = 省区域权益
AUTH_CITY_COMPANY: '市团队', // 授权市公司 = 市团队权益(40U)
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> = {
PENDING: '待授权',
AUTHORIZED: '已授权',
@ -394,33 +411,43 @@ export default function UserDetailPage() {
) : referralTree ? (
<div className={styles.referralTree}>
{/* 向上的引荐人链 */}
{referralTree.ancestors.length > 0 && (
<div className={styles.referralTree__ancestors}>
<div className={styles.referralTree__label}> ()</div>
<div className={styles.referralTree__nodeList}>
{referralTree.ancestors.map((ancestor, index) => (
<div key={ancestor.accountSequence} className={styles.referralTree__nodeWrapper}>
<button
className={styles.referralTree__node}
onClick={() => handleTreeNodeClick(ancestor)}
>
<span className={styles.referralTree__nodeSeq}>{ancestor.accountSequence}</span>
<span className={styles.referralTree__nodeNickname}>
{ancestor.nickname || '未设置'}
</span>
<span className={styles.referralTree__nodeAdoptions}>
: {formatNumber(ancestor.personalAdoptions)}
</span>
</button>
{index < referralTree.ancestors.length - 1 && (
<div className={styles.referralTree__connector}></div>
)}
</div>
))}
</div>
<div className={styles.referralTree__connector}></div>
</div>
)}
<div className={styles.referralTree__ancestors}>
<div className={styles.referralTree__label}> ()</div>
{referralTree.ancestors.length > 0 ? (
<>
<div className={styles.referralTree__nodeList}>
{referralTree.ancestors.map((ancestor, index) => (
<div key={ancestor.accountSequence} className={styles.referralTree__nodeWrapper}>
<button
className={styles.referralTree__node}
onClick={() => handleTreeNodeClick(ancestor)}
>
<span className={styles.referralTree__nodeSeq}>{ancestor.accountSequence}</span>
<span className={styles.referralTree__nodeNickname}>
{ancestor.nickname || '未设置'}
</span>
<span className={styles.referralTree__nodeAdoptions}>
: {formatNumber(ancestor.personalAdoptions)} / : {formatNumber(ancestor.teamAdoptions)}
</span>
</button>
{index < referralTree.ancestors.length - 1 && (
<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}>
@ -434,9 +461,6 @@ export default function UserDetailPage() {
/>
</div>
{referralTree.directReferrals.length === 0 && referralTree.ancestors.length === 0 && (
<div className={styles.referralTree__empty}></div>
)}
</div>
) : (
<div className={styles.referralTab__empty}></div>
@ -699,7 +723,7 @@ export default function UserDetailPage() {
</span>
</div>
<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>
@ -945,7 +969,7 @@ function ReferralNodeItem({
{node.nickname || '未设置'}
</span>
<span className={styles.referralTree__nodeAdoptions}>
: {formatNumber(node.personalAdoptions)}
: {formatNumber(node.personalAdoptions)} / : {formatNumber(node.teamAdoptions)}
</span>
{node.directReferralCount > 0 && (
<span className={styles.referralTree__nodeCount}>

View File

@ -480,6 +480,29 @@
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
// ============================================================================

View File

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