From 4b8bf68179c64a737af10dee045c5c4fae69535c Mon Sep 17 00:00:00 2001 From: mtvpls Date: Wed, 29 Apr 2026 16:09:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=BF=81=E7=A7=BB=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=94=B5=E5=AD=90=E4=B9=A6=E5=92=8C=E6=BC=AB=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/admin/data_migration/export/route.ts | 18 +++-- .../api/admin/data_migration/import/route.ts | 68 +++++++++++++++++++ src/components/DataMigration.tsx | 33 +++++++++ src/lib/d1.db.ts | 14 ++-- src/lib/postgres.db.ts | 14 ++-- 5 files changed, 135 insertions(+), 12 deletions(-) diff --git a/src/app/api/admin/data_migration/export/route.ts b/src/app/api/admin/data_migration/export/route.ts index dc84755..7ce6b5b 100644 --- a/src/app/api/admin/data_migration/export/route.ts +++ b/src/app/api/admin/data_migration/export/route.ts @@ -44,7 +44,7 @@ export async function POST(req: NextRequest) { } // 解析请求体获取密码 - const { password } = await req.json(); + const { password, includeMangaData = true, includeBookData = true } = await req.json(); if (!password || typeof password !== 'string') { return NextResponse.json({ error: '请提供加密密码' }, { status: 400 }); } @@ -59,7 +59,7 @@ export async function POST(req: NextRequest) { // 所有用户数据 userData: {} as { [username: string]: any }, // V2用户信息 - usersV2: [] as any[] + usersV2: [] as any[], } }; @@ -116,14 +116,22 @@ export async function POST(req: NextRequest) { searchHistory, skipConfigs, musicV2History, - playlists + playlists, + mangaShelf, + mangaReadRecords, + bookShelf, + bookReadRecords ] = await Promise.all([ db.getAllPlayRecords(username), db.getAllFavorites(username), db.getSearchHistory(username), db.getAllSkipConfigs(username), db.listMusicV2History(username), - db.listMusicV2Playlists(username) + db.listMusicV2Playlists(username), + includeMangaData ? db.getAllMangaShelf(username) : Promise.resolve({}), + includeMangaData ? db.getAllMangaReadRecords(username) : Promise.resolve({}), + includeBookData ? db.getAllBookShelf(username) : Promise.resolve({}), + includeBookData ? db.getAllBookReadRecords(username) : Promise.resolve({}) ]); // 并行获取所有歌单的歌曲 @@ -143,6 +151,8 @@ export async function POST(req: NextRequest) { skipConfigs, musicV2History, musicV2Playlists: playlistsWithSongs, + ...(includeMangaData ? { mangaData: { shelf: mangaShelf, readRecords: mangaReadRecords } } : {}), + ...(includeBookData ? { bookData: { shelf: bookShelf, readRecords: bookReadRecords } } : {}), passwordV2: finalPasswordV2 } }; diff --git a/src/app/api/admin/data_migration/import/route.ts b/src/app/api/admin/data_migration/import/route.ts index f36aee6..d368e63 100644 --- a/src/app/api/admin/data_migration/import/route.ts +++ b/src/app/api/admin/data_migration/import/route.ts @@ -80,6 +80,32 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: '备份文件格式无效' }, { status: 400 }); } + const importUsernames = Object.keys(importData.data.userData || {}); + const backupHasMangaData = importUsernames.some((name) => Object.prototype.hasOwnProperty.call(importData.data.userData?.[name] || {}, 'mangaData')); + const backupHasBookData = importUsernames.some((name) => Object.prototype.hasOwnProperty.call(importData.data.userData?.[name] || {}, 'bookData')); + const preserveMangaData = !backupHasMangaData; + const preserveBookData = !backupHasBookData; + + const preservedMangaData = preserveMangaData + ? Object.fromEntries(await Promise.all(importUsernames.map(async (name) => ([ + name, + { + mangaShelf: await db.getAllMangaShelf(name), + mangaReadRecords: await db.getAllMangaReadRecords(name), + }, + ])))) + : {}; + + const preservedBookData = preserveBookData + ? Object.fromEntries(await Promise.all(importUsernames.map(async (name) => ([ + name, + { + bookShelf: await db.getAllBookShelf(name), + bookReadRecords: await db.getAllBookReadRecords(name), + }, + ])))) + : {}; + // 开始导入数据 - 先清空现有数据 updateProgress(username, 'import', 'clearing', 0, 1, '正在清空现有数据...'); await db.clearAllData(); @@ -371,6 +397,46 @@ export async function POST(req: NextRequest) { } } } + })(), + + // 导入漫画书架 / 阅读记录 + (async () => { + if (!backupHasMangaData) return; + const mangaShelfEntries = Object.entries((user.mangaData?.shelf || preservedMangaData[username]?.mangaShelf || {})); + for (let j = 0; j < mangaShelfEntries.length; j += DATA_BATCH_SIZE) { + const batch = mangaShelfEntries.slice(j, j + DATA_BATCH_SIZE); + await Promise.all( + batch.map(([, item]: [string, any]) => db.saveMangaShelf(username, item.sourceId, item.mangaId, item)) + ); + } + + const mangaReadEntries = Object.entries((user.mangaData?.readRecords || preservedMangaData[username]?.mangaReadRecords || {})); + for (let j = 0; j < mangaReadEntries.length; j += DATA_BATCH_SIZE) { + const batch = mangaReadEntries.slice(j, j + DATA_BATCH_SIZE); + await Promise.all( + batch.map(([, record]: [string, any]) => db.saveMangaReadRecord(username, record.sourceId, record.mangaId, record)) + ); + } + })(), + + // 导入电子书书架 / 阅读记录 + (async () => { + if (!backupHasBookData) return; + const bookShelfEntries = Object.entries((user.bookData?.shelf || preservedBookData[username]?.bookShelf || {})); + for (let j = 0; j < bookShelfEntries.length; j += DATA_BATCH_SIZE) { + const batch = bookShelfEntries.slice(j, j + DATA_BATCH_SIZE); + await Promise.all( + batch.map(([, item]: [string, any]) => db.saveBookShelf(username, item.sourceId, item.bookId, item)) + ); + } + + const bookReadEntries = Object.entries((user.bookData?.readRecords || preservedBookData[username]?.bookReadRecords || {})); + for (let j = 0; j < bookReadEntries.length; j += DATA_BATCH_SIZE) { + const batch = bookReadEntries.slice(j, j + DATA_BATCH_SIZE); + await Promise.all( + batch.map(([, record]: [string, any]) => db.saveBookReadRecord(username, record.sourceId, record.bookId, record)) + ); + } })() ]); @@ -404,6 +470,8 @@ export async function POST(req: NextRequest) { message: '数据导入成功', importedUsers: Object.keys(userData).length, importedUsersV2: importData.data.usersV2?.length || 0, + importedMangaData: backupHasMangaData, + importedBookData: backupHasBookData, timestamp: importData.timestamp, serverVersion: typeof importData.serverVersion === 'string' ? importData.serverVersion : '未知版本' }); diff --git a/src/components/DataMigration.tsx b/src/components/DataMigration.tsx index 012a524..2959376 100644 --- a/src/components/DataMigration.tsx +++ b/src/components/DataMigration.tsx @@ -142,6 +142,8 @@ const DataMigration = ({ onRefreshConfig }: DataMigrationProps) => { const [exportPassword, setExportPassword] = useState(''); const [importPassword, setImportPassword] = useState(''); const [selectedFile, setSelectedFile] = useState(null); + const [includeMangaExport, setIncludeMangaExport] = useState(true); + const [includeBooksExport, setIncludeBooksExport] = useState(true); const [isExporting, setIsExporting] = useState(false); const [isImporting, setIsImporting] = useState(false); const [exportProgress, setExportProgress] = useState<{ @@ -216,6 +218,8 @@ const DataMigration = ({ onRefreshConfig }: DataMigrationProps) => { }, body: JSON.stringify({ password: exportPassword, + includeMangaData: includeMangaExport, + includeBookData: includeBooksExport, }), }); @@ -336,6 +340,8 @@ const DataMigration = ({ onRefreshConfig }: DataMigrationProps) => {

