fix
This commit is contained in:
69
examples/user_postgres_example.py
Normal file
69
examples/user_postgres_example.py
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
UserTablePostgres 使用示例
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from python_core.database.user_postgres import UserTablePostgres
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("=== UserTablePostgres 使用示例 ===\n")
|
||||
|
||||
# 创建用户表实例
|
||||
user_table = UserTablePostgres()
|
||||
print("✅ 用户表初始化成功")
|
||||
|
||||
# 创建用户
|
||||
print("\n1. 创建用户...")
|
||||
user_id = user_table.create_user(
|
||||
username="john_doe",
|
||||
email="john@example.com",
|
||||
password="secure_password_123",
|
||||
display_name="John Doe"
|
||||
)
|
||||
print(f"用户创建成功,ID: {user_id}")
|
||||
|
||||
# 获取用户信息
|
||||
print("\n2. 获取用户信息...")
|
||||
user = user_table.get_user_by_username("john_doe")
|
||||
print(f"用户信息: {user['display_name']} ({user['email']})")
|
||||
|
||||
# 用户认证
|
||||
print("\n3. 用户认证...")
|
||||
auth_result = user_table.authenticate_user("john_doe", "secure_password_123")
|
||||
if auth_result:
|
||||
print(f"认证成功: {auth_result['username']}")
|
||||
else:
|
||||
print("认证失败")
|
||||
|
||||
# 更新用户信息
|
||||
print("\n4. 更新用户信息...")
|
||||
user_table.update_user(user_id, {
|
||||
"display_name": "John Smith",
|
||||
"avatar_url": "https://example.com/avatar.jpg"
|
||||
})
|
||||
print("用户信息更新成功")
|
||||
|
||||
# 获取用户统计
|
||||
print("\n5. 用户统计...")
|
||||
user_count = user_table.get_user_count()
|
||||
print(f"总用户数: {user_count}")
|
||||
|
||||
# 搜索用户
|
||||
print("\n6. 搜索用户...")
|
||||
search_results = user_table.search_users("john")
|
||||
print(f"搜索到 {len(search_results)} 个用户")
|
||||
|
||||
# 清理:删除测试用户
|
||||
print("\n7. 清理测试数据...")
|
||||
user_table.delete_user(user_id)
|
||||
print("测试用户已删除")
|
||||
|
||||
print("\n✅ 示例完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -18,7 +18,7 @@ except ImportError:
|
||||
project_root = Path(__file__).parent.parent
|
||||
class Settings(BaseSettings):
|
||||
"""Application settings with environment variable support."""
|
||||
|
||||
db: str = "postgres://dcb93e55a36f8056ce4ddc37c4ddd46d717859834d9d7475a080f57c25a99067:sk_qN1plssQ5n2gKU4ZNNG0_@db.prisma.io:5432/?sslmode=require"
|
||||
# Application Info
|
||||
app_name: str = "MixVideo V2"
|
||||
app_version: str = "2.0.0"
|
||||
|
||||
620
python_core/database/user_postgres.py
Normal file
620
python_core/database/user_postgres.py
Normal file
@@ -0,0 +1,620 @@
|
||||
# 用户表 - PostgreSQL 版本
|
||||
|
||||
import hashlib
|
||||
import uuid
|
||||
from typing import Dict, List, Any, Optional
|
||||
from datetime import datetime
|
||||
from contextlib import contextmanager
|
||||
from python_core.config import settings
|
||||
from python_core.utils.logger import setup_logger
|
||||
|
||||
# 尝试导入 psycopg2,如果失败则提供友好的错误信息
|
||||
try:
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
PSYCOPG2_AVAILABLE = True
|
||||
except ImportError as e:
|
||||
PSYCOPG2_AVAILABLE = False
|
||||
PSYCOPG2_ERROR = str(e)
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
class UserTablePostgres:
|
||||
"""
|
||||
用户表类 - PostgreSQL 版本
|
||||
基于 PostgreSQL 数据库实现的用户管理功能
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# 检查 psycopg2 是否可用
|
||||
if not PSYCOPG2_AVAILABLE:
|
||||
error_msg = f"""
|
||||
PostgreSQL 驱动 psycopg2 未安装。请安装:
|
||||
|
||||
方案1(推荐):
|
||||
pip install psycopg2-binary
|
||||
|
||||
方案2:
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install postgresql libpq-dev python3-dev
|
||||
pip install psycopg2
|
||||
|
||||
# CentOS/RHEL
|
||||
sudo yum install postgresql postgresql-devel python3-devel
|
||||
pip install psycopg2
|
||||
|
||||
# macOS
|
||||
brew install postgresql
|
||||
pip install psycopg2
|
||||
|
||||
原始错误: {PSYCOPG2_ERROR}
|
||||
"""
|
||||
logger.error(error_msg)
|
||||
raise ImportError(error_msg)
|
||||
|
||||
self.db_url = settings.db
|
||||
self.table_name = "users"
|
||||
|
||||
# 初始化用户表
|
||||
self._init_user_table()
|
||||
|
||||
@contextmanager
|
||||
def _get_connection(self):
|
||||
"""获取数据库连接的上下文管理器"""
|
||||
conn = None
|
||||
try:
|
||||
conn = psycopg2.connect(self.db_url)
|
||||
yield conn
|
||||
except Exception as e:
|
||||
if conn:
|
||||
conn.rollback()
|
||||
logger.error(f"Database connection error: {e}")
|
||||
raise e
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
def _init_user_table(self):
|
||||
"""初始化用户表"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
# 创建用户表(如果不存在)
|
||||
create_table_sql = """
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
email VARCHAR(100) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(64) NOT NULL,
|
||||
display_name VARCHAR(100) NOT NULL,
|
||||
avatar_url TEXT DEFAULT '',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
last_login TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
"""
|
||||
cursor.execute(create_table_sql)
|
||||
|
||||
# 创建索引
|
||||
indexes = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_users_is_active ON users(is_active);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at);"
|
||||
]
|
||||
|
||||
for index_sql in indexes:
|
||||
cursor.execute(index_sql)
|
||||
|
||||
conn.commit()
|
||||
logger.info("User table initialized")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize user table: {e}")
|
||||
raise e
|
||||
|
||||
def _hash_password(self, password: str) -> str:
|
||||
"""密码哈希"""
|
||||
return hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
def _verify_password(self, password: str, password_hash: str) -> bool:
|
||||
"""验证密码"""
|
||||
return self._hash_password(password) == password_hash
|
||||
|
||||
def create_user(self, username: str, email: str, password: str, display_name: str = None) -> str:
|
||||
"""
|
||||
创建用户
|
||||
|
||||
Args:
|
||||
username: 用户名
|
||||
email: 邮箱
|
||||
password: 密码
|
||||
display_name: 显示名称
|
||||
|
||||
Returns:
|
||||
用户ID
|
||||
"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
# 检查用户名是否已存在
|
||||
cursor.execute("SELECT id FROM users WHERE username = %s", (username,))
|
||||
if cursor.fetchone():
|
||||
raise ValueError(f"Username '{username}' already exists")
|
||||
|
||||
# 检查邮箱是否已存在
|
||||
cursor.execute("SELECT id FROM users WHERE email = %s", (email,))
|
||||
if cursor.fetchone():
|
||||
raise ValueError(f"Email '{email}' already exists")
|
||||
|
||||
# 生成用户ID
|
||||
user_id = str(uuid.uuid4())
|
||||
|
||||
# 插入用户记录
|
||||
insert_sql = """
|
||||
INSERT INTO users (id, username, email, password_hash, display_name, avatar_url, is_active, created_at, updated_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
|
||||
now = datetime.now()
|
||||
cursor.execute(insert_sql, (
|
||||
user_id,
|
||||
username,
|
||||
email,
|
||||
self._hash_password(password),
|
||||
display_name or username,
|
||||
'',
|
||||
True,
|
||||
now,
|
||||
now
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"Created user: {username} (ID: {user_id})")
|
||||
return user_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create user '{username}': {e}")
|
||||
raise e
|
||||
|
||||
def get_user_by_id(self, user_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
根据ID获取用户
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
用户信息,如果不存在返回None
|
||||
"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cursor:
|
||||
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
||||
row = cursor.fetchone()
|
||||
|
||||
if row:
|
||||
return {
|
||||
'id': row['id'],
|
||||
'username': row['username'],
|
||||
'email': row['email'],
|
||||
'password_hash': row['password_hash'],
|
||||
'display_name': row['display_name'],
|
||||
'avatar_url': row['avatar_url'],
|
||||
'is_active': row['is_active'],
|
||||
'last_login': row['last_login'].isoformat() if row['last_login'] else None,
|
||||
'created_at': row['created_at'].isoformat() if row['created_at'] else None,
|
||||
'updated_at': row['updated_at'].isoformat() if row['updated_at'] else None
|
||||
}
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get user by ID '{user_id}': {e}")
|
||||
return None
|
||||
|
||||
def get_user_by_username(self, username: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
根据用户名获取用户
|
||||
|
||||
Args:
|
||||
username: 用户名
|
||||
|
||||
Returns:
|
||||
用户信息,如果不存在返回None
|
||||
"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cursor:
|
||||
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
|
||||
row = cursor.fetchone()
|
||||
|
||||
if row:
|
||||
return {
|
||||
'id': row['id'],
|
||||
'username': row['username'],
|
||||
'email': row['email'],
|
||||
'password_hash': row['password_hash'],
|
||||
'display_name': row['display_name'],
|
||||
'avatar_url': row['avatar_url'],
|
||||
'is_active': row['is_active'],
|
||||
'last_login': row['last_login'].isoformat() if row['last_login'] else None,
|
||||
'created_at': row['created_at'].isoformat() if row['created_at'] else None,
|
||||
'updated_at': row['updated_at'].isoformat() if row['updated_at'] else None
|
||||
}
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get user by username '{username}': {e}")
|
||||
return None
|
||||
|
||||
def get_user_by_email(self, email: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
根据邮箱获取用户
|
||||
|
||||
Args:
|
||||
email: 邮箱
|
||||
|
||||
Returns:
|
||||
用户信息,如果不存在返回None
|
||||
"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cursor:
|
||||
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
|
||||
row = cursor.fetchone()
|
||||
|
||||
if row:
|
||||
return {
|
||||
'id': row['id'],
|
||||
'username': row['username'],
|
||||
'email': row['email'],
|
||||
'password_hash': row['password_hash'],
|
||||
'display_name': row['display_name'],
|
||||
'avatar_url': row['avatar_url'],
|
||||
'is_active': row['is_active'],
|
||||
'last_login': row['last_login'].isoformat() if row['last_login'] else None,
|
||||
'created_at': row['created_at'].isoformat() if row['created_at'] else None,
|
||||
'updated_at': row['updated_at'].isoformat() if row['updated_at'] else None
|
||||
}
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get user by email '{email}': {e}")
|
||||
return None
|
||||
|
||||
def authenticate_user(self, username_or_email: str, password: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
用户认证
|
||||
|
||||
Args:
|
||||
username_or_email: 用户名或邮箱
|
||||
password: 密码
|
||||
|
||||
Returns:
|
||||
认证成功返回用户信息,失败返回None
|
||||
"""
|
||||
try:
|
||||
# 尝试按用户名查找
|
||||
user = self.get_user_by_username(username_or_email)
|
||||
|
||||
# 如果按用户名找不到,尝试按邮箱查找
|
||||
if not user:
|
||||
user = self.get_user_by_email(username_or_email)
|
||||
|
||||
# 验证用户存在且密码正确
|
||||
if user and self._verify_password(password, user["password_hash"]):
|
||||
# 更新最后登录时间
|
||||
self.update_last_login(user["id"])
|
||||
|
||||
# 返回用户信息(不包含密码哈希)
|
||||
user_info = user.copy()
|
||||
del user_info["password_hash"]
|
||||
|
||||
logger.info(f"User authenticated: {user['username']}")
|
||||
return user_info
|
||||
|
||||
logger.warning(f"Authentication failed for: {username_or_email}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Authentication error for '{username_or_email}': {e}")
|
||||
return None
|
||||
|
||||
def update_user(self, user_id: str, updates: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
更新用户信息
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
updates: 要更新的字段
|
||||
|
||||
Returns:
|
||||
更新成功返回True
|
||||
"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
# 检查用户是否存在
|
||||
cursor.execute("SELECT id FROM users WHERE id = %s", (user_id,))
|
||||
if not cursor.fetchone():
|
||||
logger.warning(f"User not found: {user_id}")
|
||||
return False
|
||||
|
||||
# 移除不应该直接更新的字段
|
||||
protected_fields = ["id", "created_at", "password_hash"]
|
||||
filtered_updates = {k: v for k, v in updates.items() if k not in protected_fields}
|
||||
|
||||
if not filtered_updates:
|
||||
logger.warning("No valid fields to update")
|
||||
return False
|
||||
|
||||
# 添加更新时间
|
||||
filtered_updates["updated_at"] = datetime.now()
|
||||
|
||||
# 构建更新SQL
|
||||
set_clauses = []
|
||||
values = []
|
||||
for key, value in filtered_updates.items():
|
||||
set_clauses.append(f"{key} = %s")
|
||||
values.append(value)
|
||||
|
||||
values.append(user_id) # WHERE条件的参数
|
||||
|
||||
update_sql = f"UPDATE users SET {', '.join(set_clauses)} WHERE id = %s"
|
||||
cursor.execute(update_sql, values)
|
||||
|
||||
conn.commit()
|
||||
|
||||
if cursor.rowcount > 0:
|
||||
logger.info(f"Updated user: {user_id}")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"No rows updated for user: {user_id}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update user '{user_id}': {e}")
|
||||
return False
|
||||
|
||||
def update_password(self, user_id: str, new_password: str) -> bool:
|
||||
"""
|
||||
更新用户密码
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
new_password: 新密码
|
||||
|
||||
Returns:
|
||||
更新成功返回True
|
||||
"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
# 检查用户是否存在
|
||||
cursor.execute("SELECT id FROM users WHERE id = %s", (user_id,))
|
||||
if not cursor.fetchone():
|
||||
logger.warning(f"User not found: {user_id}")
|
||||
return False
|
||||
|
||||
# 更新密码
|
||||
update_sql = "UPDATE users SET password_hash = %s, updated_at = %s WHERE id = %s"
|
||||
cursor.execute(update_sql, (
|
||||
self._hash_password(new_password),
|
||||
datetime.now(),
|
||||
user_id
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
|
||||
if cursor.rowcount > 0:
|
||||
logger.info(f"Password updated for user: {user_id}")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"No rows updated for user: {user_id}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update password for user '{user_id}': {e}")
|
||||
return False
|
||||
|
||||
def update_last_login(self, user_id: str) -> bool:
|
||||
"""
|
||||
更新最后登录时间
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
更新成功返回True
|
||||
"""
|
||||
try:
|
||||
return self.update_user(user_id, {
|
||||
"last_login": datetime.now()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update last login for user '{user_id}': {e}")
|
||||
return False
|
||||
|
||||
def deactivate_user(self, user_id: str) -> bool:
|
||||
"""
|
||||
停用用户
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
操作成功返回True
|
||||
"""
|
||||
try:
|
||||
return self.update_user(user_id, {"is_active": False})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to deactivate user '{user_id}': {e}")
|
||||
return False
|
||||
|
||||
def activate_user(self, user_id: str) -> bool:
|
||||
"""
|
||||
激活用户
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
操作成功返回True
|
||||
"""
|
||||
try:
|
||||
return self.update_user(user_id, {"is_active": True})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to activate user '{user_id}': {e}")
|
||||
return False
|
||||
|
||||
def delete_user(self, user_id: str) -> bool:
|
||||
"""
|
||||
删除用户
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
删除成功返回True
|
||||
"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("DELETE FROM users WHERE id = %s", (user_id,))
|
||||
conn.commit()
|
||||
|
||||
if cursor.rowcount > 0:
|
||||
logger.info(f"Deleted user: {user_id}")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"No user found to delete: {user_id}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete user '{user_id}': {e}")
|
||||
return False
|
||||
|
||||
def get_all_users(self, include_inactive: bool = False, limit: int = 100) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取所有用户
|
||||
|
||||
Args:
|
||||
include_inactive: 是否包含非活跃用户
|
||||
limit: 最大返回数量
|
||||
|
||||
Returns:
|
||||
用户列表
|
||||
"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cursor:
|
||||
if include_inactive:
|
||||
sql = "SELECT * FROM users ORDER BY created_at DESC LIMIT %s"
|
||||
cursor.execute(sql, (limit,))
|
||||
else:
|
||||
sql = "SELECT * FROM users WHERE is_active = TRUE ORDER BY created_at DESC LIMIT %s"
|
||||
cursor.execute(sql, (limit,))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
users = []
|
||||
|
||||
for row in rows:
|
||||
user_info = {
|
||||
'id': row['id'],
|
||||
'username': row['username'],
|
||||
'email': row['email'],
|
||||
'display_name': row['display_name'],
|
||||
'avatar_url': row['avatar_url'],
|
||||
'is_active': row['is_active'],
|
||||
'last_login': row['last_login'].isoformat() if row['last_login'] else None,
|
||||
'created_at': row['created_at'].isoformat() if row['created_at'] else None,
|
||||
'updated_at': row['updated_at'].isoformat() if row['updated_at'] else None
|
||||
}
|
||||
# 不包含密码哈希
|
||||
users.append(user_info)
|
||||
|
||||
return users
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get all users: {e}")
|
||||
return []
|
||||
|
||||
def get_user_count(self, include_inactive: bool = False) -> int:
|
||||
"""
|
||||
获取用户数量
|
||||
|
||||
Args:
|
||||
include_inactive: 是否包含非活跃用户
|
||||
|
||||
Returns:
|
||||
用户数量
|
||||
"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
if include_inactive:
|
||||
cursor.execute("SELECT COUNT(*) FROM users")
|
||||
else:
|
||||
cursor.execute("SELECT COUNT(*) FROM users WHERE is_active = TRUE")
|
||||
|
||||
result = cursor.fetchone()
|
||||
return result[0] if result else 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get user count: {e}")
|
||||
return 0
|
||||
|
||||
def search_users(self, query: str, limit: int = 50) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
搜索用户
|
||||
|
||||
Args:
|
||||
query: 搜索关键词(匹配用户名、邮箱、显示名称)
|
||||
limit: 最大返回数量
|
||||
|
||||
Returns:
|
||||
匹配的用户列表
|
||||
"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cursor:
|
||||
search_pattern = f"%{query}%"
|
||||
sql = """
|
||||
SELECT * FROM users
|
||||
WHERE (username ILIKE %s OR email ILIKE %s OR display_name ILIKE %s)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
cursor.execute(sql, (search_pattern, search_pattern, search_pattern, limit))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
users = []
|
||||
|
||||
for row in rows:
|
||||
user_info = {
|
||||
'id': row['id'],
|
||||
'username': row['username'],
|
||||
'email': row['email'],
|
||||
'display_name': row['display_name'],
|
||||
'avatar_url': row['avatar_url'],
|
||||
'is_active': row['is_active'],
|
||||
'last_login': row['last_login'].isoformat() if row['last_login'] else None,
|
||||
'created_at': row['created_at'].isoformat() if row['created_at'] else None,
|
||||
'updated_at': row['updated_at'].isoformat() if row['updated_at'] else None
|
||||
}
|
||||
# 不包含密码哈希
|
||||
users.append(user_info)
|
||||
|
||||
return users
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to search users with query '{query}': {e}")
|
||||
return []
|
||||
|
||||
|
||||
# 创建全局用户表实例
|
||||
user_table = UserTablePostgres()
|
||||
@@ -3,9 +3,53 @@ from python_core.config import settings
|
||||
import httpx
|
||||
from python_core.utils.logger import setup_logger
|
||||
from typing import Dict
|
||||
import time
|
||||
import random
|
||||
from functools import wraps
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
def retry_on_timeout(max_retries=3, base_delay=1.0, max_delay=10.0, backoff_factor=2.0):
|
||||
"""
|
||||
重试装饰器,用于处理网络超时等临时性错误
|
||||
|
||||
Args:
|
||||
max_retries: 最大重试次数
|
||||
base_delay: 基础延迟时间(秒)
|
||||
max_delay: 最大延迟时间(秒)
|
||||
backoff_factor: 退避因子
|
||||
"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except (httpx.ConnectTimeout, httpx.ReadTimeout, httpx.TimeoutException) as e:
|
||||
last_exception = e
|
||||
if attempt == max_retries:
|
||||
logger.error(f"Function {func.__name__} failed after {max_retries} retries: {e}")
|
||||
raise e
|
||||
|
||||
# 计算延迟时间(指数退避 + 随机抖动)
|
||||
delay = min(base_delay * (backoff_factor ** attempt), max_delay)
|
||||
jitter = random.uniform(0, delay * 0.1) # 10% 随机抖动
|
||||
total_delay = delay + jitter
|
||||
|
||||
logger.warning(f"Function {func.__name__} attempt {attempt + 1} failed with timeout: {e}. Retrying in {total_delay:.2f}s...")
|
||||
time.sleep(total_delay)
|
||||
except Exception as e:
|
||||
# 对于非超时错误,直接抛出
|
||||
logger.error(f"Function {func.__name__} failed with non-timeout error: {e}")
|
||||
raise e
|
||||
|
||||
# 如果所有重试都失败了,抛出最后一个异常
|
||||
raise last_exception
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
class Kv:
|
||||
kv: Dict[str, str]
|
||||
def __init__(self):
|
||||
@@ -15,6 +59,7 @@ class Kv:
|
||||
self.base_url = f"https://api.cloudflare.com/client/v4/accounts/{self.cf_account_id}/storage/kv/namespaces/{self.cf_kv_id}"
|
||||
self.headers = {"Authorization": f"Bearer {self.cf_kv_api_token}"}
|
||||
|
||||
@retry_on_timeout(max_retries=3, base_delay=1.0)
|
||||
def get(self, key: str, default=None):
|
||||
"""
|
||||
获取单个键的值
|
||||
@@ -27,7 +72,7 @@ class Kv:
|
||||
键对应的值,如果不存在则返回default
|
||||
"""
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
response = client.get(
|
||||
f"{self.base_url}/values/{key}",
|
||||
headers=self.headers
|
||||
@@ -48,6 +93,7 @@ class Kv:
|
||||
logger.error(f"An unexpected error occurred while getting key '{key}': {e}")
|
||||
raise e
|
||||
|
||||
@retry_on_timeout(max_retries=3, base_delay=1.0)
|
||||
def set(self, key: str, value: str):
|
||||
"""
|
||||
设置单个键值对
|
||||
@@ -62,7 +108,7 @@ class Kv:
|
||||
操作结果
|
||||
"""
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
params = {}
|
||||
|
||||
response = client.put(
|
||||
@@ -83,6 +129,7 @@ class Kv:
|
||||
logger.error(f"An unexpected error occurred while setting key '{key}': {e}")
|
||||
raise e
|
||||
|
||||
@retry_on_timeout(max_retries=3, base_delay=1.0)
|
||||
def remove(self, key: str):
|
||||
"""
|
||||
删除单个键
|
||||
@@ -94,7 +141,7 @@ class Kv:
|
||||
操作结果
|
||||
"""
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
response = client.delete(
|
||||
f"{self.base_url}/values/{key}",
|
||||
headers=self.headers
|
||||
@@ -115,6 +162,7 @@ class Kv:
|
||||
logger.error(f"An unexpected error occurred while removing key '{key}': {e}")
|
||||
raise e
|
||||
|
||||
@retry_on_timeout(max_retries=3, base_delay=1.0)
|
||||
def sets(self, caches: Dict[str, str]):
|
||||
"""
|
||||
批量设置多个键值对
|
||||
@@ -128,7 +176,7 @@ class Kv:
|
||||
操作结果
|
||||
"""
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
with httpx.Client(timeout=60.0) as client: # 批量操作使用更长的超时时间
|
||||
bulk_data = []
|
||||
for key, value in caches.items():
|
||||
item = {
|
||||
@@ -178,6 +226,7 @@ class Kv:
|
||||
logger.error(f"An error occurred while bulk getting keys from cloudflare: {e}")
|
||||
raise e
|
||||
|
||||
@retry_on_timeout(max_retries=3, base_delay=1.0)
|
||||
def removes(self, keys: list[str]):
|
||||
"""
|
||||
批量删除多个键
|
||||
@@ -189,7 +238,7 @@ class Kv:
|
||||
操作结果
|
||||
"""
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
with httpx.Client(timeout=60.0) as client: # 批量操作使用更长的超时时间
|
||||
response = client.post(
|
||||
f"{self.base_url}/bulk/delete",
|
||||
headers=self.headers,
|
||||
@@ -207,6 +256,7 @@ class Kv:
|
||||
logger.error(f"An unexpected error occurred while bulk removing keys: {e}")
|
||||
raise e
|
||||
|
||||
@retry_on_timeout(max_retries=3, base_delay=1.0)
|
||||
def exists(self, key: str) -> bool:
|
||||
"""
|
||||
检查键是否存在
|
||||
@@ -218,7 +268,7 @@ class Kv:
|
||||
如果键存在返回True,否则返回False
|
||||
"""
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
response = client.get(
|
||||
f"{self.base_url}/values/{key}",
|
||||
headers=self.headers
|
||||
@@ -231,6 +281,7 @@ class Kv:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@retry_on_timeout(max_retries=3, base_delay=1.0)
|
||||
def list_keys(self, prefix: str = None, limit: int = 1000, cursor: str = None):
|
||||
"""
|
||||
列出KV存储中的键
|
||||
@@ -244,7 +295,7 @@ class Kv:
|
||||
包含键列表和分页信息的字典
|
||||
"""
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
params = {"limit": limit}
|
||||
if prefix:
|
||||
params["prefix"] = prefix
|
||||
@@ -268,6 +319,7 @@ class Kv:
|
||||
logger.error(f"An unexpected error occurred while listing keys: {e}")
|
||||
raise e
|
||||
|
||||
@retry_on_timeout(max_retries=3, base_delay=1.0)
|
||||
def get_metadata(self, key: str):
|
||||
"""
|
||||
获取键的元数据
|
||||
@@ -279,7 +331,7 @@ class Kv:
|
||||
键的元数据信息
|
||||
"""
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
response = client.get(
|
||||
f"{self.base_url}/metadata/{key}",
|
||||
headers=self.headers
|
||||
|
||||
@@ -32,3 +32,4 @@ json-rpc
|
||||
langchain_core
|
||||
langchain_anthropic
|
||||
PyJWT
|
||||
psycopg2-binary
|
||||
@@ -7,7 +7,7 @@ import re
|
||||
from typing import Optional, Dict, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
from python_core.database.user import user_table
|
||||
from python_core.database.user_postgres import user_table
|
||||
from python_core.utils.jwt_auth import generate_access_token, verify_access_token
|
||||
from python_core.utils.logger import setup_logger
|
||||
|
||||
|
||||
Reference in New Issue
Block a user