fix(batch-mining): 重构分阶段挖矿计算逻辑

核心修改:
1. 正确理解 preMineDays 的含义:该批次比下一批次提前的天数
2. 新增 totalMiningDays:从挖矿开始日期到今天的总天数
3. 分阶段计算收益:
   - 阶段1: 批次1独挖 (preMineDays1 - preMineDays2) 天
   - 阶段2: 批次1+2共挖 (preMineDays2 - preMineDays3) 天
   - 阶段3: 批次1+2+3共挖 (preMineDays3 - 0) 天
   - 最终阶段: 所有批次共挖 (totalMiningDays - 已用天数) 天
4. 每个阶段按当时的全网算力比例分配收益

示例:
- 批次1 preMineDays=3,批次2 preMineDays=2,批次3 preMineDays=1
- totalMiningDays=74(从11.8到1.21)
- 阶段1: 批次1独挖1天 (3-2=1)
- 阶段2: 批次1+2共挖1天 (2-1=1)
- 阶段3: 批次1+2+3共挖1天 (1-0=1)
- 阶段4: 所有批次共挖71天 (74-3=71)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-21 18:56:45 -08:00
parent af95f8da0c
commit e4c320970f
2 changed files with 237 additions and 49 deletions

View File

@ -10,7 +10,8 @@ export interface BatchMiningItem {
treeCount: number; // 认种量(棵)
miningStartDate: string; // 挖矿开始时间
batch: number; // 批次号
preMineDays: number; // 授权提前挖的天数
preMineDays: number; // 授权提前挖的天数(该批次比后续批次提前的天数)
totalMiningDays: number; // 总挖矿天数(从挖矿开始日期到今天)
remark?: string; // 备注
}
@ -252,9 +253,7 @@ export class BatchMiningService {
/**
* Excel
* Excel :
* ID | | | | | ()
*
* preMineDays () "挖矿开始时间"
* ID | | | | | |
*/
parseExcelData(rows: any[]): BatchMiningItem[] {
this.logger.log(`[parseExcelData] 开始解析 Excel 数据, 总行数: ${rows.length}`);
@ -278,13 +277,13 @@ export class BatchMiningService {
continue;
}
// 获取用户ID (第一列)
// 获取用户ID (第一列索引0)
let accountSequence = String(row[0]).trim();
if (!accountSequence.startsWith('D')) {
accountSequence = 'D' + accountSequence;
}
// 获取认种量 (第二列)
// 获取认种量 (第二列索引1)
const treeCount = parseInt(row[1], 10);
if (isNaN(treeCount) || treeCount <= 0) {
this.logger.debug(`[parseExcelData] 跳过行 ${i + 1}: 认种量无效 (${row[1]})`);
@ -293,24 +292,13 @@ export class BatchMiningService {
// 获取挖矿开始时间 (第四列索引3)
const miningStartDateStr = String(row[3] || '').trim();
if (!miningStartDateStr) {
this.logger.warn(`[parseExcelData] 跳过行 ${i + 1}: 挖矿开始时间为空`);
continue;
}
// 解析挖矿开始时间 (格式: 2025.11.8 或 2025-11-08)
// 解析挖矿开始时间,计算总挖矿天数
const miningStartDate = this.parseDate(miningStartDateStr);
if (!miningStartDate) {
this.logger.warn(`[parseExcelData] 跳过行 ${i + 1}: 挖矿开始时间格式无效 (${miningStartDateStr})`);
continue;
}
// 计算从挖矿开始到今天的天数
const diffTime = today.getTime() - miningStartDate.getTime();
const preMineDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (preMineDays <= 0) {
this.logger.warn(`[parseExcelData] 跳过行 ${i + 1}: 挖矿天数<=0 (开始日期: ${miningStartDateStr}, 天数: ${preMineDays})`);
continue;
let totalMiningDays = 0;
if (miningStartDate) {
const diffTime = today.getTime() - miningStartDate.getTime();
totalMiningDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
}
// 获取批次 (第五列索引4)
@ -320,13 +308,24 @@ export class BatchMiningService {
continue;
}
// 获取授权提前挖的天数 (第六列索引5)
const preMineDays = parseInt(row[5], 10);
if (isNaN(preMineDays) || preMineDays <= 0) {
this.logger.warn(`[parseExcelData] 跳过行 ${i + 1}: 授权提前挖的天数无效 (${row[5]})`);
continue;
}
// 获取备注 (第七列索引6)
const remark = row[6] ? String(row[6]).trim() : undefined;
items.push({
accountSequence,
treeCount,
miningStartDate: miningStartDateStr,
batch,
preMineDays,
remark: row[5] ? String(row[5]) : undefined,
totalMiningDays,
remark,
});
}
@ -344,7 +343,8 @@ export class BatchMiningService {
* 支持格式: 2025.11.8, 2025-11-08, 2025/11/8
*/
private parseDate(dateStr: string): Date | null {
// 尝试不同格式
if (!dateStr) return null;
const formats = [
/^(\d{4})\.(\d{1,2})\.(\d{1,2})$/, // 2025.11.8
/^(\d{4})-(\d{1,2})-(\d{1,2})$/, // 2025-11-08
@ -355,7 +355,7 @@ export class BatchMiningService {
const match = dateStr.match(format);
if (match) {
const year = parseInt(match[1], 10);
const month = parseInt(match[2], 10) - 1; // 月份从0开始
const month = parseInt(match[2], 10) - 1;
const day = parseInt(match[3], 10);
const date = new Date(year, month, day);
date.setHours(0, 0, 0, 0);

View File

@ -12,7 +12,8 @@ export interface BatchMiningItem {
treeCount: number; // 认种量(棵)
miningStartDate: string; // 挖矿开始时间
batch: number; // 批次
preMineDays: number; // 授权提前挖的天数
preMineDays: number; // 授权提前挖的天数(该批次比后续批次提前的天数)
totalMiningDays?: number; // 总挖矿天数(从挖矿开始日期到今天)
remark?: string; // 备注
}
@ -90,14 +91,27 @@ const BASE_CONTRIBUTION_PER_TREE = new Decimal('22617'); // 每棵树的基础
const PERSONAL_RATE = new Decimal('0.70'); // 个人算力占比 70%
const SECONDS_PER_DAY = 86400;
/**
*
*/
interface MiningPhase {
phaseNumber: number;
startDate: Date;
endDate: Date;
daysInPhase: number;
networkContribution: Decimal; // 该阶段的全网算力
participatingBatches: number[]; // 参与该阶段的批次号
}
/**
*
*
* :
* 1.
* 2. = +
* :
* 1.
* 2.
* 3. = × / × 70%
* 4. = ( / ) × × × 86400
* 4. = ( / ) × × × 86400
* 5. =
*/
@Injectable()
export class BatchMiningService {
@ -120,6 +134,12 @@ export class BatchMiningService {
/**
*
*
*
* - 1311
* - 221+2
* - 311+2+3
* - 4
*/
async preview(items: BatchMiningItem[]): Promise<BatchMiningPreviewResult> {
this.logger.log(`[preview] 开始预览批量补发, 共 ${items.length} 条数据`);
@ -156,51 +176,107 @@ export class BatchMiningService {
const sortedBatches = Array.from(batchGroups.keys()).sort((a, b) => a - b);
this.logger.log(`[preview] 分组完成, 共 ${sortedBatches.length} 个批次: ${sortedBatches.join(', ')}`);
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);
this.logger.log(`[preview] 批次${batchNum}算力: ${batchTotal.toFixed(2)}`);
}
// 计算每个用户的算力
const userContributions = new Map<string, Decimal>();
for (const item of items) {
userContributions.set(item.accountSequence, this.calculateUserContribution(item.treeCount));
}
// 定义挖矿阶段
// 阶段1: 批次1独挖3天
// 阶段2: 批次1+2共挖2天
// 阶段3: 批次1+2+3共挖1天
// 阶段4: 所有批次共挖(剩余天数)
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)
})))}`);
// 计算每个用户在各阶段的收益
const userAmounts = new Map<string, Decimal>();
for (const item of items) {
userAmounts.set(item.accountSequence, new Decimal(0));
}
for (const phase of phases) {
const dailyDistribution = secondDistribution.times(SECONDS_PER_DAY);
const phaseDistribution = dailyDistribution.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 currentAmount = userAmounts.get(item.accountSequence)!;
userAmounts.set(item.accountSequence, currentAmount.plus(phaseAmount));
}
}
}
// 构建返回结果
let grandTotalAmount = new Decimal(0);
const batchResults: BatchMiningPreviewResult['batches'] = [];
// 计算累计全网算力(最终状态)
let finalNetworkContribution = new Decimal(0);
for (const contribution of batchContributions.values()) {
finalNetworkContribution = finalNetworkContribution.plus(contribution);
}
for (const batchNum of sortedBatches) {
const batchItems = batchGroups.get(batchNum)!;
const batchContribution = batchContributions.get(batchNum)!;
// 计算当前批次的总算力
let batchTotalContribution = new Decimal(0);
for (const item of batchItems) {
const userContribution = this.calculateUserContribution(item.treeCount);
batchTotalContribution = batchTotalContribution.plus(userContribution);
}
// 当前批次的全网算力 = 累计算力 + 当前批次算力
cumulativeContribution = cumulativeContribution.plus(batchTotalContribution);
// 计算当前批次每个用户的补发金额
const users: BatchMiningPreviewResult['batches'][0]['users'] = [];
let batchTotalAmount = new Decimal(0);
for (const item of batchItems) {
const userContribution = this.calculateUserContribution(item.treeCount);
const ratio = userContribution.dividedBy(cumulativeContribution);
const totalSeconds = item.preMineDays * SECONDS_PER_DAY;
const amount = secondDistribution.times(totalSeconds).times(ratio);
const userContribution = userContributions.get(item.accountSequence)!;
const amount = userAmounts.get(item.accountSequence)!;
const ratio = userContribution.dividedBy(finalNetworkContribution);
users.push({
accountSequence: item.accountSequence,
treeCount: item.treeCount,
preMineDays: item.preMineDays,
userContribution: userContribution.toFixed(10),
networkContribution: cumulativeContribution.toFixed(10),
networkContribution: finalNetworkContribution.toFixed(10),
contributionRatio: ratio.toFixed(18),
totalSeconds: totalSeconds.toString(),
totalSeconds: (item.preMineDays * SECONDS_PER_DAY).toString(),
estimatedAmount: amount.toFixed(8),
});
batchTotalAmount = batchTotalAmount.plus(amount);
}
// 计算到当前批次的累计算力
let cumulativeContribution = new Decimal(0);
for (const b of sortedBatches) {
cumulativeContribution = cumulativeContribution.plus(batchContributions.get(b)!);
if (b === batchNum) break;
}
batchResults.push({
batch: batchNum,
users,
batchTotalContribution: batchTotalContribution.toFixed(10),
batchTotalContribution: batchContribution.toFixed(10),
cumulativeNetworkContribution: cumulativeContribution.toFixed(10),
batchTotalAmount: batchTotalAmount.toFixed(8),
});
@ -221,6 +297,118 @@ export class BatchMiningService {
return result;
}
/**
*
*
* "提前天数"
* - preMineDays
* - 1 preMineDays=3 123
* - 2 preMineDays=2 232
* - 3 preMineDays=1 341
*
*
* - 阶段1: 只有批次1 (1preMineDays - 2preMineDays)
* - 阶段2: 批次1+2 (2preMineDays - 3preMineDays)
* - ...
* - 最后阶段: 所有批次一起挖 (totalMiningDays - )
*/
private buildMiningPhases(
items: BatchMiningItem[],
sortedBatches: number[],
batchContributions: Map<number, Decimal>,
): MiningPhase[] {
const phases: MiningPhase[] = [];
if (sortedBatches.length === 0) {
return phases;
}
// 获取每个批次的提前天数和总挖矿天数
const batchPreMineDays = new Map<number, number>();
let maxTotalMiningDays = 0;
for (const item of items) {
if (!batchPreMineDays.has(item.batch)) {
batchPreMineDays.set(item.batch, item.preMineDays);
}
// 使用最大的总挖矿天数(第一批次的用户应该有最长的挖矿时间)
if (item.totalMiningDays && item.totalMiningDays > maxTotalMiningDays) {
maxTotalMiningDays = item.totalMiningDays;
}
}
this.logger.log(`[buildMiningPhases] 各批次提前天数: ${JSON.stringify(Object.fromEntries(batchPreMineDays))}`);
this.logger.log(`[buildMiningPhases] 最大总挖矿天数: ${maxTotalMiningDays}`);
// 获取第一批次的提前天数
const firstBatchPreMineDays = batchPreMineDays.get(sortedBatches[0]) || 0;
if (maxTotalMiningDays <= 0) {
this.logger.warn('[buildMiningPhases] 总挖矿天数<=0无法计算');
return phases;
}
let currentPhase = 1;
let participatingBatches: number[] = [];
let usedDays = 0; // 已分配的天数
// 按批次顺序添加阶段(提前挖矿阶段)
for (let i = 0; i < sortedBatches.length; i++) {
const currentBatch = sortedBatches[i];
const currentPreMineDays = batchPreMineDays.get(currentBatch) || 0;
const nextBatch = sortedBatches[i + 1];
const nextPreMineDays = nextBatch !== undefined ? (batchPreMineDays.get(nextBatch) || 0) : 0;
// 当前批次加入挖矿
participatingBatches.push(currentBatch);
// 计算当前阶段持续的天数 = 当前批次提前天数 - 下一批次提前天数
const phaseDays = currentPreMineDays - nextPreMineDays;
if (phaseDays > 0) {
// 计算该阶段的全网算力
let networkContribution = new Decimal(0);
for (const batch of participatingBatches) {
networkContribution = networkContribution.plus(batchContributions.get(batch) || 0);
}
phases.push({
phaseNumber: currentPhase,
startDate: new Date(),
endDate: new Date(),
daysInPhase: phaseDays,
networkContribution,
participatingBatches: [...participatingBatches],
});
this.logger.log(`[buildMiningPhases] 阶段${currentPhase}: ${phaseDays}天, 批次[${participatingBatches.join(',')}], 算力=${networkContribution.toFixed(2)}`);
currentPhase++;
usedDays += phaseDays;
}
}
// 添加最后阶段:所有批次一起挖(剩余天数)
const remainingDays = maxTotalMiningDays - usedDays;
if (remainingDays > 0) {
// 所有批次都参与
let networkContribution = new Decimal(0);
for (const batch of sortedBatches) {
networkContribution = networkContribution.plus(batchContributions.get(batch) || 0);
}
phases.push({
phaseNumber: currentPhase,
startDate: new Date(),
endDate: new Date(),
daysInPhase: remainingDays,
networkContribution,
participatingBatches: [...sortedBatches],
});
this.logger.log(`[buildMiningPhases] 阶段${currentPhase}(最终): ${remainingDays}天, 所有批次[${sortedBatches.join(',')}], 算力=${networkContribution.toFixed(2)}`);
}
return phases;
}
/**
*
*/