导入的用户数量: ${result.importedUsers}

备份时间: ${new Date(result.timestamp).toLocaleString('zh-CN')}

服务器版本: ${result.serverVersion || '未知版本'}

+

漫画数据: ${result.importedMangaData ? '已导入' : '未导入'}

+

电子书数据: ${result.importedBookData ? '已导入' : '未导入'}

请刷新页面以查看最新数据。

`, @@ -419,6 +425,30 @@ const DataMigration = ({ onRefreshConfig }: DataMigrationProps) => {

+
+

附加数据

+ + +
+ {/* 备份内容列表 */}

备份内容:

@@ -427,6 +457,8 @@ const DataMigration = ({ onRefreshConfig }: DataMigrationProps) => {
• 用户数据
• 播放记录
• 收藏夹
+
• 搜索历史
+
• 音乐数据
@@ -520,6 +552,7 @@ const DataMigration = ({ onRefreshConfig }: DataMigrationProps) => { disabled={isImporting} /> + {/* 导入按钮 */} diff --git a/src/lib/d1.db.ts b/src/lib/d1.db.ts index 75efdfb..6f00e9a 100644 --- a/src/lib/d1.db.ts +++ b/src/lib/d1.db.ts @@ -2804,9 +2804,6 @@ export class D1Storage implements IStorage { 'book_shelf', 'book_read_records', 'skip_configs', - 'music_play_records', - 'music_playlists', - 'music_playlist_songs', 'music_v2_history', 'music_v2_playlists', 'music_v2_playlist_items', @@ -2819,7 +2816,16 @@ export class D1Storage implements IStorage { ]; for (const table of tables) { - await this.db.prepare(`DELETE FROM ${table}`).run(); + try { + await this.db.prepare(`DELETE FROM ${table}`).run(); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + if (message.includes('no such table') || message.includes('does not exist')) { + console.warn('D1Storage.clearAllData warning:', table, message); + continue; + } + throw err; + } } } catch (err) { console.error('D1Storage.clearAllData error:', err); diff --git a/src/lib/postgres.db.ts b/src/lib/postgres.db.ts index 391af5e..4631287 100644 --- a/src/lib/postgres.db.ts +++ b/src/lib/postgres.db.ts @@ -2796,9 +2796,6 @@ export class PostgresStorage implements IStorage { 'book_shelf', 'book_read_records', 'skip_configs', - 'music_play_records', - 'music_playlists', - 'music_playlist_songs', 'music_v2_history', 'music_v2_playlists', 'music_v2_playlist_items', @@ -2811,7 +2808,16 @@ export class PostgresStorage implements IStorage { ]; for (const table of tables) { - await this.db.prepare(`DELETE FROM ${table}`).run(); + try { + await this.db.prepare(`DELETE FROM ${table}`).run(); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + if (message.includes('no such table') || message.includes('does not exist')) { + console.warn('PostgresStorage.clearAllData warning:', table, message); + continue; + } + throw err; + } } } catch (err) { console.error('PostgresStorage.clearAllData error:', err);