feat(batch-mining): 动态获取批量补发计算起始日期

重构批量补发功能,将硬编码的起始日期(2025-11-08)改为从 Excel 数据中
动态获取,提高计算的准确性和灵活性。

后端改动 (mining-service):
- 新增 DEFAULT_MINING_START_DATE 常量作为找不到有效数据时的默认值
- 新增 getCalculatedStartDate() 方法:从批次1用户的 miningStartDate 中
  获取最早日期
- 新增 parseDate() 方法:支持解析 2025.11.8、2025-11-08、2025/11/8 格式
- 修改 buildMiningPhases() 方法:新增 startDateStr 参数,不再硬编码日期
- 修改 preview/execute 方法:在返回结果中包含 calculatedStartDate 字段

前端改动 (mining-admin-web):
- 更新 BatchPreviewResult 接口,新增 calculatedStartDate 字段
- 预览结果描述中显示计算起始日期(蓝色高亮)
- 确认对话框中新增"计算起始日期"行

降级策略:
- 若批次1用户不存在或日期均无效,自动使用默认日期 2025-11-08

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-23 02:01:40 -08:00
parent e56c86545c
commit e9dea69ee9
2 changed files with 102 additions and 7 deletions

View File

@ -84,9 +84,11 @@ export interface BatchMiningPreviewResult {
}[];
grandTotalAmount: string;
message: string;
calculatedStartDate: string; // 计算使用的起始日期 (YYYY-MM-DD)
}
// 常量
const DEFAULT_MINING_START_DATE = '2025-11-08'; // 默认起始日期(找不到有效数据时使用)
const BASE_CONTRIBUTION_PER_TREE = new Decimal('22617'); // 每棵树的基础算力
const SECONDS_PER_DAY = 86400;
// 每天产出的70%分给补发用户
@ -158,6 +160,7 @@ export class BatchMiningService {
batches: [],
grandTotalAmount: '0',
message: `批量补发已于 ${existing?.executedAt?.toISOString()} 执行过,操作人: ${existing?.operatorName}`,
calculatedStartDate: DEFAULT_MINING_START_DATE,
};
}
@ -215,12 +218,16 @@ export class BatchMiningService {
}
}
// 获取计算起始日期(从第一批次用户的挖矿开始时间获取,找不到则使用默认值)
const calculatedStartDate = this.getCalculatedStartDate(items);
this.logger.log(`[preview] 计算起始日期: ${calculatedStartDate}`);
// 定义挖矿阶段
// 阶段1: 批次1独挖 preMineDays 天
// 阶段2: 批次1+2共挖 preMineDays 天
// ...依次类推
// 最后阶段: 所有批次共挖(剩余天数)
const phases = this.buildMiningPhases(items, sortedBatches, batchContributions);
const phases = this.buildMiningPhases(items, sortedBatches, batchContributions, calculatedStartDate);
this.logger.log(`[preview] 挖矿阶段: ${JSON.stringify(phases.map(p => ({
phase: p.phaseNumber,
days: p.daysInPhase,
@ -350,8 +357,9 @@ export class BatchMiningService {
batches: batchResults,
grandTotalAmount: grandTotalAmount.toFixed(8),
message: `预览成功: ${sortedBatches.length} 个批次, ${items.length} 个用户, 总补发金额 ${grandTotalAmount.toFixed(8)}`,
calculatedStartDate,
};
this.logger.log(`[preview] 预览完成: ${result.message}`);
this.logger.log(`[preview] 预览完成: ${result.message}, 起始日期: ${calculatedStartDate}`);
return result;
}
@ -359,7 +367,7 @@ export class BatchMiningService {
*
*
*
* - = 118
* - =
* - preMineDays
* - 阶段1: 只有批次11 preMineDays
* - 阶段2: 批次1+22 preMineDays
@ -370,6 +378,7 @@ export class BatchMiningService {
items: BatchMiningItem[],
sortedBatches: number[],
batchContributions: Map<number, Decimal>,
startDateStr: string, // 计算起始日期 (YYYY-MM-DD)
): MiningPhase[] {
const phases: MiningPhase[] = [];
@ -377,13 +386,13 @@ export class BatchMiningService {
return phases;
}
// 计算总挖矿天数:从2025年11月8日到今天
const miningStartDate = new Date('2025-11-08');
// 计算总挖矿天数:从起始日期到今天
const miningStartDate = new Date(startDateStr);
miningStartDate.setHours(0, 0, 0, 0);
const today = new Date();
today.setHours(0, 0, 0, 0);
const totalMiningDays = Math.floor((today.getTime() - miningStartDate.getTime()) / (1000 * 60 * 60 * 24));
this.logger.log(`[buildMiningPhases] 总挖矿天数: ${totalMiningDays} (从2025-11-08到今天)`);
this.logger.log(`[buildMiningPhases] 总挖矿天数: ${totalMiningDays} (从${startDateStr}到今天)`);
// 获取每个批次的提前天数
const batchPreMineDays = new Map<number, number>();
@ -516,8 +525,12 @@ export class BatchMiningService {
}
}
// 获取计算起始日期
const calculatedStartDate = this.getCalculatedStartDate(items);
this.logger.log(`[execute] 计算起始日期: ${calculatedStartDate}`);
// 构建挖矿阶段
const phases = this.buildMiningPhases(items, sortedBatches, batchContributions);
const phases = this.buildMiningPhases(items, sortedBatches, batchContributions, calculatedStartDate);
// 每天补发额度 = 日产出 × 70%
const dailyDistribution = secondDistribution.times(SECONDS_PER_DAY);
@ -881,4 +894,76 @@ export class BatchMiningService {
private calculateUserContribution(treeCount: number): Decimal {
return BASE_CONTRIBUTION_PER_TREE.times(treeCount);
}
/**
*
*
* 2025-11-08
*/
private getCalculatedStartDate(items: BatchMiningItem[]): string {
// 找出批次1的用户
const batch1Items = items.filter(item => item.batch === 1);
if (batch1Items.length === 0) {
this.logger.warn(`[getCalculatedStartDate] 未找到批次1的用户使用默认日期: ${DEFAULT_MINING_START_DATE}`);
return DEFAULT_MINING_START_DATE;
}
// 解析并找出最早的日期
let earliestDate: Date | null = null;
let earliestDateStr = '';
for (const item of batch1Items) {
const dateStr = item.miningStartDate;
if (!dateStr) continue;
const parsed = this.parseDate(dateStr);
if (parsed && (!earliestDate || parsed < earliestDate)) {
earliestDate = parsed;
earliestDateStr = dateStr;
}
}
if (!earliestDate) {
this.logger.warn(`[getCalculatedStartDate] 批次1用户的挖矿开始日期均无效使用默认日期: ${DEFAULT_MINING_START_DATE}`);
return DEFAULT_MINING_START_DATE;
}
// 格式化为 YYYY-MM-DD
const year = earliestDate.getFullYear();
const month = String(earliestDate.getMonth() + 1).padStart(2, '0');
const day = String(earliestDate.getDate()).padStart(2, '0');
const formattedDate = `${year}-${month}-${day}`;
this.logger.log(`[getCalculatedStartDate] 从批次1用户中找到最早日期: ${formattedDate} (原始值: ${earliestDateStr})`);
return formattedDate;
}
/**
*
* 支持格式: 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
/^(\d{4})\/(\d{1,2})\/(\d{1,2})$/, // 2025/11/8
];
for (const format of formats) {
const match = dateStr.match(format);
if (match) {
const year = parseInt(match[1], 10);
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);
return date;
}
}
return null;
}
}

View File

@ -63,6 +63,7 @@ interface BatchPreviewResult {
message: string;
parsedItems?: BatchItem[];
originalFileName?: string;
calculatedStartDate?: string; // 计算使用的起始日期 (YYYY-MM-DD)
}
interface BatchExecutionRecord {
@ -427,6 +428,11 @@ export default function BatchMiningPage() {
<CardDescription>
{previewResult.totalBatches} {previewResult.totalUsers}
: <span className="text-green-600 font-semibold">{formatNumber(previewResult.grandTotalAmount)}</span>
{previewResult.calculatedStartDate && (
<span className="ml-2 text-blue-600">
( {previewResult.calculatedStartDate} )
</span>
)}
</CardDescription>
</CardHeader>
<CardContent>
@ -549,6 +555,10 @@ export default function BatchMiningPage() {
<span className="text-muted-foreground"></span>
<span className="font-semibold">{previewResult?.totalBatches}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span className="font-semibold text-blue-600">{previewResult?.calculatedStartDate}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span className="font-mono font-semibold text-green-600">