modify: task 集成数据库操作

This commit is contained in:
2026-04-15 16:05:31 +08:00
parent 84b122e93a
commit 69d1c5fc67
39 changed files with 1374 additions and 70 deletions
+21 -22
View File
@@ -1,7 +1,7 @@
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, Depends
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from sqlmodel import SQLModel, text
@@ -12,10 +12,9 @@ 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, get_task_service_instance, TaskService
from .core.static_files import static_files_handler
from .core.scheduler import get_task_service_instance
from .core.static_files import SVFSStaticFiles
from .api import api_router
from .task.execute_background import execute_background_task
from .task import init_tasks_callback
@@ -45,12 +44,6 @@ async def lifespan(app: FastAPI):
if database_config is None:
raise ValueError("配置不存在")
task_service = get_task_service_instance()
await task_service.start(init_tasks_callback)
app.state.task_service = task_service
# 启动时:创建引擎和会话工厂,初始化数据库表
engine = create_async_engine(database["url"], **database_config)
async_session_maker = async_sessionmaker(
@@ -77,6 +70,10 @@ async def lifespan(app: FastAPI):
if "already exists" in str(e):
raise
task_service = get_task_service_instance(async_session_maker)
app.state.task_service = task_service
await task_service.start(init_tasks_callback)
yield # 应用运行期间
# 关闭时:释放引擎
@@ -114,21 +111,23 @@ async def root():
}
@app.post("/schedule")
async def add_task(
request: TaskRequest, task_service: TaskService = Depends(get_task_service)
):
"""添加定时任务接口"""
job = await task_service.add_interval_job(
execute_background_task,
seconds=request.interval,
args=[request.task_name],
job_id=request.task_name,
)
return {"status": "success", "job_id": job.id}
# @app.post("/schedule")
# async def add_task(
# request: TaskRequest, task_service: TaskService = Depends(get_task_service)
# ):
# """添加定时任务接口"""
# job = await task_service.add_interval_job(
# execute_background_task,
# seconds=request.interval,
# args=[request.task_name],
# job_id=request.task_name,
# )
# return {"status": "success", "job_id": job.id}
if os.path.exists("static.svfs"):
logger.info("静态文件已存在,将使用静态文件服务")
static_files_handler = SVFSStaticFiles(directory="/")
app.mount("/static", static_files_handler, name="static")
+3 -3
View File
@@ -3,7 +3,7 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from ...core.database import get_session
from ...core.jwt_cache import TokenData
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
@@ -38,7 +38,7 @@ async def register(
return user
@router.post("/login")
@router.post("/login", response_model=TokenResponse)
async def login(
user_data: UserLogin,
request: Request,
@@ -55,7 +55,7 @@ async def login(
return await auth_service.create_tokens(user, request, device_id=None)
@router.post("/refresh", response_model=dict, summary="刷新访问令牌")
@router.post("/refresh", response_model=RefreshTokenResponse, summary="刷新访问令牌")
async def refresh_token(
request: Request,
credentials: HTTPAuthorizationCredentials = Depends(refresh_scheme),
+7
View File
@@ -366,6 +366,13 @@ class TokenResponse(BaseModel):
refresh_expires_in: int
class RefreshTokenResponse(BaseModel):
"""刷新 Token 响应模型"""
access_token: str
token_type: str = "bearer"
# Token 数据模型
class TokenData(BaseModel):
"""Token 数据模型"""
+12 -5
View File
@@ -48,7 +48,8 @@ def acquire_scheduler_lock():
class TaskService:
"""任务调度服务"""
def __init__(self):
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"
@@ -172,15 +173,21 @@ class TaskService:
async def add_date_job(
self,
func: Callable,
run_date: datetime,
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 {}
func,
trigger=trigger,
id=job_id,
args=args or [],
kwargs=kwargs or {},
replace_existing=replace_existing,
)
logger.info(f"添加一次性任务: {job.id}")
return job
@@ -270,10 +277,10 @@ class TaskService:
_task_service: Optional[TaskService] = None
def get_task_service_instance():
def get_task_service_instance(async_session_maker=None):
global _task_service
if _task_service is None:
_task_service = TaskService()
_task_service = TaskService(async_session_maker)
return _task_service
+8 -16
View File
@@ -1,4 +1,3 @@
import os
from fastapi import HTTPException
from fastapi.responses import Response
from fastapi.staticfiles import StaticFiles
@@ -7,12 +6,8 @@ from sqlite_vfs.core import SQLiteVFS
class SVFSStaticFiles(StaticFiles):
def __init__(self, directory, vfs_db_path="static.svfs"):
if not os.path.exists(vfs_db_path):
raise RuntimeError(
f"虚拟文件系统数据库 {vfs_db_path} 不存在,请先运行打包脚本"
)
self.vfs_db_path = vfs_db_path
self.vfs = SQLiteVFS(vfs_db_path, compress=False)
vfs_db_path = vfs_db_path
self._vfs = SQLiteVFS(vfs_db_path, compress=False)
super().__init__(directory=directory)
async def get_response(self, path: str, scope):
@@ -20,7 +15,7 @@ class SVFSStaticFiles(StaticFiles):
normalized_path = "/" + path.replace("\\", "/")
# 获取文件信息
file_info = self.vfs.get_file_info(normalized_path)
file_info = self._vfs.get_file_info(normalized_path)
if file_info is None:
# SPA 回退:如果不是静态资源扩展名,尝试返回 index.html
@@ -37,22 +32,22 @@ class SVFSStaticFiles(StaticFiles):
".json",
]
):
index_info = self.vfs.get_file_info("/index.html")
index_info = self._vfs.get_file_info("/index.html")
if index_info:
content = self.vfs.read_file("/index.html")
content = self._vfs.read_file("/index.html")
return Response(content=content, media_type="text/html")
raise HTTPException(status_code=404, detail="File not found")
# 如果是目录,也返回 index.html(某些情况)
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:
content = self.vfs.read_file(f"{normalized_path}/index.html")
content = self._vfs.read_file(f"{normalized_path}/index.html")
return Response(content=content, media_type="text/html")
raise HTTPException(status_code=404, detail="Directory index not found")
# 读取文件内容
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"
# 简单根据扩展名补充 MIME 类型(可选)
if normalized_path.endswith(".js"):
@@ -65,6 +60,3 @@ class SVFSStaticFiles(StaticFiles):
content_type = "image/x-icon"
return Response(content=content, media_type=content_type)
static_files_handler = SVFSStaticFiles(directory="/")
+15
View File
@@ -0,0 +1,15 @@
from .user import User
from .role import Role
from .permission import Permission
from .relationship_table import UserRole, RolePermission
from .todo import Todo
__all__ = [
"User",
"Role",
"Permission",
"UserRole",
"RolePermission",
"Todo",
]
+24
View File
@@ -0,0 +1,24 @@
from typing import Optional, List, TYPE_CHECKING
from sqlmodel import SQLModel, Field, Relationship
from .relationship_table import RolePermission
if TYPE_CHECKING:
from .role import Role
# ---------- 权限表 ----------
class Permission(SQLModel, table=True):
"""权限项,如 user:create, order:delete"""
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(unique=True, index=True) # 权限标识符
description: Optional[str] = None
resource: str = Field(index=True) # 资源,如 user, order, product
action: str # 操作,如 create, read, update, delete
# 多对多关联
roles: List["Role"] = Relationship(
back_populates="permissions", link_model=RolePermission
)
@@ -0,0 +1,17 @@
from uuid import UUID
from sqlmodel import SQLModel, Field
# ---------- 关联表 ----------
class UserRole(SQLModel, table=True):
"""用户角色关联表"""
user_id: UUID = Field(foreign_key="user.id", primary_key=True)
role_id: int = Field(foreign_key="role.id", primary_key=True)
class RolePermission(SQLModel, table=True):
"""角色权限关联表"""
role_id: int = Field(foreign_key="role.id", primary_key=True)
permission_id: int = Field(foreign_key="permission.id", primary_key=True)
+21
View File
@@ -0,0 +1,21 @@
from typing import Optional, List, TYPE_CHECKING
from sqlmodel import SQLModel, Field, Relationship
from .relationship_table import UserRole, RolePermission
if TYPE_CHECKING:
from .user import User
from .permission import Permission
# ---------- 角色表 ----------
class Role(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(unique=True, index=True) # admin, manager, operator, viewer
description: Optional[str] = None
is_only_user: bool = Field(default=False)
users: List["User"] = Relationship(back_populates="roles", link_model=UserRole)
permissions: List["Permission"] = Relationship(
back_populates="roles", link_model=RolePermission
)
+11 -4
View File
@@ -1,8 +1,14 @@
from typing import Optional
from typing import Optional, List, TYPE_CHECKING
from sqlmodel import SQLModel, Field
from sqlmodel import SQLModel, Field, Relationship
from pydantic import BaseModel
from .relationship_table import UserRole
from uuid import uuid4, UUID
if TYPE_CHECKING:
from .role import Role
class UserBase(SQLModel):
email: str = Field(unique=True, index=True)
@@ -16,8 +22,9 @@ class UserBase(SQLModel):
class User(UserBase, table=True):
"""用户模型"""
id: Optional[int] = Field(default=None, primary_key=True)
id: UUID = Field(default_factory=uuid4, primary_key=True)
hashed_password: str
roles: List["Role"] = Relationship(back_populates="users", link_model=UserRole)
class UserCreate(UserBase):
@@ -29,7 +36,7 @@ class UserCreate(UserBase):
class UserRead(UserBase):
"""用户读取模型"""
id: int
id: UUID
class UserLogin(BaseModel):
+5 -12
View File
@@ -1,20 +1,13 @@
from typing import Callable, Optional
from ..core.scheduler import TaskService
from .execute_background import execute_background_task
task_dict = {"execute_background_task": execute_background_task}
def get_task(task_name: str) -> Optional[Callable]:
return task_dict.get(task_name, None)
from .execute_background import dev_init_admin
async def init_tasks_callback(task_service: TaskService):
await task_service.add_interval_job(
execute_background_task,
seconds=1,
args=["task_name"],
await task_service.add_date_job(
dev_init_admin,
run_date=None,
args=[],
job_id="task_name",
)
+12 -5
View File
@@ -1,8 +1,15 @@
from ..core.scheduler import logger
from ..service.user import UserService
from ..core.scheduler import get_task_service_instance
async def execute_background_task(task_name: str):
from datetime import datetime
"""模拟后台任务执行"""
logger.info(f"Executing task: {task_name} at {datetime.utcnow()}")
async def dev_init_admin():
task_service = get_task_service_instance()
if task_service.async_session_maker is None:
return
async with task_service.async_session_maker() as session:
admin_user = await UserService.get_user_by_email("admin", session)
if admin_user: # type: ignore
logger.info("管理员用户已存在") # type: ignore
return
await UserService.create_user(session, "admin", "admin", is_superuser=True) # type: ignore
+9
View File
@@ -157,6 +157,8 @@ async def init_database():
from ..app.core.config import get_config
from sqlmodel import SQLModel, text
from sqlalchemy.exc import OperationalError
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from ..app.service.user import UserService
database = get_config()["database"]
database_type = database["type"]
@@ -167,6 +169,9 @@ async def init_database():
engine = create_async_engine(
"sqlite+aiosqlite:///./build/database.db", **database_config
)
async_session_maker = async_sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
async with engine.begin() as conn:
if database_type == "sqlite":
@@ -176,9 +181,13 @@ async def init_database():
await conn.run_sync(
lambda conn: SQLModel.metadata.create_all(bind=conn, checkfirst=True)
)
except OperationalError as e:
if "already exists" in str(e):
raise
# 添加管理员用户
async with async_session_maker() as session:
await UserService.create_user(session, "admin", "admin", is_superuser=True) # type: ignore
async def main():