mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 12:52:26 +00:00
refactor: 移除多租户和客户相关代码及功能
refactor: 统一状态字段为字符串类型并更新相关组件 refactor: 更新模型基类移除租户和客户相关字段 refactor: 简化数据权限控制逻辑 refactor: 优化类型注解使用Python 3.10+语法 refactor: 清理无用导入和注释 docs: 更新文档移除多租户相关内容
This commit is contained in:
@@ -7,10 +7,10 @@ from app.common.response import StreamResponse, SuccessResponse
|
||||
from app.common.request import PaginationService
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.core.logger import log
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from .service import McpService
|
||||
from .schema import McpCreateSchema, McpUpdateSchema, ChatQuerySchema, McpQueryParam
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Dict, List, Optional, Sequence, Union, Any
|
||||
from typing import Sequence, Any
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
|
||||
@@ -22,47 +22,47 @@ class McpCRUD(CRUDBase[McpModel, McpCreateSchema, McpUpdateSchema]):
|
||||
self.auth = auth
|
||||
super().__init__(model=McpModel, auth=auth)
|
||||
|
||||
async def get_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[McpModel]:
|
||||
async def get_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> McpModel | None:
|
||||
"""
|
||||
获取MCP服务器详情
|
||||
|
||||
参数:
|
||||
- id (int): MCP服务器ID
|
||||
- preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Optional[McpModel]: MCP服务器模型实例(如果存在)
|
||||
- McpModel | None: MCP服务器模型实例(如果存在)
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_by_name_crud(self, name: str, preload: Optional[List[Union[str, Any]]] = None) -> Optional[McpModel]:
|
||||
async def get_by_name_crud(self, name: str, preload: list[str | Any] | None = None) -> McpModel | None:
|
||||
"""
|
||||
通过名称获取MCP服务器
|
||||
|
||||
参数:
|
||||
- name (str): MCP服务器名称
|
||||
- preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Optional[McpModel]: MCP服务器模型实例(如果存在)
|
||||
"""
|
||||
return await self.get(name=name, preload=preload)
|
||||
|
||||
async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[McpModel]:
|
||||
async def get_list_crud(self, search: dict | None = None, order_by: list[dict[str, str]] | None = None, preload: list[str | Any] | None = None) -> Sequence[McpModel]:
|
||||
"""
|
||||
列表查询MCP服务器
|
||||
|
||||
参数:
|
||||
- search (Optional[Dict]): 查询参数字典
|
||||
- order_by (Optional[List[Dict[str, str]]]): 排序参数列表
|
||||
- preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项
|
||||
- search (dict | None): 查询参数字典
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[McpModel]: MCP服务器模型实例序列
|
||||
"""
|
||||
return await self.list(search=search or {}, order_by=order_by or [{'id': 'asc'}], preload=preload)
|
||||
|
||||
async def create_crud(self, data: McpCreateSchema) -> Optional[McpModel]:
|
||||
async def create_crud(self, data: McpCreateSchema) -> McpModel | None:
|
||||
"""
|
||||
创建MCP服务器
|
||||
|
||||
@@ -74,7 +74,7 @@ class McpCRUD(CRUDBase[McpModel, McpCreateSchema, McpUpdateSchema]):
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_crud(self, id: int, data: McpUpdateSchema) -> Optional[McpModel]:
|
||||
async def update_crud(self, id: int, data: McpUpdateSchema) -> McpModel | None:
|
||||
"""
|
||||
更新MCP服务器
|
||||
|
||||
@@ -83,16 +83,16 @@ class McpCRUD(CRUDBase[McpModel, McpCreateSchema, McpUpdateSchema]):
|
||||
- data (McpUpdateSchema): 更新MCP服务器模型
|
||||
|
||||
返回:
|
||||
- Optional[McpModel]: 更新的MCP服务器模型实例(如果成功)
|
||||
- McpModel | None: 更新的MCP服务器模型实例(如果成功)
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_crud(self, ids: List[int]) -> None:
|
||||
async def delete_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
批量删除MCP服务器
|
||||
|
||||
参数:
|
||||
- ids (List[int]): MCP服务器ID列表
|
||||
- ids (list[int]): MCP服务器ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
|
||||
@@ -3,26 +3,19 @@
|
||||
from sqlalchemy import JSON, String, Integer
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin, TenantMixin, CustomerMixin
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class McpModel(ModelMixin, UserMixin, TenantMixin, CustomerMixin):
|
||||
class McpModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
MCP 服务器表
|
||||
|
||||
数据隔离策略:
|
||||
===========
|
||||
- 系统级MCP: tenant_id=1, customer_id=NULL (平台预置MCP,所有租户可用)
|
||||
- 租户级MCP: tenant_id>1, customer_id=NULL (租户自定义MCP,仅本租户可用)
|
||||
- 客户级MCP: tenant_id>1, customer_id>0 (客户专属MCP,仅该客户可用)
|
||||
|
||||
MCP类型:
|
||||
- 0: stdio (标准输入输出)
|
||||
- 1: sse (Server-Sent Events)
|
||||
"""
|
||||
__tablename__: str = 'app_ai_mcp'
|
||||
__table_args__: dict[str, str] = ({'comment': 'MCP 服务器表'})
|
||||
__loader_options__: list[str] = ["created_by", "updated_by", "tenant", "customer"]
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
name: Mapped[str] = mapped_column(String(50), comment='MCP 名称')
|
||||
type: Mapped[int] = mapped_column(Integer, default=0, comment='MCP 类型(0:stdio 1:sse)')
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Optional, Dict, Any, List
|
||||
from pydantic import ConfigDict, Field, HttpUrl, BaseModel
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.base_schema import BaseSchema
|
||||
from app.common.enums import McpLLMProvider
|
||||
from app.core.base_schema import BaseSchema, UserBySchema, TenantSchema, CustomerSchema
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
from app.common.enums import McpType
|
||||
from app.core.validator import DateTimeStr
|
||||
|
||||
|
||||
class ChatQuerySchema(BaseModel):
|
||||
@@ -17,13 +17,13 @@ class ChatQuerySchema(BaseModel):
|
||||
|
||||
class McpCreateSchema(BaseModel):
|
||||
"""创建 MCP 服务器参数"""
|
||||
name: str = Field(..., max_length=50, description='MCP 名称')
|
||||
name: str = Field(..., max_length=64, description='MCP 名称')
|
||||
type: McpType = Field(McpType.stdio, description='MCP 类型')
|
||||
description: Optional[str] = Field(None, max_length=255, description='MCP 描述')
|
||||
url: Optional[HttpUrl] = Field(None, description='远程 SSE 地址')
|
||||
command: Optional[str] = Field(None, max_length=255, description='MCP 命令')
|
||||
args: Optional[str] = Field(None, max_length=255, description='MCP 命令参数,多个参数用英文逗号隔开')
|
||||
env: Optional[Dict[str, Any]] = Field(None, description='MCP 环境变量')
|
||||
description: str | None = Field(None, max_length=255, description='MCP 描述')
|
||||
url: HttpUrl | None = Field(None, description='远程 SSE 地址')
|
||||
command: str | None = Field(None, max_length=255, description='MCP 命令')
|
||||
args: str | None = Field(None, max_length=255, description='MCP 命令参数,多个参数用英文逗号隔开')
|
||||
env: dict[str, str] | None = Field(None, description='MCP 环境变量')
|
||||
|
||||
|
||||
class McpUpdateSchema(McpCreateSchema):
|
||||
@@ -31,7 +31,7 @@ class McpUpdateSchema(McpCreateSchema):
|
||||
...
|
||||
|
||||
|
||||
class McpOutSchema(McpCreateSchema, BaseSchema, UserBySchema, TenantSchema, CustomerSchema):
|
||||
class McpOutSchema(McpCreateSchema, BaseSchema, UserBySchema):
|
||||
"""MCP 服务器详情"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -41,8 +41,12 @@ class McpQueryParam:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: Optional[str] = Query(None, description="MCP 名称"),
|
||||
type: Optional[int] = Query(None, description="MCP 类型"),
|
||||
name: str | None = Query(None, description="MCP 名称"),
|
||||
type: McpType | None = Query(None, description="MCP 类型"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", example=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", example=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
created_id: int | None = Query(None, description="创建人"),
|
||||
updated_id: int | None = Query(None, description="更新人"),
|
||||
) -> None:
|
||||
|
||||
# 模糊查询字段
|
||||
@@ -50,13 +54,21 @@ class McpQueryParam:
|
||||
|
||||
# 精确查询字段
|
||||
self.type = type
|
||||
self.created_id = created_id
|
||||
self.updated_id = updated_id
|
||||
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
|
||||
|
||||
class McpChatParam(BaseSchema):
|
||||
"""MCP 聊天参数"""
|
||||
pk: List[int] = Field(..., description='MCP ID 列表')
|
||||
pk: list[int] = Field(..., description='MCP ID 列表')
|
||||
provider: McpLLMProvider = Field(McpLLMProvider.openai, description='LLM 供应商')
|
||||
model: str = Field(..., description='LLM 名称')
|
||||
key: str = Field(..., description='LLM API Key')
|
||||
base_url: Optional[str] = Field(None, description='自定义 LLM API 地址,必须兼容 openai 供应商')
|
||||
base_url: str | None = Field(None, description='自定义 LLM API 地址,必须兼容 openai 供应商')
|
||||
prompt: str = Field(..., description='用户提示词')
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import List, Dict, Optional, Any
|
||||
from typing import Any, AsyncGenerator
|
||||
|
||||
from app.core.exceptions import CustomException
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
@@ -13,7 +13,7 @@ class McpService:
|
||||
"""MCP服务层"""
|
||||
|
||||
@classmethod
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> Dict[str, Any]:
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> dict[str, Any]:
|
||||
"""
|
||||
获取MCP服务器详情
|
||||
|
||||
@@ -22,7 +22,7 @@ class McpService:
|
||||
- id (int): MCP服务器ID
|
||||
|
||||
返回:
|
||||
- Dict[str, Any]: MCP服务器详情字典
|
||||
- dict[str, Any]: MCP服务器详情字典
|
||||
"""
|
||||
obj = await McpCRUD(auth).get_by_id_crud(id=id)
|
||||
if not obj:
|
||||
@@ -30,24 +30,24 @@ class McpService:
|
||||
return McpOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_service(cls, auth: AuthSchema, search: Optional[McpQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict[str, Any]]:
|
||||
async def list_service(cls, auth: AuthSchema, search: McpQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> list[dict[str, Any]]:
|
||||
"""
|
||||
列表查询MCP服务器
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- search (Optional[McpQueryParam]): 查询参数模型
|
||||
- order_by (Optional[List[Dict[str, str]]]): 排序参数列表
|
||||
- search (McpQueryParam | None): 查询参数模型
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
|
||||
返回:
|
||||
- List[Dict[str, Any]]: MCP服务器详情字典列表
|
||||
- list[dict[str, Any]]: MCP服务器详情字典列表
|
||||
"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await McpCRUD(auth).get_list_crud(search=search_dict, order_by=order_by)
|
||||
return [McpOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def create_service(cls, auth: AuthSchema, data: McpCreateSchema) -> Dict[str, Any]:
|
||||
async def create_service(cls, auth: AuthSchema, data: McpCreateSchema) -> dict[str, Any]:
|
||||
"""
|
||||
创建MCP服务器
|
||||
|
||||
@@ -56,7 +56,7 @@ class McpService:
|
||||
- data (McpCreateSchema): 创建MCP服务器模型
|
||||
|
||||
返回:
|
||||
- Dict[str, Any]: 创建的MCP服务器详情字典
|
||||
- dict[str, Any]: 创建的MCP服务器详情字典
|
||||
"""
|
||||
obj = await McpCRUD(auth).get_by_name_crud(name=data.name)
|
||||
if obj:
|
||||
@@ -65,7 +65,7 @@ class McpService:
|
||||
return McpOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_service(cls, auth: AuthSchema, id: int, data: McpUpdateSchema) -> Dict[str, Any]:
|
||||
async def update_service(cls, auth: AuthSchema, id: int, data: McpUpdateSchema) -> dict[str, Any]:
|
||||
"""
|
||||
更新MCP服务器
|
||||
|
||||
@@ -75,7 +75,7 @@ class McpService:
|
||||
- data (McpUpdateSchema): 更新MCP服务器模型
|
||||
|
||||
返回:
|
||||
- Dict[str, Any]: 更新的MCP服务器详情字典
|
||||
- dict[str, Any]: 更新的MCP服务器详情字典
|
||||
"""
|
||||
obj = await McpCRUD(auth).get_by_id_crud(id=id)
|
||||
if not obj:
|
||||
@@ -87,13 +87,13 @@ class McpService:
|
||||
return McpOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_service(cls, auth: AuthSchema, ids: List[int]) -> None:
|
||||
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
批量删除MCP服务器
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- ids (List[int]): MCP服务器ID列表
|
||||
- ids (list[int]): MCP服务器ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
@@ -107,7 +107,7 @@ class McpService:
|
||||
await McpCRUD(auth).delete_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def chat_query(cls, query: ChatQuerySchema):
|
||||
async def chat_query(cls, query: ChatQuerySchema) -> AsyncGenerator[str, Any]:
|
||||
"""
|
||||
处理聊天查询
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Any, AsyncGenerator
|
||||
from typing import AsyncGenerator
|
||||
from openai import AsyncOpenAI, OpenAI
|
||||
from openai.types.chat.chat_completion import ChatCompletion
|
||||
import httpx
|
||||
|
||||
@@ -5,10 +5,10 @@ from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
from app.common.response import StreamResponse, SuccessResponse
|
||||
from app.common.request import PaginationService
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.core.logger import log
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
@@ -211,7 +211,7 @@ async def get_job_log_controller():
|
||||
"coalesce": i.coalesce,
|
||||
"max_instances": i.max_instances,
|
||||
"next_run_time": i.next_run_time,
|
||||
"state": SchedulerUtil.get_single_job_status(job_id=i.id, tenant_id=None)
|
||||
"state": SchedulerUtil.get_single_job_status(job_id=i.id)
|
||||
}
|
||||
for i in SchedulerUtil.get_all_jobs()
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Dict, List, Optional, Sequence, Union, Any
|
||||
from typing import Sequence, Any
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
|
||||
@@ -22,34 +22,34 @@ class JobCRUD(CRUDBase[JobModel, JobCreateSchema, JobUpdateSchema]):
|
||||
self.auth = auth
|
||||
super().__init__(model=JobModel, auth=auth)
|
||||
|
||||
async def get_obj_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[JobModel]:
|
||||
async def get_obj_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> JobModel | None:
|
||||
"""
|
||||
获取定时任务详情
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务ID
|
||||
- preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Optional[JobModel]: 定时任务模型,如果不存在则为None
|
||||
- JobModel | None: 定时任务模型,如果不存在则为None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_obj_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[JobModel]:
|
||||
async def get_obj_list_crud(self, search: dict | None = None, order_by: list[dict[str, str]] | None = None, preload: list[str | Any] | None = None) -> Sequence[JobModel]:
|
||||
"""
|
||||
获取定时任务列表
|
||||
|
||||
参数:
|
||||
- search (Optional[Dict]): 查询参数字典
|
||||
- order_by (Optional[List[Dict[str, str]]]): 排序参数列表
|
||||
- preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项
|
||||
- search (dict | None): 查询参数字典
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[JobModel]: 定时任务模型序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_obj_crud(self, data: JobCreateSchema) -> Optional[JobModel]:
|
||||
async def create_obj_crud(self, data: JobCreateSchema) -> JobModel | None:
|
||||
"""
|
||||
创建定时任务
|
||||
|
||||
@@ -57,11 +57,11 @@ class JobCRUD(CRUDBase[JobModel, JobCreateSchema, JobUpdateSchema]):
|
||||
- data (JobCreateSchema): 创建定时任务模型
|
||||
|
||||
返回:
|
||||
- Optional[JobModel]: 创建的定时任务模型,如果创建失败则为None
|
||||
- JobModel | None: 创建的定时任务模型,如果创建失败则为None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_obj_crud(self, id: int, data: JobUpdateSchema) -> Optional[JobModel]:
|
||||
async def update_obj_crud(self, id: int, data: JobUpdateSchema) -> JobModel | None:
|
||||
"""
|
||||
更新定时任务
|
||||
|
||||
@@ -70,25 +70,25 @@ class JobCRUD(CRUDBase[JobModel, JobCreateSchema, JobUpdateSchema]):
|
||||
- data (JobUpdateSchema): 更新定时任务模型
|
||||
|
||||
返回:
|
||||
- Optional[JobModel]: 更新后的定时任务模型,如果更新失败则为None
|
||||
- JobModel | None: 更新后的定时任务模型,如果更新失败则为None
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_obj_crud(self, ids: List[int]) -> None:
|
||||
async def delete_obj_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
删除定时任务
|
||||
|
||||
参数:
|
||||
- ids (List[int]): 定时任务ID列表
|
||||
- ids (list[int]): 定时任务ID列表
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_obj_field_crud(self, ids: List[int], **kwargs) -> None:
|
||||
async def set_obj_field_crud(self, ids: list[int], **kwargs) -> None:
|
||||
"""
|
||||
设置定时任务的可用状态
|
||||
|
||||
参数:
|
||||
- ids (List[int]): 定时任务ID列表
|
||||
- ids (list[int]): 定时任务ID列表
|
||||
- kwargs: 其他要设置的字段,例如 available=True 或 available=False
|
||||
"""
|
||||
return await self.set(ids=ids, **kwargs)
|
||||
@@ -116,39 +116,39 @@ class JobLogCRUD(CRUDBase[JobLogModel, JobLogCreateSchema, JobLogUpdateSchema]):
|
||||
self.auth = auth
|
||||
super().__init__(model=JobLogModel, auth=auth)
|
||||
|
||||
async def get_obj_log_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[JobLogModel]:
|
||||
async def get_obj_log_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> JobLogModel | None:
|
||||
"""
|
||||
获取定时任务日志详情
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务日志ID
|
||||
- preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Optional[JobLogModel]: 定时任务日志模型,如果不存在则为None
|
||||
- JobLogModel | None: 定时任务日志模型,如果不存在则为None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_obj_log_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[JobLogModel]:
|
||||
async def get_obj_log_list_crud(self, search: dict | None = None, order_by: list[dict[str, str]] | None = None, preload: list[str | Any] | None = None) -> Sequence[JobLogModel]:
|
||||
"""
|
||||
获取定时任务日志列表
|
||||
|
||||
参数:
|
||||
- search (Optional[Dict]): 查询参数字典
|
||||
- order_by (Optional[List[Dict[str, str]]]): 排序参数列表
|
||||
- preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项
|
||||
- search (dict | None): 查询参数字典
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[JobLogModel]: 定时任务日志模型序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def delete_obj_log_crud(self, ids: List[int]) -> None:
|
||||
async def delete_obj_log_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
删除定时任务日志
|
||||
|
||||
参数:
|
||||
- ids (List[int]): 定时任务日志ID列表
|
||||
- ids (list[int]): 定时任务日志ID列表
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
|
||||
@@ -3,26 +3,18 @@
|
||||
from sqlalchemy import Boolean, String, Integer, Text, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin, TenantMixin, CustomerMixin
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class JobModel(ModelMixin, UserMixin, TenantMixin, CustomerMixin):
|
||||
class JobModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
定时任务调度表
|
||||
|
||||
数据隔离策略:
|
||||
===========
|
||||
- 系统级任务: tenant_id=1, customer_id=NULL (平台定时任务,如系统维护)
|
||||
- 租户级任务: tenant_id>1, customer_id=NULL (租户定时任务,如数据统计)
|
||||
- 客户级任务: tenant_id>1, customer_id>0 (客户专属定时任务)
|
||||
|
||||
任务状态:
|
||||
- 0: 运行中
|
||||
- 1: 暂停中
|
||||
"""
|
||||
__tablename__: str = 'app_job'
|
||||
__table_args__: dict[str, str] = ({'comment': '定时任务调度表'})
|
||||
__loader_options__: list[str] = ["job_logs", "created_by", "updated_by", "tenant", "customer"]
|
||||
__loader_options__: list[str] = ["job_logs", "created_by", "updated_by"]
|
||||
|
||||
name: Mapped[str | None] = mapped_column(String(64), nullable=True, default='', comment='任务名称')
|
||||
jobstore: Mapped[str | None] = mapped_column(String(64), nullable=True, default='default', comment='存储器')
|
||||
@@ -44,12 +36,9 @@ class JobModel(ModelMixin, UserMixin, TenantMixin, CustomerMixin):
|
||||
)
|
||||
|
||||
|
||||
class JobLogModel(ModelMixin, TenantMixin):
|
||||
class JobLogModel(ModelMixin):
|
||||
"""
|
||||
定时任务调度日志表
|
||||
|
||||
添加tenant_id字段以支持多租户隔离,提高查询性能
|
||||
即使job记录被删除,日志仍然保留租户标识信息
|
||||
"""
|
||||
__tablename__: str = 'app_job_log'
|
||||
__table_args__: dict[str, str] = ({'comment': '定时任务调度日志表'})
|
||||
@@ -73,17 +62,6 @@ class JobLogModel(ModelMixin, TenantMixin):
|
||||
comment='任务ID'
|
||||
)
|
||||
|
||||
# 索引优化 - 为租户ID创建索引
|
||||
__table_args__ = ({
|
||||
'comment': '定时任务调度日志表',
|
||||
})
|
||||
|
||||
# 为多租户查询性能优化添加复合索引
|
||||
# 注意:实际索引会在数据库迁移时创建
|
||||
__indexes__ = [
|
||||
'tenant_id_idx', # 租户ID索引
|
||||
'job_id_tenant_id_idx' # 任务ID和租户ID的复合索引
|
||||
]
|
||||
job: Mapped["JobModel | None"] = relationship(
|
||||
back_populates="job_logs",
|
||||
lazy="selectin"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
from typing import Optional
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.base_schema import BaseSchema, UserBySchema, TenantSchema, CustomerSchema
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
from app.core.validator import DateTimeStr, datetime_validator
|
||||
|
||||
|
||||
@@ -12,20 +11,20 @@ class JobCreateSchema(BaseModel):
|
||||
"""
|
||||
定时任务调度表对应pydantic模型
|
||||
"""
|
||||
name: Optional[str] = Field(..., max_length=64, description='任务名称')
|
||||
name: str = Field(..., max_length=64, description='任务名称')
|
||||
func: str = Field(..., description='任务函数')
|
||||
trigger: str = Field(..., description='触发器:控制此作业计划的 trigger 对象')
|
||||
args: Optional[str] = Field(default=None, description='位置参数')
|
||||
kwargs: Optional[str] = Field(default=None, description='关键字参数')
|
||||
coalesce: Optional[bool] = Field(..., description='是否合并运行:是否在多个运行时间到期时仅运行作业一次')
|
||||
max_instances: Optional[int] = Field(default=1, ge=1, description='最大实例数:允许的最大并发执行实例数')
|
||||
jobstore: Optional[str] = Field(..., max_length=64, description='任务存储')
|
||||
executor: Optional[str] = Field(..., max_length=64, description='任务执行器:将运行此作业的执行程序的名称')
|
||||
trigger_args: Optional[str] = Field(default=None, description='触发器参数')
|
||||
start_date: Optional[str] = Field(default=None, description='开始时间')
|
||||
end_date: Optional[str] = Field(default=None, description='结束时间')
|
||||
description: Optional[str] = Field(default=None, max_length=255, description='描述')
|
||||
status: Optional[str] = Field(default='0', description='任务状态:启动,停止')
|
||||
args: str | None = Field(default=None, description='位置参数')
|
||||
kwargs: str | None = Field(default=None, description='关键字参数')
|
||||
coalesce: bool | None = Field(..., description='是否合并运行:是否在多个运行时间到期时仅运行作业一次')
|
||||
max_instances: int | None = Field(default=1, ge=1, description='最大实例数:允许的最大并发执行实例数')
|
||||
jobstore: str | None = Field(..., max_length=64, description='任务存储')
|
||||
executor: str | None = Field(..., max_length=64, description='任务执行器:将运行此作业的执行程序的名称')
|
||||
trigger_args: str | None = Field(default=None, description='触发器参数')
|
||||
start_date: str | None = Field(default=None, description='开始时间')
|
||||
end_date: str | None = Field(default=None, description='结束时间')
|
||||
description: str | None = Field(default=None, max_length=255, description='描述')
|
||||
status: str = Field(default='0', description='任务状态:启动,停止')
|
||||
|
||||
@field_validator('trigger')
|
||||
@classmethod
|
||||
@@ -55,7 +54,7 @@ class JobUpdateSchema(JobCreateSchema):
|
||||
...
|
||||
|
||||
|
||||
class JobOutSchema(JobCreateSchema, BaseSchema, UserBySchema, TenantSchema, CustomerSchema):
|
||||
class JobOutSchema(JobCreateSchema, BaseSchema, UserBySchema):
|
||||
"""定时任务响应模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
...
|
||||
@@ -68,26 +67,28 @@ class JobLogCreateSchema(BaseModel):
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
job_name: Optional[str] = Field(default=None, description='任务名称')
|
||||
job_group: Optional[str] = Field(default=None, description='任务组名')
|
||||
job_executor: Optional[str] = Field(default=None, description='任务执行器')
|
||||
invoke_target: Optional[str] = Field(default=None, description='调用目标字符串')
|
||||
job_args: Optional[str] = Field(default=None, description='位置参数')
|
||||
job_kwargs: Optional[str] = Field(default=None, description='关键字参数')
|
||||
job_trigger: Optional[str] = Field(default=None, description='任务触发器')
|
||||
job_message: Optional[str] = Field(default=None, description='日志信息')
|
||||
exception_info: Optional[str] = Field(default=None, description='异常信息')
|
||||
status: Optional[str] = Field(default='0', description='任务状态:正常,失败')
|
||||
create_time: Optional[DateTimeStr] = Field(default=None, description='创建时间')
|
||||
job_name: str = Field(..., description='任务名称')
|
||||
job_group: str | None = Field(default=None, description='任务组名')
|
||||
job_executor: str | None = Field(default=None, description='任务执行器')
|
||||
invoke_target: str | None = Field(default=None, description='调用目标字符串')
|
||||
job_args: str | None = Field(default=None, description='位置参数')
|
||||
job_kwargs: str | None = Field(default=None, description='关键字参数')
|
||||
job_trigger: str | None = Field(default=None, description='任务触发器')
|
||||
job_message: str | None = Field(default=None, description='日志信息')
|
||||
exception_info: str | None = Field(default=None, description='异常信息')
|
||||
status: str = Field(default='0', description='任务状态:正常,失败')
|
||||
description: str | None = Field(default=None, max_length=255, description='描述')
|
||||
create_time: DateTimeStr | None = Field(default=None, description='创建时间')
|
||||
update_time: DateTimeStr | None = Field(default=None, description='更新时间')
|
||||
|
||||
|
||||
class JobLogUpdateSchema(JobLogCreateSchema):
|
||||
"""定时任务调度日志表更新模型"""
|
||||
...
|
||||
id: Optional[int] = Field(default=None, description='任务日志ID')
|
||||
id: int | None = Field(default=None, description='任务日志ID')
|
||||
|
||||
|
||||
class JobLogOutSchema(JobLogUpdateSchema, BaseSchema, TenantSchema):
|
||||
class JobLogOutSchema(JobLogUpdateSchema, BaseSchema, UserBySchema):
|
||||
"""定时任务调度日志表响应模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
...
|
||||
@@ -98,22 +99,27 @@ class JobQueryParam:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: Optional[str] = Query(None, description="任务名称"),
|
||||
status: Optional[str] = Query(None, description="状态: 启动,停止"),
|
||||
creator: Optional[int] = Query(None, description="创建人"),
|
||||
created_time: Optional[list[DateTimeStr]] = Query(None, description="创建时间范围", example=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
name: str | None = Query(None, description="任务名称"),
|
||||
status: str | None = Query(None, description="状态: 启动,停止"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", example=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", example=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
created_id: int | None = Query(None, description="创建人"),
|
||||
updated_id: int | None = Query(None, description="更新人"),
|
||||
) -> None:
|
||||
|
||||
# 模糊查询字段
|
||||
self.name = ("like", f"%{name}%") if name else None
|
||||
|
||||
# 精确查询字段
|
||||
self.created_id = creator
|
||||
self.created_id = created_id
|
||||
self.updated_id = updated_id
|
||||
self.status = status
|
||||
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
|
||||
|
||||
class JobLogQueryParam:
|
||||
@@ -121,10 +127,11 @@ class JobLogQueryParam:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
job_id: Optional[int] = Query(None, description="定时任务ID"),
|
||||
job_name: Optional[str] = Query(None, description="任务名称"),
|
||||
status: Optional[str] = Query(None, description="状态: 正常,失败"),
|
||||
created_time: Optional[list[DateTimeStr]] = Query(None, description="创建时间范围", example=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
job_id: int | None = Query(None, description="定时任务ID"),
|
||||
job_name: str | None = Query(None, description="任务名称"),
|
||||
status: str | None = Query(None, description="状态: 正常,失败"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", example=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", example=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
) -> None:
|
||||
# 定时任务ID查询
|
||||
self.job_id = job_id
|
||||
@@ -135,3 +142,5 @@ class JobLogQueryParam:
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Any, List, Dict, Optional
|
||||
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.cron_util import CronUtil
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .tools.ap_scheduler import SchedulerUtil
|
||||
from .crud import JobCRUD, JobLogCRUD
|
||||
from .schema import (
|
||||
JobCreateSchema,
|
||||
JobUpdateSchema,
|
||||
@@ -15,7 +14,6 @@ from .schema import (
|
||||
JobQueryParam,
|
||||
JobLogQueryParam
|
||||
)
|
||||
from .crud import JobCRUD, JobLogCRUD
|
||||
|
||||
|
||||
class JobService:
|
||||
@@ -24,7 +22,7 @@ class JobService:
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_job_detail_service(cls, auth: AuthSchema, id: int) -> Dict:
|
||||
async def get_job_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""
|
||||
获取定时任务详情
|
||||
|
||||
@@ -39,14 +37,14 @@ class JobService:
|
||||
return JobOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_job_list_service(cls, auth: AuthSchema, search: Optional[JobQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict]:
|
||||
async def get_job_list_service(cls, auth: AuthSchema, search: JobQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> list[dict]:
|
||||
"""
|
||||
获取定时任务列表
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- search (Optional[JobQueryParam]): 查询参数模型
|
||||
- order_by (Optional[List[Dict[str, str]]]): 排序参数列表
|
||||
- search (JobQueryParam | None): 查询参数模型
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
|
||||
返回:
|
||||
- List[Dict]: 定时任务详情字典列表
|
||||
@@ -55,7 +53,7 @@ class JobService:
|
||||
return [JobOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def create_job_service(cls, auth: AuthSchema, data: JobCreateSchema) -> Dict:
|
||||
async def create_job_service(cls, auth: AuthSchema, data: JobCreateSchema) -> dict:
|
||||
"""
|
||||
创建定时任务
|
||||
|
||||
@@ -77,7 +75,7 @@ class JobService:
|
||||
return JobOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_job_service(cls, auth: AuthSchema, id:int, data: JobUpdateSchema) -> Dict:
|
||||
async def update_job_service(cls, auth: AuthSchema, id:int, data: JobUpdateSchema) -> dict:
|
||||
"""
|
||||
更新定时任务
|
||||
|
||||
@@ -87,7 +85,7 @@ class JobService:
|
||||
- data (JobUpdateSchema): 定时任务更新模型
|
||||
|
||||
返回:
|
||||
- Dict: 定时任务详情字典
|
||||
- dict: 定时任务详情字典
|
||||
"""
|
||||
exist_obj = await JobCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if not exist_obj:
|
||||
@@ -153,7 +151,7 @@ class JobService:
|
||||
SchedulerUtil().pause_job(job_id=id)
|
||||
await JobCRUD(auth).set_obj_field_crud(ids=[id], status=False)
|
||||
elif option == 2:
|
||||
SchedulerUtil().resume_job(job_id=id, tenant_id=obj.tenant_id)
|
||||
SchedulerUtil().resume_job(job_id=id)
|
||||
await JobCRUD(auth).set_obj_field_crud(ids=[id], status=True)
|
||||
elif option == 3:
|
||||
# 重启任务:先移除再添加,确保使用最新的任务配置
|
||||
@@ -167,12 +165,12 @@ class JobService:
|
||||
await JobCRUD(auth).set_obj_field_crud(ids=[id], status=True)
|
||||
|
||||
@classmethod
|
||||
async def export_job_service(cls, data_list: List[Dict[str, Any]]) -> bytes:
|
||||
async def export_job_service(cls, data_list: list[dict]) -> bytes:
|
||||
"""
|
||||
导出定时任务列表
|
||||
|
||||
参数:
|
||||
- data_list (List[Dict[str, Any]]): 定时任务列表
|
||||
- data_list (list[dict]): 定时任务列表
|
||||
|
||||
返回:
|
||||
- bytes: Excel文件字节流
|
||||
@@ -195,15 +193,14 @@ class JobService:
|
||||
'created_time': '创建时间',
|
||||
'updated_time': '更新时间',
|
||||
'created_id': '创建者ID',
|
||||
'creator': '创建者',
|
||||
'updated_id': '更新者ID',
|
||||
}
|
||||
|
||||
# 复制数据并转换状态
|
||||
data = data_list.copy()
|
||||
for item in data:
|
||||
item['status'] = '已完成' if item['status'] == 0 else '运行中' if item['status'] == 1 else '暂停'
|
||||
item['creator'] = item.get('creator', {}).get('name', '未知') if isinstance(item.get('creator'), dict) else '未知'
|
||||
|
||||
item['status'] = '已完成' if item['status'] == '0' else '运行中' if item['status'] == '1' else '暂停'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
|
||||
@@ -213,7 +210,7 @@ class JobLogService:
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_job_log_detail_service(cls, auth: AuthSchema, id: int) -> Dict:
|
||||
async def get_job_log_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""
|
||||
获取定时任务日志详情
|
||||
|
||||
@@ -222,23 +219,23 @@ class JobLogService:
|
||||
- id (int): 定时任务日志ID
|
||||
|
||||
返回:
|
||||
- Dict: 定时任务日志详情字典
|
||||
- dict: 定时任务日志详情字典
|
||||
"""
|
||||
obj = await JobLogCRUD(auth).get_obj_log_by_id_crud(id=id)
|
||||
return JobLogOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_job_log_list_service(cls, auth: AuthSchema, search: Optional[JobLogQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict]:
|
||||
async def get_job_log_list_service(cls, auth: AuthSchema, search: JobLogQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
||||
"""
|
||||
获取定时任务日志列表
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- search (Optional[JobLogQueryParam]): 查询参数模型, 包含分页信息和查询条件
|
||||
- order_by (Optional[List[Dict[str, str]]]): 排序参数列表, 每个元素为一个字典, 包含字段名和排序方向
|
||||
- search (JobLogQueryParam | None): 查询参数模型, 包含分页信息和查询条件
|
||||
- order_by (list[dict] | None): 排序参数列表, 每个元素为一个字典, 包含字段名和排序方向
|
||||
|
||||
返回:
|
||||
- List[Dict]: 定时任务日志详情字典列表
|
||||
- list[dict]: 定时任务日志详情字典列表
|
||||
"""
|
||||
obj_list = await JobLogCRUD(auth).get_obj_log_list_crud(search=search.__dict__, order_by=order_by)
|
||||
return [JobLogOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
@@ -275,7 +272,7 @@ class JobLogService:
|
||||
await JobLogCRUD(auth).delete_obj_log_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def export_job_log_service(cls, data_list: List[Dict[str, Any]]) -> bytes:
|
||||
async def export_job_log_service(cls, data_list: list[dict]) -> bytes:
|
||||
"""
|
||||
导出定时任务日志列表
|
||||
|
||||
@@ -303,7 +300,7 @@ class JobLogService:
|
||||
# 复制数据并转换状态
|
||||
data = data_list.copy()
|
||||
for item in data:
|
||||
item['status'] = '成功' if item.get('status') else '失败'
|
||||
item['status'] = '成功' if item.get('status') == '0' else '失败'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import json
|
||||
import importlib
|
||||
from datetime import datetime
|
||||
from typing import Union, List, Any, Optional, Callable, Dict
|
||||
from typing import Any
|
||||
from asyncio import iscoroutinefunction
|
||||
from apscheduler.job import Job
|
||||
from apscheduler.events import JobExecutionEvent, JobEvent
|
||||
from apscheduler.events import JobExecutionEvent, EVENT_ALL, JobEvent
|
||||
from apscheduler.executors.asyncio import AsyncIOExecutor
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.executors.pool import ProcessPoolExecutor
|
||||
@@ -24,36 +24,7 @@ from app.core.exceptions import CustomException
|
||||
from app.core.logger import log
|
||||
from app.utils.cron_util import CronUtil
|
||||
|
||||
from app.api.v1.module_application.job.model import JobLogModel, JobModel
|
||||
|
||||
# 租户上下文管理器
|
||||
class TenantContext:
|
||||
"""
|
||||
租户上下文管理器
|
||||
用于在任务执行时保存和恢复租户上下文
|
||||
"""
|
||||
_current_tenant_id = None
|
||||
_current_user_id = None
|
||||
|
||||
@classmethod
|
||||
def set(cls, tenant_id: int | None = None, user_id: int | None = None):
|
||||
"""设置租户上下文"""
|
||||
cls._current_tenant_id = tenant_id
|
||||
cls._current_user_id = user_id
|
||||
|
||||
@classmethod
|
||||
def get(cls) -> Dict[str, Optional[int]]:
|
||||
"""获取租户上下文"""
|
||||
return {
|
||||
'tenant_id': cls._current_tenant_id,
|
||||
'user_id': cls._current_user_id
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def clear(cls):
|
||||
"""清除租户上下文"""
|
||||
cls._current_tenant_id = None
|
||||
cls._current_user_id = None
|
||||
from app.api.v1.module_application.job.model import JobModel
|
||||
|
||||
job_stores = {
|
||||
'default': MemoryJobStore(),
|
||||
@@ -89,7 +60,6 @@ class SchedulerUtil:
|
||||
"""
|
||||
定时任务相关方法
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def scheduler_event_listener(cls, event: JobEvent | JobExecutionEvent) -> None:
|
||||
"""
|
||||
@@ -101,8 +71,8 @@ class SchedulerUtil:
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
# 使用配置的模型路径
|
||||
from ..model import JobLogModel
|
||||
# 延迟导入避免循环导入
|
||||
from app.api.v1.module_application.job.model import JobLogModel
|
||||
|
||||
# 获取事件类型和任务ID
|
||||
event_type = event.__class__.__name__
|
||||
@@ -114,20 +84,14 @@ class SchedulerUtil:
|
||||
status = False
|
||||
if hasattr(event, 'job_id'):
|
||||
job_id = event.job_id
|
||||
|
||||
# 从任务ID中提取租户信息
|
||||
tenant_info = cls._extract_tenant_info(job_id)
|
||||
tenant_id = tenant_info.get('tenant_id')
|
||||
|
||||
# 使用原始任务ID查询任务信息
|
||||
query_job = cls.get_job(job_id=job_id, tenant_id=tenant_id)
|
||||
query_job = cls.get_job(job_id=job_id)
|
||||
if query_job:
|
||||
query_job_info = query_job.__getstate__()
|
||||
# 获取任务名称
|
||||
job_name = query_job_info.get('name')
|
||||
# 获取任务组名
|
||||
job_group = query_job._jobstore_alias
|
||||
# 获取任务执行器
|
||||
# # 获取任务执行器
|
||||
job_executor = query_job_info.get('executor')
|
||||
# 获取调用目标字符串
|
||||
invoke_target = query_job_info.get('func')
|
||||
@@ -138,7 +102,7 @@ class SchedulerUtil:
|
||||
# 获取任务触发器
|
||||
job_trigger = str(query_job_info.get('trigger'))
|
||||
# 构造日志消息
|
||||
job_message = f"事件类型: {event_type}, 任务ID: {job_id}, 租户ID: {tenant_id}, 任务名称: {job_name}, 状态: {status}, 任务组: {job_group}, 错误详情: {exception_info}, 执行于{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
job_message = f"事件类型: {event_type}, 任务ID: {job_id}, 任务名称: {job_name}, 状态: {status}, 任务组: {job_group}, 错误详情: {exception_info}, 执行于{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
|
||||
# 创建ORM对象
|
||||
job_log = JobLogModel(
|
||||
@@ -154,155 +118,63 @@ class SchedulerUtil:
|
||||
exception_info=exception_info,
|
||||
create_time=datetime.now(),
|
||||
job_id=job_id,
|
||||
tenant_id=tenant_id # 添加租户ID
|
||||
)
|
||||
|
||||
# 使用线程池执行操作以避免阻塞调度器和数据库锁定问题
|
||||
executor = ThreadPoolExecutor(max_workers=1)
|
||||
executor.submit(cls._save_job_log_async_wrapper, job_log, tenant_id)
|
||||
executor.submit(cls._save_job_log_async_wrapper, job_log)
|
||||
executor.shutdown(wait=False)
|
||||
|
||||
log.info(f"任务执行事件: {event_type}, 租户ID: {tenant_id}, 任务ID: {job_id}")
|
||||
|
||||
@classmethod
|
||||
def _save_job_log_async_wrapper(cls, job_log, tenant_id):
|
||||
def _save_job_log_async_wrapper(cls, job_log) -> None:
|
||||
"""
|
||||
异步保存任务日志的包装器函数,在独立线程中运行
|
||||
|
||||
参数:
|
||||
- job_log (JobLogModel): 任务日志对象
|
||||
- tenant_id (int): 租户ID
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
# 设置租户上下文用于日志保存
|
||||
TenantContext.set(tenant_id=tenant_id)
|
||||
try:
|
||||
with db_session() as session:
|
||||
try:
|
||||
# 确保session能正确处理多租户隔离
|
||||
session.add(job_log)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
log.error(f"保存任务日志失败 (租户ID: {tenant_id}): {str(e)}")
|
||||
finally:
|
||||
session.close()
|
||||
finally:
|
||||
# 清除租户上下文
|
||||
TenantContext.clear()
|
||||
|
||||
@classmethod
|
||||
def _format_job_id(cls, job_id: str, tenant_id: int) -> str:
|
||||
"""
|
||||
格式化任务ID,添加租户标识前缀
|
||||
|
||||
参数:
|
||||
- job_id: 原始任务ID
|
||||
- tenant_id: 租户ID
|
||||
|
||||
返回:
|
||||
- str: 格式化后的任务ID
|
||||
"""
|
||||
return f"tenant_{tenant_id}_{job_id}"
|
||||
|
||||
@classmethod
|
||||
def _extract_tenant_info(cls, formatted_job_id: str) -> Dict[str, Optional[int]]:
|
||||
"""
|
||||
从格式化的任务ID中提取租户信息
|
||||
|
||||
参数:
|
||||
- formatted_job_id: 格式化的任务ID
|
||||
|
||||
返回:
|
||||
- Dict: 包含租户信息的字典
|
||||
"""
|
||||
parts = formatted_job_id.split('_', 2)
|
||||
if len(parts) >= 3 and parts[0] == 'tenant':
|
||||
with db_session.begin() as session:
|
||||
try:
|
||||
return {
|
||||
'tenant_id': int(parts[1]),
|
||||
}
|
||||
except ValueError:
|
||||
pass
|
||||
return {
|
||||
'tenant_id': None,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _wrap_function_with_context(cls, func: Callable, tenant_id: int | None = None, user_id: int | None = None) -> Callable:
|
||||
"""
|
||||
包装函数,在执行前设置租户上下文
|
||||
|
||||
参数:
|
||||
- func: 原始函数
|
||||
- tenant_id: 租户ID
|
||||
- user_id: 用户ID
|
||||
|
||||
返回:
|
||||
- Callable: 包装后的函数
|
||||
"""
|
||||
async def async_wrapped(*args, **kwargs):
|
||||
try:
|
||||
# 设置租户上下文
|
||||
TenantContext.set(tenant_id=tenant_id, user_id=user_id)
|
||||
# 执行原始函数
|
||||
return await func(*args, **kwargs)
|
||||
session.add(job_log)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
log.error(f"保存任务日志失败: {str(e)}")
|
||||
finally:
|
||||
# 清除租户上下文
|
||||
TenantContext.clear()
|
||||
|
||||
def sync_wrapped(*args, **kwargs):
|
||||
try:
|
||||
# 设置租户上下文
|
||||
TenantContext.set(tenant_id=tenant_id, user_id=user_id)
|
||||
# 执行原始函数
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
# 清除租户上下文
|
||||
TenantContext.clear()
|
||||
|
||||
return async_wrapped if iscoroutinefunction(func) else sync_wrapped
|
||||
session.close()
|
||||
|
||||
@classmethod
|
||||
async def init_system_scheduler(cls):
|
||||
async def init_system_scheduler(cls) -> None:
|
||||
"""
|
||||
应用启动时初始化定时任务。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
# 延迟导入避免循环导入
|
||||
from app.api.v1.module_application.job.crud import JobCRUD
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from ..crud import JobCRUD
|
||||
|
||||
log.info('🔎 开始启动定时任务...')
|
||||
scheduler.start()
|
||||
async with async_db_session() as session:
|
||||
async with session.begin():
|
||||
# 在初始化过程中,不需要检查数据权限
|
||||
auth = AuthSchema(db=session, check_data_scope=False)
|
||||
auth = AuthSchema(db=session)
|
||||
job_list = await JobCRUD(auth).get_obj_list_crud()
|
||||
for item in job_list:
|
||||
|
||||
# 删除旧任务(使用租户ID进行格式化)
|
||||
cls.remove_job(job_id=item.id, tenant_id=item.tenant_id)
|
||||
|
||||
# 添加任务,传入租户ID
|
||||
cls.add_job(item, tenant_id=item.tenant_id)
|
||||
|
||||
cls.remove_job(job_id=item.id) # 删除旧任务
|
||||
cls.add_job(item)
|
||||
# 根据数据库中保存的状态来设置任务状态
|
||||
if item.status is False:
|
||||
if hasattr(item, 'status') and item.status == "1":
|
||||
# 如果任务状态为暂停,则立即暂停刚添加的任务
|
||||
cls.pause_job(job_id=item.id, tenant_id=item.tenant_id)
|
||||
|
||||
# 添加租户隔离的事件监听器,只监听任务执行相关事件
|
||||
from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR, EVENT_JOB_MISSED, EVENT_JOB_ADDED, EVENT_JOB_REMOVED
|
||||
scheduler.add_listener(cls.scheduler_event_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR | EVENT_JOB_MISSED | EVENT_JOB_ADDED | EVENT_JOB_REMOVED)
|
||||
cls.pause_job(job_id=item.id)
|
||||
scheduler.add_listener(cls.scheduler_event_listener, EVENT_ALL)
|
||||
log.info('✅️ 系统初始定时任务加载成功')
|
||||
|
||||
@classmethod
|
||||
async def close_system_scheduler(cls):
|
||||
async def close_system_scheduler(cls) -> None:
|
||||
"""
|
||||
关闭系统定时任务。
|
||||
|
||||
@@ -319,54 +191,43 @@ class SchedulerUtil:
|
||||
log.error(f'关闭定时任务失败: {str(e)}')
|
||||
|
||||
@classmethod
|
||||
def get_job(cls, job_id: int, tenant_id: Optional[int] = None) -> Optional[Job]:
|
||||
def get_job(cls, job_id: str | int) -> Job | None:
|
||||
"""
|
||||
根据任务ID获取任务对象。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
- tenant_id (int, optional): 租户ID,如果提供则使用租户隔离的任务ID。
|
||||
|
||||
返回:
|
||||
- Optional[Job]: 任务对象,未找到则为 None。
|
||||
- Job | None: 任务对象,未找到则为 None。
|
||||
"""
|
||||
# 如果提供了租户ID,则格式化任务ID
|
||||
formatted_job_id = cls._format_job_id(str(job_id), tenant_id) if tenant_id is not None else str(job_id)
|
||||
return scheduler.get_job(job_id=formatted_job_id)
|
||||
return scheduler.get_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def get_all_jobs(cls) -> List[Job]:
|
||||
def get_all_jobs(cls) -> list[Job]:
|
||||
"""
|
||||
获取全部调度任务列表。
|
||||
|
||||
返回:
|
||||
- List[Job]: 任务列表。
|
||||
- list[Job]: 任务列表。
|
||||
"""
|
||||
return scheduler.get_jobs()
|
||||
|
||||
@classmethod
|
||||
def add_job(cls, job_info: JobModel, tenant_id: int | None = None) -> Job:
|
||||
def add_job(cls, job_info: JobModel) -> Job:
|
||||
"""
|
||||
根据任务配置创建并添加调度任务。
|
||||
|
||||
参数:
|
||||
- job_info (JobModel): 任务对象信息(包含触发器、函数、参数等)。
|
||||
- tenant_id (int, optional): 租户ID,用于多租户隔离。
|
||||
|
||||
返回:
|
||||
- Job: 新增的任务对象。
|
||||
"""
|
||||
# 从job_info中获取租户ID(如果存在)
|
||||
if tenant_id is None and hasattr(job_info, 'tenant_id'):
|
||||
tenant_id = job_info.tenant_id
|
||||
|
||||
# 动态导入模块
|
||||
# 1. 解析调用目标
|
||||
module_path, func_name = str(job_info.func).rsplit('.', 1)
|
||||
# 使用配置或动态模块路径,避免硬编码
|
||||
base_module_path = getattr(settings, 'TASK_MODULE_BASE_PATH', 'app.api.v1.module_application.job.function_task')
|
||||
module_path = f"{base_module_path}.{module_path}"
|
||||
|
||||
module_path = "app.api.v1.module_application.job.function_task." + module_path
|
||||
try:
|
||||
module = importlib.import_module(module_path)
|
||||
job_func = getattr(module, func_name)
|
||||
@@ -380,11 +241,8 @@ class SchedulerUtil:
|
||||
if job_info.trigger_args is None:
|
||||
raise ValueError("interval 触发器缺少参数")
|
||||
|
||||
# 确定执行器类型
|
||||
if iscoroutinefunction(job_func):
|
||||
job_executor = 'default'
|
||||
|
||||
# 3. 创建触发器
|
||||
if job_info.trigger == 'date':
|
||||
trigger = DateTrigger(run_date=job_info.trigger_args)
|
||||
elif job_info.trigger == 'interval':
|
||||
@@ -433,78 +291,44 @@ class SchedulerUtil:
|
||||
else:
|
||||
raise ValueError("无效的 trigger 触发器")
|
||||
|
||||
# 4. 准备任务参数
|
||||
args = str(job_info.args).split(',') if job_info.args else None
|
||||
kwargs = json.loads(job_info.kwargs) if job_info.kwargs else {}
|
||||
|
||||
# 添加租户信息到kwargs
|
||||
if tenant_id is not None:
|
||||
kwargs['tenant_id'] = tenant_id
|
||||
# 获取创建者信息(如果存在)
|
||||
if hasattr(job_info, 'created_by'):
|
||||
kwargs['created_by'] = job_info.created_by
|
||||
|
||||
# 5. 包装函数,添加租户上下文
|
||||
# 获取创建者信息作为user_id
|
||||
user_id = getattr(job_info, 'created_by', None)
|
||||
wrapped_func = cls._wrap_function_with_context(job_func, tenant_id, user_id)
|
||||
|
||||
# 6. 生成任务ID
|
||||
job_id = str(job_info.id)
|
||||
formatted_job_id = cls._format_job_id(job_id, tenant_id) if tenant_id is not None else job_id
|
||||
|
||||
# 7. 添加任务
|
||||
# 3. 添加任务
|
||||
job = scheduler.add_job(
|
||||
func=wrapped_func, # 使用包装后的函数
|
||||
func=job_func, # 直接使用函数对象
|
||||
trigger=trigger,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
id=formatted_job_id,
|
||||
name=f"{job_info.name} (租户:{tenant_id or '系统'})" if tenant_id is not None else job_info.name,
|
||||
args=str(job_info.args).split(',') if job_info.args else None,
|
||||
kwargs=json.loads(job_info.kwargs) if job_info.kwargs else None,
|
||||
id=str(job_info.id),
|
||||
name=job_info.name,
|
||||
coalesce=job_info.coalesce,
|
||||
max_instances=job_info.max_instances,
|
||||
jobstore=job_info.jobstore,
|
||||
executor=job_executor,
|
||||
# 添加任务元数据
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
log.info(f"添加任务成功: ID={formatted_job_id}, 名称={job_info.name}, 租户ID={tenant_id}")
|
||||
return job
|
||||
except ModuleNotFoundError:
|
||||
raise ValueError(f"未找到该模块:{module_path}")
|
||||
except AttributeError:
|
||||
raise ValueError(f"未找到该模块下的方法:{func_name}")
|
||||
except Exception as e:
|
||||
log.error(f"添加任务失败 (租户ID: {tenant_id}, 任务ID: {job_info.id}): {str(e)}")
|
||||
raise CustomException(msg=f"添加任务失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
def remove_job(cls, job_id: Union[str, int], tenant_id: Optional[int] = None) -> None:
|
||||
def remove_job(cls, job_id: str | int) -> None:
|
||||
"""
|
||||
根据任务ID删除调度任务。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
- tenant_id (int, optional): 租户ID,如果提供则使用租户隔离的任务ID。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
# 格式化任务ID
|
||||
job_id_str = str(job_id)
|
||||
formatted_job_id = cls._format_job_id(job_id_str, tenant_id) if tenant_id is not None else job_id_str
|
||||
|
||||
# 先尝试直接删除格式化后的任务ID
|
||||
try:
|
||||
scheduler.remove_job(job_id=formatted_job_id)
|
||||
log.info(f"删除任务成功: ID={formatted_job_id}, 租户ID={tenant_id}")
|
||||
except Exception as e:
|
||||
# 如果失败,记录日志但不抛出异常
|
||||
log.warning(f"删除任务失败 (可能不存在): ID={formatted_job_id}, 租户ID={tenant_id}, 错误: {str(e)}")
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if query_job:
|
||||
scheduler.remove_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def clear_jobs(cls):
|
||||
def clear_jobs(cls) -> None:
|
||||
"""
|
||||
删除所有调度任务。
|
||||
|
||||
@@ -514,7 +338,7 @@ class SchedulerUtil:
|
||||
scheduler.remove_all_jobs()
|
||||
|
||||
@classmethod
|
||||
def modify_job(cls, job_id: int) -> Job:
|
||||
def modify_job(cls, job_id: str | int) -> Job:
|
||||
"""
|
||||
更新指定任务的配置(运行中的任务下次执行生效)。
|
||||
|
||||
@@ -527,19 +351,18 @@ class SchedulerUtil:
|
||||
异常:
|
||||
- CustomException: 当任务不存在时抛出。
|
||||
"""
|
||||
query_job = cls.get_job(job_id=job_id)
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if not query_job:
|
||||
raise CustomException(msg=f"未找到该任务:{job_id}")
|
||||
return scheduler.modify_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def pause_job(cls, job_id: int, tenant_id: int | None = None):
|
||||
def pause_job(cls, job_id: str | int) -> None:
|
||||
"""
|
||||
暂停指定任务(仅运行中可暂停,已终止不可)。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
- tenant_id (int, optional): 租户ID,如果提供则使用租户隔离的任务ID。
|
||||
|
||||
返回:
|
||||
- None
|
||||
@@ -547,21 +370,18 @@ class SchedulerUtil:
|
||||
异常:
|
||||
- ValueError: 当任务不存在时抛出。
|
||||
"""
|
||||
formatted_job_id = cls._format_job_id(str(job_id), tenant_id) if tenant_id is not None else str(job_id)
|
||||
query_job = cls.get_job(job_id=job_id, tenant_id=tenant_id)
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if not query_job:
|
||||
raise ValueError(f"未找到该任务:{job_id} (租户: {tenant_id})")
|
||||
scheduler.pause_job(job_id=formatted_job_id)
|
||||
log.info(f"暂停任务成功: ID={formatted_job_id}, 租户ID={tenant_id}")
|
||||
raise ValueError(f"未找到该任务:{job_id}")
|
||||
scheduler.pause_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def resume_job(cls, job_id: int, tenant_id: int | None = None):
|
||||
def resume_job(cls, job_id: str | int) -> None:
|
||||
"""
|
||||
恢复指定任务(仅暂停中可恢复,已终止不可)。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
- tenant_id (int, optional): 租户ID,如果提供则使用租户隔离的任务ID。
|
||||
|
||||
返回:
|
||||
- None
|
||||
@@ -569,21 +389,18 @@ class SchedulerUtil:
|
||||
异常:
|
||||
- ValueError: 当任务不存在时抛出。
|
||||
"""
|
||||
formatted_job_id = cls._format_job_id(str(job_id), tenant_id) if tenant_id is not None else str(job_id)
|
||||
query_job = cls.get_job(job_id=job_id, tenant_id=tenant_id)
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if not query_job:
|
||||
raise ValueError(f"未找到该任务:{job_id} (租户: {tenant_id})")
|
||||
scheduler.resume_job(job_id=formatted_job_id)
|
||||
log.info(f"恢复任务成功: ID={formatted_job_id}, 租户ID={tenant_id}")
|
||||
raise ValueError(f"未找到该任务:{job_id}")
|
||||
scheduler.resume_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def reschedule_job(cls, job_id: int, tenant_id: int, trigger=None, **trigger_args) -> Optional[Job]:
|
||||
def reschedule_job(cls, job_id: str | int, trigger=None, **trigger_args) -> Job | None:
|
||||
"""
|
||||
重启指定任务的触发器。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
- tenant_id (int, optional): 租户ID,如果提供则使用租户隔离的任务ID。
|
||||
- trigger: 触发器类型
|
||||
- **trigger_args: 触发器参数
|
||||
|
||||
@@ -593,48 +410,37 @@ class SchedulerUtil:
|
||||
异常:
|
||||
- CustomException: 当任务不存在时抛出。
|
||||
"""
|
||||
# 格式化任务ID
|
||||
job_id_str = str(job_id)
|
||||
formatted_job_id = cls._format_job_id(job_id_str, tenant_id) if tenant_id is not None else job_id_str
|
||||
|
||||
query_job = cls.get_job(job_id=job_id, tenant_id=tenant_id)
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if not query_job:
|
||||
raise CustomException(msg=f"未找到该任务:{job_id} (租户: {tenant_id})")
|
||||
raise CustomException(msg=f"未找到该任务:{job_id}")
|
||||
|
||||
# 如果没有提供新的触发器,则使用现有触发器
|
||||
if trigger is None:
|
||||
# 获取当前任务的触发器配置
|
||||
current_trigger = query_job.trigger
|
||||
# 重新调度任务,使用当前的触发器
|
||||
result = scheduler.reschedule_job(job_id=formatted_job_id, trigger=current_trigger)
|
||||
return scheduler.reschedule_job(job_id=str(job_id), trigger=current_trigger)
|
||||
else:
|
||||
# 使用新提供的触发器
|
||||
result = scheduler.reschedule_job(job_id=formatted_job_id, trigger=trigger, **trigger_args)
|
||||
|
||||
log.info(f"重新调度任务成功: ID={formatted_job_id}, 租户ID={tenant_id}")
|
||||
return result
|
||||
return scheduler.reschedule_job(job_id=str(job_id), trigger=trigger, **trigger_args)
|
||||
|
||||
@classmethod
|
||||
def get_single_job_status(cls, job_id: int, tenant_id: int | None = None) -> str:
|
||||
def get_single_job_status(cls, job_id: str | int) -> str:
|
||||
"""
|
||||
获取单个任务的当前状态。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID
|
||||
- tenant_id (int, optional): 租户ID,如果提供则使用租户隔离的任务ID。
|
||||
|
||||
返回:
|
||||
- str: 任务状态('running' | 'paused' | 'stopped' | 'unknown')
|
||||
"""
|
||||
job = cls.get_job(job_id=job_id, tenant_id=tenant_id)
|
||||
job = cls.get_job(job_id=str(job_id))
|
||||
if not job:
|
||||
return 'unknown'
|
||||
|
||||
job_id_str = str(job_id)
|
||||
formatted_job_id = cls._format_job_id(job_id_str, tenant_id) if tenant_id is not None else job_id_str
|
||||
|
||||
# 检查任务是否在暂停列表中
|
||||
if formatted_job_id in scheduler._jobstores[job._jobstore_alias]._paused_jobs:
|
||||
if job_id in scheduler._jobstores[job._jobstore_alias]._paused_jobs:
|
||||
return 'paused'
|
||||
|
||||
# 检查调度器状态
|
||||
@@ -643,82 +449,23 @@ class SchedulerUtil:
|
||||
|
||||
return 'running'
|
||||
|
||||
@classmethod
|
||||
def get_jobs_by_tenant(cls, tenant_id: int, jobstore: Optional[str] = None) -> List[Job]:
|
||||
"""
|
||||
获取指定租户的所有任务
|
||||
|
||||
参数:
|
||||
- tenant_id (int): 租户ID
|
||||
- jobstore (str, optional): 任务存储别名
|
||||
|
||||
返回:
|
||||
- List[Job]: 任务列表
|
||||
"""
|
||||
all_jobs = scheduler.get_jobs(jobstore=jobstore)
|
||||
tenant_jobs = []
|
||||
|
||||
for job in all_jobs:
|
||||
tenant_info = cls._extract_tenant_info(job.id)
|
||||
if tenant_info.get('tenant_id') == tenant_id:
|
||||
tenant_jobs.append(job)
|
||||
|
||||
return tenant_jobs
|
||||
|
||||
# 获取当前租户上下文的辅助函数
|
||||
@classmethod
|
||||
def get_current_tenant(cls) -> Dict[str, Optional[int]]:
|
||||
"""
|
||||
获取当前任务执行的租户上下文
|
||||
|
||||
返回:
|
||||
- Dict: 包含租户ID和用户ID的字典
|
||||
"""
|
||||
return TenantContext.get()
|
||||
|
||||
@classmethod
|
||||
def export_jobs(cls):
|
||||
"""
|
||||
导出任务到文件,使用配置的路径。
|
||||
"""
|
||||
from app.config.setting import settings
|
||||
from app.core.logger import log
|
||||
|
||||
# 使用配置的导出路径或默认路径
|
||||
export_path = getattr(settings, 'JOB_EXPORT_PATH', '/tmp/jobs.json')
|
||||
try:
|
||||
scheduler.export_jobs(export_path)
|
||||
log.info(f"任务导出成功: {export_path}")
|
||||
except Exception as e:
|
||||
log.error(f"任务导出失败: {str(e)}")
|
||||
raise
|
||||
scheduler.export_jobs("/temp/jobs.json")
|
||||
|
||||
@classmethod
|
||||
def import_jobs(cls):
|
||||
"""
|
||||
从文件导入任务,使用配置的路径。
|
||||
"""
|
||||
from app.config.setting import settings
|
||||
from app.core.logger import log
|
||||
|
||||
# 使用配置的导入路径或默认路径
|
||||
import_path = getattr(settings, 'JOB_IMPORT_PATH', '/tmp/jobs.json')
|
||||
try:
|
||||
scheduler.import_jobs(import_path)
|
||||
log.info(f"任务导入成功: {import_path}")
|
||||
except Exception as e:
|
||||
log.error(f"任务导入失败: {str(e)}")
|
||||
raise
|
||||
scheduler.import_jobs("/temp/jobs.json")
|
||||
|
||||
@classmethod
|
||||
def print_jobs(cls,jobstore: Any | None = None, out: Any | None = None):
|
||||
"""
|
||||
打印调度任务列表。
|
||||
|
||||
|
||||
参数:
|
||||
- jobstore (Any | None): 任务存储别名。
|
||||
- out (Any | None): 输出目标。
|
||||
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
@@ -728,7 +475,7 @@ class SchedulerUtil:
|
||||
def get_job_status(cls) -> str:
|
||||
"""
|
||||
获取调度器当前状态。
|
||||
|
||||
|
||||
返回:
|
||||
- str: 状态字符串('stopped' | 'running' | 'paused' | 'unknown')。
|
||||
"""
|
||||
|
||||
@@ -7,11 +7,11 @@ from app.common.response import SuccessResponse
|
||||
from app.common.request import PaginationService
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.logger import log
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from .service import ApplicationService
|
||||
from .schema import (
|
||||
ApplicationCreateSchema,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Dict, List, Optional, Sequence, Union, Any
|
||||
from typing import Sequence, Any
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import ApplicationModel
|
||||
from .schema import ApplicationCreateSchema, ApplicationUpdateSchema
|
||||
@@ -22,34 +21,34 @@ class ApplicationCRUD(CRUDBase[ApplicationModel, ApplicationCreateSchema, Applic
|
||||
self.auth = auth
|
||||
super().__init__(model=ApplicationModel, auth=auth)
|
||||
|
||||
async def get_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[ApplicationModel]:
|
||||
async def get_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> ApplicationModel | None:
|
||||
"""
|
||||
根据id获取应用详情
|
||||
|
||||
参数:
|
||||
- id (int): 应用ID
|
||||
- preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Optional[ApplicationModel]: 应用详情,如果不存在则为None
|
||||
- ApplicationModel | None: 应用详情,如果不存在则为None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[ApplicationModel]:
|
||||
async def list_crud(self, search: dict[str, Any] | None = None, order_by: list[dict[str, str]] | None = None, preload: list[str | Any] | None = None) -> Sequence[ApplicationModel]:
|
||||
"""
|
||||
列表查询应用
|
||||
|
||||
参数:
|
||||
- search (Optional[Dict]): 查询参数,默认None
|
||||
- order_by (Optional[List[Dict[str, str]]]): 排序参数,默认None
|
||||
- preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项
|
||||
- search (dict[str, Any] | None): 查询参数,默认None
|
||||
- order_by (list[dict[str, str]] | None): 排序参数,默认None
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[ApplicationModel]: 应用列表
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_crud(self, data: ApplicationCreateSchema) -> Optional[ApplicationModel]:
|
||||
async def create_crud(self, data: ApplicationCreateSchema) -> ApplicationModel | None:
|
||||
"""
|
||||
创建应用
|
||||
|
||||
@@ -57,11 +56,11 @@ class ApplicationCRUD(CRUDBase[ApplicationModel, ApplicationCreateSchema, Applic
|
||||
- data (ApplicationCreateSchema): 应用创建模型
|
||||
|
||||
返回:
|
||||
- Optional[ApplicationModel]: 创建的应用详情,如果创建失败则为None
|
||||
- ApplicationModel | None: 创建的应用详情,如果创建失败则为None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_crud(self, id: int, data: ApplicationUpdateSchema) -> Optional[ApplicationModel]:
|
||||
async def update_crud(self, id: int, data: ApplicationUpdateSchema) -> ApplicationModel | None:
|
||||
"""
|
||||
更新应用
|
||||
|
||||
@@ -70,25 +69,25 @@ class ApplicationCRUD(CRUDBase[ApplicationModel, ApplicationCreateSchema, Applic
|
||||
- data (ApplicationUpdateSchema): 应用更新模型
|
||||
|
||||
返回:
|
||||
- Optional[ApplicationModel]: 更新后的应用详情,如果更新失败则为None
|
||||
- ApplicationModel | None: 更新后的应用详情,如果更新失败则为None
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_crud(self, ids: List[int]) -> None:
|
||||
async def delete_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
批量删除应用
|
||||
|
||||
参数:
|
||||
- ids (List[int]): 应用ID列表
|
||||
- ids (list[int]): 应用ID列表
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_available_crud(self, ids: List[int], status: str) -> None:
|
||||
async def set_available_crud(self, ids: list[int], status: str) -> None:
|
||||
"""
|
||||
批量设置可用状态
|
||||
|
||||
参数:
|
||||
- ids (List[int]): 应用ID列表
|
||||
- status (bool): 可用状态,True为可用,False为不可用
|
||||
- ids (list[int]): 应用ID列表
|
||||
- status (str): 可用状态,True为可用,False为不可用
|
||||
"""
|
||||
return await self.set(ids=ids, status=status)
|
||||
@@ -3,24 +3,16 @@
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin, TenantMixin, CustomerMixin
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class ApplicationModel(ModelMixin, UserMixin, TenantMixin, CustomerMixin):
|
||||
class ApplicationModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
应用系统表 - 用于管理分系统和外部应用
|
||||
|
||||
数据隔离策略:
|
||||
===========
|
||||
- 系统级应用: tenant_id=1, customer_id=NULL (平台级应用,所有租户可见)
|
||||
- 租户级应用: tenant_id>1, customer_id=NULL (租户自己的应用,仅本租户可见)
|
||||
- 客户级应用: tenant_id>1, customer_id>0 (客户专属应用,仅该客户可见)
|
||||
|
||||
根据业务需求,此表支持三级隔离
|
||||
应用系统表
|
||||
"""
|
||||
__tablename__: str = 'app_myapp'
|
||||
__table_args__: dict[str, str] = ({'comment': '应用系统表'})
|
||||
__loader_options__: list[str] = ["created_by", "updated_by", "tenant", "customer"]
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False, comment='应用名称')
|
||||
access_url: Mapped[str] = mapped_column(String(500), nullable=False, comment='访问地址')
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from urllib.parse import urlparse
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
from app.core.base_schema import BaseSchema, UserBySchema, TenantSchema, CustomerSchema
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
|
||||
class ApplicationCreateSchema(BaseModel):
|
||||
"""应用创建模型"""
|
||||
name: str = Field(..., max_length=64, description='应用名称')
|
||||
access_url: str = Field(..., max_length=255, description="访问地址")
|
||||
icon_url: Optional[str] = Field(None, max_length=300, description="应用图标URL")
|
||||
status: str = Field(True, description="是否启用(0:启用 1:禁用)")
|
||||
description: Optional[str] = Field(default=None, max_length=255, description="描述")
|
||||
icon_url: str | None = Field(None, max_length=300, description="应用图标URL")
|
||||
status: str = Field("0", description="是否启用(0:启用 1:禁用)")
|
||||
description: str | None = Field(default=None, max_length=255, description="描述")
|
||||
|
||||
@field_validator('access_url')
|
||||
@classmethod
|
||||
@@ -30,7 +29,7 @@ class ApplicationCreateSchema(BaseModel):
|
||||
|
||||
@field_validator('icon_url')
|
||||
@classmethod
|
||||
def _validate_icon_url(cls, v: Optional[str]) -> Optional[str]:
|
||||
def _validate_icon_url(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
v = v.strip()
|
||||
@@ -47,7 +46,7 @@ class ApplicationUpdateSchema(ApplicationCreateSchema):
|
||||
...
|
||||
|
||||
|
||||
class ApplicationOutSchema(ApplicationCreateSchema, BaseSchema, UserBySchema, TenantSchema, CustomerSchema):
|
||||
class ApplicationOutSchema(ApplicationCreateSchema, BaseSchema, UserBySchema):
|
||||
"""应用响应模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -57,10 +56,12 @@ class ApplicationQueryParam:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: Optional[str] = Query(None, description="应用名称"),
|
||||
status: Optional[str] = Query(None, description="是否启用"),
|
||||
created_id: Optional[int] = Query(None, description="创建人"),
|
||||
created_time: Optional[list[DateTimeStr]] = Query(None, description="创建时间范围", example=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
name: str | None = Query(None, description="应用名称"),
|
||||
status: str | None = Query(None, description="是否启用"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", example=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", example=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
created_id: int | None = Query(None, description="创建人"),
|
||||
updated_id: int | None = Query(None, description="更新人"),
|
||||
) -> None:
|
||||
|
||||
# 模糊查询字段
|
||||
@@ -69,7 +70,10 @@ class ApplicationQueryParam:
|
||||
# 精确查询字段
|
||||
self.status = status
|
||||
self.created_id = created_id
|
||||
self.updated_id = updated_id
|
||||
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import List, Dict, Optional, Union
|
||||
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.exceptions import CustomException
|
||||
|
||||
@@ -21,7 +19,7 @@ class ApplicationService:
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> Dict:
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""
|
||||
获取应用详情
|
||||
|
||||
@@ -30,7 +28,7 @@ class ApplicationService:
|
||||
- id (int): 应用ID
|
||||
|
||||
返回:
|
||||
- Dict: 应用详情字典
|
||||
- dict: 应用详情字典
|
||||
"""
|
||||
obj = await ApplicationCRUD(auth).get_by_id_crud(id=id)
|
||||
if not obj:
|
||||
@@ -38,17 +36,17 @@ class ApplicationService:
|
||||
return ApplicationOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_service(cls, auth: AuthSchema, search: Optional[ApplicationQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict]:
|
||||
async def list_service(cls, auth: AuthSchema, search: ApplicationQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> list[dict]:
|
||||
"""
|
||||
获取应用列表
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- search (Optional[ApplicationQueryParam]): 查询参数模型
|
||||
- order_by (Optional[Union[str, List[Dict[str, str]]]]): 排序参数,支持字符串或字典列表
|
||||
- search (ApplicationQueryParam | None): 查询参数模型
|
||||
- order_by (list[dict[str, str]] | None): 排序参数,支持字符串或字典列表
|
||||
|
||||
返回:
|
||||
- List[Dict]: 应用详情字典列表
|
||||
- list[dict]: 应用详情字典列表
|
||||
"""
|
||||
# 过滤空值
|
||||
search_dict = search.__dict__ if search else None
|
||||
@@ -56,7 +54,7 @@ class ApplicationService:
|
||||
return [ApplicationOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def create_service(cls, auth: AuthSchema, data: ApplicationCreateSchema) -> Dict:
|
||||
async def create_service(cls, auth: AuthSchema, data: ApplicationCreateSchema) -> dict:
|
||||
"""
|
||||
创建应用
|
||||
|
||||
@@ -76,7 +74,7 @@ class ApplicationService:
|
||||
return ApplicationOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_service(cls, auth: AuthSchema, id: int, data: ApplicationUpdateSchema) -> Dict:
|
||||
async def update_service(cls, auth: AuthSchema, id: int, data: ApplicationUpdateSchema) -> dict:
|
||||
"""
|
||||
更新应用
|
||||
|
||||
|
||||
Reference in New Issue
Block a user