迁移跳过配置到新结构

This commit is contained in:
mtvpls
2026-01-01 10:01:35 +08:00
parent 9854f60716
commit 761ac4eaf3
11 changed files with 212 additions and 41 deletions

View File

@@ -338,3 +338,4 @@ async function refreshOpenList() {
console.error('OpenList 定时扫描失败:', err);
}
}

View File

@@ -25,6 +25,17 @@ export async function GET(request: NextRequest) {
if (userInfoV2.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
}
// 检查是否需要迁移跳过配置
if (!userInfoV2.skip_migrated) {
await db.migrateSkipConfigs(authInfo.username);
}
} else {
// 站长也需要检查迁移
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
if (!userInfoV2?.skip_migrated) {
await db.migrateSkipConfigs(authInfo.username);
}
}
const { searchParams } = new URL(request.url);

View File

@@ -52,7 +52,7 @@ export default function WarningPage() {
📢
</p>
<p className='text-sm sm:text-base text-blue-700'>
v2.0.5
v200.5.0
</p>
</div>

View File

@@ -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: [

View File

@@ -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') {

View File

@@ -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} 的跳过配置迁移完成`);
}
// ---------- 弹幕过滤配置 ----------

View File

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

View File

@@ -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} 的跳过配置迁移完成`);
}
// ---------- 弹幕过滤配置 ----------

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-console */
const CURRENT_VERSION = '205.0.0';
const CURRENT_VERSION = '205.0.1';
// 导出当前版本号供其他地方使用
export { CURRENT_VERSION };