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);