Files
MuMuAINovel/backend/app/user_manager.py
T
xiamuceer 0f6c2d344a init
2025-10-30 11:14:43 +08:00

294 lines
11 KiB
Python

"""
用户管理模块 - 支持 LinuxDO OAuth2
"""
import json
import os
import asyncio
from datetime import datetime
from typing import Optional, Dict, List
from pydantic import BaseModel
from app.config import settings, DATA_DIR
class User(BaseModel):
"""用户模型"""
user_id: str # 格式: linuxdo_{linuxdo_id}
username: str
display_name: str
avatar_url: Optional[str] = None
trust_level: int = 0 # 仅用于显示
is_admin: bool = False # 手动设置的管理员权限
linuxdo_id: str # LinuxDO 用户 ID
created_at: str
last_login: str
class UserManager:
"""用户管理器 - 线程安全版本"""
USERS_FILE = str(DATA_DIR / "users.json")
ADMINS_FILE = str(DATA_DIR / "admins.json")
def __init__(self):
"""初始化用户管理器"""
# DATA_DIR 已在 config.py 中创建,无需重复创建
# 添加文件锁保护并发读写
self._users_lock = asyncio.Lock()
self._admins_lock = asyncio.Lock()
self._ensure_files_exist()
def _ensure_files_exist(self):
"""确保必要的文件存在"""
if not os.path.exists(self.USERS_FILE):
with open(self.USERS_FILE, "w", encoding="utf-8") as f:
json.dump({}, f, ensure_ascii=False, indent=2)
if not os.path.exists(self.ADMINS_FILE):
with open(self.ADMINS_FILE, "w", encoding="utf-8") as f:
json.dump({"admins": []}, f, ensure_ascii=False, indent=2)
def _load_users_unsafe(self) -> Dict[str, dict]:
"""加载用户数据(不加锁,内部使用)"""
try:
with open(self.USERS_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"加载用户数据失败: {e}")
return {}
def _save_users_unsafe(self, users: Dict[str, dict]):
"""保存用户数据(不加锁,内部使用)"""
try:
with open(self.USERS_FILE, "w", encoding="utf-8") as f:
json.dump(users, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"保存用户数据失败: {e}")
async def _load_users(self) -> Dict[str, dict]:
"""加载用户数据(加锁)"""
async with self._users_lock:
return self._load_users_unsafe()
async def _save_users(self, users: Dict[str, dict]):
"""保存用户数据(加锁)"""
async with self._users_lock:
self._save_users_unsafe(users)
def _load_admin_list_unsafe(self) -> List[str]:
"""加载管理员列表(不加锁,内部使用)"""
try:
with open(self.ADMINS_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("admins", [])
except Exception as e:
print(f"加载管理员列表失败: {e}")
return []
def _save_admin_list_unsafe(self, admin_list: List[str]):
"""保存管理员列表(不加锁,内部使用)"""
try:
with open(self.ADMINS_FILE, "w", encoding="utf-8") as f:
json.dump({"admins": admin_list}, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"保存管理员列表失败: {e}")
async def _load_admin_list(self) -> List[str]:
"""加载管理员列表(加锁)"""
async with self._admins_lock:
return self._load_admin_list_unsafe()
async def _save_admin_list(self, admin_list: List[str]):
"""保存管理员列表(加锁)"""
async with self._admins_lock:
self._save_admin_list_unsafe(admin_list)
async def create_or_update_from_linuxdo(
self,
linuxdo_id: str,
username: str,
display_name: str,
avatar_url: Optional[str],
trust_level: int
) -> User:
"""
从 LinuxDO 用户信息创建或更新用户(线程安全)
Args:
linuxdo_id: LinuxDO 用户 ID(本地用户时为 local_xxx 格式)
username: 用户名
display_name: 显示名称
avatar_url: 头像 URL
trust_level: 信任等级 (仅用于显示)
Returns:
用户对象
"""
# 如果已经是 local_ 开头,直接使用;否则添加 linuxdo_ 前缀
if linuxdo_id.startswith("local_"):
user_id = linuxdo_id
else:
user_id = f"linuxdo_{linuxdo_id}"
# 使用锁保护整个读-改-写操作
async with self._users_lock:
async with self._admins_lock:
users = self._load_users_unsafe()
admin_list = self._load_admin_list_unsafe()
now = datetime.now().isoformat()
# 检查是否为初始管理员
initial_admin_id = settings.INITIAL_ADMIN_LINUXDO_ID
is_initial_admin = (initial_admin_id and linuxdo_id == initial_admin_id)
# 检查是否为本地用户(所有 local_ 开头的用户默认为管理员)
is_local_user = user_id.startswith("local_")
if user_id in users:
# 更新现有用户
user_data = users[user_id]
user_data["username"] = username
user_data["display_name"] = display_name
user_data["avatar_url"] = avatar_url
user_data["trust_level"] = trust_level
user_data["last_login"] = now
# 如果是初始管理员或本地用户且还不在管理员列表中,添加进去
if (is_initial_admin or is_local_user) and user_id not in admin_list:
admin_list.append(user_id)
self._save_admin_list_unsafe(admin_list)
user_data["is_admin"] = True
else:
# 从管理员列表同步 is_admin 状态
user_data["is_admin"] = user_id in admin_list
else:
# 创建新用户(本地用户默认为管理员)
is_admin = is_initial_admin or is_local_user
if is_admin and user_id not in admin_list:
admin_list.append(user_id)
self._save_admin_list_unsafe(admin_list)
user_data = {
"user_id": user_id,
"username": username,
"display_name": display_name,
"avatar_url": avatar_url,
"trust_level": trust_level,
"is_admin": is_admin,
"linuxdo_id": linuxdo_id,
"created_at": now,
"last_login": now
}
users[user_id] = user_data
self._save_users_unsafe(users)
return User(**user_data)
async def get_user(self, user_id: str) -> Optional[User]:
"""获取用户(线程安全)"""
users = await self._load_users()
user_data = users.get(user_id)
if user_data:
# 同步管理员状态
admin_list = await self._load_admin_list()
user_data["is_admin"] = user_id in admin_list
return User(**user_data)
return None
async def get_all_users(self) -> List[User]:
"""获取所有用户(线程安全)"""
users = await self._load_users()
admin_list = await self._load_admin_list()
user_list = []
for user_data in users.values():
# 同步管理员状态
user_data["is_admin"] = user_data["user_id"] in admin_list
user_list.append(User(**user_data))
return user_list
async def set_admin(self, user_id: str, is_admin: bool) -> bool:
"""
设置用户的管理员权限(线程安全)
Args:
user_id: 用户 ID
is_admin: 是否为管理员
Returns:
是否成功
"""
# 使用锁保护整个读-改-写操作
async with self._users_lock:
async with self._admins_lock:
users = self._load_users_unsafe()
if user_id not in users:
return False
admin_list = self._load_admin_list_unsafe()
if is_admin:
# 授予管理员权限
if user_id not in admin_list:
admin_list.append(user_id)
self._save_admin_list_unsafe(admin_list)
else:
# 撤销管理员权限
if user_id in admin_list:
# 确保至少保留一个管理员
if len(admin_list) <= 1:
return False
admin_list.remove(user_id)
self._save_admin_list_unsafe(admin_list)
# 更新用户数据中的 is_admin 字段
users[user_id]["is_admin"] = is_admin
self._save_users_unsafe(users)
return True
async def delete_user(self, user_id: str) -> bool:
"""
删除用户(线程安全)
Args:
user_id: 用户 ID
Returns:
是否成功
"""
# 使用锁保护整个读-改-写操作
async with self._users_lock:
async with self._admins_lock:
users = self._load_users_unsafe()
if user_id not in users:
return False
# 不能删除管理员
admin_list = self._load_admin_list_unsafe()
if user_id in admin_list:
return False
# 删除用户数据
del users[user_id]
self._save_users_unsafe(users)
# 删除用户数据库文件(在锁外执行,避免阻塞)
db_file = str(DATA_DIR / f"ai_story_user_{user_id}.db")
if os.path.exists(db_file):
try:
os.remove(db_file)
except Exception as e:
print(f"删除用户数据库文件失败: {e}")
return True
async def is_admin(self, user_id: str) -> bool:
"""检查用户是否为管理员(线程安全)"""
admin_list = await self._load_admin_list()
return user_id in admin_list
# 全局用户管理器实例
user_manager = UserManager()