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:
parent
f55ac7e9cb
commit
e2b2c17d38
|
|
@ -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,
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue