mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-28 01:13:15 +08:00
使用 hash 优化用户信息获取速度
This commit is contained in:
@@ -179,13 +179,7 @@ export async function DELETE(request: NextRequest) {
|
|||||||
await db.deleteFavorite(username, source, id);
|
await db.deleteFavorite(username, source, id);
|
||||||
} else {
|
} else {
|
||||||
// 清空全部
|
// 清空全部
|
||||||
const all = await db.getAllFavorites(username);
|
await db.deleteAllFavorites(username);
|
||||||
await Promise.all(
|
|
||||||
Object.keys(all).map(async (k) => {
|
|
||||||
const [s, i] = k.split('+');
|
|
||||||
if (s && i) await db.deleteFavorite(username, s, i);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true }, { status: 200 });
|
return NextResponse.json({ success: true }, { status: 200 });
|
||||||
|
|||||||
@@ -147,14 +147,7 @@ export async function DELETE(request: NextRequest) {
|
|||||||
await db.deletePlayRecord(username, source, id);
|
await db.deletePlayRecord(username, source, id);
|
||||||
} else {
|
} else {
|
||||||
// 未提供 key,则清空全部播放记录
|
// 未提供 key,则清空全部播放记录
|
||||||
// 目前 DbManager 没有对应方法,这里直接遍历删除
|
await db.deleteAllPlayRecords(username);
|
||||||
const all = await db.getAllPlayRecords(username);
|
|
||||||
await Promise.all(
|
|
||||||
Object.keys(all).map(async (k) => {
|
|
||||||
const [s, i] = k.split('+');
|
|
||||||
if (s && i) await db.deletePlayRecord(username, s, i);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true }, { status: 200 });
|
return NextResponse.json({ success: true }, { status: 200 });
|
||||||
|
|||||||
@@ -48,9 +48,24 @@ export function generateStorageKey(source: string, id: string): string {
|
|||||||
// 导出便捷方法
|
// 导出便捷方法
|
||||||
export class DbManager {
|
export class DbManager {
|
||||||
private storage: IStorage;
|
private storage: IStorage;
|
||||||
|
private migrationPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.storage = getStorage();
|
this.storage = getStorage();
|
||||||
|
// 启动时自动触发数据迁移(异步,不阻塞构造)
|
||||||
|
if (this.storage && typeof this.storage.migrateData === 'function') {
|
||||||
|
this.migrationPromise = this.storage.migrateData().catch((err) => {
|
||||||
|
console.error('数据迁移异常:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 等待迁移完成(内部方法,首次调用后 migrationPromise 会被置空) */
|
||||||
|
private async ensureMigrated(): Promise<void> {
|
||||||
|
if (this.migrationPromise) {
|
||||||
|
await this.migrationPromise;
|
||||||
|
this.migrationPromise = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 播放记录相关方法
|
// 播放记录相关方法
|
||||||
@@ -76,6 +91,7 @@ export class DbManager {
|
|||||||
async getAllPlayRecords(userName: string): Promise<{
|
async getAllPlayRecords(userName: string): Promise<{
|
||||||
[key: string]: PlayRecord;
|
[key: string]: PlayRecord;
|
||||||
}> {
|
}> {
|
||||||
|
await this.ensureMigrated();
|
||||||
return this.storage.getAllPlayRecords(userName);
|
return this.storage.getAllPlayRecords(userName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +104,10 @@ export class DbManager {
|
|||||||
await this.storage.deletePlayRecord(userName, key);
|
await this.storage.deletePlayRecord(userName, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteAllPlayRecords(userName: string): Promise<void> {
|
||||||
|
await this.storage.deleteAllPlayRecords(userName);
|
||||||
|
}
|
||||||
|
|
||||||
// 收藏相关方法
|
// 收藏相关方法
|
||||||
async getFavorite(
|
async getFavorite(
|
||||||
userName: string,
|
userName: string,
|
||||||
@@ -111,6 +131,7 @@ export class DbManager {
|
|||||||
async getAllFavorites(
|
async getAllFavorites(
|
||||||
userName: string
|
userName: string
|
||||||
): Promise<{ [key: string]: Favorite }> {
|
): Promise<{ [key: string]: Favorite }> {
|
||||||
|
await this.ensureMigrated();
|
||||||
return this.storage.getAllFavorites(userName);
|
return this.storage.getAllFavorites(userName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +144,10 @@ export class DbManager {
|
|||||||
await this.storage.deleteFavorite(userName, key);
|
await this.storage.deleteFavorite(userName, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteAllFavorites(userName: string): Promise<void> {
|
||||||
|
await this.storage.deleteAllFavorites(userName);
|
||||||
|
}
|
||||||
|
|
||||||
async isFavorited(
|
async isFavorited(
|
||||||
userName: string,
|
userName: string,
|
||||||
source: string,
|
source: string,
|
||||||
|
|||||||
@@ -151,8 +151,8 @@ export abstract class BaseRedisStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 播放记录 ----------
|
// ---------- 播放记录 ----------
|
||||||
private prKey(user: string, key: string) {
|
private prHashKey(user: string) {
|
||||||
return `u:${user}:pr:${key}`; // u:username:pr:source+id
|
return `u:${user}:pr`; // 一个用户的所有播放记录存在一个 Hash 中
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPlayRecord(
|
async getPlayRecord(
|
||||||
@@ -160,7 +160,7 @@ export abstract class BaseRedisStorage implements IStorage {
|
|||||||
key: string
|
key: string
|
||||||
): Promise<PlayRecord | null> {
|
): Promise<PlayRecord | null> {
|
||||||
const val = await this.withRetry(() =>
|
const val = await this.withRetry(() =>
|
||||||
this.client.get(this.prKey(userName, key))
|
this.client.hGet(this.prHashKey(userName), key)
|
||||||
);
|
);
|
||||||
return val ? (JSON.parse(val) as PlayRecord) : null;
|
return val ? (JSON.parse(val) as PlayRecord) : null;
|
||||||
}
|
}
|
||||||
@@ -171,42 +171,43 @@ export abstract class BaseRedisStorage implements IStorage {
|
|||||||
record: PlayRecord
|
record: PlayRecord
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.withRetry(() =>
|
await this.withRetry(() =>
|
||||||
this.client.set(this.prKey(userName, key), JSON.stringify(record))
|
this.client.hSet(this.prHashKey(userName), key, JSON.stringify(record))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllPlayRecords(
|
async getAllPlayRecords(
|
||||||
userName: string
|
userName: string
|
||||||
): Promise<Record<string, PlayRecord>> {
|
): Promise<Record<string, PlayRecord>> {
|
||||||
const pattern = `u:${userName}:pr:*`;
|
const all = await this.withRetry(() =>
|
||||||
const keys: string[] = await this.withRetry(() => this.client.keys(pattern));
|
this.client.hGetAll(this.prHashKey(userName))
|
||||||
if (keys.length === 0) return {};
|
);
|
||||||
const values = await this.withRetry(() => this.client.mGet(keys));
|
|
||||||
const result: Record<string, PlayRecord> = {};
|
const result: Record<string, PlayRecord> = {};
|
||||||
keys.forEach((fullKey: string, idx: number) => {
|
for (const [field, raw] of Object.entries(all)) {
|
||||||
const raw = values[idx];
|
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const rec = JSON.parse(raw) as PlayRecord;
|
result[field] = JSON.parse(raw) as PlayRecord;
|
||||||
// 截取 source+id 部分
|
|
||||||
const keyPart = ensureString(fullKey.replace(`u:${userName}:pr:`, ''));
|
|
||||||
result[keyPart] = rec;
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deletePlayRecord(userName: string, key: string): Promise<void> {
|
async deletePlayRecord(userName: string, key: string): Promise<void> {
|
||||||
await this.withRetry(() => this.client.del(this.prKey(userName, key)));
|
await this.withRetry(() =>
|
||||||
|
this.client.hDel(this.prHashKey(userName), key)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAllPlayRecords(userName: string): Promise<void> {
|
||||||
|
await this.withRetry(() => this.client.del(this.prHashKey(userName)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 收藏 ----------
|
// ---------- 收藏 ----------
|
||||||
private favKey(user: string, key: string) {
|
private favHashKey(user: string) {
|
||||||
return `u:${user}:fav:${key}`;
|
return `u:${user}:fav`; // 一个用户的所有收藏存在一个 Hash 中
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -217,29 +218,31 @@ 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 all = 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 [field, raw] of Object.entries(all)) {
|
||||||
const raw = values[idx];
|
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const fav = JSON.parse(raw) as Favorite;
|
result[field] = 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAllFavorites(userName: string): Promise<void> {
|
||||||
|
await this.withRetry(() => this.client.del(this.favHashKey(userName)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 用户注册 / 登录 ----------
|
// ---------- 用户注册 / 登录 ----------
|
||||||
@@ -286,23 +289,11 @@ export abstract class BaseRedisStorage implements IStorage {
|
|||||||
// 删除搜索历史
|
// 删除搜索历史
|
||||||
await this.withRetry(() => this.client.del(this.shKey(userName)));
|
await this.withRetry(() => this.client.del(this.shKey(userName)));
|
||||||
|
|
||||||
// 删除播放记录
|
// 删除播放记录(Hash key 直接删除)
|
||||||
const playRecordPattern = `u:${userName}:pr:*`;
|
await this.withRetry(() => this.client.del(this.prHashKey(userName)));
|
||||||
const playRecordKeys = await this.withRetry(() =>
|
|
||||||
this.client.keys(playRecordPattern)
|
|
||||||
);
|
|
||||||
if (playRecordKeys.length > 0) {
|
|
||||||
await this.withRetry(() => this.client.del(playRecordKeys));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除收藏夹
|
// 删除收藏夹(Hash key 直接删除)
|
||||||
const favoritePattern = `u:${userName}:fav:*`;
|
await this.withRetry(() => this.client.del(this.favHashKey(userName)));
|
||||||
const favoriteKeys = await this.withRetry(() =>
|
|
||||||
this.client.keys(favoritePattern)
|
|
||||||
);
|
|
||||||
if (favoriteKeys.length > 0) {
|
|
||||||
await this.withRetry(() => this.client.del(favoriteKeys));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除跳过片头片尾配置
|
// 删除跳过片头片尾配置
|
||||||
const skipConfigPattern = `u:${userName}:skip:*`;
|
const skipConfigPattern = `u:${userName}:skip:*`;
|
||||||
@@ -443,6 +434,81 @@ export abstract class BaseRedisStorage implements IStorage {
|
|||||||
return configs;
|
return configs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 数据迁移:旧扁平 key → Hash 结构 ----------
|
||||||
|
private migrationKey() {
|
||||||
|
return 'sys:migration:hash_v1';
|
||||||
|
}
|
||||||
|
|
||||||
|
async migrateData(): Promise<void> {
|
||||||
|
// 检查是否已迁移
|
||||||
|
const migrated = await this.withRetry(() => this.client.get(this.migrationKey()));
|
||||||
|
if (migrated === 'done') return;
|
||||||
|
|
||||||
|
console.log('开始数据迁移:扁平 key → Hash 结构...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 迁移播放记录:u:*:pr:* → u:username:pr (Hash)
|
||||||
|
const prKeys = await this.withRetry(() => this.client.keys('u:*:pr:*'));
|
||||||
|
if (prKeys.length > 0) {
|
||||||
|
// 过滤掉新 Hash key(没有第四段的就是 Hash key 本身)
|
||||||
|
const oldPrKeys = prKeys.filter((k) => {
|
||||||
|
const parts = k.split(':');
|
||||||
|
return parts.length >= 4 && parts[2] === 'pr' && parts[3] !== '';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (oldPrKeys.length > 0) {
|
||||||
|
const values = await this.withRetry(() => this.client.mGet(oldPrKeys));
|
||||||
|
for (let i = 0; i < oldPrKeys.length; i++) {
|
||||||
|
const raw = values[i];
|
||||||
|
if (!raw) continue;
|
||||||
|
const match = oldPrKeys[i].match(/^u:(.+?):pr:(.+)$/);
|
||||||
|
if (!match) continue;
|
||||||
|
const [, userName, field] = match;
|
||||||
|
await this.withRetry(() =>
|
||||||
|
this.client.hSet(this.prHashKey(userName), field, raw)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 删除旧 key
|
||||||
|
await this.withRetry(() => this.client.del(oldPrKeys));
|
||||||
|
console.log(`迁移了 ${oldPrKeys.length} 条播放记录`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 迁移收藏:u:*:fav:* → u:username:fav (Hash)
|
||||||
|
const favKeys = await this.withRetry(() => this.client.keys('u:*:fav:*'));
|
||||||
|
if (favKeys.length > 0) {
|
||||||
|
const oldFavKeys = favKeys.filter((k) => {
|
||||||
|
const parts = k.split(':');
|
||||||
|
return parts.length >= 4 && parts[2] === 'fav' && parts[3] !== '';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (oldFavKeys.length > 0) {
|
||||||
|
const values = await this.withRetry(() => this.client.mGet(oldFavKeys));
|
||||||
|
for (let i = 0; i < oldFavKeys.length; i++) {
|
||||||
|
const raw = values[i];
|
||||||
|
if (!raw) continue;
|
||||||
|
const match = oldFavKeys[i].match(/^u:(.+?):fav:(.+)$/);
|
||||||
|
if (!match) continue;
|
||||||
|
const [, userName, field] = match;
|
||||||
|
await this.withRetry(() =>
|
||||||
|
this.client.hSet(this.favHashKey(userName), field, raw)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 删除旧 key
|
||||||
|
await this.withRetry(() => this.client.del(oldFavKeys));
|
||||||
|
console.log(`迁移了 ${oldFavKeys.length} 条收藏`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记迁移完成
|
||||||
|
await this.withRetry(() => this.client.set(this.migrationKey(), 'done'));
|
||||||
|
console.log('数据迁移完成');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('数据迁移失败:', error);
|
||||||
|
// 不抛出异常,允许服务继续运行
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 清空所有数据
|
// 清空所有数据
|
||||||
async clearAllData(): Promise<void> {
|
async clearAllData(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -37,12 +37,14 @@ export interface IStorage {
|
|||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
getAllPlayRecords(userName: string): Promise<{ [key: string]: PlayRecord }>;
|
getAllPlayRecords(userName: string): Promise<{ [key: string]: PlayRecord }>;
|
||||||
deletePlayRecord(userName: string, key: string): Promise<void>;
|
deletePlayRecord(userName: string, key: string): Promise<void>;
|
||||||
|
deleteAllPlayRecords(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>;
|
||||||
|
deleteAllFavorites(userName: string): Promise<void>;
|
||||||
|
|
||||||
// 用户相关
|
// 用户相关
|
||||||
registerUser(userName: string, password: string): Promise<void>;
|
registerUser(userName: string, password: string): Promise<void>;
|
||||||
@@ -81,6 +83,9 @@ export interface IStorage {
|
|||||||
deleteSkipConfig(userName: string, source: string, id: string): Promise<void>;
|
deleteSkipConfig(userName: string, source: string, id: string): Promise<void>;
|
||||||
getAllSkipConfigs(userName: string): Promise<{ [key: string]: SkipConfig }>;
|
getAllSkipConfigs(userName: string): Promise<{ [key: string]: SkipConfig }>;
|
||||||
|
|
||||||
|
// 数据迁移(旧扁平 key → Hash 结构)
|
||||||
|
migrateData?(): Promise<void>;
|
||||||
|
|
||||||
// 数据清理相关
|
// 数据清理相关
|
||||||
clearAllData(): Promise<void>;
|
clearAllData(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ export class UpstashRedisStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 播放记录 ----------
|
// ---------- 播放记录 ----------
|
||||||
private prKey(user: string, key: string) {
|
private prHashKey(user: string) {
|
||||||
return `u:${user}:pr:${key}`; // u:username:pr:source+id
|
return `u:${user}:pr`; // 一个用户的所有播放记录存在一个 Hash 中
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPlayRecord(
|
async getPlayRecord(
|
||||||
@@ -70,7 +70,7 @@ export class UpstashRedisStorage implements IStorage {
|
|||||||
key: string
|
key: string
|
||||||
): Promise<PlayRecord | null> {
|
): Promise<PlayRecord | null> {
|
||||||
const val = await withRetry(() =>
|
const val = await withRetry(() =>
|
||||||
this.client.get(this.prKey(userName, key))
|
this.client.hget(this.prHashKey(userName), key)
|
||||||
);
|
);
|
||||||
return val ? (val as PlayRecord) : null;
|
return val ? (val as PlayRecord) : null;
|
||||||
}
|
}
|
||||||
@@ -80,40 +80,43 @@ export class UpstashRedisStorage implements IStorage {
|
|||||||
key: string,
|
key: string,
|
||||||
record: PlayRecord
|
record: PlayRecord
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await withRetry(() => this.client.set(this.prKey(userName, key), record));
|
await withRetry(() =>
|
||||||
|
this.client.hset(this.prHashKey(userName), { [key]: record })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllPlayRecords(
|
async getAllPlayRecords(
|
||||||
userName: string
|
userName: string
|
||||||
): Promise<Record<string, PlayRecord>> {
|
): Promise<Record<string, PlayRecord>> {
|
||||||
const pattern = `u:${userName}:pr:*`;
|
const all = await withRetry(() =>
|
||||||
const keys: string[] = await withRetry(() => this.client.keys(pattern));
|
this.client.hgetall(this.prHashKey(userName))
|
||||||
if (keys.length === 0) return {};
|
);
|
||||||
|
if (!all || Object.keys(all).length === 0) return {};
|
||||||
const result: Record<string, PlayRecord> = {};
|
const result: Record<string, PlayRecord> = {};
|
||||||
for (const fullKey of keys) {
|
for (const [field, value] of Object.entries(all)) {
|
||||||
const value = await withRetry(() => this.client.get(fullKey));
|
|
||||||
if (value) {
|
if (value) {
|
||||||
// 截取 source+id 部分
|
result[field] = value as PlayRecord;
|
||||||
const keyPart = ensureString(fullKey.replace(`u:${userName}:pr:`, ''));
|
|
||||||
result[keyPart] = value as PlayRecord;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deletePlayRecord(userName: string, key: string): Promise<void> {
|
async deletePlayRecord(userName: string, key: string): Promise<void> {
|
||||||
await withRetry(() => this.client.del(this.prKey(userName, key)));
|
await withRetry(() => this.client.hdel(this.prHashKey(userName), key));
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAllPlayRecords(userName: string): Promise<void> {
|
||||||
|
await withRetry(() => this.client.del(this.prHashKey(userName)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 收藏 ----------
|
// ---------- 收藏 ----------
|
||||||
private favKey(user: string, key: string) {
|
private favHashKey(user: string) {
|
||||||
return `u:${user}:fav:${key}`;
|
return `u:${user}:fav`; // 一个用户的所有收藏存在一个 Hash 中
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -124,28 +127,30 @@ export class UpstashRedisStorage implements IStorage {
|
|||||||
favorite: Favorite
|
favorite: Favorite
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await withRetry(() =>
|
await withRetry(() =>
|
||||||
this.client.set(this.favKey(userName, key), favorite)
|
this.client.hset(this.favHashKey(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 all = await withRetry(() =>
|
||||||
const keys: string[] = await withRetry(() => this.client.keys(pattern));
|
this.client.hgetall(this.favHashKey(userName))
|
||||||
if (keys.length === 0) return {};
|
);
|
||||||
|
if (!all || Object.keys(all).length === 0) return {};
|
||||||
const result: Record<string, Favorite> = {};
|
const result: Record<string, Favorite> = {};
|
||||||
for (const fullKey of keys) {
|
for (const [field, value] of Object.entries(all)) {
|
||||||
const value = await withRetry(() => this.client.get(fullKey));
|
|
||||||
if (value) {
|
if (value) {
|
||||||
const keyPart = ensureString(fullKey.replace(`u:${userName}:fav:`, ''));
|
result[field] = 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAllFavorites(userName: string): Promise<void> {
|
||||||
|
await withRetry(() => this.client.del(this.favHashKey(userName)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 用户注册 / 登录 ----------
|
// ---------- 用户注册 / 登录 ----------
|
||||||
@@ -192,23 +197,11 @@ export class UpstashRedisStorage implements IStorage {
|
|||||||
// 删除搜索历史
|
// 删除搜索历史
|
||||||
await withRetry(() => this.client.del(this.shKey(userName)));
|
await withRetry(() => this.client.del(this.shKey(userName)));
|
||||||
|
|
||||||
// 删除播放记录
|
// 删除播放记录(Hash key 直接删除)
|
||||||
const playRecordPattern = `u:${userName}:pr:*`;
|
await withRetry(() => this.client.del(this.prHashKey(userName)));
|
||||||
const playRecordKeys = await withRetry(() =>
|
|
||||||
this.client.keys(playRecordPattern)
|
|
||||||
);
|
|
||||||
if (playRecordKeys.length > 0) {
|
|
||||||
await withRetry(() => this.client.del(...playRecordKeys));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除收藏夹
|
// 删除收藏夹(Hash key 直接删除)
|
||||||
const favoritePattern = `u:${userName}:fav:*`;
|
await withRetry(() => this.client.del(this.favHashKey(userName)));
|
||||||
const favoriteKeys = await withRetry(() =>
|
|
||||||
this.client.keys(favoritePattern)
|
|
||||||
);
|
|
||||||
if (favoriteKeys.length > 0) {
|
|
||||||
await withRetry(() => this.client.del(...favoriteKeys));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除跳过片头片尾配置
|
// 删除跳过片头片尾配置
|
||||||
const skipConfigPattern = `u:${userName}:skip:*`;
|
const skipConfigPattern = `u:${userName}:skip:*`;
|
||||||
@@ -344,6 +337,77 @@ export class UpstashRedisStorage implements IStorage {
|
|||||||
return configs;
|
return configs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 数据迁移:旧扁平 key → Hash 结构 ----------
|
||||||
|
private migrationKey() {
|
||||||
|
return 'sys:migration:hash_v1';
|
||||||
|
}
|
||||||
|
|
||||||
|
async migrateData(): Promise<void> {
|
||||||
|
// 检查是否已迁移
|
||||||
|
const migrated = await withRetry(() => this.client.get(this.migrationKey()));
|
||||||
|
if (migrated === 'done') return;
|
||||||
|
|
||||||
|
console.log('开始数据迁移:扁平 key → Hash 结构...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 迁移播放记录:u:*:pr:* → u:username:pr (Hash)
|
||||||
|
const prKeys: string[] = await withRetry(() => this.client.keys('u:*:pr:*'));
|
||||||
|
if (prKeys.length > 0) {
|
||||||
|
const oldPrKeys = prKeys.filter((k) => {
|
||||||
|
const parts = k.split(':');
|
||||||
|
return parts.length >= 4 && parts[2] === 'pr' && parts[3] !== '';
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const oldKey of oldPrKeys) {
|
||||||
|
const match = oldKey.match(/^u:(.+?):pr:(.+)$/);
|
||||||
|
if (!match) continue;
|
||||||
|
const [, userName, field] = match;
|
||||||
|
const value = await withRetry(() => this.client.get(oldKey));
|
||||||
|
if (value) {
|
||||||
|
await withRetry(() =>
|
||||||
|
this.client.hset(this.prHashKey(userName), { [field]: value })
|
||||||
|
);
|
||||||
|
await withRetry(() => this.client.del(oldKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldPrKeys.length > 0) {
|
||||||
|
console.log(`迁移了 ${oldPrKeys.length} 条播放记录`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 迁移收藏:u:*:fav:* → u:username:fav (Hash)
|
||||||
|
const favKeys: string[] = await withRetry(() => this.client.keys('u:*:fav:*'));
|
||||||
|
if (favKeys.length > 0) {
|
||||||
|
const oldFavKeys = favKeys.filter((k) => {
|
||||||
|
const parts = k.split(':');
|
||||||
|
return parts.length >= 4 && parts[2] === 'fav' && parts[3] !== '';
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const oldKey of oldFavKeys) {
|
||||||
|
const match = oldKey.match(/^u:(.+?):fav:(.+)$/);
|
||||||
|
if (!match) continue;
|
||||||
|
const [, userName, field] = match;
|
||||||
|
const value = await withRetry(() => this.client.get(oldKey));
|
||||||
|
if (value) {
|
||||||
|
await withRetry(() =>
|
||||||
|
this.client.hset(this.favHashKey(userName), { [field]: value })
|
||||||
|
);
|
||||||
|
await withRetry(() => this.client.del(oldKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldFavKeys.length > 0) {
|
||||||
|
console.log(`迁移了 ${oldFavKeys.length} 条收藏`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记迁移完成
|
||||||
|
await withRetry(() => this.client.set(this.migrationKey(), 'done'));
|
||||||
|
console.log('数据迁移完成');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('数据迁移失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 清空所有数据
|
// 清空所有数据
|
||||||
async clearAllData(): Promise<void> {
|
async clearAllData(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user