迁移收藏到新数据结构

This commit is contained in:
mtvpls
2025-12-30 22:00:36 +08:00
parent f43d9c7d0e
commit 39f2bb3450
6 changed files with 268 additions and 34 deletions

View File

@@ -24,6 +24,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
} }
// 检查用户状态和执行迁移
if (authInfo.username !== process.env.USERNAME) { if (authInfo.username !== process.env.USERNAME) {
// 非站长,检查用户存在或被封禁 // 非站长,检查用户存在或被封禁
const userInfoV2 = await db.getUserInfoV2(authInfo.username); const userInfoV2 = await db.getUserInfoV2(authInfo.username);
@@ -33,6 +34,19 @@ export async function GET(request: NextRequest) {
if (userInfoV2.banned) { if (userInfoV2.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
} }
// 检查收藏迁移标识,没有迁移标识时执行迁移
if (!userInfoV2.favorite_migrated) {
console.log(`用户 ${authInfo.username} 收藏未迁移,开始执行迁移...`);
await db.migrateFavorites(authInfo.username);
}
} else {
// 站长也需要执行迁移(站长可能不在数据库中,直接尝试迁移)
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
if (!userInfoV2 || !userInfoV2.favorite_migrated) {
console.log(`站长 ${authInfo.username} 收藏未迁移,开始执行迁移...`);
await db.migrateFavorites(authInfo.username);
}
} }
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);

View File

@@ -17,6 +17,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
} }
// 检查用户状态和执行迁移
if (authInfo.username !== process.env.USERNAME) { if (authInfo.username !== process.env.USERNAME) {
// 非站长,检查用户存在或被封禁 // 非站长,检查用户存在或被封禁
const userInfoV2 = await db.getUserInfoV2(authInfo.username); const userInfoV2 = await db.getUserInfoV2(authInfo.username);
@@ -32,6 +33,13 @@ export async function GET(request: NextRequest) {
console.log(`用户 ${authInfo.username} 播放记录未迁移,开始执行迁移...`); console.log(`用户 ${authInfo.username} 播放记录未迁移,开始执行迁移...`);
await db.migratePlayRecords(authInfo.username); await db.migratePlayRecords(authInfo.username);
} }
} else {
// 站长也需要执行迁移(站长可能不在数据库中,直接尝试迁移)
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
if (!userInfoV2 || !userInfoV2.playrecord_migrated) {
console.log(`站长 ${authInfo.username} 播放记录未迁移,开始执行迁移...`);
await db.migratePlayRecords(authInfo.username);
}
} }
const records = await db.getAllPlayRecords(authInfo.username); const records = await db.getAllPlayRecords(authInfo.username);

View File

