refactor: 移除多租户和客户相关代码及功能

refactor: 统一状态字段为字符串类型并更新相关组件
refactor: 更新模型基类移除租户和客户相关字段
refactor: 简化数据权限控制逻辑
refactor: 优化类型注解使用Python 3.10+语法
refactor: 清理无用导入和注释
docs: 更新文档移除多租户相关内容
This commit is contained in:
zhangtao
2025-12-01 00:36:05 +08:00
parent 463e2fca23
commit 3c12fa56eb
173 changed files with 3143 additions and 7672 deletions
@@ -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:
"""
更新应用