From 97d748fa79d24d7ba8ff18e946999ad0f1fe80d0 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Thu, 1 Jan 2026 22:21:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BD=BB=E5=BA=95=E7=A7=BB=E9=99=A4=E6=97=A7?= =?UTF-8?q?=E7=89=88=E7=94=A8=E6=88=B7=E5=AF=BC=E5=85=A5=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/admin/data_migration/export/route.ts | 68 +++++---- .../api/admin/data_migration/import/route.ts | 143 +++++++++--------- src/lib/redis-base.db.ts | 4 + 3 files changed, 113 insertions(+), 102 deletions(-) diff --git a/src/app/api/admin/data_migration/export/route.ts b/src/app/api/admin/data_migration/export/route.ts index 77ea112..7d974d3 100644 --- a/src/app/api/admin/data_migration/export/route.ts +++ b/src/app/api/admin/data_migration/export/route.ts @@ -61,23 +61,39 @@ export async function POST(req: NextRequest) { }; // 获取所有V2用户 - const usersV2Result = await db.getUserListV2(0, 10000, process.env.USERNAME); + const usersV2Result = await db.getUserListV2(0, 1000000, process.env.USERNAME); exportData.data.usersV2 = usersV2Result.users; + console.log(`从getUserListV2获取到 ${usersV2Result.users.length} 个用户`); - // 获取所有用户(包括旧版用户) + // 获取所有用户(getAllUsers返回的是V2用户) let allUsers = await db.getAllUsers(); - // 添加站长用户 - allUsers.push(process.env.USERNAME); - // 添加V2用户 + allUsers.push(process.env.USERNAME); // 添加站长 + // 添加V2用户列表中的用户 usersV2Result.users.forEach(user => { if (!allUsers.includes(user.username)) { allUsers.push(user.username); } }); allUsers = Array.from(new Set(allUsers)); + console.log(`准备导出 ${allUsers.length} 个V2用户(包括站长)`); - // 为每个用户收集数据 + // 为每个用户收集数据(只导出V2用户) + let exportedCount = 0; for (const username of allUsers) { + // 站长特殊处理:使用环境变量密码 + let finalPasswordV2 = username === process.env.USERNAME ? process.env.PASSWORD : null; + + // 如果不是站长,获取V2密码 + if (!finalPasswordV2) { + finalPasswordV2 = await getUserPasswordV2(username); + } + + // 跳过没有V2密码的用户 + if (!finalPasswordV2) { + console.log(`跳过用户 ${username}:没有V2密码`); + continue; + } + const userData = { // 播放记录 playRecords: await db.getAllPlayRecords(username), @@ -87,17 +103,15 @@ export async function POST(req: NextRequest) { searchHistory: await db.getSearchHistory(username), // 跳过片头片尾配置 skipConfigs: await db.getAllSkipConfigs(username), - // 用户密码(通过验证空密码来检查用户是否存在,然后获取密码) - password: await getUserPassword(username), // V2用户的加密密码 - passwordV2: await getUserPasswordV2(username) + passwordV2: finalPasswordV2 }; exportData.data.userData[username] = userData; + exportedCount++; } - // 覆盖站长密码 - exportData.data.userData[process.env.USERNAME].password = process.env.PASSWORD; + console.log(`成功导出 ${exportedCount} 个用户的数据`); // 将数据转换为JSON字符串 const jsonData = JSON.stringify(exportData); @@ -132,32 +146,22 @@ export async function POST(req: NextRequest) { } } -// 辅助函数:获取用户密码(通过数据库直接访问) -async function getUserPassword(username: string): Promise { - try { - // 使用 Redis 存储的直接访问方法 - const storage = (db as any).storage; - if (storage && typeof storage.client?.get === 'function') { - const passwordKey = `u:${username}:pwd`; - const password = await storage.client.get(passwordKey); - return password; - } - return null; - } catch (error) { - console.error(`获取用户 ${username} 密码失败:`, error); - return null; - } -} - // 辅助函数:获取V2用户的加密密码 async function getUserPasswordV2(username: string): Promise { try { const storage = (db as any).storage; - if (storage && typeof storage.client?.hget === 'function') { - const userInfoKey = `user:${username}:info`; - const password = await storage.client.hget(userInfoKey, 'password'); - return password; + if (!storage) return null; + + // 直接调用hGetAll获取完整用户信息(包括密码) + const userInfoKey = `user:${username}:info`; + + if (typeof storage.withRetry === 'function' && storage.client?.hGetAll) { + const userInfo = await storage.withRetry(() => storage.client.hGetAll(userInfoKey)); + if (userInfo && userInfo.password) { + return userInfo.password; + } } + return null; } catch (error) { console.error(`获取用户 ${username} V2密码失败:`, error); diff --git a/src/app/api/admin/data_migration/import/route.ts b/src/app/api/admin/data_migration/import/route.ts index 56ec7ac..de68ce5 100644 --- a/src/app/api/admin/data_migration/import/route.ts +++ b/src/app/api/admin/data_migration/import/route.ts @@ -80,6 +80,13 @@ export async function POST(req: NextRequest) { // 开始导入数据 - 先清空现有数据 await db.clearAllData(); + // 额外清除所有V2用户(clearAllData可能只清除旧版用户) + const existingUsers = await db.getUserListV2(0, 1000000, process.env.USERNAME); + for (const user of existingUsers.users) { + await db.deleteUserV2(user.username); + } + console.log(`已清除 ${existingUsers.users.length} 个现有V2用户`); + // 导入管理员配置 importData.data.adminConfig = configSelfCheck(importData.data.adminConfig); await db.saveAdminConfig(importData.data.adminConfig); @@ -94,79 +101,73 @@ export async function POST(req: NextRequest) { // 不影响主流程,继续执行 } - // 导入V2用户信息 - if (importData.data.usersV2 && Array.isArray(importData.data.usersV2)) { - for (const userV2 of importData.data.usersV2) { - try { - // 跳过环境变量中的站长(站长使用环境变量认证) - if (userV2.username === process.env.USERNAME) { - console.log(`跳过站长 ${userV2.username} 的导入`); - continue; - } - - // 获取用户的加密密码 - const userData = importData.data.userData[userV2.username]; - const passwordV2 = userData?.passwordV2; - - if (passwordV2) { - // 将站长角色转换为普通角色 - const importedRole = userV2.role === 'owner' ? 'user' : userV2.role; - if (userV2.role === 'owner') { - console.log(`用户 ${userV2.username} 的角色从 owner 转换为 user`); - } - - // 直接使用加密后的密码创建用户 - const storage = (db as any).storage; - if (storage && typeof storage.client?.hset === 'function') { - const userInfoKey = `user:${userV2.username}:info`; - const createdAt = userV2.created_at || Date.now(); - - const userInfo: any = { - role: importedRole, - banned: userV2.banned, - password: passwordV2, - created_at: createdAt.toString(), - }; - - if (userV2.tags && userV2.tags.length > 0) { - userInfo.tags = JSON.stringify(userV2.tags); - } - - if (userV2.oidcSub) { - userInfo.oidcSub = userV2.oidcSub; - // 创建OIDC映射 - const oidcSubKey = `oidc:sub:${userV2.oidcSub}`; - await storage.client.set(oidcSubKey, userV2.username); - } - - if (userV2.enabledApis && userV2.enabledApis.length > 0) { - userInfo.enabledApis = JSON.stringify(userV2.enabledApis); - } - - await storage.client.hset(userInfoKey, userInfo); - - // 添加到用户列表(Sorted Set) - const userListKey = 'user:list'; - await storage.client.zadd(userListKey, { - score: createdAt, - member: userV2.username, - }); - - console.log(`V2用户 ${userV2.username} 导入成功`); - } - } - } catch (error) { - console.error(`导入V2用户 ${userV2.username} 失败:`, error); - } - } - } - - // 导入用户数据 + // 导入用户数据和user:info const userData = importData.data.userData; + const storage = (db as any).storage; + const usersV2Map = new Map((importData.data.usersV2 || []).map((u: any) => [u.username, u])); + + const userCount = Object.keys(userData).length; + console.log(`准备导入 ${userCount} 个用户的数据`); + + let importedCount = 0; for (const username in userData) { const user = userData[username]; - await db.createUserV2(username, user.password,user.role,user.tags); - + + // 为所有有passwordV2的用户创建user:info + if (user.passwordV2) { + const userV2 = usersV2Map.get(username); + + // 确定角色:站长为owner,其他用户从usersV2获取或默认为user + let role: 'owner' | 'admin' | 'user' = 'user'; + if (username === process.env.USERNAME) { + role = 'owner'; + } else if (userV2) { + role = userV2.role === 'owner' ? 'user' : userV2.role; + } + + const createdAt = userV2?.created_at || Date.now(); + + // 直接设置用户信息(不经过createUserV2,避免密码被再次hash) + const userInfoKey = `user:${username}:info`; + const userInfo: Record = { + role, + banned: String(userV2?.banned || false), + password: user.passwordV2, // 已经是hash过的密码,直接使用 + created_at: createdAt.toString(), + }; + + if (userV2?.tags && userV2.tags.length > 0) { + userInfo.tags = JSON.stringify(userV2.tags); + } + + if (userV2?.oidcSub) { + userInfo.oidcSub = userV2.oidcSub; + } + + if (userV2?.enabledApis && userV2.enabledApis.length > 0) { + userInfo.enabledApis = JSON.stringify(userV2.enabledApis); + } + + // 使用storage.withRetry直接设置用户信息 + await storage.withRetry(() => storage.client.hSet(userInfoKey, userInfo)); + + // 添加到用户列表 + await storage.withRetry(() => storage.client.zAdd('user:list', { + score: createdAt, + value: username, + })); + + // 如果有oidcSub,创建映射 + if (userV2?.oidcSub) { + const oidcSubKey = `oidc:sub:${userV2.oidcSub}`; + await storage.withRetry(() => storage.client.set(oidcSubKey, username)); + } + + importedCount++; + console.log(`用户 ${username} 导入成功`); + } else { + console.log(`跳过用户 ${username}:没有passwordV2`); + } // 导入播放记录 if (user.playRecords) { @@ -200,6 +201,8 @@ export async function POST(req: NextRequest) { } } + console.log(`成功导入 ${importedCount} 个用户的user:info`); + return NextResponse.json({ message: '数据导入成功', importedUsers: Object.keys(userData).length, diff --git a/src/lib/redis-base.db.ts b/src/lib/redis-base.db.ts index 89b702a..b5dc01b 100644 --- a/src/lib/redis-base.db.ts +++ b/src/lib/redis-base.db.ts @@ -838,6 +838,8 @@ export abstract class BaseRedisStorage implements IStorage { role: 'owner' as const, banned: ownerInfo?.banned || false, tags: ownerInfo?.tags, + oidcSub: ownerInfo?.oidcSub, + enabledApis: ownerInfo?.enabledApis, created_at: ownerInfo?.created_at || 0, }); } @@ -857,6 +859,8 @@ export abstract class BaseRedisStorage implements IStorage { role: userInfo.role, banned: userInfo.banned, tags: userInfo.tags, + oidcSub: userInfo.oidcSub, + enabledApis: userInfo.enabledApis, created_at: userInfo.created_at, }); }