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 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<string, Decimal>();
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<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[] = [];
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++;
}
}