优化用户管理
This commit is contained in:
@@ -7982,8 +7982,8 @@ function AdminPageClient() {
|
|||||||
// 刷新配置和用户列表
|
// 刷新配置和用户列表
|
||||||
const refreshConfigAndUsers = useCallback(async () => {
|
const refreshConfigAndUsers = useCallback(async () => {
|
||||||
await fetchConfig();
|
await fetchConfig();
|
||||||
await fetchUsersV2();
|
await fetchUsersV2(userPage); // 保持当前页码
|
||||||
}, [fetchConfig, fetchUsersV2]);
|
}, [fetchConfig, fetchUsersV2, userPage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 首次加载时显示骨架
|
// 首次加载时显示骨架
|
||||||
|
|||||||
+61
-14
@@ -372,6 +372,12 @@ export abstract class BaseRedisStorage implements IStorage {
|
|||||||
score: createdAt,
|
score: createdAt,
|
||||||
value: userName,
|
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;
|
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(() =>
|
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 = [];
|
const users = [];
|
||||||
|
|
||||||
// 如果有站长,确保站长始终在第一位
|
// 如果有站长且在第一页,确保站长始终在第一位
|
||||||
if (ownerUsername && offset === 0) {
|
if (ownerUsername && offset === 0) {
|
||||||
const ownerInfo = await this.getUserInfoV2(ownerUsername);
|
// 即使站长不在数据库中,也要添加站长(站长使用环境变量认证)
|
||||||
if (ownerInfo) {
|
users.push({
|
||||||
users.push({
|
username: ownerUsername,
|
||||||
username: ownerUsername,
|
role: 'owner' as const,
|
||||||
role: 'owner' as const,
|
banned: ownerInfo?.banned || false,
|
||||||
banned: ownerInfo.banned,
|
tags: ownerInfo?.tags,
|
||||||
tags: ownerInfo.tags,
|
created_at: ownerInfo?.created_at || 0,
|
||||||
created_at: ownerInfo.created_at,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取其他用户信息
|
// 获取其他用户信息
|
||||||
@@ -557,7 +604,7 @@ export abstract class BaseRedisStorage implements IStorage {
|
|||||||
// 删除用户信息Hash
|
// 删除用户信息Hash
|
||||||
await this.withRetry(() => this.client.del(this.userInfoKey(userName)));
|
await this.withRetry(() => this.client.del(this.userInfoKey(userName)));
|
||||||
|
|
||||||
// 从用户列表中移除
|
// 从用���列表中移除
|
||||||
await this.withRetry(() => this.client.zRem(this.userListKey(), userName));
|
await this.withRetry(() => this.client.zRem(this.userListKey(), userName));
|
||||||
|
|
||||||
// 删除用户的其他数据(播放记录、收藏等)
|
// 删除用户的其他数据(播放记录、收藏等)
|
||||||
|
|||||||
+62
-15
@@ -292,6 +292,12 @@ export class UpstashRedisStorage implements IStorage {
|
|||||||
score: createdAt,
|
score: createdAt,
|
||||||
member: userName,
|
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;
|
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(() =>
|
const usernames = await withRetry(() =>
|
||||||
this.client.zrange(this.userListKey(), offset, offset + limit - 1)
|
this.client.zrange(this.userListKey(), actualOffset, actualOffset + actualLimit - 1)
|
||||||
);
|
);
|
||||||
|
|
||||||
const users = [];
|
const users = [];
|
||||||
|
|
||||||
// 如果有站长,确保站长始终在第一位
|
// 如果有站长且在第一页,确保站长始终在第一位
|
||||||
if (ownerUsername && offset === 0) {
|
if (ownerUsername && offset === 0) {
|
||||||
const ownerInfo = await this.getUserInfoV2(ownerUsername);
|
// 即使站长不在数据库中,也要添加站长(站长使用环境变量认证)
|
||||||
if (ownerInfo) {
|
users.push({
|
||||||
users.push({
|
username: ownerUsername,
|
||||||
username: ownerUsername,
|
role: 'owner' as const,
|
||||||
role: 'owner' as const,
|
banned: ownerInfo?.banned || false,
|
||||||
banned: ownerInfo.banned,
|
tags: ownerInfo?.tags,
|
||||||
tags: ownerInfo.tags,
|
oidcSub: ownerInfo?.oidcSub,
|
||||||
oidcSub: ownerInfo.oidcSub,
|
enabledApis: ownerInfo?.enabledApis,
|
||||||
enabledApis: ownerInfo.enabledApis,
|
created_at: ownerInfo?.created_at || 0,
|
||||||
created_at: ownerInfo.created_at,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取其他用户信息
|
// 获取其他用户信息
|
||||||
|
|||||||
+59
-1
@@ -55,6 +55,51 @@ class UserInfoCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 站长存在状态缓存
|
||||||
|
class OwnerExistenceCache {
|
||||||
|
private cache: Map<string, { exists: boolean; cachedAt: number }> = 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__');
|
const globalKey = Symbol.for('__MOONTV_USER_INFO_CACHE__');
|
||||||
let userInfoCache: UserInfoCache | undefined = (global as any)[globalKey];
|
let userInfoCache: UserInfoCache | undefined = (global as any)[globalKey];
|
||||||
@@ -69,4 +114,17 @@ if (!userInfoCache) {
|
|||||||
}, 60 * 1000);
|
}, 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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user