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.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,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ export class ReferralNodeDto {
|
|||
nickname!: string | null;
|
||||
avatar!: string | null;
|
||||
personalAdoptions!: number;
|
||||
teamAdoptions!: number; // 团队认种量
|
||||
depth!: number;
|
||||
directReferralCount!: number;
|
||||
isCurrentUser?: boolean;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export interface ReferralNode {
|
|||
nickname: string | null;
|
||||
avatarUrl: string | null;
|
||||
personalAdoptionCount: number;
|
||||
teamAdoptionCount: number; // 团队认种量(包括本人和所有下级)
|
||||
depth: number;
|
||||
directReferralCount: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export interface ReferralNode {
|
|||
nickname: string | null;
|
||||
avatar: string | null;
|
||||
personalAdoptions: number;
|
||||
teamAdoptions: number; // 团队认种量
|
||||
depth: number;
|
||||
directReferralCount: number;
|
||||
isCurrentUser?: boolean;
|
||||
|
|
|
|||
Loading…
Reference in New Issue