⚒️ 重大重构 LoveACE V2
引入了 mongodb 对数据库进行了一定程度的数据加密 性能改善 代码简化 统一错误模型和响应 使用 apifox 作为文档
This commit is contained in:
114
loveace/config/logger.py
Normal file
114
loveace/config/logger.py
Normal file
@@ -0,0 +1,114 @@
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from loveace.config.manager import config_manager
|
||||
from loveace.utils.richuru_hook import install
|
||||
|
||||
|
||||
def setup_logger():
|
||||
"""根据配置文件设置loguru日志"""
|
||||
|
||||
settings = config_manager.get_settings()
|
||||
log_config = settings.log
|
||||
|
||||
# 移除默认的logger配置
|
||||
logger.remove()
|
||||
# 安装 richuru 并配置更详细的堆栈跟踪信息
|
||||
install()
|
||||
# 确保日志目录存在
|
||||
log_dir = Path(log_config.file_path).parent
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 设置主日志文件 - 带有详细路径信息
|
||||
logger.add(
|
||||
log_config.file_path,
|
||||
level=log_config.level.value,
|
||||
rotation=log_config.rotation,
|
||||
retention=log_config.retention,
|
||||
compression=log_config.compression,
|
||||
backtrace=log_config.backtrace,
|
||||
diagnose=log_config.diagnose,
|
||||
# 自定义格式,显示完整的文件路径和行号
|
||||
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} | {message}",
|
||||
)
|
||||
logger.info("日志系统初始化完成")
|
||||
|
||||
|
||||
def get_logger():
|
||||
"""获取配置好的logger实例"""
|
||||
return logger
|
||||
|
||||
|
||||
class LoggerMixin:
|
||||
"""用户日志混合类"""
|
||||
|
||||
user_id: str = ""
|
||||
trace_id: str = ""
|
||||
|
||||
def __init__(self, user_id: str = "", trace_id: str = ""):
|
||||
self.user_id = user_id
|
||||
self.trace_id = trace_id
|
||||
|
||||
def _build_message(self, message: str):
|
||||
if self.user_id and self.trace_id:
|
||||
return f"[{self.user_id}] [{self.trace_id}] {message}"
|
||||
|
||||
elif self.user_id:
|
||||
return f"[{self.user_id}] {message}"
|
||||
|
||||
elif self.trace_id:
|
||||
return f"[{self.trace_id}] {message}"
|
||||
|
||||
else:
|
||||
return message
|
||||
|
||||
def _build_alt_message(self, alt: str):
|
||||
if self.user_id and self.trace_id:
|
||||
return f"[bold green][{self.user_id}][/bold green] [bold blue][{self.trace_id}][/bold blue] {alt}"
|
||||
elif self.user_id:
|
||||
return f"[bold green][{self.user_id}][/bold green] {alt}"
|
||||
elif self.trace_id:
|
||||
return f"[bold blue][{self.trace_id}][/bold blue] {alt}"
|
||||
else:
|
||||
return alt
|
||||
|
||||
def info(self, message: str, alt: str = ""):
|
||||
logger.opt(depth=1).info(
|
||||
self._build_message(message),
|
||||
alt=self._build_alt_message(alt if alt else message),
|
||||
)
|
||||
|
||||
def debug(self, message: str, alt: str = ""):
|
||||
logger.opt(depth=1).debug(
|
||||
self._build_message(message),
|
||||
alt=self._build_alt_message(alt if alt else message),
|
||||
)
|
||||
|
||||
def warning(self, message: str, alt: str = ""):
|
||||
logger.opt(depth=1).warning(
|
||||
self._build_message(message),
|
||||
alt=self._build_alt_message(alt if alt else message),
|
||||
)
|
||||
|
||||
def error(self, message: str, alt: str = ""):
|
||||
logger.opt(depth=1).error(
|
||||
self._build_message(message),
|
||||
alt=self._build_alt_message(alt if alt else message),
|
||||
)
|
||||
|
||||
def success(self, message: str, alt: str = ""):
|
||||
logger.opt(depth=1).success(
|
||||
self._build_message(message),
|
||||
alt=self._build_alt_message(alt if alt else message),
|
||||
)
|
||||
|
||||
def exception(self, e: Exception):
|
||||
logger.opt(depth=1).exception(e)
|
||||
|
||||
|
||||
def get_user_logger(user_id: str):
|
||||
return LoggerMixin(user_id)
|
||||
|
||||
|
||||
setup_logger()
|
||||
173
loveace/config/manager.py
Normal file
173
loveace/config/manager.py
Normal file
@@ -0,0 +1,173 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import ValidationError
|
||||
|
||||
from loveace.config.settings import Settings
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""配置文件管理器"""
|
||||
|
||||
def __init__(self, config_file: str = "config.json"):
|
||||
self.config_file = Path(config_file)
|
||||
self._settings: Optional[Settings] = None
|
||||
self._ensure_config_dir()
|
||||
|
||||
def _ensure_config_dir(self):
|
||||
"""确保配置文件目录存在"""
|
||||
self.config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _create_default_config(self) -> Settings:
|
||||
"""创建默认配置"""
|
||||
logger.info("正在创建默认配置文件...")
|
||||
return Settings()
|
||||
|
||||
def _save_config(self, settings: Settings):
|
||||
"""保存配置到文件"""
|
||||
try:
|
||||
config_dict = settings.dict()
|
||||
with open(self.config_file, "w", encoding="utf-8") as f:
|
||||
json.dump(config_dict, f, indent=2, ensure_ascii=False)
|
||||
logger.info(f"配置已保存到 {self.config_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"保存配置文件失败: {e}")
|
||||
raise
|
||||
|
||||
def _load_config(self) -> Settings:
|
||||
"""从文件加载配置"""
|
||||
if not self.config_file.exists():
|
||||
logger.warning(f"配置文件 {self.config_file} 不存在,将创建默认配置")
|
||||
settings = self._create_default_config()
|
||||
self._save_config(settings)
|
||||
return settings
|
||||
|
||||
try:
|
||||
with open(self.config_file, "r", encoding="utf-8") as f:
|
||||
config_data = json.load(f)
|
||||
|
||||
# 验证并创建Settings对象
|
||||
settings = Settings(**config_data)
|
||||
logger.info(f"成功加载配置文件: {self.config_file}")
|
||||
return settings
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"配置文件JSON格式错误: {e}")
|
||||
raise
|
||||
except ValidationError as e:
|
||||
logger.error(f"配置文件验证失败: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"加载配置文件失败: {e}")
|
||||
raise
|
||||
|
||||
def get_settings(self) -> Settings:
|
||||
"""获取配置设置"""
|
||||
if self._settings is None:
|
||||
self._settings = self._load_config()
|
||||
return self._settings
|
||||
|
||||
def reload_config(self) -> Settings:
|
||||
"""重新加载配置"""
|
||||
logger.info("正在重新加载配置...")
|
||||
self._settings = self._load_config()
|
||||
return self._settings
|
||||
|
||||
def update_config(self, **kwargs) -> Settings:
|
||||
"""更新配置"""
|
||||
settings = self.get_settings()
|
||||
|
||||
# 创建新的配置字典
|
||||
config_dict = settings.dict()
|
||||
|
||||
# 更新指定的配置项
|
||||
for key, value in kwargs.items():
|
||||
if "." in key:
|
||||
# 支持嵌套键,如 'database.url'
|
||||
keys = key.split(".")
|
||||
current = config_dict
|
||||
for k in keys[:-1]:
|
||||
if k not in current:
|
||||
current[k] = {}
|
||||
current = current[k]
|
||||
current[keys[-1]] = value
|
||||
else:
|
||||
config_dict[key] = value
|
||||
|
||||
try:
|
||||
# 验证更新后的配置
|
||||
new_settings = Settings(**config_dict)
|
||||
self._save_config(new_settings)
|
||||
self._settings = new_settings
|
||||
logger.info("配置更新成功")
|
||||
return new_settings
|
||||
except ValidationError as e:
|
||||
logger.error(f"配置更新失败,验证错误: {e}")
|
||||
raise
|
||||
|
||||
def validate_config(self) -> bool:
|
||||
"""验证配置完整性"""
|
||||
try:
|
||||
settings = self.get_settings()
|
||||
|
||||
# 检查关键配置项
|
||||
issues = []
|
||||
|
||||
# 检查数据库配置
|
||||
if not settings.database.url:
|
||||
issues.append("数据库URL未配置")
|
||||
|
||||
# 检查S3配置(如果需要使用)
|
||||
if settings.s3.bucket_name and not settings.s3.access_key_id:
|
||||
issues.append("S3配置不完整:缺少access_key_id")
|
||||
if settings.s3.bucket_name and not settings.s3.secret_access_key:
|
||||
issues.append("S3配置不完整:缺少secret_access_key")
|
||||
|
||||
# 检查日志配置
|
||||
log_dir = Path(settings.log.file_path).parent
|
||||
if not log_dir.exists():
|
||||
try:
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"创建日志目录: {log_dir}")
|
||||
except Exception as e:
|
||||
issues.append(f"无法创建日志目录 {log_dir}: {e}")
|
||||
|
||||
if issues:
|
||||
logger.warning("配置验证发现问题:")
|
||||
for issue in issues:
|
||||
logger.warning(f" - {issue}")
|
||||
return False
|
||||
|
||||
logger.info("配置验证通过")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"配置验证失败: {e}")
|
||||
return False
|
||||
|
||||
def get_config_summary(self) -> Dict[str, Any]:
|
||||
"""获取配置摘要(隐藏敏感信息)"""
|
||||
settings = self.get_settings()
|
||||
config_dict = settings.dict()
|
||||
|
||||
# 隐藏敏感信息
|
||||
sensitive_keys = ["database.url", "s3.access_key_id", "s3.secret_access_key"]
|
||||
|
||||
def hide_sensitive(data: Dict[str, Any], keys: list, prefix: str = ""):
|
||||
for key, value in data.items():
|
||||
current_key = f"{prefix}.{key}" if prefix else key
|
||||
if current_key in sensitive_keys:
|
||||
if isinstance(value, str) and value:
|
||||
data[key] = value[:8] + "..." if len(value) > 8 else "***"
|
||||
elif isinstance(value, dict):
|
||||
hide_sensitive(value, keys, current_key)
|
||||
|
||||
summary = config_dict.copy()
|
||||
hide_sensitive(summary, sensitive_keys)
|
||||
return summary
|
||||
|
||||
|
||||
# 全局配置管理器实例
|
||||
config_manager = ConfigManager()
|
||||
194
loveace/config/settings.py
Normal file
194
loveace/config/settings.py
Normal file
@@ -0,0 +1,194 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class LogLevel(str, Enum):
|
||||
"""日志级别枚举"""
|
||||
|
||||
TRACE = "TRACE"
|
||||
DEBUG = "DEBUG"
|
||||
INFO = "INFO"
|
||||
SUCCESS = "SUCCESS"
|
||||
WARNING = "WARNING"
|
||||
ERROR = "ERROR"
|
||||
CRITICAL = "CRITICAL"
|
||||
|
||||
|
||||
class DatabaseConfig(BaseModel):
|
||||
"""数据库配置"""
|
||||
|
||||
url: str = Field(
|
||||
default="mysql+aiomysql://root:123456@localhost:3306/loveac",
|
||||
description="数据库连接URL",
|
||||
)
|
||||
echo: bool = Field(default=False, description="是否启用SQL日志")
|
||||
pool_size: int = Field(default=10, description="连接池大小")
|
||||
max_overflow: int = Field(default=20, description="连接池最大溢出")
|
||||
pool_timeout: int = Field(default=30, description="连接池超时时间(秒)")
|
||||
pool_recycle: int = Field(default=3600, description="连接回收时间(秒)")
|
||||
|
||||
|
||||
class ISIMConfig(BaseModel):
|
||||
"""ISIM后勤电费系统配置"""
|
||||
|
||||
base_url: str = Field(
|
||||
default="http://hqkd-aufe-edu-cn.vpn2.aufe.edu.cn",
|
||||
description="ISIM系统基础URL",
|
||||
)
|
||||
room_cache_path: str = Field(
|
||||
default="data/isim_rooms.json", description="寝室信息缓存路径"
|
||||
)
|
||||
room_cache_expire: int = Field(
|
||||
default=86400, description="寝室信息刷新间隔(秒)"
|
||||
) # 默认24小时刷新一次
|
||||
session_timeout: int = Field(default=1800, description="会话超时时间(秒)")
|
||||
retry_times: int = Field(default=3, description="请求重试次数")
|
||||
|
||||
|
||||
class AUFEConfig(BaseModel):
|
||||
"""AUFE连接配置"""
|
||||
|
||||
default_timeout: int = Field(default=30, description="默认超时时间(秒)")
|
||||
max_retries: int = Field(default=3, description="最大重试次数")
|
||||
max_reconnect_retries: int = Field(default=2, description="最大重连次数")
|
||||
activity_timeout: int = Field(default=300, description="活动超时时间(秒)")
|
||||
monitor_interval: int = Field(default=60, description="监控间隔(秒)")
|
||||
retry_base_delay: float = Field(default=1.0, description="重试基础延迟(秒)")
|
||||
retry_max_delay: float = Field(default=60.0, description="重试最大延迟(秒)")
|
||||
retry_exponential_base: float = Field(default=2, description="重试指数基数")
|
||||
server_url: str = Field(
|
||||
default="https://vpn.aufe.edu.cn", description="AUFE服务器URL"
|
||||
)
|
||||
ec_check_url: str = Field(
|
||||
default="http://txzx-aufe-edu-cn-s.vpn2.aufe.edu.cn:8118/dzzy/list.htm",
|
||||
description="EC检查URL",
|
||||
)
|
||||
|
||||
# UAAP配置
|
||||
uaap_base_url: str = Field(
|
||||
default="http://uaap-aufe-edu-cn.vpn2.aufe.edu.cn:8118/cas",
|
||||
description="UAAP基础URL",
|
||||
)
|
||||
uaap_login_url: str = Field(
|
||||
default="http://uaap-aufe-edu-cn.vpn2.aufe.edu.cn:8118/cas/login?service=http%3A%2F%2Fjwcxk2.aufe.edu.cn%2Fj_spring_cas_security_check",
|
||||
description="UAAP登录URL",
|
||||
)
|
||||
uaap_check_url: str = Field(
|
||||
default="http://jwcxk2-aufe-edu-cn.vpn2.aufe.edu.cn:8118/",
|
||||
description="UAAP检查链接",
|
||||
)
|
||||
|
||||
# 默认请求头
|
||||
default_headers: Dict[str, str] = Field(
|
||||
default_factory=lambda: {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
||||
},
|
||||
description="默认请求头",
|
||||
)
|
||||
|
||||
|
||||
class RedisConfig(BaseModel):
|
||||
"""Redis客户端配置"""
|
||||
|
||||
host: str = Field(default="localhost", description="Redis主机地址")
|
||||
port: int = Field(default=6379, description="Redis端口")
|
||||
db: int = Field(default=0, description="Redis数据库编号")
|
||||
password: Optional[str] = Field(default=None, description="Redis密码")
|
||||
encoding: str = Field(default="utf-8", description="字符编码")
|
||||
decode_responses: bool = Field(default=True, description="是否自动解码响应")
|
||||
max_connections: int = Field(default=50, description="连接池最大连接数")
|
||||
socket_keepalive: bool = Field(default=True, description="是否启用socket保活")
|
||||
socket_keepalive_options: Optional[Dict[str, Any]] = Field(
|
||||
default=None, description="Socket保活选项"
|
||||
)
|
||||
health_check_interval: int = Field(default=30, description="健康检查间隔(秒)")
|
||||
retry_on_timeout: bool = Field(default=True, description="超时时是否重试")
|
||||
|
||||
|
||||
class S3Config(BaseModel):
|
||||
"""S3客户端配置"""
|
||||
|
||||
access_key_id: str = Field(default="", description="S3访问密钥ID")
|
||||
secret_access_key: str = Field(default="", description="S3秘密访问密钥")
|
||||
endpoint_url: str = Field(default="", description="S3终端节点URL")
|
||||
region_name: str = Field(default="us-east-1", description="S3区域名称")
|
||||
bucket_name: str = Field(default="", description="默认存储桶名称")
|
||||
use_ssl: bool = Field(default=True, description="是否使用SSL")
|
||||
signature_version: str = Field(default="s3v4", description="签名版本")
|
||||
addressing_style: str = Field(
|
||||
default="auto", description="地址风格(auto, path, virtual)"
|
||||
)
|
||||
|
||||
@field_validator("access_key_id", "secret_access_key", "bucket_name")
|
||||
@classmethod
|
||||
def validate_required_fields(cls, v):
|
||||
"""验证必填字段"""
|
||||
# 允许为空,但应在运行时检查
|
||||
return v
|
||||
|
||||
|
||||
class LogConfig(BaseModel):
|
||||
"""日志配置"""
|
||||
|
||||
level: LogLevel = Field(default=LogLevel.INFO, description="日志级别")
|
||||
file_path: str = Field(default="logs/app.log", description="日志文件路径")
|
||||
rotation: str = Field(default="10 MB", description="日志轮转大小")
|
||||
retention: str = Field(default="30 days", description="日志保留时间")
|
||||
compression: str = Field(default="zip", description="日志压缩格式")
|
||||
backtrace: bool = Field(default=True, description="是否启用回溯")
|
||||
diagnose: bool = Field(default=True, description="是否启用诊断")
|
||||
console_output: bool = Field(default=True, description="是否输出到控制台")
|
||||
|
||||
|
||||
class AppConfig(BaseModel):
|
||||
"""应用程序配置"""
|
||||
|
||||
title: str = Field(default="LoveACE API", description="应用标题")
|
||||
description: str = Field(default="LoveACE API", description="应用描述")
|
||||
version: str = Field(default="1.0.0", description="应用版本")
|
||||
debug: bool = Field(default=False, description="是否启用调试模式")
|
||||
|
||||
# CORS配置
|
||||
cors_allow_origins: List[str] = Field(
|
||||
default_factory=lambda: ["*"], description="允许的CORS来源"
|
||||
)
|
||||
cors_allow_credentials: bool = Field(default=True, description="是否允许CORS凭据")
|
||||
cors_allow_methods: List[str] = Field(
|
||||
default_factory=lambda: ["*"], description="允许的CORS方法"
|
||||
)
|
||||
cors_allow_headers: List[str] = Field(
|
||||
default_factory=lambda: ["*"], description="允许的CORS头部"
|
||||
)
|
||||
|
||||
# 服务器配置
|
||||
host: str = Field(default="0.0.0.0", description="服务器主机")
|
||||
port: int = Field(default=8000, description="服务器端口")
|
||||
workers: int = Field(default=1, description="工作进程数")
|
||||
|
||||
# 安全配置
|
||||
rsa_private_key_path: str = Field(
|
||||
default="private_key.hex", description="RSA私钥路径"
|
||||
)
|
||||
rsa_protect_key_path: str = Field(
|
||||
default="data/keys/", description="RSA保护密钥存储路径"
|
||||
)
|
||||
|
||||
|
||||
class Settings(BaseModel):
|
||||
"""主配置类"""
|
||||
|
||||
database: DatabaseConfig = Field(default_factory=DatabaseConfig)
|
||||
redis: RedisConfig = Field(default_factory=RedisConfig)
|
||||
aufe: AUFEConfig = Field(default_factory=AUFEConfig)
|
||||
isim: ISIMConfig = Field(default_factory=ISIMConfig)
|
||||
s3: S3Config = Field(default_factory=S3Config)
|
||||
log: LogConfig = Field(default_factory=LogConfig)
|
||||
app: AppConfig = Field(default_factory=AppConfig)
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
# 为枚举类型提供JSON编码器
|
||||
LogLevel: lambda v: v.value
|
||||
}
|
||||
Reference in New Issue
Block a user