modify: 代码注释

This commit is contained in:
2026-04-26 21:57:17 +08:00
parent d13ad1f907
commit 3fbd7764af
32 changed files with 2029 additions and 164 deletions
+22 -5
View File
@@ -12,13 +12,17 @@ import server
use_shell = platform.system() == "Windows" use_shell = platform.system() == "Windows"
def app_run(app_config: dict, default_config: dict): def app_run(app_config: dict, default_config: dict):
""" """
应用运行函数 - 适配模块银行环境 应用运行函数 - 适配模块银行环境
Args: Args:
app_config: 应用配置 app_config: 应用配置字典,包含服务器配置、环境设置等
default_config: 默认配置 default_config: 默认配置字典,包含应用名称、版本等基础信息
Returns:
None: 此函数不返回值,直接启动服务器并阻塞运行
""" """
from app.core.logger import get_logger # type: ignore from app.core.logger import get_logger # type: ignore
@@ -48,8 +52,10 @@ def app_run(app_config: dict, default_config: dict):
logger.info(f"👥 Workers: {workers}") logger.info(f"👥 Workers: {workers}")
logger.info("=" * 50) logger.info("=" * 50)
# 根据操作系统选择不同的服务器启动方式
if use_shell: if use_shell:
import uvicorn import uvicorn
uvicorn.run( uvicorn.run(
"server:app", "server:app",
host=host, host=host,
@@ -62,16 +68,27 @@ def app_run(app_config: dict, default_config: dict):
) )
else: else:
from gunicorn.app.wsgiapp import run from gunicorn.app.wsgiapp import run
sys.argv = [ sys.argv = [
"gunicorn", "gunicorn",
"server:app", "server:app",
"-k", "uvicorn.workers.UvicornWorker", "-k",
"-w", str(workers), "uvicorn.workers.UvicornWorker",
"-b", f"{host}:{port}" "-w",
str(workers),
"-b",
f"{host}:{port}",
] ]
run() run()
def main(): def main():
"""
主函数入口 - 初始化多进程支持并启动应用
Returns:
None: 此函数不返回值,调用launch函数启动应用
"""
freeze_support() freeze_support()
launch(app_run, "production") launch(app_run, "production")
+6 -4
View File
@@ -3,17 +3,19 @@ import sys
from utils.launch import init_module_bank from utils.launch import init_module_bank
from utils.config import ConfigLoader from utils.config import ConfigLoader
# 加载配置文件并初始化模块银行
config = ConfigLoader("config.toml") config = ConfigLoader("config.toml")
init_module_bank(config) init_module_bank(config)
# 从数据库导入真正的 app 模块 # 从数据库动态导入 app 模块
# 该模块存储在 SQLite 数据库中,通过自定义导入机制加载
try: try:
import app # type: ignore # 此时会从 SQLite 数据库加载 import app # type: ignore
app_instance = app.app # 获取 FastAPI 实例 app_instance = app.app
except ImportError as e: except ImportError as e:
print(f"无法从数据库导入 app 模块: {e}") print(f"无法从数据库导入 app 模块: {e}")
sys.exit(1) sys.exit(1)
# 暴露 ASGI 应用变量,供 uvicorn 使用 # 暴露 ASGI 应用实例,供 uvicorn 服务器使用
app = app_instance app = app_instance
+105 -9
View File
@@ -6,6 +6,7 @@ import sys
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
# 根据Python版本选择合适的TOML解析库
if sys.version_info >= (3, 11): if sys.version_info >= (3, 11):
import tomllib import tomllib
else: else:
@@ -13,7 +14,15 @@ else:
class ConfigLoader: class ConfigLoader:
"""TOML配置文件加载器""" """TOML配置文件加载器
提供配置文件的加载、访问和管理功能。支持自动查找配置文件、
点号分隔的嵌套键访问、配置节获取以及配置重载等功能。
Attributes:
config_path: 配置文件的路径
_config: 存储解析后的配置数据
"""
def __init__(self, config_path: Optional[str] = None): def __init__(self, config_path: Optional[str] = None):
""" """
@@ -21,6 +30,9 @@ class ConfigLoader:
Args: Args:
config_path: 配置文件路径,如果为None则自动查找 config_path: 配置文件路径,如果为None则自动查找
Raises:
FileNotFoundError: 当无法找到配置文件时抛出异常
""" """
if config_path is None: if config_path is None:
# 自动查找配置文件 # 自动查找配置文件
@@ -31,7 +43,21 @@ class ConfigLoader:
self.load() self.load()
def _find_config_file(self) -> str: def _find_config_file(self) -> str:
"""自动查找配置文件""" """
自动查找配置文件
按照预定义的优先级顺序搜索配置文件:
1. 当前目录下的config.toml
2. config子目录下的config.toml
3. 用户配置目录~/.config/myapp/config.toml
4. 系统配置目录/etc/myapp/config.toml
Returns:
str: 找到的第一个配置文件路径
Raises:
FileNotFoundError: 当所有可能的位置都未找到配置文件时抛出异常
"""
possible_paths = [ possible_paths = [
"config.toml", "config.toml",
"config/config.toml", "config/config.toml",
@@ -47,7 +73,15 @@ class ConfigLoader:
raise FileNotFoundError("未找到配置文件") raise FileNotFoundError("未找到配置文件")
def load(self) -> None: def load(self) -> None:
"""加载配置文件""" """
加载配置文件
读取并解析TOML格式的配置文件,将配置数据存储到内部字典中。
Raises:
FileNotFoundError: 当配置文件不存在时抛出异常
tomllib.TOMLDecodeError: 当配置文件格式不正确时抛出异常
"""
if not self.config_path.exists(): if not self.config_path.exists():
raise FileNotFoundError(f"配置文件不存在: {self.config_path}") raise FileNotFoundError(f"配置文件不存在: {self.config_path}")
@@ -55,7 +89,20 @@ class ConfigLoader:
self._config = tomllib.loads(f.read()) self._config = tomllib.loads(f.read())
def get(self, key: str, default: Any = None) -> Any: def get(self, key: str, default: Any = None) -> Any:
"""获取配置值,支持点号分隔的路径""" """
获取配置值,支持点号分隔的路径
通过点号分隔的键路径访问嵌套的配置值。例如:
- "database.host" 访问 {"database": {"host": "localhost"}}
- "server.port" 访问 {"server": {"port": 8080}}
Args:
key: 配置键,支持点号分隔的嵌套路径
default: 当键不存在时返回的默认值,默认为None
Returns:
Any: 配置值,如果键不存在则返回默认值
"""
keys = key.split(".") keys = key.split(".")
value = self._config value = self._config
@@ -67,22 +114,71 @@ class ConfigLoader:
return default return default
def get_section(self, section: str) -> Dict[str, Any]: def get_section(self, section: str) -> Dict[str, Any]:
"""获取整个配置节""" """
获取整个配置节
返回指定配置节的完整字典内容,适用于需要访问某个配置节下
所有配置项的场景。
Args:
section: 配置节名称
Returns:
Dict[str, Any]: 配置节的内容字典,如果节不存在则返回空字典
"""
return self._config.get(section, {}) return self._config.get(section, {})
def reload(self) -> None: def reload(self) -> None:
"""重新加载配置文件""" """
重新加载配置文件
从磁盘重新读取配置文件并更新内部配置数据。
适用于配置文件在运行时被修改的场景。
Raises:
FileNotFoundError: 当配置文件不存在时抛出异常
"""
self.load() self.load()
def __getitem__(self, key: str) -> Any: def __getitem__(self, key: str) -> Any:
"""支持字典式访问""" """
支持字典式访问
允许使用方括号语法访问配置值,如:config["database.host"]
Args:
key: 配置键
Returns:
Any: 配置值
Raises:
KeyError: 当键不存在且无默认值时抛出异常(通过get方法处理)
"""
return self.get(key) return self.get(key)
def __contains__(self, key: str) -> bool: def __contains__(self, key: str) -> bool:
"""检查配置是否存在""" """
检查配置是否存在
支持使用in操作符检查配置键是否存在,如:"database.host" in config
Args:
key: 要检查的配置键
Returns:
bool: 如果配置键存在且值不为None则返回True,否则返回False
"""
return self.get(key) is not None return self.get(key) is not None
@property @property
def all(self) -> Dict[str, Any]: def all(self) -> Dict[str, Any]:
"""获取所有配置""" """
获取所有配置
返回完整的配置字典,适用于需要遍历或导出所有配置的场景。
Returns:
Dict[str, Any]: 包含所有配置的字典
"""
return self._config return self._config
+30
View File
@@ -7,14 +7,29 @@ key = None
def init_module_bank(config): def init_module_bank(config):
"""
初始化模块银行系统,将指定路径下的Python模块打包并安装导入器。
该函数会验证所有模块路径的有效性,创建PythonToSQLite打包器实例,
列出所有可用模块,并为每个打包器安装导入器以支持模块的动态加载。
Args:
config (dict): 配置字典,必须包含 'production' -> 'module_system' -> 'path' 键,
其值为模块路径列表
Raises:
ValueError: 当配置的模块路径中有任何一个不存在时抛出异常
"""
MODULE_BANK_PATH_LIST = config["production"]["module_system"]["path"] MODULE_BANK_PATH_LIST = config["production"]["module_system"]["path"]
for module_path in MODULE_BANK_PATH_LIST: for module_path in MODULE_BANK_PATH_LIST:
if not os.path.exists(module_path): if not os.path.exists(module_path):
raise ValueError(f"模块路径不存在: {module_path}") raise ValueError(f"模块路径不存在: {module_path}")
# 为每个模块路径创建PythonToSQLite打包器实例
packer_list = [ packer_list = [
PythonToSQLite(module_path, key=key) for module_path in MODULE_BANK_PATH_LIST PythonToSQLite(module_path, key=key) for module_path in MODULE_BANK_PATH_LIST
] ]
# 遍历所有打包器,列出模块信息并安装导入器
for packer in packer_list: for packer in packer_list:
modules = packer.list_modules() modules = packer.list_modules()
for module in modules: for module in modules:
@@ -23,6 +38,21 @@ def init_module_bank(config):
def launch(run_fun: Callable[[dict, dict], None], run_mode: str): def launch(run_fun: Callable[[dict, dict], None], run_mode: str):
"""
启动应用程序,加载配置并执行指定的运行函数。
该函数从config.toml文件中加载配置,根据运行模式获取对应的应用配置和默认配置,
然后调用传入的运行函数执行应用程序逻辑。
Args:
run_fun (Callable[[dict, dict], None]): 运行函数,接收两个参数:
- app_config (dict): 当前运行模式的配置
- default_config (dict): 默认配置
run_mode (str): 运行模式,用于从配置文件中获取对应模式的配置项
Returns:
None
"""
config = ConfigLoader("config.toml") config = ConfigLoader("config.toml")
# 获取对应运行模式的配置 # 获取对应运行模式的配置
+34 -9
View File
@@ -4,32 +4,57 @@ from .app.core.logger import get_logger
def serve(): def serve():
"""
使用 uvicorn 启动服务
从配置文件中加载服务器配置和日志配置,然后使用 uvicorn 启动应用服务。
该函数适用于开发环境或单进程部署场景。
Returns:
None: 该函数不返回值,会阻塞运行直到服务停止
"""
config = load_config() config = load_config()
env = config["default"]["env"] env = config["default"]["env"]
server_config = config[env]["server"] server_config = config[env]["server"]
log_config = config[env]["logging"] log_config = config[env]["logging"]
logget = get_logger("app") logger = get_logger("app")
logget.info(f"启动服务: {server_config}") logger.info(f"启动服务: {server_config}")
logget.info(f"启动日志: {log_config}") logger.info(f"启动日志: {log_config}")
uvicorn.run("chongming.app:app", **server_config) uvicorn.run("chongming.app:app", **server_config)
def gunicorn_serve(): def gunicorn_serve():
"""使用 gunicorn 启动服务""" """
使用 gunicorn 启动服务
通过 gunicorn 作为进程管理器启动应用,使用 UvicornWorker 作为 worker 类型,
支持多进程部署,适用于生产环境。
从配置文件中读取服务器配置,动态设置 gunicorn 的 worker 数量、绑定地址和端口等参数。
Returns:
None: 该函数不返回值,会阻塞运行直到服务停止
"""
import sys import sys
from gunicorn.app.wsgiapp import run from gunicorn.app.wsgiapp import run
config = load_config() config = load_config()
env = config["default"]["env"] env = config["default"]["env"]
server_config = config[env]["server"] server_config = config[env]["server"]
# 设置 gunicorn 参数 # 构建 gunicorn 命令行参数并启动服务
sys.argv = [ sys.argv = [
"gunicorn", "gunicorn",
"chongming.app:app", "chongming.app:app",
"-k", "uvicorn.workers.UvicornWorker", "-k",
"-w", f"{server_config.get('workers', 4)}", "uvicorn.workers.UvicornWorker",
"-b", f"{server_config.get('host', '0.0.0.0')}:{server_config.get('port', 8000)}" "-w",
f"{server_config.get('workers', 4)}",
"-b",
f"{server_config.get('host', '0.0.0.0')}:{server_config.get('port', 8000)}",
] ]
run() run()
if __name__ == "__main__": if __name__ == "__main__":
serve() serve()
+56 -17
View File
@@ -31,9 +31,28 @@ except KeyError: # pragma: no cover
raise ValueError("配置不存在") raise ValueError("配置不存在")
# 生命周期管理
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
"""
应用生命周期管理函数,负责应用的启动和关闭时的资源初始化与清理。
在应用启动时:
- 初始化JWT缓存
- 创建异步数据库引擎和会话工厂
- 在开发环境下创建数据库表
- 初始化定时任务服务
在应用关闭时:
- 关闭定时任务服务
- 释放数据库引擎资源
- 关闭缓存连接
参数:
app: FastAPI应用实例
Yields:
None: 应用运行期间保持上下文激活
"""
from plugins import hello from plugins import hello
from plugins.jwt.jwt_cache import get_jwt_cache from plugins.jwt.jwt_cache import get_jwt_cache
@@ -46,17 +65,14 @@ async def lifespan(app: FastAPI):
if database_config is None: if database_config is None:
raise ValueError("配置不存在") raise ValueError("配置不存在")
# 启动时:创建引擎和会话工厂,初始化数据库表
engine = create_async_engine(database["url"], **database_config) engine = create_async_engine(database["url"], **database_config)
async_session_maker = async_sessionmaker( async_session_maker = async_sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False engine, class_=AsyncSession, expire_on_commit=False
) )
# 将资源存储到 app.state
app.state.engine = engine app.state.engine = engine
app.state.async_session_maker = async_session_maker app.state.async_session_maker = async_session_maker
# 创建表(异步方式)
if env == "development": if env == "development":
async with engine.begin() as conn: async with engine.begin() as conn:
if database_type == "sqlite": if database_type == "sqlite":
@@ -72,22 +88,19 @@ async def lifespan(app: FastAPI):
if "already exists" in str(e): if "already exists" in str(e):
raise raise
# 启动时:初始化任务服务
task_service = get_task_service_instance( task_service = get_task_service_instance(
config, get_logger("scheduler"), async_session_maker config, get_logger("scheduler"), async_session_maker
) )
app.state.task_service = task_service app.state.task_service = task_service
await task_service.start(init_tasks_callback) await task_service.start(init_tasks_callback)
yield # 应用运行期间 yield
# 关闭时:释放引擎
await task_service.shutdown(wait=True) await task_service.shutdown(wait=True)
await engine.dispose() await engine.dispose()
cache.close() cache.close()
# 创建 FastAPI 应用
cors = config[env].get("cors", {}) cors = config[env].get("cors", {})
app = FastAPI( app = FastAPI(
title=config["default"]["app"]["name"], title=config["default"]["app"]["name"],
@@ -99,15 +112,29 @@ app = FastAPI(
lifespan=lifespan, lifespan=lifespan,
) )
# 添加 CORS 中间件
app.add_middleware(CORSMiddleware, **cors) app.add_middleware(CORSMiddleware, **cors)
# 添加路由
app.include_router(api_router, prefix=config["default"]["prefix"]) app.include_router(api_router, prefix=config["default"]["prefix"])
@app.middleware("http") @app.middleware("http")
async def access_log_middleware(request: Request, call_next): async def access_log_middleware(request: Request, call_next):
"""
HTTP访问日志中间件,记录每个请求的详细信息。
记录的信息包括:
- 请求方法和路径
- 响应状态码
- 请求处理时长(毫秒)
- 客户端IP地址
参数:
request: FastAPI请求对象
call_next: 下一个中间件或路由处理函数
返回:
Response: 处理后的响应对象
"""
start = time.perf_counter() start = time.perf_counter()
response = await call_next(request) response = await call_next(request)
duration = (time.perf_counter() - start) * 1000 duration = (time.perf_counter() - start) * 1000
@@ -116,15 +143,19 @@ async def access_log_middleware(request: Request, call_next):
logger.info( logger.info(
f"{request.method} {request.url.path} status={response.status_code} " f"{request.method} {request.url.path} status={response.status_code} "
f"duration={duration:.2f}ms client={client}", f"duration={duration:.2f}ms client={client}",
extra={"status_code": response.status_code}, # ← 关键 extra={"status_code": response.status_code},
) )
return response return response
# 根路由
@app.get("/", summary="根路径") @app.get("/", summary="根路径")
async def root(): async def root():
"""根路径,返回应用信息""" """
根路径端点,返回应用的基本信息。
返回:
dict: 包含应用名称、版本、文档地址和健康检查地址的字典
"""
return { return {
"message": f"欢迎使用 {config['default']['app']['name']}", "message": f"欢迎使用 {config['default']['app']['name']}",
"version": config["default"]["app"]["version"], "version": config["default"]["app"]["version"],
@@ -154,7 +185,6 @@ async def root():
# ) # )
# return {"status": "success", "job_id": job.id} # return {"status": "success", "job_id": job.id}
# 挂载静态文件
config = get_config() config = get_config()
IMAGE_DIR = Path(f"./{config['default']['upload_path']}/images") IMAGE_DIR = Path(f"./{config['default']['upload_path']}/images")
app.mount( app.mount(
@@ -163,17 +193,26 @@ app.mount(
name="images", name="images",
) )
# 挂载前端静态文件
if vfs_db_path := config[env].get("file_system", {}).get("path", None): if vfs_db_path := config[env].get("file_system", {}).get("path", None):
logger.info("静态文件已存在,将使用静态文件服务") logger.info("静态文件已存在,将使用静态文件服务")
static_files_handler = SVFSStaticFiles(directory="/", vfs_db_path=vfs_db_path) static_files_handler = SVFSStaticFiles(directory="/", vfs_db_path=vfs_db_path)
app.mount("/static", static_files_handler, name="static") app.mount("/static", static_files_handler, name="static")
# 全局异常处理器
@app.exception_handler(Exception) @app.exception_handler(Exception)
async def global_exception_handler(_: Request, exc: Exception): async def global_exception_handler(_: Request, exc: Exception):
"""全局异常处理器""" """
全局异常处理器,捕获并处理应用中未捕获的异常。
在调试模式下返回详细的异常信息,在生产模式下返回通用错误提示。
参数:
_: FastAPI请求对象(未使用)
exc: 捕获到的异常对象
返回:
JSONResponse: 包含错误信息的JSON响应,状态码为500
"""
logger.error(f"全局异常: {exc}") logger.error(f"全局异常: {exc}")
return JSONResponse( return JSONResponse(
status_code=500, status_code=500,
+11
View File
@@ -15,12 +15,23 @@ async def get_current_user(
获取当前用户依赖项 获取当前用户依赖项
验证访问令牌并返回用户信息 验证访问令牌并返回用户信息
Args:
credentials: HTTP授权凭证,包含Bearer token
Returns:
TokenData: 验证通过后的用户令牌数据
Raises:
HTTPException: 当访问令牌无效时抛出401未授权异常
""" """
auth_service = get_auth_service() auth_service = get_auth_service()
# 提取并验证访问令牌
token = credentials.credentials token = credentials.credentials
token_data = await auth_service.validate_access_token(token) token_data = await auth_service.validate_access_token(token)
# 令牌验证失败时抛出未授权异常
if token_data is None: if token_data is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
+98 -2
View File
@@ -30,6 +30,21 @@ async def register(
user_data: UserCreate, user_data: UserCreate,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
): ):
"""
用户注册接口
创建新用户账户,检查邮箱是否已被注册,如果邮箱未被注册则创建新用户。
Args:
user_data: 用户注册数据,包含邮箱、密码、用户名和全名等信息
session: 数据库会话对象,用于执行数据库操作
Returns:
UserRead: 创建成功的用户信息,包含用户ID、邮箱、用户名等字段
Raises:
HTTPException: 当邮箱已被注册时抛出400错误
"""
existing = await UserService.get_user_by_email(user_data.email, session) existing = await UserService.get_user_by_email(user_data.email, session)
if existing: if existing:
raise HTTPException( raise HTTPException(
@@ -63,6 +78,25 @@ async def login(
request: Request, request: Request,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
): ):
"""
用户登录接口
验证用户凭据并生成访问令牌和刷新令牌。依次检查邮箱是否存在、密码是否正确、用户是否被禁用。
Args:
user_data: 用户登录数据,包含邮箱和密码
request: FastAPI请求对象,用于获取客户端信息
session: 数据库会话对象,用于查询用户信息
Returns:
TokenResponse: 包含访问令牌(access_token)、刷新令牌(refresh_token)和令牌类型(token_type)的响应对象
Raises:
HTTPException:
- 400错误:当邮箱不存在时
- 401错误:当密码错误时
- 402错误:当用户账户被禁用时
"""
user = await UserService.get_user_by_email(user_data.email, session) user = await UserService.get_user_by_email(user_data.email, session)
if not user: if not user:
raise HTTPException( raise HTTPException(
@@ -93,6 +127,22 @@ async def refresh_token(
credentials: HTTPAuthorizationCredentials = Depends(refresh_scheme), credentials: HTTPAuthorizationCredentials = Depends(refresh_scheme),
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
): ):
"""
刷新访问令牌接口
使用有效的刷新令牌获取新的访问令牌。验证刷新令牌的有效性,如果有效则生成新的访问令牌。
Args:
request: FastAPI请求对象,用于获取客户端信息
credentials: HTTP认证凭证,从请求头中提取的刷新令牌
session: 数据库会话对象,用于验证令牌状态
Returns:
RefreshTokenResponse: 包含新的访问令牌(access_token)和令牌类型(token_type)的响应对象
Raises:
HTTPException: 当刷新令牌无效或已过期时抛出401错误
"""
auth_service = get_auth_service() auth_service = get_auth_service()
refresh_token = credentials.credentials refresh_token = credentials.credentials
@@ -112,6 +162,17 @@ async def refresh_token(
async def logout( async def logout(
credentials: HTTPAuthorizationCredentials = Depends(access_scheme), credentials: HTTPAuthorizationCredentials = Depends(access_scheme),
): ):
"""
用户登出接口
使当前的访问令牌失效,实现单设备登出功能。
Args:
credentials: HTTP认证凭证,从请求头中提取的访问令牌
Returns:
Userlogout: 登出成功响应,包含成功消息和状态码200
"""
auth_service = get_auth_service() auth_service = get_auth_service()
token = credentials.credentials token = credentials.credentials
await auth_service.logout(token) await auth_service.logout(token)
@@ -120,6 +181,17 @@ async def logout(
@router.post("/logout-all", summary="登出所有设备", response_model=Userlogout) @router.post("/logout-all", summary="登出所有设备", response_model=Userlogout)
async def logout_all(current_user: TokenData = Depends(get_current_user)): async def logout_all(current_user: TokenData = Depends(get_current_user)):
"""
登出所有设备接口
使指定用户的所有活跃会话令牌失效,实现多设备同时登出功能。
Args:
current_user: 当前登录用户的令牌数据,包含用户ID等信息
Returns:
Userlogout: 登出成功响应,包含成功消息和状态码200
"""
auth_service = get_auth_service() auth_service = get_auth_service()
await auth_service.logout_all(current_user.user_id) await auth_service.logout_all(current_user.user_id)
@@ -128,7 +200,17 @@ async def logout_all(current_user: TokenData = Depends(get_current_user)):
@router.get("/sessions", summary="获取用户会话") @router.get("/sessions", summary="获取用户会话")
async def get_sessions(current_user: TokenData = Depends(get_current_user)): async def get_sessions(current_user: TokenData = Depends(get_current_user)):
"""获取用户的所有活跃会话""" """
获取用户会话列表接口
查询并返回指定用户的所有活跃会话信息,包括每个会话的设备信息和创建时间等。
Args:
current_user: 当前登录用户的令牌数据,包含用户ID等信息
Returns:
UserSession: 用户会话信息对象,包含用户ID、会话列表和会话总数
"""
auth_service = get_auth_service() auth_service = get_auth_service()
sessions = await auth_service.get_user_sessions(current_user.user_id) sessions = await auth_service.get_user_sessions(current_user.user_id)
@@ -147,7 +229,21 @@ async def get_current_user_info(
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
current_user: TokenData = Depends(get_current_user), current_user: TokenData = Depends(get_current_user),
): ):
"""获取当前登录用户的信息""" """
获取当前用户信息接口
根据当前登录用户的令牌信息,从数据库中查询并返回完整的用户资料。
Args:
session: 数据库会话对象,用于查询用户信息
current_user: 当前登录用户的令牌数据,包含用户ID等信息
Returns:
UserRead: 用户详细信息,包含用户ID、邮箱、用户名、全名、激活状态等字段
Raises:
HTTPException: 当用户不存在时抛出404错误
"""
user = await UserService.get_user_by_id(UUID(current_user.user_id), session) user = await UserService.get_user_by_id(UUID(current_user.user_id), session)
if not user: if not user:
raise HTTPException( raise HTTPException(
+58
View File
@@ -18,6 +18,16 @@ async def create_todo(
todo: TodoCreate, todo: TodoCreate,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
): ):
"""
创建新的待办事项
Args:
todo: 待办事项创建请求体,包含待办事项的详细信息
session: 数据库会话对象,用于执行数据库操作
Returns:
TodoRead: 创建成功的待办事项对象,包含完整的待办事项信息
"""
return await TodoService.create_todo(todo, session) return await TodoService.create_todo(todo, session)
@@ -25,11 +35,35 @@ async def create_todo(
async def read_todos( async def read_todos(
offset: int = 0, limit: int = 100, session: AsyncSession = Depends(get_session) offset: int = 0, limit: int = 100, session: AsyncSession = Depends(get_session)
): ):
"""
获取待办事项列表
Args:
offset: 分页偏移量,默认为0,用于指定从第几条记录开始查询
limit: 每页返回的最大记录数,默认为100,用于限制返回结果的数量
session: 数据库会话对象,用于执行数据库操作
Returns:
List[TodoRead]: 待办事项列表,包含指定范围内的所有待办事项
"""
return await TodoService.get_todos(offset, limit, session) return await TodoService.get_todos(offset, limit, session)
@router.get("/{todo_id}", response_model=TodoRead) @router.get("/{todo_id}", response_model=TodoRead)
async def read_todo(todo_id: int, session: AsyncSession = Depends(get_session)): async def read_todo(todo_id: int, session: AsyncSession = Depends(get_session)):
"""
根据ID获取单个待办事项
Args:
todo_id: 待办事项的唯一标识符
session: 数据库会话对象,用于执行数据库操作
Returns:
TodoRead: 查询到的待办事项对象
Raises:
HTTPException: 当待办事项不存在时抛出404错误
"""
todo = await TodoService.get_todo_by_id(todo_id, session) todo = await TodoService.get_todo_by_id(todo_id, session)
if not todo: if not todo:
raise HTTPException(status_code=404, detail="Todo not found") raise HTTPException(status_code=404, detail="Todo not found")
@@ -40,12 +74,36 @@ async def read_todo(todo_id: int, session: AsyncSession = Depends(get_session)):
async def update_todo( async def update_todo(
todo_id: int, todo_update: TodoUpdate, session: AsyncSession = Depends(get_session) todo_id: int, todo_update: TodoUpdate, session: AsyncSession = Depends(get_session)
): ):
"""
更新待办事项信息
Args:
todo_id: 待办事项的唯一标识符
todo_update: 待办事项更新请求体,包含需要更新的字段信息
session: 数据库会话对象,用于执行数据库操作
Returns:
TodoRead: 更新后的待办事项对象,包含最新的待办事项信息
"""
todo = await TodoService.update_todo(todo_id, todo_update, session) todo = await TodoService.update_todo(todo_id, todo_update, session)
return todo return todo
@router.delete("/{todo_id}") @router.delete("/{todo_id}")
async def delete_todo(todo_id: int, session: AsyncSession = Depends(get_session)): async def delete_todo(todo_id: int, session: AsyncSession = Depends(get_session)):
"""
删除待办事项
Args:
todo_id: 待办事项的唯一标识符
session: 数据库会话对象,用于执行数据库操作
Returns:
dict: 包含操作结果的字典,{"ok": True} 表示删除成功
Raises:
HTTPException: 当待办事项不存在时抛出404错误
"""
is_deleted = await TodoService.delete_todo(todo_id, session) is_deleted = await TodoService.delete_todo(todo_id, session)
if not is_deleted: if not is_deleted:
raise HTTPException(status_code=404, detail="Todo not found") raise HTTPException(status_code=404, detail="Todo not found")
+33
View File
@@ -16,6 +16,8 @@ upload_image_service = UploadService(
class UploadResponse(BaseModel): class UploadResponse(BaseModel):
"""图片上传响应数据模型"""
url: str url: str
filename: str filename: str
file_type: str file_type: str
@@ -23,13 +25,44 @@ class UploadResponse(BaseModel):
@router.post("/image", response_model=UploadResponse) @router.post("/image", response_model=UploadResponse)
async def upload_image(file: UploadFile = File(...)): async def upload_image(file: UploadFile = File(...)):
"""
上传图片文件接口
该接口用于接收用户上传的图片文件,进行格式验证后保存到服务器,
并返回文件的访问URL、文件名和文件类型信息。
Args:
file (UploadFile): 上传的文件对象,必须为图片格式
Returns:
UploadResponse: 包含以下字段的响应对象:
- url (str): 上传文件的访问URL
- filename (str): 保存后的文件名
- file_type (str): 文件扩展名(如 .jpg, .png 等)
Raises:
HTTPException:
- 400: 当文件类型不是图片格式时
- 400: 当文件名为空时
- 400: 当文件扩展名不在允许列表中时
Note:
支持的图片格式包括:.jpg, .jpeg, .png, .gif, .webp
需要用户认证才能访问此接口
"""
# 验证文件内容类型是否为图片
if not file.content_type or not file.content_type.startswith("image/"): if not file.content_type or not file.content_type.startswith("image/"):
raise HTTPException(status_code=400, detail="Invalid file type") raise HTTPException(status_code=400, detail="Invalid file type")
# 验证文件名是否存在
if file.filename is None: if file.filename is None:
raise HTTPException(status_code=400, detail="Invalid file name") raise HTTPException(status_code=400, detail="Invalid file name")
# 检查文件扩展名是否在允许的范围内
ext = os.path.splitext(file.filename)[1].lower() ext = os.path.splitext(file.filename)[1].lower()
if ext not in [".jpg", ".jpeg", ".png", ".gif", ".webp"]: if ext not in [".jpg", ".jpeg", ".png", ".gif", ".webp"]:
raise HTTPException(status_code=400, detail="Invalid file type") raise HTTPException(status_code=400, detail="Invalid file type")
# 执行文件上传操作
url, filename = await upload_image_service.upload(file) url, filename = await upload_image_service.upload(file)
return UploadResponse(url=url, filename=filename, file_type=ext) return UploadResponse(url=url, filename=filename, file_type=ext)
+182 -33
View File
@@ -1,3 +1,10 @@
"""
磁盘缓存模块
基于 diskcache 实现的本地磁盘缓存系统,提供类似 Redis 的 API 接口。
支持过期时间设置、TTL 查询、键模式匹配等功能。
"""
import time import time
from typing import Any, Optional from typing import Any, Optional
@@ -7,7 +14,29 @@ from ..core.config import get_config
class Cache: class Cache:
"""
磁盘缓存类
封装 diskcache 提供统一的缓存操作接口,支持:
- 键值对存储与检索
- 过期时间管理(秒/毫秒)
- TTL 查询
- 键模式匹配
- 批量操作
Attributes:
_cache: diskcache.Cache 实例,负责实际的磁盘缓存操作
"""
def __init__(self): def __init__(self):
"""
初始化缓存实例
从配置中读取缓存存储路径,创建 diskcache.Cache 实例。
Raises:
KeyError: 当配置中缺少 cache.cache_store_path 时
"""
config = get_config() config = get_config()
cache_dir = config["cache"]["cache_store_path"] cache_dir = config["cache"]["cache_store_path"]
self._cache = diskcache.Cache(cache_dir) self._cache = diskcache.Cache(cache_dir)
@@ -23,14 +52,23 @@ class Cache:
""" """
设置缓存 设置缓存
将键值对存储到缓存中,可选设置过期时间。内部使用元组 (value, expire_at)
存储,以支持自定义的过期检查逻辑。
Args: Args:
key: 键 key: 缓存键,字符串类型
value: 值 value: 缓存值,可以是任意可序列化的 Python 对象
ex: 过期时间(秒) ex: 过期时间(秒),与 px 互斥,优先使用 ex
px: 过期时间(毫秒) px: 过期时间(毫秒),当 ex 为 None 时生效
raw: 是否原始存储(当前未使用,保留参数兼容性)
Returns: Returns:
bool: 是否设置成功 bool: 设置成功返回 True,发生异常返回 False
Note:
- ex 和 px 不能同时设置,ex 优先级更高
- 过期时间在存储时计算绝对时间戳,便于后续检查
- 异常会被捕获并打印错误信息,不会抛出异常
""" """
try: try:
expire_at = None expire_at = None
@@ -54,14 +92,21 @@ class Cache:
def get(self, key: str, default: Any = None) -> Any: def get(self, key: str, default: Any = None) -> Any:
""" """
获取缓存 获取缓存值
根据键获取缓存值,自动检查并清理已过期的缓存项。
如果键不存在或已过期,返回默认值。
Args: Args:
key: 键 key: 缓存键
default: 默认值 default: 当键不存在或已过期时的返回值,默认为 None
Returns: Returns:
缓存的值,如果key不存在或已过期返回default Any: 缓存的值,如果key不存在或已过期返回default
Note:
- 发现过期数据时会主动删除,避免脏数据累积
- 异常情况下返回 default 值,保证调用安全性
""" """
try: try:
stored = self._cache.get(key) stored = self._cache.get(key)
@@ -81,13 +126,19 @@ class Cache:
def delete(self, *keys: str) -> int: def delete(self, *keys: str) -> int:
""" """
删除一个或多个key 删除一个或多个缓存键
批量删除指定的缓存键,返回成功删除的数量。
Args: Args:
keys: 要删除的key *keys: 可变参数,要删除的键列表
Returns: Returns:
成功删除的数量 int: 成功删除的键数量
Note:
- 逐个删除,某个键删除失败不影响其他键
- 只统计实际存在的键的删除操作
""" """
deleted = 0 deleted = 0
for key in keys: for key in keys:
@@ -97,26 +148,36 @@ class Cache:
def exists(self, key: str) -> bool: def exists(self, key: str) -> bool:
""" """
检查key是否存在且未过期 检查键是否存在且未过期
通过尝试获取值来判断键的有效性,比直接检查更可靠,
因为会触发过期检查和清理逻辑。
Args: Args:
key: 键 key: 要检查的缓存键
Returns: Returns:
bool: 是否存在 bool: 键存在且未过期返回 True,否则返回 False
""" """
return self.get(key, None) is not None return self.get(key, None) is not None
def expire(self, key: str, _time: int) -> bool: def expire(self, key: str, _time: int) -> bool:
""" """
设置过期时间 更新键的过期时间
重新设置指定键的过期时间,保持原值不变。
如果键不存在,操作失败。
Args: Args:
key: 键 key: 缓存键
time: 过期时间(秒) _time: 新的过期时间(秒),从当前时间开始计算
Returns: Returns:
bool: 是否设置成功 bool: 设置成功返回 True,键不存在或异常返回 False
Note:
- 需要先读取原值,再重新存储以更新过期时间
- 原子性不保证,高并发场景可能有问题
""" """
try: try:
stored = self._cache.get(key) stored = self._cache.get(key)
@@ -133,13 +194,22 @@ class Cache:
def ttl(self, key: str) -> int: def ttl(self, key: str) -> int:
""" """
获取key的剩余存活时间 获取键的剩余生存时间(Time To Live)
计算键距离过期的剩余秒数。
Args: Args:
key: 键 key: 缓存键
Returns: Returns:
int: 剩余秒数,-1表示永不过期,-2表示key不存在 int: 剩余秒数,返回值含义:
- >= 0: 剩余存活秒数
- -1: 键存在但永不过期
- -2: 键不存在
Note:
- 返回值为整数,向下取整
- 即使剩余时间为负数也返回 0,表示即将过期
""" """
try: try:
stored = self._cache.get(key) stored = self._cache.get(key)
@@ -158,13 +228,21 @@ class Cache:
def keys(self, pattern: Optional[str] = None) -> list: def keys(self, pattern: Optional[str] = None) -> list:
""" """
获取所有key,支持简单的通配符匹配(*) 获取所有缓存键,支持通配符模式匹配
遍历所有缓存键,可根据前缀模式过滤。
Args: Args:
pattern: 匹配模式,如 "user:*" pattern: 匹配模式,仅支持前缀通配,如 "user:*" 匹配所有 "user:" 开头的键
None 或空字符串返回所有键
Returns: Returns:
list: key列表 list: 匹配的键列表,按迭代顺序排列
Note:
- 仅支持简单的 "*" 后缀通配,不支持复杂模式
- 大量键时性能较差,建议谨慎使用
- pattern 中的 "*" 会被替换为空字符串作为前缀
""" """
all_keys = list(self._cache.iterkeys()) all_keys = list(self._cache.iterkeys())
@@ -175,7 +253,17 @@ class Cache:
return all_keys return all_keys
def clear(self) -> bool: def clear(self) -> bool:
"""清空所有缓存。""" """
清空所有缓存
删除缓存目录中的所有数据,包括未过期的项。
Returns:
bool: 清空成功返回 True,异常返回 False
Warning:
此操作不可逆,会删除所有缓存数据
"""
try: try:
self._cache.clear() self._cache.clear()
return True return True
@@ -184,12 +272,36 @@ class Cache:
return False return False
def size(self) -> int: def size(self) -> int:
"""获取当前缓存数量(不含过期但尚未清理的项)。""" """
获取当前有效缓存项数量
统计当前未过期的缓存键数量。
Returns:
int: 有效缓存项的数量
Note:
- 不包含已过期但尚未清理的项
- 需要遍历所有键,大数据量时可能有性能开销
"""
# diskcache 的 __len__ 会返回当前有效条目数 # diskcache 的 __len__ 会返回当前有效条目数
return len(list(self._cache.iterkeys())) return len(list(self._cache.iterkeys()))
def get_all(self) -> dict: def get_all(self) -> dict:
"""获取所有未过期的缓存数据。""" """
获取所有未过期的缓存数据
遍历所有缓存键,返回有效的键值对字典。
自动过滤过期数据。
Returns:
dict: 所有有效缓存的键值对,格式为 {key: value}
Note:
- 会触发每个键的过期检查
- 大数据量时内存占用较高,谨慎使用
- 返回的是快照,不保证原子性
"""
result = {} result = {}
for key in self._cache.iterkeys(): for key in self._cache.iterkeys():
value = self.get(str(key)) # get 内部会检查并清理过期 value = self.get(str(key)) # get 内部会检查并清理过期
@@ -198,7 +310,15 @@ class Cache:
return result return result
def close(self): def close(self):
"""关闭缓存。""" """
关闭缓存连接
释放 diskcache 占用的资源,包括文件句柄等。
应在应用退出时调用。
Note:
关闭后不应再使用该实例,除非重新初始化
"""
self._cache.close() self._cache.close()
@@ -206,6 +326,18 @@ _cache: Optional[Cache] = None
def get_cache() -> Cache: def get_cache() -> Cache:
"""
获取全局缓存单例
使用懒加载模式创建缓存实例,确保整个应用中只有一个 Cache 实例。
Returns:
Cache: 全局缓存实例
Note:
- 线程不安全,多线程环境需要额外同步
- 首次调用时初始化,后续调用返回同一实例
"""
global _cache global _cache
if _cache is None: if _cache is None:
_cache = Cache() _cache = Cache()
@@ -217,13 +349,30 @@ def cached(ttl: Optional[int] = None):
""" """
函数结果缓存装饰器 函数结果缓存装饰器
自动缓存函数的返回值,相同参数的调用直接从缓存返回,
避免重复计算。适用于纯函数或副作用小的函数。
Args: Args:
ttl: 缓存时间(秒),None表示永不过期 ttl: 缓存生存时间(秒),None 表示永不过期
Returns:
callable: 装饰器函数
Example: Example:
@cached(ttl=60) >>> @cached(ttl=60)
def get_user(user_id: int): ... def get_user(user_id: int):
return {"id": user_id, "name": "test"} ... return {"id": user_id, "name": "test"}
>>>
>>> # 第一次调用执行函数并缓存
>>> get_user(1)
>>> # 60秒内再次调用直接返回缓存结果
>>> get_user(1)
Note:
- 缓存键由函数名 + 位置参数 + 关键字参数组成
- 参数必须是可哈希的,否则会导致错误
- 不适合缓存大型对象或频繁变化的数据
- 不同参数组合会生成不同的缓存键
""" """
def decorator(func): def decorator(func):
+55 -1
View File
@@ -3,6 +3,7 @@ from pathlib import Path
from typing import TypedDict, Literal, Optional, cast, List from typing import TypedDict, Literal, Optional, cast, List
# 根据Python版本选择合适的TOML解析库和Required类型
if sys.version_info >= (3, 11): if sys.version_info >= (3, 11):
import tomllib import tomllib
from typing import Required from typing import Required
@@ -14,6 +15,8 @@ else:
class DefaultApp(TypedDict): class DefaultApp(TypedDict):
"""应用默认配置结构"""
name: str name: str
version: str version: str
description: str description: str
@@ -21,6 +24,8 @@ class DefaultApp(TypedDict):
class Default(TypedDict): class Default(TypedDict):
"""系统默认配置结构,包含应用信息、环境、路径等基础配置"""
app: DefaultApp app: DefaultApp
env: Literal["production", "development"] env: Literal["production", "development"]
prefix: str prefix: str
@@ -28,11 +33,15 @@ class Default(TypedDict):
class SqliteConnectionArgs(TypedDict, total=False): class SqliteConnectionArgs(TypedDict, total=False):
"""SQLite数据库连接参数配置"""
timeout: int timeout: int
check_same_thread: bool check_same_thread: bool
class DatabaseSqlite(TypedDict, total=False): class DatabaseSqlite(TypedDict, total=False):
"""SQLite数据库连接池及行为配置"""
echo: bool echo: bool
pool_size: int pool_size: int
max_overflow: int max_overflow: int
@@ -43,6 +52,8 @@ class DatabaseSqlite(TypedDict, total=False):
class Database(TypedDict, total=False): class Database(TypedDict, total=False):
"""数据库配置结构,支持SQLite、MySQL、PostgreSQL三种数据库类型"""
type: Required[Literal["sqlite", "mysql", "postgresql"]] type: Required[Literal["sqlite", "mysql", "postgresql"]]
url: Required[str] url: Required[str]
generate_schemas: bool generate_schemas: bool
@@ -52,6 +63,8 @@ class Database(TypedDict, total=False):
class Security(TypedDict): class Security(TypedDict):
"""安全相关配置,包括JWT令牌和安全策略设置"""
secret_key: str secret_key: str
algorithm: Literal["HS256"] algorithm: Literal["HS256"]
access_token_expire_minutes: int access_token_expire_minutes: int
@@ -60,6 +73,8 @@ class Security(TypedDict):
class Server(TypedDict, total=False): class Server(TypedDict, total=False):
"""服务器运行配置,包含主机、端口、工作进程等设置"""
host: Required[str] host: Required[str]
port: Required[int] port: Required[int]
reload: bool reload: bool
@@ -68,6 +83,8 @@ class Server(TypedDict, total=False):
class Logging(TypedDict): class Logging(TypedDict):
"""日志系统配置,定义日志级别、输出格式和文件管理策略"""
level: str level: str
file: str file: str
format: str format: str
@@ -76,6 +93,8 @@ class Logging(TypedDict):
class Cors(TypedDict, total=False): class Cors(TypedDict, total=False):
"""跨域资源共享(CORS)配置"""
allow_origins: List[str] allow_origins: List[str]
allow_credentials: bool allow_credentials: bool
allow_methods: List[str] allow_methods: List[str]
@@ -83,16 +102,22 @@ class Cors(TypedDict, total=False):
class ModuleSystem(TypedDict, total=False): class ModuleSystem(TypedDict, total=False):
"""模块系统配置,定义模块加载类型和路径"""
type: str type: str
path: List[str] path: List[str]
class FileSystem(TypedDict, total=False): class FileSystem(TypedDict, total=False):
"""文件系统配置,定义存储类型和路径"""
type: str type: str
path: str path: str
class Env(TypedDict, total=False): class Env(TypedDict, total=False):
"""环境特定配置结构,用于开发和生产环境的差异化配置"""
debug: Required[bool] debug: Required[bool]
server: Required[Server] server: Required[Server]
logging: Required[Logging] logging: Required[Logging]
@@ -102,14 +127,20 @@ class Env(TypedDict, total=False):
class Scheduler(TypedDict): class Scheduler(TypedDict):
"""任务调度器配置"""
job_store_path: str job_store_path: str
class Cache(TypedDict): class Cache(TypedDict):
"""缓存系统配置"""
cache_store_path: str cache_store_path: str
class Config(TypedDict): class Config(TypedDict):
"""完整配置结构,整合所有配置项,包含默认配置、数据库、调度器、缓存、安全和环境配置"""
default: Default default: Default
database: Database database: Database
scheduler: Scheduler scheduler: Scheduler
@@ -120,17 +151,40 @@ class Config(TypedDict):
def load_config(config_path: Path = Path("config.toml")) -> Config: def load_config(config_path: Path = Path("config.toml")) -> Config:
"""从 TOML 文件加载配置""" """
从TOML配置文件加载配置数据
Args:
config_path: 配置文件路径,默认为当前目录下的config.toml
Returns:
Config: 解析后的配置字典对象
Raises:
FileNotFoundError: 当配置文件不存在时抛出异常
"""
if not config_path.exists(): if not config_path.exists():
raise FileNotFoundError(f"配置文件不存在: {config_path}") raise FileNotFoundError(f"配置文件不存在: {config_path}")
with open(config_path, "rb") as f: with open(config_path, "rb") as f:
return cast(Config, tomllib.load(f)) return cast(Config, tomllib.load(f))
# 全局配置缓存变量,避免重复加载配置文件
_config: Optional[Config] = None _config: Optional[Config] = None
def get_config(config_path: str = "config.toml") -> Config: def get_config(config_path: str = "config.toml") -> Config:
"""
获取应用程序配置(带缓存机制)
首次调用时加载配置文件并缓存,后续调用直接返回缓存的配置对象
Args:
config_path: 配置文件路径,默认为config.toml
Returns:
Config: 配置字典对象
"""
global _config global _config
if _config is None: if _config is None:
_config = load_config(Path(config_path)) _config = load_config(Path(config_path))
+78 -2
View File
@@ -14,7 +14,19 @@ log_config = config[env]["logging"]
class ColoredFormatter(logging.Formatter): class ColoredFormatter(logging.Formatter):
"""为 status_code 添加颜色的日志格式化器""" """
为 HTTP 状态码添加颜色的日志格式化器
根据 HTTP 状态码的首位数字为日志消息中的状态码添加不同颜色:
- 2xx: 绿色(成功)
- 3xx: 青色(重定向)
- 4xx: 黄色(客户端错误)
- 5xx: 红色(服务器错误)
Attributes:
COLORS: 状态码首位数字到 ANSI 颜色代码的映射字典
RESET: ANSI 重置颜色代码
"""
COLORS = { COLORS = {
"2": "\033[32m", # 绿色 2xx "2": "\033[32m", # 绿色 2xx
@@ -25,6 +37,18 @@ class ColoredFormatter(logging.Formatter):
RESET = "\033[0m" RESET = "\033[0m"
def format(self, record): def format(self, record):
"""
格式化日志记录,为状态码添加颜色
如果日志记录包含 status_code 属性,则查找消息中的 "status=数字" 模式
并为其添加对应的颜色代码。格式化完成后恢复原始消息以避免影响其他 handler。
Args:
record: 日志记录对象,应包含 msg 属性和可选的 status_code 属性
Returns:
str: 格式化后的日志消息字符串,状态码部分可能包含 ANSI 颜色代码
"""
# 保存原始消息 # 保存原始消息
original_msg = record.msg original_msg = record.msg
# 如果日志记录包含 status_code 属性,则为其添加颜色 # 如果日志记录包含 status_code 属性,则为其添加颜色
@@ -44,7 +68,17 @@ class ColoredFormatter(logging.Formatter):
class ExcludeWatchFilesFilter(logging.Filter): class ExcludeWatchFilesFilter(logging.Filter):
"""
日志过滤器,用于排除第三方库的日志输出
过滤掉来自 watchfiles、aiosqlite、apscheduler、sqlalchemy 等库的日志记录,
避免这些库的详细日志污染应用日志输出。
"""
def __init__(self): def __init__(self):
"""
初始化过滤器,设置需要过滤的日志名称前缀列表
"""
super().__init__() super().__init__()
self._filter_starts_with_tulpe = ( self._filter_starts_with_tulpe = (
"watchfiles", "watchfiles",
@@ -55,8 +89,17 @@ class ExcludeWatchFilesFilter(logging.Filter):
"sqlalchemy.engine.Engine", "sqlalchemy.engine.Engine",
) )
# 重写过滤方法
def filter(self, record): def filter(self, record):
"""
判断是否应该保留该日志记录
Args:
record: 日志记录对象,包含 name 属性表示日志来源
Returns:
bool: 如果日志名称不以过滤列表中的任何前缀开头则返回 True(保留),
否则返回 False(过滤掉)
"""
return not record.name.startswith(self._filter_starts_with_tulpe) return not record.name.startswith(self._filter_starts_with_tulpe)
@@ -64,6 +107,27 @@ _configured_loggers = set()
def setup_logging(logger: logging.Logger, log_name: str = "app"): def setup_logging(logger: logging.Logger, log_name: str = "app"):
"""
配置日志系统,包括文件和控制台输出
为指定的 logger 配置以下功能:
1. 设置日志级别和格式
2. 创建按日期命名的轮转文件处理器(支持文件大小限制和备份数量控制)
3. 创建带颜色格式化的控制台处理器
4. 应用过滤器排除第三方库日志
该函数具有防重复配置机制,同一个 log_name 只会配置一次。
Args:
logger: 需要配置的 logging.Logger 实例
log_name: 日志名称标识,用于区分不同的日志配置和生成日志文件名,默认为 "app"
Returns:
None
Raises:
无显式抛出异常,但可能因文件系统权限问题导致目录创建或文件写入失败
"""
if log_name in _configured_loggers: if log_name in _configured_loggers:
return return
_configured_loggers.add(log_name) _configured_loggers.add(log_name)
@@ -110,6 +174,18 @@ def setup_logging(logger: logging.Logger, log_name: str = "app"):
def get_logger(name: str) -> logging.Logger: def get_logger(name: str) -> logging.Logger:
"""
获取配置好的日志记录器实例
根据名称获取或创建 logger 实例,并自动调用 setup_logging 进行配置。
如果该名称的 logger 已经配置过,则直接返回已配置的实例。
Args:
name: 日志记录器名称,通常使用模块名如 __name__
Returns:
logging.Logger: 已配置完成的日志记录器实例,可直接用于记录日志
"""
logger = logging.getLogger(name) logger = logging.getLogger(name)
setup_logging(logger, name) setup_logging(logger, name)
return logger return logger
+29 -6
View File
@@ -1,13 +1,36 @@
# src/chongming/app/core/security.py
import bcrypt import bcrypt
def get_password_hash(password: str) -> str: def get_password_hash(password: str) -> str:
"""
对明文密码进行哈希加密
使用bcrypt算法对密码进行加盐哈希处理,确保密码存储的安全性。
Args:
password: 需要加密的明文密码字符串
Returns:
加密后的密码哈希字符串,采用UTF-8编码
"""
salt = bcrypt.gensalt() salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password.encode('utf-8'), salt) hashed = bcrypt.hashpw(password.encode("utf-8"), salt)
return hashed.decode('utf-8') return hashed.decode("utf-8")
def verify_password(plain_password: str, hashed_password: str) -> bool: def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
验证明文密码与哈希密码是否匹配
将明文密码与已存储的哈希密码进行比对,用于用户登录时的密码验证。
Args:
plain_password: 用户输入的明文密码
hashed_password: 数据库中存储的哈希密码
Returns:
密码匹配返回True,否则返回False
"""
return bcrypt.checkpw( return bcrypt.checkpw(
plain_password.encode('utf-8'), plain_password.encode("utf-8"), hashed_password.encode("utf-8")
hashed_password.encode('utf-8') )
)
+40 -2
View File
@@ -5,18 +5,56 @@ from sqlite_vfs.core import SQLiteVFS
class SVFSStaticFiles(StaticFiles): class SVFSStaticFiles(StaticFiles):
"""
基于 SQLite VFS 的静态文件服务类
该类继承自 FastAPI 的 StaticFiles,使用 SQLite 虚拟文件系统来存储和提供静态文件。
支持单页应用(SPA)的路由回退机制,当请求的文件不存在时,会自动返回 index.html。
"""
def __init__(self, directory, vfs_db_path="static.svfs"): def __init__(self, directory, vfs_db_path="static.svfs"):
"""
初始化 SVFSStaticFiles 实例
参数:
directory (str): 静态文件的根目录路径
vfs_db_path (str): SQLite VFS 数据库文件路径,默认为 "static.svfs"
返回值:
无
"""
vfs_db_path = vfs_db_path vfs_db_path = vfs_db_path
self._vfs = SQLiteVFS(vfs_db_path, compress=False) self._vfs = SQLiteVFS(vfs_db_path, compress=False)
super().__init__(directory=directory) super().__init__(directory=directory)
async def get_response(self, path: str, scope): async def get_response(self, path: str, scope):
"""
根据请求路径获取对应的静态文件响应
该方法处理静态文件的请求逻辑,包括:
1. 路径标准化处理
2. 文件存在性检查
3. SPA 路由回退支持
4. 目录索引处理
5. MIME 类型自动识别
参数:
path (str): 请求的文件路径
scope: ASGI 作用域对象,包含请求上下文信息
返回值:
Response: FastAPI 响应对象,包含文件内容和正确的 Content-Type
异常:
HTTPException: 当文件不存在且无法回退到 index.html 时,抛出 404 错误
"""
# 统一路径格式:以 / 开头,并将 Windows 反斜杠转为正斜杠 # 统一路径格式:以 / 开头,并将 Windows 反斜杠转为正斜杠
normalized_path = "/" + path.replace("\\", "/") normalized_path = "/" + path.replace("\\", "/")
# 获取文件信息 # 获取文件信息
file_info = self._vfs.get_file_info(normalized_path) file_info = self._vfs.get_file_info(normalized_path)
# 处理文件不存在的情况,尝试 SPA 回退机制
if file_info is None: if file_info is None:
# SPA 回退:如果不是静态资源扩展名,尝试返回 index.html # SPA 回退:如果不是静态资源扩展名,尝试返回 index.html
if not any( if not any(
@@ -38,7 +76,7 @@ class SVFSStaticFiles(StaticFiles):
return Response(content=content, media_type="text/html") return Response(content=content, media_type="text/html")
raise HTTPException(status_code=404, detail="File not found") raise HTTPException(status_code=404, detail="File not found")
# 如果是目录,也返回 index.html(某些情况) # 处理目录请求,返回目录下的 index.html
if file_info["is_directory"]: if file_info["is_directory"]:
index_info = self._vfs.get_file_info(f"{normalized_path}/index.html") index_info = self._vfs.get_file_info(f"{normalized_path}/index.html")
if index_info: if index_info:
@@ -46,7 +84,7 @@ class SVFSStaticFiles(StaticFiles):
return Response(content=content, media_type="text/html") return Response(content=content, media_type="text/html")
raise HTTPException(status_code=404, detail="Directory index not found") raise HTTPException(status_code=404, detail="Directory index not found")
# 读取文件内容 # 读取文件内容并确定 MIME 类型
content = self._vfs.read_file(normalized_path) content = self._vfs.read_file(normalized_path)
content_type = file_info.get("content_type") or "application/octet-stream" content_type = file_info.get("content_type") or "application/octet-stream"
# 简单根据扩展名补充 MIME 类型(可选) # 简单根据扩展名补充 MIME 类型(可选)
+20
View File
@@ -3,9 +3,29 @@ from typing import Optional
class Detail(BaseModel): class Detail(BaseModel):
"""
HTTP错误详情模型
用于封装HTTP错误的详细信息,包括错误消息和错误代码。
Attributes:
message (str): 错误描述信息
code (int): 错误状态码或业务错误码
"""
message: str message: str
code: int code: int
class HTTPError(BaseModel): class HTTPError(BaseModel):
"""
HTTP错误响应模型
用于标准化HTTP错误响应的数据结构,符合RESTful API规范。
Attributes:
detail (Optional[Detail]): 错误详情对象,包含具体的错误信息和错误码。
如果为None,表示没有详细的错误信息。
"""
detail: Optional[Detail] = None detail: Optional[Detail] = None
+18 -6
View File
@@ -8,17 +8,29 @@ if TYPE_CHECKING:
from .role import Role from .role import Role
# ---------- 权限表 ----------
class Permission(SQLModel, table=True): class Permission(SQLModel, table=True):
"""权限项,如 user:create, order:delete""" """
权限模型类,用于定义系统中的权限项。
权限采用资源:操作的格式进行标识,例如:
- user:create 表示创建用户的权限
- order:delete 表示删除订单的权限
Attributes:
id: 权限的唯一标识符,主键,自增
name: 权限标识符,唯一且建立索引,格式为"资源:操作"
description: 权限的描述信息,可选字段
resource: 资源名称,建立索引,如 user、order、product 等
action: 操作类型,如 create、read、update、delete 等
roles: 关联的角色列表,通过 RolePermission 中间表实现多对多关系
"""
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(unique=True, index=True) # 权限标识符 name: str = Field(unique=True, index=True)
description: Optional[str] = None description: Optional[str] = None
resource: str = Field(index=True) # 资源,如 user, order, product resource: str = Field(index=True)
action: str # 操作,如 create, read, update, delete action: str
# 多对多关联
roles: List["Role"] = Relationship( roles: List["Role"] = Relationship(
back_populates="permissions", link_model=RolePermission back_populates="permissions", link_model=RolePermission
) )
+20 -2
View File
@@ -4,14 +4,32 @@ from sqlmodel import SQLModel, Field
# ---------- 关联表 ---------- # ---------- 关联表 ----------
class UserRole(SQLModel, table=True): class UserRole(SQLModel, table=True):
"""用户角色关联表""" """
用户角色关联表
用于实现用户与角色之间的多对多关系。
每个记录表示一个用户拥有的一个角色。
Attributes:
user_id: 用户ID,外键关联到user表的id字段,作为复合主键的一部分
role_id: 角色ID,外键关联到role表的id字段,作为复合主键的一部分
"""
user_id: UUID = Field(foreign_key="user.id", primary_key=True) user_id: UUID = Field(foreign_key="user.id", primary_key=True)
role_id: int = Field(foreign_key="role.id", primary_key=True) role_id: int = Field(foreign_key="role.id", primary_key=True)
class RolePermission(SQLModel, table=True): class RolePermission(SQLModel, table=True):
"""角色权限关联表""" """
角色权限关联表
用于实现角色与权限之间的多对多关系。
每个记录表示一个角色拥有的一项权限。
Attributes:
role_id: 角色ID,外键关联到role表的id字段,作为复合主键的一部分
permission_id: 权限ID,外键关联到permission表的id字段,作为复合主键的一部分
"""
role_id: int = Field(foreign_key="role.id", primary_key=True) role_id: int = Field(foreign_key="role.id", primary_key=True)
permission_id: int = Field(foreign_key="permission.id", primary_key=True) permission_id: int = Field(foreign_key="permission.id", primary_key=True)
+25
View File
@@ -12,6 +12,21 @@ if TYPE_CHECKING:
# ---------- 角色表 ---------- # ---------- 角色表 ----------
class Role(SQLModel, table=True): class Role(SQLModel, table=True):
"""
角色模型类,用于定义系统中的用户角色。
该类继承自SQLModel,映射到数据库中的角色表。每个角色可以有多个用户和多个权限,
通过中间表UserRole和RolePermission实现多对多关系。
Attributes:
id: 角色的唯一标识符,主键,自增
name: 角色名称,具有唯一性约束和索引,例如:admin, manager, operator, viewer
description: 角色的描述信息,可选字段
is_only_user: 标识该角色是否仅限单个用户使用,默认为False
users: 与该角色关联的用户列表,通过UserRole中间表建立多对多关系
permissions: 与该角色关联的权限列表,通过RolePermission中间表建立多对多关系
"""
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(unique=True, index=True) # admin, manager, operator, viewer name: str = Field(unique=True, index=True) # admin, manager, operator, viewer
description: Optional[str] = None description: Optional[str] = None
@@ -23,5 +38,15 @@ class Role(SQLModel, table=True):
class RoleUpdate(BaseModel): class RoleUpdate(BaseModel):
"""
角色更新请求模型类,用于接收角色更新时的参数。
该类继承自Pydantic的BaseModel,所有字段均为可选,允许部分更新角色信息。
Attributes:
name: 更新后的角色名称,可选字段
description: 更新后的角色描述信息,可选字段
"""
name: Optional[str] = None name: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
+47
View File
@@ -5,24 +5,71 @@ from sqlmodel import SQLModel, Field
# 定义模型 # 定义模型
class TodoBase(SQLModel): class TodoBase(SQLModel):
"""
Todo任务的基础模型类,包含任务的通用字段。
Attributes:
title: 任务标题,必填字段
description: 任务描述,可选字段,默认为None
completed: 任务完成状态,布尔值,默认为False
"""
title: str title: str
description: Optional[str] = None description: Optional[str] = None
completed: bool = False completed: bool = False
class Todo(TodoBase, table=True): class Todo(TodoBase, table=True):
"""
Todo任务的数据库表模型类,继承自TodoBase。
该类对应数据库中的todo表,用于存储和查询任务数据。
Attributes:
id: 任务唯一标识符,主键,由数据库自动生成
"""
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
class TodoCreate(TodoBase): class TodoCreate(TodoBase):
"""
创建Todo任务时使用的请求模型类。
继承自TodoBase,用于接收前端传入的新建任务数据。
不包含id字段,因为id由数据库自动生成。
"""
pass pass
class TodoRead(TodoBase): class TodoRead(TodoBase):
"""
读取Todo任务时使用的响应模型类。
继承自TodoBase,用于向客户端返回任务数据。
包含完整的任务信息,包括自动生成的id字段。
Attributes:
id: 任务唯一标识符,整数类型,必填字段
"""
id: int id: int
class TodoUpdate(SQLModel): class TodoUpdate(SQLModel):
"""
更新Todo任务时使用的请求模型类。
所有字段均为可选,允许部分更新任务信息。
只有提供的字段会被更新,未提供的字段保持原值。
Attributes:
title: 任务标题,可选字段
description: 任务描述,可选字段
completed: 任务完成状态,可选字段
"""
title: Optional[str] = None title: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
completed: Optional[bool] = None completed: Optional[bool] = None
+76 -4
View File
@@ -11,16 +11,41 @@ if TYPE_CHECKING:
class UserBase(SQLModel): class UserBase(SQLModel):
"""
用户基础模型
定义用户的核心属性,作为其他用户相关模型的基类。
包含用户的基本信息和状态标识。
Attributes:
email: 用户邮箱地址,唯一且建立索引
username: 用户名,可选,最大长度50字符
full_name: 用户全名,可选,最大长度100字符
is_active: 用户激活状态,默认为True
is_superuser: 是否为超级管理员,默认为False
created_at: 创建时间,可选字段
"""
email: str = Field(unique=True, index=True) email: str = Field(unique=True, index=True)
username: Optional[str] = Field(default=None, max_length=50) username: Optional[str] = Field(default=None, max_length=50)
full_name: Optional[str] = Field(default=None, max_length=100) full_name: Optional[str] = Field(default=None, max_length=100)
is_active: bool = Field(default=True) is_active: bool = Field(default=True)
is_superuser: bool = Field(default=False) is_superuser: bool = Field(default=False)
created_at: Optional[str] = Field(default=None) # 可自动填充,此处略 created_at: Optional[str] = Field(default=None)
class User(UserBase, table=True): class User(UserBase, table=True):
"""用户模型""" """
用户数据库模型
继承自UserBase,用于数据库表映射。
包含用户的完整信息以及与角色的多对多关系。
Attributes:
id: 用户唯一标识符,UUID类型,自动生成
hashed_password: 加密后的密码
roles: 用户关联的角色列表,通过UserRole中间表实现多对多关系
"""
id: UUID = Field(default_factory=uuid4, primary_key=True) id: UUID = Field(default_factory=uuid4, primary_key=True)
hashed_password: str hashed_password: str
@@ -28,28 +53,75 @@ class User(UserBase, table=True):
class UserCreate(UserBase): class UserCreate(UserBase):
"""用户创建模型""" """
用户创建请求模型
用于创建新用户时的数据验证和传输。
在UserBase基础上增加了密码字段。
Attributes:
password: 用户明文密码,用于创建时传入
"""
password: str password: str
class UserRead(UserBase): class UserRead(UserBase):
"""用户读取模型""" """
用户读取响应模型
用于向客户端返回用户信息。
在UserBase基础上增加了用户ID字段。
Attributes:
id: 用户唯一标识符,UUID类型
"""
id: UUID id: UUID
class UserLogin(BaseModel): class UserLogin(BaseModel):
"""
用户登录请求模型
用于处理用户登录时的数据验证。
Attributes:
email: 用户邮箱地址
password: 用户密码
"""
email: str email: str
password: str password: str
class Userlogout(BaseModel): class Userlogout(BaseModel):
"""
用户登出响应模型
用于返回登出操作的结果。
Attributes:
code: 响应状态码,默认为200表示成功
message: 响应消息,默认为"logout success"
"""
code: int = 200 code: int = 200
message: str = "logout success" message: str = "logout success"
class UserSession(BaseModel): class UserSession(BaseModel):
"""
用户会话信息模型
用于存储和返回用户的会话相关信息。
Attributes:
user_id: 用户唯一标识符
sessions: 会话列表,每个会话为字典类型,键值均为字符串或None
count: 会话总数
"""
user_id: str user_id: str
sessions: List[Dict[str, Optional[str]]] sessions: List[Dict[str, Optional[str]]]
count: int count: int
+88 -10
View File
@@ -24,13 +24,24 @@ class AuthService:
async def authenticate_user( async def authenticate_user(
self, email: str, password: str, session: AsyncSession self, email: str, password: str, session: AsyncSession
) -> Optional[User]: ) -> Optional[User]:
"""验证用户凭据""" """
验证用户凭据
通过邮箱和密码验证用户身份,使用 bcrypt 进行密码哈希比对。
Args:
email: 用户邮箱地址
password: 用户明文密码
session: 数据库异步会话对象
Returns:
验证成功返回 User 对象,失败返回 None
"""
user = await UserService.get_user_by_email(email, session) user = await UserService.get_user_by_email(email, session)
if not user: if not user:
return None return None
if not bcrypt.checkpw( if not bcrypt.checkpw(
password.encode('utf-8'), password.encode("utf-8"), user.hashed_password.encode("utf-8")
user.hashed_password.encode('utf-8')
): ):
return None return None
@@ -42,7 +53,20 @@ class AuthService:
request: Optional[Request] = None, request: Optional[Request] = None,
device_id: Optional[str] = None, device_id: Optional[str] = None,
) -> TokenResponse: ) -> TokenResponse:
"""为用户创建访问和刷新令牌""" """
为用户创建访问和刷新令牌
根据用户信息和请求上下文生成 JWT access token 和 refresh token,
同时记录设备信息用于安全审计。
Args:
user: 已验证的用户对象
request: FastAPI 请求对象,用于提取客户端信息(可选)
device_id: 设备唯一标识符(可选)
Returns:
TokenResponse 对象,包含 access_token、refresh_token 及过期时间
"""
# 获取用户数据 # 获取用户数据
user_data = { user_data = {
@@ -89,7 +113,17 @@ class AuthService:
) )
async def validate_access_token(self, token: str) -> Optional[TokenData]: async def validate_access_token(self, token: str) -> Optional[TokenData]:
"""验证访问令牌""" """
验证访问令牌
校验 JWT access token 的有效性和完整性,提取用户身份信息。
Args:
token: JWT access token 字符串
Returns:
验证成功返回 TokenData 对象(包含 user_id、username、email),失败返回 None
"""
validation = self.jwt_cache.validate_token(token, token_type="access") validation = self.jwt_cache.validate_token(token, token_type="access")
if not validation: if not validation:
return None return None
@@ -107,7 +141,20 @@ class AuthService:
session: AsyncSession, session: AsyncSession,
request: Optional[Request] = None, request: Optional[Request] = None,
) -> Optional[str]: ) -> Optional[str]:
"""使用刷新令牌获取新的访问令牌""" """
使用刷新令牌获取新的访问令牌
验证 refresh token 的有效性,确认用户仍然存在,然后生成新的 access token。
保持原有的设备信息以确保会话连续性。
Args:
refresh_token: JWT refresh token 字符串
session: 数据库异步会话对象
request: FastAPI 请求对象,用于提取客户端信息(可选)
Returns:
验证成功返回新的 access token 字符串,失败返回 None
"""
validation = self.jwt_cache.validate_token(refresh_token, token_type="refresh") validation = self.jwt_cache.validate_token(refresh_token, token_type="refresh")
if not validation: if not validation:
return None return None
@@ -145,15 +192,39 @@ class AuthService:
return new_access_token return new_access_token
async def logout(self, token: str): async def logout(self, token: str):
"""用户登出 - 使令牌失效""" """
用户登出 - 使令牌失效
将指定的 token 加入黑名单,使其立即失效。
Args:
token: 需要失效的 JWT token 字符串
"""
self.jwt_cache.invalidate_token(token) self.jwt_cache.invalidate_token(token)
async def logout_all(self, user_id: str): async def logout_all(self, user_id: str):
"""使用户的所有令牌失效""" """
使用户的所有令牌失效
强制注销用户的所有活跃会话,适用于密码修改或安全事件场景。
Args:
user_id: 用户 ID 字符串
"""
self.jwt_cache.invalidate_user_tokens(str(user_id)) self.jwt_cache.invalidate_user_tokens(str(user_id))
async def get_user_sessions(self, user_id: str): async def get_user_sessions(self, user_id: str):
"""获取用户的活跃会话""" """
获取用户的活跃会话
查询指定用户当前所有有效的 token 会话信息。
Args:
user_id: 用户 ID 字符串
Returns:
用户活跃会话列表
"""
return self.jwt_cache.get_user_sessions(str(user_id)) return self.jwt_cache.get_user_sessions(str(user_id))
@@ -162,7 +233,14 @@ _auth_service = None
def get_auth_service() -> AuthService: def get_auth_service() -> AuthService:
"""获取认证服务实例(单例模式)""" """
获取认证服务实例(单例模式)
确保整个应用中只有一个 AuthService 实例,避免重复初始化。
Returns:
AuthService 单例实例
"""
global _auth_service global _auth_service
if _auth_service is None: if _auth_service is None:
_auth_service = AuthService() _auth_service = AuthService()
+81
View File
@@ -11,6 +11,8 @@ from .role import RoleService
class PermissionService: class PermissionService:
"""权限服务类,提供权限的创建、查询、分配和解除绑定等功能"""
@classmethod @classmethod
async def create_permission( async def create_permission(
cls, cls,
@@ -20,6 +22,16 @@ class PermissionService:
action: str, action: str,
description: Optional[str] = None, description: Optional[str] = None,
) -> Permission: ) -> Permission:
"""
创建新的权限记录
:param session: 数据库会话对象
:param name: 权限名称
:param resource: 资源标识
:param action: 操作类型
:param description: 权限描述,可选
:return: 创建的权限对象
"""
permission = Permission( permission = Permission(
name=name, resource=resource, action=action, description=description name=name, resource=resource, action=action, description=description
) )
@@ -32,12 +44,26 @@ class PermissionService:
async def get_permission_by_id( async def get_permission_by_id(
cls, session: AsyncSession, permission_id: int cls, session: AsyncSession, permission_id: int
) -> Optional[Permission]: ) -> Optional[Permission]:
"""
根据ID获取权限
:param session: 数据库会话对象
:param permission_id: 权限ID
:return: 权限对象,如果不存在则返回None
"""
return await session.get(Permission, permission_id) return await session.get(Permission, permission_id)
@classmethod @classmethod
async def get_permission_by_name( async def get_permission_by_name(
cls, session: AsyncSession, name: str cls, session: AsyncSession, name: str
) -> Optional[Permission]: ) -> Optional[Permission]:
"""
根据名称获取权限
:param session: 数据库会话对象
:param name: 权限名称
:return: 权限对象,如果不存在则返回None
"""
result = await session.execute( result = await session.execute(
select(Permission).where(Permission.name == name) select(Permission).where(Permission.name == name)
) )
@@ -47,6 +73,14 @@ class PermissionService:
async def assign_permission_to_role( async def assign_permission_to_role(
cls, session: AsyncSession, role_id: int, permission_id: int cls, session: AsyncSession, role_id: int, permission_id: int
) -> RolePermission: ) -> RolePermission:
"""
将权限分配给角色
:param session: 数据库会话对象
:param role_id: 角色ID
:param permission_id: 权限ID
:return: 创建的角色权限关联对象
"""
role_permission = RolePermission(role_id=role_id, permission_id=permission_id) role_permission = RolePermission(role_id=role_id, permission_id=permission_id)
session.add(role_permission) session.add(role_permission)
await session.commit() await session.commit()
@@ -60,21 +94,41 @@ class PermissionService:
user_id: UUID, user_id: UUID,
permission_id: int, permission_id: int,
) -> User: ) -> User:
"""
将权限分配给用户
如果用户没有专属角色,则会为用户创建一个专属角色,然后将权限分配给该角色
:param session: 数据库会话对象
:param user_id: 用户UUID
:param permission_id: 权限ID
:return: 更新后的用户对象
:raises HTTPException: 当权限或用户不存在时抛出404错误,当角色或权限ID无效时抛出500错误
"""
# 验证权限是否存在
permission = await cls.get_permission_by_id(session, permission_id) permission = await cls.get_permission_by_id(session, permission_id)
if not permission: if not permission:
raise HTTPException(status_code=404, detail="Permission not found") raise HTTPException(status_code=404, detail="Permission not found")
# 验证用户是否存在
user = await UserService.get_user_by_id(user_id, session) user = await UserService.get_user_by_id(user_id, session)
if user is None: if user is None:
raise HTTPException(status_code=404, detail="用户不存在") raise HTTPException(status_code=404, detail="用户不存在")
# 获取或创建用户的专属角色
role = await RoleService.get_role_by_name(session, str(user_id)) role = await RoleService.get_role_by_name(session, str(user_id))
if role is None: if role is None:
role = await RoleService.create_role( role = await RoleService.create_role(
session, str(user_id), is_only_user=True session, str(user_id), is_only_user=True
) )
# 验证角色和权限ID的有效性
if role.id is None: if role.id is None:
raise HTTPException(status_code=500, detail="角色ID无效") raise HTTPException(status_code=500, detail="角色ID无效")
if permission.id is None: if permission.id is None:
raise HTTPException(status_code=500, detail="权限ID无效") raise HTTPException(status_code=500, detail="权限ID无效")
# 将权限分配给角色
await cls.assign_permission_to_role(session, role.id, permission.id) await cls.assign_permission_to_role(session, role.id, permission.id)
user.roles.append(role) user.roles.append(role)
await session.commit() await session.commit()
@@ -85,9 +139,21 @@ class PermissionService:
async def unbind_permission_from_role( async def unbind_permission_from_role(
cls, session: AsyncSession, role_id: int, permission_id: int cls, session: AsyncSession, role_id: int, permission_id: int
) -> Role: ) -> Role:
"""
从角色中解除权限绑定
:param session: 数据库会话对象
:param role_id: 角色ID
:param permission_id: 权限ID
:return: 更新后的角色对象
:raises HTTPException: 当角色不存在时抛出404错误
"""
# 验证角色是否存在
role = await RoleService.get_role_by_id(session, role_id) role = await RoleService.get_role_by_id(session, role_id)
if role is None: if role is None:
raise HTTPException(status_code=404, detail="Role not found") raise HTTPException(status_code=404, detail="Role not found")
# 从角色的权限列表中移除指定权限
role.permissions = [p for p in role.permissions if p.id != permission_id] role.permissions = [p for p in role.permissions if p.id != permission_id]
await session.commit() await session.commit()
await session.refresh(role) await session.refresh(role)
@@ -97,12 +163,27 @@ class PermissionService:
async def unbind_permission_from_user( async def unbind_permission_from_user(
cls, session: AsyncSession, user_id: UUID, permission_id: int cls, session: AsyncSession, user_id: UUID, permission_id: int
): ):
"""
从用户中解除权限绑定
通过找到用户的专属角色,从该角色中移除指定权限
:param session: 数据库会话对象
:param user_id: 用户UUID
:param permission_id: 权限ID
:raises HTTPException: 当用户或角色不存在时抛出404错误
"""
# 验证用户是否存在
user = await UserService.get_user_by_id(user_id, session) user = await UserService.get_user_by_id(user_id, session)
if not user: if not user:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
# 获取用户的专属角色
role = await RoleService.get_role_by_name(session, str(user.id)) role = await RoleService.get_role_by_name(session, str(user.id))
if role is None: if role is None:
raise HTTPException(status_code=404, detail="Role not found") raise HTTPException(status_code=404, detail="Role not found")
# 从角色权限列表中移除指定权限
role.permissions = [ role.permissions = [
permission permission
for permission in role.permissions for permission in role.permissions
+90
View File
@@ -10,6 +10,8 @@ from ..service.user import UserService
class RoleService: class RoleService:
"""角色服务类,提供角色管理相关的业务逻辑"""
@staticmethod @staticmethod
async def create_role( async def create_role(
session: AsyncSession, session: AsyncSession,
@@ -17,6 +19,18 @@ class RoleService:
description: Optional[str] = None, description: Optional[str] = None,
is_only_user: bool = False, is_only_user: bool = False,
) -> Role: ) -> Role:
"""
创建新角色
Args:
session: 数据库会话对象
name: 角色名称
description: 角色描述,可选
is_only_user: 是否仅用于用户,默认为False
Returns:
Role: 创建成功的角色对象
"""
role = Role(name=name, description=description, is_only_user=is_only_user) role = Role(name=name, description=description, is_only_user=is_only_user)
session.add(role) session.add(role)
await session.commit() await session.commit()
@@ -30,6 +44,14 @@ class RoleService:
user_id: UUID, user_id: UUID,
role_id: int, role_id: int,
): ):
"""
为用户分配角色
Args:
session: 数据库会话对象
user_id: 用户UUID
role_id: 角色ID
"""
user_role = UserRole(user_id=user_id, role_id=role_id) user_role = UserRole(user_id=user_id, role_id=role_id)
session.add(user_role) session.add(user_role)
await session.commit() await session.commit()
@@ -37,6 +59,16 @@ class RoleService:
@classmethod @classmethod
async def get_role_by_name(cls, session: AsyncSession, name: str) -> Optional[Role]: async def get_role_by_name(cls, session: AsyncSession, name: str) -> Optional[Role]:
"""
根据角色名称查询角色
Args:
session: 数据库会话对象
name: 角色名称
Returns:
Optional[Role]: 找到的角色对象,未找到则返回None
"""
result = await session.execute(select(Role).where(Role.name == name)) result = await session.execute(select(Role).where(Role.name == name))
return result.scalar_one_or_none() return result.scalar_one_or_none()
@@ -44,6 +76,16 @@ class RoleService:
async def get_role_by_id( async def get_role_by_id(
cls, session: AsyncSession, role_id: int cls, session: AsyncSession, role_id: int
) -> Optional[Role]: ) -> Optional[Role]:
"""
根据角色ID查询角色
Args:
session: 数据库会话对象
role_id: 角色ID
Returns:
Optional[Role]: 找到的角色对象,未找到则返回None
"""
result = await session.get(Role, role_id) result = await session.get(Role, role_id)
return result return result
@@ -53,9 +95,25 @@ class RoleService:
user_id: UUID, user_id: UUID,
role_id: int, role_id: int,
): ):
"""
从用户解绑角色
Args:
session: 数据库会话对象
user_id: 用户UUID
role_id: 角色ID
Returns:
User: 更新后的用户对象
Raises:
HTTPException: 当用户不存在时抛出404错误
"""
user = await UserService.get_user_by_id(user_id, session) user = await UserService.get_user_by_id(user_id, session)
if user is None: if user is None:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
# 过滤掉指定角色,保留其他角色
user.roles = [role for role in user.roles if role.id != role_id] user.roles = [role for role in user.roles if role.id != role_id]
await session.commit() await session.commit()
await session.refresh(user) await session.refresh(user)
@@ -66,14 +124,30 @@ class RoleService:
role_id: int, role_id: int,
session: AsyncSession, session: AsyncSession,
) -> bool: ) -> bool:
"""
删除角色
Args:
role_id: 角色ID
session: 数据库会话对象
Returns:
bool: 删除成功返回True
Raises:
HTTPException: 当角色被使用时抛出400错误,角色不存在时抛出404错误
"""
# 检查角色是否被用户使用
result = await session.execute( result = await session.execute(
select(UserRole).where(UserRole.role_id == role_id) select(UserRole).where(UserRole.role_id == role_id)
) )
if result.scalars().all(): if result.scalars().all():
raise HTTPException(status_code=400, detail="Role is not empty") raise HTTPException(status_code=400, detail="Role is not empty")
role = await RoleService.get_role_by_id(session, role_id) role = await RoleService.get_role_by_id(session, role_id)
if role is None: if role is None:
raise HTTPException(status_code=404, detail="Role not found") raise HTTPException(status_code=404, detail="Role not found")
await session.delete(role) await session.delete(role)
await session.commit() await session.commit()
return True return True
@@ -85,9 +159,25 @@ class RoleService:
role_update: RoleUpdate, role_update: RoleUpdate,
session: AsyncSession, session: AsyncSession,
) -> Optional[Role]: ) -> Optional[Role]:
"""
更新角色信息
Args:
role_id: 角色ID
role_update: 角色更新数据对象
session: 数据库会话对象
Returns:
Optional[Role]: 更新后的角色对象
Raises:
HTTPException: 当角色不存在时抛出404错误
"""
role = await cls.get_role_by_id(session, role_id) role = await cls.get_role_by_id(session, role_id)
if role is None: if role is None:
raise HTTPException(status_code=404, detail="Role not found") raise HTTPException(status_code=404, detail="Role not found")
# 只更新提供的字段,保持原有值不变
role.name = role_update.name if role_update.name else role.name role.name = role_update.name if role_update.name else role.name
role.description = ( role.description = (
role_update.description if role_update.description else role.description role_update.description if role_update.description else role.description
+56
View File
@@ -7,8 +7,20 @@ from ..model.todo import TodoCreate, Todo, TodoUpdate
class TodoService: class TodoService:
"""待办事项服务类,提供待办事项的增删改查操作"""
@staticmethod @staticmethod
async def create_todo(todo: TodoCreate, session: AsyncSession) -> Todo: async def create_todo(todo: TodoCreate, session: AsyncSession) -> Todo:
"""
创建新的待办事项
Args:
todo: 待办事项创建数据对象,包含待办事项的初始信息
session: 异步数据库会话对象,用于执行数据库操作
Returns:
Todo: 创建成功后的待办事项对象,包含生成的ID等信息
"""
db_todo = Todo.model_validate(todo) db_todo = Todo.model_validate(todo)
session.add(db_todo) session.add(db_todo)
await session.commit() await session.commit()
@@ -21,6 +33,17 @@ class TodoService:
limit: int, limit: int,
session: AsyncSession, session: AsyncSession,
) -> Sequence[Todo]: ) -> Sequence[Todo]:
"""
获取待办事项列表(支持分页)
Args:
offset: 查询偏移量,指定从第几条记录开始查询
limit: 查询限制数量,指定最多返回多少条记录
session: 异步数据库会话对象,用于执行数据库操作
Returns:
Sequence[Todo]: 待办事项对象列表,按查询条件返回的结果集
"""
result = await session.execute(select(Todo).offset(offset).limit(limit)) result = await session.execute(select(Todo).offset(offset).limit(limit))
todos = result.scalars().all() todos = result.scalars().all()
return todos return todos
@@ -30,6 +53,16 @@ class TodoService:
todo_id: int, todo_id: int,
session: AsyncSession, session: AsyncSession,
) -> Optional[Todo]: ) -> Optional[Todo]:
"""
根据ID获取单个待办事项
Args:
todo_id: 待办事项的唯一标识ID
session: 异步数据库会话对象,用于执行数据库操作
Returns:
Optional[Todo]: 如果找到则返回待办事项对象,否则返回None
"""
todo = await session.get(Todo, todo_id) todo = await session.get(Todo, todo_id)
return todo return todo
@@ -39,9 +72,22 @@ class TodoService:
todo_update: TodoUpdate, todo_update: TodoUpdate,
session: AsyncSession, session: AsyncSession,
) -> Optional[Todo]: ) -> Optional[Todo]:
"""
更新待办事项信息
Args:
todo_id: 待办事项的唯一标识ID
todo_update: 待办事项更新数据对象,包含需要更新的字段信息
session: 异步数据库会话对象,用于执行数据库操作
Returns:
Optional[Todo]: 如果更新成功则返回更新后的待办事项对象,如果待办事项不存在则返回None
"""
todo = await session.get(Todo, todo_id) todo = await session.get(Todo, todo_id)
if not todo: if not todo:
return None return None
# 提取需要更新的字段数据,排除未设置的字段
update_data = todo_update.model_dump(exclude_unset=True) update_data = todo_update.model_dump(exclude_unset=True)
for key, value in update_data.items(): for key, value in update_data.items():
@@ -57,6 +103,16 @@ class TodoService:
todo_id: int, todo_id: int,
session: AsyncSession, session: AsyncSession,
) -> bool: ) -> bool:
"""
删除待办事项
Args:
todo_id: 待办事项的唯一标识ID
session: 异步数据库会话对象,用于执行数据库操作
Returns:
bool: 如果删除成功返回True,如果待办事项不存在返回False
"""
todo = await session.get(Todo, todo_id) todo = await session.get(Todo, todo_id)
if not todo: if not todo:
return False return False
+40
View File
@@ -13,7 +13,22 @@ config = get_config()
class UploadService: class UploadService:
"""
文件上传服务类
提供文件上传功能,支持将上传的文件保存到指定目录,
并生成唯一的文件名以避免冲突。
"""
def __init__(self, upload_dir: str, chunk_size: int, file_type: str): def __init__(self, upload_dir: str, chunk_size: int, file_type: str):
"""
初始化上传服务
Args:
upload_dir: 文件上传的根目录路径
chunk_size: 文件读取的块大小(字节),用于控制内存使用
file_type: 文件类型标识,用于创建对应的子目录
"""
self.upload_dir = upload_dir self.upload_dir = upload_dir
self.file_type = file_type self.file_type = file_type
self.chunk_size = chunk_size self.chunk_size = chunk_size
@@ -21,12 +36,35 @@ class UploadService:
UPLOAD_DIR.mkdir(parents=True, exist_ok=True) UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
async def upload(self, file_data: UploadFile): async def upload(self, file_data: UploadFile):
"""
异步上传文件
接收上传的文件数据,生成唯一文件名,并将文件分块写入到服务器磁盘。
上传成功后返回文件的访问URL和新文件名。
Args:
file_data: FastAPI的UploadFile对象,包含上传的文件数据和元信息
Returns:
tuple: 包含两个元素的元组
- str: 文件的完整访问URL路径
- str: 生成的新文件名(UUID + 原始扩展名)
Raises:
HTTPException:
- 400: 当文件名为空或无效时抛出
- 500: 当文件写入过程中发生异常时抛出
"""
if file_data.filename is None: if file_data.filename is None:
raise HTTPException(status_code=400, detail="Invalid file name") raise HTTPException(status_code=400, detail="Invalid file name")
# 提取文件扩展名并生成唯一的UUID文件名
ext = os.path.splitext(file_data.filename)[1].lower() ext = os.path.splitext(file_data.filename)[1].lower()
new_filename = f"{uuid.uuid4().hex}{ext}" new_filename = f"{uuid.uuid4().hex}{ext}"
file_path = os.path.join(self.upload_dir, self.file_type, new_filename) file_path = os.path.join(self.upload_dir, self.file_type, new_filename)
try: try:
# 分块读取上传文件并异步写入磁盘,避免大文件占用过多内存
async with aiofiles.open(file_path, "wb") as out_file: async with aiofiles.open(file_path, "wb") as out_file:
while chunk := await file_data.read(self.chunk_size): while chunk := await file_data.read(self.chunk_size):
await out_file.write(chunk) await out_file.write(chunk)
@@ -34,6 +72,8 @@ class UploadService:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
finally: finally:
await file_data.close() await file_data.close()
# 构建并返回文件的访问URL和新文件名
return ( return (
f"{config['default']['prefix']}/{self.file_type}/{new_filename}".replace( f"{config['default']['prefix']}/{self.file_type}/{new_filename}".replace(
"\\", "/" "\\", "/"
+37
View File
@@ -9,19 +9,56 @@ from ..core.security import get_password_hash
class UserService: class UserService:
"""用户服务类,提供用户相关的业务逻辑操作"""
@staticmethod @staticmethod
async def get_user_by_email(email: str, session: AsyncSession) -> Optional[User]: async def get_user_by_email(email: str, session: AsyncSession) -> Optional[User]:
"""
根据邮箱地址查询用户
Args:
email: 用户的邮箱地址
session: 异步数据库会话
Returns:
如果找到则返回用户对象,否则返回None
"""
result = await session.execute(select(User).where(User.email == email)) # type: ignore result = await session.execute(select(User).where(User.email == email)) # type: ignore
return result.scalar_one_or_none() return result.scalar_one_or_none()
@staticmethod @staticmethod
async def get_user_by_id(user_id: UUID, session: AsyncSession) -> Optional[User]: async def get_user_by_id(user_id: UUID, session: AsyncSession) -> Optional[User]:
"""
根据用户ID查询用户
Args:
user_id: 用户的UUID标识
session: 异步数据库会话
Returns:
如果找到则返回用户对象,否则返回None
"""
return await session.get(User, user_id) return await session.get(User, user_id)
@staticmethod @staticmethod
async def create_user( async def create_user(
session: AsyncSession, email: str, password: str, **kwargs session: AsyncSession, email: str, password: str, **kwargs
) -> User: ) -> User:
"""
创建新用户
Args:
session: 异步数据库会话
email: 用户的邮箱地址
password: 用户的明文密码,将自动进行哈希加密
**kwargs: 其他用户属性参数
Returns:
创建成功的用户对象
Raises:
IntegrityError: 当邮箱已存在时可能抛出完整性错误
"""
hashed = get_password_hash(password) hashed = get_password_hash(password)
user = User(email=email, hashed_password=hashed, **kwargs) user = User(email=email, hashed_password=hashed, **kwargs)
session.add(user) session.add(user)
+14
View File
@@ -8,6 +8,20 @@ config = get_config()
async def init_tasks_callback(task_service: TaskService): async def init_tasks_callback(task_service: TaskService):
"""
初始化任务回调函数
在开发环境下自动添加数据库初始化定时任务。
Args:
task_service (TaskService): 任务服务实例,用于管理和调度定时任务
Returns:
None: 此函数不返回任何值
Note:
仅在开发环境(env == "development")下执行数据库初始化任务
"""
if config["default"]["env"] == "development": if config["default"]["env"] == "development":
await task_service.add_date_job( await task_service.add_date_job(
+50
View File
@@ -10,6 +10,23 @@ logger = get_logger("scheduler")
async def dev_init_admin(session): async def dev_init_admin(session):
"""
初始化系统管理员用户
检查是否存在邮箱为"admin"的管理员用户,如果不存在则创建一个新的超级管理员账户。
该函数用于系统首次部署时的管理员账户初始化。
Args:
session: 数据库会话对象,用于执行数据库操作
Returns:
None
Note:
- 默认管理员邮箱和密码均为"admin"
- 如果管理员已存在,则跳过创建并记录日志
- 新创建的用户具有超级管理员权限(is_superuser=True)
"""
admin_user = await UserService.get_user_by_email("admin", session) admin_user = await UserService.get_user_by_email("admin", session)
if admin_user: # type: ignore if admin_user: # type: ignore
logger.info("管理员用户已存在") # type: ignore logger.info("管理员用户已存在") # type: ignore
@@ -18,6 +35,24 @@ async def dev_init_admin(session):
async def init_permission(session): async def init_permission(session):
"""
初始化系统权限数据
遍历所有预定义的权限常量,检查数据库中是否已存在对应权限记录。
对于不存在的权限,创建新的权限记录并关联对应的资源和操作类型。
Args:
session: 数据库会话对象,用于执行数据库操作
Returns:
None
Note:
- 权限格式为"resource:action",例如"user:create"
- 使用冒号分隔资源和操作,如果缺少操作部分则默认为空字符串
- 已存在的权限会被跳过,避免重复创建
- 每个新创建的权限都会记录到日志中
"""
for permission in PermissionConstant.get_all(): for permission in PermissionConstant.get_all():
parts = permission.split(":", 1) parts = permission.split(":", 1)
resource = parts[0] resource = parts[0]
@@ -35,6 +70,21 @@ async def init_permission(session):
async def init_db(): async def init_db():
"""
执行数据库初始化流程
协调执行所有数据库初始化任务,包括管理员账户创建和权限数据初始化。
该函数是系统启动时数据库初始化的入口点。
Returns:
None
Note:
- 通过任务服务获取异步会话工厂
- 如果会话工厂未初始化,则直接返回不执行任何操作
- 按顺序执行:先初始化管理员,再初始化权限
- 使用异步上下文管理器确保会话正确关闭
"""
task_service = get_task_service_instance() task_service = get_task_service_instance()
if task_service.async_session_maker is None: if task_service.async_session_maker is None:
return return
+91 -13
View File
@@ -1,21 +1,31 @@
import platform
from module_bank import PythonToSQLite
from module_bank.encryption import Encryption
import os import os
from pathlib import Path import platform
import shutil import shutil
import subprocess import subprocess
import sys import sys
import asyncio import asyncio
from pathlib import Path
from module_bank import PythonToSQLite
from module_bank.encryption import Encryption
from sqlite_vfs.core import SQLiteVFS from sqlite_vfs.core import SQLiteVFS
from sqlite_vfs.folder_packer import FolderPacker from sqlite_vfs.folder_packer import FolderPacker
is_windows = platform.system() == "Windows" is_windows = platform.system() == "Windows"
def build_vue_web(): def build_vue_web():
"""打包 Vue Web""" """
打包 Vue Web 前端项目
该函数执行以下操作:
1. 运行 npm build 命令构建 Vue 项目
2. 将构建产物打包到 SQLite VFS 虚拟文件系统中
Returns:
None: 无返回值,通过打印信息反馈执行结果
"""
# 构建 Vue Web 命令 # 构建 Vue Web 命令
dist_dir = "./src/chongming-web/dist" dist_dir = "./src/chongming-web/dist"
output_db = "./build/static.svfs" output_db = "./build/static.svfs"
@@ -43,12 +53,12 @@ def build_vue_web():
print("Vue Web 打包成功") print("Vue Web 打包成功")
if result.stdout: if result.stdout:
print(result.stdout) print(result.stdout)
# 检查 dist 目录是否存在 # 检查 dist 目录是否存在
if not os.path.exists(dist_dir): if not os.path.exists(dist_dir):
print(f"错误: dist 目录不存在: {dist_dir}") print(f"错误: dist 目录不存在: {dist_dir}")
return return
vfs = SQLiteVFS(output_db, compress=False) vfs = SQLiteVFS(output_db, compress=False)
packer = FolderPacker( packer = FolderPacker(
sqlite_vfs=vfs, sqlite_vfs=vfs,
@@ -58,12 +68,27 @@ def build_vue_web():
try: try:
packer.pack_folder(dist_dir) packer.pack_folder(dist_dir)
print(f"✅ 打包完成,文件保存为 {output_db}") print(f"✅ 打包完成,文件保存为 {output_db}")
finally: finally:
vfs.close() vfs.close()
def run_pyarmor_obfuscate(): def run_pyarmor_obfuscate():
"""
使用 PyArmor 对关键 Python 文件进行代码混淆
该函数遍历预定义的文件列表,对每个文件执行 pyarmor gen 命令进行混淆处理,
以保护源代码不被轻易反编译。
处理的文件包括:
- main.py (主入口文件)
- server.py (服务器文件)
- utils/launch.py (启动工具)
- utils/config.py (配置工具)
Returns:
None: 无返回值,遇到错误时提前返回
"""
# pyarmor obfuscate --output=obfuscated_script.py original_script.py # pyarmor obfuscate --output=obfuscated_script.py original_script.py
obfuscate_files = [ obfuscate_files = [
("main.py", "./"), ("main.py", "./"),
@@ -89,7 +114,18 @@ def run_pyarmor_obfuscate():
def run_pyinstaller(): def run_pyinstaller():
"""运行 PyInstaller 打包成可执行文件""" """
使用 PyInstaller 将应用打包成单文件可执行程序
该函数根据操作系统平台(Windows/非Windows)构建不同的 PyInstaller 命令,
包含所有必要的隐藏导入以确保依赖库被正确打包。
Windows 和非 Windows 平台的主要区别:
- 非 Windows 平台额外包含 gunicorn 相关模块
Returns:
None: 无返回值,通过打印信息反馈执行结果
"""
try: try:
# 构建 PyInstaller 命令 # 构建 PyInstaller 命令
if is_windows: if is_windows:
@@ -230,19 +266,36 @@ def run_pyinstaller():
result = subprocess.run(cmd, cwd="./build", capture_output=True, text=True) result = subprocess.run(cmd, cwd="./build", capture_output=True, text=True)
if result.returncode == 0: if result.returncode == 0:
print("PyInstaller 打包成功!") print("PyInstaller 打包成功!")
print(result.stdout) print(result.stdout)
else: else:
print("PyInstaller 打包失败!") print("PyInstaller 打包失败!")
print("错误信息:", result.stderr) print("错误信息:", result.stderr)
except FileNotFoundError: except FileNotFoundError:
print("错误:未找到 PyInstaller,请先安装 PyInstaller:pip install pyinstaller") print("错误:未找到 PyInstaller,请先安装 PyInstaller:pip install pyinstaller")
except Exception as e: except Exception as e:
print(f"PyInstaller 执行出错: {e}") print(f"PyInstaller 执行出错: {e}")
async def init_database(): async def init_database():
"""
异步初始化数据库
该函数执行以下操作:
1. 从配置中读取数据库连接信息
2. 创建异步数据库引擎和会话工厂
3. 设置 SQLite 优化参数(WAL 模式和同步策略)
4. 创建所有 SQLModel 定义的数据库表
5. 初始化权限系统和默认管理员账户
Raises:
ValueError: 当数据库配置不存在时抛出
OperationalError: 当数据库操作出现异常时抛出(已存在表的情况除外)
Returns:
None: 无返回值
"""
from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy.ext.asyncio import create_async_engine
from sqlmodel import SQLModel, text from sqlmodel import SQLModel, text
from sqlalchemy.exc import OperationalError from sqlalchemy.exc import OperationalError
@@ -284,6 +337,23 @@ async def init_database():
async def main(): async def main():
"""
主构建流程函数
该函数协调整个应用的构建过程,按顺序执行以下步骤:
1. 加密打包 Python 模块(app 和 plugins)到 SQLite 数据库文件
2. 创建并准备 build 目录结构
3. 复制必要的源文件和配置文件到 build 目录
4. 注入动态生成的加密密钥到配置文件
5. 对关键文件进行代码混淆
6. 使用 PyInstaller 打包成可执行文件
7. 清理临时构建文件
8. 初始化数据库
9. 构建 Vue 前端项目
Returns:
None: 无返回值
"""
# 打包模块 # 打包模块
key = Encryption.generate_key() key = Encryption.generate_key()
resources_path = Path("resources") resources_path = Path("resources")
@@ -367,4 +437,12 @@ async def main():
def build(): def build():
"""
构建入口函数
该函数作为整个构建流程的入口点,通过 asyncio.run() 启动异步主构建流程。
Returns:
None: 无返回值
"""
asyncio.run(main()) asyncio.run(main())
+150 -26
View File
@@ -26,6 +26,7 @@ class TokenCacheData:
token_type: str = "access" # access 或 refresh token_type: str = "access" # access 或 refresh
def __post_init__(self): def __post_init__(self):
"""初始化后处理,设置默认时间戳"""
if self.created_at is None: if self.created_at is None:
self.created_at = datetime.utcnow().isoformat() self.created_at = datetime.utcnow().isoformat()
if self.last_used is None: if self.last_used is None:
@@ -33,9 +34,19 @@ class TokenCacheData:
class JWTCache: class JWTCache:
"""JWT Token 缓存管理器""" """JWT Token 缓存管理器
负责 JWT Token 的创建、验证、失效和会话管理。
使用 Redis 缓存存储 Token 哈希和用户会话索引,支持多设备登录和会话限制。
"""
def __init__(self, config, cache): def __init__(self, config, cache):
"""初始化 JWT 缓存管理器
Args:
config: 配置字典,包含 security 相关配置项
cache: 缓存实例,提供 get/set/delete/exists/keys 等方法
"""
self.config = config self.config = config
self.lock = threading.RLock() self.lock = threading.RLock()
@@ -46,7 +57,14 @@ class JWTCache:
self.algorithm = self.config["security"]["algorithm"] self.algorithm = self.config["security"]["algorithm"]
def _json_encode(self, obj): def _json_encode(self, obj):
"""自定义 JSON 编码器,处理 datetime 对象""" """自定义 JSON 编码器,处理 datetime 对象
Args:
obj: 需要序列化的对象
Returns:
JSON 字符串
"""
def convert_datetime(o): def convert_datetime(o):
if isinstance(o, datetime): if isinstance(o, datetime):
@@ -58,11 +76,25 @@ class JWTCache:
return json.dumps(obj, default=convert_datetime) return json.dumps(obj, default=convert_datetime)
def _json_decode(self, s): def _json_decode(self, s):
"""自定义 JSON 解码器""" """自定义 JSON 解码器
Args:
s: JSON 字符串
Returns:
解码后的 Python 对象
"""
return json.loads(s) return json.loads(s)
def _hash_token(self, token: str) -> str: def _hash_token(self, token: str) -> str:
"""Token 哈希化(避免存储明文 Token)""" """Token 哈希化(避免存储明文 Token)
Args:
token: 原始 JWT Token 字符串
Returns:
SHA256 哈希值(十六进制字符串)
"""
return hashlib.sha256(token.encode()).hexdigest() return hashlib.sha256(token.encode()).hexdigest()
def create_token( def create_token(
@@ -74,16 +106,18 @@ class JWTCache:
user_agent: Optional[str] = None, user_agent: Optional[str] = None,
ip_address: Optional[str] = None, ip_address: Optional[str] = None,
) -> str: ) -> str:
""" """创建并缓存 JWT Token
创建并缓存 JWT Token
生成新的 JWT Token,将其哈希值和元数据存储到缓存中,
并更新用户会话索引。如果超出会话限制,会自动清理最早的会话。
Args: Args:
user_id: 用户ID user_id: 用户ID
user_data: 用户数据 user_data: 用户数据,将嵌入到 Token payload 中
token_type: Token 类型(access 或 refresh) token_type: Token 类型(access 或 refresh)
device_id: 设备ID device_id: 设备ID,用于标识登录设备
user_agent: 用户代理 user_agent: 用户代理字符串,记录客户端信息
ip_address: IP地址 ip_address: IP地址,记录登录来源
Returns: Returns:
JWT Token 字符串 JWT Token 字符串
@@ -140,7 +174,11 @@ class JWTCache:
return token return token
def _generate_jti(self) -> str: def _generate_jti(self) -> str:
"""生成唯一的 JWT ID""" """生成唯一的 JWT ID
Returns:
UUID v4 字符串,用作 Token 的唯一标识符
"""
import uuid import uuid
return str(uuid.uuid4()) return str(uuid.uuid4())
@@ -148,7 +186,16 @@ class JWTCache:
def _update_user_session_index( def _update_user_session_index(
self, user_id: str, token_hash: str, token_type: str self, user_id: str, token_hash: str, token_type: str
): ):
"""更新用户会话索引""" """更新用户会话索引
维护每个用户每种 Token 类型的会话列表,限制最大数量为 100。
当超过限制时,保留最新的 100 个会话。
Args:
user_id: 用户ID
token_hash: Token 的 SHA256 哈希值
token_type: Token 类型(access 或 refresh)
"""
with self.lock: with self.lock:
key = f"user:{user_id}:{token_type}" key = f"user:{user_id}:{token_type}"
user_tokens = self.cache.get(key, []) user_tokens = self.cache.get(key, [])
@@ -164,7 +211,14 @@ class JWTCache:
self.cache.set(key, user_tokens) self.cache.set(key, user_tokens)
def _enforce_session_limit(self, user_id: str): def _enforce_session_limit(self, user_id: str):
"""强制用户会话限制""" """强制用户会话限制
检查用户的 access 和 refresh token 数量,如果超过配置的最大会话数,
则删除最早的会话以维持限制。
Args:
user_id: 用户ID
"""
max_sessions = self.config["security"]["max_sessions_per_user"] max_sessions = self.config["security"]["max_sessions_per_user"]
access_key = f"user:{user_id}:access" access_key = f"user:{user_id}:access"
@@ -195,15 +249,22 @@ class JWTCache:
def validate_token( def validate_token(
self, token: str, token_type: str = "access" self, token: str, token_type: str = "access"
) -> Optional[Dict[str, Any]]: ) -> Optional[Dict[str, Any]]:
""" """验证 JWT Token
验证 JWT Token
执行多层验证:JWT 签名验证、类型检查、缓存存在性检查、
活跃状态检查和过期时间检查。验证成功后更新最后使用时间。
Args: Args:
token: JWT Token token: JWT Token 字符串
token_type: 期望的 Token 类型 token_type: 期望的 Token 类型(access 或 refresh)
Returns: Returns:
验证成功返回用户数据,失败返回 None 验证成功返回包含用户信息的字典,包含以下字段:
- user_id: 用户ID
- device_id: 设备ID
- payload: JWT payload 数据
- cache_data: 缓存中的完整会话数据
验证失败返回 None
""" """
try: try:
# 解码验证 Token # 解码验证 Token
@@ -259,7 +320,14 @@ class JWTCache:
return None return None
def invalidate_token(self, token: str): def invalidate_token(self, token: str):
"""使单个 Token 失效""" """使单个 Token 失效
通过标记缓存中的 Token 为非活跃状态来实现失效,
而不是直接删除,以便保留审计信息。
Args:
token: 需要失效的 JWT Token 字符串
"""
with self.lock: with self.lock:
token_hash = self._hash_token(token) token_hash = self._hash_token(token)
@@ -270,7 +338,16 @@ class JWTCache:
self.cache.set(f"token:{token_hash}", cache_data) self.cache.set(f"token:{token_hash}", cache_data)
def invalidate_user_tokens(self, user_id: str, token_type: Optional[str] = None): def invalidate_user_tokens(self, user_id: str, token_type: Optional[str] = None):
"""使用户的所有 Token 失效""" """使用户的所有 Token 失效
批量失效指定用户的所有 Token,可用于用户登出、密码修改等场景。
可以指定失效特定类型的 Token,或者同时失效 access 和 refresh token。
Args:
user_id: 用户ID
token_type: 可选,指定要失效的 Token 类型(access 或 refresh)。
如果不指定,则失效该用户的所有类型 Token
"""
with self.lock: with self.lock:
if token_type: if token_type:
keys = [f"user:{user_id}:{token_type}"] keys = [f"user:{user_id}:{token_type}"]
@@ -292,7 +369,24 @@ class JWTCache:
self.cache.set(key, []) self.cache.set(key, [])
def get_user_sessions(self, user_id: str) -> List[Dict[str, Any]]: def get_user_sessions(self, user_id: str) -> List[Dict[str, Any]]:
"""获取用户的所有活跃会话""" """获取用户的所有活跃会话
遍历用户的所有 Token 会话,过滤出当前活跃且未过期的会话,
返回包含设备信息、时间戳等详细信息的会话列表。
Args:
user_id: 用户ID
Returns:
活跃会话列表,每个会话包含以下字段:
- token_type: Token 类型
- device_id: 设备ID
- user_agent: 用户代理
- ip_address: IP地址
- created_at: 创建时间
- last_used: 最后使用时间
- expires_at: 过期时间
"""
with self.lock: with self.lock:
sessions = [] sessions = []
@@ -325,7 +419,14 @@ class JWTCache:
return sessions return sessions
def cleanup_expired(self): def cleanup_expired(self):
"""清理过期数据""" """清理过期数据
扫描所有 Token 缓存,删除已过期的 Token 记录。
建议定期调用此方法以释放缓存空间。
Returns:
清理的过期 Token 数量
"""
with self.lock: with self.lock:
cleaned_count = 0 cleaned_count = 0
current_time = datetime.utcnow() current_time = datetime.utcnow()
@@ -345,7 +446,21 @@ _jwt_cache = None
def get_jwt_cache(config=None, cache=None) -> JWTCache: def get_jwt_cache(config=None, cache=None) -> JWTCache:
"""获取 JWT 缓存实例(单例模式)""" """获取 JWT 缓存实例(单例模式)
确保整个应用中只有一个 JWTCache 实例,避免重复初始化和资源浪费。
首次调用时必须提供 config 和 cache 参数。
Args:
config: 配置字典,仅在首次初始化时需要
cache: 缓存实例,仅在首次初始化时需要
Returns:
JWTCache 单例实例
Raises:
ValueError: 首次调用时如果 config 或 cache 为空则抛出异常
"""
global _jwt_cache global _jwt_cache
if _jwt_cache is None: if _jwt_cache is None:
if config is None or cache is None: if config is None or cache is None:
@@ -356,7 +471,10 @@ def get_jwt_cache(config=None, cache=None) -> JWTCache:
# Token 响应模型 # Token 响应模型
class TokenResponse(BaseModel): class TokenResponse(BaseModel):
"""Token 响应模型""" """Token 响应模型
用于登录接口返回 access token 和 refresh token
"""
access_token: str access_token: str
refresh_token: str refresh_token: str
@@ -366,7 +484,10 @@ class TokenResponse(BaseModel):
class RefreshTokenResponse(BaseModel): class RefreshTokenResponse(BaseModel):
"""刷新 Token 响应模型""" """刷新 Token 响应模型
用于刷新 token 接口,仅返回新的 access token
"""
access_token: str access_token: str
token_type: str = "bearer" token_type: str = "bearer"
@@ -374,7 +495,10 @@ class RefreshTokenResponse(BaseModel):
# Token 数据模型 # Token 数据模型
class TokenData(BaseModel): class TokenData(BaseModel):
"""Token 数据模型""" """Token 数据模型
用于从 Token 中提取的用户身份信息
"""
user_id: str user_id: str
username: Optional[str] = None username: Optional[str] = None
+289 -13
View File
@@ -21,6 +21,17 @@ from apscheduler.triggers.date import DateTrigger
def acquire_scheduler_lock(): def acquire_scheduler_lock():
"""
获取调度器分布式锁,确保只有一个进程实例运行调度器
通过在临时目录创建锁文件并使用文件系统锁机制,防止多个进程同时启动调度器。
支持 Windows 和 Unix/Linux 系统的不同锁实现。
Returns:
tuple: 包含两个元素的元组
- lock_file (file object or None): 锁文件对象,如果获取锁失败则为 None
- bool: 是否成功获取锁,成功为 True,失败为 False
"""
lock_path = os.path.join(tempfile.gettempdir(), "chongming_scheduler.lock") lock_path = os.path.join(tempfile.gettempdir(), "chongming_scheduler.lock")
lock_file = open(lock_path, "w") lock_file = open(lock_path, "w")
try: try:
@@ -39,9 +50,33 @@ def acquire_scheduler_lock():
class TaskService: class TaskService:
"""任务调度服务""" """
任务调度服务类
基于 APScheduler 实现的异步任务调度服务,支持多种触发器类型(间隔、Cron、一次性)。
使用 SQLite 数据库持久化存储任务信息,确保服务重启后任务不丢失。
通过文件锁机制保证多进程环境下只有一个实例执行调度任务。
"""
def __init__(self, config, logger, async_session_maker=None): def __init__(self, config, logger, async_session_maker=None):
"""
初始化任务调度服务
Args:
config (dict): 配置字典,包含调度器相关配置项
- scheduler.job_store_path (str): 任务存储数据库路径,默认为 "scheduler_jobs.db"
logger (logging.Logger): 日志记录器实例,用于记录调度器运行日志
async_session_maker (callable, optional): 异步会话工厂函数,用于数据库操作,默认为 None
Attributes:
async_session_maker: 异步会话工厂函数
job_store_db_path: 任务存储数据库文件路径
scheduler: AsyncIOScheduler 调度器实例
_started (bool): 调度器是否已启动的标志
lock_file: 锁文件对象
is_scheduler_worker (bool): 当前进程是否为调度器工作进程
logger: 日志记录器实例
"""
self.async_session_maker = async_session_maker self.async_session_maker = async_session_maker
# 使用独立的 SQLite 数据库存储任务(与业务数据库分开) # 使用独立的 SQLite 数据库存储任务(与业务数据库分开)
self.job_store_db_path = config.get("scheduler", {}).get( self.job_store_db_path = config.get("scheduler", {}).get(
@@ -69,7 +104,20 @@ class TaskService:
self.logger = logger self.logger = logger
async def start(self, init_tasks_callback: Optional[Callable] = None): async def start(self, init_tasks_callback: Optional[Callable] = None):
"""启动调度器""" """
启动任务调度器
仅在当前进程成功获取调度器锁时启动调度器,避免多进程重复启动。
启动后可选择执行初始化任务回调函数。
Args:
init_tasks_callback (callable, optional): 初始化任务回调函数,接收 TaskService 实例作为参数。
用于在调度器启动后注册初始任务。默认为 None。
Note:
- 如果调度器已经启动,不会重复启动
- 只有成功获取锁的进程才会启动调度器
"""
if self.is_scheduler_worker: if self.is_scheduler_worker:
if not self._started: if not self._started:
self.scheduler.start() self.scheduler.start()
@@ -79,7 +127,20 @@ class TaskService:
await init_tasks_callback(self) await init_tasks_callback(self)
async def shutdown(self, wait: bool = True): async def shutdown(self, wait: bool = True):
"""关闭调度器""" """
关闭任务调度器
优雅地关闭调度器,包括暂停所有运行中的任务、关闭调度器、释放文件锁。
Args:
wait (bool): 是否等待正在执行的任务完成后再关闭。
True 表示等待任务完成,False 表示立即关闭。默认为 True。
Note:
- 先暂停所有任务,再关闭调度器
- 释放文件锁以允许其他进程获取锁
- 使用 suppress 捕获所有异常,确保关闭过程不会抛出异常
"""
if self.lock_file: if self.lock_file:
with suppress(Exception): with suppress(Exception):
for job in await self.get_jobs(): for job in await self.get_jobs():
@@ -111,7 +172,28 @@ class TaskService:
kwargs: Optional[Dict] = None, kwargs: Optional[Dict] = None,
replace_existing: bool = True, replace_existing: bool = True,
) -> Job: ) -> Job:
"""添加间隔执行任务""" """
添加间隔执行任务
创建按照固定时间间隔重复执行的任务。
Args:
func (callable): 要执行的任务函数
seconds (int): 间隔秒数,默认为 0
minutes (int): 间隔分钟数,默认为 0
hours (int): 间隔小时数,默认为 0
days (int): 间隔天数,默认为 0
job_id (str, optional): 任务唯一标识符,如果未提供则自动生成
args (list, optional): 传递给任务函数的位置参数列表,默认为空列表
kwargs (dict, optional): 传递给任务函数的关键字参数字典,默认为空字典
replace_existing (bool): 如果存在相同 ID 的任务,是否替换它。默认为 True
Returns:
Job: 创建的任务对象
Example:
>>> await service.add_interval_job(my_func, minutes=5, job_id="cleanup_task")
"""
trigger = IntervalTrigger( trigger = IntervalTrigger(
seconds=seconds, minutes=minutes, hours=hours, days=days seconds=seconds, minutes=minutes, hours=hours, days=days
) )
@@ -142,7 +224,33 @@ class TaskService:
kwargs: Optional[Dict] = None, kwargs: Optional[Dict] = None,
replace_existing: bool = True, replace_existing: bool = True,
) -> Job: ) -> Job:
"""添加 cron 表达式任务""" """
添加 Cron 表达式任务
创建按照 Cron 表达式规则执行的任务,支持精确的时间调度。
Args:
func (callable): 要执行的任务函数
job_id (str, optional): 任务唯一标识符,如果未提供则自动生成
year (int, optional): 年份,支持通配符和范围
month (int, optional): 月份 (1-12)
day (int, optional): 日期 (1-31)
week (int, optional): 周数 (1-53)
day_of_week (int, optional): 星期几 (0-6, 0 表示周一)
hour (int, optional): 小时 (0-23)
minute (int, optional): 分钟 (0-59)
second (int, optional): 秒 (0-59)
args (list, optional): 传递给任务函数的位置参数列表,默认为空列表
kwargs (dict, optional): 传递给任务函数的关键字参数字典,默认为空字典
replace_existing (bool): 如果存在相同 ID 的任务,是否替换它。默认为 True
Returns:
Job: 创建的任务对象
Example:
>>> # 每天凌晨 2 点执行
>>> await service.add_cron_job(my_func, hour=2, minute=0, job_id="daily_task")
"""
trigger = CronTrigger( trigger = CronTrigger(
year=year, year=year,
month=month, month=month,
@@ -173,7 +281,27 @@ class TaskService:
kwargs: Optional[Dict] = None, kwargs: Optional[Dict] = None,
replace_existing: bool = True, replace_existing: bool = True,
) -> Job: ) -> Job:
"""添加一次性任务""" """
添加一次性任务
创建在指定时间点执行一次的任务,执行后自动移除。
Args:
func (callable): 要执行的任务函数
run_date (datetime, optional): 任务执行的具体时间,如果未提供则立即执行
job_id (str, optional): 任务唯一标识符,如果未提供则自动生成
args (list, optional): 传递给任务函数的位置参数列表,默认为空列表
kwargs (dict, optional): 传递给任务函数的关键字参数字典,默认为空字典
replace_existing (bool): 如果存在相同 ID 的任务,是否替换它。默认为 True
Returns:
Job: 创建的任务对象
Example:
>>> from datetime import datetime
>>> run_time = datetime(2024, 12, 25, 10, 0, 0)
>>> await service.add_date_job(my_func, run_date=run_time, job_id="christmas_task")
"""
trigger = DateTrigger(run_date=run_date) trigger = DateTrigger(run_date=run_date)
job = self.scheduler.add_job( job = self.scheduler.add_job(
func, func,
@@ -187,7 +315,21 @@ class TaskService:
return job return job
async def remove_job(self, job_id: str) -> bool: async def remove_job(self, job_id: str) -> bool:
"""删除任务""" """
删除任务
从调度器中移除指定任务,任务将不再执行。
Args:
job_id (str): 要删除的任务的唯一标识符
Returns:
bool: 删除是否成功,成功返回 True,失败返回 False
Note:
- 如果任务不存在或删除过程中发生异常,返回 False
- 会记录删除操作的日志
"""
try: try:
self.scheduler.remove_job(job_id) self.scheduler.remove_job(job_id)
self.logger.info(f"删除任务: {job_id}") self.logger.info(f"删除任务: {job_id}")
@@ -197,7 +339,21 @@ class TaskService:
return False return False
async def pause_job(self, job_id: str) -> bool: async def pause_job(self, job_id: str) -> bool:
"""暂停任务""" """
暂停任务
暂停指定任务的执行,任务保留在调度器中但不会触发。
Args:
job_id (str): 要暂停的任务的唯一标识符
Returns:
bool: 暂停是否成功,成功返回 True,失败返回 False
Note:
- 暂停的任务可以通过 resume_job 恢复
- 如果任务不存在或暂停过程中发生异常,返回 False
"""
try: try:
self.scheduler.pause_job(job_id) self.scheduler.pause_job(job_id)
self.logger.info(f"暂停任务: {job_id}") self.logger.info(f"暂停任务: {job_id}")
@@ -207,7 +363,21 @@ class TaskService:
return False return False
async def resume_job(self, job_id: str) -> bool: async def resume_job(self, job_id: str) -> bool:
"""恢复任务""" """
恢复任务
恢复之前被暂停的任务,使其继续按照原定计划执行。
Args:
job_id (str): 要恢复的任务的唯一标识符
Returns:
bool: 恢复是否成功,成功返回 True,失败返回 False
Note:
- 只能恢复已被暂停的任务
- 如果任务不存在或恢复过程中发生异常,返回 False
"""
try: try:
self.scheduler.resume_job(job_id) self.scheduler.resume_job(job_id)
self.logger.info(f"恢复任务: {job_id}") self.logger.info(f"恢复任务: {job_id}")
@@ -217,21 +387,72 @@ class TaskService:
return False return False
async def get_job(self, job_id: str) -> Optional[Dict]: async def get_job(self, job_id: str) -> Optional[Dict]:
"""获取任务信息""" """
获取单个任务信息
查询指定任务的详细信息,包括下次执行时间、触发器等。
Args:
job_id (str): 要查询的任务的唯一标识符
Returns:
dict or None: 任务信息字典,如果任务不存在则返回 None。
字典包含以下字段:
- id (str): 任务 ID
- name (str): 任务名称
- next_run_time (str or None): 下次执行时间的 ISO 格式字符串
- trigger (str): 触发器描述
- pending (bool): 任务是否处于待处理状态
- args (list): 任务函数的位置参数
- kwargs (dict): 任务函数的关键字参数
"""
job = self.scheduler.get_job(job_id) job = self.scheduler.get_job(job_id)
if job: if job:
return self._job_to_dict(job) return self._job_to_dict(job)
return None return None
async def get_jobs(self) -> List[Dict]: async def get_jobs(self) -> List[Dict]:
"""获取所有任务""" """
获取所有任务信息
查询调度器中所有任务的详细信息列表。
Returns:
list[dict]: 任务信息字典列表,每个字典的结构与 get_job 返回的相同。
如果没有任务,返回空列表。
"""
jobs = self.scheduler.get_jobs() jobs = self.scheduler.get_jobs()
return [self._job_to_dict(job) for job in jobs] return [self._job_to_dict(job) for job in jobs]
async def reschedule_job( async def reschedule_job(
self, job_id: str, trigger_type: str, **trigger_args self, job_id: str, trigger_type: str, **trigger_args
) -> Optional[Job]: ) -> Optional[Job]:
"""重新调度任务(修改触发时间)""" """
重新调度任务(修改触发时间)
修改现有任务的触发器配置,改变任务的执行时间规则。
Args:
job_id (str): 要重新调度的任务的唯一标识符
trigger_type (str): 触发器类型,可选值:
- "interval": 间隔触发器
- "cron": Cron 表达式触发器
- "date": 一次性触发器
**trigger_args: 触发器参数,根据 trigger_type 不同而不同:
- interval: seconds, minutes, hours, days
- cron: year, month, day, week, day_of_week, hour, minute, second
- date: run_date
Returns:
Job or None: 重新调度后的任务对象,如果任务不存在或重新调度失败则返回 None
Raises:
ValueError: 当传入不支持的 trigger_type 时抛出
Example:
>>> # 将任务改为每 10 分钟执行一次
>>> await service.reschedule_job("task_id", "interval", minutes=10)
"""
try: try:
job = self.scheduler.get_job(job_id) job = self.scheduler.get_job(job_id)
if not job: if not job:
@@ -254,7 +475,25 @@ class TaskService:
return None return None
def _job_to_dict(self, job: Job) -> Dict: def _job_to_dict(self, job: Job) -> Dict:
"""将 Job 对象转换为字典""" """
将 Job 对象转换为字典
内部辅助方法,将 APScheduler 的 Job 对象序列化为字典格式,
便于 API 返回和数据传输。
Args:
job (Job): APScheduler 的 Job 对象
Returns:
dict: 包含任务信息的字典,字段包括:
- id (str): 任务 ID
- name (str): 任务名称
- next_run_time (str or None): 下次执行时间的 ISO 8601 格式字符串
- trigger (str): 触发器的字符串描述
- pending (bool): 任务是否处于待处理状态
- args (list): 任务函数的位置参数列表
- kwargs (dict): 任务函数的关键字参数字典
"""
return { return {
"id": job.id, "id": job.id,
"name": job.name, "name": job.name,
@@ -272,6 +511,28 @@ _task_service: Optional[TaskService] = None
def get_task_service_instance(config=None, logger=None, async_session_maker=None): def get_task_service_instance(config=None, logger=None, async_session_maker=None):
"""
获取任务调度服务单例实例
使用全局变量实现单例模式,确保整个应用中只有一个 TaskService 实例。
首次调用时创建实例,后续调用直接返回已创建的实例。
Args:
config (dict, optional): 配置字典,仅在首次创建实例时需要
logger (logging.Logger, optional): 日志记录器,仅在首次创建实例时需要
async_session_maker (callable, optional): 异步会话工厂函数,仅在首次创建实例时需要
Returns:
TaskService: 任务调度服务单例实例
Raises:
ValueError: 如果首次调用时未提供 config 或 logger 参数
Note:
- 这是一个单例模式实现,确保全局只有一个调度器实例
- 首次调用必须提供 config 和 logger 参数
- 后续调用可以不提供参数,直接返回已创建的实例
"""
global _task_service global _task_service
if _task_service is None: if _task_service is None:
if config is None or logger is None: if config is None or logger is None:
@@ -281,4 +542,19 @@ def get_task_service_instance(config=None, logger=None, async_session_maker=None
async def get_task_service(request: Request) -> AsyncGenerator: async def get_task_service(request: Request) -> AsyncGenerator:
"""
FastAPI 依赖注入函数,获取任务调度服务实例
用于 FastAPI 路由函数的依赖注入,从应用状态中获取已初始化的任务调度服务。
Args:
request (Request): FastAPI 请求对象,从中获取应用实例
Returns:
TaskService: 从 request.app.state.task_service 获取的任务调度服务实例
Note:
- 此函数应在应用启动时通过 startup 事件初始化 task_service
- 适用于 FastAPI 的 Depends 依赖注入机制
"""
return request.app.state.task_service return request.app.state.task_service