modify: task 集成数据库操作
This commit is contained in:
@@ -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,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),
|
||||
|
||||
@@ -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 数据模型"""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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="/")
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user