Files
MoonTVPlus/src/lib/redis-base.db.ts

1411 lines
45 KiB
TypeScript
Raw Normal View History

2025-08-15 19:26:13 +08:00
/* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import { createClient, RedisClientType } from 'redis';
import { AdminConfig } from './admin.types';
import { RedisAdapter } from './redis-adapter';
2025-08-15 19:26:13 +08:00
import { Favorite, IStorage, PlayRecord, SkipConfig } from './types';
2026-01-16 16:11:31 +08:00
import { userInfoCache } from './user-cache';
2025-08-15 19:26:13 +08:00
// 搜索历史最大条数
const SEARCH_HISTORY_LIMIT = 20;
// 数据类型转换辅助函数
function ensureString(value: any): string {
return String(value);
}
function ensureStringArray(value: any[]): string[] {
return value.map((item) => String(item));
}
// 内存锁:用于防止同一用户的并发播放记录操作(迁移、清理等)
const playRecordLocks = new Map<string, Promise<void>>();
2025-08-15 19:26:13 +08:00
// 连接配置接口
export interface RedisConnectionConfig {
url: string;
clientName: string; // 用于日志显示,如 "Redis" 或 "Pika"
}
// 添加Redis操作重试包装器
export function createRetryWrapper(clientName: string, getClient: () => RedisClientType) {
2025-08-15 19:26:13 +08:00
return async function withRetry<T>(
operation: () => Promise<T>,
maxRetries = 3
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (err: any) {
const isLastAttempt = i === maxRetries - 1;
const isConnectionError =
err.message?.includes('Connection') ||
err.message?.includes('ECONNREFUSED') ||
err.message?.includes('ENOTFOUND') ||
err.code === 'ECONNRESET' ||
err.code === 'EPIPE';
if (isConnectionError && !isLastAttempt) {
console.log(
`${clientName} operation failed, retrying... (${i + 1}/${maxRetries})`
);
console.error('Error:', err.message);
// 等待一段时间后重试
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
// 尝试重新连接
try {
const client = getClient();
if (!client.isOpen) {
await client.connect();
}
} catch (reconnectErr) {
console.error('Failed to reconnect:', reconnectErr);
}
continue;
}
throw err;
}
}
throw new Error('Max retries exceeded');
};
}
// 创建客户端的工厂函数
export function createRedisClient(config: RedisConnectionConfig, globalSymbol: symbol): RedisClientType {
let client: RedisClientType | undefined = (global as any)[globalSymbol];
if (!client) {
if (!config.url) {
throw new Error(`${config.clientName}_URL env variable not set`);
}
// 创建客户端配置
const clientConfig: any = {
url: config.url,
socket: {
// 重连策略指数退避最大30秒
reconnectStrategy: (retries: number) => {
console.log(`${config.clientName} reconnection attempt ${retries + 1}`);
if (retries > 10) {
console.error(`${config.clientName} max reconnection attempts exceeded`);
return false; // 停止重连
}
return Math.min(1000 * Math.pow(2, retries), 30000); // 指数退避最大30秒
},
connectTimeout: 10000, // 10秒连接超时
// 设置no delay减少延迟
noDelay: true,
},
// 添加其他配置
pingInterval: 30000, // 30秒ping一次保持连接活跃
};
client = createClient(clientConfig);
// 添加错误事件监听
client.on('error', (err) => {
console.error(`${config.clientName} client error:`, err);
});
client.on('connect', () => {
console.log(`${config.clientName} connected`);
});
client.on('reconnecting', () => {
console.log(`${config.clientName} reconnecting...`);
});
client.on('ready', () => {
console.log(`${config.clientName} ready`);
});
// 初始连接,带重试机制
const connectWithRetry = async () => {
try {
await client!.connect();
console.log(`${config.clientName} connected successfully`);
} catch (err) {
console.error(`${config.clientName} initial connection failed:`, err);
console.log('Will retry in 5 seconds...');
setTimeout(connectWithRetry, 5000);
}
};
connectWithRetry();
(global as any)[globalSymbol] = client;
}
return client;
}
// 抽象基类包含所有通用的Redis操作逻辑
export abstract class BaseRedisStorage implements IStorage {
protected adapter: RedisAdapter;
2025-08-15 19:26:13 +08:00
protected withRetry: <T>(operation: () => Promise<T>, maxRetries?: number) => Promise<T>;
// 保留 client 属性用于向后兼容(数据迁移代码使用)
client: any;
constructor(adapter: RedisAdapter, withRetryFn: <T>(operation: () => Promise<T>, maxRetries?: number) => Promise<T>) {
this.adapter = adapter;
this.withRetry = withRetryFn;
// 创建兼容层,同时支持驼峰和小写命名(用于数据迁移代码)
this.client = {
hSet: (key: string, ...args: any[]) => {
if (args.length === 1) {
return this.adapter.hSet(key, args[0]);
}
return this.adapter.hSet(key, args[0], args[1]);
},
hset: (key: string, ...args: any[]) => {
if (args.length === 1) {
return this.adapter.hSet(key, args[0]);
}
return this.adapter.hSet(key, args[0], args[1]);
},
hGet: (key: string, field: string) => this.adapter.hGet(key, field),
hget: (key: string, field: string) => this.adapter.hGet(key, field),
hGetAll: (key: string) => this.adapter.hGetAll(key),
hgetall: (key: string) => this.adapter.hGetAll(key),
zAdd: (key: string, member: { score: number; value: string }) => this.adapter.zAdd(key, member),
zadd: (key: string, member: { score: number; value: string }) => this.adapter.zAdd(key, member),
set: (key: string, value: string) => this.adapter.set(key, value),
get: (key: string) => this.adapter.get(key),
del: (...keys: string[]) => this.adapter.del(keys),
};
2025-08-15 19:26:13 +08:00
}
// ---------- 播放记录 ----------
private prHashKey(user: string) {
return `u:${user}:pr`; // u:username:pr (hash结构)
}
// 旧版播放记录key用于迁移
private prOldKey(user: string, key: string) {
2025-08-15 19:26:13 +08:00
return `u:${user}:pr:${key}`; // u:username:pr:source+id
}
async getPlayRecord(
userName: string,
key: string
): Promise<PlayRecord | null> {
const val = await this.withRetry(() =>
this.adapter.hGet(this.prHashKey(userName), key)
2025-08-15 19:26:13 +08:00
);
return val ? (JSON.parse(val) as PlayRecord) : null;
}
async setPlayRecord(
userName: string,
key: string,
record: PlayRecord
): Promise<void> {
await this.withRetry(() =>
this.adapter.hSet(this.prHashKey(userName), key, JSON.stringify(record))
2025-08-15 19:26:13 +08:00
);
}
async getAllPlayRecords(
userName: string
): Promise<Record<string, PlayRecord>> {
const hashData = await this.withRetry(() =>
this.adapter.hGetAll(this.prHashKey(userName))
);
2025-08-15 19:26:13 +08:00
const result: Record<string, PlayRecord> = {};
for (const [key, value] of Object.entries(hashData)) {
if (value) {
result[key] = JSON.parse(value) as PlayRecord;
2025-08-15 19:26:13 +08:00
}
}
2025-08-15 19:26:13 +08:00
return result;
}
async deletePlayRecord(userName: string, key: string): Promise<void> {
await this.withRetry(() => this.adapter.hDel(this.prHashKey(userName), key));
}
// 清理超出限制的旧播放记录
async cleanupOldPlayRecords(userName: string): Promise<void> {
// 检查是否已有正在进行的操作
const existingLock = playRecordLocks.get(userName);
if (existingLock) {
console.log(`用户 ${userName} 的播放记录操作正在进行中,跳过清理`);
await existingLock;
return;
}
// 创建新的操作Promise
const cleanupPromise = this.doCleanup(userName);
playRecordLocks.set(userName, cleanupPromise);
try {
await cleanupPromise;
} finally {
// 操作完成后清除锁
playRecordLocks.delete(userName);
}
}
// 实际执行清理的方法
private async doCleanup(userName: string): Promise<void> {
try {
// 获取配置的最大播放记录数默认100
const maxRecords = parseInt(process.env.MAX_PLAY_RECORDS_PER_USER || '100', 10);
const threshold = maxRecords + 10; // 超过最大值+10时才触发清理
// 获取所有播放记录
const allRecords = await this.getAllPlayRecords(userName);
const recordCount = Object.keys(allRecords).length;
// 如果记录数未超过阈值,不需要清理
if (recordCount <= threshold) {
return;
}
console.log(`用户 ${userName} 的播放记录数 ${recordCount} 超过阈值 ${threshold},开始清理...`);
// 将记录转换为数组并按 save_time 排序(从旧到新)
const sortedRecords = Object.entries(allRecords).sort(
([, a], [, b]) => a.save_time - b.save_time
);
// 计算需要删除的记录数
const deleteCount = recordCount - maxRecords;
// 删除最旧的记录
const recordsToDelete = sortedRecords.slice(0, deleteCount);
for (const [key] of recordsToDelete) {
await this.deletePlayRecord(userName, key);
}
console.log(`已删除用户 ${userName}${deleteCount} 条最旧播放记录`);
} catch (error) {
console.error(`清理用户 ${userName} 播放记录失败:`, error);
// 清理失败不影响主流程,只记录错误
}
}
// 迁移播放记录从旧的多key结构迁移到新的hash结构
async migratePlayRecords(userName: string): Promise<void> {
// 检查是否已有正在进行的迁移
2025-12-30 22:00:36 +08:00
const existingMigration = playRecordLocks.get(userName);
if (existingMigration) {
console.log(`用户 ${userName} 的播放记录正在迁移中,等待完成...`);
await existingMigration;
return;
}
// 创建新的迁移Promise
const migrationPromise = this.doMigration(userName);
2025-12-30 22:00:36 +08:00
playRecordLocks.set(userName, migrationPromise);
try {
await migrationPromise;
} finally {
// 迁移完成后清除锁
2025-12-30 22:00:36 +08:00
playRecordLocks.delete(userName);
}
}
// 实际执行迁移的方法
private async doMigration(userName: string): Promise<void> {
console.log(`开始迁移用户 ${userName} 的播放记录...`);
// 1. 检查是否已经迁移过
const userInfo = await this.getUserInfoV2(userName);
if (userInfo?.playrecord_migrated) {
console.log(`用户 ${userName} 的播放记录已经迁移过,跳过`);
return;
}
// 2. 获取旧结构的所有播放记录key
const pattern = `u:${userName}:pr:*`;
const oldKeys: string[] = await this.withRetry(() => this.adapter.keys(pattern));
if (oldKeys.length === 0) {
console.log(`用户 ${userName} 没有旧的播放记录,标记为已迁移`);
// 即使没有数据也标记为已迁移
await this.withRetry(() =>
this.adapter.hSet(this.userInfoKey(userName), 'playrecord_migrated', 'true')
);
// 清除用户信息缓存
const { userInfoCache } = await import('./user-cache');
userInfoCache?.delete(userName);
return;
}
console.log(`找到 ${oldKeys.length} 条旧播放记录,开始迁移...`);
// 3. 批量获取旧数据
const oldValues = await this.withRetry(() => this.adapter.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}:pr:`, ''));
hashData[keyPart] = raw;
}
});
// 5. 写入新的hash结构
if (Object.keys(hashData).length > 0) {
await this.withRetry(() =>
this.adapter.hSet(this.prHashKey(userName), hashData)
);
console.log(`成功迁移 ${Object.keys(hashData).length} 条播放记录到hash结构`);
}
// 6. 删除旧的key
await this.withRetry(() => this.adapter.del(oldKeys));
console.log(`删除了 ${oldKeys.length} 个旧的播放记录key`);
// 7. 标记迁移完成
await this.withRetry(() =>
this.adapter.hSet(this.userInfoKey(userName), 'playrecord_migrated', 'true')
);
// 8. 清除用户信息缓存,确保下次获取时能读取到最新的迁移标识
const { userInfoCache } = await import('./user-cache');
userInfoCache?.delete(userName);
console.log(`用户 ${userName} 的播放记录迁移完成`);
2025-08-15 19:26:13 +08:00
}
// ---------- 收藏 ----------
2025-12-30 22:00:36 +08:00
private favHashKey(user: string) {
return `u:${user}:fav`; // u:username:fav (hash结构)
}
// 旧版收藏key用于迁移
private favOldKey(user: string, key: string) {
2025-08-15 19:26:13 +08:00
return `u:${user}:fav:${key}`;
}
async getFavorite(userName: string, key: string): Promise<Favorite | null> {
const val = await this.withRetry(() =>
this.adapter.hGet(this.favHashKey(userName), key)
2025-08-15 19:26:13 +08:00
);
return val ? (JSON.parse(val) as Favorite) : null;
}
async setFavorite(
userName: string,
key: string,
favorite: Favorite
): Promise<void> {
await this.withRetry(() =>
this.adapter.hSet(this.favHashKey(userName), key, JSON.stringify(favorite))
2025-08-15 19:26:13 +08:00
);
}
async getAllFavorites(userName: string): Promise<Record<string, Favorite>> {
2025-12-30 22:00:36 +08:00
const hashData = await this.withRetry(() =>
this.adapter.hGetAll(this.favHashKey(userName))
2025-12-30 22:00:36 +08:00
);
2025-08-15 19:26:13 +08:00
const result: Record<string, Favorite> = {};
2025-12-30 22:00:36 +08:00
for (const [key, value] of Object.entries(hashData)) {
if (value) {
result[key] = JSON.parse(value) as Favorite;
2025-08-15 19:26:13 +08:00
}
2025-12-30 22:00:36 +08:00
}
2025-08-15 19:26:13 +08:00
return result;
}
async deleteFavorite(userName: string, key: string): Promise<void> {
await this.withRetry(() => this.adapter.hDel(this.favHashKey(userName), key));
2025-12-30 22:00:36 +08:00
}
// 迁移收藏从旧的多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.adapter.keys(pattern));
2025-12-30 22:00:36 +08:00
if (oldKeys.length === 0) {
console.log(`用户 ${userName} 没有旧的收藏,标记为已迁移`);
// 即使没有数据也标记为已迁移
await this.withRetry(() =>
this.adapter.hSet(this.userInfoKey(userName), 'favorite_migrated', 'true')
2025-12-30 22:00:36 +08:00
);
// 清除用户信息缓存
const { userInfoCache } = await import('./user-cache');
userInfoCache?.delete(userName);
return;
}
console.log(`找到 ${oldKeys.length} 条旧收藏,开始迁移...`);
// 3. 批量获取旧数据
const oldValues = await this.withRetry(() => this.adapter.mGet(oldKeys));
2025-12-30 22:00:36 +08:00
// 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.adapter.hSet(this.favHashKey(userName), hashData)
2025-12-30 22:00:36 +08:00
);
console.log(`成功迁移 ${Object.keys(hashData).length} 条收藏到hash结构`);
}
// 6. 删除旧的key
await this.withRetry(() => this.adapter.del(oldKeys));
2025-12-30 22:00:36 +08:00
console.log(`删除了 ${oldKeys.length} 个旧的收藏key`);
// 7. 标记迁移完成
await this.withRetry(() =>
this.adapter.hSet(this.userInfoKey(userName), 'favorite_migrated', 'true')
2025-12-30 22:00:36 +08:00
);
// 8. 清除用户信息缓存,确保下次获取时能读取到最新的迁移标识
const { userInfoCache } = await import('./user-cache');
userInfoCache?.delete(userName);
console.log(`用户 ${userName} 的收藏迁移完成`);
2025-08-15 19:26:13 +08:00
}
2026-02-01 17:07:50 +08:00
// ---------- 音乐播放记录相关 ----------
private musicPlayRecordHashKey(userName: string) {
return `u:${userName}:music_play_records`;
}
async getMusicPlayRecord(userName: string, key: string): Promise<any | null> {
const value = await this.withRetry(() =>
this.adapter.hGet(this.musicPlayRecordHashKey(userName), key)
);
return value ? JSON.parse(value) : null;
}
async setMusicPlayRecord(userName: string, key: string, record: any): Promise<void> {
await this.withRetry(() =>
this.adapter.hSet(
this.musicPlayRecordHashKey(userName),
key,
JSON.stringify(record)
)
);
}
async getAllMusicPlayRecords(userName: string): Promise<Record<string, any>> {
const hashData = await this.withRetry(() =>
this.adapter.hGetAll(this.musicPlayRecordHashKey(userName))
);
const result: Record<string, any> = {};
for (const [key, value] of Object.entries(hashData)) {
if (value) {
result[key] = JSON.parse(value);
}
}
return result;
}
async deleteMusicPlayRecord(userName: string, key: string): Promise<void> {
await this.withRetry(() =>
this.adapter.hDel(this.musicPlayRecordHashKey(userName), key)
);
}
async clearAllMusicPlayRecords(userName: string): Promise<void> {
await this.withRetry(() =>
this.adapter.del(this.musicPlayRecordHashKey(userName))
);
}
2025-12-24 00:24:50 +08:00
// ---------- 用户注册 / 登录(旧版本,保持兼容) ----------
2025-08-15 19:26:13 +08:00
private userPwdKey(user: string) {
return `u:${user}:pwd`;
}
async verifyUser(userName: string, password: string): Promise<boolean> {
const stored = await this.withRetry(() =>
this.adapter.get(this.userPwdKey(userName))
2025-08-15 19:26:13 +08:00
);
if (stored === null) return false;
// 确保比较时都是字符串类型
return ensureString(stored) === password;
}
// 检查用户是否存在
async checkUserExist(userName: string): Promise<boolean> {
// 使用 EXISTS 判断 key 是否存在
const exists = await this.withRetry(() =>
this.adapter.exists(this.userPwdKey(userName))
2025-08-15 19:26:13 +08:00
);
return exists === 1;
}
// 修改用户密码
async changePassword(userName: string, newPassword: string): Promise<void> {
// 简单存储明文密码,生产环境应加密
await this.withRetry(() =>
this.adapter.set(this.userPwdKey(userName), newPassword)
2025-08-15 19:26:13 +08:00
);
}
// 删除用户及其所有数据
async deleteUser(userName: string): Promise<void> {
// 删除用户密码
await this.withRetry(() => this.adapter.del(this.userPwdKey(userName)));
2025-08-15 19:26:13 +08:00
// 删除搜索历史
await this.withRetry(() => this.adapter.del(this.shKey(userName)));
2025-08-15 19:26:13 +08:00
// 删除播放记录新hash结构
await this.withRetry(() => this.adapter.del(this.prHashKey(userName)));
// 删除旧的播放记录key如果有
2025-08-15 19:26:13 +08:00
const playRecordPattern = `u:${userName}:pr:*`;
const playRecordKeys = await this.withRetry(() =>
this.adapter.keys(playRecordPattern)
2025-08-15 19:26:13 +08:00
);
if (playRecordKeys.length > 0) {
await this.withRetry(() => this.adapter.del(playRecordKeys));
2025-08-15 19:26:13 +08:00
}
2025-12-30 22:00:36 +08:00
// 删除收藏夹新hash结构
await this.withRetry(() => this.adapter.del(this.favHashKey(userName)));
2025-12-30 22:00:36 +08:00
// 删除旧的收藏key如果有
2025-08-15 19:26:13 +08:00
const favoritePattern = `u:${userName}:fav:*`;
const favoriteKeys = await this.withRetry(() =>
this.adapter.keys(favoritePattern)
2025-08-15 19:26:13 +08:00
);
if (favoriteKeys.length > 0) {
await this.withRetry(() => this.adapter.del(favoriteKeys));
2025-08-15 19:26:13 +08:00
}
2026-01-01 10:01:35 +08:00
// 删除跳过片头片尾配置新hash结构
await this.withRetry(() => this.adapter.del(this.skipHashKey(userName)));
2026-01-01 10:01:35 +08:00
// 删除旧的跳过配置key如果有
2025-08-15 19:26:13 +08:00
const skipConfigPattern = `u:${userName}:skip:*`;
const skipConfigKeys = await this.withRetry(() =>
this.adapter.keys(skipConfigPattern)
2025-08-15 19:26:13 +08:00
);
if (skipConfigKeys.length > 0) {
await this.withRetry(() => this.adapter.del(skipConfigKeys));
2025-08-15 19:26:13 +08:00
}
}
2025-12-24 00:24:50 +08:00
// ---------- 新版用户存储使用Hash和Sorted Set ----------
private userInfoKey(userName: string) {
return `user:${userName}:info`;
}
private userListKey() {
return 'user:list';
}
private oidcSubKey(oidcSub: string) {
return `oidc:sub:${oidcSub}`;
}
// SHA256加密密码
private async hashPassword(password: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(password);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
// 创建新用户(新版本)
async createUserV2(
userName: string,
password: string,
role: 'owner' | 'admin' | 'user' = 'user',
tags?: string[],
2025-12-25 00:39:30 +08:00
oidcSub?: string,
enabledApis?: string[]
2025-12-24 00:24:50 +08:00
): Promise<void> {
const hashedPassword = await this.hashPassword(password);
const createdAt = Date.now();
// 存储用户信息到Hash
const userInfo: Record<string, string> = {
role,
banned: 'false',
password: hashedPassword,
created_at: createdAt.toString(),
};
if (tags && tags.length > 0) {
userInfo.tags = JSON.stringify(tags);
}
2025-12-25 00:39:30 +08:00
if (enabledApis && enabledApis.length > 0) {
userInfo.enabledApis = JSON.stringify(enabledApis);
}
2025-12-24 00:24:50 +08:00
if (oidcSub) {
userInfo.oidcSub = oidcSub;
// 创建OIDC映射
await this.withRetry(() => this.adapter.set(this.oidcSubKey(oidcSub), userName));
2025-12-24 00:24:50 +08:00
}
await this.withRetry(() => this.adapter.hSet(this.userInfoKey(userName), userInfo));
2025-12-24 00:24:50 +08:00
// 添加到用户列表Sorted Set按注册时间排序
await this.withRetry(() => this.adapter.zAdd(this.userListKey(), {
2025-12-24 00:24:50 +08:00
score: createdAt,
value: userName,
}));
2025-12-24 20:49:00 +08:00
// 如果创建的是站长用户,清除站长存在状态缓存
if (userName === process.env.USERNAME) {
const { ownerExistenceCache } = await import('./user-cache');
ownerExistenceCache.delete(userName);
}
2025-12-24 00:24:50 +08:00
}
// 验证用户密码(新版本)
async verifyUserV2(userName: string, password: string): Promise<boolean> {
const userInfo = await this.withRetry(() =>
this.adapter.hGetAll(this.userInfoKey(userName))
2025-12-24 00:24:50 +08:00
);
if (!userInfo || !userInfo.password) {
return false;
}
const hashedPassword = await this.hashPassword(password);
return userInfo.password === hashedPassword;
}
// 获取用户信息(新版本)
async getUserInfoV2(userName: string): Promise<{
role: 'owner' | 'admin' | 'user';
banned: boolean;
tags?: string[];
oidcSub?: string;
2025-12-25 00:39:30 +08:00
enabledApis?: string[];
2025-12-24 00:24:50 +08:00
created_at: number;
playrecord_migrated?: boolean;
2025-12-30 22:00:36 +08:00
favorite_migrated?: boolean;
2026-01-01 10:01:35 +08:00
skip_migrated?: boolean;
2026-01-12 21:06:45 +08:00
last_movie_request_time?: number;
2026-01-16 16:11:31 +08:00
email?: string;
emailNotifications?: boolean;
2025-12-24 00:24:50 +08:00
} | null> {
const userInfo = await this.withRetry(() =>
this.adapter.hGetAll(this.userInfoKey(userName))
2025-12-24 00:24:50 +08:00
);
if (!userInfo || Object.keys(userInfo).length === 0) {
return null;
}
return {
role: (userInfo.role as 'owner' | 'admin' | 'user') || 'user',
banned: userInfo.banned === 'true',
tags: userInfo.tags ? JSON.parse(userInfo.tags) : undefined,
oidcSub: userInfo.oidcSub,
2025-12-25 00:39:30 +08:00
enabledApis: userInfo.enabledApis ? JSON.parse(userInfo.enabledApis) : undefined,
2025-12-24 00:24:50 +08:00
created_at: parseInt(userInfo.created_at || '0', 10),
playrecord_migrated: userInfo.playrecord_migrated === 'true',
2025-12-30 22:00:36 +08:00
favorite_migrated: userInfo.favorite_migrated === 'true',
2026-01-01 10:01:35 +08:00
skip_migrated: userInfo.skip_migrated === 'true',
2026-01-12 21:06:45 +08:00
last_movie_request_time: userInfo.last_movie_request_time ? parseInt(userInfo.last_movie_request_time, 10) : undefined,
2026-01-16 16:11:31 +08:00
email: userInfo.email,
emailNotifications: userInfo.emailNotifications === 'true',
2025-12-24 00:24:50 +08:00
};
}
// 更新用户信息(新版本)
async updateUserInfoV2(
userName: string,
updates: {
role?: 'owner' | 'admin' | 'user';
banned?: boolean;
tags?: string[];
oidcSub?: string;
2025-12-25 00:39:30 +08:00
enabledApis?: string[];
2025-12-24 00:24:50 +08:00
}
): Promise<void> {
const userInfo: Record<string, string> = {};
if (updates.role !== undefined) {
userInfo.role = updates.role;
}
if (updates.banned !== undefined) {
userInfo.banned = updates.banned ? 'true' : 'false';
}
if (updates.tags !== undefined) {
if (updates.tags.length > 0) {
userInfo.tags = JSON.stringify(updates.tags);
} else {
// 删除tags字段
await this.withRetry(() => this.adapter.hDel(this.userInfoKey(userName), 'tags'));
2025-12-24 00:24:50 +08:00
}
}
2025-12-25 00:39:30 +08:00
if (updates.enabledApis !== undefined) {
if (updates.enabledApis.length > 0) {
userInfo.enabledApis = JSON.stringify(updates.enabledApis);
} else {
// 删除enabledApis字段
await this.withRetry(() => this.adapter.hDel(this.userInfoKey(userName), 'enabledApis'));
2025-12-25 00:39:30 +08:00
}
}
2025-12-24 00:24:50 +08:00
if (updates.oidcSub !== undefined) {
const oldInfo = await this.getUserInfoV2(userName);
if (oldInfo?.oidcSub && oldInfo.oidcSub !== updates.oidcSub) {
// 删除旧的OIDC映射
await this.withRetry(() => this.adapter.del(this.oidcSubKey(oldInfo.oidcSub!)));
2025-12-24 00:24:50 +08:00
}
userInfo.oidcSub = updates.oidcSub;
// 创建新的OIDC映射
await this.withRetry(() => this.adapter.set(this.oidcSubKey(updates.oidcSub!), userName));
2025-12-24 00:24:50 +08:00
}
if (Object.keys(userInfo).length > 0) {
await this.withRetry(() => this.adapter.hSet(this.userInfoKey(userName), userInfo));
2025-12-24 00:24:50 +08:00
}
}
// 修改用户密码(新版本)
async changePasswordV2(userName: string, newPassword: string): Promise<void> {
const hashedPassword = await this.hashPassword(newPassword);
await this.withRetry(() =>
this.adapter.hSet(this.userInfoKey(userName), 'password', hashedPassword)
2025-12-24 00:24:50 +08:00
);
}
// 检查用户是否存在(新版本)
async checkUserExistV2(userName: string): Promise<boolean> {
const exists = await this.withRetry(() =>
this.adapter.exists(this.userInfoKey(userName))
2025-12-24 00:24:50 +08:00
);
return exists === 1;
}
// 通过OIDC Sub查找用户名
async getUserByOidcSub(oidcSub: string): Promise<string | null> {
const userName = await this.withRetry(() =>
this.adapter.get(this.oidcSubKey(oidcSub))
2025-12-24 00:24:50 +08:00
);
return userName ? ensureString(userName) : null;
}
// 获取用户列表(分页,新版本)
async getUserListV2(
2026-01-23 16:41:49 +08:00
offset = 0,
limit = 20,
2025-12-24 00:24:50 +08:00
ownerUsername?: string
): Promise<{
users: Array<{
username: string;
role: 'owner' | 'admin' | 'user';
banned: boolean;
tags?: string[];
2025-12-25 00:39:30 +08:00
oidcSub?: string;
enabledApis?: string[];
2025-12-24 00:24:50 +08:00
created_at: number;
}>;
total: number;
}> {
// 获取总数
let total = await this.withRetry(() => this.adapter.zCard(this.userListKey()));
2025-12-24 20:49:00 +08:00
// 检查站长是否在数据库中(使用缓存)
let ownerInfo = null;
let ownerInDatabase = false;
if (ownerUsername) {
// 先检查缓存
const { ownerExistenceCache } = await import('./user-cache');
const cachedExists = ownerExistenceCache.get(ownerUsername);
if (cachedExists !== null) {
// 使用缓存的结果
ownerInDatabase = cachedExists;
if (ownerInDatabase) {
// 如果站长在数据库中,获取详细信息
ownerInfo = await this.getUserInfoV2(ownerUsername);
}
} else {
// 缓存未命中,查询数据库
ownerInfo = await this.getUserInfoV2(ownerUsername);
ownerInDatabase = !!ownerInfo;
// 更新缓存
ownerExistenceCache.set(ownerUsername, ownerInDatabase);
}
// 如果站长不在数据库中,总数+1无论在哪一页都要加
if (!ownerInDatabase) {
total += 1;
}
}
// 如果站长不在数据库中且在第一页,需要调整获取的用户数量和偏移量
let actualOffset = offset;
let actualLimit = limit;
if (ownerUsername && !ownerInDatabase) {
if (offset === 0) {
// 第一页:只获取 limit-1 个用户,为站长留出位置
actualLimit = limit - 1;
} else {
// 其他页偏移量需要减1因为站长占据了第一页的一个位置
actualOffset = offset - 1;
}
}
2025-12-24 00:24:50 +08:00
// 获取用户列表(按注册时间升序)
const usernames = await this.withRetry(() =>
this.adapter.zRange(this.userListKey(), actualOffset, actualOffset + actualLimit - 1)
2025-12-24 00:24:50 +08:00
);
const users = [];
2025-12-24 20:49:00 +08:00
// 如果有站长且在第一页,确保站长始终在第一位
2025-12-24 00:24:50 +08:00
if (ownerUsername && offset === 0) {
2025-12-24 20:49:00 +08:00
// 即使站长不在数据库中,也要添加站长(站长使用环境变量认证)
users.push({
username: ownerUsername,
role: 'owner' as const,
banned: ownerInfo?.banned || false,
tags: ownerInfo?.tags,
2026-01-01 22:21:46 +08:00
oidcSub: ownerInfo?.oidcSub,
enabledApis: ownerInfo?.enabledApis,
2025-12-24 20:49:00 +08:00
created_at: ownerInfo?.created_at || 0,
});
2025-12-24 00:24:50 +08:00
}
// 获取其他用户信息
for (const username of usernames) {
const usernameStr = ensureString(username);
// 跳过站长(已经添加)
if (ownerUsername && usernameStr === ownerUsername) {
continue;
}
const userInfo = await this.getUserInfoV2(usernameStr);
if (userInfo) {
users.push({
username: usernameStr,
role: userInfo.role,
banned: userInfo.banned,
tags: userInfo.tags,
2026-01-01 22:21:46 +08:00
oidcSub: userInfo.oidcSub,
enabledApis: userInfo.enabledApis,
2025-12-24 00:24:50 +08:00
created_at: userInfo.created_at,
});
}
}
return { users, total };
}
// 删除用户(新版本)
async deleteUserV2(userName: string): Promise<void> {
// 获取用户信息
const userInfo = await this.getUserInfoV2(userName);
// 删除OIDC映射
if (userInfo?.oidcSub) {
await this.withRetry(() => this.adapter.del(this.oidcSubKey(userInfo.oidcSub!)));
2025-12-24 00:24:50 +08:00
}
// 删除用户信息Hash
await this.withRetry(() => this.adapter.del(this.userInfoKey(userName)));
2025-12-24 00:24:50 +08:00
2025-12-24 20:49:00 +08:00
// 从用<E4BB8E><E794A8><EFBFBD>列表中移除
await this.withRetry(() => this.adapter.zRem(this.userListKey(), userName));
2025-12-24 00:24:50 +08:00
// 删除用户的其他数据(播放记录、收藏等)
await this.deleteUser(userName);
}
2025-08-15 19:26:13 +08:00
// ---------- 搜索历史 ----------
private shKey(user: string) {
return `u:${user}:sh`; // u:username:sh
}
async getSearchHistory(userName: string): Promise<string[]> {
const result = await this.withRetry(() =>
this.adapter.lRange(this.shKey(userName), 0, -1)
2025-08-15 19:26:13 +08:00
);
// 确保返回的都是字符串类型
return ensureStringArray(result as any[]);
}
async addSearchHistory(userName: string, keyword: string): Promise<void> {
const key = this.shKey(userName);
// 先去重
await this.withRetry(() => this.adapter.lRem(key, 0, ensureString(keyword)));
2025-08-15 19:26:13 +08:00
// 插入到最前
await this.withRetry(() => this.adapter.lPush(key, ensureString(keyword)));
2025-08-15 19:26:13 +08:00
// 限制最大长度
await this.withRetry(() => this.adapter.lTrim(key, 0, SEARCH_HISTORY_LIMIT - 1));
2025-08-15 19:26:13 +08:00
}
async deleteSearchHistory(userName: string, keyword?: string): Promise<void> {
const key = this.shKey(userName);
if (keyword) {
await this.withRetry(() => this.adapter.lRem(key, 0, ensureString(keyword)));
2025-08-15 19:26:13 +08:00
} else {
await this.withRetry(() => this.adapter.del(key));
2025-08-15 19:26:13 +08:00
}
}
// ---------- 获取全部用户 ----------
async getAllUsers(): Promise<string[]> {
// 从新版用户列表获取
const userListKey = this.userListKey();
const users = await this.withRetry(() =>
this.adapter.zRange(userListKey, 0, -1)
);
2025-12-25 00:00:22 +08:00
const userList = users.map(u => ensureString(u));
// 确保站长在列表中(站长可能不在数据库中,使用环境变量认证)
const ownerUsername = process.env.USERNAME;
if (ownerUsername && !userList.includes(ownerUsername)) {
userList.unshift(ownerUsername);
}
return userList;
2025-08-15 19:26:13 +08:00
}
// ---------- 管理员配置 ----------
private adminConfigKey() {
return 'admin:config';
}
async getAdminConfig(): Promise<AdminConfig | null> {
const val = await this.withRetry(() => this.adapter.get(this.adminConfigKey()));
2025-08-15 19:26:13 +08:00
return val ? (JSON.parse(val) as AdminConfig) : null;
}
async setAdminConfig(config: AdminConfig): Promise<void> {
await this.withRetry(() =>
this.adapter.set(this.adminConfigKey(), JSON.stringify(config))
2025-08-15 19:26:13 +08:00
);
}
// ---------- 跳过片头片尾配置 ----------
2026-01-01 10:01:35 +08:00
private skipHashKey(user: string) {
return `u:${user}:skip`; // u:username:skip (hash结构)
2025-08-15 19:26:13 +08:00
}
2025-12-03 21:34:05 +08:00
private danmakuFilterConfigKey(user: string) {
return `u:${user}:danmaku_filter`;
}
2025-08-15 19:26:13 +08:00
async getSkipConfig(
userName: string,
source: string,
id: string
): Promise<SkipConfig | null> {
2026-01-01 10:01:35 +08:00
const key = `${source}+${id}`;
2025-08-15 19:26:13 +08:00
const val = await this.withRetry(() =>
this.adapter.hGet(this.skipHashKey(userName), key)
2025-08-15 19:26:13 +08:00
);
return val ? (JSON.parse(val) as SkipConfig) : null;
}
async setSkipConfig(
userName: string,
source: string,
id: string,
config: SkipConfig
): Promise<void> {
2026-01-01 10:01:35 +08:00
const key = `${source}+${id}`;
2025-08-15 19:26:13 +08:00
await this.withRetry(() =>
this.adapter.hSet(this.skipHashKey(userName), key, JSON.stringify(config))
2025-08-15 19:26:13 +08:00
);
}
async deleteSkipConfig(
userName: string,
source: string,
id: string
): Promise<void> {
2026-01-01 10:01:35 +08:00
const key = `${source}+${id}`;
2025-08-15 19:26:13 +08:00
await this.withRetry(() =>
this.adapter.hDel(this.skipHashKey(userName), key)
2025-08-15 19:26:13 +08:00
);
}
async getAllSkipConfigs(
userName: string
): Promise<{ [key: string]: SkipConfig }> {
2026-01-01 10:01:35 +08:00
const hashData = await this.withRetry(() =>
this.adapter.hGetAll(this.skipHashKey(userName))
2026-01-01 10:01:35 +08:00
);
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 migrationPromise = this.doSkipConfigMigration(userName);
playRecordLocks.set(`${userName}:skip`, migrationPromise);
try {
await migrationPromise;
} finally {
playRecordLocks.delete(`${userName}:skip`);
}
}
private async doSkipConfigMigration(userName: string): Promise<void> {
console.log(`开始迁移用户 ${userName} 的跳过配置...`);
2025-08-15 19:26:13 +08:00
2026-01-01 10:01:35 +08:00
const userInfo = await this.getUserInfoV2(userName);
if (userInfo?.skip_migrated) {
console.log(`用户 ${userName} 的跳过配置已经迁移过,跳过`);
return;
2025-08-15 19:26:13 +08:00
}
2026-01-01 10:01:35 +08:00
const pattern = `u:${userName}:skip:*`;
const oldKeys: string[] = await this.withRetry(() => this.adapter.keys(pattern));
2025-08-15 19:26:13 +08:00
2026-01-01 10:01:35 +08:00
if (oldKeys.length === 0) {
console.log(`用户 ${userName} 没有旧的跳过配置,标记为已迁移`);
await this.withRetry(() =>
this.adapter.hSet(this.userInfoKey(userName), 'skip_migrated', 'true')
2026-01-01 10:01:35 +08:00
);
const { userInfoCache } = await import('./user-cache');
userInfoCache?.delete(userName);
return;
}
const values = await this.withRetry(() => this.adapter.mGet(oldKeys));
2025-08-15 19:26:13 +08:00
2026-01-01 10:01:35 +08:00
const hashData: Record<string, string> = {};
oldKeys.forEach((key, index) => {
2025-08-15 19:26:13 +08:00
const value = values[index];
if (value) {
const match = key.match(/^u:.+?:skip:(.+)$/);
if (match) {
const sourceAndId = match[1];
2026-01-01 10:01:35 +08:00
hashData[sourceAndId] = value as string;
2025-08-15 19:26:13 +08:00
}
}
});
2026-01-01 10:01:35 +08:00
if (Object.keys(hashData).length > 0) {
await this.withRetry(() =>
this.adapter.hSet(this.skipHashKey(userName), hashData)
2026-01-01 10:01:35 +08:00
);
console.log(`成功迁移 ${Object.keys(hashData).length} 条跳过配置到hash结构`);
}
await this.withRetry(() => this.adapter.del(oldKeys));
2026-01-01 10:01:35 +08:00
console.log(`删除了 ${oldKeys.length} 个旧的跳过配置key`);
await this.withRetry(() =>
this.adapter.hSet(this.userInfoKey(userName), 'skip_migrated', 'true')
2026-01-01 10:01:35 +08:00
);
const { userInfoCache } = await import('./user-cache');
userInfoCache?.delete(userName);
console.log(`用户 ${userName} 的跳过配置迁移完成`);
2025-08-15 19:26:13 +08:00
}
2025-12-03 21:34:05 +08:00
// ---------- 弹幕过滤配置 ----------
async getDanmakuFilterConfig(
userName: string
): Promise<import('./types').DanmakuFilterConfig | null> {
const val = await this.withRetry(() =>
this.adapter.get(this.danmakuFilterConfigKey(userName))
2025-12-03 21:34:05 +08:00
);
return val ? (JSON.parse(val) as import('./types').DanmakuFilterConfig) : null;
}
async setDanmakuFilterConfig(
userName: string,
config: import('./types').DanmakuFilterConfig
): Promise<void> {
await this.withRetry(() =>
this.adapter.set(
2025-12-03 21:34:05 +08:00
this.danmakuFilterConfigKey(userName),
JSON.stringify(config)
)
);
}
async deleteDanmakuFilterConfig(userName: string): Promise<void> {
await this.withRetry(() =>
this.adapter.del(this.danmakuFilterConfigKey(userName))
2025-12-03 21:34:05 +08:00
);
}
2025-08-15 19:26:13 +08:00
// 清空所有数据
async clearAllData(): Promise<void> {
try {
// 获取所有用户
const allUsers = await this.getAllUsers();
// 删除所有用户及其数据
for (const username of allUsers) {
await this.deleteUser(username);
}
// 删除管理员配置
await this.withRetry(() => this.adapter.del(this.adminConfigKey()));
2025-08-15 19:26:13 +08:00
console.log('所有数据已清空');
} catch (error) {
console.error('清空数据失败:', error);
throw new Error('清空数据失败');
}
}
2025-12-18 21:36:31 +08:00
// ---------- 通用键值存储 ----------
private globalValueKey(key: string) {
return `global:${key}`;
}
async getGlobalValue(key: string): Promise<string | null> {
const val = await this.withRetry(() =>
this.adapter.get(this.globalValueKey(key))
2025-12-18 21:36:31 +08:00
);
return val ? ensureString(val) : null;
}
async setGlobalValue(key: string, value: string): Promise<void> {
await this.withRetry(() =>
this.adapter.set(this.globalValueKey(key), ensureString(value))
2025-12-18 21:36:31 +08:00
);
}
async deleteGlobalValue(key: string): Promise<void> {
await this.withRetry(() => this.adapter.del(this.globalValueKey(key)));
2025-12-18 21:36:31 +08:00
}
2025-12-18 23:31:10 +08:00
// ---------- 通知相关 ----------
private notificationsKey(userName: string) {
return `u:${userName}:notifications`;
}
private lastFavoriteCheckKey(userName: string) {
return `u:${userName}:last_fav_check`;
}
async getNotifications(userName: string): Promise<import('./types').Notification[]> {
const val = await this.withRetry(() =>
this.adapter.get(this.notificationsKey(userName))
2025-12-18 23:31:10 +08:00
);
return val ? (JSON.parse(val) as import('./types').Notification[]) : [];
}
async addNotification(
userName: string,
notification: import('./types').Notification
): Promise<void> {
const notifications = await this.getNotifications(userName);
notifications.unshift(notification); // 新通知放在最前面
// 限制通知数量最多保留100条
if (notifications.length > 100) {
notifications.splice(100);
}
await this.withRetry(() =>
this.adapter.set(this.notificationsKey(userName), JSON.stringify(notifications))
2025-12-18 23:31:10 +08:00
);
}
async markNotificationAsRead(
userName: string,
notificationId: string
): Promise<void> {
const notifications = await this.getNotifications(userName);
const notification = notifications.find((n) => n.id === notificationId);
if (notification) {
notification.read = true;
await this.withRetry(() =>
this.adapter.set(this.notificationsKey(userName), JSON.stringify(notifications))
2025-12-18 23:31:10 +08:00
);
}
}
async deleteNotification(
userName: string,
notificationId: string
): Promise<void> {
const notifications = await this.getNotifications(userName);
const filtered = notifications.filter((n) => n.id !== notificationId);
await this.withRetry(() =>
this.adapter.set(this.notificationsKey(userName), JSON.stringify(filtered))
2025-12-18 23:31:10 +08:00
);
}
async clearAllNotifications(userName: string): Promise<void> {
await this.withRetry(() => this.adapter.del(this.notificationsKey(userName)));
2025-12-18 23:31:10 +08:00
}
async getUnreadNotificationCount(userName: string): Promise<number> {
const notifications = await this.getNotifications(userName);
return notifications.filter((n) => !n.read).length;
}
async getLastFavoriteCheckTime(userName: string): Promise<number> {
const val = await this.withRetry(() =>
this.adapter.get(this.lastFavoriteCheckKey(userName))
2025-12-18 23:31:10 +08:00
);
return val ? parseInt(val, 10) : 0;
}
async setLastFavoriteCheckTime(
userName: string,
timestamp: number
): Promise<void> {
await this.withRetry(() =>
this.adapter.set(this.lastFavoriteCheckKey(userName), timestamp.toString())
2025-12-18 23:31:10 +08:00
);
}
2026-01-12 20:16:21 +08:00
2026-01-30 17:47:30 +08:00
async updateLastMovieRequestTime(userName: string, timestamp: number): Promise<void> {
await this.withRetry(() =>
this.adapter.hSet(
this.userInfoKey(userName),
'last_movie_request_time',
timestamp.toString()
)
);
}
2026-01-12 20:16:21 +08:00
// ---------- 求片相关 ----------
private movieRequestsKey() {
return 'movie_requests:all';
}
private userMovieRequestsKey(userName: string) {
return `u:${userName}:mr`;
}
async getAllMovieRequests(): Promise<import('./types').MovieRequest[]> {
const data = await this.withRetry(() => this.adapter.hGetAll(this.movieRequestsKey()));
2026-01-12 20:16:21 +08:00
if (!data || Object.keys(data).length === 0) return [];
return Object.values(data).map(v => JSON.parse(v) as import('./types').MovieRequest);
}
async getMovieRequest(requestId: string): Promise<import('./types').MovieRequest | null> {
const val = await this.withRetry(() => this.adapter.hGet(this.movieRequestsKey(), requestId));
2026-01-12 20:16:21 +08:00
return val ? (JSON.parse(val) as import('./types').MovieRequest) : null;
}
async createMovieRequest(request: import('./types').MovieRequest): Promise<void> {
await this.withRetry(() => this.adapter.hSet(this.movieRequestsKey(), request.id, JSON.stringify(request)));
2026-01-12 20:16:21 +08:00
}
async updateMovieRequest(requestId: string, updates: Partial<import('./types').MovieRequest>): Promise<void> {
const existing = await this.getMovieRequest(requestId);
if (!existing) throw new Error('Movie request not found');
const updated = { ...existing, ...updates };
await this.withRetry(() => this.adapter.hSet(this.movieRequestsKey(), requestId, JSON.stringify(updated)));
2026-01-12 20:16:21 +08:00
}
async deleteMovieRequest(requestId: string): Promise<void> {
await this.withRetry(() => this.adapter.hDel(this.movieRequestsKey(), requestId));
2026-01-12 20:16:21 +08:00
}
async getUserMovieRequests(userName: string): Promise<string[]> {
const val = await this.withRetry(() => this.adapter.sMembers(this.userMovieRequestsKey(userName)));
2026-01-12 20:16:21 +08:00
return val ? ensureStringArray(val) : [];
}
async addUserMovieRequest(userName: string, requestId: string): Promise<void> {
await this.withRetry(() => this.adapter.sAdd(this.userMovieRequestsKey(userName), requestId));
2026-01-12 20:16:21 +08:00
}
async removeUserMovieRequest(userName: string, requestId: string): Promise<void> {
await this.withRetry(() => this.adapter.sRem(this.userMovieRequestsKey(userName), requestId));
2026-01-12 20:16:21 +08:00
}
2026-01-16 16:11:31 +08:00
// ---------- 用户邮箱相关 ----------
async getUserEmail(userName: string): Promise<string | null> {
const userInfo = await this.getUserInfoV2(userName);
return userInfo?.email || null;
}
async setUserEmail(userName: string, email: string): Promise<void> {
await this.withRetry(() =>
this.adapter.hSet(this.userInfoKey(userName), 'email', email)
2026-01-16 16:11:31 +08:00
);
// 清除缓存
userInfoCache?.delete(userName);
}
async getEmailNotificationPreference(userName: string): Promise<boolean> {
const userInfo = await this.getUserInfoV2(userName);
return userInfo?.emailNotifications || false;
}
async setEmailNotificationPreference(userName: string, enabled: boolean): Promise<void> {
await this.withRetry(() =>
this.adapter.hSet(this.userInfoKey(userName), 'emailNotifications', enabled.toString())
2026-01-16 16:11:31 +08:00
);
// 清除缓存
userInfoCache?.delete(userName);
}
2025-08-15 19:26:13 +08:00
}