diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 130c3a3..0cde3f3 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -7982,8 +7982,8 @@ function AdminPageClient() { // 刷新配置和用户列表 const refreshConfigAndUsers = useCallback(async () => { await fetchConfig(); - await fetchUsersV2(); - }, [fetchConfig, fetchUsersV2]); + await fetchUsersV2(userPage); // 保持当前页码 + }, [fetchConfig, fetchUsersV2, userPage]); useEffect(() => { // 首次加载时显示骨架 diff --git a/src/lib/redis-base.db.ts b/src/lib/redis-base.db.ts index 94aa0ba..9a93c48 100644 --- a/src/lib/redis-base.db.ts +++ b/src/lib/redis-base.db.ts @@ -372,6 +372,12 @@ export abstract class BaseRedisStorage implements IStorage { score: createdAt, value: userName, })); + + // 如果创建的是站长用户,清除站长存在状态缓存 + if (userName === process.env.USERNAME) { + const { ownerExistenceCache } = await import('./user-cache'); + ownerExistenceCache.delete(userName); + } } // 验证用户密码(新版本) @@ -498,27 +504,68 @@ export abstract class BaseRedisStorage implements IStorage { total: number; }> { // 获取总数 - const total = await this.withRetry(() => this.client.zCard(this.userListKey())); + let total = await this.withRetry(() => this.client.zCard(this.userListKey())); + + // 检查站长是否在数据库中(使用缓存) + 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; + } + } // 获取用户列表(按注册时间升序) const usernames = await this.withRetry(() => - this.client.zRange(this.userListKey(), offset, offset + limit - 1) + this.client.zRange(this.userListKey(), actualOffset, actualOffset + actualLimit - 1) ); const users = []; - // 如果有站长,确保站长始终在第一位 + // 如果有站长且在第一页,确保站长始终在第一位 if (ownerUsername && offset === 0) { - const ownerInfo = await this.getUserInfoV2(ownerUsername); - if (ownerInfo) { - users.push({ - username: ownerUsername, - role: 'owner' as const, - banned: ownerInfo.banned, - tags: ownerInfo.tags, - created_at: ownerInfo.created_at, - }); - } + // 即使站长不在数据库中,也要添加站长(站长使用环境变量认证) + users.push({ + username: ownerUsername, + role: 'owner' as const, + banned: ownerInfo?.banned || false, + tags: ownerInfo?.tags, + created_at: ownerInfo?.created_at || 0, + }); } // 获取其他用户信息 @@ -557,7 +604,7 @@ export abstract class BaseRedisStorage implements IStorage { // 删除用户信息Hash await this.withRetry(() => this.client.del(this.userInfoKey(userName))); - // 从用户列表中移除 + // 从用���列表中移除 await this.withRetry(() => this.client.zRem(this.userListKey(), userName)); // 删除用户的其他数据(播放记录、收藏等) diff --git a/src/lib/upstash.db.ts b/src/lib/upstash.db.ts index bd76783..b8ed0b1 100644 --- a/src/lib/upstash.db.ts +++ b/src/lib/upstash.db.ts @@ -292,6 +292,12 @@ export class UpstashRedisStorage implements IStorage { score: createdAt, member: userName, })); + + // 如果创建的是站长用户,清除站长存在状态缓存 + if (userName === process.env.USERNAME) { + const { ownerExistenceCache } = await import('./user-cache'); + ownerExistenceCache.delete(userName); + } } // 验证用户密码(新版本) @@ -519,29 +525,70 @@ export class UpstashRedisStorage implements IStorage { total: number; }> { // 获取总数 - const total = await withRetry(() => this.client.zcard(this.userListKey())); + let total = await withRetry(() => this.client.zcard(this.userListKey())); + + // 检查站长是否在数据库中(使用缓存) + 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; + } + } // 获取用户列表(按注册时间升序) const usernames = await withRetry(() => - this.client.zrange(this.userListKey(), offset, offset + limit - 1) + this.client.zrange(this.userListKey(), actualOffset, actualOffset + actualLimit - 1) ); const users = []; - // 如果有站长,确保站长始终在第一位 + // 如果有站长且在第一页,确保站长始终在第一位 if (ownerUsername && offset === 0) { - const ownerInfo = await this.getUserInfoV2(ownerUsername); - if (ownerInfo) { - users.push({ - username: ownerUsername, - role: 'owner' as const, - banned: ownerInfo.banned, - tags: ownerInfo.tags, - oidcSub: ownerInfo.oidcSub, - enabledApis: ownerInfo.enabledApis, - created_at: ownerInfo.created_at, - }); - } + // 即使站长不在数据库中,也要添加站长(站长使用环境变量认证) + users.push({ + username: ownerUsername, + role: 'owner' as const, + banned: ownerInfo?.banned || false, + tags: ownerInfo?.tags, + oidcSub: ownerInfo?.oidcSub, + enabledApis: ownerInfo?.enabledApis, + created_at: ownerInfo?.created_at || 0, + }); } // 获取其他用户信息 diff --git a/src/lib/user-cache.ts b/src/lib/user-cache.ts index 92517b8..9c71e18 100644 --- a/src/lib/user-cache.ts +++ b/src/lib/user-cache.ts @@ -55,6 +55,51 @@ class UserInfoCache { } } +// 站长存在状态缓存 +class OwnerExistenceCache { + private cache: Map = new Map(); + private readonly TTL = 10 * 60 * 1000; // 10分钟过期 + + get(ownerUsername: string): boolean | null { + const cached = this.cache.get(ownerUsername); + if (!cached) return null; + + // 检查是否过期 + if (Date.now() - cached.cachedAt > this.TTL) { + this.cache.delete(ownerUsername); + return null; + } + + return cached.exists; + } + + set(ownerUsername: string, exists: boolean): void { + this.cache.set(ownerUsername, { + exists, + cachedAt: Date.now(), + }); + } + + delete(ownerUsername: string): void { + this.cache.delete(ownerUsername); + } + + clear(): void { + this.cache.clear(); + } + + // 清理过期的缓存 + cleanup(): void { + const now = Date.now(); + const entries = Array.from(this.cache.entries()); + for (const [username, cached] of entries) { + if (now - cached.cachedAt > this.TTL) { + this.cache.delete(username); + } + } + } +} + // 全局单例 const globalKey = Symbol.for('__MOONTV_USER_INFO_CACHE__'); let userInfoCache: UserInfoCache | undefined = (global as any)[globalKey]; @@ -69,4 +114,17 @@ if (!userInfoCache) { }, 60 * 1000); } -export { userInfoCache }; +const ownerExistenceGlobalKey = Symbol.for('__MOONTV_OWNER_EXISTENCE_CACHE__'); +let ownerExistenceCache: OwnerExistenceCache | undefined = (global as any)[ownerExistenceGlobalKey]; + +if (!ownerExistenceCache) { + ownerExistenceCache = new OwnerExistenceCache(); + (global as any)[ownerExistenceGlobalKey] = ownerExistenceCache; + + // 每分钟清理一次过期缓存 + setInterval(() => { + ownerExistenceCache?.cleanup(); + }, 60 * 1000); +} + +export { userInfoCache, ownerExistenceCache };