feat(contribution/mining-app): add team tree API using contribution-service 2.0

Add team info and direct referrals endpoints to contribution-service,
using SyncedReferral data synced via CDC. Update mining-app to use the
new v2 contribution API instead of legacy referral-service.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-16 09:17:18 -08:00
parent 3d6b6ae405
commit 4ec6c9f48b
8 changed files with 190 additions and 100 deletions

View File

@ -4,6 +4,7 @@ import { GetContributionAccountQuery } from '../../application/queries/get-contr
import { GetContributionStatsQuery } from '../../application/queries/get-contribution-stats.query';
import { GetContributionRankingQuery } from '../../application/queries/get-contribution-ranking.query';
import { GetPlantingLedgerQuery, PlantingLedgerDto } from '../../application/queries/get-planting-ledger.query';
import { GetTeamTreeQuery, DirectReferralsResponseDto, MyTeamInfoDto } from '../../application/queries/get-team-tree.query';
import {
ContributionAccountResponse,
ContributionRecordsResponse,
@ -22,6 +23,7 @@ export class ContributionController {
private readonly getStatsQuery: GetContributionStatsQuery,
private readonly getRankingQuery: GetContributionRankingQuery,
private readonly getPlantingLedgerQuery: GetPlantingLedgerQuery,
private readonly getTeamTreeQuery: GetTeamTreeQuery,
) {}
@Get('stats')
@ -117,4 +119,34 @@ export class ContributionController {
pageSize ?? 20,
);
}
// ========== 团队树 API ==========
@Get('accounts/:accountSequence/team')
@ApiOperation({ summary: '获取账户团队信息' })
@ApiParam({ name: 'accountSequence', description: '账户序号' })
@ApiResponse({ status: 200, description: '团队信息' })
async getMyTeamInfo(
@Param('accountSequence') accountSequence: string,
): Promise<MyTeamInfoDto> {
return this.getTeamTreeQuery.getMyTeamInfo(accountSequence);
}
@Get('accounts/:accountSequence/team/direct-referrals')
@ApiOperation({ summary: '获取账户直推列表(用于伞下树懒加载)' })
@ApiParam({ name: 'accountSequence', description: '账户序号' })
@ApiQuery({ name: 'limit', required: false, type: Number, description: '每页数量' })
@ApiQuery({ name: 'offset', required: false, type: Number, description: '偏移量' })
@ApiResponse({ status: 200, description: '直推列表' })
async getDirectReferrals(
@Param('accountSequence') accountSequence: string,
@Query('limit') limit?: number,
@Query('offset') offset?: number,
): Promise<DirectReferralsResponseDto> {
return this.getTeamTreeQuery.getDirectReferrals(
accountSequence,
limit ?? 100,
offset ?? 0,
);
}
}

View File

@ -20,6 +20,7 @@ import { GetContributionAccountQuery } from './queries/get-contribution-account.
import { GetContributionStatsQuery } from './queries/get-contribution-stats.query';
import { GetContributionRankingQuery } from './queries/get-contribution-ranking.query';
import { GetPlantingLedgerQuery } from './queries/get-planting-ledger.query';
import { GetTeamTreeQuery } from './queries/get-team-tree.query';
// Schedulers
import { ContributionScheduler } from './schedulers/contribution.scheduler';
@ -48,6 +49,7 @@ import { ContributionScheduler } from './schedulers/contribution.scheduler';
GetContributionStatsQuery,
GetContributionRankingQuery,
GetPlantingLedgerQuery,
GetTeamTreeQuery,
// Schedulers
ContributionScheduler,
@ -60,6 +62,7 @@ import { ContributionScheduler } from './schedulers/contribution.scheduler';
GetContributionStatsQuery,
GetContributionRankingQuery,
GetPlantingLedgerQuery,
GetTeamTreeQuery,
],
})
export class ApplicationModule {}

View File

