From 47c68cadf3e6f0bf3601ff3bf774f8bf8ba2f6ab Mon Sep 17 00:00:00 2001 From: kjqwer <2990346238@qq.com> Date: Sat, 11 Oct 2025 11:56:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B3=A8=E5=86=8C=E8=A1=A8=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E5=AD=98=E5=82=A8=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ry-db-backup-2025-10-11T02-48-22-620Z.json | 7 + backend/config/cache-config.js | 4 +- backend/config/database.json | 9 + backend/core.js | 49 +- backend/database/database-manager.js | 339 +++++++ backend/database/registry-database.js | 667 ++++++++++++++ backend/database/registry-schema.js | 241 +++++ backend/routes/database.js | 670 ++++++++++++++ backend/routes/download.js | 54 +- backend/routes/index.js | 2 + backend/services/download-registry.js | 311 ++++--- backend/services/download.js | 5 +- backend/services/registry-migration.js | 461 ++++++++++ backend/utils/logger.js | 3 + package.json | 1 + pnpm-lock.yaml | 73 ++ ui/src/assets/icons/navigation.ts | 4 +- ui/src/assets/icons/viewbox-config.ts | 1 + .../components/common/DatabaseConfigModal.vue | 831 ++++++++++++++++++ ui/src/components/common/RegistryWidget.vue | 478 +++++++++- ui/src/services/database.ts | 239 +++++ ui/src/stores/registry.ts | 82 +- ui/src/stores/update.ts | 25 +- 23 files changed, 4396 insertions(+), 160 deletions(-) create mode 100644 backend/backups/registry/registry-db-backup-2025-10-11T02-48-22-620Z.json create mode 100644 backend/config/database.json create mode 100644 backend/database/database-manager.js create mode 100644 backend/database/registry-database.js create mode 100644 backend/database/registry-schema.js create mode 100644 backend/routes/database.js create mode 100644 backend/services/registry-migration.js create mode 100644 ui/src/components/common/DatabaseConfigModal.vue create mode 100644 ui/src/services/database.ts diff --git a/backend/backups/registry/registry-db-backup-2025-10-11T02-48-22-620Z.json b/backend/backups/registry/registry-db-backup-2025-10-11T02-48-22-620Z.json new file mode 100644 index 0000000..d11f47c --- /dev/null +++ b/backend/backups/registry/registry-db-backup-2025-10-11T02-48-22-620Z.json @@ -0,0 +1,7 @@ +{ + "version": "1.0.5", + "created_at": "2025-10-11T02:10:36.567Z", + "updated_at": "2025-10-11T02:48:22.684Z", + "exported_at": "2025-10-11T02:48:22.684Z", + "artists": {} +} diff --git a/backend/config/cache-config.js b/backend/config/cache-config.js index 6f54260..3bdb7d2 100644 --- a/backend/config/cache-config.js +++ b/backend/config/cache-config.js @@ -39,7 +39,7 @@ class CacheConfigManager { retryDelay: 1000, }, allowedExtensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'], - // 新增并发下载配置 + // 下载检测配置 download: { concurrentDownloads: 3, // 同时下载任务数 maxConcurrentFiles: 5, // 单个任务内最大并发文件数 @@ -52,6 +52,8 @@ class CacheConfigManager { useRegistryCheck: true, // 是否使用注册表检测(默认启用) fallbackToScan: false, // 检测失败时是否回退到扫盘检测 maxFileSize: 50 * 1024 * 1024, // 最大文件大小 50MB + // 存储模式配置 + storageMode: 'json', // 存储模式:'json' 或 'database' }, // 新增Windows特定配置 windows: { diff --git a/backend/config/database.json b/backend/config/database.json new file mode 100644 index 0000000..de24b14 --- /dev/null +++ b/backend/config/database.json @@ -0,0 +1,9 @@ +{ + "host": "sywb.top", + "port": 3306, + "user": "pixiv", + "password": "yT6LYysxB4HPPkZc", + "database": "pixiv", + "connectionLimit": 10, + "ssl": false +} \ No newline at end of file diff --git a/backend/core.js b/backend/core.js index 1a0a79b..e5b84e9 100644 --- a/backend/core.js +++ b/backend/core.js @@ -29,6 +29,7 @@ class PixivBackend { this.auth = null; this.isLoggedIn = false; this.downloadService = null; + this.databaseManager = null; } /** @@ -41,6 +42,9 @@ class PixivBackend { this.initConfig(); this.config = this.readConfig(); + // 自动加载数据库配置 + await this.initDatabaseConfig(); + // 创建认证实例,传入代理配置 this.auth = new PixivAuth(this.config.proxy); @@ -63,7 +67,7 @@ class PixivBackend { } // 创建下载服务实例 - this.downloadService = new DownloadService(this.auth); + this.downloadService = new DownloadService(this.auth, this.databaseManager); await this.downloadService.init(); // 检查登录状态 @@ -335,6 +339,49 @@ class PixivBackend { } } + /** + * 初始化数据库配置 + */ + async initDatabaseConfig() { + try { + const fs = require('fs-extra'); + const path = require('path'); + + // 检测是否在pkg打包环境中运行 + const isPkg = process.pkg !== undefined; + + const configPath = isPkg + ? path.join(process.cwd(), 'data', 'database.json') // 打包环境:当前工作目录的data文件夹 + : path.join(__dirname, '..', 'data', 'database.json'); // 开发环境:项目根目录的data文件夹 + + if (await fs.pathExists(configPath)) { + const config = await fs.readJson(configPath); + logger.info('检测到数据库配置文件,正在初始化数据库连接...'); + + // 动态导入数据库管理器 + const DatabaseManager = require('./database/database-manager'); + const RegistryDatabase = require('./database/registry-database'); + + // 创建并初始化数据库管理器 + this.databaseManager = new DatabaseManager(); + await this.databaseManager.init(config); + + // 初始化注册表数据库 + const registryDatabase = new RegistryDatabase(this.databaseManager); + + // 将实例设置到数据库路由模块中 + const databaseRoute = require('./routes/database'); + databaseRoute.setDatabaseInstances(this.databaseManager, registryDatabase); + + logger.info('数据库连接已自动初始化'); + } else { + logger.info('未检测到数据库配置文件,跳过数据库初始化'); + } + } catch (error) { + logger.error('初始化数据库配置时出错:', error.message); + } + } + /** * 获取下载服务实例 */ diff --git a/backend/database/database-manager.js b/backend/database/database-manager.js new file mode 100644 index 0000000..4f449b4 --- /dev/null +++ b/backend/database/database-manager.js @@ -0,0 +1,339 @@ +const mysql = require('mysql2/promise'); +const { defaultLogger } = require('../utils/logger'); + +const logger = defaultLogger.child('DatabaseManager'); + +/** + * 数据库连接管理器 + * 提供MySQL连接管理和基础CRUD操作 + */ +class DatabaseManager { + constructor() { + this.pool = null; + this.config = null; + this.isConnected = false; + } + + /** + * 初始化数据库连接 + * @param {Object} config 数据库配置 + * @param {string} config.host 主机地址 + * @param {number} config.port 端口号 + * @param {string} config.user 用户名 + * @param {string} config.password 密码 + * @param {string} config.database 数据库名 + */ + async init(config) { + try { + this.config = { + host: config.host || 'localhost', + port: config.port || 3306, + user: config.user, + password: config.password, + database: config.database, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, + charset: 'utf8mb4' + }; + + // 创建连接池 + this.pool = mysql.createPool(this.config); + + // 测试连接 + await this.testConnection(); + + this.isConnected = true; + logger.info('数据库连接初始化成功'); + + return { success: true }; + } catch (error) { + logger.error('数据库连接初始化失败:', error); + this.isConnected = false; + throw error; + } + } + + /** + * 测试数据库连接 + */ + async testConnection() { + if (!this.pool) { + throw new Error('数据库连接池未初始化'); + } + + try { + const connection = await this.pool.getConnection(); + await connection.ping(); + connection.release(); + logger.info('数据库连接测试成功'); + return { success: true, message: '数据库连接正常' }; + } catch (error) { + logger.error('数据库连接测试失败:', error); + throw new Error(`数据库连接失败: ${error.message}`); + } + } + + /** + * 执行SQL查询 + * @param {string} sql SQL语句 + * @param {Array} params 参数 + */ + async query(sql, params = []) { + if (!this.pool) { + throw new Error('数据库连接池未初始化'); + } + + try { + const [rows, fields] = await this.pool.execute(sql, params); + return { success: true, data: rows, fields }; + } catch (error) { + logger.error('SQL查询执行失败:', { sql, params, error: error.message }); + throw error; + } + } + + /** + * 执行事务 + * @param {Function} callback 事务回调函数 + */ + async transaction(callback) { + if (!this.pool) { + throw new Error('数据库连接池未初始化'); + } + + const connection = await this.pool.getConnection(); + + try { + await connection.beginTransaction(); + + const result = await callback(connection); + + await connection.commit(); + connection.release(); + + return result; + } catch (error) { + await connection.rollback(); + connection.release(); + logger.error('事务执行失败:', error); + throw error; + } + } + + /** + * 插入数据 + * @param {string} table 表名 + * @param {Object} data 数据对象 + */ + async insert(table, data) { + const keys = Object.keys(data); + const values = Object.values(data); + const placeholders = keys.map(() => '?').join(', '); + + const sql = `INSERT INTO ${table} (${keys.join(', ')}) VALUES (${placeholders})`; + + try { + const result = await this.query(sql, values); + return { + success: true, + insertId: result.data.insertId, + affectedRows: result.data.affectedRows + }; + } catch (error) { + logger.error('插入数据失败:', { table, data, error: error.message }); + throw error; + } + } + + /** + * 更新数据 + * @param {string} table 表名 + * @param {Object} data 更新数据 + * @param {Object} where 条件 + */ + async update(table, data, where) { + const setClause = Object.keys(data).map(key => `${key} = ?`).join(', '); + const whereClause = Object.keys(where).map(key => `${key} = ?`).join(' AND '); + + const sql = `UPDATE ${table} SET ${setClause} WHERE ${whereClause}`; + const params = [...Object.values(data), ...Object.values(where)]; + + try { + const result = await this.query(sql, params); + return { + success: true, + affectedRows: result.data.affectedRows, + changedRows: result.data.changedRows + }; + } catch (error) { + logger.error('更新数据失败:', { table, data, where, error: error.message }); + throw error; + } + } + + /** + * 删除数据 + * @param {string} table 表名 + * @param {Object} where 条件 + */ + async delete(table, where) { + const whereClause = Object.keys(where).map(key => `${key} = ?`).join(' AND '); + const sql = `DELETE FROM ${table} WHERE ${whereClause}`; + const params = Object.values(where); + + try { + const result = await this.query(sql, params); + return { + success: true, + affectedRows: result.data.affectedRows + }; + } catch (error) { + logger.error('删除数据失败:', { table, where, error: error.message }); + throw error; + } + } + + /** + * 查询数据 + * @param {string} table 表名 + * @param {Object} where 条件 + * @param {Object} options 选项 + */ + async select(table, where = {}, options = {}) { + let sql = `SELECT * FROM ${table}`; + let params = []; + + if (Object.keys(where).length > 0) { + const whereClause = Object.keys(where).map(key => `${key} = ?`).join(' AND '); + sql += ` WHERE ${whereClause}`; + params = Object.values(where); + } + + if (options.orderBy) { + sql += ` ORDER BY ${options.orderBy}`; + } + + if (options.limit) { + sql += ` LIMIT ${options.limit}`; + } + + if (options.offset) { + sql += ` OFFSET ${options.offset}`; + } + + try { + const result = await this.query(sql, params); + return { + success: true, + data: result.data + }; + } catch (error) { + logger.error('查询数据失败:', { table, where, options, error: error.message }); + throw error; + } + } + + /** + * 检查表是否存在 + * @param {string} tableName 表名 + */ + async tableExists(tableName) { + try { + const sql = ` + SELECT COUNT(*) as count + FROM information_schema.tables + WHERE table_schema = ? AND table_name = ? + `; + const result = await this.query(sql, [this.config.database, tableName]); + return result.data[0].count > 0; + } catch (error) { + logger.error('检查表是否存在失败:', { tableName, error: error.message }); + return false; + } + } + + /** + * 创建表 + * @param {string} tableName 表名 + * @param {string} createSQL 创建表的SQL语句 + */ + async createTable(tableName, createSQL) { + try { + await this.query(createSQL); + logger.info(`表 ${tableName} 创建成功`); + return { success: true }; + } catch (error) { + logger.error(`创建表 ${tableName} 失败:`, error); + throw error; + } + } + + /** + * 批量插入数据 + * @param {string} table 表名 + * @param {Array} dataArray 数据数组 + */ + async batchInsert(table, dataArray) { + if (!dataArray || dataArray.length === 0) { + return { success: true, affectedRows: 0 }; + } + + const keys = Object.keys(dataArray[0]); + const placeholders = keys.map(() => '?').join(', '); + const sql = `INSERT INTO ${table} (${keys.join(', ')}) VALUES (${placeholders})`; + + try { + return await this.transaction(async (connection) => { + let totalAffectedRows = 0; + + for (const data of dataArray) { + const values = keys.map(key => data[key]); + const [result] = await connection.execute(sql, values); + totalAffectedRows += result.affectedRows; + } + + return { + success: true, + affectedRows: totalAffectedRows + }; + }); + } catch (error) { + logger.error('批量插入数据失败:', { table, count: dataArray.length, error: error.message }); + throw error; + } + } + + /** + * 关闭数据库连接 + */ + async close() { + if (this.pool) { + try { + await this.pool.end(); + this.pool = null; + this.isConnected = false; + logger.info('数据库连接已关闭'); + } catch (error) { + logger.error('关闭数据库连接失败:', error); + } + } + } + + /** + * 获取连接状态 + */ + getConnectionStatus() { + return { + isConnected: this.isConnected, + config: this.config ? { + host: this.config.host, + port: this.config.port, + user: this.config.user, + database: this.config.database + } : null + }; + } +} + +module.exports = DatabaseManager; \ No newline at end of file diff --git a/backend/database/registry-database.js b/backend/database/registry-database.js new file mode 100644 index 0000000..1301058 --- /dev/null +++ b/backend/database/registry-database.js @@ -0,0 +1,667 @@ +const { defaultLogger } = require('../utils/logger'); +const RegistrySchema = require('./registry-schema'); + +const logger = defaultLogger.child('RegistryDatabase'); + +/** + * 数据库版本的下载注册表 + * 提供与JSON版本相同的接口,但使用MySQL数据库存储 + */ +class RegistryDatabase { + constructor(databaseManager) { + this.db = databaseManager; + this.schema = new RegistrySchema(databaseManager); + this.loaded = false; + } + + /** + * 初始化数据库注册表 + */ + async init() { + try { + // 初始化数据库表结构 + await this.schema.initializeTables(); + this.loaded = true; + + const stats = await this.getStats(); + logger.info(`数据库注册表初始化完成,总共包含${stats.artistCount}个作者,${stats.artworkCount}个作品`); + + return { success: true }; + } catch (error) { + logger.error('数据库注册表初始化失败:', error); + throw error; + } + } + + /** + * 标准化作者名称 + */ + normalizeArtistName(artistName) { + if (!artistName || typeof artistName !== 'string') { + return 'Unknown Artist'; + } + return artistName.trim(); + } + + /** + * 获取或创建艺术家记录 + * @param {string} artistName 艺术家名称 + * @returns {number} 艺术家ID + */ + async getOrCreateArtist(artistName) { + const normalizedName = this.normalizeArtistName(artistName); + + try { + // 先尝试查找现有艺术家 + const existingResult = await this.db.select('registry_artists', { + normalized_name: normalizedName + }); + + if (existingResult.data.length > 0) { + return existingResult.data[0].id; + } + + // 创建新艺术家 + const insertResult = await this.db.insert('registry_artists', { + artist_name: artistName, + normalized_name: normalizedName, + artwork_count: 0 + }); + + return insertResult.insertId; + } catch (error) { + logger.error('获取或创建艺术家失败:', { artistName, error: error.message }); + throw error; + } + } + + /** + * 添加作品到注册表 + * @param {string} artistName 艺术家名称 + * @param {number} artworkId 作品ID + * @param {string} filePath 文件路径(可选) + */ + async addArtwork(artistName, artworkId, filePath = null) { + try { + const normalizedArtworkId = parseInt(artworkId); + if (!normalizedArtworkId) { + throw new Error('无效的作品ID'); + } + + // 获取或创建艺术家 + const artistId = await this.getOrCreateArtist(artistName); + + // 检查作品是否已存在 + const existingResult = await this.db.select('registry_artworks', { + artist_id: artistId, + artwork_id: normalizedArtworkId + }); + + if (existingResult.data.length > 0) { + logger.debug('作品已存在于注册表中', { artistName, artworkId: normalizedArtworkId }); + return; + } + + // 添加作品记录 + await this.db.insert('registry_artworks', { + artist_id: artistId, + artwork_id: normalizedArtworkId, + artist_name: artistName, + file_path: filePath, + download_date: new Date() + }); + + // 更新艺术家作品数量 + await this.updateArtistArtworkCount(artistId); + + logger.debug('成功添加作品到注册表', { artistName, artworkId: normalizedArtworkId }); + } catch (error) { + logger.error('添加作品到注册表失败:', { artistName, artworkId, error: error.message }); + throw error; + } + } + + /** + * 从注册表移除作品 + * @param {string} artistName 艺术家名称 + * @param {number} artworkId 作品ID + */ + async removeArtwork(artistName, artworkId) { + try { + const normalizedArtworkId = parseInt(artworkId); + const normalizedArtistName = this.normalizeArtistName(artistName); + + // 查找艺术家 + const artistResult = await this.db.select('registry_artists', { + normalized_name: normalizedArtistName + }); + + if (artistResult.data.length === 0) { + logger.warn('艺术家在注册表中未找到', { artistName: normalizedArtistName }); + return; + } + + const artistId = artistResult.data[0].id; + + // 删除作品记录 + const deleteResult = await this.db.delete('registry_artworks', { + artist_id: artistId, + artwork_id: normalizedArtworkId + }); + + if (deleteResult.affectedRows > 0) { + // 更新艺术家作品数量 + await this.updateArtistArtworkCount(artistId); + logger.debug('成功移除作品记录', { artistName: normalizedArtistName, artworkId: normalizedArtworkId }); + } else { + logger.warn('作品在注册表中未找到', { artistName: normalizedArtistName, artworkId: normalizedArtworkId }); + } + } catch (error) { + logger.error('移除作品记录失败:', { artistName, artworkId, error: error.message }); + throw error; + } + } + + /** + * 更新艺术家作品数量 + * @param {number} artistId 艺术家ID + */ + async updateArtistArtworkCount(artistId) { + try { + const countResult = await this.db.query( + 'SELECT COUNT(*) as count FROM registry_artworks WHERE artist_id = ?', + [artistId] + ); + + const count = countResult.data[0].count; + + await this.db.update('registry_artists', + { artwork_count: count }, + { id: artistId } + ); + } catch (error) { + logger.error('更新艺术家作品数量失败:', { artistId, error: error.message }); + } + } + + /** + * 检查作品是否已下载 + * @param {number|string} artworkId - 作品ID + * @returns {boolean} 是否已下载 + */ + async isArtworkDownloaded(artworkId) { + try { + const normalizedArtworkId = parseInt(artworkId); + if (!normalizedArtworkId) { + return false; + } + + const result = await this.db.query(` + SELECT COUNT(*) as count + FROM registry_artworks + WHERE artwork_id = ? + `, [normalizedArtworkId]); + + return result.data[0].count > 0; + } catch (error) { + logger.error('检查作品下载状态失败:', { artworkId, error: error.message }); + return false; + } + } + + /** + * 检查作品是否已在注册表中注册 + * @param {string} artistName 艺术家名称 + * @param {string} artworkDir 作品目录名或作品ID + * @returns {boolean} 是否已注册 + */ + async isArtworkRegistered(artistName, artworkDir) { + try { + // 从目录名提取作品ID + let artworkId; + if (typeof artworkDir === 'string' && isNaN(artworkDir)) { + // 如果是目录名,需要提取ID + const artworkUtils = require('../utils/artwork-utils'); + artworkId = await artworkUtils.extractArtworkIdFromDir(artworkDir); + } else { + artworkId = parseInt(artworkDir); + } + + if (!artworkId) { + logger.warn(`无法从作品目录名中提取作品ID: ${artworkDir}`); + return false; + } + + const normalizedArtistName = this.normalizeArtistName(artistName); + + // 查询数据库 + const result = await this.db.query(` + SELECT COUNT(*) as count + FROM registry_artworks ra + JOIN registry_artists rt ON ra.artist_id = rt.id + WHERE rt.normalized_name = ? AND ra.artwork_id = ? + `, [normalizedArtistName, artworkId]); + + return result.data[0].count > 0; + } catch (error) { + logger.error('检查作品注册状态失败:', { artistName, artworkDir, error: error.message }); + return false; + } + } + + /** + * 获取已下载的作品ID列表 + * @returns {number[]} 作品ID数组 + */ + async getDownloadedArtworkIds() { + try { + const result = await this.db.query(` + SELECT DISTINCT artwork_id + FROM registry_artworks + ORDER BY artwork_id DESC + `); + + return result.data.map(row => row.artwork_id); + } catch (error) { + logger.error('获取已下载作品ID列表失败:', error); + return []; + } + } + + /** + * 获取指定作者的已下载作品 + * @param {string} artistName 作者名称 + * @returns {number[]} 作品ID数组 + */ + async getArtistArtworks(artistName) { + try { + const normalizedArtistName = this.normalizeArtistName(artistName); + + const result = await this.db.query(` + SELECT ra.artwork_id + FROM registry_artworks ra + JOIN registry_artists rt ON ra.artist_id = rt.id + WHERE rt.normalized_name = ? + ORDER BY ra.artwork_id DESC + `, [normalizedArtistName]); + + return result.data.map(row => row.artwork_id); + } catch (error) { + logger.error('获取艺术家作品列表失败:', { artistName, error: error.message }); + return []; + } + } + + /** + * 获取所有已下载的作者列表 + * @returns {string[]} 作者名称数组 + */ + async getDownloadedArtists() { + try { + const result = await this.db.query(` + SELECT DISTINCT artist_name + FROM registry_artists + WHERE artwork_count > 0 + ORDER BY artist_name + `); + + return result.data.map(row => row.artist_name); + } catch (error) { + logger.error('获取已下载作者列表失败:', error); + return []; + } + } + + /** + * 获取统计信息 + */ + async getStats() { + try { + // 获取艺术家数量 + const artistCountResult = await this.db.query(` + SELECT COUNT(*) as count FROM registry_artists WHERE artwork_count > 0 + `); + + // 获取作品数量 + const artworkCountResult = await this.db.query(` + SELECT COUNT(*) as count FROM registry_artworks + `); + + // 获取版本信息 + const versionResult = await this.db.select('registry_meta', { meta_key: 'version' }); + const createdAtResult = await this.db.select('registry_meta', { meta_key: 'created_at' }); + + // 获取最后更新时间 + const lastUpdatedResult = await this.db.query(` + SELECT MAX(updated_at) as last_updated FROM registry_artworks + `); + + return { + artistCount: artistCountResult.data[0].count, + artworkCount: artworkCountResult.data[0].count, + version: versionResult.data[0]?.meta_value || '1.0.5', + created_at: createdAtResult.data[0]?.meta_value || new Date().toISOString(), + updated_at: lastUpdatedResult.data[0]?.last_updated || new Date().toISOString() + }; + } catch (error) { + logger.error('获取统计信息失败:', error); + return { + artistCount: 0, + artworkCount: 0, + version: '1.0.5', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + }; + } + } + + /** + * 导出注册表数据(转换为JSON格式) + */ + async exportRegistry() { + try { + const result = await this.db.query(` + SELECT + rt.artist_name, + rt.normalized_name, + GROUP_CONCAT(ra.artwork_id ORDER BY ra.artwork_id DESC) as artwork_ids + FROM registry_artists rt + LEFT JOIN registry_artworks ra ON rt.id = ra.artist_id + WHERE rt.artwork_count > 0 + GROUP BY rt.id, rt.artist_name, rt.normalized_name + `); + + const artists = {}; + + for (const row of result.data) { + const artworkIds = row.artwork_ids ? + row.artwork_ids.split(',').map(id => parseInt(id)) : []; + + artists[row.artist_name] = { + artworks: artworkIds + }; + } + + const stats = await this.getStats(); + + return { + version: stats.version, + created_at: stats.created_at, + updated_at: stats.updated_at, + exported_at: new Date().toISOString(), + artists: artists + }; + } catch (error) { + logger.error('导出注册表数据失败:', error); + throw error; + } + } + + /** + * 导入注册表数据(从JSON格式) + * @param {Object} importData 要导入的数据 + */ + async importRegistry(importData) { + if (!importData || !importData.artists) { + throw new Error('导入数据格式不正确'); + } + + let addedArtists = 0; + let addedArtworks = 0; + let skippedArtworks = 0; + + try { + await this.db.transaction(async (connection) => { + for (const artistName in importData.artists) { + const importArtworks = importData.artists[artistName].artworks || []; + + // 获取或创建艺术家 + const artistId = await this.getOrCreateArtist(artistName); + + // 检查是否是新艺术家 + const existingArtworkCount = await this.db.query( + 'SELECT COUNT(*) as count FROM registry_artworks WHERE artist_id = ?', + [artistId] + ); + + if (existingArtworkCount.data[0].count === 0) { + addedArtists++; + } + + // 获取现有作品ID + const existingArtworksResult = await this.db.select('registry_artworks', { + artist_id: artistId + }); + const existingArtworkIds = new Set( + existingArtworksResult.data.map(row => row.artwork_id) + ); + + // 添加新作品 + for (const artworkId of importArtworks) { + const normalizedArtworkId = parseInt(artworkId); + + if (!existingArtworkIds.has(normalizedArtworkId)) { + await this.db.insert('registry_artworks', { + artist_id: artistId, + artwork_id: normalizedArtworkId, + artist_name: artistName, + download_date: new Date() + }); + addedArtworks++; + } else { + skippedArtworks++; + } + } + + // 更新艺术家作品数量 + await this.updateArtistArtworkCount(artistId); + } + }); + + const stats = await this.getStats(); + const result = { + addedArtists, + addedArtworks, + skippedArtworks, + totalArtists: stats.artistCount, + totalArtworks: stats.artworkCount + }; + + logger.info(`注册表导入完成,导入了 ${result.addedArtists} 个作者,${result.addedArtworks} 个作品,跳过了 ${result.skippedArtworks} 个重复作品。当前总计:${result.totalArtists} 个作者,${result.totalArtworks} 个作品`); + return result; + } catch (error) { + logger.error('导入注册表数据失败:', error); + throw error; + } + } + + /** + * 从文件系统重建注册表 + * @param {FileManager} fileManager + * @param {string} taskId 任务ID + */ + async rebuildFromFileSystem(fileManager, taskId = null) { + logger.info('开始从文件系统重建数据库注册表...'); + + const stats = { + scannedArtists: 0, + scannedArtworks: 0, + addedArtworks: 0, + skippedArtworks: 0 + }; + + // 进度更新函数 + const updateProgress = (currentArtist) => { + if (taskId) { + const progressManager = require('../services/progress-manager'); + progressManager.updateProgress(taskId, { + ...stats, + currentArtist: currentArtist || '' + }); + } + }; + + try { + // 获取所有艺术家目录 + const artistDirs = await fileManager.getArtistDirectories(); + logger.info(`发现 ${artistDirs.length} 个艺术家目录`); + + for (const artistDir of artistDirs) { + try { + stats.scannedArtists++; + updateProgress(artistDir); + + logger.info(`扫描艺术家目录: ${artistDir}`); + + // 获取艺术家目录下的所有作品目录 + const artworkDirs = await fileManager.getArtworkDirectories(artistDir); + + for (const artworkDir of artworkDirs) { + try { + stats.scannedArtworks++; + updateProgress(artistDir); + + // 检查作品是否已在注册表中 + const isRegistered = await this.isArtworkRegistered(artistDir, artworkDir); + + if (!isRegistered) { + // 从作品目录名中提取作品ID并添加到注册表 + const artworkUtils = require('../utils/artwork-utils'); + const artworkId = await artworkUtils.extractArtworkIdFromDir(artworkDir); + if (artworkId) { + await this.addArtwork(artistDir, artworkId); + stats.addedArtworks++; + logger.debug(`添加作品到注册表: ${artistDir}/${artworkDir}`); + } + } else { + stats.skippedArtworks++; + } + + // 每处理10个作品更新一次进度 + if (stats.scannedArtworks % 10 === 0) { + updateProgress(artistDir); + } + + } catch (error) { + logger.warn(`处理作品目录失败 ${artistDir}/${artworkDir}:`, error.message); + } + } + + } catch (error) { + logger.warn(`处理艺术家目录失败 ${artistDir}:`, error.message); + } + } + + // 最终更新进度 + updateProgress(null); + + logger.info('从文件系统重建数据库注册表完成', stats); + return stats; + } catch (error) { + logger.error('从文件系统重建数据库注册表失败:', error); + throw error; + } + } + + /** + * 清理注册表(移除不存在的记录) + * @param {FileManager} fileManager 文件管理器实例 + */ + async cleanupRegistry(fileManager) { + try { + logger.info('开始清理数据库注册表...'); + + let removedArtists = 0; + let removedArtworks = 0; + const downloadPath = await fileManager.getDownloadPath(); + + // 获取所有作品记录 + const artworksResult = await this.db.query(` + SELECT ra.id, ra.artist_id, ra.artwork_id, rt.artist_name, rt.normalized_name + FROM registry_artworks ra + JOIN registry_artists rt ON ra.artist_id = rt.id + `); + + const fs = require('fs-extra'); + const path = require('path'); + const artworkUtils = require('../utils/artwork-utils'); + + for (const artwork of artworksResult.data) { + let found = false; + + try { + const artistPath = path.join(downloadPath, artwork.artist_name); + if (await fileManager.directoryExists(artistPath)) { + const artworkEntries = await fileManager.listDirectory(artistPath); + + for (const entry of artworkEntries) { + const extractedArtworkId = await artworkUtils.extractArtworkIdFromDir(entry); + if (extractedArtworkId && extractedArtworkId === artwork.artwork_id) { + const artworkPath = path.join(artistPath, entry); + const infoPath = path.join(artworkPath, 'artwork_info.json'); + + // 检查信息文件是否存在 + if (await fs.pathExists(infoPath)) { + found = true; + break; + } + } + } + } + } catch (error) { + logger.debug(`检查作品 ${artwork.artwork_id} 时出错:`, error); + } + + if (!found) { + // 删除作品记录 + await this.db.delete('registry_artworks', { id: artwork.id }); + removedArtworks++; + logger.debug(`移除无效作品记录: ${artwork.artist_name} - ${artwork.artwork_id}`); + } + } + + // 更新所有艺术家的作品数量 + const artistsResult = await this.db.select('registry_artists'); + for (const artist of artistsResult.data) { + await this.updateArtistArtworkCount(artist.id); + } + + // 删除没有作品的艺术家 + const emptyArtistsResult = await this.db.select('registry_artists', { artwork_count: 0 }); + for (const artist of emptyArtistsResult.data) { + await this.db.delete('registry_artists', { id: artist.id }); + removedArtists++; + logger.debug(`移除空作者记录: ${artist.artist_name}`); + } + + const stats = await this.getStats(); + const result = { + removedArtists, + removedArtworks, + remainingArtists: stats.artistCount, + remainingArtworks: stats.artworkCount + }; + + logger.info('数据库注册表清理完成', result); + return result; + } catch (error) { + logger.error('数据库注册表清理失败:', error); + throw error; + } + } + + /** + * 获取总作品数量 + */ + async getTotalArtworkCount() { + try { + const result = await this.db.query('SELECT COUNT(*) as count FROM registry_artworks'); + return result.data[0].count; + } catch (error) { + logger.error('获取总作品数量失败:', error); + return 0; + } + } +} + +module.exports = RegistryDatabase; \ No newline at end of file diff --git a/backend/database/registry-schema.js b/backend/database/registry-schema.js new file mode 100644 index 0000000..917f4ea --- /dev/null +++ b/backend/database/registry-schema.js @@ -0,0 +1,241 @@ +const { defaultLogger } = require('../utils/logger'); + +const logger = defaultLogger.child('RegistrySchema'); + +/** + * 注册表数据库表结构管理 + */ +class RegistrySchema { + constructor(databaseManager) { + this.db = databaseManager; + } + + /** + * 初始化所有注册表相关的数据库表 + */ + async initializeTables() { + try { + await this.createArtistsTable(); + await this.createArtworksTable(); + await this.createRegistryMetaTable(); + logger.info('注册表数据库表初始化完成'); + return { success: true }; + } catch (error) { + logger.error('初始化注册表数据库表失败:', error); + throw error; + } + } + + /** + * 创建艺术家表 + */ + async createArtistsTable() { + const tableName = 'registry_artists'; + + if (await this.db.tableExists(tableName)) { + logger.debug(`表 ${tableName} 已存在,跳过创建`); + return; + } + + const createSQL = ` + CREATE TABLE ${tableName} ( + id INT AUTO_INCREMENT PRIMARY KEY, + artist_name VARCHAR(255) NOT NULL UNIQUE COMMENT '艺术家名称', + normalized_name VARCHAR(255) NOT NULL COMMENT '标准化后的艺术家名称', + artwork_count INT DEFAULT 0 COMMENT '作品数量', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX idx_artist_name (artist_name), + INDEX idx_normalized_name (normalized_name) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='注册表艺术家表'; + `; + + await this.db.createTable(tableName, createSQL); + } + + /** + * 创建作品表 + */ + async createArtworksTable() { + const tableName = 'registry_artworks'; + + if (await this.db.tableExists(tableName)) { + logger.debug(`表 ${tableName} 已存在,跳过创建`); + return; + } + + const createSQL = ` + CREATE TABLE ${tableName} ( + id INT AUTO_INCREMENT PRIMARY KEY, + artist_id INT NOT NULL COMMENT '艺术家ID', + artwork_id BIGINT NOT NULL COMMENT '作品ID', + artist_name VARCHAR(255) NOT NULL COMMENT '艺术家名称(冗余字段,便于查询)', + file_path TEXT COMMENT '文件路径(可选)', + download_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '下载时间', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + UNIQUE KEY uk_artist_artwork (artist_id, artwork_id), + INDEX idx_artwork_id (artwork_id), + INDEX idx_artist_id (artist_id), + INDEX idx_artist_name (artist_name), + INDEX idx_download_date (download_date), + FOREIGN KEY (artist_id) REFERENCES registry_artists(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='注册表作品表'; + `; + + await this.db.createTable(tableName, createSQL); + } + + /** + * 创建注册表元数据表 + */ + async createRegistryMetaTable() { + const tableName = 'registry_meta'; + + if (await this.db.tableExists(tableName)) { + logger.debug(`表 ${tableName} 已存在,跳过创建`); + return; + } + + const createSQL = ` + CREATE TABLE ${tableName} ( + id INT AUTO_INCREMENT PRIMARY KEY, + meta_key VARCHAR(100) NOT NULL UNIQUE COMMENT '元数据键', + meta_value TEXT COMMENT '元数据值', + description TEXT COMMENT '描述', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX idx_meta_key (meta_key) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='注册表元数据表'; + `; + + await this.db.createTable(tableName, createSQL); + + // 插入初始元数据 + await this.initializeMetaData(); + } + + /** + * 初始化元数据 + */ + async initializeMetaData() { + const metaData = [ + { + meta_key: 'version', + meta_value: '1.0.5', + description: '注册表版本号' + }, + { + meta_key: 'storage_type', + meta_value: 'database', + description: '存储类型:database 或 json' + }, + { + meta_key: 'created_at', + meta_value: new Date().toISOString(), + description: '注册表创建时间' + }, + { + meta_key: 'last_migration', + meta_value: new Date().toISOString(), + description: '最后一次迁移时间' + } + ]; + + for (const meta of metaData) { + try { + await this.db.insert('registry_meta', meta); + } catch (error) { + // 如果键已存在,忽略错误 + if (!error.message.includes('Duplicate entry')) { + throw error; + } + } + } + } + + /** + * 获取表结构信息 + */ + getTableInfo() { + return { + artists: { + tableName: 'registry_artists', + description: '存储艺术家信息', + fields: { + id: '主键ID', + artist_name: '艺术家名称', + normalized_name: '标准化后的艺术家名称', + artwork_count: '作品数量', + created_at: '创建时间', + updated_at: '更新时间' + } + }, + artworks: { + tableName: 'registry_artworks', + description: '存储作品信息', + fields: { + id: '主键ID', + artist_id: '艺术家ID(外键)', + artwork_id: '作品ID', + artist_name: '艺术家名称(冗余字段)', + file_path: '文件路径', + download_date: '下载时间', + created_at: '创建时间', + updated_at: '更新时间' + } + }, + meta: { + tableName: 'registry_meta', + description: '存储注册表元数据', + fields: { + id: '主键ID', + meta_key: '元数据键', + meta_value: '元数据值', + description: '描述', + created_at: '创建时间', + updated_at: '更新时间' + } + } + }; + } + + /** + * 检查所有表是否存在 + */ + async checkTablesExist() { + const tables = ['registry_artists', 'registry_artworks', 'registry_meta']; + const results = {}; + + for (const table of tables) { + results[table] = await this.db.tableExists(table); + } + + return results; + } + + /** + * 删除所有注册表相关的表(谨慎使用) + */ + async dropAllTables() { + const tables = ['registry_artworks', 'registry_artists', 'registry_meta']; + + try { + // 按照外键依赖顺序删除表 + for (const table of tables) { + if (await this.db.tableExists(table)) { + await this.db.query(`DROP TABLE ${table}`); + logger.info(`表 ${table} 已删除`); + } + } + + logger.info('所有注册表相关的表已删除'); + return { success: true }; + } catch (error) { + logger.error('删除注册表表失败:', error); + throw error; + } + } +} + +module.exports = RegistrySchema; \ No newline at end of file diff --git a/backend/routes/database.js b/backend/routes/database.js new file mode 100644 index 0000000..345c97d --- /dev/null +++ b/backend/routes/database.js @@ -0,0 +1,670 @@ +const express = require('express'); +const router = express.Router(); +const DatabaseManager = require('../database/database-manager'); +const RegistryDatabase = require('../database/registry-database'); +const RegistryMigration = require('../services/registry-migration'); +const { defaultLogger } = require('../utils/logger'); + +// 创建logger实例 +const logger = defaultLogger.child('DatabaseRouter'); + +// 全局数据库管理器实例 +let databaseManager = null; +let registryDatabase = null; + +/** + * 设置数据库实例 + */ +function setDatabaseInstances(dbManager, regDatabase) { + databaseManager = dbManager; + registryDatabase = regDatabase; +} + +/** + * 测试数据库连接 + * POST /api/database/test-connection + */ +router.post('/test-connection', async (req, res) => { + try { + const { host, port, user, password, database, ssl } = req.body; + + if (!host || !port || !user || !database) { + return res.status(400).json({ + success: false, + error: '缺少必要的连接参数' + }); + } + + // 创建临时数据库管理器进行测试 + const testManager = new DatabaseManager(); + + // 初始化连接池 + await testManager.init({ + host, + port: parseInt(port), + user, + password, + database, + ssl: ssl || false + }); + + const result = await testManager.testConnection(); + + if (result.success) { + res.json({ + success: true, + data: { + message: '数据库连接成功', + serverVersion: result.serverVersion, + connectionTime: result.connectionTime + } + }); + } else { + res.status(400).json({ + success: false, + error: result.error + }); + } + + // 关闭测试连接 + await testManager.close(); + + } catch (error) { + logger.error('测试数据库连接失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 保存数据库配置 + * POST /api/database/config + */ +router.post('/config', async (req, res) => { + try { + const { host, port, user, password, database, connectionLimit, ssl } = req.body; + + if (!host || !port || !user || !database) { + return res.status(400).json({ + success: false, + error: '缺少必要的连接参数' + }); + } + + const config = { + host, + port: parseInt(port), + user, + password, + database, + connectionLimit: parseInt(connectionLimit) || 10, + ssl: ssl || false + }; + + // 先测试连接 + const testManager = new DatabaseManager(); + await testManager.init(config); + const testResult = await testManager.testConnection(); + + if (!testResult.success) { + await testManager.close(); + return res.status(400).json({ + success: false, + error: `连接测试失败: ${testResult.error}` + }); + } + + await testManager.close(); + + // 保存配置到文件 + const fs = require('fs-extra'); + const path = require('path'); + + // 检测是否在pkg打包环境中运行 + const isPkg = process.pkg !== undefined; + + const configPath = isPkg + ? path.join(process.cwd(), 'data', 'database.json') // 打包环境:当前工作目录的data文件夹 + : path.join(__dirname, '..', '..', 'data', 'database.json'); // 开发环境:项目根目录的data文件夹 + + await fs.ensureDir(path.dirname(configPath)); + await fs.writeJson(configPath, config, { spaces: 2 }); + + // 重新初始化全局数据库管理器 + if (databaseManager) { + await databaseManager.close(); + } + + databaseManager = new DatabaseManager(); + await databaseManager.init(config); + + // 初始化注册表数据库 + registryDatabase = new RegistryDatabase(databaseManager); + await registryDatabase.init(); + + res.json({ + success: true, + data: { + message: '数据库配置已保存并连接成功', + config: { + host: config.host, + port: config.port, + user: config.user, + database: config.database, + connectionLimit: config.connectionLimit, + ssl: config.ssl + } + } + }); + + } catch (error) { + logger.error('保存数据库配置失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 获取数据库配置 + * GET /api/database/config + */ +router.get('/config', async (req, res) => { + try { + const fs = require('fs-extra'); + const path = require('path'); + + // 检测是否在pkg打包环境中运行 + const isPkg = process.pkg !== undefined; + + const configPath = isPkg + ? path.join(process.cwd(), 'data', 'database.json') // 打包环境:当前工作目录的data文件夹 + : path.join(__dirname, '..', '..', 'data', 'database.json'); // 开发环境:项目根目录的data文件夹 + + if (await fs.pathExists(configPath)) { + const config = await fs.readJson(configPath); + + // 不返回密码 + const safeConfig = { + host: config.host, + port: config.port, + user: config.user, + database: config.database, + connectionLimit: config.connectionLimit, + ssl: config.ssl, + hasPassword: !!config.password + }; + + res.json({ + success: true, + data: safeConfig + }); + } else { + res.json({ + success: true, + data: null + }); + } + + } catch (error) { + logger.error('获取数据库配置失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 获取数据库连接状态 + * GET /api/database/status + */ +router.get('/status', async (req, res) => { + try { + if (!databaseManager) { + return res.json({ + success: true, + data: { + connected: false, + message: '数据库未配置' + } + }); + } + + const isConnected = databaseManager.isConnected; + + res.json({ + success: true, + data: { + connected: isConnected, + message: isConnected ? '数据库已连接' : '数据库连接断开' + } + }); + + } catch (error) { + logger.error('获取数据库状态失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 初始化数据库表 + * POST /api/database/init-tables + */ +router.post('/init-tables', async (req, res) => { + try { + if (!databaseManager) { + return res.status(400).json({ + success: false, + error: '数据库未配置' + }); + } + + if (!registryDatabase) { + registryDatabase = new RegistryDatabase(databaseManager); + } + + await registryDatabase.init(); + + res.json({ + success: true, + data: { + message: '数据库表初始化成功' + } + }); + + } catch (error) { + logger.error('初始化数据库表失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 数据迁移:JSON到数据库 + * POST /api/database/migrate/json-to-db + */ +router.post('/migrate/json-to-db', async (req, res) => { + try { + const { overwrite = true, createBackup = true } = req.body; + + if (!databaseManager || !registryDatabase) { + return res.status(400).json({ + success: false, + error: '数据库未配置或未初始化' + }); + } + + const downloadService = req.backend.getDownloadService(); + const jsonRegistry = downloadService.downloadRegistry; + const fileManager = downloadService.fileManager; + + const migration = new RegistryMigration(jsonRegistry, registryDatabase, fileManager); + const result = await migration.performMigration('json-to-db', overwrite, createBackup); + + res.json({ + success: true, + data: result + }); + + } catch (error) { + logger.error('JSON到数据库迁移失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 数据迁移:数据库到JSON + * POST /api/database/migrate/db-to-json + */ +router.post('/migrate/db-to-json', async (req, res) => { + try { + const { overwrite = true, createBackup = true } = req.body; + + if (!databaseManager || !registryDatabase) { + return res.status(400).json({ + success: false, + error: '数据库未配置或未初始化' + }); + } + + const downloadService = req.backend.getDownloadService(); + const jsonRegistry = downloadService.downloadRegistry; + const fileManager = downloadService.fileManager; + + const migration = new RegistryMigration(jsonRegistry, registryDatabase, fileManager); + const result = await migration.performMigration('db-to-json', overwrite, createBackup); + + res.json({ + success: true, + data: result + }); + + } catch (error) { + logger.error('数据库到JSON迁移失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 比较注册表差异 + * GET /api/database/compare-registries + */ +router.get('/compare-registries', async (req, res) => { + try { + if (!databaseManager || !registryDatabase) { + return res.status(400).json({ + success: false, + error: '数据库未配置或未初始化' + }); + } + + const downloadService = req.backend.getDownloadService(); + const jsonRegistry = downloadService.downloadRegistry; + const fileManager = downloadService.fileManager; + + const migration = new RegistryMigration(jsonRegistry, registryDatabase, fileManager); + const comparison = await migration.compareRegistries(); + + res.json({ + success: true, + data: comparison + }); + + } catch (error) { + logger.error('比较注册表失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 断开数据库连接 + * POST /api/database/disconnect + */ +router.post('/disconnect', async (req, res) => { + try { + if (databaseManager) { + await databaseManager.disconnect(); + databaseManager = null; + registryDatabase = null; + } + + res.json({ + success: true, + data: { + message: '数据库连接已断开' + } + }); + + } catch (error) { + logger.error('断开数据库连接失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// 数据库注册表相关API + +/** + * 获取数据库注册表统计信息 + * GET /api/database/registry/stats + */ +router.get('/registry/stats', async (req, res) => { + try { + if (!registryDatabase) { + return res.status(400).json({ + success: false, + error: '数据库注册表未初始化' + }); + } + + const stats = await registryDatabase.getStats(); + + res.json({ + success: true, + data: stats + }); + + } catch (error) { + logger.error('获取数据库注册表统计信息失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 导出数据库注册表 + * GET /api/database/registry/export + */ +router.get('/registry/export', async (req, res) => { + try { + if (!registryDatabase) { + return res.status(400).json({ + success: false, + error: '数据库注册表未初始化' + }); + } + + const registryData = await registryDatabase.exportRegistry(); + + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Content-Disposition', 'attachment; filename="database-registry.json"'); + res.json(registryData); + + } catch (error) { + logger.error('导出数据库注册表失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 导入数据到数据库注册表 + * POST /api/database/registry/import + */ +router.post('/registry/import', async (req, res) => { + try { + const { registryData } = req.body; + + if (!registryData) { + return res.status(400).json({ + success: false, + error: '缺少注册表数据' + }); + } + + if (!registryDatabase) { + return res.status(400).json({ + success: false, + error: '数据库注册表未初始化' + }); + } + + const result = await registryDatabase.importRegistry(registryData); + + res.json({ + success: true, + data: result + }); + + } catch (error) { + logger.error('导入数据库注册表失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 从文件系统重建数据库注册表 + * POST /api/database/registry/rebuild + */ +router.post('/registry/rebuild', async (req, res) => { + try { + if (!registryDatabase) { + return res.status(400).json({ + success: false, + error: '数据库注册表未初始化' + }); + } + + const downloadService = req.backend.getDownloadService(); + const fileManager = downloadService.fileManager; + + // 生成任务ID + const taskId = `db-registry-rebuild-${Date.now()}`; + + // 立即返回任务ID,不等待完成 + res.json({ + success: true, + data: { + taskId, + status: 'started', + message: '数据库注册表重建任务已启动' + } + }); + + // 异步执行重建任务 + setImmediate(async () => { + try { + // 设置任务状态为进行中 + global.dbRegistryRebuildTasks = global.dbRegistryRebuildTasks || new Map(); + global.dbRegistryRebuildTasks.set(taskId, { + status: 'running', + startTime: Date.now(), + progress: { + scannedArtists: 0, + scannedArtworks: 0, + addedArtworks: 0, + skippedArtworks: 0, + currentArtist: null + } + }); + + const result = await registryDatabase.rebuildFromFileSystem(fileManager, taskId); + + // 更新任务状态为完成 + global.dbRegistryRebuildTasks.set(taskId, { + status: 'completed', + startTime: global.dbRegistryRebuildTasks.get(taskId).startTime, + endTime: Date.now(), + result: result + }); + + logger.info(`数据库注册表重建任务完成: ${taskId}`, result); + } catch (error) { + logger.error(`数据库注册表重建任务失败: ${taskId}`, error); + + // 更新任务状态为失败 + global.dbRegistryRebuildTasks.set(taskId, { + status: 'failed', + startTime: global.dbRegistryRebuildTasks.get(taskId).startTime, + endTime: Date.now(), + error: error.message + }); + } + }); + + } catch (error) { + logger.error('启动数据库注册表重建任务失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 获取数据库注册表重建任务状态 + * GET /api/database/registry/rebuild/status/:taskId + */ +router.get('/registry/rebuild/status/:taskId', async (req, res) => { + try { + const { taskId } = req.params; + + global.dbRegistryRebuildTasks = global.dbRegistryRebuildTasks || new Map(); + const task = global.dbRegistryRebuildTasks.get(taskId); + + if (!task) { + return res.status(404).json({ + success: false, + error: '任务不存在' + }); + } + + res.json({ + success: true, + data: task + }); + + } catch (error) { + logger.error('获取数据库注册表重建任务状态失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 清理数据库注册表 + * POST /api/database/registry/cleanup + */ +router.post('/registry/cleanup', async (req, res) => { + try { + if (!registryDatabase) { + return res.status(400).json({ + success: false, + error: '数据库注册表未初始化' + }); + } + + const downloadService = req.backend.getDownloadService(); + const fileManager = downloadService.fileManager; + + const result = await registryDatabase.cleanupRegistry(fileManager); + + res.json({ + success: true, + data: result + }); + + } catch (error) { + logger.error('清理数据库注册表失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// 导出数据库管理器实例,供其他模块使用 +router.getDatabaseManager = () => databaseManager; +router.getRegistryDatabase = () => registryDatabase; +router.setDatabaseInstances = setDatabaseInstances; + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/download.js b/backend/routes/download.js index 925ecdc..9597b1b 100644 --- a/backend/routes/download.js +++ b/backend/routes/download.js @@ -1203,7 +1203,8 @@ router.get('/registry/config', async (req, res) => { // 提取下载相关的配置 const downloadConfig = { useRegistryCheck: config.download?.useRegistryCheck !== false, // 默认启用 - fallbackToScan: config.download?.fallbackToScan === true // 默认不启用 + fallbackToScan: config.download?.fallbackToScan === true, // 默认不启用 + storageMode: config.download?.storageMode || 'json' // 默认JSON存储 }; res.json({ @@ -1225,8 +1226,9 @@ router.get('/registry/config', async (req, res) => { */ router.put('/registry/config', async (req, res) => { try { - const { useRegistryCheck, fallbackToScan } = req.body; + const { useRegistryCheck, fallbackToScan, storageMode } = req.body; + // 验证必需的布尔值参数 if (typeof useRegistryCheck !== 'boolean' || typeof fallbackToScan !== 'boolean') { return res.status(400).json({ success: false, @@ -1234,21 +1236,38 @@ router.put('/registry/config', async (req, res) => { }); } + // 验证存储模式参数(可选) + if (storageMode && !['json', 'database'].includes(storageMode)) { + return res.status(400).json({ + success: false, + error: '存储模式必须是 json 或 database' + }); + } + const downloadService = req.backend.getDownloadService(); - // 更新配置 - const updatedConfig = await downloadService.cacheConfigManager.updateConfig({ + // 构建更新配置对象 + const updateData = { download: { useRegistryCheck, fallbackToScan } - }); + }; + + // 如果提供了存储模式,添加到更新数据中 + if (storageMode) { + updateData.download.storageMode = storageMode; + } + + // 更新配置 + const updatedConfig = await downloadService.cacheConfigManager.updateConfig(updateData); res.json({ success: true, data: { useRegistryCheck: updatedConfig.download?.useRegistryCheck !== false, - fallbackToScan: updatedConfig.download?.fallbackToScan === true + fallbackToScan: updatedConfig.download?.fallbackToScan === true, + storageMode: updatedConfig.download?.storageMode || 'json' } }); } catch (error) { @@ -1260,4 +1279,27 @@ router.put('/registry/config', async (req, res) => { } }); +/** + * 清理下载注册表 + * POST /api/download/registry/cleanup + */ +router.post('/registry/cleanup', async (req, res) => { + try { + const downloadService = req.backend.getDownloadService(); + const result = await downloadService.downloadRegistry.cleanupRegistry(downloadService.fileManager); + + res.json({ + success: true, + data: result + }); + + } catch (error) { + logger.error('清理下载注册表失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/backend/routes/index.js b/backend/routes/index.js index 0f032e4..2901b1e 100644 --- a/backend/routes/index.js +++ b/backend/routes/index.js @@ -14,6 +14,7 @@ const rankingRoutes = require('./ranking'); const watchlistRoutes = require('./watchlist'); const updateRoutes = require('./update'); const systemRoutes = require('./system'); +const databaseRoutes = require('./database'); // 导入认证中间件 const { authMiddleware } = require('../middleware/auth'); @@ -49,6 +50,7 @@ function setupRoutes(app, backend) { app.use('/api/watchlist', authMiddleware, watchlistRoutes); // 待看名单,需要认证 app.use('/api/update', updateRoutes); // 更新检查,不需要认证 app.use('/api/system', systemRoutes); // 系统管理,不需要认证 + app.use('/api/database', databaseRoutes); // 数据库管理,不需要认证 // 404 处理 app.use((req, res) => { diff --git a/backend/services/download-registry.js b/backend/services/download-registry.js index aa8f95d..63d1e59 100644 --- a/backend/services/download-registry.js +++ b/backend/services/download-registry.js @@ -2,26 +2,35 @@ const path = require('path'); const fs = require('fs-extra'); const { defaultLogger } = require('../utils/logger'); const ConfigManager = require('../config/config-manager'); +const CacheConfigManager = require('../config/cache-config'); +const RegistryDatabase = require('../database/registry-database'); const artworkUtils = require('../utils/artwork-utils'); // 创建logger实例 const logger = defaultLogger.child('DownloadRegistry'); /** - * 下载记录管理器 - 维护已下载作品的JSON记录 + * 下载记录管理器 - 维护已下载作品的记录 + * 支持JSON文件和数据库两种存储模式,根据配置自动选择 * 用于快速检测作品是否已下载,支持导入导出和多设备同步 */ class DownloadRegistry { - constructor(dataPath) { + constructor(dataPath, databaseManager = null) { this.dataPath = dataPath; this.registryPath = path.join(dataPath, 'download-registry.json'); this.registry = { version: '1.0.5', artists: {}, - lastUpdated: null + lastUpdated: null, }; this.loaded = false; this.configManager = new ConfigManager(); + this.cacheConfigManager = new CacheConfigManager(); + + // 数据库相关 + this.databaseManager = databaseManager; + this.registryDatabase = null; + this.storageMode = 'json'; // 默认使用JSON存储 } /** @@ -31,17 +40,61 @@ class DownloadRegistry { try { // 确保数据目录存在 await fs.ensureDir(this.dataPath); - - // 加载现有注册表 - await this.loadRegistry(); - - logger.info(`下载记录注册表初始化完成,总共包含${Object.keys(this.registry.artists).length}个作者,${this.getTotalArtworkCount()}个作品`); + + // 获取存储模式配置 + await this.loadStorageMode(); + + // 根据存储模式初始化相应的存储系统 + if (this.storageMode === 'database' && this.databaseManager) { + await this.initDatabaseStorage(); + } else { + await this.initJsonStorage(); + } + + const stats = await this.getStats(); + logger.info(`下载记录注册表初始化完成(${this.storageMode}模式),总共包含${stats.artistCount}个作者,${stats.artworkCount}个作品`); } catch (error) { logger.error('下载记录注册表初始化失败:', error); throw error; } } + /** + * 加载存储模式配置 + */ + async loadStorageMode() { + try { + const cacheConfig = await this.cacheConfigManager.loadConfig(); + this.storageMode = cacheConfig.download?.storageMode || 'json'; + logger.debug(`存储模式配置: ${this.storageMode}`); + } catch (error) { + logger.warn('加载存储模式配置失败,使用默认JSON模式:', error.message); + this.storageMode = 'json'; + } + } + + /** + * 初始化数据库存储 + */ + async initDatabaseStorage() { + if (!this.databaseManager) { + throw new Error('数据库管理器未提供,无法使用数据库存储模式'); + } + + this.registryDatabase = new RegistryDatabase(this.databaseManager); + await this.registryDatabase.init(); + this.loaded = true; + logger.info('数据库存储模式初始化完成'); + } + + /** + * 初始化JSON存储 + */ + async initJsonStorage() { + await this.loadRegistry(); + logger.info('JSON存储模式初始化完成'); + } + /** * 加载注册表文件 */ @@ -49,14 +102,14 @@ class DownloadRegistry { try { if (await fs.pathExists(this.registryPath)) { const data = await fs.readJson(this.registryPath); - + // 验证数据格式 if (data && typeof data === 'object' && data.artists) { this.registry = { version: data.version || '1.0.0', created_at: data.created_at || new Date().toISOString(), updated_at: data.updated_at || new Date().toISOString(), - artists: data.artists || {} + artists: data.artists || {}, }; } else { logger.warn('注册表文件格式不正确,使用默认格式'); @@ -64,7 +117,7 @@ class DownloadRegistry { } else { logger.info('注册表文件不存在,将创建新的注册表'); } - + this.loaded = true; } catch (error) { logger.error('加载注册表文件失败:', error); @@ -94,25 +147,32 @@ class DownloadRegistry { */ async addArtwork(artistName, artworkId) { if (!this.loaded) { - await this.loadRegistry(); + await this.init(); } const normalizedArtistName = this.normalizeArtistName(artistName); const normalizedArtworkId = parseInt(artworkId); - if (!this.registry.artists[normalizedArtistName]) { - this.registry.artists[normalizedArtistName] = { - artworks: [] - }; - } + if (this.storageMode === 'database' && this.registryDatabase) { + // 使用数据库存储 + await this.registryDatabase.addArtwork(normalizedArtistName, normalizedArtworkId); + logger.debug('添加作品记录到数据库', { artistName: normalizedArtistName, artworkId: normalizedArtworkId }); + } else { + // 使用JSON存储 + if (!this.registry.artists[normalizedArtistName]) { + this.registry.artists[normalizedArtistName] = { + artworks: [], + }; + } - // 检查是否已存在 - if (!this.registry.artists[normalizedArtistName].artworks.includes(normalizedArtworkId)) { - this.registry.artists[normalizedArtistName].artworks.push(normalizedArtworkId); - this.registry.artists[normalizedArtistName].artworks.sort((a, b) => b - a); // 按ID倒序排列 - - await this.saveRegistry(); - logger.debug('添加作品记录', { artistName: normalizedArtistName, artworkId: normalizedArtworkId }); + // 检查是否已存在 + if (!this.registry.artists[normalizedArtistName].artworks.includes(normalizedArtworkId)) { + this.registry.artists[normalizedArtistName].artworks.push(normalizedArtworkId); + this.registry.artists[normalizedArtistName].artworks.sort((a, b) => b - a); // 按ID倒序排列 + + await this.saveRegistry(); + logger.debug('添加作品记录到JSON', { artistName: normalizedArtistName, artworkId: normalizedArtworkId }); + } } } @@ -123,45 +183,52 @@ class DownloadRegistry { */ async removeArtwork(artistName, artworkId) { if (!this.loaded) { - await this.loadRegistry(); + await this.init(); } const normalizedArtistName = this.normalizeArtistName(artistName); const normalizedArtworkId = parseInt(artworkId); - logger.debug('开始移除作品记录', { - originalArtistName: artistName, - normalizedArtistName: normalizedArtistName, - artworkId: normalizedArtworkId + logger.debug('开始移除作品记录', { + originalArtistName: artistName, + normalizedArtistName: normalizedArtistName, + artworkId: normalizedArtworkId, }); - if (this.registry.artists[normalizedArtistName]) { - const artworks = this.registry.artists[normalizedArtistName].artworks; - const index = artworks.indexOf(normalizedArtworkId); - - logger.debug('查找作品在注册表中的位置', { - artistName: normalizedArtistName, - artworkId: normalizedArtworkId, - index: index, - artworks: artworks - }); - - if (index !== -1) { - artworks.splice(index, 1); - - // 如果作者下没有作品了,删除作者记录 - if (artworks.length === 0) { - delete this.registry.artists[normalizedArtistName]; - logger.info('作者下无作品,删除作者记录', { artistName: normalizedArtistName }); - } - - await this.saveRegistry(); - logger.debug('成功移除作品记录', { artistName: normalizedArtistName, artworkId: normalizedArtworkId }); - } else { - logger.warn('作品在注册表中未找到', { artistName: normalizedArtistName, artworkId: normalizedArtworkId }); - } + if (this.storageMode === 'database' && this.registryDatabase) { + // 使用数据库存储 + await this.registryDatabase.removeArtwork(normalizedArtistName, normalizedArtworkId); + logger.debug('从数据库移除作品记录', { artistName: normalizedArtistName, artworkId: normalizedArtworkId }); } else { - logger.warn('作者在注册表中未找到', { artistName: normalizedArtistName }); + // 使用JSON存储 + if (this.registry.artists[normalizedArtistName]) { + const artworks = this.registry.artists[normalizedArtistName].artworks; + const index = artworks.indexOf(normalizedArtworkId); + + logger.debug('查找作品在注册表中的位置', { + artistName: normalizedArtistName, + artworkId: normalizedArtworkId, + index: index, + artworks: artworks, + }); + + if (index !== -1) { + artworks.splice(index, 1); + + // 如果作者下没有作品了,删除作者记录 + if (artworks.length === 0) { + delete this.registry.artists[normalizedArtistName]; + logger.info('作者下无作品,删除作者记录', { artistName: normalizedArtistName }); + } + + await this.saveRegistry(); + logger.debug('成功移除作品记录', { artistName: normalizedArtistName, artworkId: normalizedArtworkId }); + } else { + logger.warn('作品在注册表中未找到', { artistName: normalizedArtistName, artworkId: normalizedArtworkId }); + } + } else { + logger.warn('作者在注册表中未找到', { artistName: normalizedArtistName }); + } } } @@ -172,19 +239,25 @@ class DownloadRegistry { */ async isArtworkDownloaded(artworkId) { if (!this.loaded) { - await this.loadRegistry(); + await this.init(); } const normalizedArtworkId = parseInt(artworkId); - - // 遍历所有作者查找作品 - for (const artistName in this.registry.artists) { - if (this.registry.artists[artistName].artworks.includes(normalizedArtworkId)) { - return true; + + if (this.storageMode === 'database' && this.registryDatabase) { + // 使用数据库存储 + return await this.registryDatabase.isArtworkDownloaded(normalizedArtworkId); + } else { + // 使用JSON存储 + // 遍历所有作者查找作品 + for (const artistName in this.registry.artists) { + if (this.registry.artists[artistName].artworks.includes(normalizedArtworkId)) { + return true; + } } + + return false; } - - return false; } /** @@ -235,13 +308,13 @@ class DownloadRegistry { } const artworkIds = new Set(); - + for (const artistName in this.registry.artists) { for (const artworkId of this.registry.artists[artistName].artworks) { artworkIds.add(artworkId); } } - + return Array.from(artworkIds).sort((a, b) => b - a); } @@ -252,16 +325,22 @@ class DownloadRegistry { */ async getArtistArtworks(artistName) { if (!this.loaded) { - await this.loadRegistry(); + await this.init(); } const normalizedArtistName = this.normalizeArtistName(artistName); - - if (this.registry.artists[normalizedArtistName]) { - return [...this.registry.artists[normalizedArtistName].artworks]; + + if (this.storageMode === 'database' && this.registryDatabase) { + // 使用数据库存储 + return await this.registryDatabase.getArtistArtworks(normalizedArtistName); + } else { + // 使用JSON存储 + if (this.registry.artists[normalizedArtistName]) { + return [...this.registry.artists[normalizedArtistName].artworks]; + } + + return []; } - - return []; } /** @@ -270,10 +349,16 @@ class DownloadRegistry { */ async getDownloadedArtists() { if (!this.loaded) { - await this.loadRegistry(); + await this.init(); } - return Object.keys(this.registry.artists); + if (this.storageMode === 'database' && this.registryDatabase) { + // 使用数据库存储 + return await this.registryDatabase.getDownloadedArtists(); + } else { + // 使用JSON存储 + return Object.keys(this.registry.artists); + } } /** @@ -281,19 +366,25 @@ class DownloadRegistry { */ async getStats() { if (!this.loaded) { - await this.loadRegistry(); + await this.init(); } - const artists = Object.keys(this.registry.artists); - const totalArtworks = this.getTotalArtworkCount(); - - return { - artistCount: artists.length, - artworkCount: totalArtworks, - version: this.registry.version, - created_at: this.registry.created_at, - updated_at: this.registry.updated_at - }; + if (this.storageMode === 'database' && this.registryDatabase) { + // 使用数据库存储 + return await this.registryDatabase.getStats(); + } else { + // 使用JSON存储 + const artists = Object.keys(this.registry.artists); + const totalArtworks = this.getTotalArtworkCount(); + + return { + artistCount: artists.length, + artworkCount: totalArtworks, + version: this.registry.version, + created_at: this.registry.created_at, + updated_at: this.registry.updated_at, + }; + } } /** @@ -307,7 +398,7 @@ class DownloadRegistry { return { ...this.registry, - exported_at: new Date().toISOString() + exported_at: new Date().toISOString(), }; } @@ -332,14 +423,14 @@ class DownloadRegistry { for (const artistName in importData.artists) { const normalizedArtistName = this.normalizeArtistName(artistName); const importArtworks = importData.artists[artistName].artworks || []; - + if (!this.registry.artists[normalizedArtistName]) { this.registry.artists[normalizedArtistName] = { artworks: [] }; addedArtists++; } const existingArtworks = new Set(this.registry.artists[normalizedArtistName].artworks); - + for (const artworkId of importArtworks) { const normalizedArtworkId = parseInt(artworkId); if (!existingArtworks.has(normalizedArtworkId)) { @@ -349,7 +440,7 @@ class DownloadRegistry { skippedArtworks++; } } - + // 排序 this.registry.artists[normalizedArtistName].artworks.sort((a, b) => b - a); } @@ -361,27 +452,29 @@ class DownloadRegistry { addedArtworks, skippedArtworks, totalArtists: Object.keys(this.registry.artists).length, - totalArtworks: this.getTotalArtworkCount() + totalArtworks: this.getTotalArtworkCount(), }; - logger.info(`注册表导入完成,导入了 ${result.addedArtists} 个作者,${result.addedArtworks} 个作品,跳过了 ${result.skippedArtworks} 个重复作品。当前总计:${result.totalArtists} 个作者,${result.totalArtworks} 个作品`); + logger.info( + `注册表导入完成,导入了 ${result.addedArtists} 个作者,${result.addedArtworks} 个作品,跳过了 ${result.skippedArtworks} 个重复作品。当前总计:${result.totalArtists} 个作者,${result.totalArtworks} 个作品` + ); return result; } /** * 从文件系统重建注册表 - * @param {FileManager} fileManager + * @param {FileManager} fileManager * @param {string} taskId - 任务ID,用于更新进度 * @returns {Promise<{scannedArtists: number, scannedArtworks: number, addedArtworks: number, skippedArtworks: number}>} */ async rebuildFromFileSystem(fileManager, taskId = null) { logger.info('开始从文件系统重建下载注册表...'); - + const stats = { scannedArtists: 0, scannedArtworks: 0, addedArtworks: 0, - skippedArtworks: 0 + skippedArtworks: 0, }; // 获取所有艺术家目录 @@ -397,8 +490,8 @@ class DownloadRegistry { ...task, progress: { ...stats, - currentArtist - } + currentArtist, + }, }); } // 检查是否被取消 @@ -412,20 +505,20 @@ class DownloadRegistry { try { stats.scannedArtists++; updateProgress(artistDir); - + logger.info(`扫描艺术家目录: ${artistDir}`); - + // 获取艺术家目录下的所有作品目录 const artworkDirs = await fileManager.getArtworkDirectories(artistDir); - + for (const artworkDir of artworkDirs) { try { stats.scannedArtworks++; updateProgress(artistDir); - + // 检查作品是否已在注册表中 const isRegistered = await this.isArtworkRegistered(artistDir, artworkDir); - + if (!isRegistered) { // 从作品目录名中提取作品ID并添加到注册表 const artworkId = await this.extractArtworkIdFromDir(artworkDir); @@ -437,17 +530,15 @@ class DownloadRegistry { } else { stats.skippedArtworks++; } - + // 每处理10个作品更新一次进度 if (stats.scannedArtworks % 10 === 0) { updateProgress(artistDir); } - } catch (error) { logger.warn(`处理作品目录失败 ${artistDir}/${artworkDir}:`, error.message); } } - } catch (error) { logger.warn(`处理艺术家目录失败 ${artistDir}:`, error.message); } @@ -455,7 +546,7 @@ class DownloadRegistry { // 最终更新进度 updateProgress(null); - + logger.info('从文件系统重建下载注册表完成', stats); return stats; } @@ -472,7 +563,7 @@ class DownloadRegistry { } logger.info('开始清理注册表...'); - + let removedArtists = 0; let removedArtworks = 0; const downloadPath = await fileManager.getDownloadPath(); @@ -484,18 +575,18 @@ class DownloadRegistry { for (const artworkId of artworks) { // 检查作品目录是否存在 let found = false; - + try { const artistPath = path.join(downloadPath, artistName); if (await fileManager.directoryExists(artistPath)) { const artworkEntries = await fileManager.listDirectory(artistPath); - + for (const entry of artworkEntries) { const extractedArtworkId = await artworkUtils.extractArtworkIdFromDir(entry); if (extractedArtworkId && extractedArtworkId === artworkId) { const artworkPath = path.join(artistPath, entry); const infoPath = path.join(artworkPath, 'artwork_info.json'); - + // 检查信息文件是否存在 if (await fs.pathExists(infoPath)) { found = true; @@ -531,7 +622,7 @@ class DownloadRegistry { removedArtists, removedArtworks, remainingArtists: Object.keys(this.registry.artists).length, - remainingArtworks: this.getTotalArtworkCount() + remainingArtworks: this.getTotalArtworkCount(), }; logger.info('注册表清理完成', result); @@ -571,4 +662,4 @@ class DownloadRegistry { } } -module.exports = DownloadRegistry; \ No newline at end of file +module.exports = DownloadRegistry; diff --git a/backend/services/download.js b/backend/services/download.js index 14b7b55..110d508 100644 --- a/backend/services/download.js +++ b/backend/services/download.js @@ -20,11 +20,12 @@ const logger = defaultLogger.child('DownloadService'); * 下载服务 - 主服务类,协调各个管理器 */ class DownloadService { - constructor(auth) { + constructor(auth, databaseManager = null) { this.auth = auth; this.artworkService = new ArtworkService(auth); this.artistService = new ArtistService(auth); this.cacheConfigManager = new CacheConfigManager(); + this.databaseManager = databaseManager; // 检测是否在pkg打包环境中运行 const isPkg = process.pkg !== undefined; @@ -42,7 +43,7 @@ class DownloadService { this.taskManager = new TaskManager(this.dataPath); this.progressManager = new ProgressManager(); this.historyManager = new HistoryManager(this.dataPath); - this.downloadRegistry = new DownloadRegistry(this.dataPath); + this.downloadRegistry = new DownloadRegistry(this.dataPath, this.databaseManager); // 先创建下载执行器,稍后在init方法中设置downloadService引用 this.downloadExecutor = new DownloadExecutor(this.fileManager, this.taskManager, this.progressManager, this.historyManager, this); diff --git a/backend/services/registry-migration.js b/backend/services/registry-migration.js new file mode 100644 index 0000000..98478d9 --- /dev/null +++ b/backend/services/registry-migration.js @@ -0,0 +1,461 @@ +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; \ No newline at end of file diff --git a/backend/utils/logger.js b/backend/utils/logger.js index 02a6163..94a621a 100644 --- a/backend/utils/logger.js +++ b/backend/utils/logger.js @@ -61,6 +61,9 @@ const ModuleColors = { 'ArtistService': '\x1b[95m', // 亮紫色 'DownloadService': '\x1b[96m', // 亮青色 'AbortControllerManager': '\x1b[94m', // 亮蓝色 + 'DatabaseManager': '\x1b[95m', // 亮紫色 + 'RegistrySchema': '\x1b[94m', // 亮蓝色 + 'RegistryDatabase': '\x1b[94m', // 亮蓝色 'Default': '\x1b[39m' // 默认颜色 }; diff --git a/package.json b/package.json index 0117c2b..83d5178 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "js-base64": "^3.7.8", "moment": "^2.30.1", "morgan": "^1.10.1", + "mysql2": "^3.15.2", "proxy-agent": "^6.5.0", "qs": "^6.14.0", "uuid": "^11.1.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa2a1ab..094da03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: morgan: specifier: ^1.10.1 version: 1.10.1 + mysql2: + specifier: ^3.15.2 + version: 3.15.2 proxy-agent: specifier: ^6.5.0 version: 6.5.0 @@ -144,6 +147,10 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + axios@0.27.0: resolution: {integrity: sha512-XV/WrPxXfzgZ8j4lcB5i6LyaXmi90yetmV/Fem0kmglGx+mpY06CiweL3YxU6wOTNLmqLUePW4G8h45nGZ/+pA==} deprecated: Formdata complete broken, incorrect build size @@ -266,6 +273,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -401,6 +412,9 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -530,6 +544,9 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -544,10 +561,17 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru-cache@7.18.3: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} + lru.min@1.1.2: + resolution: {integrity: sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -610,6 +634,14 @@ packages: multistream@4.1.0: resolution: {integrity: sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==} + mysql2@3.15.2: + resolution: {integrity: sha512-kFm5+jbwR5mC+lo+3Cy46eHiykWSpUtTLOH3GE+AR7GeLq8PgfJcvpMiyVWk9/O53DjQsqm6a3VOOfq7gYWFRg==} + engines: {node: '>= 8.0'} + + named-placeholders@1.1.3: + resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} + engines: {node: '>=12.0.0'} + napi-build-utils@1.0.2: resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} @@ -802,6 +834,9 @@ packages: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} engines: {node: '>= 18'} + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + serve-static@2.2.0: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} @@ -851,6 +886,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -1045,6 +1084,8 @@ snapshots: at-least-node@1.0.0: {} + aws-ssl-profiles@1.1.2: {} + axios@0.27.0: dependencies: follow-redirects: 1.15.11 @@ -1165,6 +1206,8 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: {} + depd@2.0.0: {} detect-libc@2.1.1: {} @@ -1321,6 +1364,10 @@ snapshots: function-bind@1.1.2: {} + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + get-caller-file@2.0.5: {} get-intrinsic@1.3.0: @@ -1456,6 +1503,8 @@ snapshots: is-promise@4.0.0: {} + is-property@1.0.2: {} + isarray@1.0.0: {} js-base64@3.7.8: {} @@ -1468,8 +1517,12 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + long@5.3.2: {} + lru-cache@7.18.3: {} + lru.min@1.1.2: {} + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} @@ -1522,6 +1575,22 @@ snapshots: once: 1.4.0 readable-stream: 3.6.2 + mysql2@3.15.2: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.0 + long: 5.3.2 + lru.min: 1.1.2 + named-placeholders: 1.1.3 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + named-placeholders@1.1.3: + dependencies: + lru-cache: 7.18.3 + napi-build-utils@1.0.2: {} negotiator@1.0.0: {} @@ -1756,6 +1825,8 @@ snapshots: transitivePeerDependencies: - supports-color + seq-queue@0.0.5: {} + serve-static@2.2.0: dependencies: encodeurl: 2.0.0 @@ -1823,6 +1894,8 @@ snapshots: source-map@0.6.1: optional: true + sqlstring@2.3.3: {} + statuses@2.0.1: {} statuses@2.0.2: {} diff --git a/ui/src/assets/icons/navigation.ts b/ui/src/assets/icons/navigation.ts index 18453f3..2974854 100644 --- a/ui/src/assets/icons/navigation.ts +++ b/ui/src/assets/icons/navigation.ts @@ -18,4 +18,6 @@ export const navigationIcons = { 'cleanup-history2': 'M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z', 'loading': 'M12 6v3l4-4-4-4v3c-4.42 0-8 3.58-8 8 0 1.57.46 3.03 1.24 4.26L6.7 14.8c-.45-.83-.7-1.79-.7-2.8 0-3.31 2.69-6 6-6zm6.76 1.74L17.3 9.2c.44.84.7 1.79.7 2.8 0 3.31-2.69 6-6 6v-3l-4 4 4 4v-3c4.42 0 8-3.58 8-8 0-1.57-.46-3.03-1.24-4.26z', 'menu': 'M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z', -} \ No newline at end of file + 'test-connect':'M228.864 257.28a231.850667 231.850667 0 0 0-27.562667 90.282667c-5.888 59.306667 9.472 111.701333 43.733334 147.626666l56.32 59.52a58.624 58.624 0 0 0 40.96 18.261334h1.578666c14.976 0 29.44-5.76 40.277334-16.085334l28.288-26.752 38.442666 39.381334-25.6 24.192a58.538667 58.538667 0 0 0-2.218666 82.858666l56.32 59.477334c31.658667 33.408 79.189333 51.754667 133.845333 51.754666 44.458667 0 88.533333-12.16 118.528-31.914666l185.898667 185.898666a29.312 29.312 0 0 0 41.429333-41.386666l-187.733333-187.776a231.850667 231.850667 0 0 0 27.52-90.282667c5.802667-59.136-9.557333-111.786667-43.52-147.712l-56.32-59.477333a58.581333 58.581333 0 0 0-40.96-18.304h-1.877334c-14.976 0-29.44 5.76-40.277333 16.085333l-28.8 27.392-38.442667-39.253333 26.325334-24.96a58.624 58.624 0 0 0 2.176-82.816l-56.32-59.52c-31.616-33.365333-79.146667-51.754667-133.802667-51.754667-44.458667 0-88.533333 12.202667-118.528 31.957333L137.258667 82.858667a29.312 29.312 0 0 0-41.429334 41.386666L228.864 257.28z m427.349333 198.4l56.32 59.52c49.92 52.608 26.197333 156.16-5.12 185.728-16.725333 15.872-54.954667 28.501333-94.421333 28.501333-33.706667 0-68.266667-9.173333-91.306667-33.450666l-56.32-59.52 190.848-180.778667z m-104.96 18.773333l-64.853333 61.44-38.4-39.338666 64.853333-61.696 38.4 39.594666zM292.693333 269.141333c16.597333-15.872 54.784-28.501333 94.293334-28.501333 33.664 0 68.266667 9.216 91.264 33.493333l56.32 59.477334-190.677334 180.736-56.32-59.477334C237.653333 402.218667 261.376 298.666667 292.693333 269.141333z' +} + diff --git a/ui/src/assets/icons/viewbox-config.ts b/ui/src/assets/icons/viewbox-config.ts index e5d134a..b2009c8 100644 --- a/ui/src/assets/icons/viewbox-config.ts +++ b/ui/src/assets/icons/viewbox-config.ts @@ -19,6 +19,7 @@ export const LARGE_VIEWBOX_ICONS: Record = { 'trending-up': '0 0 1024 1024', 'trending-down': '0 0 1024 1024', 'database': '0 0 1024 1024', + 'test-connect': '0 0 1024 1024', } /** diff --git a/ui/src/components/common/DatabaseConfigModal.vue b/ui/src/components/common/DatabaseConfigModal.vue new file mode 100644 index 0000000..652b1b3 --- /dev/null +++ b/ui/src/components/common/DatabaseConfigModal.vue @@ -0,0 +1,831 @@ + + + + + \ No newline at end of file diff --git a/ui/src/components/common/RegistryWidget.vue b/ui/src/components/common/RegistryWidget.vue index 4a46f30..b8f8a02 100644 --- a/ui/src/components/common/RegistryWidget.vue +++ b/ui/src/components/common/RegistryWidget.vue @@ -60,6 +60,92 @@

配置选项

+ +
+
+ + 存储模式配置 +
+
+
+
+ +
+ +
+ +
+
+ +
+
+ + + 配置已修改,需要保存 + + + + 当前模式: {{ storageMode === 'json' ? 'JSON文件存储' : 'MySQL数据库存储' }} + +
+ +
+ + + +
+
+ +
+ + +
+ +
+
+
+
+ +
@@ -75,7 +161,7 @@ 注册表检测 推荐
- 优先使用JSON注册表检测作品是否已下载,速度最快 + 优先使用注册表检测作品是否已下载,速度最快
@@ -205,6 +291,13 @@
+ + + @@ -213,14 +306,28 @@ import { ref, onMounted, watch, computed, onUnmounted } from 'vue'; import { storeToRefs } from 'pinia'; import { useRegistryStore } from '@/stores/registry'; import downloadService from '@/services/download'; +import databaseService from '@/services/database'; import SvgIcon from './SvgIcon.vue'; import LoadingSpinner from './LoadingSpinner.vue'; import ErrorMessage from './ErrorMessage.vue'; +import DatabaseConfigModal from './DatabaseConfigModal.vue'; const registryStore = useRegistryStore(); const isOpen = ref(false); const successMessage = ref(null); +// 数据库配置相关状态 +const showDatabaseConfig = ref(false); +const databaseConnected = ref(false); +const storageMode = ref<'json' | 'database'>('json'); +const selectedStorageMode = ref<'json' | 'database'>('json'); // 用户选择的存储模式 +const migrationLoading = ref(false); + +// 计算属性:检查存储模式是否有变更 +const hasStorageModeChanges = computed(() => { + return selectedStorageMode.value !== storageMode.value; +}); + // 从store中获取响应式数据 const { stats, loading, error, config } = storeToRefs(registryStore); @@ -460,6 +567,25 @@ const updateDetectionMethod = async () => { } }; +// 保存存储模式配置 +const saveStorageModeConfig = async (mode: 'json' | 'database') => { + try { + const result = await registryStore.updateConfig({ + useRegistryCheck: config.value.useRegistryCheck, + fallbackToScan: config.value.fallbackToScan, + storageMode: mode + }); + + if (result.success) { + console.log('存储模式配置已保存:', mode); + } else { + console.warn('保存存储模式配置失败:', result.error); + } + } catch (error) { + console.error('保存存储模式配置时出错:', error); + } +}; + // 更新配置(保留原方法以防其他地方调用) const updateConfig = async () => { const result = await registryStore.updateConfig({ @@ -490,6 +616,98 @@ const showError = (message: string) => { registryStore.error = message; }; +// 数据库配置相关方法 +const openDatabaseConfig = () => { + showDatabaseConfig.value = true; +}; + +const closeDatabaseConfig = () => { + showDatabaseConfig.value = false; +}; + +const handleDatabaseConfigSaved = async () => { + showDatabaseConfig.value = false; + await checkDatabaseConnection(); + showSuccess('数据库配置已保存'); +}; + +const checkDatabaseConnection = async () => { + try { + const result = await databaseService.getConnectionStatus(); + databaseConnected.value = result.success && (result.data?.connected || false); + } catch (error) { + console.error('检查数据库连接失败:', error); + databaseConnected.value = false; + } +}; + +// 应用存储模式配置 +const applyStorageModeConfig = async () => { + if (selectedStorageMode.value === 'database' && !databaseConnected.value) { + showError('请先配置并连接数据库'); + return; + } + + if (selectedStorageMode.value === storageMode.value) { + return; + } + + const confirmMessage = selectedStorageMode.value === 'database' + ? '确定要切换到数据库存储模式吗?这将使用MySQL数据库存储注册表数据。' + : '确定要切换到JSON文件存储模式吗?这将使用本地JSON文件存储注册表数据。'; + + if (!confirm(confirmMessage)) { + return; + } + + try { + migrationLoading.value = true; + + if (selectedStorageMode.value === 'database') { + // 从JSON迁移到数据库 + const result = await databaseService.migrateData('json-to-db'); + if (result.success) { + storageMode.value = 'database'; + showSuccess(`成功迁移到数据库存储,处理了 ${result.data?.recordsProcessed || 0} 条记录`); + } else { + throw new Error(result.error || '迁移到数据库失败'); + } + } else { + // 从数据库迁移到JSON + const result = await databaseService.migrateData('db-to-json'); + if (result.success) { + storageMode.value = 'json'; + showSuccess(`成功迁移到JSON存储,处理了 ${result.data?.recordsProcessed || 0} 条记录`); + } else { + throw new Error(result.error || '迁移到JSON失败'); + } + } + + // 保存存储模式配置到后端 + await saveStorageModeConfig(selectedStorageMode.value); + + // 刷新统计信息 + await refreshStats(); + } catch (error) { + console.error('存储模式切换失败:', error); + showError(`存储模式切换失败: ${error instanceof Error ? error.message : '未知错误'}`); + // 恢复选择状态 + selectedStorageMode.value = storageMode.value; + } finally { + migrationLoading.value = false; + } +}; + +// 重置存储模式配置 +const resetStorageModeConfig = () => { + selectedStorageMode.value = storageMode.value; +}; + +const switchStorageMode = async (mode: 'json' | 'database') => { + // 保留原函数以防其他地方调用,但现在只是更新选择状态 + selectedStorageMode.value = mode; +}; + // 格式化日期 const formatDate = (dateString?: string): string => { if (!dateString) return '未知'; @@ -507,18 +725,35 @@ const initDetectionMethod = () => { } }; +// 初始化存储模式 +const initStorageMode = () => { + if (config.value.storageMode) { + storageMode.value = config.value.storageMode; + selectedStorageMode.value = config.value.storageMode; // 同时更新选择状态 + } else { + storageMode.value = 'json'; // 默认值 + selectedStorageMode.value = 'json'; + } +}; + // 组件挂载时初始化 onMounted(async () => { - // 从后端获取配置并初始化检测方法 + // 从后端获取配置并初始化检测方法和存储模式 try { await registryStore.fetchConfig(); initDetectionMethod(); + initStorageMode(); } catch (error) { console.error('获取配置失败:', error); // 如果获取配置失败,使用默认值 detectionMethod.value = 'hybrid'; + storageMode.value = 'json'; + selectedStorageMode.value = 'json'; } + // 检查数据库连接状态 + await checkDatabaseConnection(); + // 初始化时加载统计数据 refreshStats(); }); @@ -528,9 +763,10 @@ onUnmounted(() => { stopProgressPolling(); }); -// 监听配置变化,自动更新检测方法 +// 监听配置变化,自动更新检测方法和存储模式 watch(config, () => { initDetectionMethod(); + initStorageMode(); }, { deep: true }); @@ -1703,7 +1939,241 @@ watch(config, () => { font-size: 1rem; } -/* 响应式设计 - 进度显示 */ +/* 存储模式配置样式 */ +.storage-mode-section { + display: flex; + flex-direction: column; + gap: var(--spacing-lg, 1rem); +} + +.storage-options { + display: flex; + flex-direction: column; + gap: var(--spacing-md, 0.75rem); +} + +.storage-option { + background: var(--color-bg-primary, white); + border: 2px solid var(--color-border-light, #e2e8f0); + border-radius: var(--radius-lg, 0.5rem); + padding: var(--spacing-md, 0.75rem); + transition: all var(--transition-normal); + cursor: pointer; + position: relative; + overflow: hidden; +} + +.storage-option:hover { + border-color: var(--color-border-hover, #cbd5e1); + box-shadow: var(--shadow-sm); +} + +.storage-option.active { + border-color: var(--color-primary, #3b82f6); + background: var(--color-primary-light, #eff6ff); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.storage-option.disabled { + opacity: 0.6; + cursor: not-allowed; + background: var(--color-bg-tertiary, #f8fafc); +} + +.storage-option.disabled:hover { + border-color: var(--color-border-light, #e2e8f0); + box-shadow: none; +} + +.storage-option label { + display: flex; + align-items: flex-start; + gap: var(--spacing-md, 0.75rem); + cursor: pointer; + width: 100%; +} + +.storage-option.disabled label { + cursor: not-allowed; +} + +.storage-option input[type="radio"] { + width: 1.25rem; + height: 1.25rem; + margin-top: 0.125rem; + accent-color: var(--color-primary); + cursor: pointer; + flex-shrink: 0; +} + +.storage-option.disabled input[type="radio"] { + cursor: not-allowed; +} + +.option-badge.advanced { + background: var(--color-info-light, #e0f2fe); + color: var(--color-info-text, #0c4a6e); + border: 1px solid var(--color-info-border, #7dd3fc); +} + +.option-badge.advanced.connected { + background: var(--color-success-light, #dcfce7); + color: var(--color-success-text, #166534); + border: 1px solid var(--color-success-border, #bbf7d0); +} + +.database-actions { + display: flex; + align-items: center; + gap: var(--spacing-md, 0.75rem); + padding: var(--spacing-md, 0.75rem); + background: var(--color-bg-secondary, #f8fafc); + border-radius: var(--radius-md, 0.375rem); + border: 1px solid var(--color-border-light, #e2e8f0); +} + +.migration-status { + display: flex; + align-items: center; + gap: var(--spacing-sm, 0.5rem); + color: var(--color-text-secondary, #6b7280); + font-size: 0.875rem; +} + +/* 存储配置操作样式 */ +.storage-config-actions { + display: flex; + flex-direction: column; + gap: var(--spacing-md, 0.75rem); + padding: var(--spacing-md, 0.75rem); + background: var(--color-bg-secondary, #f8fafc); + border-radius: var(--radius-md, 0.375rem); + border: 1px solid var(--color-border-light, #e2e8f0); +} + +.config-status { + display: flex; + align-items: center; + justify-content: center; + padding: var(--spacing-sm, 0.5rem); +} + +.config-indicator { + display: flex; + align-items: center; + gap: var(--spacing-xs, 0.25rem); + font-size: 0.875rem; + font-weight: 500; + padding: var(--spacing-xs, 0.25rem) var(--spacing-sm, 0.5rem); + border-radius: var(--radius-sm, 0.25rem); + transition: all var(--transition-normal); +} + +.config-indicator.saved { + color: var(--color-success-text, #166534); + background: var(--color-success-light, #dcfce7); + border: 1px solid var(--color-success-border, #bbf7d0); +} + +.config-indicator.unsaved { + color: var(--color-warning-text, #92400e); + background: var(--color-warning-light, #fef3c7); + border: 1px solid var(--color-warning-border, #fbbf24); + animation: pulse 2s ease-in-out infinite; +} + +.indicator-icon { + width: 0.875rem; + height: 0.875rem; + flex-shrink: 0; +} + +.storage-config-actions .action-buttons { + display: flex; + gap: var(--spacing-sm, 0.5rem); + justify-content: center; +} + +.storage-config-actions .btn { + flex: 1; + min-width: 0; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.7; + } +} + +/* 配置节样式增强 */ +.config-section { + margin-bottom: var(--spacing-xl, 1.5rem); + background: var(--color-bg-primary, white); + border: 1px solid var(--color-border-light, #e2e8f0); + border-radius: var(--radius-lg, 0.5rem); + overflow: hidden; + position: relative; +} + +.config-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, var(--color-primary), var(--color-secondary)); +} + +.config-section-title { + display: flex; + align-items: center; + gap: var(--spacing-sm, 0.5rem); + padding: var(--spacing-lg, 1rem); + background: var(--color-bg-secondary, #f8fafc); + border-bottom: 1px solid var(--color-border-light, #e2e8f0); + font-weight: 600; + color: var(--color-text-primary, #374151); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.section-icon { + width: 1rem; + height: 1rem; + color: var(--color-primary); +} + +.config-options, +.storage-options { + padding: var(--spacing-lg, 1rem); +} + +/* 响应式设计 - 存储配置 */ +@media (max-width: 768px) { + .database-actions { + flex-direction: column; + align-items: stretch; + } + + .migration-status { + justify-content: center; + } + + .storage-config-actions .action-buttons { + flex-direction: column; + } + + .storage-config-actions .btn { + flex: none; + } +} + +/* 原有的配置选项样式保持不变 */ @media (max-width: 768px) { .progress-stats { grid-template-columns: repeat(2, 1fr); diff --git a/ui/src/services/database.ts b/ui/src/services/database.ts new file mode 100644 index 0000000..77cc310 --- /dev/null +++ b/ui/src/services/database.ts @@ -0,0 +1,239 @@ +import axios from 'axios'; + +interface DatabaseConfig { + host: string; + port: number; + user: string; + password: string; + database: string; + connectionLimit: number; + acquireTimeout: number; + ssl: boolean; +} + +interface ApiResponse { + success: boolean; + data?: T; + error?: string; +} + +interface ConnectionTestResult { + connected: boolean; + version?: string; + serverInfo?: string; + connectionId?: number; + uptime?: number; +} + +interface MigrationResult { + success: boolean; + direction: 'json-to-db' | 'db-to-json'; + recordsProcessed: number; + message: string; +} + +class DatabaseService { + private baseURL = '/api/database'; + + /** + * 测试数据库连接 + */ + async testConnection(config: DatabaseConfig): Promise> { + try { + const response = await axios.post(`${this.baseURL}/test-connection`, config); + return response.data; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.error || error.message || '连接测试失败' + }; + } + } + + /** + * 保存数据库配置 + */ + async saveConfig(config: DatabaseConfig): Promise { + try { + const response = await axios.post(`${this.baseURL}/config`, config); + return response.data; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.error || error.message || '保存配置失败' + }; + } + } + + /** + * 获取数据库配置 + */ + async getConfig(): Promise> { + try { + const response = await axios.get(`${this.baseURL}/config`); + return response.data; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.error || error.message || '获取配置失败' + }; + } + } + + /** + * 检查数据库连接状态 + */ + async getConnectionStatus(): Promise }>> { + try { + const response = await axios.get(`${this.baseURL}/status`); + return response.data; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.error || error.message || '获取连接状态失败' + }; + } + } + + /** + * 初始化数据库表结构 + */ + async initializeTables(): Promise { + try { + const response = await axios.post(`${this.baseURL}/initialize`); + return response.data; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.error || error.message || '初始化数据库失败' + }; + } + } + + /** + * 执行数据迁移 + */ + async migrateData(direction: 'json-to-db' | 'db-to-json', overwrite: boolean = true): Promise> { + try { + const response = await axios.post(`${this.baseURL}/migrate/${direction}`, { + overwrite + }); + return response.data; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.error || error.message || '数据迁移失败' + }; + } + } + + /** + * 获取注册表统计信息(数据库版本) + */ + async getRegistryStats(): Promise> { + try { + const response = await axios.get(`${this.baseURL}/registry/stats`); + return response.data; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.error || error.message || '获取统计信息失败' + }; + } + } + + /** + * 导出数据库注册表数据 + */ + async exportRegistry(): Promise> { + try { + const response = await axios.get(`${this.baseURL}/registry/export`); + return response.data; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.error || error.message || '导出数据失败' + }; + } + } + + /** + * 导入数据到数据库注册表 + */ + async importRegistry(data: any): Promise> { + try { + const response = await axios.post(`${this.baseURL}/registry/import`, data); + return response.data; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.error || error.message || '导入数据失败' + }; + } + } + + /** + * 从文件系统重建数据库注册表 + */ + async rebuildRegistry(): Promise> { + try { + const response = await axios.post(`${this.baseURL}/registry/rebuild`); + return response.data; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.error || error.message || '重建注册表失败' + }; + } + } + + /** + * 清理数据库注册表 + */ + async cleanupRegistry(): Promise> { + try { + const response = await axios.post(`${this.baseURL}/registry/cleanup`); + return response.data; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.error || error.message || '清理注册表失败' + }; + } + } + + /** + * 断开数据库连接 + */ + async disconnect(): Promise { + try { + const response = await axios.post(`${this.baseURL}/disconnect`); + return response.data; + } catch (error: any) { + return { + success: false, + error: error.response?.data?.error || error.message || '断开连接失败' + }; + } + } +} + +export default new DatabaseService(); +export type { DatabaseConfig, ApiResponse, ConnectionTestResult, MigrationResult }; \ No newline at end of file diff --git a/ui/src/stores/registry.ts b/ui/src/stores/registry.ts index 664e872..976ab9e 100644 --- a/ui/src/stores/registry.ts +++ b/ui/src/stores/registry.ts @@ -1,6 +1,7 @@ import { defineStore } from 'pinia'; import { ref } from 'vue'; import downloadService from '@/services/download'; +import databaseService from '@/services/database'; import { getApiBaseUrl } from '@/services/api'; export interface RegistryStats { @@ -12,6 +13,7 @@ export interface RegistryStats { export interface RegistryConfig { useRegistryCheck: boolean; fallbackToScan: boolean; + storageMode?: 'json' | 'database'; } export const useRegistryStore = defineStore('registry', () => { @@ -19,18 +21,25 @@ export const useRegistryStore = defineStore('registry', () => { const stats = ref(null); const config = ref({ useRegistryCheck: true, - fallbackToScan: false + fallbackToScan: false, + storageMode: 'json' }); const loading = ref(false); const error = ref(null); // 获取注册表统计信息 - const fetchStats = async () => { + const fetchStats = async (useDatabase = false) => { try { loading.value = true; error.value = null; - const response = await downloadService.getRegistryStats(); + let response; + if (useDatabase) { + response = await databaseService.getRegistryStats(); + } else { + response = await downloadService.getRegistryStats(); + } + if (response.success) { // 映射API响应数据到组件期望的格式 stats.value = { @@ -50,27 +59,23 @@ export const useRegistryStore = defineStore('registry', () => { }; // 导出注册表 - const exportRegistry = async () => { + const exportRegistry = async (useDatabase = false) => { try { loading.value = true; error.value = null; - const response = await downloadService.exportRegistry(); + let response; + if (useDatabase) { + response = await databaseService.exportRegistry(); + } else { + response = await downloadService.exportRegistry(); + } - // 创建下载链接 - const blob = new Blob([JSON.stringify(response, null, 2)], { - type: 'application/json' - }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `download-registry-${new Date().toISOString().split('T')[0]}.json`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - - return { success: true }; + if (response.success) { + return { success: true, data: response.data }; + } else { + throw new Error(response.error || '导出注册表失败'); + } } catch (err: any) { error.value = err.message || '导出注册表失败'; console.error('导出注册表失败:', err); @@ -81,18 +86,21 @@ export const useRegistryStore = defineStore('registry', () => { }; // 导入注册表 - const importRegistry = async (file: File) => { + const importRegistry = async (data: any, useDatabase = false) => { try { loading.value = true; error.value = null; - const text = await file.text(); - const registryData = JSON.parse(text); + let response; + if (useDatabase) { + response = await databaseService.importRegistry(data); + } else { + response = await downloadService.importRegistry(data); + } - const response = await downloadService.importRegistry(registryData); if (response.success) { // 刷新统计信息 - await fetchStats(); + await fetchStats(useDatabase); return { success: true, data: response.data }; } else { throw new Error(response.error || '导入注册表失败'); @@ -107,15 +115,21 @@ export const useRegistryStore = defineStore('registry', () => { }; // 重建注册表 - const rebuildRegistry = async () => { + const rebuildRegistry = async (useDatabase = false) => { try { loading.value = true; error.value = null; - const response = await downloadService.rebuildRegistry(); + let response; + if (useDatabase) { + response = await databaseService.rebuildRegistry(); + } else { + response = await downloadService.rebuildRegistry(); + } + if (response.success) { // 刷新统计信息 - await fetchStats(); + await fetchStats(useDatabase); return { success: true, data: response.data }; } else { throw new Error(response.error || '重建注册表失败'); @@ -130,15 +144,21 @@ export const useRegistryStore = defineStore('registry', () => { }; // 清理注册表 - const cleanupRegistry = async () => { + const cleanupRegistry = async (useDatabase = false) => { try { loading.value = true; error.value = null; - const response = await downloadService.cleanupRegistry(); + let response; + if (useDatabase) { + response = await databaseService.cleanupRegistry(); + } else { + response = await downloadService.cleanupRegistry(); + } + if (response.success) { // 刷新统计信息 - await fetchStats(); + await fetchStats(useDatabase); return { success: true, data: response.data }; } else { throw new Error(response.error || '清理注册表失败'); @@ -159,7 +179,7 @@ export const useRegistryStore = defineStore('registry', () => { const result = await response.json(); if (result.success && result.data) { - config.value = result.data; + config.value = { ...config.value, ...result.data }; return result.data; } else { throw new Error(result.error || '获取配置失败'); diff --git a/ui/src/stores/update.ts b/ui/src/stores/update.ts index 8845181..a63a70a 100644 --- a/ui/src/stores/update.ts +++ b/ui/src/stores/update.ts @@ -21,6 +21,23 @@ export const useUpdateStore = defineStore('update', () => { const isChecking = ref(false) const lastCheckTime = ref(null) + // 从localStorage加载上次检查时间 + const loadLastCheckTime = () => { + const stored = localStorage.getItem('pixiv-manager-last-update-check') + if (stored) { + lastCheckTime.value = new Date(stored) + } + } + + // 保存检查时间到localStorage + const saveLastCheckTime = (time: Date) => { + lastCheckTime.value = time + localStorage.setItem('pixiv-manager-last-update-check', time.toISOString()) + } + + // 初始化时加载上次检查时间 + loadLastCheckTime() + // 检查更新 const checkUpdate = async (silent = false): Promise => { if (isChecking.value) return null @@ -33,7 +50,7 @@ export const useUpdateStore = defineStore('update', () => { if (result.success) { updateInfo.value = result.data - lastCheckTime.value = new Date() + saveLastCheckTime(new Date()) return result.data } else { if (!silent) { @@ -53,8 +70,8 @@ export const useUpdateStore = defineStore('update', () => { // 自动检查更新(登录后调用) const autoCheckUpdate = async () => { - // 如果距离上次检查不足1小时,跳过 - if (lastCheckTime.value && Date.now() - lastCheckTime.value.getTime() < 60 * 60 * 1000) { + // 如果距离上次检查不足24小时(1天),跳过 + if (lastCheckTime.value && Date.now() - lastCheckTime.value.getTime() < 24 * 60 * 60 * 1000) { return } @@ -89,4 +106,4 @@ export const useUpdateStore = defineStore('update', () => { autoCheckUpdate, getCurrentVersion } -}) \ No newline at end of file +}) \ No newline at end of file