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:
parent
af95f8da0c
commit
e4c320970f
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
|
||||
/**
|
||||
* 预览批量补发(计算但不执行)
|
||||
*
|
||||
* 分阶段计算逻辑:
|
||||
* - 阶段1(3天):只有批次1在挖,批次1独占全部产出
|
||||
* - 阶段2(2天):批次1+2一起挖,按算力比例分
|
||||
* - 阶段3(1天):批次1+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 意味着批次1比批次2提前3天
|
||||
* - 批次2的 preMineDays=2 意味着批次2比批次3提前2天
|
||||
* - 批次3的 preMineDays=1 意味着批次3比批次4提前1天
|
||||
*
|
||||
* 阶段划分:
|
||||
* - 阶段1: 只有批次1,持续 (批次1的preMineDays - 批次2的preMineDays) 天
|
||||
* - 阶段2: 批次1+2,持续 (批次2的preMineDays - 批次3的preMineDays) 天
|
||||
* - ...
|
||||
* - 最后阶段: 所有批次一起挖 (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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行批量补发
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue