461 lines
15 KiB
JavaScript
461 lines
15 KiB
JavaScript
const { defaultLogger } = require('../utils/logger');
|
|
const fs = require('fs-extra');
|
|
const path = require('path');
|
|
|
|
const logger = defaultLogger.child('RegistryMigration');
|
|
|
|
/**
|
|
* 注册表数据迁移服务
|
|
* 处理JSON和数据库之间的数据转换
|
|
*/
|
|
class RegistryMigration {
|
|
constructor(jsonRegistry, databaseRegistry, fileManager) {
|
|
this.jsonRegistry = jsonRegistry;
|
|
this.databaseRegistry = databaseRegistry;
|
|
this.fileManager = fileManager;
|
|
}
|
|
|
|
/**
|
|
* 从JSON迁移到数据库
|
|
* @param {boolean} overwrite 是否覆盖现有数据
|
|
* @returns {Object} 迁移结果
|
|
*/
|
|
async migrateJsonToDatabase(overwrite = true) {
|
|
logger.info('开始从JSON迁移数据到数据库...');
|
|
|
|
try {
|
|
// 确保JSON注册表已加载
|
|
if (!this.jsonRegistry.loaded) {
|
|
await this.jsonRegistry.init();
|
|
}
|
|
|
|
// 初始化数据库注册表
|
|
await this.databaseRegistry.init();
|
|
|
|
// 如果需要覆盖,先清空数据库
|
|
if (overwrite) {
|
|
logger.info('清空数据库中的现有数据...');
|
|
await this.clearDatabaseRegistry();
|
|
}
|
|
|
|
// 获取JSON数据
|
|
const jsonData = await this.jsonRegistry.exportRegistry();
|
|
if (!jsonData || !jsonData.artists) {
|
|
throw new Error('JSON注册表数据为空或格式不正确');
|
|
}
|
|
|
|
// 导入到数据库
|
|
const importResult = await this.databaseRegistry.importRegistry(jsonData);
|
|
|
|
const result = {
|
|
success: true,
|
|
direction: 'json-to-db',
|
|
recordsProcessed: importResult.addedArtworks,
|
|
addedArtists: importResult.addedArtists,
|
|
addedArtworks: importResult.addedArtworks,
|
|
skippedArtworks: importResult.skippedArtworks,
|
|
totalArtists: importResult.totalArtists,
|
|
totalArtworks: importResult.totalArtworks,
|
|
message: `成功从JSON迁移到数据库,处理了${importResult.addedArtists}个作者,${importResult.addedArtworks}个作品`
|
|
};
|
|
|
|
logger.info('JSON到数据库迁移完成', result);
|
|
return result;
|
|
|
|
} catch (error) {
|
|
logger.error('JSON到数据库迁移失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 从数据库迁移到JSON
|
|
* @param {boolean} overwrite 是否覆盖现有数据
|
|
* @returns {Object} 迁移结果
|
|
*/
|
|
async migrateDatabaseToJson(overwrite = true) {
|
|
logger.info('开始从数据库迁移数据到JSON...');
|
|
|
|
try {
|
|
// 确保数据库注册表已初始化
|
|
await this.databaseRegistry.init();
|
|
|
|
// 确保JSON注册表已加载
|
|
if (!this.jsonRegistry.loaded) {
|
|
await this.jsonRegistry.init();
|
|
}
|
|
|
|
// 获取数据库数据
|
|
const dbData = await this.databaseRegistry.exportRegistry();
|
|
if (!dbData || !dbData.artists) {
|
|
throw new Error('数据库注册表数据为空或格式不正确');
|
|
}
|
|
|
|
// 如果需要覆盖,先清空JSON注册表
|
|
if (overwrite) {
|
|
logger.info('清空JSON注册表中的现有数据...');
|
|
this.jsonRegistry.artists = {};
|
|
this.jsonRegistry.version = dbData.version || '1.0.5';
|
|
this.jsonRegistry.created_at = dbData.created_at || new Date().toISOString();
|
|
}
|
|
|
|
// 统计信息
|
|
let addedArtists = 0;
|
|
let addedArtworks = 0;
|
|
let skippedArtworks = 0;
|
|
|
|
// 导入数据到JSON注册表
|
|
for (const artistName in dbData.artists) {
|
|
const artistData = dbData.artists[artistName];
|
|
const artworks = artistData.artworks || [];
|
|
|
|
// 检查艺术家是否已存在
|
|
const isNewArtist = !this.jsonRegistry.artists[artistName];
|
|
if (isNewArtist) {
|
|
this.jsonRegistry.artists[artistName] = { artworks: [] };
|
|
addedArtists++;
|
|
}
|
|
|
|
// 获取现有作品ID
|
|
const existingArtworkIds = new Set(this.jsonRegistry.artists[artistName].artworks);
|
|
|
|
// 添加新作品
|
|
for (const artworkId of artworks) {
|
|
if (!existingArtworkIds.has(artworkId)) {
|
|
this.jsonRegistry.artists[artistName].artworks.push(artworkId);
|
|
addedArtworks++;
|
|
} else {
|
|
skippedArtworks++;
|
|
}
|
|
}
|
|
|
|
// 排序作品ID
|
|
this.jsonRegistry.artists[artistName].artworks.sort((a, b) => b - a);
|
|
}
|
|
|
|
// 更新时间戳
|
|
this.jsonRegistry.updated_at = new Date().toISOString();
|
|
|
|
// 保存JSON注册表
|
|
await this.jsonRegistry.save();
|
|
|
|
// 获取最终统计
|
|
const finalStats = await this.jsonRegistry.getStats();
|
|
|
|
const result = {
|
|
success: true,
|
|
direction: 'db-to-json',
|
|
recordsProcessed: addedArtworks,
|
|
addedArtists,
|
|
addedArtworks,
|
|
skippedArtworks,
|
|
totalArtists: finalStats.totalArtists,
|
|
totalArtworks: finalStats.totalArtworks,
|
|
message: `成功从数据库迁移到JSON,处理了${addedArtists}个作者,${addedArtworks}个作品`
|
|
};
|
|
|
|
logger.info('数据库到JSON迁移完成', result);
|
|
return result;
|
|
|
|
} catch (error) {
|
|
logger.error('数据库到JSON迁移失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 清空数据库注册表
|
|
*/
|
|
async clearDatabaseRegistry() {
|
|
try {
|
|
const db = this.databaseRegistry.db;
|
|
|
|
// 删除所有作品记录
|
|
await db.query('DELETE FROM registry_artworks');
|
|
|
|
// 删除所有艺术家记录
|
|
await db.query('DELETE FROM registry_artists');
|
|
|
|
// 重置自增ID
|
|
await db.query('ALTER TABLE registry_artists AUTO_INCREMENT = 1');
|
|
await db.query('ALTER TABLE registry_artworks AUTO_INCREMENT = 1');
|
|
|
|
logger.info('数据库注册表已清空');
|
|
} catch (error) {
|
|
logger.error('清空数据库注册表失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 比较JSON和数据库注册表的差异
|
|
* @returns {Object} 差异报告
|
|
*/
|
|
async compareRegistries() {
|
|
logger.info('开始比较JSON和数据库注册表...');
|
|
|
|
try {
|
|
// 确保两个注册表都已初始化
|
|
if (!this.jsonRegistry.loaded) {
|
|
await this.jsonRegistry.init();
|
|
}
|
|
await this.databaseRegistry.init();
|
|
|
|
// 获取统计信息
|
|
const jsonStats = await this.jsonRegistry.getStats();
|
|
const dbStats = await this.databaseRegistry.getStats();
|
|
|
|
// 获取艺术家列表
|
|
const jsonArtists = await this.jsonRegistry.getDownloadedArtists();
|
|
const dbArtists = await this.databaseRegistry.getDownloadedArtists();
|
|
|
|
// 计算差异
|
|
const jsonArtistSet = new Set(jsonArtists);
|
|
const dbArtistSet = new Set(dbArtists);
|
|
|
|
const onlyInJson = jsonArtists.filter(artist => !dbArtistSet.has(artist));
|
|
const onlyInDb = dbArtists.filter(artist => !jsonArtistSet.has(artist));
|
|
const commonArtists = jsonArtists.filter(artist => dbArtistSet.has(artist));
|
|
|
|
// 比较共同艺术家的作品差异
|
|
let artworkDifferences = [];
|
|
for (const artist of commonArtists.slice(0, 10)) { // 限制比较数量以避免性能问题
|
|
const jsonArtworks = await this.jsonRegistry.getArtistArtworks(artist);
|
|
const dbArtworks = await this.databaseRegistry.getArtistArtworks(artist);
|
|
|
|
const jsonArtworkSet = new Set(jsonArtworks);
|
|
const dbArtworkSet = new Set(dbArtworks);
|
|
|
|
const onlyInJsonArtworks = jsonArtworks.filter(id => !dbArtworkSet.has(id));
|
|
const onlyInDbArtworks = dbArtworks.filter(id => !jsonArtworkSet.has(id));
|
|
|
|
if (onlyInJsonArtworks.length > 0 || onlyInDbArtworks.length > 0) {
|
|
artworkDifferences.push({
|
|
artist,
|
|
onlyInJson: onlyInJsonArtworks.length,
|
|
onlyInDb: onlyInDbArtworks.length,
|
|
jsonTotal: jsonArtworks.length,
|
|
dbTotal: dbArtworks.length
|
|
});
|
|
}
|
|
}
|
|
|
|
const comparison = {
|
|
json: {
|
|
artists: jsonStats.totalArtists,
|
|
artworks: jsonStats.totalArtworks,
|
|
version: jsonStats.version,
|
|
updated_at: jsonStats.updated_at
|
|
},
|
|
database: {
|
|
artists: dbStats.artistCount,
|
|
artworks: dbStats.artworkCount,
|
|
version: dbStats.version,
|
|
updated_at: dbStats.updated_at
|
|
},
|
|
differences: {
|
|
artistsOnlyInJson: onlyInJson.length,
|
|
artistsOnlyInDb: onlyInDb.length,
|
|
commonArtists: commonArtists.length,
|
|
artworkDifferences: artworkDifferences.length,
|
|
sampleArtworkDifferences: artworkDifferences.slice(0, 5)
|
|
},
|
|
recommendation: this.getRecommendation(jsonStats, dbStats, onlyInJson, onlyInDb)
|
|
};
|
|
|
|
logger.info('注册表比较完成', {
|
|
jsonArtists: jsonStats.totalArtists,
|
|
dbArtists: dbStats.artistCount,
|
|
differences: comparison.differences
|
|
});
|
|
|
|
return comparison;
|
|
|
|
} catch (error) {
|
|
logger.error('比较注册表失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 根据比较结果给出建议
|
|
*/
|
|
getRecommendation(jsonStats, dbStats, onlyInJson, onlyInDb) {
|
|
if (jsonStats.totalArtworks === 0 && dbStats.artworkCount === 0) {
|
|
return '两个注册表都为空,无需迁移';
|
|
}
|
|
|
|
if (jsonStats.totalArtworks === 0) {
|
|
return '建议从数据库迁移到JSON';
|
|
}
|
|
|
|
if (dbStats.artworkCount === 0) {
|
|
return '建议从JSON迁移到数据库';
|
|
}
|
|
|
|
if (jsonStats.totalArtworks > dbStats.artworkCount) {
|
|
return 'JSON注册表包含更多数据,建议从JSON迁移到数据库';
|
|
}
|
|
|
|
if (dbStats.artworkCount > jsonStats.totalArtworks) {
|
|
return '数据库注册表包含更多数据,建议从数据库迁移到JSON';
|
|
}
|
|
|
|
if (onlyInJson.length > onlyInDb.length) {
|
|
return 'JSON注册表有更多独有艺术家,建议从JSON迁移到数据库';
|
|
}
|
|
|
|
if (onlyInDb.length > onlyInJson.length) {
|
|
return '数据库注册表有更多独有艺术家,建议从数据库迁移到JSON';
|
|
}
|
|
|
|
return '两个注册表数据相似,可根据需要选择迁移方向';
|
|
}
|
|
|
|
/**
|
|
* 验证迁移结果
|
|
* @param {string} direction 迁移方向
|
|
* @returns {Object} 验证结果
|
|
*/
|
|
async validateMigration(direction) {
|
|
logger.info(`验证${direction}迁移结果...`);
|
|
|
|
try {
|
|
const comparison = await this.compareRegistries();
|
|
|
|
let isValid = false;
|
|
let message = '';
|
|
|
|
if (direction === 'json-to-db') {
|
|
// 验证数据库是否包含JSON的所有数据
|
|
isValid = comparison.database.artworks >= comparison.json.artworks &&
|
|
comparison.differences.artistsOnlyInJson === 0;
|
|
message = isValid ?
|
|
'迁移验证成功:数据库包含了JSON的所有数据' :
|
|
'迁移验证失败:数据库缺少部分JSON数据';
|
|
} else if (direction === 'db-to-json') {
|
|
// 验证JSON是否包含数据库的所有数据
|
|
isValid = comparison.json.artworks >= comparison.database.artworks &&
|
|
comparison.differences.artistsOnlyInDb === 0;
|
|
message = isValid ?
|
|
'迁移验证成功:JSON包含了数据库的所有数据' :
|
|
'迁移验证失败:JSON缺少部分数据库数据';
|
|
}
|
|
|
|
const result = {
|
|
valid: isValid,
|
|
message,
|
|
comparison
|
|
};
|
|
|
|
logger.info('迁移验证完成', { valid: isValid, message });
|
|
return result;
|
|
|
|
} catch (error) {
|
|
logger.error('验证迁移结果失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 创建数据备份
|
|
* @param {string} type 备份类型 ('json' | 'database')
|
|
* @returns {string} 备份文件路径
|
|
*/
|
|
async createBackup(type) {
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
|
|
// 检测是否在pkg打包环境中运行
|
|
const isPkg = process.pkg !== undefined;
|
|
|
|
const backupDir = isPkg
|
|
? path.join(process.cwd(), 'data', 'backups', 'registry') // 打包环境:当前工作目录的data文件夹
|
|
: path.join(__dirname, '..', '..', 'backups', 'registry'); // 开发环境:项目根目录的backups文件夹
|
|
|
|
await fs.ensureDir(backupDir);
|
|
|
|
try {
|
|
if (type === 'json') {
|
|
// 备份JSON注册表
|
|
const jsonData = await this.jsonRegistry.exportRegistry();
|
|
const backupPath = path.join(backupDir, `registry-json-backup-${timestamp}.json`);
|
|
|
|
await fs.writeJson(backupPath, jsonData, { spaces: 2 });
|
|
logger.info(`JSON注册表备份已创建: ${backupPath}`);
|
|
return backupPath;
|
|
|
|
} else if (type === 'database') {
|
|
// 备份数据库注册表
|
|
const dbData = await this.databaseRegistry.exportRegistry();
|
|
const backupPath = path.join(backupDir, `registry-db-backup-${timestamp}.json`);
|
|
|
|
await fs.writeJson(backupPath, dbData, { spaces: 2 });
|
|
logger.info(`数据库注册表备份已创建: ${backupPath}`);
|
|
return backupPath;
|
|
}
|
|
|
|
throw new Error(`不支持的备份类型: ${type}`);
|
|
|
|
} catch (error) {
|
|
logger.error(`创建${type}备份失败:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 执行完整的迁移流程(包含备份和验证)
|
|
* @param {string} direction 迁移方向
|
|
* @param {boolean} overwrite 是否覆盖
|
|
* @param {boolean} createBackup 是否创建备份
|
|
* @returns {Object} 迁移结果
|
|
*/
|
|
async performMigration(direction, overwrite = true, createBackup = true) {
|
|
logger.info(`开始执行完整迁移流程: ${direction}`);
|
|
|
|
const result = {
|
|
success: false,
|
|
direction,
|
|
backupPath: null,
|
|
migrationResult: null,
|
|
validationResult: null,
|
|
error: null
|
|
};
|
|
|
|
try {
|
|
// 创建备份
|
|
if (createBackup) {
|
|
const backupType = direction === 'json-to-db' ? 'database' : 'json';
|
|
result.backupPath = await this.createBackup(backupType);
|
|
}
|
|
|
|
// 执行迁移
|
|
if (direction === 'json-to-db') {
|
|
result.migrationResult = await this.migrateJsonToDatabase(overwrite);
|
|
} else if (direction === 'db-to-json') {
|
|
result.migrationResult = await this.migrateDatabaseToJson(overwrite);
|
|
} else {
|
|
throw new Error(`不支持的迁移方向: ${direction}`);
|
|
}
|
|
|
|
// 验证迁移结果
|
|
result.validationResult = await this.validateMigration(direction);
|
|
|
|
result.success = result.migrationResult.success && result.validationResult.valid;
|
|
|
|
logger.info('完整迁移流程完成', {
|
|
direction,
|
|
success: result.success,
|
|
recordsProcessed: result.migrationResult.recordsProcessed
|
|
});
|
|
|
|
return result;
|
|
|
|
} catch (error) {
|
|
result.error = error.message;
|
|
logger.error('完整迁移流程失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = RegistryMigration; |