用户数据结构变更
This commit is contained in:
+251
-1
@@ -242,7 +242,7 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
await this.withRetry(() => this.client.del(this.favKey(userName, key)));
|
||||
}
|
||||
|
||||
// ---------- 用户注册 / 登录 ----------
|
||||
// ---------- 用户注册 / 登录(旧版本,保持兼容) ----------
|
||||
private userPwdKey(user: string) {
|
||||
return `u:${user}:pwd`;
|
||||
}
|
||||
@@ -314,6 +314,256 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 新版用户存储(使用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[],
|
||||
oidcSub?: string
|
||||
): 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);
|
||||
}
|
||||
|
||||
if (oidcSub) {
|
||||
userInfo.oidcSub = oidcSub;
|
||||
// 创建OIDC映射
|
||||
await this.withRetry(() => this.client.set(this.oidcSubKey(oidcSub), userName));
|
||||
}
|
||||
|
||||
await this.withRetry(() => this.client.hSet(this.userInfoKey(userName), userInfo));
|
||||
|
||||
// 添加到用户列表(Sorted Set,按注册时间排序)
|
||||
await this.withRetry(() => this.client.zAdd(this.userListKey(), {
|
||||
score: createdAt,
|
||||
value: userName,
|
||||
}));
|
||||
}
|
||||
|
||||
// 验证用户密码(新版本)
|
||||
async verifyUserV2(userName: string, password: string): Promise<boolean> {
|
||||
const userInfo = await this.withRetry(() =>
|
||||
this.client.hGetAll(this.userInfoKey(userName))
|
||||
);
|
||||
|
||||
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;
|
||||
created_at: number;
|
||||
} | null> {
|
||||
const userInfo = await this.withRetry(() =>
|
||||
this.client.hGetAll(this.userInfoKey(userName))
|
||||
);
|
||||
|
||||
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,
|
||||
created_at: parseInt(userInfo.created_at || '0', 10),
|
||||
};
|
||||
}
|
||||
|
||||
// 更新用户信息(新版本)
|
||||
async updateUserInfoV2(
|
||||
userName: string,
|
||||
updates: {
|
||||
role?: 'owner' | 'admin' | 'user';
|
||||
banned?: boolean;
|
||||
tags?: string[];
|
||||
oidcSub?: string;
|
||||
}
|
||||
): 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.client.hDel(this.userInfoKey(userName), 'tags'));
|
||||
}
|
||||
}
|
||||
|
||||
if (updates.oidcSub !== undefined) {
|
||||
const oldInfo = await this.getUserInfoV2(userName);
|
||||
if (oldInfo?.oidcSub && oldInfo.oidcSub !== updates.oidcSub) {
|
||||
// 删除旧的OIDC映射
|
||||
await this.withRetry(() => this.client.del(this.oidcSubKey(oldInfo.oidcSub!)));
|
||||
}
|
||||
userInfo.oidcSub = updates.oidcSub;
|
||||
// 创建新的OIDC映射
|
||||
await this.withRetry(() => this.client.set(this.oidcSubKey(updates.oidcSub!), userName));
|
||||
}
|
||||
|
||||
if (Object.keys(userInfo).length > 0) {
|
||||
await this.withRetry(() => this.client.hSet(this.userInfoKey(userName), userInfo));
|
||||
}
|
||||
}
|
||||
|
||||
// 修改用户密码(新版本)
|
||||
async changePasswordV2(userName: string, newPassword: string): Promise<void> {
|
||||
const hashedPassword = await this.hashPassword(newPassword);
|
||||
await this.withRetry(() =>
|
||||
this.client.hSet(this.userInfoKey(userName), 'password', hashedPassword)
|
||||
);
|
||||
}
|
||||
|
||||
// 检查用户是否存在(新版本)
|
||||
async checkUserExistV2(userName: string): Promise<boolean> {
|
||||
const exists = await this.withRetry(() =>
|
||||
this.client.exists(this.userInfoKey(userName))
|
||||
);
|
||||
return exists === 1;
|
||||
}
|
||||
|
||||
// 通过OIDC Sub查找用户名
|
||||
async getUserByOidcSub(oidcSub: string): Promise<string | null> {
|
||||
const userName = await this.withRetry(() =>
|
||||
this.client.get(this.oidcSubKey(oidcSub))
|
||||
);
|
||||
return userName ? ensureString(userName) : null;
|
||||
}
|
||||
|
||||
// 获取用户列表(分页,新版本)
|
||||
async getUserListV2(
|
||||
offset: number = 0,
|
||||
limit: number = 20,
|
||||
ownerUsername?: string
|
||||
): Promise<{
|
||||
users: Array<{
|
||||
username: string;
|
||||
role: 'owner' | 'admin' | 'user';
|
||||
banned: boolean;
|
||||
tags?: string[];
|
||||
created_at: number;
|
||||
}>;
|
||||
total: number;
|
||||
}> {
|
||||
// 获取总数
|
||||
const total = await this.withRetry(() => this.client.zCard(this.userListKey()));
|
||||
|
||||
// 获取用户列表(按注册时间升序)
|
||||
const usernames = await this.withRetry(() =>
|
||||
this.client.zRange(this.userListKey(), offset, offset + limit - 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取其他用户信息
|
||||
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,
|
||||
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.client.del(this.oidcSubKey(userInfo.oidcSub!)));
|
||||
}
|
||||
|
||||
// 删除用户信息Hash
|
||||
await this.withRetry(() => this.client.del(this.userInfoKey(userName)));
|
||||
|
||||
// 从用户列表中移除
|
||||
await this.withRetry(() => this.client.zRem(this.userListKey(), userName));
|
||||
|
||||
// 删除用户的其他数据(播放记录、收藏等)
|
||||
await this.deleteUser(userName);
|
||||
}
|
||||
|
||||
// ---------- 搜索历史 ----------
|
||||
private shKey(user: string) {
|
||||
return `u:${user}:sh`; // u:username:sh
|
||||
|
||||
Reference in New Issue
Block a user