Files
QQuiz/backend/utils.py
handsomezhuzhu a01f3540c5 feat: 实现数据库驱动的API配置管理和项目结构重组
## 新功能
- 实现管理后台API配置管理(OpenAI/Anthropic/Qwen)
- API配置保存到数据库,实时生效无需重启
- API密钥遮罩显示(前10位+后4位)
- 完整endpoint URL自动显示

## 后端改进
- 新增 config_service.py 用于加载数据库配置
- LLMService 支持动态配置注入,回退到环境变量
- 更新 exam.py 和 question.py 使用数据库配置
- 扩展 schemas.py 支持所有API配置字段

## 前端改进
- 重写 AdminSettings.jsx 增强UI体验
- API密钥显示/隐藏切换
- 当前使用的提供商可视化标识
- 移除"需要重启"的误导性提示

## 项目结构重组
- 移动所有脚本到 scripts/ 目录
- 移动所有文档到 docs/ 目录
- 清理 Python 缓存文件

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 19:24:12 +08:00

89 lines
2.7 KiB
Python

"""
Utility functions
"""
import hashlib
import re
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
import os
# Password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# JWT settings
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-this")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
def hash_password(password: str) -> str:
"""Hash a password"""
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against a hash"""
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
"""Create a JWT access token"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
print(f"🔑 Creating token with SECRET_KEY (first 20 chars): {SECRET_KEY[:20]}...")
print(f"📦 Token payload: {to_encode}")
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def decode_access_token(token: str) -> Optional[dict]:
"""Decode a JWT access token"""
try:
print(f"🔑 SECRET_KEY (first 20 chars): {SECRET_KEY[:20]}...")
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
print(f"✅ Token decoded successfully: {payload}")
return payload
except JWTError as e:
print(f"❌ JWT decode error: {type(e).__name__}: {str(e)}")
return None
def normalize_content(content: str) -> str:
"""
Normalize content for deduplication.
Removes whitespace, punctuation, and converts to lowercase.
"""
# Remove all whitespace
normalized = re.sub(r'\s+', '', content)
# Remove punctuation
normalized = re.sub(r'[^\w\u4e00-\u9fff]', '', normalized)
# Convert to lowercase
normalized = normalized.lower()
return normalized
def calculate_content_hash(content: str) -> str:
"""
Calculate MD5 hash of normalized content for deduplication.
"""
normalized = normalize_content(content)
return hashlib.md5(normalized.encode('utf-8')).hexdigest()
def get_file_extension(filename: str) -> str:
"""Get file extension from filename"""
return filename.rsplit('.', 1)[-1].lower() if '.' in filename else ''
def is_allowed_file(filename: str) -> bool:
"""Check if file extension is allowed"""
allowed_extensions = {'txt', 'pdf', 'doc', 'docx', 'xlsx', 'xls'}
return get_file_extension(filename) in allowed_extensions