This commit is contained in:
root
2025-07-12 21:56:16 +08:00
parent 50106f548d
commit 0b6958b7b6
6 changed files with 753 additions and 11 deletions

View 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()

View File

@@ -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"

View 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()

View File

@@ -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

View File

@@ -32,3 +32,4 @@ json-rpc
langchain_core
langchain_anthropic
PyJWT
psycopg2-binary

View File

@@ -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