@ -0,0 +1,121 @@
import { Injectable, Inject } from '@nestjs/common';
import {
ISyncedDataRepository,
SYNCED_DATA_REPOSITORY,
} from '../../domain/repositories/synced-data.repository.interface';
/**
*
*/
export interface TeamMemberDto {
accountSequence: string;
personalPlantingCount: number;
teamPlantingCount: number;
directReferralCount: number;
}
/**
*
*/
export interface DirectReferralsResponseDto {
referrals: TeamMemberDto[];
total: number;
hasMore: boolean;
}
/**
*
*/
export interface MyTeamInfoDto {
accountSequence: string;
personalPlantingCount: number;
teamPlantingCount: number;
directReferralCount: number;
}
@Injectable()
export class GetTeamTreeQuery {
constructor(
@Inject(SYNCED_DATA_REPOSITORY)
private readonly syncedDataRepository: ISyncedDataRepository,
) {}
/**
*
*/
async getMyTeamInfo(accountSequence: string): Promise<MyTeamInfoDto> {
// 获取个人认种棵数
const personalPlantingCount = await this.syncedDataRepository.getTotalTreesByAccountSequence(accountSequence);
// 获取直推数量
const directReferrals = await this.syncedDataRepository.findDirectReferrals(accountSequence);
// 获取团队认种棵数(伞下各级总和)
const teamTreesByLevel = await this.syncedDataRepository.getTeamTreesByLevel(accountSequence, 15);
let teamPlantingCount = 0;
teamTreesByLevel.forEach((count) => {
teamPlantingCount += count;
});
return {
accountSequence,
personalPlantingCount,
teamPlantingCount,
directReferralCount: directReferrals.length,
};
}
/**
*
*/
async getDirectReferrals(
accountSequence: string,
limit: number = 100,
offset: number = 0,
): Promise<DirectReferralsResponseDto> {
// 获取所有直推
const allDirectReferrals = await this.syncedDataRepository.findDirectReferrals(accountSequence);
// 分页
const total = allDirectReferrals.length;
const paginatedReferrals = allDirectReferrals.slice(offset, offset + limit);
// 获取每个直推成员的详细信息
const referrals: TeamMemberDto[] = await Promise.all(
paginatedReferrals.map(async (ref) => {
return this.getTeamMemberInfo(ref.accountSequence);
}),
);
return {
referrals,
total,
hasMore: offset + limit < total,
};
}
/**
*
*/
private async getTeamMemberInfo(accountSequence: string): Promise<TeamMemberDto> {
// 获取个人认种棵数
const personalPlantingCount = await this.syncedDataRepository.getTotalTreesByAccountSequence(accountSequence);
// 获取直推数量
const directReferrals = await this.syncedDataRepository.findDirectReferrals(accountSequence);
// 获取团队认种棵数
const teamTreesByLevel = await this.syncedDataRepository.getTeamTreesByLevel(accountSequence, 15);
let teamPlantingCount = 0;
teamTreesByLevel.forEach((count) => {
teamPlantingCount += count;
});
return {
accountSequence,
personalPlantingCount,
teamPlantingCount,
directReferralCount: directReferrals.length,
};
}
}

View File

@ -79,9 +79,9 @@ class ApiEndpoints {
// Mining Wallet Service 2.0 (Kong路由: /api/v2/mining-wallet)
static const String sharePoolBalance = '/api/v2/mining-wallet/pool-accounts/share-pool-balance';
// Referral Service 2.0 (Kong路由: /api/v2/referral)
static const String referralMe = '/api/v2/referral/me';
static const String referralDirects = '/api/v2/referral/me/direct-referrals';
static String userDirectReferrals(String accountSequence) =>
'/api/v2/referral/user/$accountSequence/direct-referrals';
// Team Tree (Contribution Service 2.0)
static String teamInfo(String accountSequence) =>
'/api/v2/contribution/accounts/$accountSequence/team';
static String teamDirectReferrals(String accountSequence) =>
'/api/v2/contribution/accounts/$accountSequence/team/direct-referrals';
}

View File

@ -4,13 +4,9 @@ import '../../../core/network/api_endpoints.dart';
import '../../models/referral_model.dart';
abstract class ReferralRemoteDataSource {
///
Future<ReferralInfoResponse> getMyReferralInfo();
///
Future<DirectReferralsResponse> getDirectReferrals({
int limit = 50,
int offset = 0,
///
Future<ReferralInfoResponse> getTeamInfo({
required String accountSequence,
});
///
@ -27,48 +23,22 @@ class ReferralRemoteDataSourceImpl implements ReferralRemoteDataSource {
ReferralRemoteDataSourceImpl({required this.client});
@override
Future<ReferralInfoResponse> getMyReferralInfo() async {
Future<ReferralInfoResponse> getTeamInfo({
required String accountSequence,
}) async {
try {
debugPrint('获取推荐信息...');
final response = await client.get(ApiEndpoints.referralMe);
debugPrint('获取团队信息: accountSequence=$accountSequence');
final response = await client.get(ApiEndpoints.teamInfo(accountSequence));
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
debugPrint('推荐信息获取成功: directReferralCount=${data['directReferralCount']}');
debugPrint('团队信息获取成功: directReferralCount=${data['directReferralCount']}');
return ReferralInfoResponse.fromJson(data);
}
throw Exception('获取推荐信息失败');
throw Exception('获取团队信息失败');
} catch (e) {
debugPrint('获取推荐信息失败: $e');
rethrow;
}
}
@override
Future<DirectReferralsResponse> getDirectReferrals({
int limit = 50,
int offset = 0,
}) async {
try {
debugPrint('获取直推列表...');
final response = await client.get(
ApiEndpoints.referralDirects,
queryParameters: {
'limit': limit,
'offset': offset,
},
);
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
debugPrint('直推列表获取成功: total=${data['total']}');
return DirectReferralsResponse.fromJson(data);
}
throw Exception('获取直推列表失败');
} catch (e) {
debugPrint('获取直推列表失败: $e');
debugPrint('获取团队信息失败: $e');
rethrow;
}
}
@ -82,7 +52,7 @@ class ReferralRemoteDataSourceImpl implements ReferralRemoteDataSource {
try {
debugPrint('获取用户直推列表: accountSequence=$accountSequence');
final response = await client.get(
ApiEndpoints.userDirectReferrals(accountSequence),
ApiEndpoints.teamDirectReferrals(accountSequence),
queryParameters: {
'limit': limit,
'offset': offset,

View File

@ -1,81 +1,47 @@
/// ( referral-service)
/// ( contribution-service)
class ReferralInfoResponse {
final String userId;
final String referralCode;
final String? referrerId;
final int referralChainDepth;
final int directReferralCount;
final int totalTeamCount;
final String accountSequence;
final int personalPlantingCount;
final int teamPlantingCount;
final double leaderboardScore;
final int? leaderboardRank;
final DateTime createdAt;
final int directReferralCount;
ReferralInfoResponse({
required this.userId,
required this.referralCode,
this.referrerId,
required this.referralChainDepth,
required this.directReferralCount,
required this.totalTeamCount,
required this.accountSequence,
required this.personalPlantingCount,
required this.teamPlantingCount,
required this.leaderboardScore,
this.leaderboardRank,
required this.createdAt,
required this.directReferralCount,
});
factory ReferralInfoResponse.fromJson(Map<String, dynamic> json) {
return ReferralInfoResponse(
userId: json['userId']?.toString() ?? '',
referralCode: json['referralCode'] ?? '',
referrerId: json['referrerId']?.toString(),
referralChainDepth: json['referralChainDepth'] ?? 0,
directReferralCount: json['directReferralCount'] ?? 0,
totalTeamCount: json['totalTeamCount'] ?? 0,
accountSequence: json['accountSequence']?.toString() ?? '',
personalPlantingCount: json['personalPlantingCount'] ?? 0,
teamPlantingCount: json['teamPlantingCount'] ?? 0,
leaderboardScore: (json['leaderboardScore'] ?? 0).toDouble(),
leaderboardRank: json['leaderboardRank'],
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'])
: DateTime.now(),
directReferralCount: json['directReferralCount'] ?? 0,
);
}
}
///
class DirectReferralInfo {
final String userId;
final String accountSequence; // : D + YYMMDD + 5
final String referralCode;
final int personalPlantingCount; //
final int teamPlantingCount; //
final int directReferralCount; //
final DateTime joinedAt;
final String accountSequence;
final int personalPlantingCount;
final int teamPlantingCount;
final int directReferralCount;
DirectReferralInfo({
required this.userId,
required this.accountSequence,
required this.referralCode,
required this.personalPlantingCount,
required this.teamPlantingCount,
required this.directReferralCount,
required this.joinedAt,
});
factory DirectReferralInfo.fromJson(Map<String, dynamic> json) {
return DirectReferralInfo(
userId: json['userId']?.toString() ?? '',
accountSequence: json['accountSequence']?.toString() ?? '',
referralCode: json['referralCode'] ?? '',
personalPlantingCount: json['personalPlantingCount'] ?? 0,
teamPlantingCount: json['teamPlantingCount'] ?? 0,
directReferralCount: json['directReferralCount'] ?? 0,
joinedAt: json['joinedAt'] != null
? DateTime.parse(json['joinedAt'])
: DateTime.now(),
);
}
}

View File

@ -44,14 +44,16 @@ class _TeamPageState extends ConsumerState<TeamPage> {
try {
final dataSource = getIt<ReferralRemoteDataSource>();
final referralInfo = await dataSource.getMyReferralInfo();
final teamInfo = await dataSource.getTeamInfo(
accountSequence: user.accountSequence!,
);
setState(() {
_rootNode = TeamTreeNode.createRoot(
accountSequence: user.accountSequence!,
personalPlantingCount: referralInfo.personalPlantingCount,
teamPlantingCount: referralInfo.teamPlantingCount,
directReferralCount: referralInfo.directReferralCount,
personalPlantingCount: teamInfo.personalPlantingCount,
teamPlantingCount: teamInfo.teamPlantingCount,
directReferralCount: teamInfo.directReferralCount,
);
_isLoading = false;
});

View File

@ -4,7 +4,6 @@ import '../../data/datasources/remote/referral_remote_datasource.dart';
///
class TeamTreeNode {
final String userId;
final String accountSequence;
final int personalPlantingCount;
final int teamPlantingCount;
@ -14,7 +13,6 @@ class TeamTreeNode {
bool isLoading;
TeamTreeNode({
required this.userId,
required this.accountSequence,
required this.personalPlantingCount,
required this.teamPlantingCount,
@ -30,7 +28,6 @@ class TeamTreeNode {
/// DirectReferralInfo
factory TeamTreeNode.fromDirectReferralInfo(DirectReferralInfo info) {
return TeamTreeNode(
userId: info.userId,
accountSequence: info.accountSequence,
personalPlantingCount: info.personalPlantingCount,
teamPlantingCount: info.teamPlantingCount,
@ -46,7 +43,6 @@ class TeamTreeNode {
required int directReferralCount,
}) {
return TeamTreeNode(
userId: '',
accountSequence: accountSequence,
personalPlantingCount: personalPlantingCount,
teamPlantingCount: teamPlantingCount,