迁移跳过配置到新结构
This commit is contained in:
@@ -11,6 +11,17 @@ export interface ChangelogEntry {
|
||||
|
||||
export const changelog: ChangelogEntry[] = [
|
||||
{
|
||||
version: '205.0.1',
|
||||
date: '2026-01-01',
|
||||
added: [
|
||||
],
|
||||
changed: [
|
||||
"迁移跳过配置到新数据结构"
|
||||
],
|
||||
fixed: [
|
||||
]
|
||||
},
|
||||
{
|
||||
version: '205.0.0',
|
||||
date: '2026-01-01',
|
||||
added: [
|
||||
|
||||
@@ -180,6 +180,7 @@ export class DbManager {
|
||||
created_at: number;
|
||||
playrecord_migrated?: boolean;
|
||||
favorite_migrated?: boolean;
|
||||
skip_migrated?: boolean;
|
||||
} | null> {
|
||||
if (typeof (this.storage as any).getUserInfoV2 === 'function') {
|
||||
return (this.storage as any).getUserInfoV2(userName);
|
||||
@@ -271,6 +272,13 @@ export class DbManager {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 跳过配置迁移 ----------
|
||||
async migrateSkipConfigs(userName: string): Promise<void> {
|
||||
if (typeof (this.storage as any).migrateSkipConfigs === 'function') {
|
||||
await (this.storage as any).migrateSkipConfigs(userName);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 数据迁移 ----------
|
||||
async migrateUsersFromConfig(adminConfig: AdminConfig): Promise<void> {
|
||||
if (typeof (this.storage as any).createUserV2 !== 'function') {
|
||||
|
||||
@@ -550,7 +550,10 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
await this.withRetry(() => this.client.del(favoriteKeys));
|
||||
}
|
||||
|
||||
// 删除跳过片头片尾配置
|
||||
// 删除跳过片头片尾配置(新hash结构)
|
||||
await this.withRetry(() => this.client.del(this.skipHashKey(userName)));
|
||||
|
||||
// 删除旧的跳过配置key(如果有)
|
||||
const skipConfigPattern = `u:${userName}:skip:*`;
|
||||
const skipConfigKeys = await this.withRetry(() =>
|
||||
this.client.keys(skipConfigPattern)
|
||||
@@ -655,6 +658,7 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
created_at: number;
|
||||
playrecord_migrated?: boolean;
|
||||
favorite_migrated?: boolean;
|
||||
skip_migrated?: boolean;
|
||||
} | null> {
|
||||
const userInfo = await this.withRetry(() =>
|
||||
this.client.hGetAll(this.userInfoKey(userName))
|
||||
@@ -673,6 +677,7 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
created_at: parseInt(userInfo.created_at || '0', 10),
|
||||
playrecord_migrated: userInfo.playrecord_migrated === 'true',
|
||||
favorite_migrated: userInfo.favorite_migrated === 'true',
|
||||
skip_migrated: userInfo.skip_migrated === 'true',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -947,8 +952,8 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
}
|
||||
|
||||
// ---------- 跳过片头片尾配置 ----------
|
||||
private skipConfigKey(user: string, source: string, id: string) {
|
||||
return `u:${user}:skip:${source}+${id}`;
|
||||
private skipHashKey(user: string) {
|
||||
return `u:${user}:skip`; // u:username:skip (hash结构)
|
||||
}
|
||||
|
||||
private danmakuFilterConfigKey(user: string) {
|
||||
@@ -960,8 +965,9 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<SkipConfig | null> {
|
||||
const key = `${source}+${id}`;
|
||||
const val = await this.withRetry(() =>
|
||||
this.client.get(this.skipConfigKey(userName, source, id))
|
||||
this.client.hGet(this.skipHashKey(userName), key)
|
||||
);
|
||||
return val ? (JSON.parse(val) as SkipConfig) : null;
|
||||
}
|
||||
@@ -972,11 +978,9 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
id: string,
|
||||
config: SkipConfig
|
||||
): Promise<void> {
|
||||
const key = `${source}+${id}`;
|
||||
await this.withRetry(() =>
|
||||
this.client.set(
|
||||
this.skipConfigKey(userName, source, id),
|
||||
JSON.stringify(config)
|
||||
)
|
||||
this.client.hSet(this.skipHashKey(userName), key, JSON.stringify(config))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -985,39 +989,100 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const key = `${source}+${id}`;
|
||||
await this.withRetry(() =>
|
||||
this.client.del(this.skipConfigKey(userName, source, id))
|
||||
this.client.hDel(this.skipHashKey(userName), key)
|
||||
);
|
||||
}
|
||||
|
||||
async getAllSkipConfigs(
|
||||
userName: string
|
||||
): Promise<{ [key: string]: SkipConfig }> {
|
||||
const pattern = `u:${userName}:skip:*`;
|
||||
const keys = await this.withRetry(() => this.client.keys(pattern));
|
||||
const hashData = await this.withRetry(() =>
|
||||
this.client.hGetAll(this.skipHashKey(userName))
|
||||
);
|
||||
|
||||
if (keys.length === 0) {
|
||||
return {};
|
||||
const result: Record<string, SkipConfig> = {};
|
||||
for (const [key, value] of Object.entries(hashData)) {
|
||||
if (value) {
|
||||
result[key] = JSON.parse(value) as SkipConfig;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 迁移跳过配置:从旧的多key结构迁移到新的hash结构
|
||||
async migrateSkipConfigs(userName: string): Promise<void> {
|
||||
const existingMigration = playRecordLocks.get(`${userName}:skip`);
|
||||
if (existingMigration) {
|
||||
console.log(`用户 ${userName} 的跳过配置正在迁移中,等待完成...`);
|
||||
await existingMigration;
|
||||
return;
|
||||
}
|
||||
|
||||
const configs: { [key: string]: SkipConfig } = {};
|
||||
const migrationPromise = this.doSkipConfigMigration(userName);
|
||||
playRecordLocks.set(`${userName}:skip`, migrationPromise);
|
||||
|
||||
// 批量获取所有配置
|
||||
const values = await this.withRetry(() => this.client.mGet(keys));
|
||||
try {
|
||||
await migrationPromise;
|
||||
} finally {
|
||||
playRecordLocks.delete(`${userName}:skip`);
|
||||
}
|
||||
}
|
||||
|
||||
keys.forEach((key, index) => {
|
||||
private async doSkipConfigMigration(userName: string): Promise<void> {
|
||||
console.log(`开始迁移用户 ${userName} 的跳过配置...`);
|
||||
|
||||
const userInfo = await this.getUserInfoV2(userName);
|
||||
if (userInfo?.skip_migrated) {
|
||||
console.log(`用户 ${userName} 的跳过配置已经迁移过,跳过`);
|
||||
return;
|
||||
}
|
||||
|
||||
const pattern = `u:${userName}:skip:*`;
|
||||
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), 'skip_migrated', 'true')
|
||||
);
|
||||
const { userInfoCache } = await import('./user-cache');
|
||||
userInfoCache?.delete(userName);
|
||||
return;
|
||||
}
|
||||
|
||||
const values = await this.withRetry(() => this.client.mGet(oldKeys));
|
||||
|
||||
const hashData: Record<string, string> = {};
|
||||
oldKeys.forEach((key, index) => {
|
||||
const value = values[index];
|
||||
if (value) {
|
||||
// 从key中提取source+id
|
||||
const match = key.match(/^u:.+?:skip:(.+)$/);
|
||||
if (match) {
|
||||
const sourceAndId = match[1];
|
||||
configs[sourceAndId] = JSON.parse(value as string) as SkipConfig;
|
||||
hashData[sourceAndId] = value as string;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return configs;
|
||||
if (Object.keys(hashData).length > 0) {
|
||||
await this.withRetry(() =>
|
||||
this.client.hSet(this.skipHashKey(userName), hashData)
|
||||
);
|
||||
console.log(`成功迁移 ${Object.keys(hashData).length} 条跳过配置到hash结构`);
|
||||
}
|
||||
|
||||
await this.withRetry(() => this.client.del(oldKeys));
|
||||
console.log(`删除了 ${oldKeys.length} 个旧的跳过配置key`);
|
||||
|
||||
await this.withRetry(() =>
|
||||
this.client.hSet(this.userInfoKey(userName), 'skip_migrated', 'true')
|
||||
);
|
||||
const { userInfoCache } = await import('./user-cache');
|
||||
userInfoCache?.delete(userName);
|
||||
|
||||
console.log(`用户 ${userName} 的跳过配置迁移完成`);
|
||||
}
|
||||
|
||||
// ---------- 弹幕过滤配置 ----------
|
||||
|
||||
@@ -87,6 +87,8 @@ export interface IStorage {
|
||||
): Promise<void>;
|
||||
deleteSkipConfig(userName: string, source: string, id: string): Promise<void>;
|
||||
getAllSkipConfigs(userName: string): Promise<{ [key: string]: SkipConfig }>;
|
||||
// 迁移跳过配置
|
||||
migrateSkipConfigs(userName: string): Promise<void>;
|
||||
|
||||
// 弹幕过滤配置相关
|
||||
getDanmakuFilterConfig(userName: string): Promise<DanmakuFilterConfig | null>;
|
||||
|
||||
@@ -451,7 +451,10 @@ export class UpstashRedisStorage implements IStorage {
|
||||
await withRetry(() => this.client.del(...favoriteKeys));
|
||||
}
|
||||
|
||||
// 删除跳过片头片尾配置
|
||||
// 删除跳过片头片尾配置(新hash结构)
|
||||
await withRetry(() => this.client.del(this.skipHashKey(userName)));
|
||||
|
||||
// 删除旧的跳过配置key(如果有)
|
||||
const skipConfigPattern = `u:${userName}:skip:*`;
|
||||
const skipConfigKeys = await withRetry(() =>
|
||||
this.client.keys(skipConfigPattern)
|
||||
@@ -564,6 +567,7 @@ export class UpstashRedisStorage implements IStorage {
|
||||
created_at: number;
|
||||
playrecord_migrated?: boolean;
|
||||
favorite_migrated?: boolean;
|
||||
skip_migrated?: boolean;
|
||||
} | null> {
|
||||
// 先从缓存获取
|
||||
const cached = userInfoCache?.get(userName);
|
||||
@@ -607,6 +611,16 @@ export class UpstashRedisStorage implements IStorage {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 skip_migrated 字段
|
||||
let skip_migrated: boolean | undefined = undefined;
|
||||
if (userInfo.skip_migrated !== undefined) {
|
||||
if (typeof userInfo.skip_migrated === 'boolean') {
|
||||
skip_migrated = userInfo.skip_migrated;
|
||||
} else if (typeof userInfo.skip_migrated === 'string') {
|
||||
skip_migrated = userInfo.skip_migrated === 'true';
|
||||
}
|
||||
}
|
||||
|
||||
// 安全解析 tags 字段
|
||||
let tags: string[] | undefined = undefined;
|
||||
if (userInfo.tags) {
|
||||
@@ -646,6 +660,7 @@ export class UpstashRedisStorage implements IStorage {
|
||||
created_at: parseInt((userInfo.created_at as string) || '0', 10),
|
||||
playrecord_migrated,
|
||||
favorite_migrated,
|
||||
skip_migrated,
|
||||
};
|
||||
|
||||
// 存入缓存
|
||||
@@ -968,8 +983,8 @@ export class UpstashRedisStorage implements IStorage {
|
||||
}
|
||||
|
||||
// ---------- 跳过片头片尾配置 ----------
|
||||
private skipConfigKey(user: string, source: string, id: string) {
|
||||
return `u:${user}:skip:${source}+${id}`;
|
||||
private skipHashKey(user: string) {
|
||||
return `u:${user}:skip`; // u:username:skip (hash结构)
|
||||
}
|
||||
|
||||
private danmakuFilterConfigKey(user: string) {
|
||||
@@ -981,8 +996,9 @@ export class UpstashRedisStorage implements IStorage {
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<SkipConfig | null> {
|
||||
const key = `${source}+${id}`;
|
||||
const val = await withRetry(() =>
|
||||
this.client.get(this.skipConfigKey(userName, source, id))
|
||||
this.client.hget(this.skipHashKey(userName), key)
|
||||
);
|
||||
return val ? (val as SkipConfig) : null;
|
||||
}
|
||||
@@ -993,8 +1009,9 @@ export class UpstashRedisStorage implements IStorage {
|
||||
id: string,
|
||||
config: SkipConfig
|
||||
): Promise<void> {
|
||||
const key = `${source}+${id}`;
|
||||
await withRetry(() =>
|
||||
this.client.set(this.skipConfigKey(userName, source, id), config)
|
||||
this.client.hset(this.skipHashKey(userName), { [key]: config })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1003,39 +1020,92 @@ export class UpstashRedisStorage implements IStorage {
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const key = `${source}+${id}`;
|
||||
await withRetry(() =>
|
||||
this.client.del(this.skipConfigKey(userName, source, id))
|
||||
this.client.hdel(this.skipHashKey(userName), key)
|
||||
);
|
||||
}
|
||||
|
||||
async getAllSkipConfigs(
|
||||
userName: string
|
||||
): Promise<{ [key: string]: SkipConfig }> {
|
||||
const pattern = `u:${userName}:skip:*`;
|
||||
const keys = await withRetry(() => this.client.keys(pattern));
|
||||
const hashData = await withRetry(() =>
|
||||
this.client.hgetall<Record<string, SkipConfig>>(this.skipHashKey(userName))
|
||||
);
|
||||
|
||||
if (keys.length === 0) {
|
||||
return {};
|
||||
return hashData || {};
|
||||
}
|
||||
|
||||
// 迁移跳过配置:从旧的多key结构迁移到新的hash结构
|
||||
async migrateSkipConfigs(userName: string): Promise<void> {
|
||||
const existingMigration = playRecordLocks.get(`${userName}:skip`);
|
||||
if (existingMigration) {
|
||||
console.log(`用户 ${userName} 的跳过配置正在迁移中,等待完成...`);
|
||||
await existingMigration;
|
||||
return;
|
||||
}
|
||||
|
||||
const configs: { [key: string]: SkipConfig } = {};
|
||||
const migrationPromise = this.doSkipConfigMigration(userName);
|
||||
playRecordLocks.set(`${userName}:skip`, migrationPromise);
|
||||
|
||||
// 批量获取所有配置
|
||||
const values = await withRetry(() => this.client.mget(keys));
|
||||
try {
|
||||
await migrationPromise;
|
||||
} finally {
|
||||
playRecordLocks.delete(`${userName}:skip`);
|
||||
}
|
||||
}
|
||||
|
||||
keys.forEach((key, index) => {
|
||||
private async doSkipConfigMigration(userName: string): Promise<void> {
|
||||
console.log(`开始迁移用户 ${userName} 的跳过配置...`);
|
||||
|
||||
const userInfo = await this.getUserInfoV2(userName);
|
||||
if (userInfo?.skip_migrated) {
|
||||
console.log(`用户 ${userName} 的跳过配置已经迁移过,跳过`);
|
||||
return;
|
||||
}
|
||||
|
||||
const pattern = `u:${userName}:skip:*`;
|
||||
const oldKeys: string[] = await withRetry(() => this.client.keys(pattern));
|
||||
|
||||
if (oldKeys.length === 0) {
|
||||
console.log(`用户 ${userName} 没有旧的跳过配置,标记为已迁移`);
|
||||
await withRetry(() =>
|
||||
this.client.hset(this.userInfoKey(userName), { skip_migrated: 'true' })
|
||||
);
|
||||
userInfoCache?.delete(userName);
|
||||
return;
|
||||
}
|
||||
|
||||
const values = await withRetry(() => this.client.mget(oldKeys));
|
||||
|
||||
const hashData: Record<string, SkipConfig> = {};
|
||||
oldKeys.forEach((key, index) => {
|
||||
const value = values[index];
|
||||
if (value) {
|
||||
// 从key中提取source+id
|
||||
const match = key.match(/^u:.+?:skip:(.+)$/);
|
||||
if (match) {
|
||||
const sourceAndId = match[1];
|
||||
configs[sourceAndId] = value as SkipConfig;
|
||||
hashData[sourceAndId] = value as SkipConfig;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return configs;
|
||||
if (Object.keys(hashData).length > 0) {
|
||||
await withRetry(() =>
|
||||
this.client.hset(this.skipHashKey(userName), hashData)
|
||||
);
|
||||
console.log(`成功迁移 ${Object.keys(hashData).length} 条跳过配置到hash结构`);
|
||||
}
|
||||
|
||||
await withRetry(() => this.client.del(...oldKeys));
|
||||
console.log(`删除了 ${oldKeys.length} 个旧的跳过配置key`);
|
||||
|
||||
await withRetry(() =>
|
||||
this.client.hset(this.userInfoKey(userName), { skip_migrated: 'true' })
|
||||
);
|
||||
userInfoCache?.delete(userName);
|
||||
|
||||
console.log(`用户 ${userName} 的跳过配置迁移完成`);
|
||||
}
|
||||
|
||||
// ---------- 弹幕过滤配置 ----------
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const CURRENT_VERSION = '205.0.0';
|
||||
const CURRENT_VERSION = '205.0.1';
|
||||
|
||||
// 导出当前版本号供其他地方使用
|
||||
export { CURRENT_VERSION };
|
||||
|
||||
Reference in New Issue
Block a user