feat(mining-admin): 算力同步完成前禁用激活挖矿按钮

- 后端:getMiningStatus 接口并行获取 contribution-service 总算力,对比两边是否一致
- 前端:未同步时显示"全网算力同步中..."提示,禁用激活按钮
- 前端:同步中每 3 秒刷新状态,同步完成后恢复 30 秒刷新

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-16 03:00:40 -08:00
parent 72b3b44d37
commit 7c00c900a0
4 changed files with 81 additions and 15 deletions

View File

@ -41,21 +41,45 @@ export class ConfigController {
@ApiOperation({ summary: '获取挖矿状态' })
async getMiningStatus() {
const miningServiceUrl = this.appConfigService.get<string>('MINING_SERVICE_URL', 'http://localhost:3021');
const contributionServiceUrl = this.appConfigService.get<string>('CONTRIBUTION_SERVICE_URL', 'http://localhost:3020');
this.logger.log(`Fetching mining status from ${miningServiceUrl}/api/v2/admin/status`);
try {
const response = await fetch(`${miningServiceUrl}/api/v2/admin/status`);
if (!response.ok) {
throw new Error(`Failed to fetch mining status: ${response.status}`);
// 并行获取 mining-service 状态和 contribution-service 总算力
const [miningResponse, contributionResponse] = await Promise.all([
fetch(`${miningServiceUrl}/api/v2/admin/status`),
fetch(`${contributionServiceUrl}/api/v1/contribution/stats`).catch(() => null),
]);
if (!miningResponse.ok) {
throw new Error(`Failed to fetch mining status: ${miningResponse.status}`);
}
const result = await response.json();
this.logger.log(`Mining service response: ${JSON.stringify(result)}`);
if (result.data) {
return result.data;
const miningResult = await miningResponse.json();
this.logger.log(`Mining service response: ${JSON.stringify(miningResult)}`);
// 获取 contribution-service 的总有效算力
let contributionTotal: string | null = null;
if (contributionResponse && contributionResponse.ok) {
const contributionResult = await contributionResponse.json();
// contribution-service 返回的是 data.totalContribution
contributionTotal = contributionResult.data?.totalContribution || contributionResult.totalContribution || null;
}
const miningData = miningResult.data || miningResult;
const miningTotal = miningData.totalContribution || '0';
// 判断算力是否同步完成:两边总算力相等
const isSynced = contributionTotal !== null &&
parseFloat(contributionTotal) > 0 &&
Math.abs(parseFloat(miningTotal) - parseFloat(contributionTotal)) < 0.01;
return {
initialized: false,
isActive: false,
error: 'Invalid response from mining service',
...miningData,
contributionSyncStatus: {
isSynced,
miningTotal,
contributionTotal: contributionTotal || '0',
},
};
} catch (error) {
this.logger.error('Failed to get mining status', error);
@ -63,6 +87,11 @@ export class ConfigController {
initialized: false,
isActive: false,
error: `Unable to connect to mining service: ${error.message}`,
contributionSyncStatus: {
isSynced: false,
miningTotal: '0',
contributionTotal: '0',
},
};
}
}

View File

@ -12,7 +12,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '
import { Label } from '@/components/ui/label';
import { Skeleton } from '@/components/ui/skeleton';
import { Badge } from '@/components/ui/badge';
import { Pencil, Save, X, Play, Pause, AlertCircle, CheckCircle2 } from 'lucide-react';
import { Pencil, Save, X, Play, Pause, AlertCircle, CheckCircle2, Loader2 } from 'lucide-react';
import type { SystemConfig } from '@/types/config';
const categoryLabels: Record<string, string> = {
@ -154,6 +154,20 @@ export default function ConfigsPage() {
</div>
)}
{/* 算力同步状态提示 */}
{miningStatus.contributionSyncStatus && !miningStatus.contributionSyncStatus.isSynced && (
<div className="flex items-center gap-2 p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
<Loader2 className="h-4 w-4 animate-spin text-yellow-600" />
<div className="flex-1">
<p className="text-sm font-medium text-yellow-800 dark:text-yellow-200">...</p>
<p className="text-xs text-yellow-600 dark:text-yellow-400">
: {formatNumber(miningStatus.contributionSyncStatus.miningTotal)} /
: {formatNumber(miningStatus.contributionSyncStatus.contributionTotal)}
</p>
</div>
</div>
)}
<div className="flex justify-end pt-4 border-t">
{miningStatus.isActive ? (
<Button
@ -167,10 +181,19 @@ export default function ConfigsPage() {
) : (
<Button
onClick={() => activateMining.mutate()}
disabled={activateMining.isPending}
disabled={activateMining.isPending || (miningStatus.contributionSyncStatus && !miningStatus.contributionSyncStatus.isSynced)}
>
<Play className="h-4 w-4 mr-2" />
{activateMining.isPending ? '激活中...' : '激活挖矿'}
{miningStatus.contributionSyncStatus && !miningStatus.contributionSyncStatus.isSynced ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
...
</>
) : (
<>
<Play className="h-4 w-4 mr-2" />
{activateMining.isPending ? '激活中...' : '激活挖矿'}
</>
)}
</Button>
)}
</div>

View File

@ -1,6 +1,12 @@
import { apiClient } from '@/lib/api/client';
import type { SystemConfig } from '@/types/config';
export interface ContributionSyncStatus {
isSynced: boolean;
miningTotal: string;
contributionTotal: string;
}
export interface MiningStatus {
initialized: boolean;
isActive: boolean;
@ -15,6 +21,7 @@ export interface MiningStatus {
};
accountCount: number;
totalContribution: string;
contributionSyncStatus?: ContributionSyncStatus;
error?: string;
}

View File

@ -52,7 +52,14 @@ export function useMiningStatus() {
return useQuery({
queryKey: ['configs', 'mining-status'],
queryFn: () => configsApi.getMiningStatus(),
refetchInterval: 30000,
// 当算力未同步完成时,每 3 秒刷新一次;同步完成后每 30 秒刷新一次
refetchInterval: (query) => {
const data = query.state.data;
if (data?.contributionSyncStatus && !data.contributionSyncStatus.isSynced) {
return 3000; // 3 秒
}
return 30000; // 30 秒
},
});
}