From 23dabb021946256cc570d28cf4c85c20baeb91bf Mon Sep 17 00:00:00 2001 From: hailin Date: Mon, 12 Jan 2026 09:23:02 -0800 Subject: [PATCH] feat(contribution): display distribution details with actual amounts Changes: 1. contribution-service: - Add distributionSummary field to SyncedAdoption schema - Store distribution summary after contribution calculation 2. mining-admin-service: - Add distributionSummary field to SyncedAdoption schema - Calculate actual distribution from contribution_records table - Return distribution details in planting ledger API 3. mining-admin-web: - Display distribution details in planting ledger table - Show: 70%(amount) personal, 12%(amount) operation, 1%(amount) province, 2%(amount) city, 15%(amount) team - Show team distribution breakdown (distributed vs unallocated) Co-Authored-By: Claude Opus 4.5 --- .../contribution-service/prisma/schema.prisma | 4 + .../contribution-calculation.service.ts | 75 ++++++++- .../synced-data.repository.interface.ts | 8 +- .../repositories/synced-data.repository.ts | 7 +- .../mining-admin-service/prisma/schema.prisma | 1 + .../src/application/services/users.service.ts | 157 ++++++++++++++++-- .../users/components/planting-ledger.tsx | 31 +++- 7 files changed, 267 insertions(+), 16 deletions(-) diff --git a/backend/services/contribution-service/prisma/schema.prisma b/backend/services/contribution-service/prisma/schema.prisma index 91c2fce1..fbb3b1df 100644 --- a/backend/services/contribution-service/prisma/schema.prisma +++ b/backend/services/contribution-service/prisma/schema.prisma @@ -58,6 +58,10 @@ model SyncedAdoption { contributionDistributed Boolean @default(false) @map("contribution_distributed") contributionDistributedAt DateTime? @map("contribution_distributed_at") + // 分配明细摘要 (JSON 字符串格式) + // 格式: { personal: {rate, amount}, operation: {rate, amount}, province: {rate, amount}, city: {rate, amount}, team: {rate, amount, distributed, unallocated} } + distributionSummary String? @map("distribution_summary") @db.Text + createdAt DateTime @default(now()) @map("created_at") @@index([accountSequence]) 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 52551790..fa2bd34a 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 @@ -75,8 +75,14 @@ export class ContributionCalculationService { await this.unitOfWork.executeInTransaction(async () => { await this.saveDistributionResult(result, adoption.originalAdoptionId, adoption.accountSequence); - // 标记认种已处理 - await this.syncedDataRepository.markAdoptionContributionDistributed(adoption.originalAdoptionId); + // 构建分配摘要 + const distributionSummary = this.buildDistributionSummary(adoption, result); + + // 标记认种已处理,同时保存分配摘要 + await this.syncedDataRepository.markAdoptionContributionDistributed( + adoption.originalAdoptionId, + JSON.stringify(distributionSummary), + ); // 更新认种人的解锁状态(如果是首次认种) await this.updateAdopterUnlockStatus(adoption.accountSequence); @@ -322,4 +328,69 @@ export class ContributionCalculationService { ); } } + + /** + * 构建分配摘要 + */ + private buildDistributionSummary( + adoption: { treeCount: number; contributionPerTree: { toString: () => string } }, + result: ContributionDistributionResult, + ): object { + const totalContribution = adoption.treeCount * Number(adoption.contributionPerTree.toString()); + + // 计算团队分配的实际金额 + let teamDistributed = 0; + let teamUnallocated = 0; + + // TEAM_LEVEL 分配 + for (const record of result.teamLevelRecords) { + teamDistributed += Number(record.amount.value.toString()); + } + + // TEAM_BONUS 分配 + for (const record of result.teamBonusRecords) { + teamDistributed += Number(record.amount.value.toString()); + } + + // 未分配的团队算力(归总部) + for (const unalloc of result.unallocatedContributions) { + teamUnallocated += Number(unalloc.amount.value.toString()); + } + + // 系统账户分配 + const operationAmount = result.systemContributions.find(s => s.accountType === 'OPERATION')?.amount.value.toString() || '0'; + const provinceAmount = result.systemContributions.find(s => s.accountType === 'PROVINCE')?.amount.value.toString() || '0'; + const cityAmount = result.systemContributions.find(s => s.accountType === 'CITY')?.amount.value.toString() || '0'; + + return { + totalContribution: totalContribution.toFixed(4), + personal: { + rate: 0.70, + amount: result.personalRecord.amount.value.toString(), + label: '认种人账户', + }, + operation: { + rate: 0.12, + amount: operationAmount, + label: '运营账户', + }, + province: { + rate: 0.01, + amount: provinceAmount, + label: '省公司账户', + }, + city: { + rate: 0.02, + amount: cityAmount, + label: '市公司账户', + }, + team: { + rate: 0.15, + amount: (totalContribution * 0.15).toFixed(4), + distributed: teamDistributed.toFixed(4), + unallocated: teamUnallocated.toFixed(4), + label: '团队分配', + }, + }; + } } diff --git a/backend/services/contribution-service/src/domain/repositories/synced-data.repository.interface.ts b/backend/services/contribution-service/src/domain/repositories/synced-data.repository.interface.ts index 70850326..b960f16d 100644 --- a/backend/services/contribution-service/src/domain/repositories/synced-data.repository.interface.ts +++ b/backend/services/contribution-service/src/domain/repositories/synced-data.repository.interface.ts @@ -118,9 +118,13 @@ export interface ISyncedDataRepository { findAdoptionsByAccountSequence(accountSequence: string): Promise; /** - * 标记认种贡献值已分配 + * 标记认种贡献值已分配,同时保存分配摘要 */ - markAdoptionContributionDistributed(originalAdoptionId: bigint, tx?: any): Promise; + markAdoptionContributionDistributed( + originalAdoptionId: bigint, + distributionSummary?: string, + tx?: any, + ): Promise; /** * 获取账户的总认种棵数 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 e7ac6e48..331fe91b 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 @@ -153,13 +153,18 @@ export class SyncedDataRepository implements ISyncedDataRepository { return records.map((r) => this.toSyncedAdoption(r)); } - async markAdoptionContributionDistributed(originalAdoptionId: bigint, tx?: any): Promise { + async markAdoptionContributionDistributed( + originalAdoptionId: bigint, + distributionSummary?: string, + tx?: any, + ): Promise { const client = tx ?? this.client; await client.syncedAdoption.update({ where: { originalAdoptionId }, data: { contributionDistributed: true, contributionDistributedAt: new Date(), + distributionSummary: distributionSummary ?? null, }, }); } diff --git a/backend/services/mining-admin-service/prisma/schema.prisma b/backend/services/mining-admin-service/prisma/schema.prisma index a8674360..aa176382 100644 --- a/backend/services/mining-admin-service/prisma/schema.prisma +++ b/backend/services/mining-admin-service/prisma/schema.prisma @@ -235,6 +235,7 @@ model SyncedAdoption { adoptionDate DateTime @db.Date status String? // 认种状态 contributionPerTree Decimal @db.Decimal(20, 10) + distributionSummary String? @db.Text // 分配明细摘要 (JSON格式) syncedAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/backend/services/mining-admin-service/src/application/services/users.service.ts b/backend/services/mining-admin-service/src/application/services/users.service.ts index fb2fb8e7..eec501db 100644 --- a/backend/services/mining-admin-service/src/application/services/users.service.ts +++ b/backend/services/mining-admin-service/src/application/services/users.service.ts @@ -768,16 +768,55 @@ export class UsersService { totalAmount += a.treeCount * Number(a.contributionPerTree); } - // 格式化认种记录 - const items = adoptions.map((a) => ({ - id: a.id, - originalAdoptionId: a.originalAdoptionId.toString(), - treeCount: a.treeCount, - adoptionDate: a.adoptionDate, - status: a.status || 'ACTIVE', - contributionPerTree: a.contributionPerTree.toString(), - totalContribution: (a.treeCount * Number(a.contributionPerTree)).toString(), - })); + // 获取每笔认种的实际分配明细(从 contribution_records 统计) + const adoptionIds = adoptions.map((a) => a.originalAdoptionId); + const distributionByAdoption = await this.getDistributionByAdoptions(adoptionIds); + + // 格式化认种记录,包含分配明细 + const items = adoptions.map((a) => { + const totalContribution = a.treeCount * Number(a.contributionPerTree); + const distribution = distributionByAdoption.get(a.originalAdoptionId.toString()); + + return { + id: a.id, + originalAdoptionId: a.originalAdoptionId.toString(), + treeCount: a.treeCount, + adoptionDate: a.adoptionDate, + status: a.status || 'ACTIVE', + contributionPerTree: a.contributionPerTree.toString(), + totalContribution: totalContribution.toString(), + // 分配明细 + distribution: distribution || { + personal: { + rate: 0.70, + amount: (totalContribution * 0.70).toFixed(4), + label: '认种人账户', + }, + operation: { + rate: 0.12, + amount: (totalContribution * 0.12).toFixed(4), + label: '运营账户', + }, + province: { + rate: 0.01, + amount: (totalContribution * 0.01).toFixed(4), + label: '省公司账户', + }, + city: { + rate: 0.02, + amount: (totalContribution * 0.02).toFixed(4), + label: '市公司账户', + }, + team: { + rate: 0.15, + amount: (totalContribution * 0.15).toFixed(4), + distributed: '0', + unallocated: (totalContribution * 0.15).toFixed(4), + label: '团队分配', + }, + }, + }; + }); return { summary: { @@ -972,4 +1011,102 @@ export class UsersService { if (!phone || phone.length < 7) return phone; return phone.substring(0, 3) + '****' + phone.substring(phone.length - 4); } + + /** + * 获取认种的实际分配明细 + * 从 contribution_records 表统计每笔认种的实际分配 + */ + private async getDistributionByAdoptions( + adoptionIds: bigint[], + ): Promise> { + const result = new Map(); + + if (adoptionIds.length === 0) return result; + + // 查询这些认种产生的所有算力记录 + const records = await this.prisma.syncedContributionRecord.findMany({ + where: { sourceAdoptionId: { in: adoptionIds } }, + select: { + sourceAdoptionId: true, + sourceType: true, + amount: true, + baseContribution: true, + treeCount: true, + }, + }); + + // 按认种ID分组统计 + const byAdoption = new Map(); + + for (const record of records) { + const adoptionId = record.sourceAdoptionId.toString(); + if (!byAdoption.has(adoptionId)) { + byAdoption.set(adoptionId, { + personalAmount: 0, + teamLevelAmount: 0, + teamBonusAmount: 0, + totalContribution: record.treeCount * Number(record.baseContribution), + }); + } + const stats = byAdoption.get(adoptionId)!; + + const amount = Number(record.amount); + switch (record.sourceType) { + case 'PERSONAL': + stats.personalAmount += amount; + break; + case 'TEAM_LEVEL': + stats.teamLevelAmount += amount; + break; + case 'TEAM_BONUS': + stats.teamBonusAmount += amount; + break; + } + } + + // 构建分配明细 + for (const [adoptionId, stats] of byAdoption) { + const totalContribution = stats.totalContribution; + const teamTotal = totalContribution * 0.15; + const teamDistributed = stats.teamLevelAmount + stats.teamBonusAmount; + const teamUnallocated = teamTotal - teamDistributed; + + result.set(adoptionId, { + personal: { + rate: 0.70, + amount: stats.personalAmount.toFixed(4), + label: '认种人账户', + }, + operation: { + rate: 0.12, + amount: (totalContribution * 0.12).toFixed(4), + label: '运营账户', + }, + province: { + rate: 0.01, + amount: (totalContribution * 0.01).toFixed(4), + label: '省公司账户', + }, + city: { + rate: 0.02, + amount: (totalContribution * 0.02).toFixed(4), + label: '市公司账户', + }, + team: { + rate: 0.15, + amount: teamTotal.toFixed(4), + distributed: teamDistributed.toFixed(4), + unallocated: Math.max(0, teamUnallocated).toFixed(4), + label: '团队分配', + }, + }); + } + + return result; + } } diff --git a/frontend/mining-admin-web/src/features/users/components/planting-ledger.tsx b/frontend/mining-admin-web/src/features/users/components/planting-ledger.tsx index 5097d899..899f5834 100644 --- a/frontend/mining-admin-web/src/features/users/components/planting-ledger.tsx +++ b/frontend/mining-admin-web/src/features/users/components/planting-ledger.tsx @@ -142,6 +142,7 @@ export function PlantingLedger({ accountSequence }: PlantingLedgerProps) { 认种数量 单棵算力 总算力 + 分配明细 状态 认种日期 @@ -149,7 +150,7 @@ export function PlantingLedger({ accountSequence }: PlantingLedgerProps) { {data.items.length === 0 ? ( - + 暂无认种记录 @@ -160,6 +161,34 @@ export function PlantingLedger({ accountSequence }: PlantingLedgerProps) { {formatNumber(item.treeCount)} {formatDecimal(item.contributionPerTree || '0', 4)} {formatDecimal(item.totalContribution || item.totalAmount || '0', 4)} + + {item.distribution ? ( +
+
+ ①{(item.distribution.personal.rate * 100).toFixed(0)}%({formatDecimal(item.distribution.personal.amount, 2)}){item.distribution.personal.label} +
+
+ ②{(item.distribution.operation.rate * 100).toFixed(0)}%({formatDecimal(item.distribution.operation.amount, 2)}){item.distribution.operation.label} +
+
+ ③{(item.distribution.province.rate * 100).toFixed(0)}%({formatDecimal(item.distribution.province.amount, 2)}){item.distribution.province.label} +
+
+ ④{(item.distribution.city.rate * 100).toFixed(0)}%({formatDecimal(item.distribution.city.amount, 2)}){item.distribution.city.label} +
+
+ ⑤{(item.distribution.team.rate * 100).toFixed(0)}%({formatDecimal(item.distribution.team.amount, 2)}){item.distribution.team.label} + {item.distribution.team.distributed && ( + + [已分配:{formatDecimal(item.distribution.team.distributed, 2)} / 待分配:{formatDecimal(item.distribution.team.unallocated, 2)}] + + )} +
+
+ ) : ( + - + )} +
{plantingStatusLabels[item.status] || item.status}