fix(batch-mining): 修正补发计算逻辑

- 去掉虚构的'全网算力'概念
- 每天固定分配70%产出给参与用户
- 用户收益 = 每日产出 × 70% × 天数 × (用户算力/当前参与总算力)
- 总补发金额固定为: 日产出 × 70% × 总天数

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-21 23:08:55 -08:00
parent 702fa937e8
commit f14ad0b7ad
1 changed files with 202 additions and 167 deletions

View File

@ -89,8 +89,8 @@ export interface BatchMiningPreviewResult {
// 常量 // 常量
const BASE_CONTRIBUTION_PER_TREE = new Decimal('22617'); // 每棵树的基础算力 const BASE_CONTRIBUTION_PER_TREE = new Decimal('22617'); // 每棵树的基础算力
const SECONDS_PER_DAY = 86400; const SECONDS_PER_DAY = 86400;
// 用户算力占全网算力的比例用户占70%30%是系统、运营、层级、团队的) // 每天产出的70%分给补发用户
const USER_NETWORK_RATIO = new Decimal('0.70'); const DAILY_DISTRIBUTION_RATIO = new Decimal('0.70');
/** /**
* *
@ -100,7 +100,7 @@ interface MiningPhase {
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
daysInPhase: number; daysInPhase: number;
networkContribution: Decimal; // 该阶段的全网算力 participatingContribution: Decimal; // 该阶段参与用户的总算力
participatingBatches: number[]; // 参与该阶段的批次号 participatingBatches: number[]; // 参与该阶段的批次号
} }
@ -109,9 +109,9 @@ interface MiningPhase {
* *
* : * :
* 1. * 1.
* 2. * 2. 70%
* 3. = × / × 70% * 3. = × /
* 4. = ( / ) × × × 86400 * 4. = × 70% × × ( / )
* 5. = * 5. =
*/ */
@Injectable() @Injectable()
@ -196,18 +196,23 @@ export class BatchMiningService {
} }
// 定义挖矿阶段 // 定义挖矿阶段
// 阶段1: 批次1独挖3 // 阶段1: 批次1独挖 preMineDays
// 阶段2: 批次1+2共挖2 // 阶段2: 批次1+2共挖 preMineDays
// 阶段3: 批次1+2+3共挖1天 // ...依次类推
// 阶段4: 所有批次共挖(剩余天数) // 最后阶段: 所有批次共挖(剩余天数)
const phases = this.buildMiningPhases(items, sortedBatches, batchContributions); const phases = this.buildMiningPhases(items, sortedBatches, batchContributions);
this.logger.log(`[preview] 挖矿阶段: ${JSON.stringify(phases.map(p => ({ this.logger.log(`[preview] 挖矿阶段: ${JSON.stringify(phases.map(p => ({
phase: p.phaseNumber, phase: p.phaseNumber,
days: p.daysInPhase, days: p.daysInPhase,
batches: p.participatingBatches, 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<string, Decimal>(); const userAmounts = new Map<string, Decimal>();
for (const item of items) { for (const item of items) {
@ -215,15 +220,16 @@ export class BatchMiningService {
} }
for (const phase of phases) { 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) { for (const item of items) {
// 检查该用户的批次是否参与此阶段 // 检查该用户的批次是否参与此阶段
if (phase.participatingBatches.includes(item.batch)) { if (phase.participatingBatches.includes(item.batch)) {
const userContribution = userContributions.get(item.accountSequence)!; 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)!; const currentAmount = userAmounts.get(item.accountSequence)!;
userAmounts.set(item.accountSequence, currentAmount.plus(phaseAmount)); userAmounts.set(item.accountSequence, currentAmount.plus(phaseAmount));
@ -235,14 +241,12 @@ export class BatchMiningService {
let grandTotalAmount = new Decimal(0); let grandTotalAmount = new Decimal(0);
const batchResults: BatchMiningPreviewResult['batches'] = []; const batchResults: BatchMiningPreviewResult['batches'] = [];
// 计算累计全网算力(最终状态) // 计算用户总算力(用于显示)
// 用户只占全网的70%30%是系统、运营、层级、团队),所以全网算力 = 用户算力 / 0.7
let totalUserContribution = new Decimal(0); let totalUserContribution = new Decimal(0);
for (const contribution of batchContributions.values()) { for (const contribution of batchContributions.values()) {
totalUserContribution = totalUserContribution.plus(contribution); totalUserContribution = totalUserContribution.plus(contribution);
} }
const finalNetworkContribution = totalUserContribution.dividedBy(USER_NETWORK_RATIO); this.logger.log(`[preview] 用户总算力: ${totalUserContribution.toFixed(2)}`);
this.logger.log(`[preview] 用户总算力: ${totalUserContribution.toFixed(2)}, 全网算力(用户占70%): ${finalNetworkContribution.toFixed(2)}`);
for (const batchNum of sortedBatches) { for (const batchNum of sortedBatches) {
const batchItems = batchGroups.get(batchNum)!; const batchItems = batchGroups.get(batchNum)!;
@ -254,14 +258,15 @@ export class BatchMiningService {
for (const item of batchItems) { for (const item of batchItems) {
const userContribution = userContributions.get(item.accountSequence)!; const userContribution = userContributions.get(item.accountSequence)!;
const amount = userAmounts.get(item.accountSequence)!; const amount = userAmounts.get(item.accountSequence)!;
const ratio = userContribution.dividedBy(finalNetworkContribution); // 显示用:用户算力占总用户算力的比例
const ratio = userContribution.dividedBy(totalUserContribution);
users.push({ users.push({
accountSequence: item.accountSequence, accountSequence: item.accountSequence,
treeCount: item.treeCount, treeCount: item.treeCount,
preMineDays: item.preMineDays, preMineDays: item.preMineDays,
userContribution: userContribution.toFixed(10), userContribution: userContribution.toFixed(10),
networkContribution: finalNetworkContribution.toFixed(10), networkContribution: totalUserContribution.toFixed(10), // 显示总用户算力
contributionRatio: ratio.toFixed(18), contributionRatio: ratio.toFixed(18),
totalSeconds: (item.preMineDays * SECONDS_PER_DAY).toString(), totalSeconds: (item.preMineDays * SECONDS_PER_DAY).toString(),
estimatedAmount: amount.toFixed(8), estimatedAmount: amount.toFixed(8),
@ -367,25 +372,22 @@ export class BatchMiningService {
const phaseDays = currentPreMineDays; const phaseDays = currentPreMineDays;
if (phaseDays > 0) { if (phaseDays > 0) {
// 计算该阶段参与用户的算力 // 计算该阶段参与用户的算力
let participatingContribution = new Decimal(0); let participatingContribution = new Decimal(0);
for (const batch of participatingBatches) { for (const batch of participatingBatches) {
participatingContribution = participatingContribution.plus(batchContributions.get(batch) || 0); participatingContribution = participatingContribution.plus(batchContributions.get(batch) || 0);
} }
// 实际全网算力 = 参与用户算力 / 用户占比
// 因为用户只占全网的70%30%是系统、运营、层级、团队),所以全网算力 = 用户算力 / 0.7
const networkContribution = participatingContribution.dividedBy(USER_NETWORK_RATIO);
phases.push({ phases.push({
phaseNumber: currentPhase, phaseNumber: currentPhase,
startDate: new Date(), startDate: new Date(),
endDate: new Date(), endDate: new Date(),
daysInPhase: phaseDays, daysInPhase: phaseDays,
networkContribution, participatingContribution,
participatingBatches: [...participatingBatches], 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++; currentPhase++;
usedDays += phaseDays; usedDays += phaseDays;
} }
@ -399,19 +401,17 @@ export class BatchMiningService {
for (const batch of sortedBatches) { for (const batch of sortedBatches) {
participatingContribution = participatingContribution.plus(batchContributions.get(batch) || 0); participatingContribution = participatingContribution.plus(batchContributions.get(batch) || 0);
} }
// 实际全网算力 = 参与用户算力 / 用户占比
const networkContribution = participatingContribution.dividedBy(USER_NETWORK_RATIO);
phases.push({ phases.push({
phaseNumber: currentPhase, phaseNumber: currentPhase,
startDate: new Date(), startDate: new Date(),
endDate: new Date(), endDate: new Date(),
daysInPhase: remainingDays, daysInPhase: remainingDays,
networkContribution, participatingContribution,
participatingBatches: [...sortedBatches], 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; return phases;
@ -447,7 +447,57 @@ export class BatchMiningService {
const batchGroups = this.groupByBatch(items); const batchGroups = this.groupByBatch(items);
const sortedBatches = Array.from(batchGroups.keys()).sort((a, b) => a - b); const sortedBatches = Array.from(batchGroups.keys()).sort((a, b) => a - b);
let cumulativeContribution = new Decimal(0); // 计算每个批次的算力
const batchContributions = new Map<number, Decimal>();
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<string, Decimal>();
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<string, Decimal>();
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[] = []; const results: BatchMiningItemResult[] = [];
let successCount = 0; let successCount = 0;
let failedCount = 0; let failedCount = 0;
@ -467,156 +517,141 @@ export class BatchMiningService {
}, },
}); });
// 2. 按批次处理 // 2. 处理每个用户
for (const batchNum of sortedBatches) { for (const item of items) {
const batchItems = batchGroups.get(batchNum)!; 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); let account = await tx.miningAccount.findUnique({
for (const item of batchItems) { where: { accountSequence: item.accountSequence },
const userContribution = this.calculateUserContribution(item.treeCount); });
batchTotalContribution = batchTotalContribution.plus(userContribution);
}
// 当前批次的全网算力 if (!account) {
cumulativeContribution = cumulativeContribution.plus(batchTotalContribution); // 创建新账户
account = await tx.miningAccount.create({
// 处理当前批次的每个用户
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({
data: { data: {
accountSequence: item.accountSequence, accountSequence: item.accountSequence,
type: 'BATCH_MINING', totalMined: new Decimal(0),
amount: manualAmount.value, availableBalance: new Decimal(0),
balanceBefore, frozenBalance: new Decimal(0),
balanceAfter, totalContribution: userContribution, // 设置初始算力
referenceId: execution.id,
referenceType: 'BATCH_MINING',
memo: description,
}, },
}); });
}
// 创建批量补发明细记录 const balanceBefore = new Decimal(account.availableBalance);
await tx.batchMiningRecord.create({ const balanceAfter = balanceBefore.plus(manualAmount.value);
data: { 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, executionId: execution.id,
accountSequence: item.accountSequence, accountSequence: item.accountSequence,
batch: batchNum, batch: item.batch,
amount: manualAmount.value.toString(),
treeCount: item.treeCount, treeCount: item.treeCount,
preMineDays: item.preMineDays, preMineDays: item.preMineDays,
userContribution, userContribution: userContribution.toString(),
networkContribution: cumulativeContribution, networkContribution: totalUserContribution.toString(),
contributionRatio: ratio, contributionRatio: ratio.toString(),
totalSeconds, totalSeconds: totalSeconds.toString(),
amount: manualAmount.value, operatorId,
remark: item.remark, operatorName,
reason,
}, },
}); status: 'PENDING',
},
});
// 发布事件到 Kafka results.push({
await tx.outboxEvent.create({ accountSequence: item.accountSequence,
data: { batch: item.batch,
aggregateType: 'BatchMining', treeCount: item.treeCount,
aggregateId: execution.id, userContribution: userContribution.toFixed(10),
eventType: 'BATCH_MINING_COMPLETED', networkContribution: totalUserContribution.toFixed(10),
topic: 'mining.batch-mining.completed', contributionRatio: ratio.toFixed(18),
key: item.accountSequence, preMineDays: item.preMineDays,
payload: { totalSeconds: totalSeconds.toString(),
eventId: `${execution.id}-${item.accountSequence}`, amount: manualAmount.value.toFixed(8),
executionId: execution.id, success: true,
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({ successCount++;
accountSequence: item.accountSequence, totalAmount = totalAmount.plus(manualAmount.value);
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++; } catch (error: any) {
totalAmount = totalAmount.plus(manualAmount.value); this.logger.error(`Failed to process ${item.accountSequence}: ${error.message}`);
results.push({
} catch (error: any) { accountSequence: item.accountSequence,
this.logger.error(`Failed to process ${item.accountSequence}: ${error.message}`); batch: item.batch,
results.push({ treeCount: item.treeCount,
accountSequence: item.accountSequence, userContribution: '0',
batch: batchNum, networkContribution: '0',
treeCount: item.treeCount, contributionRatio: '0',
userContribution: '0', preMineDays: item.preMineDays,
networkContribution: '0', totalSeconds: '0',
contributionRatio: '0', amount: '0',
preMineDays: item.preMineDays, success: false,
totalSeconds: '0', error: error.message,
amount: '0', });
success: false, failedCount++;
error: error.message,
});
failedCount++;
}
} }
} }