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:
parent
e56c86545c
commit
e9dea69ee9
|
|
@ -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 {
|
|||
* 构建挖矿阶段
|
||||
*
|
||||
* 业务规则:
|
||||
* - 总挖矿天数 = 从第一批用户开始挖矿(11月8日)到今天的自然天数
|
||||
* - 总挖矿天数 = 从第一批用户开始挖矿到今天的自然天数
|
||||
* - preMineDays 表示该批次比下一批次提前开始的天数
|
||||
* - 阶段1: 只有批次1,持续批次1的 preMineDays 天
|
||||
* - 阶段2: 批次1+2,持续批次2的 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in New Issue