feat(admin): 引荐关系树节点增加个人/团队预种份数展示

后端:
- ReferralNodeDto 新增 selfPrePlantingPortions, teamPrePlantingPortions
- user-detail.controller: getReferralTree 中并行调用
  ReferralProxyService 批量获取所有节点的预种统计
  (当前用户用 getPrePlantingStats,祖先+下级用 batchGetPrePlantingStats)

前端:
- ReferralNode 类型新增两个预种字段
- 引荐关系树节点(祖先链 + 递归展开节点)在"本人认种/团队认种"
  下方新增一行"个人预种: X份 / 团队预种: Y份"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-02 19:16:02 -08:00
parent eb425b0f92
commit 1621b75a47
4 changed files with 42 additions and 7 deletions

View File

@ -138,11 +138,12 @@ export class UserDetailController {
} }
// 获取引荐信息和实时统计 // 获取引荐信息和实时统计
const [referralInfo, personalAdoptionCount, directReferralCount, teamStats] = await Promise.all([ const [referralInfo, personalAdoptionCount, directReferralCount, teamStats, prePlantingStats] = 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]), this.userDetailRepository.getBatchUserStats([accountSequence]),
this.referralProxyService.getPrePlantingStats(accountSequence),
]); ]);
const currentUserStats = teamStats.get(accountSequence); const currentUserStats = teamStats.get(accountSequence);
@ -153,6 +154,8 @@ export class UserDetailController {
avatar: user.avatarUrl, avatar: user.avatarUrl,
personalAdoptions: personalAdoptionCount, personalAdoptions: personalAdoptionCount,
teamAdoptions: currentUserStats?.teamAdoptionCount || 0, teamAdoptions: currentUserStats?.teamAdoptionCount || 0,
selfPrePlantingPortions: prePlantingStats.selfPrePlantingPortions,
teamPrePlantingPortions: prePlantingStats.teamPrePlantingPortions,
depth: referralInfo?.depth || 0, depth: referralInfo?.depth || 0,
directReferralCount: directReferralCount, directReferralCount: directReferralCount,
isCurrentUser: true, isCurrentUser: true,
@ -161,34 +164,56 @@ export class UserDetailController {
let ancestors: ReferralNodeDto[] = []; let ancestors: ReferralNodeDto[] = [];
let directReferrals: ReferralNodeDto[] = []; let directReferrals: ReferralNodeDto[] = [];
// 收集所有需要查预种的 accountSequences
const allNodeSeqs: string[] = [];
// 向上查询 // 向上查询
let ancestorNodes: typeof ancestors extends (infer T)[] ? any[] : never = [];
if (query.direction === 'up' || query.direction === 'both') { if (query.direction === 'up' || query.direction === 'both') {
const ancestorNodes = await this.userDetailRepository.getAncestors( ancestorNodes = await this.userDetailRepository.getAncestors(
accountSequence, accountSequence,
query.depth || 1, query.depth || 1,
); );
ancestors = ancestorNodes.map((node) => ({ allNodeSeqs.push(...ancestorNodes.map((n: any) => n.accountSequence));
}
// 向下查询
let referralNodes: typeof directReferrals extends (infer T)[] ? any[] : never = [];
if (query.direction === 'down' || query.direction === 'both') {
referralNodes = await this.userDetailRepository.getDirectReferrals(accountSequence);
allNodeSeqs.push(...referralNodes.map((n: any) => n.accountSequence));
}
// 批量获取所有节点的预种统计
const batchPrePlanting = allNodeSeqs.length > 0
? await this.referralProxyService.batchGetPrePlantingStats(allNodeSeqs)
: {};
if (ancestorNodes.length > 0) {
ancestors = ancestorNodes.map((node: any) => ({
accountSequence: node.accountSequence, accountSequence: node.accountSequence,
userId: node.userId.toString(), userId: node.userId.toString(),
nickname: node.nickname, nickname: node.nickname,
avatar: node.avatarUrl, avatar: node.avatarUrl,
personalAdoptions: node.personalAdoptionCount, personalAdoptions: node.personalAdoptionCount,
teamAdoptions: node.teamAdoptionCount, teamAdoptions: node.teamAdoptionCount,
selfPrePlantingPortions: batchPrePlanting[node.accountSequence]?.selfPrePlantingPortions ?? 0,
teamPrePlantingPortions: batchPrePlanting[node.accountSequence]?.teamPrePlantingPortions ?? 0,
depth: node.depth, depth: node.depth,
directReferralCount: node.directReferralCount, directReferralCount: node.directReferralCount,
})); }));
} }
// 向下查询 if (referralNodes.length > 0) {
if (query.direction === 'down' || query.direction === 'both') { directReferrals = referralNodes.map((node: any) => ({
const referralNodes = await this.userDetailRepository.getDirectReferrals(accountSequence);
directReferrals = referralNodes.map((node) => ({
accountSequence: node.accountSequence, accountSequence: node.accountSequence,
userId: node.userId.toString(), userId: node.userId.toString(),
nickname: node.nickname, nickname: node.nickname,
avatar: node.avatarUrl, avatar: node.avatarUrl,
personalAdoptions: node.personalAdoptionCount, personalAdoptions: node.personalAdoptionCount,
teamAdoptions: node.teamAdoptionCount, teamAdoptions: node.teamAdoptionCount,
selfPrePlantingPortions: batchPrePlanting[node.accountSequence]?.selfPrePlantingPortions ?? 0,
teamPrePlantingPortions: batchPrePlanting[node.accountSequence]?.teamPrePlantingPortions ?? 0,
depth: node.depth, depth: node.depth,
directReferralCount: node.directReferralCount, directReferralCount: node.directReferralCount,
})); }));

View File

@ -74,6 +74,8 @@ export class ReferralNodeDto {
avatar!: string | null; avatar!: string | null;
personalAdoptions!: number; personalAdoptions!: number;
teamAdoptions!: number; // 团队认种量 teamAdoptions!: number; // 团队认种量
selfPrePlantingPortions!: number; // 个人预种份数
teamPrePlantingPortions!: number; // 团队预种份数
depth!: number; depth!: number;
directReferralCount!: number; directReferralCount!: number;
isCurrentUser?: boolean; isCurrentUser?: boolean;

View File

@ -490,6 +490,9 @@ export default function UserDetailPage() {
<span className={styles.referralTree__nodeAdoptions}> <span className={styles.referralTree__nodeAdoptions}>
: {formatNumber(ancestor.personalAdoptions)} / : {formatNumber(ancestor.teamAdoptions)} : {formatNumber(ancestor.personalAdoptions)} / : {formatNumber(ancestor.teamAdoptions)}
</span> </span>
<span className={styles.referralTree__nodeAdoptions}>
: {formatNumber(ancestor.selfPrePlantingPortions)} / : {formatNumber(ancestor.teamPrePlantingPortions)}
</span>
</button> </button>
{index < referralTree.ancestors.length - 1 && ( {index < referralTree.ancestors.length - 1 && (
<div className={styles.referralTree__connector}></div> <div className={styles.referralTree__connector}></div>
@ -1198,6 +1201,9 @@ function ReferralNodeItem({
<span className={styles.referralTree__nodeAdoptions}> <span className={styles.referralTree__nodeAdoptions}>
: {formatNumber(node.personalAdoptions)} / : {formatNumber(node.teamAdoptions)} : {formatNumber(node.personalAdoptions)} / : {formatNumber(node.teamAdoptions)}
</span> </span>
<span className={styles.referralTree__nodeAdoptions}>
: {formatNumber(node.selfPrePlantingPortions)} / : {formatNumber(node.teamPrePlantingPortions)}
</span>
{node.directReferralCount > 0 && ( {node.directReferralCount > 0 && (
<span className={styles.referralTree__nodeCount}> <span className={styles.referralTree__nodeCount}>
: {formatNumber(node.directReferralCount)} : {formatNumber(node.directReferralCount)}

View File

@ -64,6 +64,8 @@ export interface ReferralNode {
avatar: string | null; avatar: string | null;
personalAdoptions: number; personalAdoptions: number;
teamAdoptions: number; // 团队认种量 teamAdoptions: number; // 团队认种量
selfPrePlantingPortions: number; // 个人预种份数
teamPrePlantingPortions: number; // 团队预种份数
depth: number; depth: number;
directReferralCount: number; directReferralCount: number;
isCurrentUser?: boolean; isCurrentUser?: boolean;