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

View File

@ -12,7 +12,8 @@ export interface BatchMiningItem {
treeCount: number; // 认种量(棵) treeCount: number; // 认种量(棵)
miningStartDate: string; // 挖矿开始时间 miningStartDate: string; // 挖矿开始时间
batch: number; // 批次 batch: number; // 批次
preMineDays: number; // 授权提前挖的天数 preMineDays: number; // 授权提前挖的天数(该批次比后续批次提前的天数)
totalMiningDays?: number; // 总挖矿天数(从挖矿开始日期到今天)
remark?: string; // 备注 remark?: string; // 备注
} }
@ -90,14 +91,27 @@ const BASE_CONTRIBUTION_PER_TREE = new Decimal('22617'); // 每棵树的基础
const PERSONAL_RATE = new Decimal('0.70'); // 个人算力占比 70% const PERSONAL_RATE = new Decimal('0.70'); // 个人算力占比 70%
const SECONDS_PER_DAY = 86400; const SECONDS_PER_DAY = 86400;
/**
*
*/
interface MiningPhase {
phaseNumber: number;
startDate: Date;
endDate: Date;
daysInPhase: number;
networkContribution: Decimal; // 该阶段的全网算力
participatingBatches: number[]; // 参与该阶段的批次号
}
/** /**
* *
* *
* : * :
* 1. * 1.
* 2. = + * 2.
* 3. = × / × 70% * 3. = × / × 70%
* 4. = ( / ) × × × 86400 * 4. = ( / ) × × × 86400
* 5. =
*/ */
@Injectable() @Injectable()
export class BatchMiningService { export class BatchMiningService {
@ -120,6 +134,12 @@ export class BatchMiningService {
/** /**
* *
*
*
* - 1311
* - 221+2
* - 311+2+3
* - 4
*/ */
async preview(items: BatchMiningItem[]): Promise<BatchMiningPreviewResult> { async preview(items: BatchMiningItem[]): Promise<BatchMiningPreviewResult> {
this.logger.log(`[preview] 开始预览批量补发, 共 ${items.length} 条数据`); this.logger.log(`[preview] 开始预览批量补发, 共 ${items.length} 条数据`);
@ -156,51 +176,107 @@ export class BatchMiningService {
const sortedBatches = Array.from(batchGroups.keys()).sort((a, b) => a - b); const sortedBatches = Array.from(batchGroups.keys()).sort((a, b) => a - b);
this.logger.log(`[preview] 分组完成, 共 ${sortedBatches.length} 个批次: ${sortedBatches.join(', ')}`); 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); let grandTotalAmount = new Decimal(0);
const batchResults: BatchMiningPreviewResult['batches'] = []; const batchResults: BatchMiningPreviewResult['batches'] = [];
// 计算累计全网算力(最终状态)
let finalNetworkContribution = new Decimal(0);
for (const contribution of batchContributions.values()) {
finalNetworkContribution = finalNetworkContribution.plus(contribution);
}
for (const batchNum of sortedBatches) { for (const batchNum of sortedBatches) {
const batchItems = batchGroups.get(batchNum)!; 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'] = []; const users: BatchMiningPreviewResult['batches'][0]['users'] = [];
let batchTotalAmount = new Decimal(0); let batchTotalAmount = new Decimal(0);
for (const item of batchItems) { for (const item of batchItems) {
const userContribution = this.calculateUserContribution(item.treeCount); const userContribution = userContributions.get(item.accountSequence)!;
const ratio = userContribution.dividedBy(cumulativeContribution); const amount = userAmounts.get(item.accountSequence)!;
const totalSeconds = item.preMineDays * SECONDS_PER_DAY; const ratio = userContribution.dividedBy(finalNetworkContribution);
const amount = secondDistribution.times(totalSeconds).times(ratio);
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: cumulativeContribution.toFixed(10), networkContribution: finalNetworkContribution.toFixed(10),
contributionRatio: ratio.toFixed(18), contributionRatio: ratio.toFixed(18),
totalSeconds: totalSeconds.toString(), totalSeconds: (item.preMineDays * SECONDS_PER_DAY).toString(),
estimatedAmount: amount.toFixed(8), estimatedAmount: amount.toFixed(8),
}); });
batchTotalAmount = batchTotalAmount.plus(amount); 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({ batchResults.push({
batch: batchNum, batch: batchNum,
users, users,
batchTotalContribution: batchTotalContribution.toFixed(10), batchTotalContribution: batchContribution.toFixed(10),
cumulativeNetworkContribution: cumulativeContribution.toFixed(10), cumulativeNetworkContribution: cumulativeContribution.toFixed(10),
batchTotalAmount: batchTotalAmount.toFixed(8), batchTotalAmount: batchTotalAmount.toFixed(8),
}); });
@ -221,6 +297,118 @@ export class BatchMiningService {
return result; 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;
}
/** /**
* *
*/ */