@@ -267,6 +267,13 @@ export class DbManager {
} }
} }
// ---------- 收藏迁移 ----------
async migrateFavorites(userName: string): Promise<void> {
if (typeof (this.storage as any).migrateFavorites === 'function') {
await (this.storage as any).migrateFavorites(userName);
}
}
// ---------- 数据迁移 ---------- // ---------- 数据迁移 ----------
async migrateUsersFromConfig(adminConfig: AdminConfig): Promise<void> { async migrateUsersFromConfig(adminConfig: AdminConfig): Promise<void> {
if (typeof (this.storage as any).createUserV2 !== 'function') { if (typeof (this.storage as any).createUserV2 !== 'function') {

View File

@@ -267,7 +267,7 @@ export abstract class BaseRedisStorage implements IStorage {
// 迁移播放记录从旧的多key结构迁移到新的hash结构 // 迁移播放记录从旧的多key结构迁移到新的hash结构
async migratePlayRecords(userName: string): Promise<void> { async migratePlayRecords(userName: string): Promise<void> {
// 检查是否已有正在进行的迁移 // 检查是否已有正在进行的迁移
const existingMigration = migrationLocks.get(userName); const existingMigration = playRecordLocks.get(userName);
if (existingMigration) { if (existingMigration) {
console.log(`用户 ${userName} 的播放记录正在迁移中,等待完成...`); console.log(`用户 ${userName} 的播放记录正在迁移中,等待完成...`);
await existingMigration; await existingMigration;
@@ -276,13 +276,13 @@ export abstract class BaseRedisStorage implements IStorage {
// 创建新的迁移Promise // 创建新的迁移Promise
const migrationPromise = this.doMigration(userName); const migrationPromise = this.doMigration(userName);
migrationLocks.set(userName, migrationPromise); playRecordLocks.set(userName, migrationPromise);
try { try {
await migrationPromise; await migrationPromise;
} finally { } finally {
// 迁移完成后清除锁 // 迁移完成后清除锁
migrationLocks.delete(userName); playRecordLocks.delete(userName);
} }
} }
@@ -354,13 +354,18 @@ export abstract class BaseRedisStorage implements IStorage {
} }
// ---------- 收藏 ---------- // ---------- 收藏 ----------
private favKey(user: string, key: string) { private favHashKey(user: string) {
return `u:${user}:fav`; // u:username:fav (hash结构)
}
// 旧版收藏key用于迁移
private favOldKey(user: string, key: string) {
return `u:${user}:fav:${key}`; return `u:${user}:fav:${key}`;
} }
async getFavorite(userName: string, key: string): Promise<Favorite | null> { async getFavorite(userName: string, key: string): Promise<Favorite | null> {
const val = await this.withRetry(() => const val = await this.withRetry(() =>
this.client.get(this.favKey(userName, key)) this.client.hGet(this.favHashKey(userName), key)
); );
return val ? (JSON.parse(val) as Favorite) : null; return val ? (JSON.parse(val) as Favorite) : null;
} }
@@ -371,29 +376,115 @@ export abstract class BaseRedisStorage implements IStorage {
favorite: Favorite favorite: Favorite
): Promise<void> { ): Promise<void> {
await this.withRetry(() => await this.withRetry(() =>
this.client.set(this.favKey(userName, key), JSON.stringify(favorite)) this.client.hSet(this.favHashKey(userName), key, JSON.stringify(favorite))
); );
} }
async getAllFavorites(userName: string): Promise<Record<string, Favorite>> { async getAllFavorites(userName: string): Promise<Record<string, Favorite>> {
const pattern = `u:${userName}:fav:*`; const hashData = await this.withRetry(() =>
const keys: string[] = await this.withRetry(() => this.client.keys(pattern)); this.client.hGetAll(this.favHashKey(userName))
if (keys.length === 0) return {}; );
const values = await this.withRetry(() => this.client.mGet(keys));
const result: Record<string, Favorite> = {}; const result: Record<string, Favorite> = {};
keys.forEach((fullKey: string, idx: number) => { for (const [key, value] of Object.entries(hashData)) {
const raw = values[idx]; if (value) {
if (raw) { result[key] = JSON.parse(value) as Favorite;
const fav = JSON.parse(raw) as Favorite;
const keyPart = ensureString(fullKey.replace(`u:${userName}:fav:`, ''));
result[keyPart] = fav;
} }
}); }
return result; return result;
} }
async deleteFavorite(userName: string, key: string): Promise<void> { async deleteFavorite(userName: string, key: string): Promise<void> {
await this.withRetry(() => this.client.del(this.favKey(userName, key))); await this.withRetry(() => this.client.hDel(this.favHashKey(userName), key));
}
// 迁移收藏从旧的多key结构迁移到新的hash结构
async migrateFavorites(userName: string): Promise<void> {
// 检查是否已有正在进行的迁移
const existingMigration = playRecordLocks.get(userName);
if (existingMigration) {
console.log(`用户 ${userName} 的收藏正在迁移中,等待完成...`);
await existingMigration;
return;
}
// 创建新的迁移Promise
const migrationPromise = this.doFavoriteMigration(userName);
playRecordLocks.set(userName, migrationPromise);
try {
await migrationPromise;
} finally {
// 迁移完成后清除锁
playRecordLocks.delete(userName);
}
}
// 实际执行收藏迁移的方法
private async doFavoriteMigration(userName: string): Promise<void> {
console.log(`开始迁移用户 ${userName} 的收藏...`);
// 1. 检查是否已经迁移过
const userInfo = await this.getUserInfoV2(userName);
if (userInfo?.favorite_migrated) {
console.log(`用户 ${userName} 的收藏已经迁移过,跳过`);
return;
}
// 2. 获取旧结构的所有收藏key
const pattern = `u:${userName}:fav:*`;
const oldKeys: string[] = await this.withRetry(() => this.client.keys(pattern));
if (oldKeys.length === 0) {
console.log(`用户 ${userName} 没有旧的收藏,标记为已迁移`);
// 即使没有数据也标记为已迁移
await this.withRetry(() =>
this.client.hSet(this.userInfoKey(userName), 'favorite_migrated', 'true')
);
// 清除用户信息缓存
const { userInfoCache } = await import('./user-cache');
userInfoCache?.delete(userName);
return;
}
console.log(`找到 ${oldKeys.length} 条旧收藏,开始迁移...`);
// 3. 批量获取旧数据
const oldValues = await this.withRetry(() => this.client.mGet(oldKeys));
// 4. 转换为hash格式
const hashData: Record<string, string> = {};
oldKeys.forEach((fullKey: string, idx: number) => {
const raw = oldValues[idx];
if (raw) {
// 提取 source+id 部分作为hash的field
const keyPart = ensureString(fullKey.replace(`u:${userName}:fav:`, ''));
hashData[keyPart] = raw;
}
});
// 5. 写入新的hash结构
if (Object.keys(hashData).length > 0) {
await this.withRetry(() =>
this.client.hSet(this.favHashKey(userName), hashData)
);
console.log(`成功迁移 ${Object.keys(hashData).length} 条收藏到hash结构`);
}
// 6. 删除旧的key
await this.withRetry(() => this.client.del(oldKeys));
console.log(`删除了 ${oldKeys.length} 个旧的收藏key`);
// 7. 标记迁移完成
await this.withRetry(() =>
this.client.hSet(this.userInfoKey(userName), 'favorite_migrated', 'true')
);
// 8. 清除用户信息缓存,确保下次获取时能读取到最新的迁移标识
const { userInfoCache } = await import('./user-cache');
userInfoCache?.delete(userName);
console.log(`用户 ${userName} 的收藏迁移完成`);
} }
// ---------- 用户注册 / 登录(旧版本,保持兼容) ---------- // ---------- 用户注册 / 登录(旧版本,保持兼容) ----------
@@ -452,7 +543,10 @@ export abstract class BaseRedisStorage implements IStorage {
await this.withRetry(() => this.client.del(playRecordKeys)); await this.withRetry(() => this.client.del(playRecordKeys));
} }
// 删除收藏夹 // 删除收藏夹新hash结构
await this.withRetry(() => this.client.del(this.favHashKey(userName)));
// 删除旧的收藏key如果有
const favoritePattern = `u:${userName}:fav:*`; const favoritePattern = `u:${userName}:fav:*`;
const favoriteKeys = await this.withRetry(() => const favoriteKeys = await this.withRetry(() =>
this.client.keys(favoritePattern) this.client.keys(favoritePattern)
@@ -565,6 +659,7 @@ export abstract class BaseRedisStorage implements IStorage {
enabledApis?: string[]; enabledApis?: string[];
created_at: number; created_at: number;
playrecord_migrated?: boolean; playrecord_migrated?: boolean;
favorite_migrated?: boolean;
} | null> { } | null> {
const userInfo = await this.withRetry(() => const userInfo = await this.withRetry(() =>
this.client.hGetAll(this.userInfoKey(userName)) this.client.hGetAll(this.userInfoKey(userName))
@@ -582,6 +677,7 @@ export abstract class BaseRedisStorage implements IStorage {
enabledApis: userInfo.enabledApis ? JSON.parse(userInfo.enabledApis) : undefined, enabledApis: userInfo.enabledApis ? JSON.parse(userInfo.enabledApis) : undefined,
created_at: parseInt(userInfo.created_at || '0', 10), created_at: parseInt(userInfo.created_at || '0', 10),
playrecord_migrated: userInfo.playrecord_migrated === 'true', playrecord_migrated: userInfo.playrecord_migrated === 'true',
favorite_migrated: userInfo.favorite_migrated === 'true',
}; };
} }

View File

@@ -41,12 +41,16 @@ export interface IStorage {
deletePlayRecord(userName: string, key: string): Promise<void>; deletePlayRecord(userName: string, key: string): Promise<void>;
// 清理超出限制的旧播放记录 // 清理超出限制的旧播放记录
cleanupOldPlayRecords(userName: string): Promise<void>; cleanupOldPlayRecords(userName: string): Promise<void>;
// 迁移播放记录
migratePlayRecords(userName: string): Promise<void>;
// 收藏相关 // 收藏相关
getFavorite(userName: string, key: string): Promise<Favorite | null>; getFavorite(userName: string, key: string): Promise<Favorite | null>;
setFavorite(userName: string, key: string, favorite: Favorite): Promise<void>; setFavorite(userName: string, key: string, favorite: Favorite): Promise<void>;
getAllFavorites(userName: string): Promise<{ [key: string]: Favorite }>; getAllFavorites(userName: string): Promise<{ [key: string]: Favorite }>;
deleteFavorite(userName: string, key: string): Promise<void>; deleteFavorite(userName: string, key: string): Promise<void>;
// 迁移收藏
migrateFavorites(userName: string): Promise<void>;
// 用户相关 // 用户相关
registerUser(userName: string, password: string): Promise<void>; registerUser(userName: string, password: string): Promise<void>;

View File

@@ -260,13 +260,18 @@ export class UpstashRedisStorage implements IStorage {
} }
// ---------- 收藏 ---------- // ---------- 收藏 ----------
private favKey(user: string, key: string) { private favHashKey(user: string) {
return `u:${user}:fav`; // u:username:fav (hash结构)
}
// 旧版收藏key用于迁移
private favOldKey(user: string, key: string) {
return `u:${user}:fav:${key}`; return `u:${user}:fav:${key}`;
} }
async getFavorite(userName: string, key: string): Promise<Favorite | null> { async getFavorite(userName: string, key: string): Promise<Favorite | null> {
const val = await withRetry(() => const val = await withRetry(() =>
this.client.get(this.favKey(userName, key)) this.client.hget(this.favHashKey(userName), key)
); );
return val ? (val as Favorite) : null; return val ? (val as Favorite) : null;
} }
@@ -276,29 +281,111 @@ export class UpstashRedisStorage implements IStorage {
key: string, key: string,
favorite: Favorite favorite: Favorite
): Promise<void> { ): Promise<void> {
await withRetry(() => await withRetry(() => this.client.hset(this.favHashKey(userName), { [key]: favorite }));
this.client.set(this.favKey(userName, key), favorite)
);
} }
async getAllFavorites(userName: string): Promise<Record<string, Favorite>> { async getAllFavorites(userName: string): Promise<Record<string, Favorite>> {
const pattern = `u:${userName}:fav:*`; const hashData = await withRetry(() =>
const keys: string[] = await withRetry(() => this.client.keys(pattern)); this.client.hgetall(this.favHashKey(userName))
if (keys.length === 0) return {}; );
if (!hashData || Object.keys(hashData).length === 0) return {};
const result: Record<string, Favorite> = {}; const result: Record<string, Favorite> = {};
for (const fullKey of keys) { for (const [key, value] of Object.entries(hashData)) {
const value = await withRetry(() => this.client.get(fullKey));
if (value) { if (value) {
const keyPart = ensureString(fullKey.replace(`u:${userName}:fav:`, '')); result[key] = value as Favorite;
result[keyPart] = value as Favorite;
} }
} }
return result; return result;
} }
async deleteFavorite(userName: string, key: string): Promise<void> { async deleteFavorite(userName: string, key: string): Promise<void> {
await withRetry(() => this.client.del(this.favKey(userName, key))); await withRetry(() => this.client.hdel(this.favHashKey(userName), key));
}
// 迁移收藏从旧的多key结构迁移到新的hash结构
async migrateFavorites(userName: string): Promise<void> {
// 检查是否已有正在进行的迁移
const existingMigration = playRecordLocks.get(userName);
if (existingMigration) {
console.log(`用户 ${userName} 的收藏正在迁移中,等待完成...`);
await existingMigration;
return;
}
// 创建新的迁移Promise
const migrationPromise = this.doFavoriteMigration(userName);
playRecordLocks.set(userName, migrationPromise);
try {
await migrationPromise;
} finally {
// 迁移完成后清除锁
playRecordLocks.delete(userName);
}
}
// 实际执行收藏迁移的方法
private async doFavoriteMigration(userName: string): Promise<void> {
console.log(`开始迁移用户 ${userName} 的收藏...`);
// 1. 检查是否已经迁移过
const userInfo = await this.getUserInfoV2(userName);
if (userInfo?.favorite_migrated) {
console.log(`用户 ${userName} 的收藏已经迁移过,跳过`);
return;
}
// 2. 获取旧结构的所有收藏key
const pattern = `u:${userName}:fav:*`;
const oldKeys: string[] = await withRetry(() => this.client.keys(pattern));
if (oldKeys.length === 0) {
console.log(`用户 ${userName} 没有旧的收藏,标记为已迁移`);
// 即使没有数据也标记为已迁移
await withRetry(() =>
this.client.hset(this.userInfoKey(userName), { favorite_migrated: true })
);
// 清除用户信息缓存
userInfoCache?.delete(userName);
return;
}
console.log(`找到 ${oldKeys.length} 条旧收藏,开始迁移...`);
// 3. 批量获取旧数据并转换为hash格式
const hashData: Record<string, any> = {};
for (const fullKey of oldKeys) {
const value = await withRetry(() => this.client.get(fullKey));
if (value) {
// 提取 source+id 部分作为hash的field
const keyPart = ensureString(fullKey.replace(`u:${userName}:fav:`, ''));
hashData[keyPart] = value;
}
}
// 4. 写入新的hash结构
if (Object.keys(hashData).length > 0) {
await withRetry(() =>
this.client.hset(this.favHashKey(userName), hashData)
);
console.log(`成功迁移 ${Object.keys(hashData).length} 条收藏到hash结构`);
}
// 5. 删除旧的key
await withRetry(() => this.client.del(...oldKeys));
console.log(`删除了 ${oldKeys.length} 个旧的收藏key`);
// 6. 标记迁移完成
await withRetry(() =>
this.client.hset(this.userInfoKey(userName), { favorite_migrated: true })
);
// 7. 清除用户信息缓存,确保下次获取时能读取到最新的迁移标识
userInfoCache?.delete(userName);
console.log(`用户 ${userName} 的收藏迁移完成`);
} }
// ---------- 用户注册 / 登录 ---------- // ---------- 用户注册 / 登录 ----------
@@ -345,7 +432,10 @@ export class UpstashRedisStorage implements IStorage {
// 删除搜索历史 // 删除搜索历史
await withRetry(() => this.client.del(this.shKey(userName))); await withRetry(() => this.client.del(this.shKey(userName)));
// 删除播放记录 // 删除播放记录新hash结构
await withRetry(() => this.client.del(this.prHashKey(userName)));
// 删除旧的播放记录key如果有
const playRecordPattern = `u:${userName}:pr:*`; const playRecordPattern = `u:${userName}:pr:*`;
const playRecordKeys = await withRetry(() => const playRecordKeys = await withRetry(() =>
this.client.keys(playRecordPattern) this.client.keys(playRecordPattern)
@@ -354,7 +444,10 @@ export class UpstashRedisStorage implements IStorage {
await withRetry(() => this.client.del(...playRecordKeys)); await withRetry(() => this.client.del(...playRecordKeys));
} }
// 删除收藏夹 // 删除收藏夹新hash结构
await withRetry(() => this.client.del(this.favHashKey(userName)));
// 删除旧的收藏key如果有
const favoritePattern = `u:${userName}:fav:*`; const favoritePattern = `u:${userName}:fav:*`;
const favoriteKeys = await withRetry(() => const favoriteKeys = await withRetry(() =>
this.client.keys(favoritePattern) this.client.keys(favoritePattern)
@@ -475,6 +568,7 @@ export class UpstashRedisStorage implements IStorage {
enabledApis?: string[]; enabledApis?: string[];
created_at: number; created_at: number;
playrecord_migrated?: boolean; playrecord_migrated?: boolean;
favorite_migrated?: boolean;
} | null> { } | null> {
// 先从缓存获取 // 先从缓存获取
const cached = userInfoCache?.get(userName); const cached = userInfoCache?.get(userName);
@@ -508,6 +602,16 @@ export class UpstashRedisStorage implements IStorage {
} }
} }
// 处理 favorite_migrated 字段
let favorite_migrated: boolean | undefined = undefined;
if (userInfo.favorite_migrated !== undefined) {
if (typeof userInfo.favorite_migrated === 'boolean') {
favorite_migrated = userInfo.favorite_migrated;
} else if (typeof userInfo.favorite_migrated === 'string') {
favorite_migrated = userInfo.favorite_migrated === 'true';
}
}
// 安全解析 tags 字段 // 安全解析 tags 字段
let tags: string[] | undefined = undefined; let tags: string[] | undefined = undefined;
if (userInfo.tags) { if (userInfo.tags) {
@@ -546,6 +650,7 @@ export class UpstashRedisStorage implements IStorage {
enabledApis, enabledApis,
created_at: parseInt((userInfo.created_at as string) || '0', 10), created_at: parseInt((userInfo.created_at as string) || '0', 10),
playrecord_migrated, playrecord_migrated,
favorite_migrated,
}; };
// 存入缓存 // 存入缓存