feat(backup): 实现备份恢复进度实时显示功能

This commit is contained in:
zhang1106
2026-03-17 11:19:15 +08:00
parent 124cca65ba
commit 971ecd62d6
6 changed files with 589 additions and 341 deletions
+99 -76
View File
@@ -8,9 +8,26 @@ const path = require('path');
const crypto = require('crypto');
const zlib = require('zlib');
const { pipeline } = require('stream/promises');
const { sequelize, dbDialect } = require('../db');
const BACKUP_VERSION = '2.0.0';
async function disableForeignKeyChecks() {
if (dbDialect === 'sqlite') {
await sequelize.query('PRAGMA foreign_keys = OFF');
} else if (dbDialect === 'mysql') {
await sequelize.query('SET FOREIGN_KEY_CHECKS = 0');
}
}
async function enableForeignKeyChecks() {
if (dbDialect === 'sqlite') {
await sequelize.query('PRAGMA foreign_keys = ON');
} else if (dbDialect === 'mysql') {
await sequelize.query('SET FOREIGN_KEY_CHECKS = 1');
}
}
// 数据表名称中英文映射
const TABLE_NAME_MAPPING = {
'User': '用户',
@@ -684,99 +701,105 @@ async function restoreData(backupData, options = {}) {
return results;
}
for (const tableName of RESTORE_ORDER) {
if (skipTables.includes(tableName)) {
results.skipped.push(tableName);
onProgress(tableName, 'skipped');
continue;
}
await disableForeignKeyChecks();
const tableData = dataToRestore[tableName];
if (!tableData || !Array.isArray(tableData) || tableData.length === 0) {
onProgress(tableName, 'empty');
continue;
}
const config = BACKUP_MODELS_CONFIG.find(c => c.name === tableName);
if (!config) {
results.errors.push({ table: tableName, error: '未找到模型配置' });
continue;
}
try {
const Model = require(config.modelPath);
if (overwriteExisting) {
await Model.destroy({ where: {}, truncate: true });
try {
for (const tableName of RESTORE_ORDER) {
if (skipTables.includes(tableName)) {
results.skipped.push(tableName);
onProgress(tableName, 'skipped');
continue;
}
const processedRecords = tableData.map(record => {
const processed = { ...record };
const tableData = dataToRestore[tableName];
if (!tableData || !Array.isArray(tableData) || tableData.length === 0) {
onProgress(tableName, 'empty');
continue;
}
const config = BACKUP_MODELS_CONFIG.find(c => c.name === tableName);
if (!config) {
results.errors.push({ table: tableName, error: '未找到模型配置' });
continue;
}
try {
const Model = require(config.modelPath);
if (tableName === 'Device' && processed.customFields !== undefined && processed.customFields !== null) {
if (typeof processed.customFields === 'string') {
try {
processed.customFields = JSON.parse(processed.customFields);
} catch (e) {
console.warn(`解析 Device.customFields 失败:${processed.deviceId}, 错误:${e.message}`);
processed.customFields = {};
if (overwriteExisting) {
await Model.destroy({ where: {}, truncate: true });
}
const processedRecords = tableData.map(record => {
const processed = { ...record };
if (tableName === 'Device' && processed.customFields !== undefined && processed.customFields !== null) {
if (typeof processed.customFields === 'string') {
try {
processed.customFields = JSON.parse(processed.customFields);
} catch (e) {
console.warn(`解析 Device.customFields 失败:${processed.deviceId}, 错误:${e.message}`);
processed.customFields = {};
}
}
}
}
return processed;
});
return processed;
});
let insertedCount = 0;
for (const record of processedRecords) {
try {
await Model.create(record, { validate: false, silent: true });
insertedCount++;
} catch (insertError) {
if (insertError.name === 'SequelizeUniqueConstraintError') {
try {
await Model.upsert(record, { validate: false, silent: true });
insertedCount++;
} catch (upsertError) {
let insertedCount = 0;
for (const record of processedRecords) {
try {
await Model.create(record, { validate: false, silent: true });
insertedCount++;
} catch (insertError) {
if (insertError.name === 'SequelizeUniqueConstraintError') {
try {
await Model.upsert(record, { validate: false, silent: true });
insertedCount++;
} catch (upsertError) {
results.errors.push({
table: tableName,
record: record[Object.keys(record)[0]],
error: upsertError.message,
});
}
} else {
results.errors.push({
table: tableName,
record: record[Object.keys(record)[0]],
error: upsertError.message,
error: insertError.message,
});
}
} else {
results.errors.push({
table: tableName,
record: record[Object.keys(record)[0]],
error: insertError.message,
});
}
}
results.tablesRestored++;
results.recordsRestored += insertedCount;
results.tableDetails[tableName] = {
recordCount: insertedCount,
displayName: TABLE_NAME_MAPPING[tableName] || tableName,
success: insertedCount > 0,
};
onProgress(tableName, 'restored', insertedCount);
} catch (error) {
results.errors.push({ table: tableName, error: error.message });
onProgress(tableName, 'error', error.message);
}
results.tablesRestored++;
results.recordsRestored += insertedCount;
results.tableDetails[tableName] = {
recordCount: insertedCount,
displayName: TABLE_NAME_MAPPING[tableName] || tableName,
success: insertedCount > 0,
};
onProgress(tableName, 'restored', insertedCount);
} catch (error) {
results.errors.push({ table: tableName, error: error.message });
onProgress(tableName, 'error', error.message);
}
}
if (isIncremental && backupData.incrementalData) {
console.log('\n恢复增量数据...');
const incrementalResults = await restoreIncrementalData(backupData.incrementalData, options);
results.tablesRestored += incrementalResults.tablesRestored;
results.recordsRestored += incrementalResults.recordsRestored;
results.errors.push(...incrementalResults.errors);
Object.assign(results.tableDetails, incrementalResults.tableDetails);
if (isIncremental && backupData.incrementalData) {
console.log('\n恢复增量数据...');
const incrementalResults = await restoreIncrementalData(backupData.incrementalData, options);
results.tablesRestored += incrementalResults.tablesRestored;
results.recordsRestored += incrementalResults.recordsRestored;
results.errors.push(...incrementalResults.errors);
Object.assign(results.tableDetails, incrementalResults.tableDetails);
}
} finally {
await enableForeignKeyChecks();
}
return results;