mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-21 00:44:41 +08:00
feat: fix announcement, refactor db code
This commit is contained in:
@@ -152,7 +152,7 @@ async function initConfig() {
|
||||
SiteConfig: {
|
||||
SiteName: process.env.SITE_NAME || 'MoonTV',
|
||||
Announcement:
|
||||
process.env.NEXT_PUBLIC_ANNOUNCEMENT ||
|
||||
process.env.ANNOUNCEMENT ||
|
||||
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。',
|
||||
SearchDownstreamMaxPage:
|
||||
Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5,
|
||||
@@ -192,7 +192,7 @@ async function initConfig() {
|
||||
SiteConfig: {
|
||||
SiteName: process.env.SITE_NAME || 'MoonTV',
|
||||
Announcement:
|
||||
process.env.NEXT_PUBLIC_ANNOUNCEMENT ||
|
||||
process.env.ANNOUNCEMENT ||
|
||||
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。',
|
||||
SearchDownstreamMaxPage:
|
||||
Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5,
|
||||
@@ -252,7 +252,7 @@ export async function resetConfig() {
|
||||
SiteConfig: {
|
||||
SiteName: process.env.SITE_NAME || 'MoonTV',
|
||||
Announcement:
|
||||
process.env.NEXT_PUBLIC_ANNOUNCEMENT ||
|
||||
process.env.ANNOUNCEMENT ||
|
||||
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。',
|
||||
SearchDownstreamMaxPage:
|
||||
Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5,
|
||||
|
||||
284
src/lib/db.ts
284
src/lib/db.ts
@@ -1,6 +1,10 @@
|
||||
/* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import { AdminConfig } from './admin.types';
|
||||
import { RedisStorage } from './redis.db';
|
||||
import { AdminConfig, Favorite, IStorage, PlayRecord } from './types';
|
||||
|
||||
// 重新导出类型,保持向后兼容
|
||||
export type { AdminConfig, Favorite, IStorage, PlayRecord };
|
||||
|
||||
// storage type 常量: 'localstorage' | 'database',默认 'localstorage'
|
||||
const STORAGE_TYPE =
|
||||
@@ -9,240 +13,6 @@ const STORAGE_TYPE =
|
||||
| 'redis'
|
||||
| undefined) || 'localstorage';
|
||||
|
||||
// 播放记录数据结构
|
||||
export interface PlayRecord {
|
||||
title: string;
|
||||
source_name: string;
|
||||
cover: string;
|
||||
index: number; // 第几集
|
||||
total_episodes: number; // 总集数
|
||||
play_time: number; // 播放进度(秒)
|
||||
total_time: number; // 总进度(秒)
|
||||
save_time: number; // 记录保存时间(时间戳)
|
||||
user_id: number; // 用户ID,localStorage情况下全部为0
|
||||
}
|
||||
|
||||
// 收藏数据结构
|
||||
export interface Favorite {
|
||||
source_name: string;
|
||||
total_episodes: number; // 总集数
|
||||
title: string;
|
||||
cover: string;
|
||||
user_id: number; // 用户ID,localStorage情况下全部为0
|
||||
save_time: number; // 记录保存时间(时间戳)
|
||||
}
|
||||
|
||||
// 搜索历史最大条数
|
||||
const SEARCH_HISTORY_LIMIT = 20;
|
||||
|
||||
// 存储接口
|
||||
export interface IStorage {
|
||||
// 播放记录相关
|
||||
getPlayRecord(userName: string, key: string): Promise<PlayRecord | null>;
|
||||
setPlayRecord(
|
||||
userName: string,
|
||||
key: string,
|
||||
record: PlayRecord
|
||||
): Promise<void>;
|
||||
getAllPlayRecords(userName: string): Promise<{ [key: string]: PlayRecord }>;
|
||||
deletePlayRecord(userName: string, key: string): Promise<void>;
|
||||
|
||||
// 收藏相关
|
||||
getFavorite(userName: string, key: string): Promise<Favorite | null>;
|
||||
setFavorite(userName: string, key: string, favorite: Favorite): Promise<void>;
|
||||
getAllFavorites(userName: string): Promise<{ [key: string]: Favorite }>;
|
||||
deleteFavorite(userName: string, key: string): Promise<void>;
|
||||
|
||||
// 用户相关
|
||||
registerUser(userName: string, password: string): Promise<void>;
|
||||
verifyUser(userName: string, password: string): Promise<boolean>;
|
||||
// 检查用户是否存在(无需密码)
|
||||
checkUserExist(userName: string): Promise<boolean>;
|
||||
|
||||
// 搜索历史相关
|
||||
getSearchHistory(userName: string): Promise<string[]>;
|
||||
addSearchHistory(userName: string, keyword: string): Promise<void>;
|
||||
deleteSearchHistory(userName: string, keyword?: string): Promise<void>;
|
||||
|
||||
// 用户列表
|
||||
getAllUsers(): Promise<string[]>;
|
||||
|
||||
// 管理员配置相关
|
||||
getAdminConfig(): Promise<AdminConfig | null>;
|
||||
setAdminConfig(config: AdminConfig): Promise<void>;
|
||||
}
|
||||
|
||||
// ---------------- Redis 实现 ----------------
|
||||
import { createClient, RedisClientType } from 'redis';
|
||||
|
||||
class RedisStorage implements IStorage {
|
||||
private client: RedisClientType;
|
||||
|
||||
constructor() {
|
||||
this.client = getRedisClient();
|
||||
}
|
||||
|
||||
// ---------- 播放记录 ----------
|
||||
private prKey(user: string, key: string) {
|
||||
return `u:${user}:pr:${key}`; // u:username:pr:source+id
|
||||
}
|
||||
|
||||
async getPlayRecord(
|
||||
userName: string,
|
||||
key: string
|
||||
): Promise<PlayRecord | null> {
|
||||
const val = await this.client.get(this.prKey(userName, key));
|
||||
return val ? (JSON.parse(val) as PlayRecord) : null;
|
||||
}
|
||||
|
||||
async setPlayRecord(
|
||||
userName: string,
|
||||
key: string,
|
||||
record: PlayRecord
|
||||
): Promise<void> {
|
||||
await this.client.set(this.prKey(userName, key), JSON.stringify(record));
|
||||
}
|
||||
|
||||
async getAllPlayRecords(
|
||||
userName: string
|
||||
): Promise<Record<string, PlayRecord>> {
|
||||
const pattern = `u:${userName}:pr:*`;
|
||||
const keys: string[] = await this.client.keys(pattern);
|
||||
if (keys.length === 0) return {};
|
||||
const values = await this.client.mGet(keys);
|
||||
const result: Record<string, PlayRecord> = {};
|
||||
keys.forEach((fullKey: string, idx: number) => {
|
||||
const raw = values[idx];
|
||||
if (raw) {
|
||||
const rec = JSON.parse(raw) as PlayRecord;
|
||||
// 截取 source+id 部分
|
||||
const keyPart = fullKey.replace(`u:${userName}:pr:`, '');
|
||||
result[keyPart] = rec;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
async deletePlayRecord(userName: string, key: string): Promise<void> {
|
||||
await this.client.del(this.prKey(userName, key));
|
||||
}
|
||||
|
||||
// ---------- 收藏 ----------
|
||||
private favKey(user: string, key: string) {
|
||||
return `u:${user}:fav:${key}`;
|
||||
}
|
||||
|
||||
async getFavorite(userName: string, key: string): Promise<Favorite | null> {
|
||||
const val = await this.client.get(this.favKey(userName, key));
|
||||
return val ? (JSON.parse(val) as Favorite) : null;
|
||||
}
|
||||
|
||||
async setFavorite(
|
||||
userName: string,
|
||||
key: string,
|
||||
favorite: Favorite
|
||||
): Promise<void> {
|
||||
await this.client.set(this.favKey(userName, key), JSON.stringify(favorite));
|
||||
}
|
||||
|
||||
async getAllFavorites(userName: string): Promise<Record<string, Favorite>> {
|
||||
const pattern = `u:${userName}:fav:*`;
|
||||
const keys: string[] = await this.client.keys(pattern);
|
||||
if (keys.length === 0) return {};
|
||||
const values = await this.client.mGet(keys);
|
||||
const result: Record<string, Favorite> = {};
|
||||
keys.forEach((fullKey: string, idx: number) => {
|
||||
const raw = values[idx];
|
||||
if (raw) {
|
||||
const fav = JSON.parse(raw) as Favorite;
|
||||
const keyPart = fullKey.replace(`u:${userName}:fav:`, '');
|
||||
result[keyPart] = fav;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
async deleteFavorite(userName: string, key: string): Promise<void> {
|
||||
await this.client.del(this.favKey(userName, key));
|
||||
}
|
||||
|
||||
// ---------- 用户注册 / 登录 ----------
|
||||
private userPwdKey(user: string) {
|
||||
return `u:${user}:pwd`;
|
||||
}
|
||||
|
||||
async registerUser(userName: string, password: string): Promise<void> {
|
||||
// 简单存储明文密码,生产环境应加密
|
||||
await this.client.set(this.userPwdKey(userName), password);
|
||||
}
|
||||
|
||||
async verifyUser(userName: string, password: string): Promise<boolean> {
|
||||
const stored = await this.client.get(this.userPwdKey(userName));
|
||||
if (stored === null) return false;
|
||||
return stored === password;
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
async checkUserExist(userName: string): Promise<boolean> {
|
||||
// 使用 EXISTS 判断 key 是否存在
|
||||
const exists = await this.client.exists(this.userPwdKey(userName));
|
||||
return exists === 1;
|
||||
}
|
||||
|
||||
// ---------- 搜索历史 ----------
|
||||
private shKey(user: string) {
|
||||
return `u:${user}:sh`; // u:username:sh
|
||||
}
|
||||
|
||||
async getSearchHistory(userName: string): Promise<string[]> {
|
||||
return (await this.client.lRange(this.shKey(userName), 0, -1)) as string[];
|
||||
}
|
||||
|
||||
async addSearchHistory(userName: string, keyword: string): Promise<void> {
|
||||
const key = this.shKey(userName);
|
||||
// 先去重
|
||||
await this.client.lRem(key, 0, keyword);
|
||||
// 插入到最前
|
||||
await this.client.lPush(key, keyword);
|
||||
// 限制最大长度
|
||||
await this.client.lTrim(key, 0, SEARCH_HISTORY_LIMIT - 1);
|
||||
}
|
||||
|
||||
async deleteSearchHistory(userName: string, keyword?: string): Promise<void> {
|
||||
const key = this.shKey(userName);
|
||||
if (keyword) {
|
||||
await this.client.lRem(key, 0, keyword);
|
||||
} else {
|
||||
await this.client.del(key);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 获取全部用户 ----------
|
||||
async getAllUsers(): Promise<string[]> {
|
||||
const keys = await this.client.keys('u:*:pwd');
|
||||
return keys
|
||||
.map((k) => {
|
||||
const match = k.match(/^u:(.+?):pwd$/);
|
||||
return match ? match[1] : undefined;
|
||||
})
|
||||
.filter((u): u is string => typeof u === 'string');
|
||||
}
|
||||
|
||||
// ---------- 管理员配置 ----------
|
||||
private adminConfigKey() {
|
||||
return 'admin:config';
|
||||
}
|
||||
|
||||
async getAdminConfig(): Promise<AdminConfig | null> {
|
||||
const val = await this.client.get(this.adminConfigKey());
|
||||
return val ? (JSON.parse(val) as AdminConfig) : null;
|
||||
}
|
||||
|
||||
async setAdminConfig(config: AdminConfig): Promise<void> {
|
||||
await this.client.set(this.adminConfigKey(), JSON.stringify(config));
|
||||
}
|
||||
}
|
||||
|
||||
// 创建存储实例
|
||||
function createStorage(): IStorage {
|
||||
switch (STORAGE_TYPE) {
|
||||
@@ -432,47 +202,3 @@ export class DbManager {
|
||||
|
||||
// 导出默认实例
|
||||
export const db = new DbManager();
|
||||
|
||||
// 单例 Redis 客户端
|
||||
function getRedisClient(): RedisClientType {
|
||||
const globalKey = Symbol.for('__MOONTV_REDIS_CLIENT__');
|
||||
let client: RedisClientType | undefined = (global as any)[globalKey];
|
||||
|
||||
if (!client) {
|
||||
const url = process.env.REDIS_URL;
|
||||
if (!url) {
|
||||
throw new Error('REDIS_URL env variable not set');
|
||||
}
|
||||
|
||||
// 创建客户端,配置重连策略
|
||||
client = createClient({
|
||||
url,
|
||||
socket: {
|
||||
// 5秒重连间隔
|
||||
reconnectStrategy: (retries: number) => {
|
||||
console.log(`Redis reconnection attempt ${retries + 1}`);
|
||||
return 5000; // 5秒后重试
|
||||
},
|
||||
connectTimeout: 10000, // 10秒连接超时
|
||||
},
|
||||
});
|
||||
|
||||
// 初始连接,带重试机制
|
||||
const connectWithRetry = async () => {
|
||||
try {
|
||||
await client!.connect();
|
||||
console.log('Redis connected successfully');
|
||||
} catch (err) {
|
||||
console.error('Redis initial connection failed:', err);
|
||||
console.log('Will retry in 5 seconds...');
|
||||
setTimeout(connectWithRetry, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
connectWithRetry();
|
||||
|
||||
(global as any)[globalKey] = client;
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
307
src/lib/redis.db.ts
Normal file
307
src/lib/redis.db.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
/* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import { createClient, RedisClientType } from 'redis';
|
||||
|
||||
import { AdminConfig, Favorite, IStorage, PlayRecord } from './types';
|
||||
|
||||
// 搜索历史最大条数
|
||||
const SEARCH_HISTORY_LIMIT = 20;
|
||||
|
||||
// 添加Redis操作重试包装器
|
||||
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(
|
||||
`Redis operation failed, retrying... (${i + 1}/${maxRetries})`
|
||||
);
|
||||
console.error('Error:', err.message);
|
||||
|
||||
// 等待一段时间后重试
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
|
||||
|
||||
// 尝试重新连接
|
||||
try {
|
||||
const client = getRedisClient();
|
||||
if (!client.isOpen) {
|
||||
await client.connect();
|
||||
}
|
||||
} catch (reconnectErr) {
|
||||
console.error('Failed to reconnect:', reconnectErr);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Max retries exceeded');
|
||||
}
|
||||
|
||||
export class RedisStorage implements IStorage {
|
||||
private client: RedisClientType;
|
||||
|
||||
constructor() {
|
||||
this.client = getRedisClient();
|
||||
}
|
||||
|
||||
// ---------- 播放记录 ----------
|
||||
private prKey(user: string, key: string) {
|
||||
return `u:${user}:pr:${key}`; // u:username:pr:source+id
|
||||
}
|
||||
|
||||
async getPlayRecord(
|
||||
userName: string,
|
||||
key: string
|
||||
): Promise<PlayRecord | null> {
|
||||
const val = await withRetry(() =>
|
||||
this.client.get(this.prKey(userName, key))
|
||||
);
|
||||
return val ? (JSON.parse(val) as PlayRecord) : null;
|
||||
}
|
||||
|
||||
async setPlayRecord(
|
||||
userName: string,
|
||||
key: string,
|
||||
record: PlayRecord
|
||||
): Promise<void> {
|
||||
await withRetry(() =>
|
||||
this.client.set(this.prKey(userName, key), JSON.stringify(record))
|
||||
);
|
||||
}
|
||||
|
||||
async getAllPlayRecords(
|
||||
userName: string
|
||||
): Promise<Record<string, PlayRecord>> {
|
||||
const pattern = `u:${userName}:pr:*`;
|
||||
const keys: string[] = await withRetry(() => this.client.keys(pattern));
|
||||
if (keys.length === 0) return {};
|
||||
const values = await withRetry(() => this.client.mGet(keys));
|
||||
const result: Record<string, PlayRecord> = {};
|
||||
keys.forEach((fullKey: string, idx: number) => {
|
||||
const raw = values[idx];
|
||||
if (raw) {
|
||||
const rec = JSON.parse(raw) as PlayRecord;
|
||||
// 截取 source+id 部分
|
||||
const keyPart = fullKey.replace(`u:${userName}:pr:`, '');
|
||||
result[keyPart] = rec;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
async deletePlayRecord(userName: string, key: string): Promise<void> {
|
||||
await withRetry(() => this.client.del(this.prKey(userName, key)));
|
||||
}
|
||||
|
||||
// ---------- 收藏 ----------
|
||||
private favKey(user: string, key: string) {
|
||||
return `u:${user}:fav:${key}`;
|
||||
}
|
||||
|
||||
async getFavorite(userName: string, key: string): Promise<Favorite | null> {
|
||||
const val = await withRetry(() =>
|
||||
this.client.get(this.favKey(userName, key))
|
||||
);
|
||||
return val ? (JSON.parse(val) as Favorite) : null;
|
||||
}
|
||||
|
||||
async setFavorite(
|
||||
userName: string,
|
||||
key: string,
|
||||
favorite: Favorite
|
||||
): Promise<void> {
|
||||
await withRetry(() =>
|
||||
this.client.set(this.favKey(userName, key), JSON.stringify(favorite))
|
||||
);
|
||||
}
|
||||
|
||||
async getAllFavorites(userName: string): Promise<Record<string, Favorite>> {
|
||||
const pattern = `u:${userName}:fav:*`;
|
||||
const keys: string[] = await withRetry(() => this.client.keys(pattern));
|
||||
if (keys.length === 0) return {};
|
||||
const values = await withRetry(() => this.client.mGet(keys));
|
||||
const result: Record<string, Favorite> = {};
|
||||
keys.forEach((fullKey: string, idx: number) => {
|
||||
const raw = values[idx];
|
||||
if (raw) {
|
||||
const fav = JSON.parse(raw) as Favorite;
|
||||
const keyPart = fullKey.replace(`u:${userName}:fav:`, '');
|
||||
result[keyPart] = fav;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
async deleteFavorite(userName: string, key: string): Promise<void> {
|
||||
await withRetry(() => this.client.del(this.favKey(userName, key)));
|
||||
}
|
||||
|
||||
// ---------- 用户注册 / 登录 ----------
|
||||
private userPwdKey(user: string) {
|
||||
return `u:${user}:pwd`;
|
||||
}
|
||||
|
||||
async registerUser(userName: string, password: string): Promise<void> {
|
||||
// 简单存储明文密码,生产环境应加密
|
||||
await withRetry(() => this.client.set(this.userPwdKey(userName), password));
|
||||
}
|
||||
|
||||
async verifyUser(userName: string, password: string): Promise<boolean> {
|
||||
const stored = await withRetry(() =>
|
||||
this.client.get(this.userPwdKey(userName))
|
||||
);
|
||||
if (stored === null) return false;
|
||||
return stored === password;
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
async checkUserExist(userName: string): Promise<boolean> {
|
||||
// 使用 EXISTS 判断 key 是否存在
|
||||
const exists = await withRetry(() =>
|
||||
this.client.exists(this.userPwdKey(userName))
|
||||
);
|
||||
return exists === 1;
|
||||
}
|
||||
|
||||
// ---------- 搜索历史 ----------
|
||||
private shKey(user: string) {
|
||||
return `u:${user}:sh`; // u:username:sh
|
||||
}
|
||||
|
||||
async getSearchHistory(userName: string): Promise<string[]> {
|
||||
return withRetry(
|
||||
() => this.client.lRange(this.shKey(userName), 0, -1) as Promise<string[]>
|
||||
);
|
||||
}
|
||||
|
||||
async addSearchHistory(userName: string, keyword: string): Promise<void> {
|
||||
const key = this.shKey(userName);
|
||||
// 先去重
|
||||
await withRetry(() => this.client.lRem(key, 0, keyword));
|
||||
// 插入到最前
|
||||
await withRetry(() => this.client.lPush(key, keyword));
|
||||
// 限制最大长度
|
||||
await withRetry(() => this.client.lTrim(key, 0, SEARCH_HISTORY_LIMIT - 1));
|
||||
}
|
||||
|
||||
async deleteSearchHistory(userName: string, keyword?: string): Promise<void> {
|
||||
const key = this.shKey(userName);
|
||||
if (keyword) {
|
||||
await withRetry(() => this.client.lRem(key, 0, keyword));
|
||||
} else {
|
||||
await withRetry(() => this.client.del(key));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 获取全部用户 ----------
|
||||
async getAllUsers(): Promise<string[]> {
|
||||
const keys = await withRetry(() => this.client.keys('u:*:pwd'));
|
||||
return keys
|
||||
.map((k) => {
|
||||
const match = k.match(/^u:(.+?):pwd$/);
|
||||
return match ? match[1] : undefined;
|
||||
})
|
||||
.filter((u): u is string => typeof u === 'string');
|
||||
}
|
||||
|
||||
// ---------- 管理员配置 ----------
|
||||
private adminConfigKey() {
|
||||
return 'admin:config';
|
||||
}
|
||||
|
||||
async getAdminConfig(): Promise<AdminConfig | null> {
|
||||
const val = await withRetry(() => this.client.get(this.adminConfigKey()));
|
||||
return val ? (JSON.parse(val) as AdminConfig) : null;
|
||||
}
|
||||
|
||||
async setAdminConfig(config: AdminConfig): Promise<void> {
|
||||
await withRetry(() =>
|
||||
this.client.set(this.adminConfigKey(), JSON.stringify(config))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 单例 Redis 客户端
|
||||
function getRedisClient(): RedisClientType {
|
||||
const globalKey = Symbol.for('__MOONTV_REDIS_CLIENT__');
|
||||
let client: RedisClientType | undefined = (global as any)[globalKey];
|
||||
|
||||
if (!client) {
|
||||
const url = process.env.REDIS_URL;
|
||||
if (!url) {
|
||||
throw new Error('REDIS_URL env variable not set');
|
||||
}
|
||||
|
||||
// 创建客户端,配置重连策略
|
||||
client = createClient({
|
||||
url,
|
||||
socket: {
|
||||
// 重连策略:指数退避,最大30秒
|
||||
reconnectStrategy: (retries: number) => {
|
||||
console.log(`Redis reconnection attempt ${retries + 1}`);
|
||||
if (retries > 10) {
|
||||
console.error('Redis 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.on('error', (err) => {
|
||||
console.error('Redis client error:', err);
|
||||
});
|
||||
|
||||
client.on('connect', () => {
|
||||
console.log('Redis connected');
|
||||
});
|
||||
|
||||
client.on('reconnecting', () => {
|
||||
console.log('Redis reconnecting...');
|
||||
});
|
||||
|
||||
client.on('ready', () => {
|
||||
console.log('Redis ready');
|
||||
});
|
||||
|
||||
// 初始连接,带重试机制
|
||||
const connectWithRetry = async () => {
|
||||
try {
|
||||
await client!.connect();
|
||||
console.log('Redis connected successfully');
|
||||
} catch (err) {
|
||||
console.error('Redis initial connection failed:', err);
|
||||
console.log('Will retry in 5 seconds...');
|
||||
setTimeout(connectWithRetry, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
connectWithRetry();
|
||||
|
||||
(global as any)[globalKey] = client;
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -1,3 +1,66 @@
|
||||
import { AdminConfig } from './admin.types';
|
||||
|
||||
// 播放记录数据结构
|
||||
export interface PlayRecord {
|
||||
title: string;
|
||||
source_name: string;
|
||||
cover: string;
|
||||
index: number; // 第几集
|
||||
total_episodes: number; // 总集数
|
||||
play_time: number; // 播放进度(秒)
|
||||
total_time: number; // 总进度(秒)
|
||||
save_time: number; // 记录保存时间(时间戳)
|
||||
user_id: number; // 用户ID,localStorage情况下全部为0
|
||||
}
|
||||
|
||||
// 收藏数据结构
|
||||
export interface Favorite {
|
||||
source_name: string;
|
||||
total_episodes: number; // 总集数
|
||||
title: string;
|
||||
cover: string;
|
||||
user_id: number; // 用户ID,localStorage情况下全部为0
|
||||
save_time: number; // 记录保存时间(时间戳)
|
||||
}
|
||||
|
||||
// 存储接口
|
||||
export interface IStorage {
|
||||
// 播放记录相关
|
||||
getPlayRecord(userName: string, key: string): Promise<PlayRecord | null>;
|
||||
setPlayRecord(
|
||||
userName: string,
|
||||
key: string,
|
||||
record: PlayRecord
|
||||
): Promise<void>;
|
||||
getAllPlayRecords(userName: string): Promise<{ [key: string]: PlayRecord }>;
|
||||
deletePlayRecord(userName: string, key: string): Promise<void>;
|
||||
|
||||
// 收藏相关
|
||||
getFavorite(userName: string, key: string): Promise<Favorite | null>;
|
||||
setFavorite(userName: string, key: string, favorite: Favorite): Promise<void>;
|
||||
getAllFavorites(userName: string): Promise<{ [key: string]: Favorite }>;
|
||||
deleteFavorite(userName: string, key: string): Promise<void>;
|
||||
|
||||
// 用户相关
|
||||
registerUser(userName: string, password: string): Promise<void>;
|
||||
verifyUser(userName: string, password: string): Promise<boolean>;
|
||||
// 检查用户是否存在(无需密码)
|
||||
checkUserExist(userName: string): Promise<boolean>;
|
||||
|
||||
// 搜索历史相关
|
||||
getSearchHistory(userName: string): Promise<string[]>;
|
||||
addSearchHistory(userName: string, keyword: string): Promise<void>;
|
||||
deleteSearchHistory(userName: string, keyword?: string): Promise<void>;
|
||||
|
||||
// 用户列表
|
||||
getAllUsers(): Promise<string[]>;
|
||||
|
||||
// 管理员配置相关
|
||||
getAdminConfig(): Promise<AdminConfig | null>;
|
||||
setAdminConfig(config: AdminConfig): Promise<void>;
|
||||
}
|
||||
|
||||
// 视频详情数据结构
|
||||
export interface VideoDetail {
|
||||
code: number;
|
||||
episodes: string[];
|
||||
@@ -18,6 +81,7 @@ export interface VideoDetail {
|
||||
};
|
||||
}
|
||||
|
||||
// 搜索结果数据结构
|
||||
export interface SearchResult {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -32,6 +96,7 @@ export interface SearchResult {
|
||||
douban_id?: number;
|
||||
}
|
||||
|
||||
// 豆瓣数据结构
|
||||
export interface DoubanItem {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -44,3 +109,6 @@ export interface DoubanResult {
|
||||
message: string;
|
||||
list: DoubanItem[];
|
||||
}
|
||||
|
||||
// 导出AdminConfig类型
|
||||
export { AdminConfig };
|
||||
|
||||
Reference in New Issue
Block a user