modify: 更新日志配置

This commit is contained in:
2026-04-26 10:20:32 +08:00
parent 55463a9d07
commit 00b9e0ebc0
83 changed files with 4050 additions and 1755 deletions
+33 -8
View File
@@ -1,3 +1,4 @@
import time
from pathlib import Path
from contextlib import asynccontextmanager
@@ -8,23 +9,19 @@ from fastapi.staticfiles import StaticFiles
from sqlmodel import SQLModel, text
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.exc import OperationalError
from pydantic import BaseModel
from .core.config import get_config
from .core.cache import get_cache
from .core.logger import get_logger
from .core.scheduler import get_task_service_instance
from .core.static_files import SVFSStaticFiles
from .api import api_router
from .task import init_tasks_callback
class TaskRequest(BaseModel):
task_name: str
interval: int
from plugins.scheduler.scheduler import get_task_service_instance
config = get_config()
cache = get_cache()
logger = get_logger("app")
@@ -38,6 +35,9 @@ except KeyError: # pragma: no cover
@asynccontextmanager
async def lifespan(app: FastAPI):
from plugins import hello
from plugins.jwt.jwt_cache import get_jwt_cache
get_jwt_cache(config, cache)
hello()
database = config["database"]
@@ -73,7 +73,9 @@ async def lifespan(app: FastAPI):
raise
# 启动时:初始化任务服务
task_service = get_task_service_instance(async_session_maker)
task_service = get_task_service_instance(
config, get_logger("scheduler"), async_session_maker
)
app.state.task_service = task_service
await task_service.start(init_tasks_callback)
@@ -82,7 +84,7 @@ async def lifespan(app: FastAPI):
# 关闭时:释放引擎
await task_service.shutdown(wait=True)
await engine.dispose()
get_cache().close()
cache.close()
# 创建 FastAPI 应用
@@ -104,6 +106,21 @@ app.add_middleware(CORSMiddleware, **cors)
app.include_router(api_router, prefix=config["default"]["prefix"])
@app.middleware("http")
async def access_log_middleware(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration = (time.perf_counter() - start) * 1000
client = request.client.host if request.client else "-"
logger.info(
f"{request.method} {request.url.path} status={response.status_code} "
f"duration={duration:.2f}ms client={client}",
extra={"status_code": response.status_code}, # ← 关键
)
return response
# 根路由
@app.get("/", summary="根路径")
async def root():
@@ -116,6 +133,14 @@ async def root():
}
# from pydantic import BaseModel
# class TaskRequest(BaseModel):
# task_name: str
# interval: int
# @app.post("/schedule")
# async def add_task(
# request: TaskRequest, task_service: TaskService = Depends(get_task_service)
+1 -1
View File
@@ -2,7 +2,7 @@ from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from ..service.auth import get_auth_service
from ..core.jwt_cache import TokenData
from plugins.jwt.jwt_cache import TokenData
access_scheme = HTTPBearer(scheme_name="AccessToken")
+59 -23
View File
@@ -5,13 +5,15 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from ...core.database import get_session
from ...core.jwt_cache import TokenData, TokenResponse, RefreshTokenResponse
from ...core.security import verify_password
from ...service.user import UserService
from ...service.auth import get_auth_service
from ...model.user import UserCreate, UserRead, UserLogin
from ...model.user import UserCreate, UserRead, UserLogin, Userlogout, UserSession
from ...model.http_error import HTTPError, Detail
from ..deps import get_current_user
from plugins.jwt.jwt_cache import TokenData, TokenResponse, RefreshTokenResponse
router = APIRouter(tags=["authentication"])
@@ -19,16 +21,23 @@ access_scheme = HTTPBearer(scheme_name="AccessToken")
refresh_scheme = HTTPBearer(scheme_name="RefreshToken")
@router.post("/register", response_model=UserRead)
@router.post(
"/register",
response_model=UserRead,
responses={400: {"model": HTTPError, "description": "邮箱已被注册"}},
)
async def register(
user_data: UserCreate,
session: AsyncSession = Depends(get_session),
):
print(user_data.password)
existing = await UserService.get_user_by_email(user_data.email, session)
if existing:
raise HTTPException(status_code=400, detail="邮箱已被注册")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=Detail(
message="邮箱已被注册", code=status.HTTP_400_BAD_REQUEST
).model_dump(),
)
user = await UserService.create_user(
session=session,
@@ -40,7 +49,15 @@ async def register(
return user
@router.post("/login", response_model=TokenResponse)
@router.post(
"/login",
response_model=TokenResponse,
responses={
400: {"model": HTTPError, "description": "邮箱不存在"},
401: {"model": HTTPError, "description": "密码错误"},
402: {"model": HTTPError, "description": "用户被禁用"},
},
)
async def login(
user_data: UserLogin,
request: Request,
@@ -48,16 +65,29 @@ async def login(
):
user = await UserService.get_user_by_email(user_data.email, session)
if not user:
raise HTTPException(status_code=400, detail="邮箱或密码错误")
raise HTTPException(
status_code=400, detail=Detail(message="邮箱不存在", code=400).model_dump()
)
if not verify_password(user_data.password, user.hashed_password):
raise HTTPException(status_code=400, detail="邮箱或密码错误")
raise HTTPException(
status_code=400, detail=Detail(message="密码错误", code=401).model_dump()
)
if not user.is_active:
raise HTTPException(status_code=400, detail="用户被禁用")
raise HTTPException(
status_code=400, detail=Detail(message="用户被禁用", code=402).model_dump()
)
auth_service = get_auth_service()
return await auth_service.create_tokens(user, request, device_id=None)
@router.post("/refresh", response_model=RefreshTokenResponse, summary="刷新访问令牌")
@router.post(
"/refresh",
response_model=RefreshTokenResponse,
responses={
401: {"model": HTTPError, "description": "无效的刷新令牌"},
},
summary="刷新访问令牌",
)
async def refresh_token(
request: Request,
credentials: HTTPAuthorizationCredentials = Depends(refresh_scheme),
@@ -72,28 +102,28 @@ async def refresh_token(
if not new_access_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的刷新令牌",
detail=Detail(message="无效的刷新令牌", code=401).model_dump(),
headers={"WWW-Authenticate": "Bearer"},
)
return {"access_token": new_access_token, "token_type": "bearer"}
@router.post("/logout")
@router.post("/logout", response_model=Userlogout)
async def logout(
credentials: HTTPAuthorizationCredentials = Depends(access_scheme),
):
auth_service = get_auth_service()
token = credentials.credentials
await auth_service.logout(token)
return {"message": "登出成功"}
return {"message": "登出成功", "code": 200}
@router.post("/logout-all", summary="登出所有设备")
@router.post("/logout-all", summary="登出所有设备", response_model=Userlogout)
async def logout_all(current_user: TokenData = Depends(get_current_user)):
auth_service = get_auth_service()
await auth_service.logout_all(current_user.user_id)
return {"message": "所有设备已登出"}
return {"message": "所有设备已登出", "code": 200}
@router.get("/sessions", summary="获取用户会话")
@@ -102,14 +132,17 @@ async def get_sessions(current_user: TokenData = Depends(get_current_user)):
auth_service = get_auth_service()
sessions = await auth_service.get_user_sessions(current_user.user_id)
return {
"user_id": current_user.user_id,
"sessions": sessions,
"count": len(sessions),
}
return UserSession(
user_id=current_user.user_id, sessions=sessions, count=len(sessions)
)
@router.get("/me", response_model=UserRead, summary="获取当前用户信息")
@router.get(
"/me",
response_model=UserRead,
summary="获取当前用户信息",
responses={404: {"model": HTTPError, "description": "用户不存在"}},
)
async def get_current_user_info(
session: AsyncSession = Depends(get_session),
current_user: TokenData = Depends(get_current_user),
@@ -117,6 +150,9 @@ async def get_current_user_info(
"""获取当前登录用户的信息"""
user = await UserService.get_user_by_id(UUID(current_user.user_id), session)
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=Detail(message="用户不存在", code=404).model_dump(),
)
return user
+1
View File
@@ -64,6 +64,7 @@ class Server(TypedDict, total=False):
port: Required[int]
reload: bool
workers: int
access_log: bool
class Logging(TypedDict):
-383
View File
@@ -1,383 +0,0 @@
import json
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List
from pydantic import BaseModel
from dataclasses import dataclass, asdict
import hashlib
import threading
import jwt
from .config import get_config
from .cache import get_cache
@dataclass
class TokenCacheData:
"""Token 缓存数据结构"""
token_hash: str
user_id: str
device_id: Optional[str] = None
user_agent: Optional[str] = None
ip_address: Optional[str] = None
created_at: Optional[str] = None
expires_at: Optional[str] = None
last_used: Optional[str] = None
is_active: bool = True
payload_data: Optional[Dict[str, Any]] = None
token_type: str = "access" # access 或 refresh
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.utcnow().isoformat()
if self.last_used is None:
self.last_used = datetime.utcnow().isoformat()
class JWTCache:
"""JWT Token 缓存管理器"""
def __init__(self, db_path: Optional[str] = None):
self.config = get_config()
self.lock = threading.RLock()
self.cache = get_cache()
self.secret_key = self.config["security"]["secret_key"]
self.algorithm = self.config["security"]["algorithm"]
def _json_encode(self, obj):
"""自定义 JSON 编码器,处理 datetime 对象"""
def convert_datetime(o):
if isinstance(o, datetime):
return o.isoformat()
raise TypeError(
f"Object of type {o.__class__.__name__} is not JSON serializable"
)
return json.dumps(obj, default=convert_datetime)
def _json_decode(self, s):
"""自定义 JSON 解码器"""
return json.loads(s)
def _hash_token(self, token: str) -> str:
"""Token 哈希化(避免存储明文 Token)"""
return hashlib.sha256(token.encode()).hexdigest()
def create_token(
self,
user_id: str,
user_data: Dict[str, Any],
token_type: str = "access",
device_id: Optional[str] = None,
user_agent: Optional[str] = None,
ip_address: Optional[str] = None,
) -> str:
"""
创建并缓存 JWT Token
Args:
user_id: 用户ID
user_data: 用户数据
token_type: Token 类型(access 或 refresh)
device_id: 设备ID
user_agent: 用户代理
ip_address: IP地址
Returns:
JWT Token 字符串
"""
with self.lock:
# 计算过期时间
if token_type == "access":
expire_minutes = self.config["security"]["access_token_expire_minutes"]
expires_delta = timedelta(minutes=expire_minutes)
else:
expire_days = self.config["security"]["refresh_token_expire_days"]
expires_delta = timedelta(days=expire_days)
expire = datetime.utcnow() + expires_delta
# 构建 payload
payload = {
"sub": user_id,
"exp": expire,
"iat": datetime.utcnow(),
"type": token_type,
"data": user_data,
"device_id": device_id,
"jti": self._generate_jti(), # JWT ID,唯一标识
}
# 生成 JWT Token
token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
if isinstance(token, bytes):
token = token.decode("utf-8")
# 创建缓存数据
token_hash = self._hash_token(token)
cache_data = TokenCacheData(
token_hash=token_hash,
user_id=user_id,
device_id=device_id,
user_agent=user_agent,
ip_address=ip_address,
expires_at=expire.isoformat(), # 转换为字符串
payload_data=payload,
token_type=token_type,
)
# 存储到缓存
self.cache.set(f"token:{token_hash}", asdict(cache_data))
# 更新用户会话索引
self._update_user_session_index(user_id, token_hash, token_type)
# 检查用户会话数限制
self._enforce_session_limit(user_id)
return token
def _generate_jti(self) -> str:
"""生成唯一的 JWT ID"""
import uuid
return str(uuid.uuid4())
def _update_user_session_index(
self, user_id: str, token_hash: str, token_type: str
):
"""更新用户会话索引"""
with self.lock:
key = f"user:{user_id}:{token_type}"
user_tokens = self.cache.get(key, [])
# 添加新 token
user_tokens.append(token_hash)
# 只保留最近的一定数量的 token
max_tokens = 100 # 每个用户每种类型最多保留100个 token
if len(user_tokens) > max_tokens:
user_tokens = user_tokens[-max_tokens:]
self.cache.set(key, user_tokens)
def _enforce_session_limit(self, user_id: str):
"""强制用户会话限制"""
max_sessions = self.config["security"]["max_sessions_per_user"]
access_key = f"user:{user_id}:access"
refresh_key = f"user:{user_id}:refresh"
# 检查 access token 数量
access_tokens = self.cache.get(access_key, [])
if len(access_tokens) > max_sessions:
# 删除最早的 token
tokens_to_remove = access_tokens[:-max_sessions]
for token_hash in tokens_to_remove:
if self.cache.exists(f"token:{token_hash}"):
self.cache.delete(f"token:{token_hash}")
# 更新索引
self.cache.set(access_key, access_tokens[-max_sessions:])
# 检查 refresh token 数量
refresh_tokens = self.cache.get(refresh_key, [])
if len(refresh_tokens) > max_sessions:
tokens_to_remove = refresh_tokens[:-max_sessions]
for token_hash in tokens_to_remove:
if self.cache.exists(f"token:{token_hash}"):
self.cache.delete(f"token:{token_hash}")
self.cache.set(refresh_key, refresh_tokens[-max_sessions:])
def validate_token(
self, token: str, token_type: str = "access"
) -> Optional[Dict[str, Any]]:
"""
验证 JWT Token
Args:
token: JWT Token
token_type: 期望的 Token 类型
Returns:
验证成功返回用户数据,失败返回 None
"""
try:
# 解码验证 Token
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
# 检查 Token 类型
print(f"payload: {payload}")
if payload.get("type") != token_type:
return None
user_id = payload.get("sub")
jti = payload.get("jti")
if not user_id or not jti:
return None
token_hash = self._hash_token(token)
# 检查缓存
with self.lock:
cache_data = self.cache.get(f"token:{token_hash}")
if not cache_data:
return None
# 检查 Token 是否激活
if not cache_data.get("is_active", True):
return None
# 检查是否过期
expires_at = datetime.fromisoformat(cache_data["expires_at"])
if datetime.utcnow() > expires_at:
# 标记为不活跃
cache_data["is_active"] = False
self.cache.set(f"token:{token_hash}", cache_data)
return None
# 更新最后使用时间
cache_data["last_used"] = datetime.utcnow().isoformat()
self.cache.set(f"token:{token_hash}", cache_data)
return {
"user_id": user_id,
"device_id": cache_data.get("device_id"),
"payload": payload,
"cache_data": cache_data,
}
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
except Exception:
return None
def invalidate_token(self, token: str):
"""使单个 Token 失效"""
with self.lock:
token_hash = self._hash_token(token)
# 标记为不活跃
if self.cache.exists(f"token:{token_hash}"):
cache_data = self.cache.get(f"token:{token_hash}")
cache_data["is_active"] = False
self.cache.set(f"token:{token_hash}", cache_data)
def invalidate_user_tokens(self, user_id: str, token_type: Optional[str] = None):
"""使用户的所有 Token 失效"""
with self.lock:
if token_type:
keys = [f"user:{user_id}:{token_type}"]
else:
keys = [f"user:{user_id}:access", f"user:{user_id}:refresh"]
for key in keys:
token_hashes = self.cache.get(key, [])
for token_hash in token_hashes:
# 添加到黑名单
# 注意:这里我们只有 token_hash,没有原始 token
# 所以只能标记为不活跃
if self.cache.exists(f"token:{token_hash}"):
cache_data = self.cache.get(f"token:{token_hash}")
cache_data["is_active"] = False
self.cache.set(f"token:{token_hash}", cache_data)
# 清空用户会话索引
self.cache.set(key, [])
def get_user_sessions(self, user_id: str) -> List[Dict[str, Any]]:
"""获取用户的所有活跃会话"""
with self.lock:
sessions = []
for token_type in ["access", "refresh"]:
key = f"user:{user_id}:{token_type}"
token_hashes = self.cache.get(key, [])
for token_hash in token_hashes:
if self.cache.exists(f"token:{token_hash}"):
cache_data = self.cache.get(f"token:{token_hash}")
# 检查是否活跃且未过期
if cache_data.get("is_active", True):
expires_at = datetime.fromisoformat(
cache_data["expires_at"]
)
if datetime.utcnow() <= expires_at:
sessions.append(
{
"token_type": token_type,
"device_id": cache_data.get("device_id"),
"user_agent": cache_data.get("user_agent"),
"ip_address": cache_data.get("ip_address"),
"created_at": cache_data["created_at"],
"last_used": cache_data["last_used"],
"expires_at": cache_data["expires_at"],
}
)
return sessions
def cleanup_expired(self):
"""清理过期数据"""
with self.lock:
cleaned_count = 0
current_time = datetime.utcnow()
# 清理过期 Token
for token_hash in self.cache.keys("token:*"):
expires_at = datetime.fromisoformat(self.cache.get(token_hash))
if current_time > expires_at:
self.cache.delete(token_hash)
cleaned_count += 1
return cleaned_count
# 全局缓存实例
_jwt_cache = None
def get_jwt_cache() -> JWTCache:
"""获取 JWT 缓存实例(单例模式)"""
global _jwt_cache
if _jwt_cache is None:
_jwt_cache = JWTCache()
return _jwt_cache
# Token 响应模型
class TokenResponse(BaseModel):
"""Token 响应模型"""
access_token: str
refresh_token: str
token_type: str = "bearer"
expires_in: int
refresh_expires_in: int
class RefreshTokenResponse(BaseModel):
"""刷新 Token 响应模型"""
access_token: str
token_type: str = "bearer"
# Token 数据模型
class TokenData(BaseModel):
"""Token 数据模型"""
user_id: str
username: Optional[str] = None
email: Optional[str] = None
scopes: List[str] = []
+41 -3
View File
@@ -1,3 +1,4 @@
import re
import logging
import datetime
from pathlib import Path
@@ -12,6 +13,36 @@ server_config = config[env]["server"]
log_config = config[env]["logging"]
class ColoredFormatter(logging.Formatter):
"""为 status_code 添加颜色的日志格式化器"""
COLORS = {
"2": "\033[32m", # 绿色 2xx
"3": "\033[36m", # 青色 3xx
"4": "\033[33m", # 黄色 4xx
"5": "\033[31m", # 红色 5xx
}
RESET = "\033[0m"
def format(self, record):
# 保存原始消息
original_msg = record.msg
# 如果日志记录包含 status_code 属性,则为其添加颜色
status_code = getattr(record, "status_code", None)
if status_code and isinstance(status_code, int):
first_digit = str(status_code)[0]
color = self.COLORS.get(first_digit, "")
colored_status = f"{color}{status_code}{self.RESET}"
# 替换消息中的 "status=数字" 为带颜色的版本
record.msg = re.sub(
rf"status={status_code}\b", f"status={colored_status}", original_msg
)
result = super().format(record)
# 恢复原始消息,避免影响其他 handler
record.msg = original_msg
return result
class ExcludeWatchFilesFilter(logging.Filter):
def __init__(self):
super().__init__()
@@ -29,7 +60,13 @@ class ExcludeWatchFilesFilter(logging.Filter):
return not record.name.startswith(self._filter_starts_with_tulpe)
_configured_loggers = set()
def setup_logging(logger: logging.Logger, log_name: str = "app"):
if log_name in _configured_loggers:
return
_configured_loggers.add(log_name)
log_level = getattr(logging, log_config["level"], logging.INFO)
log_format = log_config["format"]
# 创建文件夹
@@ -41,7 +78,8 @@ def setup_logging(logger: logging.Logger, log_name: str = "app"):
logger.setLevel(log_level)
# 创建格式化器
formatter = logging.Formatter(log_format)
plain_formatter = logging.Formatter(log_format)
color_formatter = ColoredFormatter(log_format)
# 创建轮转文件处理器 - 限制文件大小为 10MB,保留 5 个备份文件
max_bytes = log_config.get("max_size", 10 * 1024 * 1024) # 默认 10MB
@@ -53,12 +91,12 @@ def setup_logging(logger: logging.Logger, log_name: str = "app"):
maxBytes=max_bytes,
backupCount=backup_count,
)
file_handler.setFormatter(formatter)
file_handler.setFormatter(plain_formatter)
file_handler.addFilter(ExcludeWatchFilesFilter())
# 创建控制台处理器并添加过滤器
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
console_handler.setFormatter(color_formatter)
console_handler.addFilter(ExcludeWatchFilesFilter())
# 添加处理器到根日志记录器
View File
-288
View File
@@ -1,288 +0,0 @@
"""
APScheduler 集成模块
提供持久化任务调度服务
"""
import os
import sys
import tempfile
from contextlib import suppress
from typing import Dict, List, Optional, Callable, AsyncGenerator
from datetime import datetime
from fastapi import Request
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.executors.asyncio import AsyncIOExecutor
from apscheduler.job import Job
from apscheduler.triggers.interval import IntervalTrigger
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from ..core.config import get_config
from ..core.logger import get_logger
config = get_config()
logger = get_logger("scheduler")
def acquire_scheduler_lock():
lock_path = os.path.join(tempfile.gettempdir(), "chongming_scheduler.lock")
lock_file = open(lock_path, "w")
try:
if sys.platform == "win32":
import msvcrt
msvcrt.locking(lock_file.fileno(), msvcrt.LK_NBLCK, 1)
else:
import fcntl
fcntl.flock(lock_file.fileno(), fcntl.LOCK_NB | fcntl.LOCK_EX)
return lock_file, True
except (IOError, OSError):
lock_file.close()
return None, False
class TaskService:
"""任务调度服务"""
def __init__(self, async_session_maker=None):
self.async_session_maker = async_session_maker
# 使用独立的 SQLite 数据库存储任务(与业务数据库分开)
self.job_store_db_path = config.get("scheduler", {}).get(
"job_store_path", "scheduler_jobs.db"
)
job_store_url = f"sqlite:///{self.job_store_db_path}"
# 配置 job stores 和 executors
jobstores = {"default": SQLAlchemyJobStore(url=job_store_url)}
executors = {"default": AsyncIOExecutor()}
job_defaults = {
"coalesce": True, # 合并错过的任务
"max_instances": 3, # 最大并发实例数
"misfire_grace_time": 30, # 错过任务的宽限时间(秒)
}
self.scheduler = AsyncIOScheduler(
jobstores=jobstores,
executors=executors,
job_defaults=job_defaults,
timezone="Asia/Shanghai",
)
self._started = False
self.lock_file, self.is_scheduler_worker = acquire_scheduler_lock()
async def start(self, init_tasks_callback: Optional[Callable] = None):
"""启动调度器"""
if self.is_scheduler_worker:
if not self._started:
self.scheduler.start()
self._started = True
logger.info("任务调度器已启动")
if init_tasks_callback:
await init_tasks_callback(self)
async def shutdown(self, wait: bool = True):
"""关闭调度器"""
if self.lock_file:
with suppress(Exception):
for job in await self.get_jobs():
await self.pause_job(job.get("id", ""))
if self._started:
self.scheduler.shutdown(wait=wait)
self._started = False
logger.info("任务调度器已关闭")
if sys.platform == "win32":
import msvcrt
msvcrt.locking(self.lock_file.fileno(), msvcrt.LK_UNLCK, 1)
else:
import fcntl
fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_UN)
self.lock_file.close()
async def add_interval_job(
self,
func: Callable,
seconds: int = 0,
minutes: int = 0,
hours: int = 0,
days: int = 0,
job_id: Optional[str] = None,
args: Optional[List] = None,
kwargs: Optional[Dict] = None,
replace_existing: bool = True,
) -> Job:
"""添加间隔执行任务"""
trigger = IntervalTrigger(
seconds=seconds, minutes=minutes, hours=hours, days=days
)
job = self.scheduler.add_job(
func,
trigger=trigger,
id=job_id,
args=args or [],
kwargs=kwargs or {},
replace_existing=replace_existing,
)
logger.info(f"添加间隔任务: {job.id}")
return job
async def add_cron_job(
self,
func: Callable,
job_id: Optional[str] = None,
year: Optional[int] = None,
month: Optional[int] = None,
day: Optional[int] = None,
week: Optional[int] = None,
day_of_week: Optional[int] = None,
hour: Optional[int] = None,
minute: Optional[int] = None,
second: Optional[int] = None,
args: Optional[List] = None,
kwargs: Optional[Dict] = None,
replace_existing: bool = True,
) -> Job:
"""添加 cron 表达式任务"""
trigger = CronTrigger(
year=year,
month=month,
day=day,
week=week,
day_of_week=day_of_week,
hour=hour,
minute=minute,
second=second,
)
job = self.scheduler.add_job(
func,
trigger=trigger,
id=job_id,
args=args or [],
kwargs=kwargs or {},
replace_existing=replace_existing,
)
logger.info(f"添加 cron 任务: {job.id}")
return job
async def add_date_job(
self,
func: Callable,
run_date: Optional[datetime] = None,
job_id: Optional[str] = None,
args: Optional[List] = None,
kwargs: Optional[Dict] = None,
replace_existing: bool = True,
) -> Job:
"""添加一次性任务"""
trigger = DateTrigger(run_date=run_date)
job = self.scheduler.add_job(
func,
trigger=trigger,
id=job_id,
args=args or [],
kwargs=kwargs or {},
replace_existing=replace_existing,
)
logger.info(f"添加一次性任务: {job.id}")
return job
async def remove_job(self, job_id: str) -> bool:
"""删除任务"""
try:
self.scheduler.remove_job(job_id)
logger.info(f"删除任务: {job_id}")
return True
except Exception as e:
logger.error(f"删除任务失败 {job_id}: {e}")
return False
async def pause_job(self, job_id: str) -> bool:
"""暂停任务"""
try:
self.scheduler.pause_job(job_id)
logger.info(f"暂停任务: {job_id}")
return True
except Exception as e:
logger.error(f"暂停任务失败 {job_id}: {e}")
return False
async def resume_job(self, job_id: str) -> bool:
"""恢复任务"""
try:
self.scheduler.resume_job(job_id)
logger.info(f"恢复任务: {job_id}")
return True
except Exception as e:
logger.error(f"恢复任务失败 {job_id}: {e}")
return False
async def get_job(self, job_id: str) -> Optional[Dict]:
"""获取任务信息"""
job = self.scheduler.get_job(job_id)
if job:
return self._job_to_dict(job)
return None
async def get_jobs(self) -> List[Dict]:
"""获取所有任务"""
jobs = self.scheduler.get_jobs()
return [self._job_to_dict(job) for job in jobs]
async def reschedule_job(
self, job_id: str, trigger_type: str, **trigger_args
) -> Optional[Job]:
"""重新调度任务(修改触发时间)"""
try:
job = self.scheduler.get_job(job_id)
if not job:
return None
if trigger_type == "interval":
trigger = IntervalTrigger(**trigger_args)
elif trigger_type == "cron":
trigger = CronTrigger(**trigger_args)
elif trigger_type == "date":
trigger = DateTrigger(**trigger_args)
else:
raise ValueError(f"不支持的 trigger 类型: {trigger_type}")
job.reschedule(trigger=trigger)
logger.info(f"重新调度任务: {job_id}")
return job
except Exception as e:
logger.error(f"重新调度任务失败 {job_id}: {e}")
return None
def _job_to_dict(self, job: Job) -> Dict:
"""将 Job 对象转换为字典"""
return {
"id": job.id,
"name": job.name,
"next_run_time": (
job.next_run_time.isoformat() if job.next_run_time else None
),
"trigger": str(job.trigger),
"pending": job.pending,
"args": job.args,
"kwargs": job.kwargs,
}
_task_service: Optional[TaskService] = None
def get_task_service_instance(async_session_maker=None):
global _task_service
if _task_service is None:
_task_service = TaskService(async_session_maker)
return _task_service
async def get_task_service(request: Request) -> AsyncGenerator:
return request.app.state.task_service
View File
+11
View File
@@ -0,0 +1,11 @@
from pydantic import BaseModel
from typing import Optional
class Detail(BaseModel):
message: str
code: int
class HTTPError(BaseModel):
detail: Optional[Detail] = None
+12 -1
View File
@@ -1,4 +1,4 @@
from typing import Optional, List, TYPE_CHECKING
from typing import Optional, List, Dict, TYPE_CHECKING
from sqlmodel import SQLModel, Field, Relationship
from pydantic import BaseModel
@@ -42,3 +42,14 @@ class UserRead(UserBase):
class UserLogin(BaseModel):
email: str
password: str
class Userlogout(BaseModel):
code: int = 200
message: str = "logout success"
class UserSession(BaseModel):
user_id: str
sessions: List[Dict[str, Optional[str]]]
count: int
+2 -1
View File
@@ -8,10 +8,11 @@ from passlib.context import CryptContext
from sqlalchemy.ext.asyncio import AsyncSession
from ..model.user import User
from ..core.jwt_cache import get_jwt_cache, TokenResponse, TokenData
from ..core.config import get_config
from .user import UserService
from plugins.jwt.jwt_cache import get_jwt_cache, TokenResponse, TokenData
class AuthService:
"""认证服务"""
-1
View File
@@ -1,5 +1,4 @@
import os
import time
import uuid
from pathlib import Path
+2 -1
View File
@@ -1,7 +1,8 @@
from ..core.scheduler import TaskService
from ..core.config import get_config
from .dev_init_db import init_db
from plugins.scheduler.scheduler import TaskService
config = get_config()
+6 -2
View File
@@ -1,9 +1,13 @@
from ..core.scheduler import logger
from ..core.scheduler import get_task_service_instance
from ..core.logger import get_logger
from ..core.constant import PermissionConstant
from ..service.user import UserService
from ..service.permission import PermissionService
from plugins.scheduler.scheduler import get_task_service_instance
logger = get_logger("scheduler")
async def dev_init_admin(session):
admin_user = await UserService.get_user_by_email("admin", session)
+7
View File
@@ -250,6 +250,13 @@ async def main():
with open("./build/utils/launch.py", "w", encoding="utf-8") as f:
f.write(context)
jwt_key = Encryption.generate_key()
with open("./build/config.toml", "r", encoding="utf-8") as f:
context = f.read()
context = context.replace("your-secret-key-change-in-production", jwt_key)
with open("build/config.toml", "w", encoding="utf-8") as f:
f.write(context)
# 混淆
run_pyarmor_obfuscate()