fix(contribution-service): calculate totalContribution correctly in CDC event

Previously, totalContribution was incorrectly set to effectiveContribution.
Now correctly calculated as: personal + teamLevel + teamBonus

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-14 23:40:50 -08:00
parent debc8605df
commit 81a58edaca
4 changed files with 184 additions and 2 deletions

View File

@ -1,8 +1,9 @@
import { Controller, Get, Param, Query, NotFoundException } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
import { GetContributionAccountQuery } from '../../application/queries/get-contribution-account.query';
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 {
ContributionAccountResponse,
ContributionRecordsResponse,
@ -19,6 +20,7 @@ export class ContributionController {
private readonly getAccountQuery: GetContributionAccountQuery,
private readonly getStatsQuery: GetContributionStatsQuery,
private readonly getRankingQuery: GetContributionRankingQuery,
private readonly getPlantingLedgerQuery: GetPlantingLedgerQuery,
) {}
@Get('stats')
@ -95,4 +97,22 @@ export class ContributionController {
}
return result;
}
@Get('accounts/:accountSequence/planting-ledger')
@ApiOperation({ summary: '获取账户认种分类账' })
@ApiParam({ name: 'accountSequence', description: '账户序号' })
@ApiQuery({ name: 'page', required: false, type: Number, description: '页码' })
@ApiQuery({ name: 'pageSize', required: false, type: Number, description: '每页数量' })
@ApiResponse({ status: 200, description: '认种分类账' })
async getPlantingLedger(
@Param('accountSequence') accountSequence: string,
@Query('page') page?: number,
@Query('pageSize') pageSize?: number,
): Promise<PlantingLedgerDto> {
return this.getPlantingLedgerQuery.execute(
accountSequence,
page ?? 1,
pageSize ?? 20,
);
}
}

View File

@ -0,0 +1,74 @@
import { Injectable } from '@nestjs/common';
import { SyncedDataRepository } from '../../infrastructure/persistence/repositories/synced-data.repository';
export interface PlantingRecordDto {
orderId: string;
orderNo: string;
originalAdoptionId: string;
treeCount: number;
contributionPerTree: string;
totalContribution: string;
status: string;
adoptionDate: string | null;
createdAt: string;
}
export interface PlantingSummaryDto {
totalOrders: number;
totalTreeCount: number;
totalAmount: string;
effectiveTreeCount: number;
firstPlantingAt: string | null;
lastPlantingAt: string | null;
}
export interface PlantingLedgerDto {
summary: PlantingSummaryDto;
items: PlantingRecordDto[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
@Injectable()
export class GetPlantingLedgerQuery {
constructor(private readonly syncedDataRepository: SyncedDataRepository) {}
async execute(
accountSequence: string,
page: number = 1,
pageSize: number = 20,
): Promise<PlantingLedgerDto> {
const [summary, ledger] = await Promise.all([
this.syncedDataRepository.getPlantingSummary(accountSequence),
this.syncedDataRepository.getPlantingLedger(accountSequence, page, pageSize),
]);
return {
summary: {
totalOrders: summary.totalOrders,
totalTreeCount: summary.totalTreeCount,
totalAmount: summary.totalAmount,
effectiveTreeCount: summary.effectiveTreeCount,
firstPlantingAt: summary.firstPlantingAt?.toISOString() || null,
lastPlantingAt: summary.lastPlantingAt?.toISOString() || null,
},
items: ledger.items.map((item) => ({
orderId: item.id.toString(),
orderNo: `ORD-${item.originalAdoptionId}`,
originalAdoptionId: item.originalAdoptionId.toString(),
treeCount: item.treeCount,
contributionPerTree: item.contributionPerTree.toString(),
totalContribution: item.contributionPerTree.mul(item.treeCount).toString(),
status: item.status || 'UNKNOWN',
adoptionDate: item.adoptionDate?.toISOString() || null,
createdAt: item.createdAt.toISOString(),
})),
total: ledger.total,
page: ledger.page,
pageSize: ledger.pageSize,
totalPages: ledger.totalPages,
};
}
}

View File

@ -439,12 +439,17 @@ export class ContributionCalculationService {
private async publishContributionAccountUpdatedEvent(
account: ContributionAccountAggregate,
): Promise<void> {
// 总算力 = 个人算力 + 层级待解锁 + 加成待解锁
const totalContribution = account.personalContribution.value
.plus(account.totalLevelPending.value)
.plus(account.totalBonusPending.value);
const event = new ContributionAccountUpdatedEvent(
account.accountSequence,
account.personalContribution.value.toString(),
account.totalLevelPending.value.toString(),
account.totalBonusPending.value.toString(),
account.effectiveContribution.value.toString(),
totalContribution.toString(),
account.effectiveContribution.value.toString(),
account.hasAdopted,
account.directReferralAdoptedCount,

View File

@ -359,6 +359,89 @@ export class SyncedDataRepository implements ISyncedDataRepository {
return result;
}
// ========== 认种分类账查询 ==========
async getPlantingLedger(
accountSequence: string,
page: number = 1,
pageSize: number = 20,
): Promise<{
items: SyncedAdoption[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}> {
const skip = (page - 1) * pageSize;
const [items, total] = await Promise.all([
this.client.syncedAdoption.findMany({
where: { accountSequence },
orderBy: { adoptionDate: 'desc' },
skip,
take: pageSize,
}),
this.client.syncedAdoption.count({
where: { accountSequence },
}),
]);
return {
items: items.map((r) => this.toSyncedAdoption(r)),
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
};
}
async getPlantingSummary(accountSequence: string): Promise<{
totalOrders: number;
totalTreeCount: number;
totalAmount: string;
effectiveTreeCount: number;
firstPlantingAt: Date | null;
lastPlantingAt: Date | null;
}> {
const adoptions = await this.client.syncedAdoption.findMany({
where: { accountSequence },
orderBy: { adoptionDate: 'asc' },
});
if (adoptions.length === 0) {
return {
totalOrders: 0,
totalTreeCount: 0,
totalAmount: '0',
effectiveTreeCount: 0,
firstPlantingAt: null,
lastPlantingAt: null,
};
}
const totalOrders = adoptions.length;
const totalTreeCount = adoptions.reduce((sum, a) => sum + a.treeCount, 0);
const effectiveTreeCount = adoptions
.filter((a) => a.status === 'MINING_ENABLED')
.reduce((sum, a) => sum + a.treeCount, 0);
// 计算总金额treeCount * contributionPerTree (假设每棵树价格等于算力值)
let totalAmount = new Decimal(0);
for (const adoption of adoptions) {
const amount = new Decimal(adoption.contributionPerTree).mul(adoption.treeCount);
totalAmount = totalAmount.add(amount);
}
return {
totalOrders,
totalTreeCount,
totalAmount: totalAmount.toString(),
effectiveTreeCount,
firstPlantingAt: adoptions[0]?.adoptionDate || null,
lastPlantingAt: adoptions[adoptions.length - 1]?.adoptionDate || null,
};
}
// ========== 统计方法(用于查询服务)==========
async countUsers(): Promise<number> {