refactor(task): 重构定时任务与存储路由

- 移除各路由文件顶部冗余注释
- 将 JobRouter/NodeRouter 重命名为 CornJobRouter/CornJobNodeRouter
- 新增存储浏览、节点、传输、工作流路由注册
- 调整路由导入来源与任务表名
- 优化 main.py 启动方式及环境配置加载
This commit is contained in:
zhangtao
2026-09-06 04:14:17 +08:00
parent d470c7eb1f
commit 9e80f970d0
78 changed files with 4383 additions and 4383 deletions
-2
View File
@@ -1,5 +1,3 @@
"""ai 路由清单:仅聚合路由,不含业务逻辑。"""
from fastapi import APIRouter from fastapi import APIRouter
from app.modules.ai.chat.controller import ChatRouter from app.modules.ai.chat.controller import ChatRouter
+7
View File
@@ -0,0 +1,7 @@
from fastapi import APIRouter
from app.modules.common.file.controller import FileRouter
file_router = APIRouter()
file_router.include_router(FileRouter)
-9
View File
@@ -1,9 +0,0 @@
"""file 路由清单:仅聚合路由,不含业务逻辑。"""
from fastapi import APIRouter
from app.modules.file.file.controller import FileRouter
file_router = APIRouter(prefix="/file")
file_router.include_router(FileRouter)
-2
View File
@@ -1,5 +1,3 @@
"""generator 路由清单:仅聚合路由,不含业务逻辑。"""
from fastapi import APIRouter from fastapi import APIRouter
from app.modules.generator.gencode.controller import GenRouter from app.modules.generator.gencode.controller import GenRouter
-2
View File
@@ -1,5 +1,3 @@
"""monitor 路由清单:仅聚合路由,不含业务逻辑。"""
from fastapi import APIRouter from fastapi import APIRouter
from app.modules.monitor.cache.controller import CacheRouter from app.modules.monitor.cache.controller import CacheRouter
-2
View File
@@ -1,5 +1,3 @@
"""system 路由清单:仅聚合路由,不含业务逻辑。"""
from fastapi import APIRouter from fastapi import APIRouter
from app.modules.system.auth.controller import AuthRouter from app.modules.system.auth.controller import AuthRouter
+12 -6
View File
@@ -1,11 +1,17 @@
"""task 路由清单:仅聚合路由,不含业务逻辑。"""
from fastapi import APIRouter from fastapi import APIRouter
from app.modules.task.cronjob.job.controller import JobRouter from app.modules.task.cronjob.job.controller import CornJobRouter
from app.modules.task.cronjob.node.controller import NodeRouter from app.modules.task.cronjob.node.controller import CornJobNodeRouter
from app.modules.task.storage.browse.controller import StorageBrowseRouter
from app.modules.task.storage.node.controller import StorageNodeRouter
from app.modules.task.storage.transfer.controller import StorageTransferRouter
from app.modules.task.storage.workflow.controller import StorageWorkflowRouter
task_router = APIRouter(prefix="/task") task_router = APIRouter(prefix="/task")
task_router.include_router(JobRouter) task_router.include_router(CornJobRouter)
task_router.include_router(NodeRouter) task_router.include_router(CornJobNodeRouter)
task_router.include_router(StorageNodeRouter)
task_router.include_router(StorageBrowseRouter)
task_router.include_router(StorageTransferRouter)
task_router.include_router(StorageWorkflowRouter)
-15
View File
@@ -1,15 +0,0 @@
"""workflow 路由清单:仅聚合路由,不含业务逻辑。"""
from fastapi import APIRouter
from app.modules.workflow.flow.controller import WorkflowFlowRouter
from app.modules.workflow.source.controller import StorageSourceRouter
from app.modules.workflow.storage.controller import StorageFileRouter
from app.modules.workflow.transfer.controller import StorageTransferRouter
workflow_router = APIRouter(prefix="/workflow")
workflow_router.include_router(StorageSourceRouter)
workflow_router.include_router(StorageFileRouter)
workflow_router.include_router(StorageTransferRouter)
workflow_router.include_router(WorkflowFlowRouter)
+1 -3
View File
@@ -75,12 +75,11 @@ def register_exceptions(app: FastAPI) -> None:
def register_routers(app: FastAPI) -> None: def register_routers(app: FastAPI) -> None:
from app.api.v1.ai import ai_router from app.api.v1.ai import ai_router
from app.api.v1.file import file_router from app.api.v1.common import file_router
from app.api.v1.generator import generator_router from app.api.v1.generator import generator_router
from app.api.v1.monitor import monitor_router from app.api.v1.monitor import monitor_router
from app.api.v1.system import system_router from app.api.v1.system import system_router
from app.api.v1.task import task_router from app.api.v1.task import task_router
from app.api.v1.workflow import workflow_router
app.include_router(file_router) app.include_router(file_router)
app.include_router(monitor_router) app.include_router(monitor_router)
@@ -88,7 +87,6 @@ def register_routers(app: FastAPI) -> None:
app.include_router(ai_router) app.include_router(ai_router)
app.include_router(generator_router) app.include_router(generator_router)
app.include_router(task_router) app.include_router(task_router)
app.include_router(workflow_router)
from app.core.discover import dynamic_router from app.core.discover import dynamic_router
dynamic_router.init_app(app) dynamic_router.init_app(app)
@@ -12,7 +12,7 @@ from app.utils.upload_util import UploadUtil
from .service import FileService from .service import FileService
FileRouter = APIRouter(route_class=OperationLogRoute, tags=["文件管理"]) FileRouter = APIRouter(route_class=OperationLogRoute, prefix="/file", tags=["文件管理"])
@FileRouter.post("/upload", summary="上传文件", response_model=ResponseSchema[UploadResponseSchema], dependencies=[Security(AuthPermission(["module_common:file:upload"]))]) @FileRouter.post("/upload", summary="上传文件", response_model=ResponseSchema[UploadResponseSchema], dependencies=[Security(AuthPermission(["module_common:file:upload"]))])
@@ -1 +0,0 @@
"""定时任务子包:调度器(job)与 APScheduler 节点(node)。"""
@@ -18,58 +18,58 @@ from .schema import (
) )
from .service import JobService from .service import JobService
JobRouter = APIRouter(route_class=OperationLogRoute, prefix="/cronjob/job", tags=["定时任务管理"]) CornJobRouter = APIRouter(route_class=OperationLogRoute, prefix="/cronjob/job", tags=["定时任务管理"])
@JobRouter.get("/scheduler/status", summary="获取调度器状态", response_model=ResponseSchema[SchedulerStatusSchema], dependencies=[Security(AuthPermission(["module_task:cronjob:job:query"]))]) @CornJobRouter.get("/scheduler/status", summary="获取调度器状态", response_model=ResponseSchema[SchedulerStatusSchema], dependencies=[Security(AuthPermission(["module_task:cronjob:job:query"]))])
async def get_scheduler_status_controller() -> JSONResponse: async def get_scheduler_status_controller() -> JSONResponse:
data: SchedulerStatusSchema = JobService.get_scheduler_status() data: SchedulerStatusSchema = JobService.get_scheduler_status()
return SuccessResponse(data=data, msg="获取调度器状态成功") return SuccessResponse(data=data, msg="获取调度器状态成功")
@JobRouter.get("/scheduler/jobs", summary="获取调度器任务列表", response_model=ResponseSchema[list[SchedulerJobSchema]], dependencies=[Security(AuthPermission(["module_task:cronjob:job:query"]))]) @CornJobRouter.get("/scheduler/jobs", summary="获取调度器任务列表", response_model=ResponseSchema[list[SchedulerJobSchema]], dependencies=[Security(AuthPermission(["module_task:cronjob:job:query"]))])
async def get_scheduler_jobs_controller() -> JSONResponse: async def get_scheduler_jobs_controller() -> JSONResponse:
data: list[SchedulerJobSchema] = JobService.get_scheduler_jobs() data: list[SchedulerJobSchema] = JobService.get_scheduler_jobs()
return SuccessResponse(data=data, msg="获取调度器任务列表成功") return SuccessResponse(data=data, msg="获取调度器任务列表成功")
@JobRouter.post("/scheduler/start", summary="启动调度器", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:scheduler"]))]) @CornJobRouter.post("/scheduler/start", summary="启动调度器", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:scheduler"]))])
async def start_scheduler_controller() -> JSONResponse: async def start_scheduler_controller() -> JSONResponse:
await JobService.start_scheduler() await JobService.start_scheduler()
return SuccessResponse(msg="调度器已启动") return SuccessResponse(msg="调度器已启动")
@JobRouter.post("/scheduler/pause", summary="暂停调度器", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:scheduler"]))]) @CornJobRouter.post("/scheduler/pause", summary="暂停调度器", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:scheduler"]))])
async def pause_scheduler_controller() -> JSONResponse: async def pause_scheduler_controller() -> JSONResponse:
JobService.pause_scheduler() JobService.pause_scheduler()
return SuccessResponse(msg="调度器已暂停") return SuccessResponse(msg="调度器已暂停")
@JobRouter.post("/scheduler/resume", summary="恢复调度器", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:scheduler"]))]) @CornJobRouter.post("/scheduler/resume", summary="恢复调度器", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:scheduler"]))])
async def resume_scheduler_controller() -> JSONResponse: async def resume_scheduler_controller() -> JSONResponse:
JobService.resume_scheduler() JobService.resume_scheduler()
return SuccessResponse(msg="调度器已恢复") return SuccessResponse(msg="调度器已恢复")
@JobRouter.post("/scheduler/shutdown", summary="关闭调度器", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:scheduler"]))]) @CornJobRouter.post("/scheduler/shutdown", summary="关闭调度器", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:scheduler"]))])
async def shutdown_scheduler_controller() -> JSONResponse: async def shutdown_scheduler_controller() -> JSONResponse:
JobService.shutdown_scheduler() JobService.shutdown_scheduler()
return SuccessResponse(msg="调度器已关闭") return SuccessResponse(msg="调度器已关闭")
@JobRouter.delete("/scheduler/jobs/clear", summary="清空所有任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:task"]))]) @CornJobRouter.delete("/scheduler/jobs/clear", summary="清空所有任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:task"]))])
async def clear_jobs_controller() -> JSONResponse: async def clear_jobs_controller() -> JSONResponse:
JobService.clear_scheduler_jobs() JobService.clear_scheduler_jobs()
return SuccessResponse(msg="已清空所有任务") return SuccessResponse(msg="已清空所有任务")
@JobRouter.get("/scheduler/console", summary="获取调度器控制台信息", response_model=ResponseSchema[str], dependencies=[Security(AuthPermission(["module_task:cronjob:job:query"]))]) @CornJobRouter.get("/scheduler/console", summary="获取调度器控制台信息", response_model=ResponseSchema[str], dependencies=[Security(AuthPermission(["module_task:cronjob:job:query"]))])
async def get_scheduler_console_controller() -> JSONResponse: async def get_scheduler_console_controller() -> JSONResponse:
console_output = JobService.get_scheduler_console() console_output = JobService.get_scheduler_console()
return SuccessResponse(data=console_output, msg="获取控制台信息成功") return SuccessResponse(data=console_output, msg="获取控制台信息成功")
@JobRouter.post("/task/pause/{job_id}", summary="暂停任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:task"]))]) @CornJobRouter.post("/task/pause/{job_id}", summary="暂停任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:task"]))])
async def pause_job_controller( async def pause_job_controller(
job_id: Annotated[str, Path(description="调度器任务ID")], job_id: Annotated[str, Path(description="调度器任务ID")],
) -> JSONResponse: ) -> JSONResponse:
@@ -77,7 +77,7 @@ async def pause_job_controller(
return SuccessResponse(msg="暂停任务成功") return SuccessResponse(msg="暂停任务成功")
@JobRouter.post("/task/resume/{job_id}", summary="恢复任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:task"]))]) @CornJobRouter.post("/task/resume/{job_id}", summary="恢复任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:task"]))])
async def resume_job_controller( async def resume_job_controller(
job_id: Annotated[str, Path(description="调度器任务ID")], job_id: Annotated[str, Path(description="调度器任务ID")],
) -> JSONResponse: ) -> JSONResponse:
@@ -85,7 +85,7 @@ async def resume_job_controller(
return SuccessResponse(msg="恢复任务成功") return SuccessResponse(msg="恢复任务成功")
@JobRouter.post("/task/run/{job_id}", summary="立即执行任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:task"]))]) @CornJobRouter.post("/task/run/{job_id}", summary="立即执行任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:task"]))])
async def run_job_controller( async def run_job_controller(
job_id: Annotated[str, Path(description="调度器任务ID")], job_id: Annotated[str, Path(description="调度器任务ID")],
) -> JSONResponse: ) -> JSONResponse:
@@ -93,7 +93,7 @@ async def run_job_controller(
return SuccessResponse(msg="立即执行任务成功") return SuccessResponse(msg="立即执行任务成功")
@JobRouter.put("/task/modify/{job_id}", summary="修改任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:task"]))]) @CornJobRouter.put("/task/modify/{job_id}", summary="修改任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:task"]))])
async def modify_job_controller( async def modify_job_controller(
job_id: Annotated[str, Path(description="调度器任务ID")], job_id: Annotated[str, Path(description="调度器任务ID")],
data: Annotated[SchedulerJobModifySchema, Body(description="要修改的任务属性,如 name、coalesce、max_instances 等")], data: Annotated[SchedulerJobModifySchema, Body(description="要修改的任务属性,如 name、coalesce、max_instances 等")],
@@ -102,7 +102,7 @@ async def modify_job_controller(
return SuccessResponse(msg="修改任务成功") return SuccessResponse(msg="修改任务成功")
@JobRouter.delete("/task/remove/{job_id}", summary="移除任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:delete"]))]) @CornJobRouter.delete("/task/remove/{job_id}", summary="移除任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:delete"]))])
async def remove_job_controller( async def remove_job_controller(
job_id: Annotated[str, Path(description="调度器任务ID")], job_id: Annotated[str, Path(description="调度器任务ID")],
) -> JSONResponse: ) -> JSONResponse:
@@ -110,7 +110,7 @@ async def remove_job_controller(
return SuccessResponse(msg="移除任务成功") return SuccessResponse(msg="移除任务成功")
@JobRouter.get("/log/list", summary="查询执行日志列表", response_model=ResponseSchema[PageResultSchema[JobOutSchema]]) @CornJobRouter.get("/log/list", summary="查询执行日志列表", response_model=ResponseSchema[PageResultSchema[JobOutSchema]])
async def get_job_log_list_controller( async def get_job_log_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:job:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:job:query"]))],
page: Annotated[PaginationQueryParam, Depends()], page: Annotated[PaginationQueryParam, Depends()],
@@ -126,7 +126,7 @@ async def get_job_log_list_controller(
return SuccessResponse(data=result_dict, msg="查询执行日志列表成功") return SuccessResponse(data=result_dict, msg="查询执行日志列表成功")
@JobRouter.get("/log/detail/{id}", summary="获取执行日志详情", response_model=ResponseSchema[JobOutSchema]) @CornJobRouter.get("/log/detail/{id}", summary="获取执行日志详情", response_model=ResponseSchema[JobOutSchema])
async def get_job_log_detail_controller( async def get_job_log_detail_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:job:detail"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:job:detail"]))],
id: Annotated[int, Path(description="日志ID")], id: Annotated[int, Path(description="日志ID")],
@@ -136,7 +136,7 @@ async def get_job_log_detail_controller(
return SuccessResponse(data=result_dict, msg="获取执行日志详情成功") return SuccessResponse(data=result_dict, msg="获取执行日志详情成功")
@JobRouter.delete("/log/delete", summary="删除执行日志", response_model=ResponseSchema[None]) @CornJobRouter.delete("/log/delete", summary="删除执行日志", response_model=ResponseSchema[None])
async def delete_job_log_controller( async def delete_job_log_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:job:delete"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:job:delete"]))],
ids: Annotated[list[int], Body(description="ID列表")], ids: Annotated[list[int], Body(description="ID列表")],
@@ -8,7 +8,7 @@ class JobModel(ModelMixin):
"""任务执行日志表 """任务执行日志表
""" """
__tablename__: str = "task_job" __tablename__: str = "task_corn_job"
__table_args__: dict[str, str] = {"comment": "任务执行日志表"} __table_args__: dict[str, str] = {"comment": "任务执行日志表"}
job_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="任务ID") job_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="任务ID")
@@ -12,10 +12,10 @@ from app.core.router_class import OperationLogRoute
from .schema import NodeCreateSchema, NodeOutSchema, NodeQueryParam, NodeRunResultSchema, NodeUpdateSchema from .schema import NodeCreateSchema, NodeOutSchema, NodeQueryParam, NodeRunResultSchema, NodeUpdateSchema
from .service import NodeService from .service import NodeService
NodeRouter = APIRouter(route_class=OperationLogRoute, prefix="/cronjob/node", tags=["定时任务节点管理"]) CornJobNodeRouter = APIRouter(route_class=OperationLogRoute, prefix="/cronjob/node", tags=["定时任务节点管理"])
@NodeRouter.get("/options", summary="获取定时任务节点列表", response_model=ResponseSchema[list[NodeOutSchema]]) @CornJobNodeRouter.get("/options", summary="获取定时任务节点列表", response_model=ResponseSchema[list[NodeOutSchema]])
async def get_node_options_controller( async def get_node_options_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
@@ -25,7 +25,7 @@ async def get_node_options_controller(
return SuccessResponse(data=result, msg="获取定时任务节点选项成功") return SuccessResponse(data=result, msg="获取定时任务节点选项成功")
@NodeRouter.get("/detail/{id}", summary="获取节点详情", response_model=ResponseSchema[NodeOutSchema]) @CornJobNodeRouter.get("/detail/{id}", summary="获取节点详情", response_model=ResponseSchema[NodeOutSchema])
async def get_obj_detail_controller( async def get_obj_detail_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:detail"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:detail"]))],
id: Annotated[int, Path(description="节点ID")], id: Annotated[int, Path(description="节点ID")],
@@ -36,7 +36,7 @@ async def get_obj_detail_controller(
return SuccessResponse(data=result_dict, msg="获取节点详情成功") return SuccessResponse(data=result_dict, msg="获取节点详情成功")
@NodeRouter.get("/list", summary="查询节点", response_model=ResponseSchema[PageResultSchema[NodeOutSchema]]) @CornJobNodeRouter.get("/list", summary="查询节点", response_model=ResponseSchema[PageResultSchema[NodeOutSchema]])
async def get_obj_list_controller( async def get_obj_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:query"]))],
page: Annotated[PaginationQueryParam, Depends()], page: Annotated[PaginationQueryParam, Depends()],
@@ -53,7 +53,7 @@ async def get_obj_list_controller(
return SuccessResponse(data=result_dict, msg="查询节点列表成功") return SuccessResponse(data=result_dict, msg="查询节点列表成功")
@NodeRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建节点", response_model=ResponseSchema[NodeOutSchema]) @CornJobNodeRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建节点", response_model=ResponseSchema[NodeOutSchema])
async def create_obj_controller( async def create_obj_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:create"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:create"]))],
data: Annotated[NodeCreateSchema, Body(description="创建节点参数")], data: Annotated[NodeCreateSchema, Body(description="创建节点参数")],
@@ -64,7 +64,7 @@ async def create_obj_controller(
return SuccessResponse(data=result_dict, msg="创建节点成功") return SuccessResponse(data=result_dict, msg="创建节点成功")
@NodeRouter.put("/update/{id}", summary="修改节点", response_model=ResponseSchema[NodeOutSchema]) @CornJobNodeRouter.put("/update/{id}", summary="修改节点", response_model=ResponseSchema[NodeOutSchema])
async def update_obj_controller( async def update_obj_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:update"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:update"]))],
id: Annotated[int, Path(description="节点ID")], id: Annotated[int, Path(description="节点ID")],
@@ -76,7 +76,7 @@ async def update_obj_controller(
return SuccessResponse(data=result_dict, msg="修改节点成功") return SuccessResponse(data=result_dict, msg="修改节点成功")
@NodeRouter.delete("/delete", summary="删除节点", response_model=ResponseSchema[None]) @CornJobNodeRouter.delete("/delete", summary="删除节点", response_model=ResponseSchema[None])
async def delete_obj_controller( async def delete_obj_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:delete"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:delete"]))],
ids: Annotated[list[int], Body(description="ID列表")], ids: Annotated[list[int], Body(description="ID列表")],
@@ -87,7 +87,7 @@ async def delete_obj_controller(
return SuccessResponse(msg="删除节点成功") return SuccessResponse(msg="删除节点成功")
@NodeRouter.delete("/clear", summary="清空节点", response_model=ResponseSchema[None]) @CornJobNodeRouter.delete("/clear", summary="清空节点", response_model=ResponseSchema[None])
async def clear_obj_controller( async def clear_obj_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:delete"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:delete"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
@@ -97,7 +97,7 @@ async def clear_obj_controller(
return SuccessResponse(msg="清空节点成功") return SuccessResponse(msg="清空节点成功")
@NodeRouter.post("/execute/{id}", summary="手动执行节点(立即执行一次)", response_model=ResponseSchema[NodeRunResultSchema]) @CornJobNodeRouter.post("/execute/{id}", summary="手动执行节点(立即执行一次)", response_model=ResponseSchema[NodeRunResultSchema])
async def execute_job_controller( async def execute_job_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:execute"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:execute"]))],
id: Annotated[int, Path(description="节点ID")], id: Annotated[int, Path(description="节点ID")],
@@ -108,7 +108,7 @@ async def execute_job_controller(
return SuccessResponse(data=result, msg="节点已触发执行") return SuccessResponse(data=result, msg="节点已触发执行")
@NodeRouter.patch("/status/batch", summary="批量设置节点状态", response_model=ResponseSchema[None]) @CornJobNodeRouter.patch("/status/batch", summary="批量设置节点状态", response_model=ResponseSchema[None])
async def batch_set_status_controller( async def batch_set_status_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:update"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:update"]))],
data: Annotated[BatchSetAvailable, Body(description="状态设置")], data: Annotated[BatchSetAvailable, Body(description="状态设置")],
@@ -8,7 +8,7 @@ class NodeModel(ModelMixin, UserMixin):
"""节点类型模型 - 动态定义节点类型 """节点类型模型 - 动态定义节点类型
""" """
__tablename__: str = "task_node" __tablename__: str = "task_cornjob_node"
__table_args__: dict[str, str] = {"comment": "节点类型表"} __table_args__: dict[str, str] = {"comment": "节点类型表"}
name: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="节点名称") name: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="节点名称")
@@ -0,0 +1,10 @@
"""存储传输模块:
- ``node``: 存储源管理,即“节点”(OSS/COS/OBS/S3/SFTP/FTP 等连接配置)
- ``core``: 对象存储协议适配器与工厂
- ``browse``: 存储文件浏览
- ``transfer``: 传输任务引擎
- ``workflow``: 传输流程编排(画布 CRUD、发布、执行 API)
路由统一挂在 ``/task/storage`` 下(由 ``app/api/v1/task.py`` 将各 controller 的 ``/storage/*`` 前缀聚合进 task 域)。
"""
@@ -9,12 +9,12 @@ from app.common.response import ResponseSchema, SuccessResponse, UploadFileRespo
from app.core.base_schema import AuthSchema from app.core.base_schema import AuthSchema
from app.core.dependencies import AuthPermission, db_getter from app.core.dependencies import AuthPermission, db_getter
from app.core.router_class import OperationLogRoute from app.core.router_class import OperationLogRoute
from app.modules.workflow.core.base import StorageObject, StoragePage from app.modules.task.storage.core.base import StorageObject, StoragePage
from .schema import StoragePathCreateSchema, StoragePathResultSchema, StorageUploadResultSchema from .schema import StoragePathCreateSchema, StoragePathResultSchema, StorageUploadResultSchema
from .service import StorageFileService from .service import StorageFileService
StorageFileRouter = APIRouter(route_class=OperationLogRoute, prefix="/storage", tags=["存储管理"]) StorageBrowseRouter = APIRouter(route_class=OperationLogRoute, prefix="/storage/browse", tags=["存储管理"])
def _delete_temp_file(path: str) -> None: def _delete_temp_file(path: str) -> None:
@@ -25,9 +25,9 @@ def _delete_temp_file(path: str) -> None:
pass pass
@StorageFileRouter.post("/upload", summary="上传文件到存储源", response_model=ResponseSchema[StorageUploadResultSchema]) @StorageBrowseRouter.post("/upload", summary="上传文件到存储源", response_model=ResponseSchema[StorageUploadResultSchema])
async def upload_storage_file_controller( async def upload_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:storage:upload"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:browse:upload"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
file: Annotated[UploadFile, File(description="上传文件")], file: Annotated[UploadFile, File(description="上传文件")],
source_id: Annotated[int | None, Form(description="存储源ID(不传使用默认存储源)")] = None, source_id: Annotated[int | None, Form(description="存储源ID(不传使用默认存储源)")] = None,
@@ -40,9 +40,9 @@ async def upload_storage_file_controller(
return SuccessResponse(data=result, msg="上传文件成功") return SuccessResponse(data=result, msg="上传文件成功")
@StorageFileRouter.post("/download", summary="下载存储源文件", response_model=None) @StorageBrowseRouter.post("/download", summary="下载存储源文件", response_model=None)
async def download_storage_file_controller( async def download_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:storage:download"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:browse:download"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
remote_path: Annotated[str, Body(description="远端文件路径")], remote_path: Annotated[str, Body(description="远端文件路径")],
@@ -56,9 +56,9 @@ async def download_storage_file_controller(
return UploadFileResponse(file_path=local_path, filename=file_name) return UploadFileResponse(file_path=local_path, filename=file_name)
@StorageFileRouter.post("/download_dir", summary="下载存储源目录(递归打包ZIP)", response_model=None) @StorageBrowseRouter.post("/download_dir", summary="下载存储源目录(递归打包ZIP)", response_model=None)
async def download_dir_storage_file_controller( async def download_dir_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:storage:download"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:browse:download"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
remote_path: Annotated[str, Body(description="远端目录路径")], remote_path: Annotated[str, Body(description="远端目录路径")],
@@ -72,9 +72,9 @@ async def download_dir_storage_file_controller(
return UploadFileResponse(file_path=local_path, filename=file_name) return UploadFileResponse(file_path=local_path, filename=file_name)
@StorageFileRouter.delete("/delete", summary="删除存储源文件", response_model=ResponseSchema[None]) @StorageBrowseRouter.delete("/delete", summary="删除存储源文件", response_model=ResponseSchema[None])
async def delete_storage_file_controller( async def delete_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:storage:delete"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:browse:delete"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
remote_path: Annotated[str, Body(description="远端文件路径")], remote_path: Annotated[str, Body(description="远端文件路径")],
source_id: Annotated[int | None, Body(description="存储源ID(不传使用默认存储源)")] = None, source_id: Annotated[int | None, Body(description="存储源ID(不传使用默认存储源)")] = None,
@@ -84,9 +84,9 @@ async def delete_storage_file_controller(
return SuccessResponse(msg="删除文件成功") return SuccessResponse(msg="删除文件成功")
@StorageFileRouter.get("/list", summary="查询存储源文件列表", response_model=None) @StorageBrowseRouter.get("/list", summary="查询存储源文件列表", response_model=None)
async def list_storage_file_controller( async def list_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:storage:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:browse:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
source_id: Annotated[int | None, Query(description="存储源ID(不传使用默认存储源)")] = None, source_id: Annotated[int | None, Query(description="存储源ID(不传使用默认存储源)")] = None,
prefix: Annotated[str | None, Query(description="目录前缀(可选)")] = None, prefix: Annotated[str | None, Query(description="目录前缀(可选)")] = None,
@@ -100,9 +100,9 @@ async def list_storage_file_controller(
return SuccessResponse(data=result, msg="查询文件列表成功") return SuccessResponse(data=result, msg="查询文件列表成功")
@StorageFileRouter.get("/buckets", summary="查询存储源桶列表", response_model=ResponseSchema[list[str]]) @StorageBrowseRouter.get("/buckets", summary="查询存储源桶列表", response_model=ResponseSchema[list[str]])
async def list_storage_buckets_controller( async def list_storage_buckets_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:storage:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:browse:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
source_id: Annotated[int | None, Query(description="存储源ID(不传使用默认存储源)")] = None, source_id: Annotated[int | None, Query(description="存储源ID(不传使用默认存储源)")] = None,
) -> JSONResponse: ) -> JSONResponse:
@@ -110,9 +110,9 @@ async def list_storage_buckets_controller(
return SuccessResponse(data=result, msg="查询桶列表成功") return SuccessResponse(data=result, msg="查询桶列表成功")
@StorageFileRouter.post("/copy", summary="复制/移动文件", response_model=ResponseSchema[StoragePathResultSchema]) @StorageBrowseRouter.post("/copy", summary="复制/移动文件", response_model=ResponseSchema[StoragePathResultSchema])
async def copy_or_move_storage_file_controller( async def copy_or_move_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:storage:update"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:browse:update"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
source_id: Annotated[int | None, Body(description="源存储源ID(不传使用默认存储源)")] = None, source_id: Annotated[int | None, Body(description="源存储源ID(不传使用默认存储源)")] = None,
source_path: Annotated[str, Body(description="源文件路径")] = "", source_path: Annotated[str, Body(description="源文件路径")] = "",
@@ -132,9 +132,9 @@ async def copy_or_move_storage_file_controller(
return SuccessResponse(data=result, msg="操作文件成功") return SuccessResponse(data=result, msg="操作文件成功")
@StorageFileRouter.put("/rename", summary="重命名/移动文件", response_model=ResponseSchema[StoragePathResultSchema]) @StorageBrowseRouter.put("/rename", summary="重命名/移动文件", response_model=ResponseSchema[StoragePathResultSchema])
async def rename_storage_file_controller( async def rename_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:storage:update"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:browse:update"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
source_id: Annotated[int | None, Body(description="存储源ID(不传使用默认存储源)")] = None, source_id: Annotated[int | None, Body(description="存储源ID(不传使用默认存储源)")] = None,
source_path: Annotated[str, Body(description="原路径")] = "", source_path: Annotated[str, Body(description="原路径")] = "",
@@ -147,9 +147,9 @@ async def rename_storage_file_controller(
return SuccessResponse(data=result, msg="重命名成功") return SuccessResponse(data=result, msg="重命名成功")
@StorageFileRouter.post("/mkdir", summary="新建目录", response_model=ResponseSchema[StoragePathCreateSchema]) @StorageBrowseRouter.post("/mkdir", summary="新建目录", response_model=ResponseSchema[StoragePathCreateSchema])
async def mkdir_storage_file_controller( async def mkdir_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:storage:update"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:browse:update"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
source_id: Annotated[int | None, Body(description="存储源ID(不传使用默认存储源)")] = None, source_id: Annotated[int | None, Body(description="存储源ID(不传使用默认存储源)")] = None,
remote_dir: Annotated[str, Body(description="目录路径")] = "", remote_dir: Annotated[str, Body(description="目录路径")] = "",
@@ -159,9 +159,9 @@ async def mkdir_storage_file_controller(
return SuccessResponse(data=result, msg="新建目录成功") return SuccessResponse(data=result, msg="新建目录成功")
@StorageFileRouter.post("/share", summary="生成分享链接", response_model=ResponseSchema[str | None]) @StorageBrowseRouter.post("/share", summary="生成分享链接", response_model=ResponseSchema[str | None])
async def share_storage_file_controller( async def share_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:storage:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:browse:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
remote_path: Annotated[str, Body(description="远端文件路径")] = "", remote_path: Annotated[str, Body(description="远端文件路径")] = "",
source_id: Annotated[int | None, Body(description="存储源ID(不传使用默认存储源)")] = None, source_id: Annotated[int | None, Body(description="存储源ID(不传使用默认存储源)")] = None,
@@ -11,15 +11,15 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_schema import AuthSchema from app.core.base_schema import AuthSchema
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
from app.modules.workflow.core.base import ( from app.modules.task.storage.core.base import (
_OBJECT_STORE_PROTOCOLS, _OBJECT_STORE_PROTOCOLS,
BaseStorageAdapter, BaseStorageAdapter,
StorageAdapterConfig, StorageAdapterConfig,
StorageObject, StorageObject,
StoragePage, StoragePage,
) )
from app.modules.workflow.core.factory import StorageAdapterFactory from app.modules.task.storage.core.factory import StorageAdapterFactory
from app.modules.workflow.source.service import StorageSourceService from app.modules.task.storage.node.service import StorageNodeService
from app.utils.upload_util import UploadUtil from app.utils.upload_util import UploadUtil
from .schema import StoragePathCreateSchema, StoragePathResultSchema, StorageUploadResultSchema from .schema import StoragePathCreateSchema, StoragePathResultSchema, StorageUploadResultSchema
@@ -46,8 +46,8 @@ class StorageFileService:
async def _get_source(self, source_id: int | None) -> StorageAdapterConfig: async def _get_source(self, source_id: int | None) -> StorageAdapterConfig:
"""获取存储源并构造适配器配置(密码已解密,含 SDK 高级配置)。""" """获取存储源并构造适配器配置(密码已解密,含 SDK 高级配置)。"""
source = await StorageSourceService(self.auth, self.db).get_active_source(source_id) source = await StorageNodeService(self.auth, self.db).get_active_source(source_id)
return StorageSourceService._build_config(source) return StorageNodeService._build_config(source)
async def _get_adapter(self, source_id: int | None, bucket: str | None = None) -> BaseStorageAdapter: async def _get_adapter(self, source_id: int | None, bucket: str | None = None) -> BaseStorageAdapter:
"""构造适配器并切换当前操作桶(对象存储多桶浏览:桶随请求传入,适配器按请求创建)。""" """构造适配器并切换当前操作桶(对象存储多桶浏览:桶随请求传入,适配器按请求创建)。"""
@@ -98,7 +98,7 @@ _STREAM_THRESHOLD = 100 * 1024 * 1024 * 1024
class StorageAdapterConfig(BaseModel): class StorageAdapterConfig(BaseModel):
"""存储适配器配置(从 StorageSourceModel 剥离加密字段后注入,解耦 ORM 与协议层)""" """存储适配器配置(从 StorageNodeModel 剥离加密字段后注入,解耦 ORM 与协议层)"""
protocol: StorageProtocol = Field(description="存储协议") protocol: StorageProtocol = Field(description="存储协议")
host: str | None = Field(default=None, description="主机地址(对象存储可不填)") host: str | None = Field(default=None, description="主机地址(对象存储可不填)")
@@ -4,7 +4,7 @@ from qcloud_cos import CosConfig, CosS3Client
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
from app.core.logger import logger from app.core.logger import logger
from app.modules.workflow.core.base import ( from app.modules.task.storage.core.base import (
BaseStorageAdapter, BaseStorageAdapter,
CosAdvancedConfig, CosAdvancedConfig,
StorageObject, StorageObject,
@@ -1,12 +1,12 @@
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
from app.modules.workflow.core.base import BaseStorageAdapter, StorageAdapterConfig, StorageProtocol from app.modules.task.storage.core.base import BaseStorageAdapter, StorageAdapterConfig, StorageProtocol
from app.modules.workflow.core.cos_adapter import CosStorageAdapter from app.modules.task.storage.core.cos_adapter import CosStorageAdapter
from app.modules.workflow.core.ftp_ftps_adapter import FtpStorageAdapter from app.modules.task.storage.core.ftp_ftps_adapter import FtpStorageAdapter
from app.modules.workflow.core.local_adapter import LocalStorageAdapter from app.modules.task.storage.core.local_adapter import LocalStorageAdapter
from app.modules.workflow.core.obs_adapter import ObsStorageAdapter from app.modules.task.storage.core.obs_adapter import ObsStorageAdapter
from app.modules.workflow.core.oss_adapter import OssStorageAdapter from app.modules.task.storage.core.oss_adapter import OssStorageAdapter
from app.modules.workflow.core.s3_adapter import S3StorageAdapter from app.modules.task.storage.core.s3_adapter import S3StorageAdapter
from app.modules.workflow.core.sftp_adapter import SftpStorageAdapter from app.modules.task.storage.core.sftp_adapter import SftpStorageAdapter
# 协议 → 适配器类映射(FTPS 复用 FTP 适配器,由配置区分显式/隐式 TLS) # 协议 → 适配器类映射(FTPS 复用 FTP 适配器,由配置区分显式/隐式 TLS)
_STORAGE_ADAPTERS: dict[str, type[BaseStorageAdapter]] = { _STORAGE_ADAPTERS: dict[str, type[BaseStorageAdapter]] = {
@@ -7,7 +7,7 @@ from datetime import datetime
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
from app.core.logger import logger from app.core.logger import logger
from app.modules.workflow.core.base import BaseStorageAdapter, FtpAdvancedConfig, StorageObject, StorageProtocol from app.modules.task.storage.core.base import BaseStorageAdapter, FtpAdvancedConfig, StorageObject, StorageProtocol
class _ImplicitFTP_TLS(ftplib.FTP_TLS): class _ImplicitFTP_TLS(ftplib.FTP_TLS):
@@ -4,7 +4,7 @@ from datetime import datetime
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
from app.core.logger import logger from app.core.logger import logger
from app.modules.workflow.core.base import BaseStorageAdapter, StorageObject, StorageProtocol from app.modules.task.storage.core.base import BaseStorageAdapter, StorageObject, StorageProtocol
class LocalStorageAdapter(BaseStorageAdapter): class LocalStorageAdapter(BaseStorageAdapter):
@@ -5,7 +5,7 @@ from obs import ObsClient
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
from app.core.logger import logger from app.core.logger import logger
from app.modules.workflow.core.base import ( from app.modules.task.storage.core.base import (
BaseStorageAdapter, BaseStorageAdapter,
ObsAdvancedConfig, ObsAdvancedConfig,
StorageObject, StorageObject,
@@ -4,7 +4,7 @@ import alibabacloud_oss_v2 as oss
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
from app.core.logger import logger from app.core.logger import logger
from app.modules.workflow.core.base import ( from app.modules.task.storage.core.base import (
BaseStorageAdapter, BaseStorageAdapter,
OssAdvancedConfig, OssAdvancedConfig,
StorageObject, StorageObject,
@@ -7,7 +7,7 @@ from botocore.exceptions import ClientError
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
from app.core.logger import logger from app.core.logger import logger
from app.modules.workflow.core.base import ( from app.modules.task.storage.core.base import (
BaseStorageAdapter, BaseStorageAdapter,
S3AdvancedConfig, S3AdvancedConfig,
StorageObject, StorageObject,
@@ -13,7 +13,7 @@ import paramiko
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
from app.core.logger import logger from app.core.logger import logger
from app.modules.workflow.core.base import BaseStorageAdapter, SftpAdvancedConfig, StorageObject, StorageProtocol from app.modules.task.storage.core.base import BaseStorageAdapter, SftpAdvancedConfig, StorageObject, StorageProtocol
class SftpStorageAdapter(BaseStorageAdapter): class SftpStorageAdapter(BaseStorageAdapter):
@@ -8,7 +8,7 @@ from app.common.response import ResponseSchema, SuccessResponse
from app.core.base_schema import AuthSchema, PageResultSchema, PaginationQueryParam from app.core.base_schema import AuthSchema, PageResultSchema, PaginationQueryParam
from app.core.dependencies import AuthPermission, db_getter from app.core.dependencies import AuthPermission, db_getter
from app.core.router_class import OperationLogRoute from app.core.router_class import OperationLogRoute
from app.modules.workflow.core.base import ( from app.modules.task.storage.core.base import (
ADVANCED_FIELD_DEFS, ADVANCED_FIELD_DEFS,
DEFAULT_PORTS, DEFAULT_PORTS,
AdvancedFieldDefSchema, AdvancedFieldDefSchema,
@@ -16,15 +16,15 @@ from app.modules.workflow.core.base import (
StorageProtocolDefSchema, StorageProtocolDefSchema,
) )
from .schema import StorageSourceCreateSchema, StorageSourceOutSchema, StorageSourceQueryParam, StorageSourceTestSchema, StorageSourceUpdateSchema from .schema import StorageNodeCreateSchema, StorageNodeOutSchema, StorageNodeQueryParam, StorageNodeTestSchema, StorageNodeUpdateSchema
from .service import StorageSourceService from .service import StorageNodeService
StorageSourceRouter = APIRouter(route_class=OperationLogRoute, prefix="/source", tags=["存储源管理"]) StorageNodeRouter = APIRouter(route_class=OperationLogRoute, prefix="/storage/node", tags=["节点管理"])
@StorageSourceRouter.get("/protocols", summary="查询支持的存储协议", response_model=ResponseSchema[list[StorageProtocolDefSchema]]) @StorageNodeRouter.get("/protocols", summary="查询支持的存储协议", response_model=ResponseSchema[list[StorageProtocolDefSchema]])
async def get_storage_protocols_controller( async def get_storage_protocols_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:node:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:node:query"]))],
) -> JSONResponse: ) -> JSONResponse:
result: list[StorageProtocolDefSchema] = [ result: list[StorageProtocolDefSchema] = [
StorageProtocolDefSchema(protocol=p.value, name=p.name, default_port=DEFAULT_PORTS[p]) StorageProtocolDefSchema(protocol=p.value, name=p.name, default_port=DEFAULT_PORTS[p])
@@ -33,22 +33,22 @@ async def get_storage_protocols_controller(
return SuccessResponse(data=result, msg="查询存储协议成功") return SuccessResponse(data=result, msg="查询存储协议成功")
@StorageSourceRouter.get("/advanced-fields", summary="查询存储 SDK 高级配置字段定义", response_model=ResponseSchema[dict[str, list[AdvancedFieldDefSchema]]]) @StorageNodeRouter.get("/advanced-fields", summary="查询存储 SDK 高级配置字段定义", response_model=ResponseSchema[dict[str, list[AdvancedFieldDefSchema]]])
async def get_advanced_fields_controller( async def get_advanced_fields_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:node:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:node:query"]))],
) -> JSONResponse: ) -> JSONResponse:
"""返回按协议分组的 SDK 高级配置字段元数据,供前端「高级设置」面板按协议动态渲染。""" """返回按协议分组的 SDK 高级配置字段元数据,供前端「高级设置」面板按协议动态渲染。"""
return SuccessResponse(data=ADVANCED_FIELD_DEFS, msg="查询高级配置字段成功") return SuccessResponse(data=ADVANCED_FIELD_DEFS, msg="查询高级配置字段成功")
@StorageSourceRouter.get("/page", summary="分页查询存储源", response_model=ResponseSchema[PageResultSchema[StorageSourceOutSchema]]) @StorageNodeRouter.get("/page", summary="分页查询存储源", response_model=ResponseSchema[PageResultSchema[StorageNodeOutSchema]])
async def get_storage_source_page_controller( async def get_storage_source_page_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:node:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:node:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Depends()], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[StorageSourceQueryParam, Query()], search: Annotated[StorageNodeQueryParam, Query()],
) -> JSONResponse: ) -> JSONResponse:
result: PageResultSchema[StorageSourceOutSchema] = await StorageSourceService(auth, db).page( result: PageResultSchema[StorageNodeOutSchema] = await StorageNodeService(auth, db).page(
search=search, search=search,
page_no=page.page_no, page_no=page.page_no,
page_size=page.page_size, page_size=page.page_size,
@@ -57,72 +57,72 @@ async def get_storage_source_page_controller(
return SuccessResponse(data=result, msg="查询存储源分页成功") return SuccessResponse(data=result, msg="查询存储源分页成功")
@StorageSourceRouter.get("/list", summary="查询存储源列表", response_model=ResponseSchema[list[StorageSourceOutSchema]]) @StorageNodeRouter.get("/list", summary="查询存储源列表", response_model=ResponseSchema[list[StorageNodeOutSchema]])
async def get_storage_source_list_controller( async def get_storage_source_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:node:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:node:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
search: Annotated[StorageSourceQueryParam, Query()], search: Annotated[StorageNodeQueryParam, Query()],
) -> JSONResponse: ) -> JSONResponse:
result: list[StorageSourceOutSchema] = await StorageSourceService(auth, db).get_list(search=search) result: list[StorageNodeOutSchema] = await StorageNodeService(auth, db).get_list(search=search)
return SuccessResponse(data=result, msg="查询存储源列表成功") return SuccessResponse(data=result, msg="查询存储源列表成功")
@StorageSourceRouter.get("/detail/{id}", summary="查询存储源详情", response_model=ResponseSchema[StorageSourceOutSchema]) @StorageNodeRouter.get("/detail/{id}", summary="查询存储源详情", response_model=ResponseSchema[StorageNodeOutSchema])
async def get_storage_source_detail_controller( async def get_storage_source_detail_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:node:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:node:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="存储源ID", ge=1)], id: Annotated[int, Path(description="存储源ID", ge=1)],
) -> JSONResponse: ) -> JSONResponse:
result: StorageSourceOutSchema = await StorageSourceService(auth, db).detail(id=id) result: StorageNodeOutSchema = await StorageNodeService(auth, db).detail(id=id)
return SuccessResponse(data=result, msg="查询存储源详情成功") return SuccessResponse(data=result, msg="查询存储源详情成功")
@StorageSourceRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建存储源", response_model=ResponseSchema[StorageSourceOutSchema]) @StorageNodeRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建存储源", response_model=ResponseSchema[StorageNodeOutSchema])
async def create_storage_source_controller( async def create_storage_source_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:node:create"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:node:create"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
data: Annotated[StorageSourceCreateSchema, Body(description="存储源创建参数")], data: Annotated[StorageNodeCreateSchema, Body(description="存储源创建参数")],
) -> JSONResponse: ) -> JSONResponse:
result: StorageSourceOutSchema = await StorageSourceService(auth, db).create(data=data) result: StorageNodeOutSchema = await StorageNodeService(auth, db).create(data=data)
return SuccessResponse(data=result, msg="创建存储源成功") return SuccessResponse(data=result, msg="创建存储源成功")
@StorageSourceRouter.put("/update/{id}", summary="修改存储源", response_model=ResponseSchema[StorageSourceOutSchema]) @StorageNodeRouter.put("/update/{id}", summary="修改存储源", response_model=ResponseSchema[StorageNodeOutSchema])
async def update_storage_source_controller( async def update_storage_source_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:node:update"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:node:update"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="存储源ID", ge=1)], id: Annotated[int, Path(description="存储源ID", ge=1)],
data: Annotated[StorageSourceUpdateSchema, Body(description="存储源修改参数")], data: Annotated[StorageNodeUpdateSchema, Body(description="存储源修改参数")],
) -> JSONResponse: ) -> JSONResponse:
result: StorageSourceOutSchema = await StorageSourceService(auth, db).update(id=id, data=data) result: StorageNodeOutSchema = await StorageNodeService(auth, db).update(id=id, data=data)
return SuccessResponse(data=result, msg="修改存储源成功") return SuccessResponse(data=result, msg="修改存储源成功")
@StorageSourceRouter.delete("/delete", summary="删除存储源", response_model=ResponseSchema[None]) @StorageNodeRouter.delete("/delete", summary="删除存储源", response_model=ResponseSchema[None])
async def delete_storage_source_controller( async def delete_storage_source_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:node:delete"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:node:delete"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
ids: Annotated[list[int], Body(description="存储源ID列表")], ids: Annotated[list[int], Body(description="存储源ID列表")],
) -> JSONResponse: ) -> JSONResponse:
await StorageSourceService(auth, db).delete(ids=ids) await StorageNodeService(auth, db).delete(ids=ids)
return SuccessResponse(msg="删除存储源成功") return SuccessResponse(msg="删除存储源成功")
@StorageSourceRouter.post("/test/{id}", summary="测试存储源连接", response_model=ResponseSchema[bool]) @StorageNodeRouter.post("/test/{id}", summary="测试存储源连接", response_model=ResponseSchema[bool])
async def test_storage_source_controller( async def test_storage_source_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:node:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:node:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="存储源ID", ge=1)], id: Annotated[int, Path(description="存储源ID", ge=1)],
) -> JSONResponse: ) -> JSONResponse:
result: bool = await StorageSourceService(auth, db).test_connection(id=id) result: bool = await StorageNodeService(auth, db).test_connection(id=id)
return SuccessResponse(data=result, msg="连接成功") return SuccessResponse(data=result, msg="连接成功")
@StorageSourceRouter.post("/test", summary="测试存储源连接(配置)", response_model=ResponseSchema[bool]) @StorageNodeRouter.post("/test", summary="测试存储源连接(配置)", response_model=ResponseSchema[bool])
async def test_storage_source_config_controller( async def test_storage_source_config_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:node:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:node:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
data: Annotated[StorageSourceTestSchema, Body(description="存储源连接配置")], data: Annotated[StorageNodeTestSchema, Body(description="存储源连接配置")],
) -> JSONResponse: ) -> JSONResponse:
result: bool = await StorageSourceService(auth, db).test_config(data=data) result: bool = await StorageNodeService(auth, db).test_config(data=data)
return SuccessResponse(data=result, msg="连接成功") return SuccessResponse(data=result, msg="连接成功")
@@ -0,0 +1,14 @@
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_crud import CRUDBase
from app.core.base_schema import AuthSchema
from .model import StorageNodeModel
from .schema import StorageNodeCreateSchema, StorageNodeUpdateSchema
class StorageNodeCRUD(CRUDBase[StorageNodeModel, StorageNodeCreateSchema, StorageNodeUpdateSchema]):
"""存储源模块数据层"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(model=StorageNodeModel, auth=auth, db=db)
@@ -4,10 +4,10 @@ from sqlalchemy.orm import Mapped, mapped_column
from app.core.base_model import ModelMixin, UserMixin from app.core.base_model import ModelMixin, UserMixin
class StorageSourceModel(ModelMixin, UserMixin): class StorageNodeModel(ModelMixin, UserMixin):
"""存储源配置模型""" """存储源配置模型"""
__tablename__: str = "task_workflow_storage_source" __tablename__: str = "task_storage_node"
__table_args__: dict[str, str] = {"comment": "存储源配置表"} __table_args__: dict[str, str] = {"comment": "存储源配置表"}
name: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True, comment="存储源名称") name: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True, comment="存储源名称")
@@ -3,10 +3,10 @@ import codecs
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from app.core.base_schema import BaseQueryParam, BaseSchema, UserByQueryParam, UserBySchema from app.core.base_schema import BaseQueryParam, BaseSchema, UserByQueryParam, UserBySchema
from app.modules.workflow.core.base import DEFAULT_PORTS, StorageProtocol from app.modules.task.storage.core.base import DEFAULT_PORTS, StorageProtocol
class StorageSourceConfigSchema(BaseModel): class StorageNodeConfigSchema(BaseModel):
"""存储源连接配置模型(创建/测试共用)""" """存储源连接配置模型(创建/测试共用)"""
protocol: StorageProtocol = Field(..., description="协议(ftp/ftps/sftp/s3/obs/oss/cos/local)") protocol: StorageProtocol = Field(..., description="协议(ftp/ftps/sftp/s3/obs/oss/cos/local)")
@@ -108,7 +108,7 @@ class StorageSourceConfigSchema(BaseModel):
return self return self
class StorageSourceCreateSchema(StorageSourceConfigSchema): class StorageNodeCreateSchema(StorageNodeConfigSchema):
"""存储源创建模型""" """存储源创建模型"""
name: str = Field(..., min_length=1, max_length=64, description="存储源名称") name: str = Field(..., min_length=1, max_length=64, description="存储源名称")
@@ -125,17 +125,17 @@ class StorageSourceCreateSchema(StorageSourceConfigSchema):
return value return value
class StorageSourceUpdateSchema(StorageSourceCreateSchema): class StorageNodeUpdateSchema(StorageNodeCreateSchema):
"""存储源更新模型(password 为空表示不修改原密码)""" """存储源更新模型(password 为空表示不修改原密码)"""
class StorageSourceTestSchema(StorageSourceConfigSchema): class StorageNodeTestSchema(StorageNodeConfigSchema):
"""存储源连接测试模型(仅校验连接配置,不落库;密码留空且传 source_id 时回退已保存密码)""" """存储源连接测试模型(仅校验连接配置,不落库;密码留空且传 source_id 时回退已保存密码)"""
source_id: int | None = Field(default=None, ge=1, description="已保存的存储源ID(编辑态测试时使用)") node_id: int | None = Field(default=None, ge=1, description="已保存的存储源ID(编辑态测试时使用)")
class StorageSourceOutSchema(StorageSourceCreateSchema, BaseSchema, UserBySchema): class StorageNodeOutSchema(StorageNodeCreateSchema, BaseSchema, UserBySchema):
"""存储源详情响应模型(密码永不明文返回)""" """存储源详情响应模型(密码永不明文返回)"""
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
@@ -149,7 +149,7 @@ class StorageSourceOutSchema(StorageSourceCreateSchema, BaseSchema, UserBySchema
return None return None
class StorageSourceQueryParam(BaseQueryParam, UserByQueryParam): class StorageNodeQueryParam(BaseQueryParam, UserByQueryParam):
"""存储源管理查询参数""" """存储源管理查询参数"""
name: str | None = Field(None, description="存储源名称", json_schema_extra={"q": "like"}) name: str | None = Field(None, description="存储源名称", json_schema_extra={"q": "like"})
@@ -5,22 +5,22 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_schema import AuthSchema, PageResultSchema from app.core.base_schema import AuthSchema, PageResultSchema
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
from app.modules.workflow.core.base import ( from app.modules.task.storage.core.base import (
DEFAULT_PORTS, DEFAULT_PORTS,
StorageAdapterConfig, StorageAdapterConfig,
StorageProtocol, StorageProtocol,
decrypt_password, decrypt_password,
encrypt_password, encrypt_password,
) )
from app.modules.workflow.core.factory import StorageAdapterFactory from app.modules.task.storage.core.factory import StorageAdapterFactory
from app.utils.common_util import search_to_dict from app.utils.common_util import search_to_dict
from .crud import StorageSourceCRUD from .crud import StorageNodeCRUD
from .model import StorageSourceModel from .model import StorageNodeModel
from .schema import StorageSourceCreateSchema, StorageSourceOutSchema, StorageSourceQueryParam, StorageSourceTestSchema, StorageSourceUpdateSchema from .schema import StorageNodeCreateSchema, StorageNodeOutSchema, StorageNodeQueryParam, StorageNodeTestSchema, StorageNodeUpdateSchema
class StorageSourceService: class StorageNodeService:
"""存储源管理服务""" """存储源管理服务"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
@@ -29,21 +29,21 @@ class StorageSourceService:
# ── 内部工具 ──────────────────────────────────────────────────── # ── 内部工具 ────────────────────────────────────────────────────
def _crud(self) -> StorageSourceCRUD: def _crud(self) -> StorageNodeCRUD:
return StorageSourceCRUD(self.auth, self.db) return StorageNodeCRUD(self.auth, self.db)
@staticmethod @staticmethod
def _to_out(obj: StorageSourceModel) -> StorageSourceOutSchema: def _to_out(obj: StorageNodeModel) -> StorageNodeOutSchema:
out = StorageSourceOutSchema.model_validate(obj) out = StorageNodeOutSchema.model_validate(obj)
out.has_password = bool(obj.password) out.has_password = bool(obj.password)
return out return out
async def _clear_other_default(self, keep_id: int | None = None) -> None: async def _clear_other_default(self, keep_id: int | None = None) -> None:
"""取消其他存储源的默认标记,保证同时只有一个默认源。""" """取消其他存储源的默认标记,保证同时只有一个默认源。"""
conditions: list[ColumnElement[bool]] = [StorageSourceModel.is_default.is_(True)] conditions: list[ColumnElement[bool]] = [StorageNodeModel.is_default.is_(True)]
if keep_id is not None: if keep_id is not None:
conditions.append(StorageSourceModel.id != keep_id) conditions.append(StorageNodeModel.id != keep_id)
await self.db.execute(update(StorageSourceModel).where(*conditions).values(is_default=False)) await self.db.execute(update(StorageNodeModel).where(*conditions).values(is_default=False))
@staticmethod @staticmethod
def _build_config_from(source: Any, password: str) -> StorageAdapterConfig: def _build_config_from(source: Any, password: str) -> StorageAdapterConfig:
@@ -71,29 +71,29 @@ class StorageSourceService:
) )
@staticmethod @staticmethod
def _build_config(obj: StorageSourceModel) -> StorageAdapterConfig: def _build_config(obj: StorageNodeModel) -> StorageAdapterConfig:
return StorageSourceService._build_config_from(obj, decrypt_password(obj.password)) return StorageNodeService._build_config_from(obj, decrypt_password(obj.password))
# ── 查询 ──────────────────────────────────────────────────────── # ── 查询 ────────────────────────────────────────────────────────
async def detail(self, id: int) -> StorageSourceOutSchema: async def detail(self, id: int) -> StorageNodeOutSchema:
obj = await self._crud().get_or_404(id=id) obj = await self._crud().get_or_404(id=id)
return self._to_out(obj) return self._to_out(obj)
async def page( async def page(
self, self,
search: StorageSourceQueryParam | None, search: StorageNodeQueryParam | None,
page_no: int, page_no: int,
page_size: int, page_size: int,
order_by: list[dict] | None = None, order_by: list[dict] | None = None,
) -> PageResultSchema[StorageSourceOutSchema]: ) -> PageResultSchema[StorageNodeOutSchema]:
result = await self._crud().page( result = await self._crud().page(
offset=(page_no - 1) * page_size, offset=(page_no - 1) * page_size,
limit=page_size, limit=page_size,
order_by=order_by or [{"id": "asc"}], order_by=order_by or [{"id": "asc"}],
search=search_to_dict(search), search=search_to_dict(search),
) )
return PageResultSchema[StorageSourceOutSchema]( return PageResultSchema[StorageNodeOutSchema](
page_no=result.page_no, page_no=result.page_no,
page_size=result.page_size, page_size=result.page_size,
total=result.total, total=result.total,
@@ -101,13 +101,13 @@ class StorageSourceService:
items=[self._to_out(obj) for obj in result.items], items=[self._to_out(obj) for obj in result.items],
) )
async def get_list(self, search: StorageSourceQueryParam | None = None) -> list[StorageSourceOutSchema]: async def get_list(self, search: StorageNodeQueryParam | None = None) -> list[StorageNodeOutSchema]:
objs = await self._crud().get_list(search=search_to_dict(search), order_by=[{"id": "asc"}]) objs = await self._crud().get_list(search=search_to_dict(search), order_by=[{"id": "asc"}])
return [self._to_out(obj) for obj in objs] return [self._to_out(obj) for obj in objs]
# ── 写入 ──────────────────────────────────────────────────────── # ── 写入 ────────────────────────────────────────────────────────
async def create(self, data: StorageSourceCreateSchema) -> StorageSourceOutSchema: async def create(self, data: StorageNodeCreateSchema) -> StorageNodeOutSchema:
exist = await self._crud().get(name=data.name) exist = await self._crud().get(name=data.name)
if exist: if exist:
raise CustomException(msg="创建失败,存储源名称已存在") raise CustomException(msg="创建失败,存储源名称已存在")
@@ -121,7 +121,7 @@ class StorageSourceService:
await self._clear_other_default(keep_id=obj.id) await self._clear_other_default(keep_id=obj.id)
return self._to_out(obj) return self._to_out(obj)
async def update(self, id: int, data: StorageSourceUpdateSchema) -> StorageSourceOutSchema: async def update(self, id: int, data: StorageNodeUpdateSchema) -> StorageNodeOutSchema:
await self._crud().get_or_404(id=id, msg="更新失败,该存储源不存在") await self._crud().get_or_404(id=id, msg="更新失败,该存储源不存在")
exist = await self._crud().get(name=data.name) exist = await self._crud().get(name=data.name)
if exist and exist.id != id: if exist and exist.id != id:
@@ -158,11 +158,11 @@ class StorageSourceService:
raise CustomException(msg="连接失败,请检查存储源配置") raise CustomException(msg="连接失败,请检查存储源配置")
return True return True
async def test_config(self, data: StorageSourceTestSchema) -> bool: async def test_config(self, data: StorageNodeTestSchema) -> bool:
"""使用表单提交的配置直接测试连接(不落库),密码留空且传 source_id 时回退已保存密码。""" """使用表单提交的配置直接测试连接(不落库),密码留空且传 source_id 时回退已保存密码。"""
password = data.password or "" password = data.password or ""
if not password and data.source_id: if not password and data.node_id:
obj = await self._crud().get_or_404(id=data.source_id, msg="该存储源不存在") obj = await self._crud().get_or_404(id=data.node_id, msg="该存储源不存在")
password = decrypt_password(obj.password) password = decrypt_password(obj.password)
adapter = StorageAdapterFactory.create(self._build_config_from(data, password)) adapter = StorageAdapterFactory.create(self._build_config_from(data, password))
try: try:
@@ -175,7 +175,7 @@ class StorageSourceService:
# ── 供文件模块复用 ────────────────────────────────────────────── # ── 供文件模块复用 ──────────────────────────────────────────────
async def get_active_source(self, source_id: int | None = None) -> StorageSourceModel: async def get_active_source(self, source_id: int | None = None) -> StorageNodeModel:
"""获取可用存储源:优先指定 id;否则默认源;再退化为任一启用源。""" """获取可用存储源:优先指定 id;否则默认源;再退化为任一启用源。"""
if source_id: if source_id:
source = await self._crud().get_or_404(id=source_id) source = await self._crud().get_or_404(id=source_id)
@@ -190,7 +190,7 @@ class StorageSourceService:
return source return source
raise CustomException(msg="未配置可用的存储源,请先在存储源管理中创建") raise CustomException(msg="未配置可用的存储源,请先在存储源管理中创建")
async def get_active_sources(self, ids: list[int]) -> list[StorageSourceModel]: async def get_active_sources(self, ids: list[int]) -> list[StorageNodeModel]:
"""批量获取可用存储源:全部存在且启用,任一无效即抛错(一次查询,避免 N+1)。""" """批量获取可用存储源:全部存在且启用,任一无效即抛错(一次查询,避免 N+1)。"""
if not ids: if not ids:
return [] return []
@@ -0,0 +1,8 @@
# 见 docs/PLUGIN_ARCHITECTURE.md
name = "storage"
title = "存储传输"
version = "1.0.0"
description = "存储源管理、对象存储适配(OSS/COS/OBS/S3/SFTP/FTP)、文件浏览、传输任务与传输流程编排。"
optional = true
tags = ["storage", "transfer", "workflow"]
@@ -15,22 +15,22 @@ from app.core.exceptions import CustomException
from app.core.logger import logger from app.core.logger import logger
from app.core.router_class import OperationLogRoute from app.core.router_class import OperationLogRoute
from app.core.sse_manager import SSE_QUEUE_MAX_SIZE from app.core.sse_manager import SSE_QUEUE_MAX_SIZE
from app.modules.workflow.transfer.schema import ( from app.modules.task.storage.transfer.schema import (
TransferTaskCreateResultSchema, TransferTaskCreateResultSchema,
TransferTaskCreateSchema, TransferTaskCreateSchema,
TransferTaskOutSchema, TransferTaskOutSchema,
TransferTaskQueryParam, TransferTaskQueryParam,
TransferTaskType, TransferTaskType,
) )
from app.modules.workflow.transfer.service import StorageTransferService from app.modules.task.storage.transfer.service import StorageTransferService
from app.modules.workflow.transfer.sse_manager import transfer_stream_manager from app.modules.task.storage.transfer.sse_manager import transfer_stream_manager
StorageTransferRouter = APIRouter(route_class=OperationLogRoute, prefix="/transfer", tags=["文件传输"]) StorageTransferRouter = APIRouter(route_class=OperationLogRoute, prefix="/storage/transfer", tags=["文件传输"])
@StorageTransferRouter.post("/task", summary="创建传输任务(远端源)", response_model=ResponseSchema[TransferTaskCreateResultSchema]) @StorageTransferRouter.post("/task", summary="创建传输任务(远端源)", response_model=ResponseSchema[TransferTaskCreateResultSchema])
async def create_transfer_task_controller( async def create_transfer_task_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:transfer:create"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:transfer:create"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
data: Annotated[TransferTaskCreateSchema, Body(description="任务参数(远端源)")], data: Annotated[TransferTaskCreateSchema, Body(description="任务参数(远端源)")],
) -> JSONResponse: ) -> JSONResponse:
@@ -40,7 +40,7 @@ async def create_transfer_task_controller(
@StorageTransferRouter.post("/task/upload", summary="创建传输任务(本地上传源)", response_model=ResponseSchema[TransferTaskCreateResultSchema]) @StorageTransferRouter.post("/task/upload", summary="创建传输任务(本地上传源)", response_model=ResponseSchema[TransferTaskCreateResultSchema])
async def create_local_transfer_task_controller( async def create_local_transfer_task_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:transfer:create"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:transfer:create"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
file: Annotated[UploadFile, File(description="本地源文件")], file: Annotated[UploadFile, File(description="本地源文件")],
name: Annotated[str, Form(description="任务名称")], name: Annotated[str, Form(description="任务名称")],
@@ -58,7 +58,7 @@ async def create_local_transfer_task_controller(
@StorageTransferRouter.get("/task/page", summary="分页查询传输任务", response_model=ResponseSchema[PageResultSchema[TransferTaskOutSchema]]) @StorageTransferRouter.get("/task/page", summary="分页查询传输任务", response_model=ResponseSchema[PageResultSchema[TransferTaskOutSchema]])
async def get_transfer_task_page_controller( async def get_transfer_task_page_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:transfer:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:transfer:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Depends()], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[TransferTaskQueryParam, Query()], search: Annotated[TransferTaskQueryParam, Query()],
@@ -74,7 +74,7 @@ async def get_transfer_task_page_controller(
@StorageTransferRouter.get("/task/{id}", summary="查询传输任务详情", response_model=ResponseSchema[TransferTaskOutSchema]) @StorageTransferRouter.get("/task/{id}", summary="查询传输任务详情", response_model=ResponseSchema[TransferTaskOutSchema])
async def get_transfer_task_detail_controller( async def get_transfer_task_detail_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:transfer:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:transfer:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="任务ID", ge=1)], id: Annotated[int, Path(description="任务ID", ge=1)],
) -> JSONResponse: ) -> JSONResponse:
@@ -84,7 +84,7 @@ async def get_transfer_task_detail_controller(
@StorageTransferRouter.post("/task/{id}/cancel", summary="取消传输任务", response_model=ResponseSchema[None]) @StorageTransferRouter.post("/task/{id}/cancel", summary="取消传输任务", response_model=ResponseSchema[None])
async def cancel_transfer_task_controller( async def cancel_transfer_task_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:transfer:update"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:transfer:update"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="任务ID", ge=1)], id: Annotated[int, Path(description="任务ID", ge=1)],
) -> JSONResponse: ) -> JSONResponse:
@@ -94,7 +94,7 @@ async def cancel_transfer_task_controller(
@StorageTransferRouter.delete("/task", summary="删除传输任务", response_model=ResponseSchema[None]) @StorageTransferRouter.delete("/task", summary="删除传输任务", response_model=ResponseSchema[None])
async def delete_transfer_task_controller( async def delete_transfer_task_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:transfer:delete"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:transfer:delete"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
ids: Annotated[list[int], Body(description="任务ID列表")], ids: Annotated[list[int], Body(description="任务ID列表")],
) -> JSONResponse: ) -> JSONResponse:
@@ -19,12 +19,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import async_db_session from app.core.database import async_db_session
from app.core.logger import logger from app.core.logger import logger
from app.modules.workflow.core.base import StorageAdapterConfig from app.modules.task.storage.core.base import StorageAdapterConfig
from app.modules.workflow.core.factory import StorageAdapterFactory from app.modules.task.storage.core.factory import StorageAdapterFactory
from app.modules.workflow.source.model import StorageSourceModel from app.modules.task.storage.node.model import StorageNodeModel
from app.modules.workflow.source.service import StorageSourceService from app.modules.task.storage.node.service import StorageNodeService
from app.modules.workflow.transfer.registry import transfer_task_registry from app.modules.task.storage.transfer.registry import transfer_task_registry
from app.modules.workflow.transfer.sse_manager import transfer_stream_manager from app.modules.task.storage.transfer.sse_manager import transfer_stream_manager
from .model import StorageTransferStepModel, StorageTransferTaskModel from .model import StorageTransferStepModel, StorageTransferTaskModel
from .schema import TransferMode, TransferStepOutSchema, TransferTaskOutSchema from .schema import TransferMode, TransferStepOutSchema, TransferTaskOutSchema
@@ -56,10 +56,10 @@ async def _broadcast(task: StorageTransferTaskModel, steps: list[StorageTransfer
async def _build_config(db: AsyncSession, source_id: int) -> StorageAdapterConfig | None: async def _build_config(db: AsyncSession, source_id: int) -> StorageAdapterConfig | None:
"""获取存储源并构造适配器配置(复用存储源服务,含 SDK 高级配置);不存在或停用返回 None。""" """获取存储源并构造适配器配置(复用存储源服务,含 SDK 高级配置);不存在或停用返回 None。"""
source = await db.get(StorageSourceModel, source_id) source = await db.get(StorageNodeModel, source_id)
if source is None or source.status == 1: if source is None or source.status == 1:
return None return None
return StorageSourceService._build_config(source) return StorageNodeService._build_config(source)
def _remove_local_temp(task: StorageTransferTaskModel) -> None: def _remove_local_temp(task: StorageTransferTaskModel) -> None:
@@ -9,7 +9,7 @@ from app.core.base_model import ModelMixin, UserMixin
class StorageTransferTaskModel(ModelMixin, UserMixin): class StorageTransferTaskModel(ModelMixin, UserMixin):
"""文件传输任务模型(多目标 / 链式)""" """文件传输任务模型(多目标 / 链式)"""
__tablename__: str = "task_workflow_transfer_task" __tablename__: str = "task_storage_transfer"
__table_args__: dict[str, str] = {"comment": "文件传输任务表"} __table_args__: dict[str, str] = {"comment": "文件传输任务表"}
name: Mapped[str] = mapped_column(String(128), nullable=False, comment="任务名称") name: Mapped[str] = mapped_column(String(128), nullable=False, comment="任务名称")
@@ -32,11 +32,11 @@ class StorageTransferTaskModel(ModelMixin, UserMixin):
class StorageTransferStepModel(ModelMixin): class StorageTransferStepModel(ModelMixin):
"""文件传输步骤模型(一个任务展开为多个步骤)""" """文件传输步骤模型(一个任务展开为多个步骤)"""
__tablename__: str = "task_workflow_transfer_step" __tablename__: str = "task_storage_transfer_step"
__table_args__: dict[str, str] = {"comment": "文件传输步骤表"} __table_args__: dict[str, str] = {"comment": "文件传输步骤表"}
task_id: Mapped[int] = mapped_column( task_id: Mapped[int] = mapped_column(
ForeignKey("task_workflow_transfer_task.id", ondelete="CASCADE"), ForeignKey("task_storage_transfer.id", ondelete="CASCADE"),
nullable=False, nullable=False,
index=True, index=True,
comment="任务ID", comment="任务ID",
@@ -10,9 +10,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_schema import AuthSchema, PageResultSchema from app.core.base_schema import AuthSchema, PageResultSchema
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
from app.modules.workflow.source.service import StorageSourceService from app.modules.task.storage.node.service import StorageNodeService
from app.modules.workflow.transfer.engine import _broadcast, execute_transfer_task from app.modules.task.storage.transfer.engine import _broadcast, execute_transfer_task
from app.modules.workflow.transfer.registry import transfer_task_registry from app.modules.task.storage.transfer.registry import transfer_task_registry
from app.utils.common_util import search_to_dict from app.utils.common_util import search_to_dict
from .crud import StorageTransferTaskCRUD from .crud import StorageTransferTaskCRUD
@@ -47,7 +47,7 @@ class StorageTransferService:
async def _validate_targets(self, targets: list[TransferTargetSchema]) -> None: async def _validate_targets(self, targets: list[TransferTargetSchema]) -> None:
"""校验目标存储源均存在且启用(批量一次查询)。""" """校验目标存储源均存在且启用(批量一次查询)。"""
source_service = StorageSourceService(self.auth, self.db) source_service = StorageNodeService(self.auth, self.db)
await source_service.get_active_sources([t.target_id for t in targets]) await source_service.get_active_sources([t.target_id for t in targets])
@staticmethod @staticmethod
@@ -118,7 +118,7 @@ class StorageTransferService:
async def create(self, data: TransferTaskCreateSchema) -> int: async def create(self, data: TransferTaskCreateSchema) -> int:
"""创建远端源传输任务。""" """创建远端源传输任务。"""
source_service = StorageSourceService(self.auth, self.db) source_service = StorageNodeService(self.auth, self.db)
if data.source_type == "remote": if data.source_type == "remote":
await source_service.get_active_source(data.source_id) await source_service.get_active_source(data.source_id)
await self._validate_targets(data.targets) await self._validate_targets(data.targets)
@@ -8,20 +8,20 @@ from app.common.response import ResponseSchema, SuccessResponse
from app.core.base_schema import AuthSchema, PageResultSchema, PaginationQueryParam from app.core.base_schema import AuthSchema, PageResultSchema, PaginationQueryParam
from app.core.dependencies import AuthPermission, db_getter from app.core.dependencies import AuthPermission, db_getter
from app.core.router_class import OperationLogRoute from app.core.router_class import OperationLogRoute
from app.modules.workflow.flow.schema import WorkflowFlowCreateSchema, WorkflowFlowExecuteSchema, WorkflowFlowOutSchema, WorkflowFlowQueryParam, WorkflowFlowUpdateSchema from app.modules.task.storage.workflow.schema import WorkflowCreateSchema, WorkflowExecuteSchema, WorkflowOutSchema, WorkflowQueryParam, WorkflowUpdateSchema
from app.modules.workflow.flow.service import WorkflowFlowService from app.modules.task.storage.workflow.service import WorkflowService
WorkflowFlowRouter = APIRouter(route_class=OperationLogRoute, prefix="/flow", tags=["传输流程"]) StorageWorkflowRouter = APIRouter(route_class=OperationLogRoute, prefix="/storage/workflow", tags=["传输流程"])
@WorkflowFlowRouter.get("/page", summary="分页查询传输流程", response_model=ResponseSchema[PageResultSchema[WorkflowFlowOutSchema]]) @StorageWorkflowRouter.get("/page", summary="分页查询传输流程", response_model=ResponseSchema[PageResultSchema[WorkflowOutSchema]])
async def get_flow_page_controller( async def get_flow_page_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:flow:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Depends()], page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[WorkflowFlowQueryParam, Query()], search: Annotated[WorkflowQueryParam, Query()],
) -> JSONResponse: ) -> JSONResponse:
result: PageResultSchema[WorkflowFlowOutSchema] = await WorkflowFlowService(auth, db).page( result: PageResultSchema[WorkflowOutSchema] = await WorkflowService(auth, db).page(
search=search, search=search,
page_no=page.page_no, page_no=page.page_no,
page_size=page.page_size, page_size=page.page_size,
@@ -30,65 +30,65 @@ async def get_flow_page_controller(
return SuccessResponse(data=result, msg="查询传输流程分页成功") return SuccessResponse(data=result, msg="查询传输流程分页成功")
@WorkflowFlowRouter.get("/list", summary="查询传输流程列表", response_model=ResponseSchema[list[WorkflowFlowOutSchema]]) @StorageWorkflowRouter.get("/list", summary="查询传输流程列表", response_model=ResponseSchema[list[WorkflowOutSchema]])
async def get_flow_list_controller( async def get_flow_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:flow:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
search: Annotated[WorkflowFlowQueryParam, Query()], search: Annotated[WorkflowQueryParam, Query()],
) -> JSONResponse: ) -> JSONResponse:
result: list[WorkflowFlowOutSchema] = await WorkflowFlowService(auth, db).get_list(search=search) result: list[WorkflowOutSchema] = await WorkflowService(auth, db).get_list(search=search)
return SuccessResponse(data=result, msg="查询传输流程列表成功") return SuccessResponse(data=result, msg="查询传输流程列表成功")
@WorkflowFlowRouter.get("/detail/{id}", summary="查询传输流程详情", response_model=ResponseSchema[WorkflowFlowOutSchema]) @StorageWorkflowRouter.get("/detail/{id}", summary="查询传输流程详情", response_model=ResponseSchema[WorkflowOutSchema])
async def get_flow_detail_controller( async def get_flow_detail_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:query"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:flow:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="流程ID", ge=1)], id: Annotated[int, Path(description="流程ID", ge=1)],
) -> JSONResponse: ) -> JSONResponse:
result: WorkflowFlowOutSchema = await WorkflowFlowService(auth, db).detail(id=id) result: WorkflowOutSchema = await WorkflowService(auth, db).detail(id=id)
return SuccessResponse(data=result, msg="查询传输流程详情成功") return SuccessResponse(data=result, msg="查询传输流程详情成功")
@WorkflowFlowRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建传输流程", response_model=ResponseSchema[WorkflowFlowOutSchema]) @StorageWorkflowRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建传输流程", response_model=ResponseSchema[WorkflowOutSchema])
async def create_flow_controller( async def create_flow_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:create"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:flow:create"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
data: Annotated[WorkflowFlowCreateSchema, Body(description="流程创建参数")], data: Annotated[WorkflowCreateSchema, Body(description="流程创建参数")],
) -> JSONResponse: ) -> JSONResponse:
result: WorkflowFlowOutSchema = await WorkflowFlowService(auth, db).create(data=data) result: WorkflowOutSchema = await WorkflowService(auth, db).create(data=data)
return SuccessResponse(data=result, msg="创建传输流程成功") return SuccessResponse(data=result, msg="创建传输流程成功")
@WorkflowFlowRouter.put("/update/{id}", summary="修改传输流程", response_model=ResponseSchema[WorkflowFlowOutSchema]) @StorageWorkflowRouter.put("/update/{id}", summary="修改传输流程", response_model=ResponseSchema[WorkflowOutSchema])
async def update_flow_controller( async def update_flow_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:update"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:flow:update"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="流程ID", ge=1)], id: Annotated[int, Path(description="流程ID", ge=1)],
data: Annotated[WorkflowFlowUpdateSchema, Body(description="流程修改参数")], data: Annotated[WorkflowUpdateSchema, Body(description="流程修改参数")],
) -> JSONResponse: ) -> JSONResponse:
result: WorkflowFlowOutSchema = await WorkflowFlowService(auth, db).update(id=id, data=data) result: WorkflowOutSchema = await WorkflowService(auth, db).update(id=id, data=data)
return SuccessResponse(data=result, msg="修改传输流程成功") return SuccessResponse(data=result, msg="修改传输流程成功")
@WorkflowFlowRouter.delete("/delete", summary="删除传输流程", response_model=ResponseSchema[None]) @StorageWorkflowRouter.delete("/delete", summary="删除传输流程", response_model=ResponseSchema[None])
async def delete_flow_controller( async def delete_flow_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:delete"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:flow:delete"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
ids: Annotated[list[int], Body(description="流程ID列表")], ids: Annotated[list[int], Body(description="流程ID列表")],
) -> JSONResponse: ) -> JSONResponse:
await WorkflowFlowService(auth, db).delete(ids=ids) await WorkflowService(auth, db).delete(ids=ids)
return SuccessResponse(msg="删除传输流程成功") return SuccessResponse(msg="删除传输流程成功")
@WorkflowFlowRouter.post("/execute/{id}", summary="执行传输流程", response_model=ResponseSchema[list[int]]) @StorageWorkflowRouter.post("/execute/{id}", summary="执行传输流程", response_model=ResponseSchema[list[int]])
async def execute_flow_controller( async def execute_flow_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:transfer:create"]))], auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:transfer:create"]))],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="流程ID", ge=1)], id: Annotated[int, Path(description="流程ID", ge=1)],
data: Annotated[WorkflowFlowExecuteSchema | None, Body(description="执行参数(源文件/目录路径映射,可选)")] = None, data: Annotated[WorkflowExecuteSchema | None, Body(description="执行参数(源文件/目录路径映射,可选)")] = None,
) -> JSONResponse: ) -> JSONResponse:
task_ids: list[int] = await WorkflowFlowService(auth, db).execute( task_ids: list[int] = await WorkflowService(auth, db).execute(
id=id, source_paths=data.source_paths if data else None id=id, source_paths=data.source_paths if data else None
) )
return SuccessResponse(data=task_ids, msg=f"执行传输流程成功,已生成 {len(task_ids)} 个传输任务") return SuccessResponse(data=task_ids, msg=f"执行传输流程成功,已生成 {len(task_ids)} 个传输任务")
@@ -6,18 +6,18 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_crud import CRUDBase from app.core.base_crud import CRUDBase
from app.core.base_schema import AuthSchema from app.core.base_schema import AuthSchema
from .model import WorkflowFlowEdgeModel, WorkflowFlowModel, WorkflowFlowNodeModel from .model import WorkflowEdgeModel, WorkflowModel, WorkflowNodeModel
from .schema import WorkflowFlowCreateSchema, WorkflowFlowUpdateSchema from .schema import WorkflowCreateSchema, WorkflowUpdateSchema
class WorkflowFlowCRUD(CRUDBase[WorkflowFlowModel, WorkflowFlowCreateSchema, WorkflowFlowUpdateSchema]): class WorkflowCRUD(CRUDBase[WorkflowModel, WorkflowCreateSchema, WorkflowUpdateSchema]):
"""传输流程数据层""" """传输流程数据层"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(model=WorkflowFlowModel, auth=auth, db=db) super().__init__(model=WorkflowModel, auth=auth, db=db)
class WorkflowFlowNodeCRUD(CRUDBase[WorkflowFlowNodeModel, object, object]): class WorkflowNodeCRUD(CRUDBase[WorkflowNodeModel, object, object]):
"""流程节点明细数据层 """流程节点明细数据层
明细是 flow 的派生从属数据:随父流程全量覆写/删除,无独立数据权限主体, 明细是 flow 的派生从属数据:随父流程全量覆写/删除,无独立数据权限主体,
@@ -26,23 +26,23 @@ class WorkflowFlowNodeCRUD(CRUDBase[WorkflowFlowNodeModel, object, object]):
""" """
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(model=WorkflowFlowNodeModel, auth=auth, db=db) super().__init__(model=WorkflowNodeModel, auth=auth, db=db)
async def hard_delete_by_flow_ids(self, flow_ids: Sequence[int]) -> None: async def hard_delete_by_flow_ids(self, flow_ids: Sequence[int]) -> None:
"""按 flow 物理删除明细(父流程行已过数据权限校验)。""" """按 flow 物理删除明细(父流程行已过数据权限校验)。"""
if not flow_ids: if not flow_ids:
return return
_ = await self.db.execute(delete(WorkflowFlowNodeModel).where(WorkflowFlowNodeModel.flow_id.in_(flow_ids))) _ = await self.db.execute(delete(WorkflowNodeModel).where(WorkflowNodeModel.flow_id.in_(flow_ids)))
class WorkflowFlowEdgeCRUD(CRUDBase[WorkflowFlowEdgeModel, object, object]): class WorkflowEdgeCRUD(CRUDBase[WorkflowEdgeModel, object, object]):
"""流程连线明细数据层,删除语义同 WorkflowFlowNodeCRUD""" """流程连线明细数据层,删除语义同 WorkflowNodeCRUD"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(model=WorkflowFlowEdgeModel, auth=auth, db=db) super().__init__(model=WorkflowEdgeModel, auth=auth, db=db)
async def hard_delete_by_flow_ids(self, flow_ids: Sequence[int]) -> None: async def hard_delete_by_flow_ids(self, flow_ids: Sequence[int]) -> None:
"""按 flow 物理删除明细(父流程行已过数据权限校验)。""" """按 flow 物理删除明细(父流程行已过数据权限校验)。"""
if not flow_ids: if not flow_ids:
return return
_ = await self.db.execute(delete(WorkflowFlowEdgeModel).where(WorkflowFlowEdgeModel.flow_id.in_(flow_ids))) _ = await self.db.execute(delete(WorkflowEdgeModel).where(WorkflowEdgeModel.flow_id.in_(flow_ids)))
@@ -4,14 +4,14 @@ from sqlalchemy.orm import Mapped, mapped_column
from app.core.base_model import ModelMixin, UserMixin from app.core.base_model import ModelMixin, UserMixin
class WorkflowFlowModel(ModelMixin, UserMixin): class WorkflowModel(ModelMixin, UserMixin):
"""传输流程:定义源节点 → 目标节点列表(parallel 多目标 / chain 链式) """传输流程:定义源节点 → 目标节点列表(parallel 多目标 / chain 链式)
graph 仅存画布布局与展示字段(节点位置、连线样式),业务配置落于 graph 仅存画布布局与展示字段(节点位置、连线样式),业务配置落于
flow_node / flow_edge 表,回显时由 service 组装,避免双份数据不一致。 flow_node / flow_edge 表,回显时由 service 组装,避免双份数据不一致。
""" """
__tablename__: str = "task_workflow_flow" __tablename__: str = "task_storage_workflow"
__table_args__: dict[str, str] = {"comment": "传输流程定义表"} __table_args__: dict[str, str] = {"comment": "传输流程定义表"}
name: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="流程名称") name: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="流程名称")
@@ -21,13 +21,13 @@ class WorkflowFlowModel(ModelMixin, UserMixin):
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注") description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
class WorkflowFlowNodeModel(ModelMixin, UserMixin): class WorkflowNodeModel(ModelMixin, UserMixin):
"""流程画布节点(业务配置):节点关联的存储源与默认源目录。 """流程画布节点(业务配置):节点关联的存储源与默认源目录。
节点在画布上的位置/名称等布局字段存 flow.graph 的 nodes 项(key=node_key)。 节点在画布上的位置/名称等布局字段存 flow.graph 的 nodes 项(key=node_key)。
""" """
__tablename__: str = "task_workflow_flow_node" __tablename__: str = "task_storage_workflow_node"
__table_args__: dict[str, str] = {"comment": "流程画布节点表"} __table_args__: dict[str, str] = {"comment": "流程画布节点表"}
flow_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True, comment="流程ID") flow_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True, comment="流程ID")
@@ -36,14 +36,14 @@ class WorkflowFlowNodeModel(ModelMixin, UserMixin):
source_path: Mapped[str | None] = mapped_column(String(1024), default=None, nullable=True, comment="默认源目录") source_path: Mapped[str | None] = mapped_column(String(1024), default=None, nullable=True, comment="默认源目录")
class WorkflowFlowEdgeModel(ModelMixin, UserMixin): class WorkflowEdgeModel(ModelMixin, UserMixin):
"""流程画布连线(业务配置):传输方式与分片参数。 """流程画布连线(业务配置):传输方式与分片参数。
连线的目标目录由目标节点的默认源目录决定(节点 source_path),连线不再配置路径。 连线的目标目录由目标节点的默认源目录决定(节点 source_path),连线不再配置路径。
连线的样式/动画等展示字段存 flow.graph 的 edges 项(key=edge_key)。 连线的样式/动画等展示字段存 flow.graph 的 edges 项(key=edge_key)。
""" """
__tablename__: str = "task_workflow_flow_edge" __tablename__: str = "task_storage_workflow_edge"
__table_args__: dict[str, str] = {"comment": "流程画布连线表"} __table_args__: dict[str, str] = {"comment": "流程画布连线表"}
flow_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True, comment="流程ID") flow_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True, comment="流程ID")
@@ -4,24 +4,24 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida
from app.core.base_schema import BaseQueryParam, BaseSchema, UserByQueryParam, UserBySchema from app.core.base_schema import BaseQueryParam, BaseSchema, UserByQueryParam, UserBySchema
FlowTaskType = Literal["parallel", "chain"] WorkflowTaskType = Literal["parallel", "chain"]
class FlowTargetSchema(BaseModel): class WorkflowTargetSchema(BaseModel):
"""流程目标节点配置(目标目录由目标节点默认源目录决定)""" """流程目标节点配置(目标目录由目标节点默认源目录决定)"""
target_id: int = Field(..., ge=1, description="目标节点ID(存储源)") target_id: int = Field(..., ge=1, description="目标节点ID(存储源)")
target_path: str = Field(..., max_length=1024, description="目标路径") target_path: str = Field(..., max_length=1024, description="目标路径")
class FlowSourceSchema(BaseModel): class WorkflowSourceSchema(BaseModel):
"""流程源节点概览(由画布连线实时派生)""" """流程源节点概览(由画布连线实时派生)"""
source_id: int = Field(..., ge=1, description="源节点ID(存储源)") source_id: int = Field(..., ge=1, description="源节点ID(存储源)")
source_name: str | None = Field(default=None, description="源存储源名称") source_name: str | None = Field(default=None, description="源存储源名称")
class FlowNodeSchema(BaseModel): class WorkflowNodeSchema(BaseModel):
"""画布节点业务配置(由画布拆分,落 flow_node 表)""" """画布节点业务配置(由画布拆分,落 flow_node 表)"""
node_key: str = Field(..., description="画布节点ID") node_key: str = Field(..., description="画布节点ID")
@@ -29,7 +29,7 @@ class FlowNodeSchema(BaseModel):
source_path: str | None = Field(default=None, max_length=1024, description="默认源目录") source_path: str | None = Field(default=None, max_length=1024, description="默认源目录")
class FlowEdgeSchema(BaseModel): class WorkflowEdgeSchema(BaseModel):
"""画布连线业务配置(由画布拆分,落 flow_edge 表) """画布连线业务配置(由画布拆分,落 flow_edge 表)
连线只定义传输方式;目标目录由目标节点的默认源目录(source_path)决定。 连线只定义传输方式;目标目录由目标节点的默认源目录(source_path)决定。
@@ -44,7 +44,7 @@ class FlowEdgeSchema(BaseModel):
multipart_concurrency: int | None = Field(default=None, ge=1, description="分片并发数") multipart_concurrency: int | None = Field(default=None, ge=1, description="分片并发数")
class FlowLayoutNodeSchema(BaseModel): class WorkflowLayoutNodeSchema(BaseModel):
"""画布节点布局展示字段(存 flow.graph,业务配置在 flow_node 表)""" """画布节点布局展示字段(存 flow.graph,业务配置在 flow_node 表)"""
id: str = Field(..., description="画布节点ID") id: str = Field(..., description="画布节点ID")
@@ -54,7 +54,7 @@ class FlowLayoutNodeSchema(BaseModel):
style: dict | None = Field(default=None, description="节点样式") style: dict | None = Field(default=None, description="节点样式")
class FlowLayoutEdgeSchema(BaseModel): class WorkflowLayoutEdgeSchema(BaseModel):
"""画布连线布局展示字段(存 flow.graph,业务配置在 flow_edge 表)""" """画布连线布局展示字段(存 flow.graph,业务配置在 flow_edge 表)"""
id: str = Field(..., description="画布连线ID") id: str = Field(..., description="画布连线ID")
@@ -66,7 +66,7 @@ class FlowLayoutEdgeSchema(BaseModel):
label: str | None = Field(default=None, description="连线显示名") label: str | None = Field(default=None, description="连线显示名")
class FlowGraphNodeDataSchema(BaseModel): class WorkflowGraphNodeDataSchema(BaseModel):
"""画布节点回显数据(组装完整画布时写入 node.data)""" """画布节点回显数据(组装完整画布时写入 node.data)"""
source_id: int = Field(..., ge=1, description="存储源ID") source_id: int = Field(..., ge=1, description="存储源ID")
@@ -80,7 +80,7 @@ class FlowGraphNodeDataSchema(BaseModel):
path_prefix: str | None = Field(default=None, description="路径前缀") path_prefix: str | None = Field(default=None, description="路径前缀")
class FlowGraphEdgeDataSchema(BaseModel): class WorkflowGraphEdgeDataSchema(BaseModel):
"""画布连线回显数据(组装完整画布时写入 edge.data)""" """画布连线回显数据(组装完整画布时写入 edge.data)"""
enabled: bool = Field(default=True, description="是否启用(禁用则不执行)") enabled: bool = Field(default=True, description="是否启用(禁用则不执行)")
@@ -95,17 +95,17 @@ class FlowGraphEdgeDataSchema(BaseModel):
target_storage_id: int | None = Field(default=None, description="目标存储源ID") target_storage_id: int | None = Field(default=None, description="目标存储源ID")
class FlowSplitResultSchema(BaseModel): class WorkflowSplitResultSchema(BaseModel):
"""画布拆分结果:业务明细(node/edge)+ 精简布局 + 派生概览""" """画布拆分结果:业务明细(node/edge)+ 精简布局 + 派生概览"""
layout: dict = Field(..., description="精简后的画布布局 {nodes:[{id,type,position,label}],edges:[{id,source,target,type,animated,style,label}]}") layout: dict = Field(..., description="精简后的画布布局 {nodes:[{id,type,position,label}],edges:[{id,source,target,type,animated,style,label}]}")
nodes: list[FlowNodeSchema] = Field(default_factory=list, description="节点业务明细") nodes: list[WorkflowNodeSchema] = Field(default_factory=list, description="节点业务明细")
edges: list[FlowEdgeSchema] = Field(default_factory=list, description="连线业务明细") edges: list[WorkflowEdgeSchema] = Field(default_factory=list, description="连线业务明细")
sources: list[FlowSourceSchema] = Field(default_factory=list, description="源节点列表(去重)") sources: list[WorkflowSourceSchema] = Field(default_factory=list, description="源节点列表(去重)")
targets: list[FlowTargetSchema] = Field(default_factory=list, description="目标列表(去重)") targets: list[WorkflowTargetSchema] = Field(default_factory=list, description="目标列表(去重)")
class FlowTransferPlanSchema(BaseModel): class WorkflowTransferPlanSchema(BaseModel):
"""执行计划:单条连线生成的传输任务参数""" """执行计划:单条连线生成的传输任务参数"""
src_id: int = Field(..., ge=1, description="源存储源ID") src_id: int = Field(..., ge=1, description="源存储源ID")
@@ -117,11 +117,11 @@ class FlowTransferPlanSchema(BaseModel):
multipart_concurrency: int | None = Field(default=None, description="分片并发数") multipart_concurrency: int | None = Field(default=None, description="分片并发数")
class WorkflowFlowCreateSchema(BaseModel): class WorkflowCreateSchema(BaseModel):
"""创建传输流程(画布驱动:业务配置解析后落 flow_node/flow_edge 表)""" """创建传输流程(画布驱动:业务配置解析后落 flow_node/flow_edge 表)"""
name: str = Field(..., min_length=1, max_length=64, description="流程名称") name: str = Field(..., min_length=1, max_length=64, description="流程名称")
task_type: FlowTaskType = Field("parallel", description="类型(parallel:多目标 chain:链式)") task_type: WorkflowTaskType = Field("parallel", description="类型(parallel:多目标 chain:链式)")
graph: dict = Field(..., description="VueFlow画布数据 {nodes:[{id,type,position,data}],edges:[{id,source,target,data}]}") graph: dict = Field(..., description="VueFlow画布数据 {nodes:[{id,type,position,data}],edges:[{id,source,target,data}]}")
status: int = Field(default=0, ge=0, le=1, description="状态(0:启用 1:停用)") status: int = Field(default=0, ge=0, le=1, description="状态(0:启用 1:停用)")
description: str | None = Field(default=None, max_length=255, description="备注") description: str | None = Field(default=None, max_length=255, description="备注")
@@ -142,11 +142,11 @@ class WorkflowFlowCreateSchema(BaseModel):
return self return self
class WorkflowFlowUpdateSchema(WorkflowFlowCreateSchema): class WorkflowUpdateSchema(WorkflowCreateSchema):
"""更新传输流程""" """更新传输流程"""
class WorkflowFlowExecuteSchema(BaseModel): class WorkflowExecuteSchema(BaseModel):
"""执行传输流程参数""" """执行传输流程参数"""
source_paths: dict[str, str] | None = Field( source_paths: dict[str, str] | None = Field(
@@ -155,24 +155,24 @@ class WorkflowFlowExecuteSchema(BaseModel):
) )
class WorkflowFlowOutSchema(BaseSchema, UserBySchema): class WorkflowOutSchema(BaseSchema, UserBySchema):
"""传输流程详情响应模型(sources/targets 由 service 从明细表派生)""" """传输流程详情响应模型(sources/targets 由 service 从明细表派生)"""
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
name: str | None = None name: str | None = None
task_type: FlowTaskType = "parallel" task_type: WorkflowTaskType = "parallel"
sources: list[FlowSourceSchema] = Field(default_factory=list, description="源节点列表(由画布派生)") sources: list[WorkflowSourceSchema] = Field(default_factory=list, description="源节点列表(由画布派生)")
targets: list[FlowTargetSchema] = Field(default_factory=list) targets: list[WorkflowTargetSchema] = Field(default_factory=list)
graph: dict | None = None graph: dict | None = None
graph_stats: dict | None = None graph_stats: dict | None = None
status: int = 0 status: int = 0
description: str | None = None description: str | None = None
class WorkflowFlowQueryParam(BaseQueryParam, UserByQueryParam): class WorkflowQueryParam(BaseQueryParam, UserByQueryParam):
"""传输流程查询参数""" """传输流程查询参数"""
name: str | None = Field(None, description="流程名称", json_schema_extra={"q": "like"}) name: str | None = Field(None, description="流程名称", json_schema_extra={"q": "like"})
task_type: FlowTaskType | None = Field(None, description="类型(parallel/chain)", json_schema_extra={"q": "eq"}) task_type: WorkflowTaskType | None = Field(None, description="类型(parallel/chain)", json_schema_extra={"q": "eq"})
status: int | None = Field(None, ge=0, le=1, description="状态(0:启用 1:停用)", json_schema_extra={"q": "eq"}) status: int | None = Field(None, ge=0, le=1, description="状态(0:启用 1:停用)", json_schema_extra={"q": "eq"})
@@ -7,34 +7,34 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_schema import AuthSchema, PageResultSchema from app.core.base_schema import AuthSchema, PageResultSchema
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
from app.modules.workflow.source.crud import StorageSourceCRUD from app.modules.task.storage.node.crud import StorageNodeCRUD
from app.modules.workflow.source.model import StorageSourceModel from app.modules.task.storage.node.model import StorageNodeModel
from app.modules.workflow.source.service import StorageSourceService from app.modules.task.storage.node.service import StorageNodeService
from app.modules.workflow.transfer.schema import TransferMode, TransferTargetSchema, TransferTaskCreateSchema from app.modules.task.storage.transfer.schema import TransferMode, TransferTargetSchema, TransferTaskCreateSchema
from app.modules.workflow.transfer.service import StorageTransferService from app.modules.task.storage.transfer.service import StorageTransferService
from app.utils.common_util import search_to_dict from app.utils.common_util import search_to_dict
from .crud import WorkflowFlowCRUD, WorkflowFlowEdgeCRUD, WorkflowFlowNodeCRUD from .crud import WorkflowCRUD, WorkflowEdgeCRUD, WorkflowNodeCRUD
from .model import WorkflowFlowEdgeModel, WorkflowFlowModel, WorkflowFlowNodeModel from .model import WorkflowEdgeModel, WorkflowModel, WorkflowNodeModel
from .schema import ( from .schema import (
FlowEdgeSchema, WorkflowCreateSchema,
FlowGraphEdgeDataSchema, WorkflowEdgeSchema,
FlowGraphNodeDataSchema, WorkflowGraphEdgeDataSchema,
FlowLayoutEdgeSchema, WorkflowGraphNodeDataSchema,
FlowLayoutNodeSchema, WorkflowLayoutEdgeSchema,
FlowNodeSchema, WorkflowLayoutNodeSchema,
FlowSourceSchema, WorkflowNodeSchema,
FlowSplitResultSchema, WorkflowOutSchema,
FlowTargetSchema, WorkflowQueryParam,
FlowTransferPlanSchema, WorkflowSourceSchema,
WorkflowFlowCreateSchema, WorkflowSplitResultSchema,
WorkflowFlowOutSchema, WorkflowTargetSchema,
WorkflowFlowQueryParam, WorkflowTransferPlanSchema,
WorkflowFlowUpdateSchema, WorkflowUpdateSchema,
) )
class WorkflowFlowService: class WorkflowService:
"""传输流程服务(源节点 → 目标节点,支持 1对多 / 多对1) """传输流程服务(源节点 → 目标节点,支持 1对多 / 多对1)
数据职责划分: 数据职责划分:
@@ -51,24 +51,24 @@ class WorkflowFlowService:
# ── 内部工具 ──────────────────────────────────────────────────── # ── 内部工具 ────────────────────────────────────────────────────
def _crud(self) -> WorkflowFlowCRUD: def _crud(self) -> WorkflowCRUD:
return WorkflowFlowCRUD(self.auth, self.db) return WorkflowCRUD(self.auth, self.db)
async def _validate_nodes( async def _validate_nodes(
self, sources: list[FlowSourceSchema], targets: list[FlowTargetSchema] self, sources: list[WorkflowSourceSchema], targets: list[WorkflowTargetSchema]
) -> None: ) -> None:
"""校验源节点与目标节点均存在且启用。""" """校验源节点与目标节点均存在且启用。"""
node_service = StorageSourceService(self.auth, self.db) node_service = StorageNodeService(self.auth, self.db)
ids = [s.source_id for s in sources] + [t.target_id for t in targets] ids = [s.source_id for s in sources] + [t.target_id for t in targets]
if ids: if ids:
await node_service.get_active_sources(ids) await node_service.get_active_sources(ids)
@staticmethod @staticmethod
def _sources_from( def _sources_from(
edge_rows: Sequence[WorkflowFlowEdgeModel], node_map: dict[str, WorkflowFlowNodeModel] edge_rows: Sequence[WorkflowEdgeModel], node_map: dict[str, WorkflowNodeModel]
) -> list[FlowSourceSchema]: ) -> list[WorkflowSourceSchema]:
"""由连线明细派生源节点列表(去重)。""" """由连线明细派生源节点列表(去重)。"""
out: list[FlowSourceSchema] = [] out: list[WorkflowSourceSchema] = []
seen: set[int] = set() seen: set[int] = set()
for e in edge_rows: for e in edge_rows:
src_node = node_map.get(e.source_node_key) src_node = node_map.get(e.source_node_key)
@@ -77,15 +77,15 @@ class WorkflowFlowService:
if src_node.source_id in seen: if src_node.source_id in seen:
continue continue
seen.add(src_node.source_id) seen.add(src_node.source_id)
out.append(FlowSourceSchema(source_id=src_node.source_id)) out.append(WorkflowSourceSchema(source_id=src_node.source_id))
return out return out
@staticmethod @staticmethod
def _targets_from( def _targets_from(
edge_rows: Sequence[WorkflowFlowEdgeModel], node_map: dict[str, WorkflowFlowNodeModel] edge_rows: Sequence[WorkflowEdgeModel], node_map: dict[str, WorkflowNodeModel]
) -> list[FlowTargetSchema]: ) -> list[WorkflowTargetSchema]:
"""由连线明细派生目标列表(去重),目标目录取目标节点的默认源目录。""" """由连线明细派生目标列表(去重),目标目录取目标节点的默认源目录。"""
out: list[FlowTargetSchema] = [] out: list[WorkflowTargetSchema] = []
seen: set[int] = set() seen: set[int] = set()
for e in edge_rows: for e in edge_rows:
tgt_node = node_map.get(e.target_node_key) tgt_node = node_map.get(e.target_node_key)
@@ -95,14 +95,14 @@ class WorkflowFlowService:
continue continue
seen.add(tgt_node.source_id) seen.add(tgt_node.source_id)
out.append( out.append(
FlowTargetSchema(target_id=tgt_node.source_id, target_path=tgt_node.source_path or "") WorkflowTargetSchema(target_id=tgt_node.source_id, target_path=tgt_node.source_path or "")
) )
return out return out
# ── 画布拆分/组装 ─────────────────────────────────────────────── # ── 画布拆分/组装 ───────────────────────────────────────────────
@staticmethod @staticmethod
def _split_graph(graph: dict) -> FlowSplitResultSchema: def _split_graph(graph: dict) -> WorkflowSplitResultSchema:
"""校验并拆分提交的画布。 """校验并拆分提交的画布。
- 业务配置(节点 source_id/source_path、连线启用与传输方式)→ FlowNode/FlowEdge 明细 - 业务配置(节点 source_id/source_path、连线启用与传输方式)→ FlowNode/FlowEdge 明细
@@ -115,11 +115,11 @@ class WorkflowFlowService:
raise CustomException(msg="画布中未找到有效的传输连线(源节点 → 目标节点)") raise CustomException(msg="画布中未找到有效的传输连线(源节点 → 目标节点)")
layout_nodes: list[dict] = [] layout_nodes: list[dict] = []
node_items: list[FlowNodeSchema] = [] node_items: list[WorkflowNodeSchema] = []
for nid, n in nodes.items(): for nid, n in nodes.items():
d = n.get("data") or {} d = n.get("data") or {}
layout_nodes.append( layout_nodes.append(
FlowLayoutNodeSchema( WorkflowLayoutNodeSchema(
id=nid, id=nid,
type=n.get("type") or "storage", type=n.get("type") or "storage",
position=n.get("position") or {"x": 0, "y": 0}, position=n.get("position") or {"x": 0, "y": 0},
@@ -129,13 +129,13 @@ class WorkflowFlowService:
) )
if d.get("source_id") is not None: if d.get("source_id") is not None:
node_items.append( node_items.append(
FlowNodeSchema(node_key=nid, source_id=d["source_id"], source_path=d.get("source_path")) WorkflowNodeSchema(node_key=nid, source_id=d["source_id"], source_path=d.get("source_path"))
) )
node_map = {it.node_key: it for it in node_items} node_map = {it.node_key: it for it in node_items}
sources: list[FlowSourceSchema] = [] sources: list[WorkflowSourceSchema] = []
targets: list[FlowTargetSchema] = [] targets: list[WorkflowTargetSchema] = []
edge_items: list[FlowEdgeSchema] = [] edge_items: list[WorkflowEdgeSchema] = []
layout_edges: list[dict] = [] layout_edges: list[dict] = []
seen_edges: set[tuple[int, int]] = set() seen_edges: set[tuple[int, int]] = set()
seen_sources: set[int] = set() seen_sources: set[int] = set()
@@ -156,14 +156,14 @@ class WorkflowFlowService:
seen_edges.add(pair) seen_edges.add(pair)
if sid not in seen_sources: if sid not in seen_sources:
seen_sources.add(sid) seen_sources.add(sid)
sources.append(FlowSourceSchema(source_id=sid)) sources.append(WorkflowSourceSchema(source_id=sid))
if tid not in seen_targets: if tid not in seen_targets:
seen_targets.add(tid) seen_targets.add(tid)
# 目标目录由目标节点默认源目录决定 # 目标目录由目标节点默认源目录决定
targets.append(FlowTargetSchema(target_id=tid, target_path=tgt_n.source_path or "")) targets.append(WorkflowTargetSchema(target_id=tid, target_path=tgt_n.source_path or ""))
ed = e.get("data") or {} ed = e.get("data") or {}
edge_items.append( edge_items.append(
FlowEdgeSchema( WorkflowEdgeSchema(
edge_key=e["id"], edge_key=e["id"],
source_node_key=e.get("source"), source_node_key=e.get("source"),
target_node_key=e.get("target"), target_node_key=e.get("target"),
@@ -174,7 +174,7 @@ class WorkflowFlowService:
) )
) )
layout_edges.append( layout_edges.append(
FlowLayoutEdgeSchema( WorkflowLayoutEdgeSchema(
id=e["id"], id=e["id"],
source=e.get("source"), source=e.get("source"),
target=e.get("target"), target=e.get("target"),
@@ -187,7 +187,7 @@ class WorkflowFlowService:
if not edge_items: if not edge_items:
raise CustomException(msg="画布中未找到有效的传输连线(源节点 → 目标节点)") raise CustomException(msg="画布中未找到有效的传输连线(源节点 → 目标节点)")
return FlowSplitResultSchema( return WorkflowSplitResultSchema(
layout={"nodes": layout_nodes, "edges": layout_edges}, layout={"nodes": layout_nodes, "edges": layout_edges},
nodes=node_items, nodes=node_items,
edges=edge_items, edges=edge_items,
@@ -195,41 +195,41 @@ class WorkflowFlowService:
targets=targets, targets=targets,
) )
async def _save_graph(self, flow_id: int, nodes: list[FlowNodeSchema], edges: list[FlowEdgeSchema]) -> None: async def _save_graph(self, flow_id: int, nodes: list[WorkflowNodeSchema], edges: list[WorkflowEdgeSchema]) -> None:
"""覆写流程的业务明细(附属表物理删除后重建,不做逻辑删除)。 """覆写流程的业务明细(附属表物理删除后重建,不做逻辑删除)。
父流程行已在调用方经 WorkflowFlowCRUD 做过数据权限校验,明细随父全量覆写, 父流程行已在调用方经 WorkflowCRUD 做过数据权限校验,明细随父全量覆写,
删除统一走子表 CRUD 的物理清理方法(基类软删 delete 不适用于逐次覆写场景)。 删除统一走子表 CRUD 的物理清理方法(基类软删 delete 不适用于逐次覆写场景)。
""" """
await WorkflowFlowNodeCRUD(self.auth, self.db).hard_delete_by_flow_ids([flow_id]) await WorkflowNodeCRUD(self.auth, self.db).hard_delete_by_flow_ids([flow_id])
await WorkflowFlowEdgeCRUD(self.auth, self.db).hard_delete_by_flow_ids([flow_id]) await WorkflowEdgeCRUD(self.auth, self.db).hard_delete_by_flow_ids([flow_id])
user_id = self.auth.user.id user_id = self.auth.user.id
for it in nodes: for it in nodes:
self.db.add(WorkflowFlowNodeModel(flow_id=flow_id, created_id=user_id, updated_id=user_id, **it.model_dump())) self.db.add(WorkflowNodeModel(flow_id=flow_id, created_id=user_id, updated_id=user_id, **it.model_dump()))
for it in edges: for it in edges:
self.db.add(WorkflowFlowEdgeModel(flow_id=flow_id, created_id=user_id, updated_id=user_id, **it.model_dump())) self.db.add(WorkflowEdgeModel(flow_id=flow_id, created_id=user_id, updated_id=user_id, **it.model_dump()))
await self.db.flush() await self.db.flush()
async def _load_flow_graph(self, flow_id: int) -> tuple[list[WorkflowFlowNodeModel], list[WorkflowFlowEdgeModel]]: async def _load_flow_graph(self, flow_id: int) -> tuple[list[WorkflowNodeModel], list[WorkflowEdgeModel]]:
result = await self.db.execute( result = await self.db.execute(
select(WorkflowFlowNodeModel) select(WorkflowNodeModel)
.where(WorkflowFlowNodeModel.flow_id == flow_id) .where(WorkflowNodeModel.flow_id == flow_id)
.order_by(WorkflowFlowNodeModel.id) .order_by(WorkflowNodeModel.id)
) )
node_rows = list(result.scalars().all()) node_rows = list(result.scalars().all())
result = await self.db.execute( result = await self.db.execute(
select(WorkflowFlowEdgeModel) select(WorkflowEdgeModel)
.where(WorkflowFlowEdgeModel.flow_id == flow_id) .where(WorkflowEdgeModel.flow_id == flow_id)
.order_by(WorkflowFlowEdgeModel.id) .order_by(WorkflowEdgeModel.id)
) )
edge_rows = list(result.scalars().all()) edge_rows = list(result.scalars().all())
return node_rows, edge_rows return node_rows, edge_rows
async def _build_graph( async def _build_graph(
self, self,
flow: WorkflowFlowModel, flow: WorkflowModel,
node_rows: Sequence[WorkflowFlowNodeModel], node_rows: Sequence[WorkflowNodeModel],
edge_rows: Sequence[WorkflowFlowEdgeModel], edge_rows: Sequence[WorkflowEdgeModel],
) -> dict: ) -> dict:
"""由布局(flow.graph)+ 业务明细(node/edge 表)+ 存储源组装完整画布,供前端直接回显。""" """由布局(flow.graph)+ 业务明细(node/edge 表)+ 存储源组装完整画布,供前端直接回显。"""
layout = flow.graph or {} layout = flow.graph or {}
@@ -239,9 +239,9 @@ class WorkflowFlowService:
edges_by_key = {e.edge_key: e for e in edge_rows} edges_by_key = {e.edge_key: e for e in edge_rows}
ids = {n.source_id for n in node_rows} ids = {n.source_id for n in node_rows}
src_map: dict[int, StorageSourceModel] = {} src_map: dict[int, StorageNodeModel] = {}
if ids: if ids:
sources = await StorageSourceCRUD(self.auth, self.db).get_list(search={"id": ("in", sorted(ids))}) sources = await StorageNodeCRUD(self.auth, self.db).get_list(search={"id": ("in", sorted(ids))})
src_map = {s.id: s for s in sources} src_map = {s.id: s for s in sources}
out_nodes: list[dict] = [] out_nodes: list[dict] = []
@@ -251,7 +251,7 @@ class WorkflowFlowService:
continue continue
src = src_map.get(node_row.source_id) src = src_map.get(node_row.source_id)
node = dict(ln) node = dict(ln)
node["data"] = FlowGraphNodeDataSchema( node["data"] = WorkflowGraphNodeDataSchema(
source_id=node_row.source_id, source_id=node_row.source_id,
source_path=node_row.source_path, source_path=node_row.source_path,
label=ln.get("label") or (src.name if src else None), label=ln.get("label") or (src.name if src else None),
@@ -274,7 +274,7 @@ class WorkflowFlowService:
src = src_map.get(src_node.source_id) if src_node else None src = src_map.get(src_node.source_id) if src_node else None
tgt = src_map.get(tgt_node.source_id) if tgt_node else None tgt = src_map.get(tgt_node.source_id) if tgt_node else None
edge = dict(le) edge = dict(le)
edge["data"] = FlowGraphEdgeDataSchema( edge["data"] = WorkflowGraphEdgeDataSchema(
enabled=edge_row.enabled, enabled=edge_row.enabled,
transfer_mode=edge_row.transfer_mode, transfer_mode=edge_row.transfer_mode,
multipart_part_size=edge_row.multipart_part_size, multipart_part_size=edge_row.multipart_part_size,
@@ -292,8 +292,8 @@ class WorkflowFlowService:
# ── 查询 ──────────────────────────────────────────────────────── # ── 查询 ────────────────────────────────────────────────────────
async def _to_out(self, obj: WorkflowFlowModel) -> WorkflowFlowOutSchema: async def _to_out(self, obj: WorkflowModel) -> WorkflowOutSchema:
out = WorkflowFlowOutSchema.model_validate(obj) out = WorkflowOutSchema.model_validate(obj)
node_rows, edge_rows = await self._load_flow_graph(obj.id) node_rows, edge_rows = await self._load_flow_graph(obj.id)
out.graph_stats = {"node_count": len(node_rows), "edge_count": len(edge_rows)} out.graph_stats = {"node_count": len(node_rows), "edge_count": len(edge_rows)}
if edge_rows: if edge_rows:
@@ -304,24 +304,24 @@ class WorkflowFlowService:
out.graph = await self._build_graph(obj, node_rows, edge_rows) out.graph = await self._build_graph(obj, node_rows, edge_rows)
return out return out
async def _to_out_list(self, objs: Sequence[WorkflowFlowModel]) -> list[WorkflowFlowOutSchema]: async def _to_out_list(self, objs: Sequence[WorkflowModel]) -> list[WorkflowOutSchema]:
"""批量组装列表概览:源/目标列表、画布统计,一次查询避免 N+1。""" """批量组装列表概览:源/目标列表、画布统计,一次查询避免 N+1。"""
flow_ids = [o.id for o in objs] flow_ids = [o.id for o in objs]
nodes_by_flow: dict[int, list[WorkflowFlowNodeModel]] = defaultdict(list) nodes_by_flow: dict[int, list[WorkflowNodeModel]] = defaultdict(list)
edges_by_flow: dict[int, list[WorkflowFlowEdgeModel]] = defaultdict(list) edges_by_flow: dict[int, list[WorkflowEdgeModel]] = defaultdict(list)
if flow_ids: if flow_ids:
result = await self.db.execute( result = await self.db.execute(
select(WorkflowFlowNodeModel).where(WorkflowFlowNodeModel.flow_id.in_(flow_ids)) select(WorkflowNodeModel).where(WorkflowNodeModel.flow_id.in_(flow_ids))
) )
for n in result.scalars().all(): for n in result.scalars().all():
nodes_by_flow[n.flow_id].append(n) nodes_by_flow[n.flow_id].append(n)
result = await self.db.execute( result = await self.db.execute(
select(WorkflowFlowEdgeModel).where(WorkflowFlowEdgeModel.flow_id.in_(flow_ids)) select(WorkflowEdgeModel).where(WorkflowEdgeModel.flow_id.in_(flow_ids))
) )
for e in result.scalars().all(): for e in result.scalars().all():
edges_by_flow[e.flow_id].append(e) edges_by_flow[e.flow_id].append(e)
outs = [WorkflowFlowOutSchema.model_validate(o) for o in objs] outs = [WorkflowOutSchema.model_validate(o) for o in objs]
for obj, out in zip(objs, outs, strict=False): for obj, out in zip(objs, outs, strict=False):
out.graph = None out.graph = None
ns = nodes_by_flow.get(obj.id, []) ns = nodes_by_flow.get(obj.id, [])
@@ -333,17 +333,17 @@ class WorkflowFlowService:
out.targets = self._targets_from(es, node_map) out.targets = self._targets_from(es, node_map)
return outs return outs
async def detail(self, id: int) -> WorkflowFlowOutSchema: async def detail(self, id: int) -> WorkflowOutSchema:
obj = await self._crud().get_or_404(id=id) obj = await self._crud().get_or_404(id=id)
return await self._to_out(obj) return await self._to_out(obj)
async def page( async def page(
self, self,
search: WorkflowFlowQueryParam | None, search: WorkflowQueryParam | None,
page_no: int, page_no: int,
page_size: int, page_size: int,
order_by: list[dict] | None = None, order_by: list[dict] | None = None,
) -> PageResultSchema[WorkflowFlowOutSchema]: ) -> PageResultSchema[WorkflowOutSchema]:
result = await self._crud().page( result = await self._crud().page(
offset=(page_no - 1) * page_size, offset=(page_no - 1) * page_size,
limit=page_size, limit=page_size,
@@ -351,7 +351,7 @@ class WorkflowFlowService:
search=search_to_dict(search), search=search_to_dict(search),
) )
items = await self._to_out_list(result.items) items = await self._to_out_list(result.items)
return PageResultSchema[WorkflowFlowOutSchema]( return PageResultSchema[WorkflowOutSchema](
page_no=result.page_no, page_no=result.page_no,
page_size=result.page_size, page_size=result.page_size,
total=result.total, total=result.total,
@@ -359,15 +359,15 @@ class WorkflowFlowService:
items=items, items=items,
) )
async def get_list(self, search: WorkflowFlowQueryParam | None = None) -> list[WorkflowFlowOutSchema]: async def get_list(self, search: WorkflowQueryParam | None = None) -> list[WorkflowOutSchema]:
objs = await self._crud().get_list(search=search_to_dict(search), order_by=[{"id": "asc"}]) objs = await self._crud().get_list(search=search_to_dict(search), order_by=[{"id": "asc"}])
return await self._to_out_list(objs) return await self._to_out_list(objs)
# ── 写入 ──────────────────────────────────────────────────────── # ── 写入 ────────────────────────────────────────────────────────
async def _prepare_and_save( async def _prepare_and_save(
self, flow_id: int | None, data: WorkflowFlowCreateSchema | WorkflowFlowUpdateSchema self, flow_id: int | None, data: WorkflowCreateSchema | WorkflowUpdateSchema
) -> tuple[dict, list[FlowNodeSchema], list[FlowEdgeSchema]]: ) -> tuple[dict, list[WorkflowNodeSchema], list[WorkflowEdgeSchema]]:
"""拆分画布:业务配置写入明细表,布局存入 flow.graph。""" """拆分画布:业务配置写入明细表,布局存入 flow.graph。"""
data_dict = data.model_dump(exclude_unset=True, exclude_none=True) data_dict = data.model_dump(exclude_unset=True, exclude_none=True)
graph = data_dict.pop("graph", None) graph = data_dict.pop("graph", None)
@@ -380,7 +380,7 @@ class WorkflowFlowService:
await self._save_graph(flow_id, split.nodes, split.edges) await self._save_graph(flow_id, split.nodes, split.edges)
return data_dict, split.nodes, split.edges return data_dict, split.nodes, split.edges
async def create(self, data: WorkflowFlowCreateSchema) -> WorkflowFlowOutSchema: async def create(self, data: WorkflowCreateSchema) -> WorkflowOutSchema:
exist = await self._crud().get(name=data.name) exist = await self._crud().get(name=data.name)
if exist: if exist:
raise CustomException(msg="创建失败,流程名称已存在") raise CustomException(msg="创建失败,流程名称已存在")
@@ -389,7 +389,7 @@ class WorkflowFlowService:
await self._save_graph(obj.id, node_items, edge_items) await self._save_graph(obj.id, node_items, edge_items)
return await self._to_out(obj) return await self._to_out(obj)
async def update(self, id: int, data: WorkflowFlowUpdateSchema) -> WorkflowFlowOutSchema: async def update(self, id: int, data: WorkflowUpdateSchema) -> WorkflowOutSchema:
await self._crud().get_or_404(id=id, msg="更新失败,该流程不存在") await self._crud().get_or_404(id=id, msg="更新失败,该流程不存在")
exist = await self._crud().get(name=data.name) exist = await self._crud().get(name=data.name)
if exist and exist.id != id: if exist and exist.id != id:
@@ -405,8 +405,8 @@ class WorkflowFlowService:
raise CustomException(msg="删除失败,删除对象不能为空") raise CustomException(msg="删除失败,删除对象不能为空")
await self._crud().delete(ids=ids) await self._crud().delete(ids=ids)
# 子表随父流程一并物理清理(无软删消费场景) # 子表随父流程一并物理清理(无软删消费场景)
await WorkflowFlowNodeCRUD(self.auth, self.db).hard_delete_by_flow_ids(ids) await WorkflowNodeCRUD(self.auth, self.db).hard_delete_by_flow_ids(ids)
await WorkflowFlowEdgeCRUD(self.auth, self.db).hard_delete_by_flow_ids(ids) await WorkflowEdgeCRUD(self.auth, self.db).hard_delete_by_flow_ids(ids)
# ── 执行 ──────────────────────────────────────────────────────── # ── 执行 ────────────────────────────────────────────────────────
@@ -427,7 +427,7 @@ class WorkflowFlowService:
source_paths = source_paths or {} source_paths = source_paths or {}
# 阶段一:解析并校验每条连线 # 阶段一:解析并校验每条连线
plans: list[FlowTransferPlanSchema] = [] plans: list[WorkflowTransferPlanSchema] = []
for e in enabled_edges: for e in enabled_edges:
src_node = nodes_by_key.get(e.source_node_key) src_node = nodes_by_key.get(e.source_node_key)
tgt_node = nodes_by_key.get(e.target_node_key) tgt_node = nodes_by_key.get(e.target_node_key)
@@ -446,7 +446,7 @@ class WorkflowFlowService:
missing = "未选择源文件/目录" if not src_path else "目标节点未配置默认目录" missing = "未选择源文件/目录" if not src_path else "目标节点未配置默认目录"
raise CustomException(msg=f"执行失败,连线 {edge_label} {missing}") raise CustomException(msg=f"执行失败,连线 {edge_label} {missing}")
plans.append( plans.append(
FlowTransferPlanSchema( WorkflowTransferPlanSchema(
src_id=src_id, src_id=src_id,
tgt_id=tgt_id, tgt_id=tgt_id,
src_path=src_path, src_path=src_path,
@@ -458,7 +458,7 @@ class WorkflowFlowService:
) )
# 阶段二:统一校验所有涉及的存储源可用,再逐个生成传输任务 # 阶段二:统一校验所有涉及的存储源可用,再逐个生成传输任务
node_service = StorageSourceService(self.auth, self.db) node_service = StorageNodeService(self.auth, self.db)
sources = await node_service.get_active_sources( sources = await node_service.get_active_sources(
[p.src_id for p in plans] + [p.tgt_id for p in plans] [p.src_id for p in plans] + [p.tgt_id for p in plans]
) )
-10
View File
@@ -1,10 +0,0 @@
"""工作流模块(存储与文件流转):
- ``source``: 存储源管理(OSS/COS/OBS/S3/SFTP/FTP 等连接配置)
- ``core``: 对象存储协议适配器与工厂
- ``storage``: 存储文件浏览
- ``transfer``: 传输任务引擎
- ``flow``: 工作流定义(画布 CRUD、发布、执行 API)
路由统一挂在 ``/workflow`` 下(见 ``app/api/v1/workflow.py``)。
"""
-8
View File
@@ -1,8 +0,0 @@
# 见 docs/PLUGIN_ARCHITECTURE.md
name = "workflow"
title = "工作流"
version = "1.0.0"
description = "存储源管理、对象存储适配(OSS/COS/OBS/S3/SFTP/FTP)、文件浏览、传输任务与流程编排。"
optional = true
tags = ["workflow", "storage", "transfer"]
@@ -1,14 +0,0 @@
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_crud import CRUDBase
from app.core.base_schema import AuthSchema
from .model import StorageSourceModel
from .schema import StorageSourceCreateSchema, StorageSourceUpdateSchema
class StorageSourceCRUD(CRUDBase[StorageSourceModel, StorageSourceCreateSchema, StorageSourceUpdateSchema]):
"""存储源模块数据层"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(model=StorageSourceModel, auth=auth, db=db)
+15 -20
View File
@@ -5,26 +5,24 @@ import typer
import uvicorn import uvicorn
from alembic import command from alembic import command
from alembic.config import Config from alembic.config import Config
from fastapi import FastAPI
from typer.main import Typer from typer.main import Typer
from app.common.enums import EnvironmentEnum from app.common.enums import EnvironmentEnum
from app.config.setting import settings
from app.init_app import create_app from app.init_app import create_app
from app.utils.banner import worship
app = create_app()
fastapiadmin_cli: Typer = typer.Typer() fastapiadmin_cli: Typer = typer.Typer()
alembic_cfg: Config = Config(file_="alembic.ini") alembic_cfg: Config = Config(file_="alembic.ini")
app: FastAPI = create_app()
@fastapiadmin_cli.command( @fastapiadmin_cli.command(
name="run", name="run",
help="启动 FastapiAdmin 服务, 运行 uv run main.py run --env=dev 不加参数默认 dev 环境", help="启动 FastapiAdmin 服务, 运行 python(或uv run) main.py run --env=dev 不加参数默认 dev 环境",
) )
def run( def run(
env: Annotated[EnvironmentEnum, typer.Option("--env", help="运行环境 (dev, prod)")] = EnvironmentEnum.DEV, env: Annotated[EnvironmentEnum, typer.Option("--env", help="运行环境 (dev, prod)")] = EnvironmentEnum.DEV,
) -> None: ) -> None:
"""按指定环境加载配置并启动 Uvicorn(开发环境开启 reload)。 """按指定环境加载配置并启动 Uvicorn(dev 环境 DEBUG=True 自动开启 reload)。
参数: 参数:
- env (EnvironmentEnum): 运行环境,对应 `--env`。 - env (EnvironmentEnum): 运行环境,对应 `--env`。
@@ -32,24 +30,21 @@ def run(
返回: 返回:
- None - None
""" """
# 设置环境变量:本进程的 settings 已在模块导入时固化,此处由 uvicorn 子进程继承
# ENVIRONMENT 后重新 import main,使 --env 真正生效(revision/upgrade 另用 cache_clear 处理)
os.environ["ENVIRONMENT"] = env.value os.environ["ENVIRONMENT"] = env.value
typer.secho( from app.utils.banner import worship
message=f"{worship()}", typer.secho(message=f"{worship()}", fg=typer.colors.GREEN)
fg=typer.colors.GREEN,
) from app.config.setting import get_settings, settings
get_settings.cache_clear()
# 启动uvicorn服务(传 import string 而非实例:reload/多 worker 模式要求子进程重新 import,
# 且 os.environ 的环境设置由子进程继承后,settings 才能按 --env 正确加载)
uvicorn.run( uvicorn.run(
app=app, app="main:app",
host=settings.SERVER_HOST, host=settings.SERVER_HOST,
port=settings.SERVER_PORT, port=settings.SERVER_PORT,
reload=env.value == EnvironmentEnum.DEV.value, reload=settings.DEBUG,
workers=settings.WORKERS if env.value == EnvironmentEnum.PROD.value else 1, workers=settings.WORKERS,
factory=True,
log_config=None, log_config=None,
timeout_graceful_shutdown=5, timeout_graceful_shutdown=5,
) )
@@ -57,7 +52,7 @@ def run(
@fastapiadmin_cli.command( @fastapiadmin_cli.command(
name="revision", name="revision",
help="生成新的 Alembic 迁移脚本, 运行 python main.py revision --env=dev", help="生成新的 Alembic 迁移脚本, 运行 python(或uv run) main.py revision --env=dev",
) )
def revision( def revision(
env: Annotated[EnvironmentEnum, typer.Option("--env", help="运行环境 (dev, prod)")] = EnvironmentEnum.DEV, env: Annotated[EnvironmentEnum, typer.Option("--env", help="运行环境 (dev, prod)")] = EnvironmentEnum.DEV,
@@ -80,7 +75,7 @@ def revision(
@fastapiadmin_cli.command( @fastapiadmin_cli.command(
name="upgrade", name="upgrade",
help="应用最新的 Alembic 迁移, 运行 python main.py upgrade --env=dev", help="应用最新的 Alembic 迁移, 运行 python(或uv run) main.py upgrade --env=dev",
) )
def upgrade( def upgrade(
env: Annotated[EnvironmentEnum, typer.Option("--env", help="运行环境 (dev, prod)")] = EnvironmentEnum.DEV, env: Annotated[EnvironmentEnum, typer.Option("--env", help="运行环境 (dev, prod)")] = EnvironmentEnum.DEV,
+3938 -3911
View File
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
import { request } from "@utils"; import { request } from "@utils";
const API_PATH = "/workflow/storage"; const API_PATH = "/task/storage/browse";
const StorageAPI = { const StorageAPI = {
/** 查询文件/目录列表。传 page_no/page_size 时分页返回(PageResult),否则返回全量数组。 */ /** 查询文件/目录列表。传 page_no/page_size 时分页返回(PageResult),否则返回全量数组。 */
@@ -1,6 +1,6 @@
import { request } from "@utils"; import { request } from "@utils";
const API_PATH = "/workflow/source"; const API_PATH = "/task/storage/node";
const NodeAPI = { const NodeAPI = {
pageNode(query?: TablePageQuery) { pageNode(query?: TablePageQuery) {
@@ -1,7 +1,7 @@
import { request } from "@utils"; import { request } from "@utils";
import { createSSEClient, httpEndpoint, type SSEClient } from "@utils/sse"; import { createSSEClient, httpEndpoint, type SSEClient } from "@utils/sse";
const API_PATH = "/workflow/transfer"; const API_PATH = "/task/storage/transfer";
const TransferAPI = { const TransferAPI = {
/** 创建传输任务(远端源,JSON) */ /** 创建传输任务(远端源,JSON) */
@@ -152,7 +152,7 @@ export class TransferStream {
connect() { connect() {
if (this.client) return; if (this.client) return;
const url = new URL( const url = new URL(
"/api/v1/task/workflow/transfer/stream", "/api/v1/task/storage/transfer/stream",
httpEndpoint(import.meta.env.VITE_APP_WS_ENDPOINT) httpEndpoint(import.meta.env.VITE_APP_WS_ENDPOINT)
); );
this.client = createSSEClient({ this.client = createSSEClient({
@@ -1,6 +1,6 @@
import { request } from "@utils"; import { request } from "@utils";
const API_PATH = "/workflow/flow"; const API_PATH = "/task/storage/workflow";
const FlowAPI = { const FlowAPI = {
pageFlow(query?: FlowPageQuery) { pageFlow(query?: FlowPageQuery) {
@@ -182,30 +182,30 @@ export const MANUAL_MODULES_AFTER_SYSTEM: ManualModuleSection[] = [
}, },
{ {
anchor: "page-workflow", anchor: "page-workflow",
title: "传输流程", title: "流程管理",
path: "module_task/workflow/flow/index.vue", path: "module_storage/workflow/index.vue",
notes: [ notes: [
"检索:FaSearchBar(可展开)。", "检索:FaSearchBar(可展开)。",
"工具栏:新增、批量删除、刷新、列配置。", "工具栏:新增、批量删除、刷新、列配置。",
"流程定义:源节点 + 目标节点列表(parallel 多目标 / chain 链式),目标路径随行配置。", "流程定义:源节点 + 目标节点列表(parallel 多目标 / chain 链式),目标路径随行配置。",
"权限:`module_task:workflow:flow:create|delete|update` + 行级操作。", "权限:`module_storage:workflow:flow:create|delete|update` + 行级操作。",
], ],
}, },
{ {
anchor: "page-workflow-node", anchor: "page-workflow-node",
title: "节点管理", title: "节点管理",
path: "module_task/workflow/node/index.vue", path: "module_storage/node/index.vue",
notes: [ notes: [
"检索:FaSearchBar(可展开)。", "检索:FaSearchBar(可展开)。",
"节点即传输地址(存储源):FTP/FTPS/SFTP/S3/OBS/OSS/COS/本地目录。", "节点即传输地址(存储源):FTP/FTPS/SFTP/S3/OBS/OSS/COS/本地目录。",
"行操作:打开(进入存储管理)、测试连接、详情、编辑、删除。", "行操作:打开(进入存储管理)、测试连接、详情、编辑、删除。",
"权限:`module_task:workflow:node:create|delete|update` + 行级操作。", "权限:`module_storage:workflow:node:create|delete|update` + 行级操作。",
], ],
}, },
{ {
anchor: "page-workflow-storage", anchor: "page-workflow-storage",
title: "存储管理", title: "文件浏览",
path: "module_task/workflow/storage/index.vue", path: "module_storage/browse/index.vue",
notes: [ notes: [
"多节点标签浏览各节点内部文件(类似 IDE 标签栏)。", "多节点标签浏览各节点内部文件(类似 IDE 标签栏)。",
"文件操作:上传、新建目录、下载、复制/移动、重命名、分享、删除。", "文件操作:上传、新建目录、下载、复制/移动、重命名、分享、删除。",
@@ -215,7 +215,7 @@ export const MANUAL_MODULES_AFTER_SYSTEM: ManualModuleSection[] = [
{ {
anchor: "page-workflow-transfer", anchor: "page-workflow-transfer",
title: "传输任务", title: "传输任务",
path: "module_task/workflow/transfer/index.vue", path: "module_storage/transfer/index.vue",
notes: [ notes: [
"创建传输任务:可选「从流程自动填充」源节点与多目标,或手动指定。", "创建传输任务:可选「从流程自动填充」源节点与多目标,或手动指定。",
"实时进度:WebSocket 推送任务/步骤状态与速度。", "实时进度:WebSocket 推送任务/步骤状态与速度。",
@@ -1,4 +1,4 @@
<!-- 文件浏览:多节点标签 + 上传/下载/新建目录/复制/移动/重命名/分享/删除(源项目风格,对接 /task/workflow/storage) --> <!-- 文件浏览:多节点标签 + 上传/下载/新建目录/复制/移动/重命名/分享/删除(源项目风格,对接 /storage/browse) -->
<template> <template>
<div class="flex h-full"> <div class="flex h-full">
<el-card shadow="never" class="browse-card flex min-h-0 flex-1 flex-col"> <el-card shadow="never" class="browse-card flex min-h-0 flex-1 flex-col">
@@ -605,14 +605,14 @@ import {
Share, Share,
Upload, Upload,
} from "@element-plus/icons-vue"; } from "@element-plus/icons-vue";
import StorageAPI, { type StorageObject } from "@/api/module_task/workflow/storage"; import StorageAPI, { type StorageObject } from "@/api/module_storage/browse";
import NodeAPI, { type SourceTable } from "@/api/module_task/workflow/node"; import NodeAPI, { type SourceTable } from "@/api/module_storage/node";
import TransferAPI from "@/api/module_task/workflow/transfer"; import TransferAPI from "@/api/module_storage/transfer";
import type { ColumnOption } from "@/types/component"; import type { ColumnOption } from "@/types/component";
import type { FormItem } from "@/components/forms/fa-form/index.vue"; import type { FormItem } from "@/components/forms/fa-form/index.vue";
defineOptions({ defineOptions({
name: "WorkflowStorage", name: "WorkflowBrowse",
inheritAttrs: false, inheritAttrs: false,
}); });
@@ -27,8 +27,8 @@
<template #left> <template #left>
<FaTableHeaderLeft <FaTableHeaderLeft
:remove-ids="selectedIds" :remove-ids="selectedIds"
:perm-create="['module_task:workflow:node:create']" :perm-create="['module_storage:workflow:node:create']"
:perm-delete="['module_task:workflow:node:delete']" :perm-delete="['module_storage:workflow:node:delete']"
:delete-loading="batchDeleting" :delete-loading="batchDeleting"
:create-loading="createLoading" :create-loading="createLoading"
@add="handleAdd" @add="handleAdd"
@@ -194,7 +194,7 @@ import NodeAPI, {
type AdvancedFieldDef, type AdvancedFieldDef,
type SourceForm, type SourceForm,
type SourceTable, type SourceTable,
} from "@/api/module_task/workflow/node"; } from "@/api/module_storage/node";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"; import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue"; import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue"; import type { FormItem } from "@/components/forms/fa-form/index.vue";
@@ -347,14 +347,14 @@ function buildSourceRowActions(
artType: "view", artType: "view",
icon: "ri:link", icon: "ri:link",
iconColor: "var(--el-color-primary)", iconColor: "var(--el-color-primary)",
perm: "module_task:workflow:node:query", perm: "module_storage:workflow:node:query",
run: () => ctx.onTest(row.id!), run: () => ctx.onTest(row.id!),
}, },
{ {
key: "detail", key: "detail",
label: "详情", label: "详情",
artType: "view", artType: "view",
perm: "module_task:workflow:node:query", perm: "module_storage:workflow:node:query",
run: () => ctx.onDetail(row.id!), run: () => ctx.onDetail(row.id!),
}, },
{ {
@@ -362,7 +362,7 @@ function buildSourceRowActions(
label: "编辑", label: "编辑",
artType: "edit", artType: "edit",
icon: "ri:edit-2-line", icon: "ri:edit-2-line",
perm: "module_task:workflow:node:update", perm: "module_storage:workflow:node:update",
run: () => ctx.onEdit(row.id!), run: () => ctx.onEdit(row.id!),
}, },
{ {
@@ -370,7 +370,7 @@ function buildSourceRowActions(
label: "删除", label: "删除",
artType: "delete", artType: "delete",
icon: "ri:delete-bin-4-line", icon: "ri:delete-bin-4-line",
perm: "module_task:workflow:node:delete", perm: "module_storage:workflow:node:delete",
run: () => ctx.onDelete(row.id!, row.name ?? ""), run: () => ctx.onDelete(row.id!, row.name ?? ""),
}, },
]; ];
@@ -458,7 +458,7 @@ async function handleTest(id: number) {
// 打开浏览:跳转到文件管理并携带存储源信息 // 打开浏览:跳转到文件管理并携带存储源信息
function handleOpen(row: SourceTable) { function handleOpen(row: SourceTable) {
router.push({ router.push({
path: "/task/workflow/storage", path: "/task/storage/workflow/browse",
query: { source_id: row.id, name: row.name, protocol: row.protocol }, query: { source_id: row.id, name: row.name, protocol: row.protocol },
}); });
} }
@@ -367,9 +367,9 @@ import TransferAPI, {
type TransferStatus, type TransferStatus,
type TransferTaskItem, type TransferTaskItem,
type TransferTaskType, type TransferTaskType,
} from "@/api/module_task/workflow/transfer"; } from "@/api/module_storage/transfer";
import NodeAPI, { type SourceTable } from "@/api/module_task/workflow/node"; import NodeAPI, { type SourceTable } from "@/api/module_storage/node";
import FlowAPI, { type FlowTable } from "@/api/module_task/workflow/flow"; import FlowAPI, { type FlowTable } from "@/api/module_storage/workflow";
import FaDescriptions, { import FaDescriptions, {
type DescriptionsItem, type DescriptionsItem,
} from "@/components/display/fa-descriptions/index.vue"; } from "@/components/display/fa-descriptions/index.vue";
@@ -83,7 +83,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Document, Folder, Refresh } from "@element-plus/icons-vue"; import { Document, Folder, Refresh } from "@element-plus/icons-vue";
import StorageAPI, { type StorageObject } from "@/api/module_task/workflow/storage"; import StorageAPI, { type StorageObject } from "@/api/module_storage/browse";
interface Props { interface Props {
visible?: boolean; visible?: boolean;
@@ -84,12 +84,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { Close, Folder, FolderOpened } from "@element-plus/icons-vue"; import { Close, Folder, FolderOpened } from "@element-plus/icons-vue";
import NodeAPI from "@/api/module_task/workflow/node"; import NodeAPI from "@/api/module_storage/node.ts";
import { import {
protocolColor, protocolColor,
protocolLabel as getProtocolLabel, protocolLabel as getProtocolLabel,
protocolText as getProtocolText, protocolText as getProtocolText,
} from "./protocol"; } from "./protocol.ts";
import FaFileBrowserDialog from "./FaFileBrowserDialog.vue"; import FaFileBrowserDialog from "./FaFileBrowserDialog.vue";
interface Props { interface Props {
@@ -256,11 +256,11 @@ import FaDynamicEdge from "./FaDynamicEdge.vue";
import FaStorageNodePanel from "./FaStorageNodePanel.vue"; import FaStorageNodePanel from "./FaStorageNodePanel.vue";
import FaEdgeConfigPanel from "./FaEdgeConfigPanel.vue"; import FaEdgeConfigPanel from "./FaEdgeConfigPanel.vue";
import FaFileBrowserDialog from "./FaFileBrowserDialog.vue"; import FaFileBrowserDialog from "./FaFileBrowserDialog.vue";
import { protocolColor, protocolText } from "./protocol"; import { protocolColor, protocolText } from "./protocol.ts";
import type { FormItem } from "@/components/forms/fa-form/index.vue"; import type { FormItem } from "@/components/forms/fa-form/index.vue";
import type FaForm from "@/components/forms/fa-form/index.vue"; import type FaForm from "@/components/forms/fa-form/index.vue";
import FlowAPI, { type FlowForm, type FlowTable } from "@/api/module_task/workflow/flow"; import FlowAPI, { type FlowForm, type FlowTable } from "@/api/module_storage/workflow.ts";
import NodeAPI, { type SourceTable } from "@/api/module_task/workflow/node"; import NodeAPI, { type SourceTable } from "@/api/module_storage/node.ts";
defineOptions({ defineOptions({
name: "WorkflowFlowDesignDrawer", name: "WorkflowFlowDesignDrawer",
@@ -27,8 +27,8 @@
<template #left> <template #left>
<FaTableHeaderLeft <FaTableHeaderLeft
:remove-ids="selectedIds" :remove-ids="selectedIds"
:perm-create="['module_task:workflow:flow:create']" :perm-create="['module_storage:workflow:flow:create']"
:perm-delete="['module_task:workflow:flow:delete']" :perm-delete="['module_storage:workflow:flow:delete']"
:delete-loading="batchDeleting" :delete-loading="batchDeleting"
:create-loading="createLoading" :create-loading="createLoading"
@add="handleAdd" @add="handleAdd"
@@ -174,17 +174,17 @@
import { ElButton, ElTag, ElTooltip } from "element-plus"; import { ElButton, ElTag, ElTooltip } from "element-plus";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { resolveStatusColumns, renderTableOperationCell, type TableOperationAction } from "@utils"; import { resolveStatusColumns, renderTableOperationCell, type TableOperationAction } from "@utils";
import FlowAPI, { type FlowTable } from "@/api/module_task/workflow/flow"; import FlowAPI, { type FlowTable } from "@/api/module_storage/workflow.ts";
import NodeAPI, { type SourceTable } from "@/api/module_task/workflow/node"; import NodeAPI, { type SourceTable } from "@/api/module_storage/node.ts";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"; import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue"; import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import FaTableHeader from "@/components/tables/fa-table-header/index.vue"; import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
import FaWorkflowDesignDrawer from "./components/FaWorkflowDesignDrawer.vue"; import FaWorkflowDesignDrawer from "./components/FaWorkflowDesignDrawer.vue";
import FaFileBrowserDialog from "./components/FaFileBrowserDialog.vue"; import FaFileBrowserDialog from "./components/FaFileBrowserDialog.vue";
import { protocolColor, protocolLabel } from "./components/protocol"; import { protocolColor, protocolLabel } from "./components/protocol.ts";
defineOptions({ defineOptions({
name: "WorkflowFlow", name: "Workflow",
inheritAttrs: false, inheritAttrs: false,
}); });
@@ -358,7 +358,7 @@ function buildFlowRowActions(
key: "detail", key: "detail",
label: "查看", label: "查看",
artType: "view", artType: "view",
perm: "module_task:workflow:flow:query", perm: "module_storage:workflow:flow:query",
run: () => ctx.onDetail(row.id!), run: () => ctx.onDetail(row.id!),
}, },
{ {
@@ -366,7 +366,7 @@ function buildFlowRowActions(
label: "设计", label: "设计",
artType: "edit", artType: "edit",
icon: "ri:pen-nib-line", icon: "ri:pen-nib-line",
perm: "module_task:workflow:flow:update", perm: "module_storage:workflow:flow:update",
run: () => ctx.onEdit(row.id!), run: () => ctx.onEdit(row.id!),
}, },
{ {
@@ -375,7 +375,7 @@ function buildFlowRowActions(
artType: "view", artType: "view",
icon: "ri:play-line", icon: "ri:play-line",
iconColor: "var(--el-color-success)", iconColor: "var(--el-color-success)",
perm: "module_task:workflow:transfer:create", perm: "module_storage:workflow:transfer:create",
run: () => ctx.onExecute(row), run: () => ctx.onExecute(row),
}, },
{ {
@@ -383,7 +383,7 @@ function buildFlowRowActions(
label: "删除", label: "删除",
artType: "delete", artType: "delete",
icon: "ri:delete-bin-4-line", icon: "ri:delete-bin-4-line",
perm: "module_task:workflow:flow:delete", perm: "module_storage:workflow:flow:delete",
run: () => ctx.onDelete(row.id!, row.name ?? ""), run: () => ctx.onDelete(row.id!, row.name ?? ""),
}, },
]; ];
@@ -490,7 +490,7 @@ async function handleExecuteConfirm() {
type: "primary", type: "primary",
size: "small", size: "small",
text: true, text: true,
onClick: () => void router.push("/task/workflow/transfer"), onClick: () => void router.push("/task/storage/workflow/transfer"),
}, },
() => "查看任务" () => "查看任务"
), ),