diff --git a/backend/services/mining-admin-service/src/application/services/batch-mining.service.ts b/backend/services/mining-admin-service/src/application/services/batch-mining.service.ts index c1291185..06887780 100644 --- a/backend/services/mining-admin-service/src/application/services/batch-mining.service.ts +++ b/backend/services/mining-admin-service/src/application/services/batch-mining.service.ts @@ -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); diff --git a/backend/services/mining-service/src/application/services/batch-mining.service.ts b/backend/services/mining-service/src/application/services/batch-mining.service.ts index 89fa0eb2..483301c0 100644 --- a/backend/services/mining-service/src/application/services/batch-mining.service.ts +++ b/backend/services/mining-service/src/application/services/batch-mining.service.ts @@ -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 { 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(); + 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(); + 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(); + 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, + ): MiningPhase[] { + const phases: MiningPhase[] = []; + + if (sortedBatches.length === 0) { + return phases; + } + + // 获取每个批次的提前天数和总挖矿天数 + const batchPreMineDays = new Map(); + 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; + } + /** * 执行批量补发 */