diff --git a/backend/services/authorization-service/src/app.module.ts b/backend/services/authorization-service/src/app.module.ts index aa9c1ca0..3a0d0210 100644 --- a/backend/services/authorization-service/src/app.module.ts +++ b/backend/services/authorization-service/src/app.module.ts @@ -20,7 +20,7 @@ import { import { RedisModule } from '@/infrastructure/redis/redis.module' import { KafkaModule } from '@/infrastructure/kafka/kafka.module' import { EventConsumerController } from '@/infrastructure/kafka/event-consumer.controller' -import { ReferralServiceClient, IdentityServiceClient } from '@/infrastructure/external' +import { ReferralServiceClient, IdentityServiceClient, RewardServiceClient } from '@/infrastructure/external' // Application import { AuthorizationApplicationService, REFERRAL_REPOSITORY, TEAM_STATISTICS_REPOSITORY } from '@/application/services' @@ -89,6 +89,7 @@ const MockReferralRepository = { // External Service Clients (replaces mock) ReferralServiceClient, IdentityServiceClient, + RewardServiceClient, { provide: TEAM_STATISTICS_REPOSITORY, useExisting: ReferralServiceClient, diff --git a/backend/services/authorization-service/src/application/services/authorization-application.service.ts b/backend/services/authorization-service/src/application/services/authorization-application.service.ts index d75d811e..6f119600 100644 --- a/backend/services/authorization-service/src/application/services/authorization-application.service.ts +++ b/backend/services/authorization-service/src/application/services/authorization-application.service.ts @@ -23,7 +23,7 @@ import { TeamStatistics, } from '@/domain/services' import { EventPublisherService } from '@/infrastructure/kafka' -import { ReferralServiceClient, IdentityServiceClient } from '@/infrastructure/external' +import { ReferralServiceClient, IdentityServiceClient, RewardServiceClient } from '@/infrastructure/external' import { ApplicationError, NotFoundError } from '@/shared/exceptions' import { ApplyCommunityAuthCommand, @@ -70,6 +70,7 @@ export class AuthorizationApplicationService { private readonly eventPublisher: EventPublisherService, private readonly referralServiceClient: ReferralServiceClient, private readonly identityServiceClient: IdentityServiceClient, + private readonly rewardServiceClient: RewardServiceClient, ) {} /** @@ -687,14 +688,31 @@ export class AuthorizationApplicationService { const accountSequences = assessments.map(a => a.userId.accountSequence) const userInfoMap = await this.identityServiceClient.batchGetUserInfoBySequence(accountSequences) + // 批量获取月度收益(从 reward-service) + // 根据角色类型确定要查询的权益类型 + const rightTypes = roleType === RoleType.AUTH_PROVINCE_COMPANY + ? ['PROVINCE_TEAM_RIGHT', 'SHARE_RIGHT'] // 省团队:省团队权益 + 分享权益 + : ['CITY_TEAM_RIGHT', 'SHARE_RIGHT'] // 市团队:市团队权益 + 分享权益 + const monthlyEarningsMap = await this.rewardServiceClient.batchGetMonthlyEarnings( + accountSequences, + month, + rightTypes, + ) + + this.logger.debug( + `[getStickmanRanking] 获取到 ${monthlyEarningsMap.size} 条月度收益记录`, + ) + const rankings: StickmanRankingDTO[] = [] const finalTarget = LadderTargetRule.getFinalTarget(roleType) for (const assessment of assessments) { const userInfo = userInfoMap.get(assessment.userId.accountSequence) + const earnings = monthlyEarningsMap.get(assessment.userId.accountSequence) this.logger.debug( `[getStickmanRanking] 处理评估记录: userId=${assessment.userId.value}, ` + - `regionCode=${assessment.regionCode.value}, cumulativeCompleted=${assessment.cumulativeCompleted}`, + `regionCode=${assessment.regionCode.value}, cumulativeCompleted=${assessment.cumulativeCompleted}, ` + + `monthlyEarnings=${earnings?.monthlyTotalUsdt || 0}`, ) rankings.push({ id: assessment.authorizationId.value, @@ -705,7 +723,7 @@ export class AuthorizationApplicationService { nickname: userInfo?.nickname || `用户${assessment.userId.accountSequence.slice(-4)}`, avatarUrl: userInfo?.avatarUrl, completedCount: assessment.cumulativeCompleted, - monthlyEarnings: 0, // TODO: 从奖励服务获取本月可结算收益 + monthlyEarnings: earnings?.monthlyTotalUsdt || 0, // 从奖励服务获取的月度可结算收益 isCurrentUser: currentUserId ? assessment.userId.value === currentUserId : false, ranking: assessment.rankingInRegion || 0, isFirstPlace: assessment.isFirstPlace, @@ -714,8 +732,8 @@ export class AuthorizationApplicationService { finalTarget, progressPercentage: (assessment.cumulativeCompleted / finalTarget) * 100, exceedRatio: assessment.exceedRatio, - monthlyRewardUsdt: 0, // TODO: 从奖励服务获取 - monthlyRewardRwad: 0, + monthlyRewardUsdt: earnings?.monthlyTotalUsdt || 0, // 与 monthlyEarnings 相同 + monthlyRewardRwad: 0, // 算力奖励暂未实现 }) } diff --git a/backend/services/authorization-service/src/infrastructure/external/index.ts b/backend/services/authorization-service/src/infrastructure/external/index.ts index acab958a..4d808ba1 100644 --- a/backend/services/authorization-service/src/infrastructure/external/index.ts +++ b/backend/services/authorization-service/src/infrastructure/external/index.ts @@ -1,2 +1,3 @@ export * from './referral-service.client'; export * from './identity-service.client'; +export * from './reward-service.client'; diff --git a/backend/services/authorization-service/src/infrastructure/external/reward-service.client.ts b/backend/services/authorization-service/src/infrastructure/external/reward-service.client.ts new file mode 100644 index 00000000..1b2d3fb5 --- /dev/null +++ b/backend/services/authorization-service/src/infrastructure/external/reward-service.client.ts @@ -0,0 +1,102 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import axios, { AxiosInstance } from 'axios'; + +/** + * 月度收益信息接口 + */ +export interface MonthlyEarningsInfo { + accountSequence: string; + monthlySettleableUsdt: number; + monthlyShareUsdt: number; + monthlyProvinceTeamUsdt: number; + monthlyCityTeamUsdt: number; + monthlyTotalUsdt: number; +} + +/** + * Reward Service HTTP 客户端 + * 用于从 reward-service 获取收益信息 + */ +@Injectable() +export class RewardServiceClient implements OnModuleInit { + private readonly logger = new Logger(RewardServiceClient.name); + private httpClient: AxiosInstance; + private readonly baseUrl: string; + private readonly enabled: boolean; + + constructor(private readonly configService: ConfigService) { + this.baseUrl = this.configService.get('REWARD_SERVICE_URL') || 'http://rwa-reward-service:3000'; + this.enabled = this.configService.get('REWARD_SERVICE_ENABLED') !== false; + } + + onModuleInit() { + this.httpClient = axios.create({ + baseURL: this.baseUrl, + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, + }); + + this.logger.log(`[INIT] RewardServiceClient initialized: ${this.baseUrl}, enabled: ${this.enabled}`); + } + + /** + * 批量获取月度收益 + * + * @param accountSequences 账户序列号列表 + * @param month 月份,格式 YYYY-MM + * @param rightTypes 可选,过滤权益类型 + */ + async batchGetMonthlyEarnings( + accountSequences: string[], + month: string, + rightTypes?: string[], + ): Promise> { + const result = new Map(); + + if (!this.enabled || accountSequences.length === 0) { + return result; + } + + try { + this.logger.debug(`[HTTP] POST /internal/monthly-earnings/batch - ${accountSequences.length} accounts, month=${month}`); + + const response = await this.httpClient.post( + `/api/v1/internal/monthly-earnings/batch`, + { + accountSequences, + month, + rightTypes, + }, + ); + + // 处理响应 + const data = response.data; + if (data && Array.isArray(data)) { + for (const item of data) { + result.set(item.accountSequence, item); + } + } + + this.logger.debug(`[HTTP] Got ${result.size} monthly earnings records`); + } catch (error) { + this.logger.error(`[HTTP] Failed to batch get monthly earnings:`, error); + } + + return result; + } + + /** + * 获取单个账户的月度收益 + */ + async getMonthlyEarnings( + accountSequence: string, + month: string, + rightTypes?: string[], + ): Promise { + const result = await this.batchGetMonthlyEarnings([accountSequence], month, rightTypes); + return result.get(accountSequence) || null; + } +} diff --git a/backend/services/reward-service/src/api/api.module.ts b/backend/services/reward-service/src/api/api.module.ts index 9db16129..f228d8aa 100644 --- a/backend/services/reward-service/src/api/api.module.ts +++ b/backend/services/reward-service/src/api/api.module.ts @@ -5,6 +5,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { HealthController } from './controllers/health.controller'; import { RewardController } from './controllers/reward.controller'; import { SettlementController } from './controllers/settlement.controller'; +import { InternalController } from './controllers/internal.controller'; import { JwtStrategy } from '../shared/strategies/jwt.strategy'; import { ApplicationModule } from '../application/application.module'; @@ -23,7 +24,7 @@ import { ApplicationModule } from '../application/application.module'; }), ApplicationModule, ], - controllers: [HealthController, RewardController, SettlementController], + controllers: [HealthController, RewardController, SettlementController, InternalController], providers: [JwtStrategy], }) export class ApiModule {} diff --git a/backend/services/reward-service/src/api/controllers/internal.controller.ts b/backend/services/reward-service/src/api/controllers/internal.controller.ts new file mode 100644 index 00000000..487f74ac --- /dev/null +++ b/backend/services/reward-service/src/api/controllers/internal.controller.ts @@ -0,0 +1,65 @@ +import { Controller, Post, Body, Logger } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { RewardApplicationService } from '../../application/services/reward-application.service'; + +/** + * 批量查询月度收益请求 DTO + */ +class BatchMonthlyEarningsRequest { + accountSequences: string[]; + month: string; // 格式: YYYY-MM + rightTypes?: string[]; // 可选,过滤权益类型 +} + +/** + * 月度收益响应项 + */ +interface MonthlyEarningsItem { + accountSequence: string; + monthlySettleableUsdt: number; + monthlyShareUsdt: number; // 分享权益收益 + monthlyProvinceTeamUsdt: number; // 省团队权益收益 + monthlyCityTeamUsdt: number; // 市团队权益收益 + monthlyTotalUsdt: number; // 月度总收益 +} + +/** + * 内部 API 控制器 + * 用于服务间通信,不需要 JWT 认证 + */ +@ApiTags('Internal') +@Controller('internal') +export class InternalController { + private readonly logger = new Logger(InternalController.name); + + constructor(private readonly rewardService: RewardApplicationService) {} + + @Post('monthly-earnings/batch') + @ApiOperation({ summary: '批量查询月度可结算收益(内部接口)' }) + @ApiResponse({ status: 200, description: '成功' }) + async batchGetMonthlyEarnings( + @Body() request: BatchMonthlyEarningsRequest, + ): Promise { + const { accountSequences, month, rightTypes } = request; + + if (!accountSequences || accountSequences.length === 0) { + return []; + } + + this.logger.debug( + `[batchGetMonthlyEarnings] 查询 ${accountSequences.length} 个账户的月度收益, month=${month}`, + ); + + const result = await this.rewardService.batchGetMonthlyEarnings( + accountSequences, + month, + rightTypes, + ); + + this.logger.debug( + `[batchGetMonthlyEarnings] 返回 ${result.length} 条记录`, + ); + + return result; + } +} diff --git a/backend/services/reward-service/src/application/services/reward-application.service.ts b/backend/services/reward-service/src/application/services/reward-application.service.ts index b85bbf7c..0efec54c 100644 --- a/backend/services/reward-service/src/application/services/reward-application.service.ts +++ b/backend/services/reward-service/src/application/services/reward-application.service.ts @@ -495,4 +495,102 @@ export class RewardApplicationService { `[OUTBOX] Published ${outboxEvents.length} reward.summary.updated events to outbox`, ); } + + /** + * 批量查询月度收益(内部接口,供 authorization-service 调用) + * + * 计算指定月份内进入可结算状态(SETTLEABLE)的收益总额 + * - 分享权益:SHARE_RIGHT + * - 省团队权益:PROVINCE_TEAM_RIGHT + * - 市团队权益:CITY_TEAM_RIGHT + * + * @param accountSequences 账户序列号列表 + * @param month 月份,格式 YYYY-MM + * @param rightTypes 可选,过滤权益类型 + */ + async batchGetMonthlyEarnings( + accountSequences: string[], + month: string, + rightTypes?: string[], + ): Promise<{ + accountSequence: string; + monthlySettleableUsdt: number; + monthlyShareUsdt: number; + monthlyProvinceTeamUsdt: number; + monthlyCityTeamUsdt: number; + monthlyTotalUsdt: number; + }[]> { + if (accountSequences.length === 0) { + return []; + } + + // 解析月份,获取该月的起止时间 + const [year, monthNum] = month.split('-').map(Number); + const startDate = new Date(year, monthNum - 1, 1); // 月份从0开始 + const endDate = new Date(year, monthNum, 1); // 下个月的第一天 + + this.logger.debug( + `[batchGetMonthlyEarnings] month=${month}, startDate=${startDate.toISOString()}, endDate=${endDate.toISOString()}`, + ); + + const results: { + accountSequence: string; + monthlySettleableUsdt: number; + monthlyShareUsdt: number; + monthlyProvinceTeamUsdt: number; + monthlyCityTeamUsdt: number; + monthlyTotalUsdt: number; + }[] = []; + + // 批量查询每个账户的月度收益 + for (const accountSequence of accountSequences) { + let monthlySettleableUsdt = 0; + let monthlyShareUsdt = 0; + let monthlyProvinceTeamUsdt = 0; + let monthlyCityTeamUsdt = 0; + + // 查询可结算状态的收益记录 + // 使用 claimedAt 作为进入可结算状态的时间(用户认种后权益进入可结算) + const entries = await this.rewardLedgerEntryRepository.findByAccountSequence( + accountSequence, + { + status: RewardStatus.SETTLEABLE, + startDate, + endDate, + }, + ); + + for (const entry of entries) { + const rightType = entry.rewardSource.rightType; + const amount = entry.usdtAmount.amount; + + // 如果指定了权益类型过滤,则只统计指定类型 + if (rightTypes && rightTypes.length > 0 && !rightTypes.includes(rightType)) { + continue; + } + + monthlySettleableUsdt += amount; + + // 按权益类型分类统计 + if (rightType === RightType.SHARE_RIGHT) { + monthlyShareUsdt += amount; + } else if (rightType === RightType.PROVINCE_TEAM_RIGHT) { + monthlyProvinceTeamUsdt += amount; + } else if (rightType === RightType.CITY_TEAM_RIGHT) { + monthlyCityTeamUsdt += amount; + } + } + + results.push({ + accountSequence, + monthlySettleableUsdt, + monthlyShareUsdt, + monthlyProvinceTeamUsdt, + monthlyCityTeamUsdt, + monthlyTotalUsdt: monthlySettleableUsdt, + }); + } + + return results; + } }