From f14ad0b7ad6aa5ff3f0ffbcb308bbf4ff05d7f55 Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 21 Jan 2026 23:08:55 -0800 Subject: [PATCH] =?UTF-8?q?fix(batch-mining):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E8=A1=A5=E5=8F=91=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 去掉虚构的'全网算力'概念 - 每天固定分配70%产出给参与用户 - 用户收益 = 每日产出 × 70% × 天数 × (用户算力/当前参与总算力) - 总补发金额固定为: 日产出 × 70% × 总天数 Co-Authored-By: Claude Opus 4.5 --- .../services/batch-mining.service.ts | 369 ++++++++++-------- 1 file changed, 202 insertions(+), 167 deletions(-) diff --git a/backend/services/mining-service/src/application/services/batch-mining.service.ts b/backend/services/mining-service/src/application/services/batch-mining.service.ts index 69a3ddf6..f51f0f88 100644 --- a/backend/services/mining-service/src/application/services/batch-mining.service.ts +++ b/backend/services/mining-service/src/application/services/batch-mining.service.ts @@ -89,8 +89,8 @@ export interface BatchMiningPreviewResult { // 常量 const BASE_CONTRIBUTION_PER_TREE = new Decimal('22617'); // 每棵树的基础算力 const SECONDS_PER_DAY = 86400; -// 用户算力占全网算力的比例(用户占70%,30%是系统、运营、层级、团队的) -const USER_NETWORK_RATIO = new Decimal('0.70'); +// 每天产出的70%分给补发用户 +const DAILY_DISTRIBUTION_RATIO = new Decimal('0.70'); /** * 挖矿阶段信息 @@ -100,7 +100,7 @@ interface MiningPhase { startDate: Date; endDate: Date; daysInPhase: number; - networkContribution: Decimal; // 该阶段的全网算力 + participatingContribution: Decimal; // 该阶段参与用户的总算力 participatingBatches: number[]; // 参与该阶段的批次号 } @@ -109,9 +109,9 @@ interface MiningPhase { * * 核心逻辑(分阶段计算): * 1. 根据各批次的挖矿开始时间,划分挖矿阶段 - * 2. 每个阶段有不同的全网算力(随着新批次加入而增加) - * 3. 用户算力 = 认种棵数 × 基础算力/棵 × 70% - * 4. 用户在每个阶段的收益 = (用户算力 / 该阶段全网算力) × 每秒分配量 × 阶段天数 × 86400 + * 2. 每天产出的70%固定分给当前参与的用户 + * 3. 用户算力 = 认种棵数 × 基础算力/棵 + * 4. 用户在每个阶段的收益 = 每日产出 × 70% × 阶段天数 × (用户算力 / 当前参与总算力) * 5. 用户总收益 = 用户参与的各阶段收益之和 */ @Injectable() @@ -196,18 +196,23 @@ export class BatchMiningService { } // 定义挖矿阶段 - // 阶段1: 批次1独挖3天 - // 阶段2: 批次1+2共挖2天 - // 阶段3: 批次1+2+3共挖1天 - // 阶段4: 所有批次共挖(剩余天数) + // 阶段1: 批次1独挖 preMineDays 天 + // 阶段2: 批次1+2共挖 preMineDays 天 + // ...依次类推 + // 最后阶段: 所有批次共挖(剩余天数) const phases = this.buildMiningPhases(items, sortedBatches, batchContributions); this.logger.log(`[preview] 挖矿阶段: ${JSON.stringify(phases.map(p => ({ phase: p.phaseNumber, days: p.daysInPhase, batches: p.participatingBatches, - networkContribution: p.networkContribution.toFixed(2) + participatingContribution: p.participatingContribution.toFixed(2) })))}`); + // 每天补发额度 = 日产出 × 70% + const dailyDistribution = secondDistribution.times(SECONDS_PER_DAY); + const dailyAllocation = dailyDistribution.times(DAILY_DISTRIBUTION_RATIO); + this.logger.log(`[preview] 每日产出: ${dailyDistribution.toFixed(8)}, 每日补发额度(70%): ${dailyAllocation.toFixed(8)}`); + // 计算每个用户在各阶段的收益 const userAmounts = new Map(); for (const item of items) { @@ -215,15 +220,16 @@ export class BatchMiningService { } for (const phase of phases) { - const dailyDistribution = secondDistribution.times(SECONDS_PER_DAY); - const phaseDistribution = dailyDistribution.times(phase.daysInPhase); + // 该阶段的总补发额 = 每日补发额度 × 阶段天数 + const phaseAllocation = dailyAllocation.times(phase.daysInPhase); for (const item of items) { // 检查该用户的批次是否参与此阶段 if (phase.participatingBatches.includes(item.batch)) { const userContribution = userContributions.get(item.accountSequence)!; - const ratio = userContribution.dividedBy(phase.networkContribution); - const phaseAmount = phaseDistribution.times(ratio); + // 用户收益 = 阶段补发额 × (用户算力 / 当前参与总算力) + const ratio = userContribution.dividedBy(phase.participatingContribution); + const phaseAmount = phaseAllocation.times(ratio); const currentAmount = userAmounts.get(item.accountSequence)!; userAmounts.set(item.accountSequence, currentAmount.plus(phaseAmount)); @@ -235,14 +241,12 @@ export class BatchMiningService { let grandTotalAmount = new Decimal(0); const batchResults: BatchMiningPreviewResult['batches'] = []; - // 计算累计全网算力(最终状态) - // 用户只占全网的70%(30%是系统、运营、层级、团队),所以全网算力 = 用户算力 / 0.7 + // 计算用户总算力(用于显示) let totalUserContribution = new Decimal(0); for (const contribution of batchContributions.values()) { totalUserContribution = totalUserContribution.plus(contribution); } - const finalNetworkContribution = totalUserContribution.dividedBy(USER_NETWORK_RATIO); - this.logger.log(`[preview] 用户总算力: ${totalUserContribution.toFixed(2)}, 全网算力(用户占70%): ${finalNetworkContribution.toFixed(2)}`); + this.logger.log(`[preview] 用户总算力: ${totalUserContribution.toFixed(2)}`); for (const batchNum of sortedBatches) { const batchItems = batchGroups.get(batchNum)!; @@ -254,14 +258,15 @@ export class BatchMiningService { for (const item of batchItems) { const userContribution = userContributions.get(item.accountSequence)!; const amount = userAmounts.get(item.accountSequence)!; - const ratio = userContribution.dividedBy(finalNetworkContribution); + // 显示用:用户算力占总用户算力的比例 + const ratio = userContribution.dividedBy(totalUserContribution); users.push({ accountSequence: item.accountSequence, treeCount: item.treeCount, preMineDays: item.preMineDays, userContribution: userContribution.toFixed(10), - networkContribution: finalNetworkContribution.toFixed(10), + networkContribution: totalUserContribution.toFixed(10), // 显示总用户算力 contributionRatio: ratio.toFixed(18), totalSeconds: (item.preMineDays * SECONDS_PER_DAY).toString(), estimatedAmount: amount.toFixed(8), @@ -367,25 +372,22 @@ export class BatchMiningService { const phaseDays = currentPreMineDays; if (phaseDays > 0) { - // 计算该阶段参与用户的算力 + // 计算该阶段参与用户的总算力 let participatingContribution = new Decimal(0); for (const batch of participatingBatches) { participatingContribution = participatingContribution.plus(batchContributions.get(batch) || 0); } - // 实际全网算力 = 参与用户算力 / 用户占比 - // 因为用户只占全网的70%(30%是系统、运营、层级、团队),所以全网算力 = 用户算力 / 0.7 - const networkContribution = participatingContribution.dividedBy(USER_NETWORK_RATIO); phases.push({ phaseNumber: currentPhase, startDate: new Date(), endDate: new Date(), daysInPhase: phaseDays, - networkContribution, + participatingContribution, participatingBatches: [...participatingBatches], }); - this.logger.log(`[buildMiningPhases] 阶段${currentPhase}: ${phaseDays}天, 批次[${participatingBatches.join(',')}], 参与算力=${participatingContribution.toFixed(2)}, 全网算力=${networkContribution.toFixed(2)}`); + this.logger.log(`[buildMiningPhases] 阶段${currentPhase}: ${phaseDays}天, 批次[${participatingBatches.join(',')}], 参与算力=${participatingContribution.toFixed(2)}`); currentPhase++; usedDays += phaseDays; } @@ -399,19 +401,17 @@ export class BatchMiningService { for (const batch of sortedBatches) { participatingContribution = participatingContribution.plus(batchContributions.get(batch) || 0); } - // 实际全网算力 = 参与用户算力 / 用户占比 - const networkContribution = participatingContribution.dividedBy(USER_NETWORK_RATIO); phases.push({ phaseNumber: currentPhase, startDate: new Date(), endDate: new Date(), daysInPhase: remainingDays, - networkContribution, + participatingContribution, participatingBatches: [...sortedBatches], }); - this.logger.log(`[buildMiningPhases] 阶段${currentPhase}(最终): ${remainingDays}天, 所有批次[${sortedBatches.join(',')}], 参与算力=${participatingContribution.toFixed(2)}, 全网算力=${networkContribution.toFixed(2)}`); + this.logger.log(`[buildMiningPhases] 阶段${currentPhase}(最终): ${remainingDays}天, 所有批次[${sortedBatches.join(',')}], 参与算力=${participatingContribution.toFixed(2)}`); } return phases; @@ -447,7 +447,57 @@ export class BatchMiningService { const batchGroups = this.groupByBatch(items); const sortedBatches = Array.from(batchGroups.keys()).sort((a, b) => a - b); - let cumulativeContribution = new Decimal(0); + // 计算每个批次的算力 + const batchContributions = new Map(); + for (const batchNum of sortedBatches) { + const batchItems = batchGroups.get(batchNum)!; + let batchTotal = new Decimal(0); + for (const item of batchItems) { + batchTotal = batchTotal.plus(this.calculateUserContribution(item.treeCount)); + } + batchContributions.set(batchNum, batchTotal); + } + + // 计算每个用户的算力 + const userContributions = new Map(); + for (const item of items) { + userContributions.set(item.accountSequence, this.calculateUserContribution(item.treeCount)); + } + + // 构建挖矿阶段 + const phases = this.buildMiningPhases(items, sortedBatches, batchContributions); + + // 每天补发额度 = 日产出 × 70% + const dailyDistribution = secondDistribution.times(SECONDS_PER_DAY); + const dailyAllocation = dailyDistribution.times(DAILY_DISTRIBUTION_RATIO); + + // 计算每个用户在各阶段的收益 + const userAmounts = new Map(); + for (const item of items) { + userAmounts.set(item.accountSequence, new Decimal(0)); + } + + for (const phase of phases) { + const phaseAllocation = dailyAllocation.times(phase.daysInPhase); + + for (const item of items) { + if (phase.participatingBatches.includes(item.batch)) { + const userContribution = userContributions.get(item.accountSequence)!; + const ratio = userContribution.dividedBy(phase.participatingContribution); + const phaseAmount = phaseAllocation.times(ratio); + + const currentAmount = userAmounts.get(item.accountSequence)!; + userAmounts.set(item.accountSequence, currentAmount.plus(phaseAmount)); + } + } + } + + // 计算总用户算力(用于记录) + let totalUserContribution = new Decimal(0); + for (const contribution of batchContributions.values()) { + totalUserContribution = totalUserContribution.plus(contribution); + } + const results: BatchMiningItemResult[] = []; let successCount = 0; let failedCount = 0; @@ -467,156 +517,141 @@ export class BatchMiningService { }, }); - // 2. 按批次处理 - for (const batchNum of sortedBatches) { - const batchItems = batchGroups.get(batchNum)!; + // 2. 处理每个用户 + for (const item of items) { + try { + const userContribution = userContributions.get(item.accountSequence)!; + const amount = userAmounts.get(item.accountSequence)!; + const ratio = userContribution.dividedBy(totalUserContribution); + const totalSeconds = BigInt(item.preMineDays * SECONDS_PER_DAY); + const manualAmount = new ShareAmount(amount); - // 计算当前批次的总算力 - let batchTotalContribution = new Decimal(0); - for (const item of batchItems) { - const userContribution = this.calculateUserContribution(item.treeCount); - batchTotalContribution = batchTotalContribution.plus(userContribution); - } + // 查找或创建挖矿账户 + let account = await tx.miningAccount.findUnique({ + where: { accountSequence: item.accountSequence }, + }); - // 当前批次的全网算力 - cumulativeContribution = cumulativeContribution.plus(batchTotalContribution); - - // 处理当前批次的每个用户 - for (const item of batchItems) { - try { - const userContribution = this.calculateUserContribution(item.treeCount); - const ratio = userContribution.dividedBy(cumulativeContribution); - const totalSeconds = BigInt(item.preMineDays * SECONDS_PER_DAY); - const amount = secondDistribution.times(totalSeconds.toString()).times(ratio); - const manualAmount = new ShareAmount(amount); - - // 查找或创建挖矿账户 - let account = await tx.miningAccount.findUnique({ - where: { accountSequence: item.accountSequence }, - }); - - if (!account) { - // 创建新账户 - account = await tx.miningAccount.create({ - data: { - accountSequence: item.accountSequence, - totalMined: new Decimal(0), - availableBalance: new Decimal(0), - frozenBalance: new Decimal(0), - totalContribution: userContribution, // 设置初始算力 - }, - }); - } - - const balanceBefore = new Decimal(account.availableBalance); - const balanceAfter = balanceBefore.plus(manualAmount.value); - const totalMinedAfter = new Decimal(account.totalMined).plus(manualAmount.value); - - // 更新账户余额 - await tx.miningAccount.update({ - where: { accountSequence: item.accountSequence }, - data: { - totalMined: totalMinedAfter, - availableBalance: balanceAfter, - totalContribution: userContribution, // 同时更新算力 - updatedAt: now, - }, - }); - - // 创建明细记录 - const description = `批量补发挖矿收益 - 批次:${batchNum} - 认种棵数:${item.treeCount} - 提前挖${item.preMineDays}天 - 操作人:${operatorName} - ${reason}`; - - await tx.miningTransaction.create({ + if (!account) { + // 创建新账户 + account = await tx.miningAccount.create({ data: { accountSequence: item.accountSequence, - type: 'BATCH_MINING', - amount: manualAmount.value, - balanceBefore, - balanceAfter, - referenceId: execution.id, - referenceType: 'BATCH_MINING', - memo: description, + totalMined: new Decimal(0), + availableBalance: new Decimal(0), + frozenBalance: new Decimal(0), + totalContribution: userContribution, // 设置初始算力 }, }); + } - // 创建批量补发明细记录 - await tx.batchMiningRecord.create({ - data: { + const balanceBefore = new Decimal(account.availableBalance); + const balanceAfter = balanceBefore.plus(manualAmount.value); + const totalMinedAfter = new Decimal(account.totalMined).plus(manualAmount.value); + + // 更新账户余额 + await tx.miningAccount.update({ + where: { accountSequence: item.accountSequence }, + data: { + totalMined: totalMinedAfter, + availableBalance: balanceAfter, + totalContribution: userContribution, // 同时更新算力 + updatedAt: now, + }, + }); + + // 创建明细记录 + const description = `批量补发挖矿收益 - 批次:${item.batch} - 认种棵数:${item.treeCount} - 提前挖${item.preMineDays}天 - 操作人:${operatorName} - ${reason}`; + + await tx.miningTransaction.create({ + data: { + accountSequence: item.accountSequence, + type: 'BATCH_MINING', + amount: manualAmount.value, + balanceBefore, + balanceAfter, + referenceId: execution.id, + referenceType: 'BATCH_MINING', + memo: description, + }, + }); + + // 创建批量补发明细记录 + await tx.batchMiningRecord.create({ + data: { + executionId: execution.id, + accountSequence: item.accountSequence, + batch: item.batch, + treeCount: item.treeCount, + preMineDays: item.preMineDays, + userContribution, + networkContribution: totalUserContribution, + contributionRatio: ratio, + totalSeconds, + amount: manualAmount.value, + remark: item.remark, + }, + }); + + // 发布事件到 Kafka + await tx.outboxEvent.create({ + data: { + aggregateType: 'BatchMining', + aggregateId: execution.id, + eventType: 'BATCH_MINING_COMPLETED', + topic: 'mining.batch-mining.completed', + key: item.accountSequence, + payload: { + eventId: `${execution.id}-${item.accountSequence}`, executionId: execution.id, accountSequence: item.accountSequence, - batch: batchNum, + batch: item.batch, + amount: manualAmount.value.toString(), treeCount: item.treeCount, preMineDays: item.preMineDays, - userContribution, - networkContribution: cumulativeContribution, - contributionRatio: ratio, - totalSeconds, - amount: manualAmount.value, - remark: item.remark, + userContribution: userContribution.toString(), + networkContribution: totalUserContribution.toString(), + contributionRatio: ratio.toString(), + totalSeconds: totalSeconds.toString(), + operatorId, + operatorName, + reason, }, - }); + status: 'PENDING', + }, + }); - // 发布事件到 Kafka - await tx.outboxEvent.create({ - data: { - aggregateType: 'BatchMining', - aggregateId: execution.id, - eventType: 'BATCH_MINING_COMPLETED', - topic: 'mining.batch-mining.completed', - key: item.accountSequence, - payload: { - eventId: `${execution.id}-${item.accountSequence}`, - executionId: execution.id, - accountSequence: item.accountSequence, - batch: batchNum, - amount: manualAmount.value.toString(), - treeCount: item.treeCount, - preMineDays: item.preMineDays, - userContribution: userContribution.toString(), - networkContribution: cumulativeContribution.toString(), - contributionRatio: ratio.toString(), - totalSeconds: totalSeconds.toString(), - operatorId, - operatorName, - reason, - }, - status: 'PENDING', - }, - }); + results.push({ + accountSequence: item.accountSequence, + batch: item.batch, + treeCount: item.treeCount, + userContribution: userContribution.toFixed(10), + networkContribution: totalUserContribution.toFixed(10), + contributionRatio: ratio.toFixed(18), + preMineDays: item.preMineDays, + totalSeconds: totalSeconds.toString(), + amount: manualAmount.value.toFixed(8), + success: true, + }); - results.push({ - accountSequence: item.accountSequence, - batch: batchNum, - treeCount: item.treeCount, - userContribution: userContribution.toFixed(10), - networkContribution: cumulativeContribution.toFixed(10), - contributionRatio: ratio.toFixed(18), - preMineDays: item.preMineDays, - totalSeconds: totalSeconds.toString(), - amount: manualAmount.value.toFixed(8), - success: true, - }); + successCount++; + totalAmount = totalAmount.plus(manualAmount.value); - successCount++; - totalAmount = totalAmount.plus(manualAmount.value); - - } catch (error: any) { - this.logger.error(`Failed to process ${item.accountSequence}: ${error.message}`); - results.push({ - accountSequence: item.accountSequence, - batch: batchNum, - treeCount: item.treeCount, - userContribution: '0', - networkContribution: '0', - contributionRatio: '0', - preMineDays: item.preMineDays, - totalSeconds: '0', - amount: '0', - success: false, - error: error.message, - }); - failedCount++; - } + } catch (error: any) { + this.logger.error(`Failed to process ${item.accountSequence}: ${error.message}`); + results.push({ + accountSequence: item.accountSequence, + batch: item.batch, + treeCount: item.treeCount, + userContribution: '0', + networkContribution: '0', + contributionRatio: '0', + preMineDays: item.preMineDays, + totalSeconds: '0', + amount: '0', + success: false, + error: error.message, + }); + failedCount++; } }