feat(snapshot): 进度精度升级 — Float百分比 + MB消息存DB

- schema: progress Int→Float,新增 progressMsg 字段
- PG handler: 百分比保留2位小数(toFixed(2)),不再 Math.floor
- orchestrator: 每2秒写DB时同时写 progressMsg (含MB信息)
- 前端: 百分比显示 toFixed(1),message 优先读 progressMsg

效果: 113GB库每次轮询进度条和MB数都有变化,不再卡在整数百分比

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-24 01:57:47 -08:00
parent 9cbc0ba580
commit 8855491637
10 changed files with 14 additions and 9 deletions

View File

@ -30,7 +30,8 @@ model SnapshotDetail {
taskId String taskId String
target String target String
status String @default("PENDING") status String @default("PENDING")
progress Int @default(0) progress Float @default(0)
progressMsg String?
fileSize BigInt @default(0) fileSize BigInt @default(0)
fileName String? fileName String?
error String? error String?

View File

@ -130,7 +130,7 @@ export class SnapshotOrchestratorService implements OnModuleInit {
const now = Date.now(); const now = Date.now();
if (now - lastDbWriteTime >= 2000) { if (now - lastDbWriteTime >= 2000) {
lastDbWriteTime = now; lastDbWriteTime = now;
this.repo.updateDetailProgress(taskId, target, percent).catch(() => {}); this.repo.updateDetailProgress(taskId, target, percent, msg).catch(() => {});
} }
}); });

View File

@ -98,7 +98,7 @@ export class PostgresBackupHandler implements BackupHandler {
if (readMB > lastReportedMB) { if (readMB > lastReportedMB) {
lastReportedMB = readMB; lastReportedMB = readMB;
const totalMB = Math.floor(totalSize / (1024 * 1024)); const totalMB = Math.floor(totalSize / (1024 * 1024));
const percent = Math.min(99, Math.floor((bytesRead / totalSize) * 100)); const percent = Math.min(99, parseFloat(((bytesRead / totalSize) * 100).toFixed(2)));
onProgress(percent, `PostgreSQL 备份中... ${readMB}MB / ~${totalMB}MB`); onProgress(percent, `PostgreSQL 备份中... ${readMB}MB / ~${totalMB}MB`);
} }
}); });

View File

@ -79,14 +79,16 @@ export class SnapshotRepository {
return this.prisma.snapshotDetail.update({ where: { id: detail.id }, data }); return this.prisma.snapshotDetail.update({ where: { id: detail.id }, data });
} }
async updateDetailProgress(taskId: string, target: string, progress: number) { async updateDetailProgress(taskId: string, target: string, progress: number, progressMsg?: string) {
const detail = await this.prisma.snapshotDetail.findFirst({ const detail = await this.prisma.snapshotDetail.findFirst({
where: { taskId, target }, where: { taskId, target },
}); });
if (!detail) return; if (!detail) return;
const data: Record<string, unknown> = { progress };
if (progressMsg !== undefined) data.progressMsg = progressMsg;
return this.prisma.snapshotDetail.update({ return this.prisma.snapshotDetail.update({
where: { id: detail.id }, where: { id: detail.id },
data: { progress }, data,
}); });
} }

View File

@ -255,7 +255,7 @@ export default function SnapshotsPage() {
{BACKUP_TARGET_LABELS[p.target] || p.target} {BACKUP_TARGET_LABELS[p.target] || p.target}
</span> </span>
<span style={{ color: statusColor(p.status.toUpperCase() as SnapshotStatus) }}> <span style={{ color: statusColor(p.status.toUpperCase() as SnapshotStatus) }}>
{p.status === 'completed' ? '100%' : p.status === 'failed' ? '失败' : `${p.percent}%`} {p.status === 'completed' ? '100%' : p.status === 'failed' ? '失败' : `${p.percent.toFixed(1)}%`}
</span> </span>
</div> </div>
<div className={styles.progressBar}> <div className={styles.progressBar}>

View File

@ -48,7 +48,7 @@ export function useSnapshotPolling(taskId: string | null): UseSnapshotPollingRet
: d.status === 'COMPLETED' : d.status === 'COMPLETED'
? '完成' ? '完成'
: d.status === 'RUNNING' : d.status === 'RUNNING'
? `备份中... ${d.progress}%` ? (d.progressMsg || `备份中... ${d.progress.toFixed(1)}%`)
: '等待中', : '等待中',
status: statusMap[d.status] || 'pending', status: statusMap[d.status] || 'pending',
}); });

View File

@ -16,6 +16,7 @@ export interface SnapshotDetail {
target: BackupTarget; target: BackupTarget;
status: SnapshotStatus; status: SnapshotStatus;
progress: number; progress: number;
progressMsg: string | null;
fileSize: string; fileSize: string;
fileName: string | null; fileName: string | null;
error: string | null; error: string | null;

View File

@ -238,7 +238,7 @@ export default function SnapshotsPage() {
: 'text-blue-500' : 'text-blue-500'
} }
> >
{p.status === 'completed' ? '100%' : p.status === 'failed' ? '失败' : `${p.percent}%`} {p.status === 'completed' ? '100%' : p.status === 'failed' ? '失败' : `${p.percent.toFixed(1)}%`}
</span> </span>
</div> </div>
<div className="h-2 overflow-hidden rounded-full bg-muted"> <div className="h-2 overflow-hidden rounded-full bg-muted">

View File

@ -48,7 +48,7 @@ export function useSnapshotPolling(taskId: string | null): UseSnapshotPollingRet
: d.status === 'COMPLETED' : d.status === 'COMPLETED'
? '完成' ? '完成'
: d.status === 'RUNNING' : d.status === 'RUNNING'
? `备份中... ${d.progress}%` ? (d.progressMsg || `备份中... ${d.progress.toFixed(1)}%`)
: '等待中', : '等待中',
status: statusMap[d.status] || 'pending', status: statusMap[d.status] || 'pending',
}); });

View File

@ -16,6 +16,7 @@ export interface SnapshotDetail {
target: BackupTarget; target: BackupTarget;
status: SnapshotStatus; status: SnapshotStatus;
progress: number; progress: number;
progressMsg: string | null;
fileSize: string; fileSize: string;
fileName: string | null; fileName: string | null;
error: string | null; error: string | null;