From 81a58edacaf9e30196c11decaefcd8d4c57bd33e Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 14 Jan 2026 23:40:50 -0800 Subject: [PATCH] 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 --- .../controllers/contribution.controller.ts | 22 ++++- .../queries/get-planting-ledger.query.ts | 74 +++++++++++++++++ .../contribution-calculation.service.ts | 7 +- .../repositories/synced-data.repository.ts | 83 +++++++++++++++++++ 4 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 backend/services/contribution-service/src/application/queries/get-planting-ledger.query.ts diff --git a/backend/services/contribution-service/src/api/controllers/contribution.controller.ts b/backend/services/contribution-service/src/api/controllers/contribution.controller.ts index 818a2bf6..d99729e2 100644 --- a/backend/services/contribution-service/src/api/controllers/contribution.controller.ts +++ b/backend/services/contribution-service/src/api/controllers/contribution.controller.ts @@ -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 { + return this.getPlantingLedgerQuery.execute( + accountSequence, + page ?? 1, + pageSize ?? 20, + ); + } } diff --git a/backend/services/contribution-service/src/application/queries/get-planting-ledger.query.ts b/backend/services/contribution-service/src/application/queries/get-planting-ledger.query.ts new file mode 100644 index 00000000..850c5f9b --- /dev/null +++ b/backend/services/contribution-service/src/application/queries/get-planting-ledger.query.ts @@ -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 { + 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, + }; + } +} diff --git a/backend/services/contribution-service/src/application/services/contribution-calculation.service.ts b/backend/services/contribution-service/src/application/services/contribution-calculation.service.ts index cb349104..ebe028a4 100644 --- a/backend/services/contribution-service/src/application/services/contribution-calculation.service.ts +++ b/backend/services/contribution-service/src/application/services/contribution-calculation.service.ts @@ -439,12 +439,17 @@ export class ContributionCalculationService { private async publishContributionAccountUpdatedEvent( account: ContributionAccountAggregate, ): Promise { + // 总算力 = 个人算力 + 层级待解锁 + 加成待解锁 + 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, diff --git a/backend/services/contribution-service/src/infrastructure/persistence/repositories/synced-data.repository.ts b/backend/services/contribution-service/src/infrastructure/persistence/repositories/synced-data.repository.ts index c8f986f1..c6da9dfb 100644 --- a/backend/services/contribution-service/src/infrastructure/persistence/repositories/synced-data.repository.ts +++ b/backend/services/contribution-service/src/infrastructure/persistence/repositories/synced-data.repository.ts @@ -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 {