feat(authorization/reward): 实现火柴人排名本月收益显示功能
- reward-service: 添加批量查询月度收益内部接口 - 新增 InternalController 提供 /internal/monthly-earnings/batch 端点 - 在 RewardApplicationService 添加 batchGetMonthlyEarnings 方法 - 支持按账户序列号、月份、权益类型批量查询可结算收益 - 统计分享权益(SHARE_RIGHT)、省团队权益(PROVINCE_TEAM_RIGHT)、市团队权益(CITY_TEAM_RIGHT) - authorization-service: 集成 reward-service 获取月度收益 - 新增 RewardServiceClient 用于调用 reward-service 内部接口 - 修改 getStickmanRanking 方法,调用 reward-service 获取月度收益 - 省团队查询省团队权益+分享权益,市团队查询市团队权益+分享权益 - monthlyEarnings 字段现在显示真实的月度可结算收益 🤖 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
b052afa065
commit
2f5ca85106
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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, // 算力奖励暂未实现
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export * from './referral-service.client';
|
||||
export * from './identity-service.client';
|
||||
export * from './reward-service.client';
|
||||
|
|
|
|||
102
backend/services/authorization-service/src/infrastructure/external/reward-service.client.ts
vendored
Normal file
102
backend/services/authorization-service/src/infrastructure/external/reward-service.client.ts
vendored
Normal file
|
|
@ -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<string>('REWARD_SERVICE_URL') || 'http://rwa-reward-service:3000';
|
||||
this.enabled = this.configService.get<boolean>('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<Map<string, MonthlyEarningsInfo>> {
|
||||
const result = new Map<string, MonthlyEarningsInfo>();
|
||||
|
||||
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<MonthlyEarningsInfo[]>(
|
||||
`/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<MonthlyEarningsInfo | null> {
|
||||
const result = await this.batchGetMonthlyEarnings([accountSequence], month, rightTypes);
|
||||
return result.get(accountSequence) || null;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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<MonthlyEarningsItem[]> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue