mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-13 20:51:09 +00:00
fix: 修复多项后端缺陷并加固安全与限流
- 修正 AI 会话表初始化及异步数据库 URI 处理 - 新增全局 API 与 WS 聊天限流,健康检查豁免 - 修复忘记密码接口防用户枚举与参数校验 - 修复 SSE 推送数据重复编码问题 - 修复 Cron 表达式 Quartz 问号兼容 - 修复存储模块权限标识前缀 - 新增过期日志清理与任务错误中文提示
This commit is contained in:
@@ -54,3 +54,5 @@ LICENSE
|
|||||||
|
|
||||||
# 数据库迁移(构建镜像时不包含,启动时动态执行)
|
# 数据库迁移(构建镜像时不包含,启动时动态执行)
|
||||||
# 取消注释以加速构建:backend/alembic/versions
|
# 取消注释以加速构建:backend/alembic/versions
|
||||||
|
# 用户上传文件(本地存储源根目录),不进镜像
|
||||||
|
backend/static
|
||||||
|
|||||||
@@ -22,12 +22,15 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
|
|||||||
from app.core.ap_scheduler import SchedulerUtil
|
from app.core.ap_scheduler import SchedulerUtil
|
||||||
from app.core.database import async_engine
|
from app.core.database import async_engine
|
||||||
from app.core.redis_crud import redis_connect
|
from app.core.redis_crud import redis_connect
|
||||||
|
from app.modules.ai.chat.crud import init_agno_tables
|
||||||
from app.modules.system.dict.service import DictDataService
|
from app.modules.system.dict.service import DictDataService
|
||||||
from app.modules.system.params.service import ParamsService
|
from app.modules.system.params.service import ParamsService
|
||||||
from app.scripts.initialize import InitializeData
|
from app.scripts.initialize import InitializeData
|
||||||
|
|
||||||
await InitializeData().init_db()
|
await InitializeData().init_db()
|
||||||
logger.info(f"✅ {settings.DATABASE_TYPE} 连接初始化完成")
|
logger.info(f"✅ {settings.DATABASE_TYPE} 连接初始化完成")
|
||||||
|
await init_agno_tables()
|
||||||
|
logger.info("✅ AI 会话表 初始化完成")
|
||||||
await redis_connect(app, status=True)
|
await redis_connect(app, status=True)
|
||||||
logger.info("✅ Redis 连接初始化完成")
|
logger.info("✅ Redis 连接初始化完成")
|
||||||
await ParamsService.init_cache(redis=app.state.redis)
|
await ParamsService.init_cache(redis=app.state.redis)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
"""v1 路由总表"""
|
"""v1 路由总表"""
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from app.core.rate_limiter import api_rate_limiter
|
||||||
from app.modules.ai.chat.controller import ChatRouter
|
from app.modules.ai.chat.controller import ChatRouter
|
||||||
from app.modules.common.file.controller import FileRouter
|
from app.modules.common.file.controller import FileRouter
|
||||||
from app.modules.generator.gencode.controller import GenRouter
|
from app.modules.generator.gencode.controller import GenRouter
|
||||||
@@ -63,7 +64,7 @@ DOMAIN_CONTROLLERS: dict[str, list[APIRouter]] = {
|
|||||||
"/common": [FileRouter],
|
"/common": [FileRouter],
|
||||||
}
|
}
|
||||||
|
|
||||||
api_v1 = APIRouter()
|
api_v1 = APIRouter(dependencies=[Depends(api_rate_limiter)])
|
||||||
for prefix, controllers in DOMAIN_CONTROLLERS.items():
|
for prefix, controllers in DOMAIN_CONTROLLERS.items():
|
||||||
router = APIRouter(prefix=prefix)
|
router = APIRouter(prefix=prefix)
|
||||||
for controller in controllers:
|
for controller in controllers:
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ class Settings(BaseSettings):
|
|||||||
SESSION_MAX_LIFETIME_SECONDS: int = 60 * 60 * 24 * 7 # 会话绝对存活上限(秒),滑动续期不得超过该上限
|
SESSION_MAX_LIFETIME_SECONDS: int = 60 * 60 * 24 * 7 # 会话绝对存活上限(秒),滑动续期不得超过该上限
|
||||||
LOGIN_RATE_LIMIT_WINDOW_SECONDS: int = 60 # 登录限流窗口(秒)
|
LOGIN_RATE_LIMIT_WINDOW_SECONDS: int = 60 # 登录限流窗口(秒)
|
||||||
LOGIN_RATE_LIMIT_MAX_ATTEMPTS: int = 10 # 限流窗口内单 IP 最大登录尝试次数
|
LOGIN_RATE_LIMIT_MAX_ATTEMPTS: int = 10 # 限流窗口内单 IP 最大登录尝试次数
|
||||||
|
API_RATE_LIMIT_TIMES: int = 60 # 全局 API 限流:窗口内单 IP 单路由最大请求数
|
||||||
|
API_RATE_LIMIT_INTERVAL_MS: int = 60 * 1000 # 全局 API 限流窗口(pyrate-limiter 以毫秒计)
|
||||||
|
WS_RATE_LIMIT_TIMES: int = 1 # WS 聊天限流:窗口内最大对话消息数
|
||||||
|
WS_RATE_LIMIT_INTERVAL_MS: int = 5 * 1000 # WS 聊天限流窗口(毫秒)
|
||||||
|
|
||||||
# ================================================= #
|
# ================================================= #
|
||||||
# ****************** 数据加密配置 ***************** #
|
# ****************** 数据加密配置 ***************** #
|
||||||
|
|||||||
@@ -35,6 +35,27 @@ from app.core.logger import logger
|
|||||||
JOB_STATUS_SUCCESS = 2
|
JOB_STATUS_SUCCESS = 2
|
||||||
JOB_STATUS_FAILED = 3
|
JOB_STATUS_FAILED = 3
|
||||||
|
|
||||||
|
# 常见执行失败模式的中文结论(匹配原始异常文本;任意第三方异常无法机翻,保留原文供诊断)
|
||||||
|
_JOB_ERROR_HINTS: list[tuple[str, str]] = [
|
||||||
|
("maximum number of running instances", "任务已达最大并发实例数,本次执行被跳过(可调大节点的 max_instances)"),
|
||||||
|
("was missed by", "任务错过计划执行时间(misfire,通常因进程繁忙或重启空窗)"),
|
||||||
|
("Insufficient Balance", "AI 模型账户余额不足"),
|
||||||
|
("Payment Required", "AI 模型服务计费失败(HTTP 402)"),
|
||||||
|
("Unauthorized", "上游认证失败(HTTP 401),请检查访问令牌"),
|
||||||
|
("rate limit", "触发上游限流(HTTP 429),请稍后重试"),
|
||||||
|
("Connection", "网络连接异常,请检查目标地址与网络"),
|
||||||
|
("Timeout", "请求超时"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _humanize_job_error(detail: Any) -> str:
|
||||||
|
"""把执行异常转成"中文结论|原始信息"回显;未命中已知模式时原样返回。"""
|
||||||
|
text = str(detail)[:4000]
|
||||||
|
for pattern, hint in _JOB_ERROR_HINTS:
|
||||||
|
if pattern in text:
|
||||||
|
return f"{hint}|原始信息: {text}"[:4000]
|
||||||
|
return text
|
||||||
|
|
||||||
# 多 worker 下单实例调度锁:所有进程共享同一个 RedisJobStore,若每个进程都自行
|
# 多 worker 下单实例调度锁:所有进程共享同一个 RedisJobStore,若每个进程都自行
|
||||||
# start,同一任务会被重复调度执行。仅持有锁的进程运行调度器并周期续期,其余进程
|
# start,同一任务会被重复调度执行。仅持有锁的进程运行调度器并周期续期,其余进程
|
||||||
# 周期争抢——持有者崩溃(锁过期)后自动接管。
|
# 周期争抢——持有者崩溃(锁过期)后自动接管。
|
||||||
@@ -396,7 +417,7 @@ class SchedulerUtil:
|
|||||||
"job_name": job.name if job else None,
|
"job_name": job.name if job else None,
|
||||||
"trigger_type": "manual" if is_temp else (SchedulerUtil._get_trigger_type(job_id) if job else "manual"),
|
"trigger_type": "manual" if is_temp else (SchedulerUtil._get_trigger_type(job_id) if job else "manual"),
|
||||||
"status": status,
|
"status": status,
|
||||||
"error": str(detail)[:4000] if status == JOB_STATUS_FAILED else None,
|
"error": _humanize_job_error(detail) if status == JOB_STATUS_FAILED else None,
|
||||||
"result": None if status == JOB_STATUS_FAILED else (str(detail)[:4000] if detail is not None else None),
|
"result": None if status == JOB_STATUS_FAILED else (str(detail)[:4000] if detail is not None else None),
|
||||||
"next_run_time": str(job.next_run_time) if job and job.next_run_time else None,
|
"next_run_time": str(job.next_run_time) if job and job.next_run_time else None,
|
||||||
"job_state": SchedulerUtil._get_job_state(job) if job else None,
|
"job_state": SchedulerUtil._get_job_state(job) if job else None,
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ def setup_logger() -> None:
|
|||||||
"alembic.autogenerate",
|
"alembic.autogenerate",
|
||||||
"alembic.runtime.plugins",
|
"alembic.runtime.plugins",
|
||||||
"asyncio",
|
"asyncio",
|
||||||
|
"websockets",
|
||||||
"sqlalchemy",
|
"sqlalchemy",
|
||||||
"aiomysql",
|
"aiomysql",
|
||||||
"tzlocal",
|
"tzlocal",
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""全局 API 限流(fastapi-limiter 0.2.0 + pyrate-limiter)。
|
||||||
|
|
||||||
|
0.2.0 为破坏性重写:无 FastAPILimiter.init,直接构造 pyrate-limiter 的
|
||||||
|
Limiter(进程内内存桶,多 worker 时配额按进程独立);默认按 IP+路由路径
|
||||||
|
计数,超限抛 HTTP 429。单路由可用 @skip_limiter 豁免(如监控探针)。
|
||||||
|
|
||||||
|
ApiRateLimiter 兼容修复:0.2.0 的 RateLimiter.__call__ 遍历 app.routes
|
||||||
|
并直接访问 route.path,而 FastAPI 0.138 include_router 的惰性挂载产物
|
||||||
|
_IncludedRouter 没有 path 属性,会抛 AttributeError——改为直接使用请求
|
||||||
|
scope 中的路径与端点,语义等价(IP + 完整路径计数、支持 _skip_limiter)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from fastapi import Request, Response
|
||||||
|
from fastapi_limiter.depends import RateLimiter, WebSocketRateLimiter
|
||||||
|
from pyrate_limiter import Limiter, Rate
|
||||||
|
from starlette.websockets import WebSocket
|
||||||
|
|
||||||
|
from app.config.setting import settings
|
||||||
|
|
||||||
|
limiter = Limiter(Rate(settings.API_RATE_LIMIT_TIMES, settings.API_RATE_LIMIT_INTERVAL_MS))
|
||||||
|
|
||||||
|
|
||||||
|
class ApiRateLimiter(RateLimiter):
|
||||||
|
"""兼容 FastAPI 0.138 惰性路由挂载的限流依赖。"""
|
||||||
|
|
||||||
|
async def __call__(self, request: Request, response: Response):
|
||||||
|
endpoint = request.scope.get("endpoint")
|
||||||
|
if endpoint is not None and getattr(endpoint, "_skip_limiter", False):
|
||||||
|
return
|
||||||
|
rate_key = await self.identifier(request)
|
||||||
|
key = f"{rate_key}:{request.scope['path']}"
|
||||||
|
success = await self.limiter.try_acquire_async(key, blocking=self.blocking)
|
||||||
|
if not success:
|
||||||
|
return await self.callback(request, response)
|
||||||
|
|
||||||
|
|
||||||
|
# 挂载点:api_v1 全局 dependencies(见 app/api/v1/routers.py)
|
||||||
|
api_rate_limiter = ApiRateLimiter(limiter=limiter)
|
||||||
|
|
||||||
|
|
||||||
|
async def ws_rate_limit_callback(ws: WebSocket) -> None:
|
||||||
|
"""WS 超限回调:WebSocket 中不能抛 HTTPException,改为下发提示并断开(1008 策略违规)。"""
|
||||||
|
await ws.send_text(json.dumps({"type": "rate_limited", "message": "发送过于频繁,请稍后再试"}))
|
||||||
|
await ws.close(code=1008, reason="rate limited")
|
||||||
|
|
||||||
|
|
||||||
|
# WS 聊天限流:每条对话消息在接收处调用一次(stop 等控制指令不限流)
|
||||||
|
ws_rate_limiter = WebSocketRateLimiter(
|
||||||
|
limiter=Limiter(Rate(settings.WS_RATE_LIMIT_TIMES, settings.WS_RATE_LIMIT_INTERVAL_MS)),
|
||||||
|
callback=ws_rate_limit_callback,
|
||||||
|
)
|
||||||
@@ -11,6 +11,7 @@ from app.core.base_schema import AuthSchema, PageResultSchema, PaginationQueryPa
|
|||||||
from app.core.dependencies import AuthPermission, redis_getter, websocket_authenticate
|
from app.core.dependencies import AuthPermission, redis_getter, websocket_authenticate
|
||||||
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.core.rate_limiter import ws_rate_limiter
|
||||||
from app.core.router_class import OperationLogRoute
|
from app.core.router_class import OperationLogRoute
|
||||||
|
|
||||||
from .schema import (
|
from .schema import (
|
||||||
@@ -259,6 +260,8 @@ async def websocket_chat_controller(websocket: WebSocket) -> None:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 对话指令
|
# 对话指令
|
||||||
|
# 对话消息限流(stop 等控制指令不受限):超限时回调下发提示并断开连接
|
||||||
|
await ws_rate_limiter(websocket)
|
||||||
logger.info("收到聊天查询: session_id={}", query.session_id)
|
logger.info("收到聊天查询: session_id={}", query.session_id)
|
||||||
|
|
||||||
is_generating.set()
|
is_generating.set()
|
||||||
|
|||||||
@@ -32,18 +32,34 @@ def _get_agno_db() -> Any:
|
|||||||
with _db_lock:
|
with _db_lock:
|
||||||
if _db_instance is None:
|
if _db_instance is None:
|
||||||
db_type = settings.DATABASE_TYPE
|
db_type = settings.DATABASE_TYPE
|
||||||
db_uri = settings.DB_URI
|
# agno 异步 DB 内部走 SQLAlchemy asyncio 扩展(create_async_engine),
|
||||||
|
# 必须用异步驱动 URI;传同步的 DB_URI(pymysql/psycopg)会报 InvalidRequestError
|
||||||
|
db_uri = settings.ASYNC_DB_URI
|
||||||
if db_type == "mysql":
|
if db_type == "mysql":
|
||||||
_db_instance = AsyncMySQLDb(db_url=db_uri, db_schema=settings.DATABASE_NAME, create_schema=False)
|
_db_instance = AsyncMySQLDb(db_url=db_uri, db_schema=settings.DATABASE_NAME, create_schema=False)
|
||||||
elif db_type == "postgres":
|
elif db_type == "postgres":
|
||||||
_db_instance = AsyncPostgresDb(db_url=db_uri, db_schema="public", create_schema=False)
|
_db_instance = AsyncPostgresDb(db_url=db_uri, db_schema="public", create_schema=False)
|
||||||
elif db_type == "sqlite":
|
elif db_type == "sqlite":
|
||||||
_db_instance = AsyncSqliteDb(db_file=db_uri.replace("sqlite:///", ""))
|
_db_instance = AsyncSqliteDb(db_file=db_uri.replace("sqlite+aiosqlite:///", ""))
|
||||||
else:
|
else:
|
||||||
raise CustomException(msg=f"不支持的数据库类型: {db_type}")
|
raise CustomException(msg=f"不支持的数据库类型: {db_type}")
|
||||||
return _db_instance
|
return _db_instance
|
||||||
|
|
||||||
|
|
||||||
|
async def init_agno_tables() -> None:
|
||||||
|
"""应用启动时确保 agno 表结构存在(幂等,表已存在则跳过)。
|
||||||
|
|
||||||
|
agno 的 create_schema 仅控制是否自动建库,建表需调用方显式触发;
|
||||||
|
不初始化则首次读写会话时报 "Table has an invalid schema"。
|
||||||
|
"""
|
||||||
|
db = _get_agno_db()
|
||||||
|
try:
|
||||||
|
await db._create_all_tables()
|
||||||
|
except Exception as e:
|
||||||
|
# 多 worker 并发首启时可能同时建表,"已存在"类冲突可安全忽略
|
||||||
|
logger.warning(f"agno 表初始化告警(多为并发建表冲突,可忽略): {e}")
|
||||||
|
|
||||||
|
|
||||||
def _row_session_name(row: dict[str, Any]) -> str:
|
def _row_session_name(row: dict[str, Any]) -> str:
|
||||||
"""从 deserialize=False 的原始会话行中提取会话标题(session_data 可能是 JSON 字符串或字典)"""
|
"""从 deserialize=False 的原始会话行中提取会话标题(session_data 可能是 JSON 字符串或字典)"""
|
||||||
session_data = row.get("session_data")
|
session_data = row.get("session_data")
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ from typing import Annotated, Any
|
|||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from fastapi.sse import EventSourceResponse, ServerSentEvent
|
from fastapi.sse import EventSourceResponse, ServerSentEvent
|
||||||
|
from fastapi_limiter.decorators import skip_limiter
|
||||||
|
|
||||||
from app.common.response import ResponseSchema, SuccessResponse
|
from app.common.response import ResponseSchema, SuccessResponse
|
||||||
from app.config.setting import Settings, get_settings
|
from app.config.setting import Settings, get_settings
|
||||||
|
from app.core.logger import logger
|
||||||
from app.core.router_class import OperationLogRoute
|
from app.core.router_class import OperationLogRoute
|
||||||
|
|
||||||
from .schema import ServiceInfoOut
|
from .schema import ServiceInfoOut
|
||||||
@@ -17,6 +19,7 @@ HealthRouter = APIRouter(route_class=OperationLogRoute, prefix="/health", tags=[
|
|||||||
|
|
||||||
|
|
||||||
@HealthRouter.get("/check", summary="健康检查", response_model=ResponseSchema[ServiceInfoOut])
|
@HealthRouter.get("/check", summary="健康检查", response_model=ResponseSchema[ServiceInfoOut])
|
||||||
|
@skip_limiter # 监控探针高频调用,豁免全局限流
|
||||||
async def health_check(request: Request, settings: Annotated[Settings, Depends(get_settings)]) -> JSONResponse:
|
async def health_check(request: Request, settings: Annotated[Settings, Depends(get_settings)]) -> JSONResponse:
|
||||||
"""健康检查:实时探测 DB / Redis 网络连通状态。"""
|
"""健康检查:实时探测 DB / Redis 网络连通状态。"""
|
||||||
redis: Any | None = getattr(request.app.state, "redis", None)
|
redis: Any | None = getattr(request.app.state, "redis", None)
|
||||||
@@ -27,6 +30,7 @@ async def health_check(request: Request, settings: Annotated[Settings, Depends(g
|
|||||||
|
|
||||||
|
|
||||||
@HealthRouter.get("/stream", summary="健康检查实时流(SSE)", response_class=EventSourceResponse)
|
@HealthRouter.get("/stream", summary="健康检查实时流(SSE)", response_class=EventSourceResponse)
|
||||||
|
@skip_limiter
|
||||||
async def health_stream_controller(
|
async def health_stream_controller(
|
||||||
request: Request,
|
request: Request,
|
||||||
) -> AsyncGenerator[ServerSentEvent, None]:
|
) -> AsyncGenerator[ServerSentEvent, None]:
|
||||||
@@ -34,6 +38,11 @@ async def health_stream_controller(
|
|||||||
redis: Any | None = getattr(request.app.state, "redis", None)
|
redis: Any | None = getattr(request.app.state, "redis", None)
|
||||||
while True:
|
while True:
|
||||||
info = await HealthService.collect(redis)
|
info = await HealthService.collect(redis)
|
||||||
yield ServerSentEvent(data=info.model_dump_json())
|
# raw_data 原样发送;用 data= 会被 fastapi.sse 再 JSON 编码一次,前端需二次解析
|
||||||
|
logger.debug(
|
||||||
|
"健康检查 SSE 推送: DB={} Redis={}",
|
||||||
|
"正常" if info.db_status == 1 else "异常",
|
||||||
|
"正常" if info.redis_status == 1 else "异常",
|
||||||
|
)
|
||||||
|
yield ServerSentEvent(raw_data=info.model_dump_json())
|
||||||
await asyncio.sleep(30) # SSE 推送间隔(秒)
|
await asyncio.sleep(30) # SSE 推送间隔(秒)
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
from typing import Any
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
|
from sqlalchemy import delete
|
||||||
|
from sqlalchemy.engine import CursorResult
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
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.database import async_db_session
|
||||||
from app.core.exceptions import CustomException
|
from app.core.exceptions import CustomException
|
||||||
|
from app.core.logger import logger
|
||||||
from app.utils.common_util import search_to_dict
|
from app.utils.common_util import search_to_dict
|
||||||
from app.utils.excel_util import ExcelUtil
|
from app.utils.excel_util import ExcelUtil
|
||||||
|
|
||||||
from .crud import LoginLogCRUD, OperationLogCRUD
|
from .crud import LoginLogCRUD, OperationLogCRUD
|
||||||
|
from .model import LoginLogModel, OperationLogModel
|
||||||
from .schema import (
|
from .schema import (
|
||||||
LoginLogDetailOutSchema,
|
LoginLogDetailOutSchema,
|
||||||
LoginLogOutSchema,
|
LoginLogOutSchema,
|
||||||
@@ -123,3 +129,34 @@ class OperationLogService:
|
|||||||
"created_id": "操作用户ID",
|
"created_id": "操作用户ID",
|
||||||
}
|
}
|
||||||
return await ExcelUtil.aexport_list2excel(list_data=operation_log_list, mapping_dict=mapping_dict)
|
return await ExcelUtil.aexport_list2excel(list_data=operation_log_list, mapping_dict=mapping_dict)
|
||||||
|
|
||||||
|
|
||||||
|
async def cleanup_expired_logs(days: int = 90) -> dict:
|
||||||
|
"""按保留天数物理清理过期日志(供定时任务调用:无请求上下文,自建会话)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- days (int): 日志保留天数,默认 90 天。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 各表清理条数统计。
|
||||||
|
"""
|
||||||
|
if days <= 0:
|
||||||
|
raise ValueError("日志保留天数必须大于 0")
|
||||||
|
|
||||||
|
cutoff = datetime.now() - timedelta(days=days)
|
||||||
|
async with async_db_session() as session:
|
||||||
|
# DML 语句运行时返回 CursorResult(含 rowcount),静态类型是 Result,需 cast 收窄
|
||||||
|
op_result = cast(CursorResult, await session.execute(delete(OperationLogModel).where(OperationLogModel.created_time < cutoff)))
|
||||||
|
login_result = cast(CursorResult, await session.execute(delete(LoginLogModel).where(LoginLogModel.created_time < cutoff)))
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
# rowcount 类型为 int | None,未命中行时驱动可能返回 None,兜底为 0
|
||||||
|
stats = {
|
||||||
|
"days": days,
|
||||||
|
"operation_deleted": op_result.rowcount or 0,
|
||||||
|
"login_deleted": login_result.rowcount or 0,
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
f"日志保留清理完成(保留 {days} 天): 操作日志 {stats['operation_deleted']} 条,登录日志 {stats['login_deleted']} 条"
|
||||||
|
)
|
||||||
|
return stats
|
||||||
|
|||||||
@@ -71,15 +71,14 @@ async def reset_password_controller(
|
|||||||
return SuccessResponse(data=result_dict, msg="重置密码成功")
|
return SuccessResponse(data=result_dict, msg="重置密码成功")
|
||||||
|
|
||||||
|
|
||||||
@UserRouter.post("/password/forget", summary="忘记密码", response_model=ResponseSchema[UserOutSchema])
|
@UserRouter.post("/password/forget", summary="忘记密码", response_model=ResponseSchema[None])
|
||||||
async def forget_password_controller(
|
async def forget_password_controller(
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
data: Annotated[UserForgetPasswordSchema, Body(description="忘记密码参数")],
|
data: Annotated[UserForgetPasswordSchema, Body(description="忘记密码参数")],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
auth = AuthSchema()
|
auth = AuthSchema()
|
||||||
user_forget_password_result: UserOutSchema = await UserService(auth, db).forget_password(data=data)
|
await UserService(auth, db).forget_password(data=data)
|
||||||
logger.info(f"{data.username} 重置密码成功")
|
return SuccessResponse(msg="如果该账号存在,请联系管理员重置密码")
|
||||||
return SuccessResponse(data=user_forget_password_result, msg="重置密码成功")
|
|
||||||
|
|
||||||
|
|
||||||
@UserRouter.post("/register", summary="用户注册", response_model=ResponseSchema[UserOutSchema])
|
@UserRouter.post("/register", summary="用户注册", response_model=ResponseSchema[UserOutSchema])
|
||||||
|
|||||||
@@ -74,10 +74,11 @@ class CurrentUserUpdateSchema(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class UserForgetPasswordSchema(BaseModel):
|
class UserForgetPasswordSchema(BaseModel):
|
||||||
"""忘记密码"""
|
"""忘记密码申请。仅接受用户名,不得携带新密码(改密走管理员 reset_password)。"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="ignore")
|
||||||
|
|
||||||
username: str = Field(..., min_length=3, max_length=32, description="用户名")
|
username: str = Field(..., min_length=3, max_length=32, description="用户名")
|
||||||
new_password: str = Field(..., description=PASSWORD_FIELD_DESC)
|
|
||||||
|
|
||||||
@field_validator("username")
|
@field_validator("username")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -91,12 +92,6 @@ class UserForgetPasswordSchema(BaseModel):
|
|||||||
raise ValueError("账号需以字母开头,3-32 位,仅允许字母、数字、_ . -")
|
raise ValueError("账号需以字母开头,3-32 位,仅允许字母、数字、_ . -")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@field_validator("new_password")
|
|
||||||
@classmethod
|
|
||||||
def validate_new_password(cls, value: str):
|
|
||||||
"""校验新密码:长度与复杂度"""
|
|
||||||
return password_validator(value)
|
|
||||||
|
|
||||||
|
|
||||||
class UserChangePasswordSchema(BaseModel):
|
class UserChangePasswordSchema(BaseModel):
|
||||||
"""修改密码"""
|
"""修改密码"""
|
||||||
|
|||||||
@@ -294,16 +294,16 @@ class UserService:
|
|||||||
await UserCRUD(self.auth, self.db).change_password(id=data.id, password_hash=new_password_hash)
|
await UserCRUD(self.auth, self.db).change_password(id=data.id, password_hash=new_password_hash)
|
||||||
return await self.detail(id=data.id)
|
return await self.detail(id=data.id)
|
||||||
|
|
||||||
async def forget_password(self, data: UserForgetPasswordSchema) -> UserOutSchema:
|
async def forget_password(self, data: UserForgetPasswordSchema) -> None:
|
||||||
user = await UserCRUD(self.auth, self.db).get_or_404(username=data.username)
|
"""接受忘记密码申请,但不根据用户名直接改密。
|
||||||
if user.status == 1:
|
|
||||||
raise CustomException(msg="用户已停用")
|
|
||||||
if user.is_superuser:
|
|
||||||
raise CustomException(msg="超级管理员密码不能重置")
|
|
||||||
|
|
||||||
new_password_hash = await PwdUtil.ahash_password(password=data.new_password)
|
未登录调用方不得设置新密码。管理员请使用 reset_password。
|
||||||
await UserCRUD(self.auth, self.db).change_password(id=user.id, password_hash=new_password_hash)
|
无论账号是否存在、是否停用、是否超管,均不返回可区分信息(防用户枚举)。
|
||||||
return await self.detail(id=user.id)
|
"""
|
||||||
|
user = await UserCRUD(self.auth, self.db).get(username=data.username)
|
||||||
|
if user is not None and user.status == 0 and not user.is_superuser:
|
||||||
|
logger.info("password reset requested for username={}", data.username)
|
||||||
|
return None
|
||||||
|
|
||||||
async def register(self, data: UserRegisterSchema) -> UserOutSchema:
|
async def register(self, data: UserRegisterSchema) -> UserOutSchema:
|
||||||
"""用户注册"""
|
"""用户注册"""
|
||||||
|
|||||||
@@ -207,11 +207,12 @@ def _build_job_trigger(
|
|||||||
fields = cron_expr.split()
|
fields = cron_expr.split()
|
||||||
if len(fields) not in (6, 7):
|
if len(fields) not in (6, 7):
|
||||||
raise ValueError("无效的 Cron 表达式,格式: 秒 分 时 日 月 周 [年]")
|
raise ValueError("无效的 Cron 表达式,格式: 秒 分 时 日 月 周 [年]")
|
||||||
|
# Quartz 风格的 ? 仅表示"该位无具体值",croniter 不支持,先统一规范化为 * 再校验
|
||||||
|
parsed = [field if field != "?" else "*" for field in fields]
|
||||||
try:
|
try:
|
||||||
croniter(cron_expr)
|
croniter(" ".join(parsed))
|
||||||
except (KeyError, ValueError):
|
except (KeyError, ValueError):
|
||||||
raise ValueError(f"Cron表达式不正确: {cron_expr}")
|
raise ValueError(f"Cron表达式不正确: {cron_expr}")
|
||||||
parsed = [field if field != "?" else "*" for field in fields]
|
|
||||||
if len(fields) == 6:
|
if len(fields) == 6:
|
||||||
parsed.append("*")
|
parsed.append("*")
|
||||||
second, minute, hour, day, month, day_of_week, year = parsed
|
second, minute, hour, day, month, day_of_week, year = parsed
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ def _delete_temp_file(path: str) -> None:
|
|||||||
|
|
||||||
@StorageBrowseRouter.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_storage:browse:upload"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task: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,
|
||||||
@@ -42,7 +42,7 @@ async def upload_storage_file_controller(
|
|||||||
|
|
||||||
@StorageBrowseRouter.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_storage:browse:download"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task: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="远端文件路径")],
|
||||||
@@ -58,7 +58,7 @@ async def download_storage_file_controller(
|
|||||||
|
|
||||||
@StorageBrowseRouter.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_storage:browse:download"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task: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="远端目录路径")],
|
||||||
@@ -74,7 +74,7 @@ async def download_dir_storage_file_controller(
|
|||||||
|
|
||||||
@StorageBrowseRouter.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_storage:browse:delete"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task: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,
|
||||||
@@ -86,7 +86,7 @@ async def delete_storage_file_controller(
|
|||||||
|
|
||||||
@StorageBrowseRouter.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_storage:browse:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task: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,
|
||||||
@@ -102,7 +102,7 @@ async def list_storage_file_controller(
|
|||||||
|
|
||||||
@StorageBrowseRouter.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_storage:browse:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task: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:
|
||||||
@@ -112,7 +112,7 @@ async def list_storage_buckets_controller(
|
|||||||
|
|
||||||
@StorageBrowseRouter.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_storage:browse:update"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task: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="源文件路径")] = "",
|
||||||
@@ -134,7 +134,7 @@ async def copy_or_move_storage_file_controller(
|
|||||||
|
|
||||||
@StorageBrowseRouter.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_storage:browse:update"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task: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="原路径")] = "",
|
||||||
@@ -149,7 +149,7 @@ async def rename_storage_file_controller(
|
|||||||
|
|
||||||
@StorageBrowseRouter.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_storage:browse:update"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task: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="目录路径")] = "",
|
||||||
@@ -161,7 +161,7 @@ async def mkdir_storage_file_controller(
|
|||||||
|
|
||||||
@StorageBrowseRouter.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_storage:browse:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task: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,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ StorageNodeRouter = APIRouter(route_class=OperationLogRoute, prefix="/storage/no
|
|||||||
|
|
||||||
@StorageNodeRouter.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_storage:workflow:node:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage: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])
|
||||||
@@ -35,7 +35,7 @@ async def get_storage_protocols_controller(
|
|||||||
|
|
||||||
@StorageNodeRouter.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_storage:workflow:node:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:node:query"]))],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""返回按协议分组的 SDK 高级配置字段元数据,供前端「高级设置」面板按协议动态渲染。"""
|
"""返回按协议分组的 SDK 高级配置字段元数据,供前端「高级设置」面板按协议动态渲染。"""
|
||||||
return SuccessResponse(data=ADVANCED_FIELD_DEFS, msg="查询高级配置字段成功")
|
return SuccessResponse(data=ADVANCED_FIELD_DEFS, msg="查询高级配置字段成功")
|
||||||
@@ -43,7 +43,7 @@ async def get_advanced_fields_controller(
|
|||||||
|
|
||||||
@StorageNodeRouter.get("/page", summary="分页查询存储源", response_model=ResponseSchema[PageResultSchema[StorageNodeOutSchema]])
|
@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_storage:workflow:node:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:node:query"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[StorageNodeQueryParam, Query()],
|
search: Annotated[StorageNodeQueryParam, Query()],
|
||||||
@@ -59,7 +59,7 @@ async def get_storage_source_page_controller(
|
|||||||
|
|
||||||
@StorageNodeRouter.get("/list", summary="查询存储源列表", response_model=ResponseSchema[list[StorageNodeOutSchema]])
|
@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_storage:workflow:node:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:node:query"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
search: Annotated[StorageNodeQueryParam, Query()],
|
search: Annotated[StorageNodeQueryParam, Query()],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
@@ -69,7 +69,7 @@ async def get_storage_source_list_controller(
|
|||||||
|
|
||||||
@StorageNodeRouter.get("/detail/{id}", summary="查询存储源详情", response_model=ResponseSchema[StorageNodeOutSchema])
|
@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_storage:workflow:node:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage: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:
|
||||||
@@ -79,7 +79,7 @@ async def get_storage_source_detail_controller(
|
|||||||
|
|
||||||
@StorageNodeRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建存储源", response_model=ResponseSchema[StorageNodeOutSchema])
|
@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_storage:workflow:node:create"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:node:create"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
data: Annotated[StorageNodeCreateSchema, Body(description="存储源创建参数")],
|
data: Annotated[StorageNodeCreateSchema, Body(description="存储源创建参数")],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
@@ -89,7 +89,7 @@ async def create_storage_source_controller(
|
|||||||
|
|
||||||
@StorageNodeRouter.put("/update/{id}", summary="修改存储源", response_model=ResponseSchema[StorageNodeOutSchema])
|
@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_storage:workflow:node:update"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage: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[StorageNodeUpdateSchema, Body(description="存储源修改参数")],
|
data: Annotated[StorageNodeUpdateSchema, Body(description="存储源修改参数")],
|
||||||
@@ -100,7 +100,7 @@ async def update_storage_source_controller(
|
|||||||
|
|
||||||
@StorageNodeRouter.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_storage:workflow:node:delete"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage: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:
|
||||||
@@ -110,7 +110,7 @@ async def delete_storage_source_controller(
|
|||||||
|
|
||||||
@StorageNodeRouter.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_storage:workflow:node:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage: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:
|
||||||
@@ -120,7 +120,7 @@ async def test_storage_source_controller(
|
|||||||
|
|
||||||
@StorageNodeRouter.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_storage:workflow:node:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:node:query"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
data: Annotated[StorageNodeTestSchema, Body(description="存储源连接配置")],
|
data: Annotated[StorageNodeTestSchema, Body(description="存储源连接配置")],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ StorageTransferRouter = APIRouter(route_class=OperationLogRoute, prefix="/storag
|
|||||||
|
|
||||||
@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_storage:workflow:transfer:create"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage: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_storage:workflow:transfer:create"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage: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_storage:workflow:transfer:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage: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_storage:workflow:transfer:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage: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_storage:workflow:transfer:update"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage: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_storage:workflow:transfer:delete"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage: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:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ StorageWorkflowRouter = APIRouter(route_class=OperationLogRoute, prefix="/storag
|
|||||||
|
|
||||||
@StorageWorkflowRouter.get("/page", summary="分页查询传输流程", response_model=ResponseSchema[PageResultSchema[WorkflowOutSchema]])
|
@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_storage:workflow:flow:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:flow:query"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
page: Annotated[PaginationQueryParam, Depends()],
|
page: Annotated[PaginationQueryParam, Depends()],
|
||||||
search: Annotated[WorkflowQueryParam, Query()],
|
search: Annotated[WorkflowQueryParam, Query()],
|
||||||
@@ -32,7 +32,7 @@ async def get_flow_page_controller(
|
|||||||
|
|
||||||
@StorageWorkflowRouter.get("/list", summary="查询传输流程列表", response_model=ResponseSchema[list[WorkflowOutSchema]])
|
@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_storage:workflow:flow:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:flow:query"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
search: Annotated[WorkflowQueryParam, Query()],
|
search: Annotated[WorkflowQueryParam, Query()],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
@@ -42,7 +42,7 @@ async def get_flow_list_controller(
|
|||||||
|
|
||||||
@StorageWorkflowRouter.get("/detail/{id}", summary="查询传输流程详情", response_model=ResponseSchema[WorkflowOutSchema])
|
@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_storage:workflow:flow:query"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage: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:
|
||||||
@@ -52,7 +52,7 @@ async def get_flow_detail_controller(
|
|||||||
|
|
||||||
@StorageWorkflowRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建传输流程", response_model=ResponseSchema[WorkflowOutSchema])
|
@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_storage:workflow:flow:create"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:flow:create"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
data: Annotated[WorkflowCreateSchema, Body(description="流程创建参数")],
|
data: Annotated[WorkflowCreateSchema, Body(description="流程创建参数")],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
@@ -62,7 +62,7 @@ async def create_flow_controller(
|
|||||||
|
|
||||||
@StorageWorkflowRouter.put("/update/{id}", summary="修改传输流程", response_model=ResponseSchema[WorkflowOutSchema])
|
@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_storage:workflow:flow:update"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage: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[WorkflowUpdateSchema, Body(description="流程修改参数")],
|
data: Annotated[WorkflowUpdateSchema, Body(description="流程修改参数")],
|
||||||
@@ -73,7 +73,7 @@ async def update_flow_controller(
|
|||||||
|
|
||||||
@StorageWorkflowRouter.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_storage:workflow:flow:delete"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage: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:
|
||||||
@@ -83,7 +83,7 @@ async def delete_flow_controller(
|
|||||||
|
|
||||||
@StorageWorkflowRouter.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_storage:workflow:transfer:create"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage: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[WorkflowExecuteSchema | None, Body(description="执行参数(源文件/目录路径映射,可选)")] = None,
|
data: Annotated[WorkflowExecuteSchema | None, Body(description="执行参数(源文件/目录路径映射,可选)")] = None,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from sqlalchemy import func, inspect, select
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.common.enums import EnvironmentEnum
|
from app.common.enums import EnvironmentEnum
|
||||||
from app.config.path_conf import ALEMBIC_VERSION_DIR, BASE_DIR, SCRIPT_DIR
|
from app.config.path_conf import ALEMBIC_VERSION_DIR, BASE_DIR, SCRIPT_DIR, STATIC_DIR
|
||||||
from app.config.setting import settings
|
from app.config.setting import settings
|
||||||
from app.core.base_model import MappedBase
|
from app.core.base_model import MappedBase
|
||||||
from app.core.database import async_db_session, async_engine, create_tables
|
from app.core.database import async_db_session, async_engine, create_tables
|
||||||
@@ -18,6 +18,8 @@ from app.modules.system.params.model import ParamsModel
|
|||||||
from app.modules.system.role.model import RoleModel
|
from app.modules.system.role.model import RoleModel
|
||||||
from app.modules.system.user.model import UserModel, UserRolesModel
|
from app.modules.system.user.model import UserModel, UserRolesModel
|
||||||
from app.modules.system.versions.model import VersionModel
|
from app.modules.system.versions.model import VersionModel
|
||||||
|
from app.modules.task.cronjob.node.model import NodeModel
|
||||||
|
from app.modules.task.storage.node.model import StorageNodeModel
|
||||||
from app.utils.import_util import ImportUtil
|
from app.utils.import_util import ImportUtil
|
||||||
|
|
||||||
# 导入全部模型:与 alembic env.py 保持一致,确保全局 MapperRegistry 的 FK 引用可完整解析
|
# 导入全部模型:与 alembic env.py 保持一致,确保全局 MapperRegistry 的 FK 引用可完整解析
|
||||||
@@ -38,6 +40,8 @@ class InitializeData:
|
|||||||
UserModel,
|
UserModel,
|
||||||
UserRolesModel,
|
UserRolesModel,
|
||||||
VersionModel,
|
VersionModel,
|
||||||
|
NodeModel,
|
||||||
|
StorageNodeModel,
|
||||||
]
|
]
|
||||||
|
|
||||||
# 树形模型:JSON 含嵌套 children,需递归创建对象
|
# 树形模型:JSON 含嵌套 children,需递归创建对象
|
||||||
@@ -50,6 +54,9 @@ class InitializeData:
|
|||||||
async with async_db_session() as session, session.begin():
|
async with async_db_session() as session, session.begin():
|
||||||
await self.__init_data(session)
|
await self.__init_data(session)
|
||||||
|
|
||||||
|
# 内置本地存储源的根目录(种子节点 host 指向 static/upload),保证开箱可浏览
|
||||||
|
(STATIC_DIR / "upload").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def __apply_migrations() -> None:
|
async def __apply_migrations() -> None:
|
||||||
"""将数据库 schema 带到模型定义的最新的版本(开源项目要求任意方言开箱即用)。"""
|
"""将数据库 schema 带到模型定义的最新的版本(开源项目要求任意方言开箱即用)。"""
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ dependencies = [
|
|||||||
"esdk-obs-python==3.26.6", # 华为云 OBS 协议
|
"esdk-obs-python==3.26.6", # 华为云 OBS 协议
|
||||||
"alibabacloud-oss-v2==1.3.2", # 阿里云 OSS 协议
|
"alibabacloud-oss-v2==1.3.2", # 阿里云 OSS 协议
|
||||||
"cos-python-sdk-v5==1.9.44", # 腾讯云 COS 协议
|
"cos-python-sdk-v5==1.9.44", # 腾讯云 COS 协议
|
||||||
|
"fastapi-limiter==0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ bleach==6.4.0 # 过滤 HTML 标签
|
|||||||
tinycss2==1.5.1 # bleach CSS 清洗依赖
|
tinycss2==1.5.1 # bleach CSS 清洗依赖
|
||||||
click==8.1.7 # 命令行参数解析
|
click==8.1.7 # 命令行参数解析
|
||||||
fastapi==0.138.2 # fastapi 框架
|
fastapi==0.138.2 # fastapi 框架
|
||||||
|
fastapi-limiter==0.2.0 # 限频器
|
||||||
greenlet==3.5.2 # 协程核心依赖(SQLAlchemy 异步)
|
greenlet==3.5.2 # 协程核心依赖(SQLAlchemy 异步)
|
||||||
httpx==0.28.1 # HTTP 客户端
|
httpx==0.28.1 # HTTP 客户端
|
||||||
jinja2==3.1.6 # 模板引擎
|
jinja2==3.1.6 # 模板引擎
|
||||||
|
|||||||
+417
-444
@@ -3332,7 +3332,7 @@
|
|||||||
"title": "存储管理",
|
"title": "存储管理",
|
||||||
"params": null,
|
"params": null,
|
||||||
"affix": false,
|
"affix": false,
|
||||||
"redirect": "/task/storage/workflow/flow",
|
"redirect": "/task/storage/flow",
|
||||||
"description": "存储源管理、文件浏览、传输任务与传输流程编排",
|
"description": "存储源管理、文件浏览、传输任务与传输流程编排",
|
||||||
"link": null,
|
"link": null,
|
||||||
"is_iframe": false,
|
"is_iframe": false,
|
||||||
@@ -3342,144 +3342,42 @@
|
|||||||
"show_text_badge": null,
|
"show_text_badge": null,
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"name": "工作流",
|
"name": "流程管理",
|
||||||
"type": 1,
|
"type": 2,
|
||||||
"icon": "ri:tools-line",
|
"icon": "ri:git-branch-line",
|
||||||
"order": 2,
|
"order": 1,
|
||||||
"permission": null,
|
"permission": "module_task:storage:flow:query",
|
||||||
"route_name": "WorkflowMgr",
|
"route_name": "WorkflowFlow",
|
||||||
"route_path": "workflow",
|
"route_path": "flow",
|
||||||
"component_path": null,
|
"component_path": "module_task/storage/workflow/index",
|
||||||
"status": 0,
|
"status": 0,
|
||||||
"keep_alive": true,
|
"keep_alive": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"always_show": true,
|
"always_show": false,
|
||||||
"title": "工作流",
|
"title": "流程管理",
|
||||||
"params": null,
|
"params": null,
|
||||||
"affix": false,
|
"affix": false,
|
||||||
"redirect": "/task/storage/workflow/flow",
|
"redirect": null,
|
||||||
"description": "传输流程与节点管理",
|
"description": "传输流程定义(源节点 + 多目标)",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"name": "流程管理",
|
"name": "新增",
|
||||||
"type": 2,
|
"type": 3,
|
||||||
"icon": "ri:git-branch-line",
|
"icon": null,
|
||||||
"order": 1,
|
"order": 1,
|
||||||
"permission": "module_storage:workflow:flow:query",
|
"permission": "module_task:storage:flow:create",
|
||||||
"route_name": "WorkflowFlow",
|
"route_name": null,
|
||||||
"route_path": "flow",
|
"route_path": null,
|
||||||
"component_path": "module_storage/workflow/index",
|
"component_path": null,
|
||||||
"status": 0,
|
"status": 0,
|
||||||
"keep_alive": true,
|
"keep_alive": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"always_show": false,
|
"always_show": false,
|
||||||
"title": "流程管理",
|
"title": "新增",
|
||||||
"params": null,
|
"params": null,
|
||||||
"affix": false,
|
"affix": false,
|
||||||
"redirect": null,
|
"redirect": null,
|
||||||
"description": "传输流程定义(源节点 + 多目标)",
|
"description": "新增",
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"name": "新增",
|
|
||||||
"type": 3,
|
|
||||||
"icon": null,
|
|
||||||
"order": 1,
|
|
||||||
"permission": "module_storage:workflow:flow:create",
|
|
||||||
"route_name": null,
|
|
||||||
"route_path": null,
|
|
||||||
"component_path": null,
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"title": "新增",
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"description": "新增",
|
|
||||||
"link": null,
|
|
||||||
"is_iframe": false,
|
|
||||||
"is_hide_tab": false,
|
|
||||||
"active_path": null,
|
|
||||||
"show_badge": false,
|
|
||||||
"show_text_badge": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "编辑",
|
|
||||||
"type": 3,
|
|
||||||
"icon": null,
|
|
||||||
"order": 3,
|
|
||||||
"permission": "module_storage:workflow:flow:update",
|
|
||||||
"route_name": null,
|
|
||||||
"route_path": null,
|
|
||||||
"component_path": null,
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"title": "编辑",
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"description": "编辑",
|
|
||||||
"link": null,
|
|
||||||
"is_iframe": false,
|
|
||||||
"is_hide_tab": false,
|
|
||||||
"active_path": null,
|
|
||||||
"show_badge": false,
|
|
||||||
"show_text_badge": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "删除流程",
|
|
||||||
"type": 3,
|
|
||||||
"icon": null,
|
|
||||||
"order": 4,
|
|
||||||
"permission": "module_storage:workflow:flow:delete",
|
|
||||||
"route_name": null,
|
|
||||||
"route_path": null,
|
|
||||||
"component_path": null,
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"title": "删除流程",
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"description": "删除流程",
|
|
||||||
"link": null,
|
|
||||||
"is_iframe": false,
|
|
||||||
"is_hide_tab": false,
|
|
||||||
"active_path": null,
|
|
||||||
"show_badge": false,
|
|
||||||
"show_text_badge": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "查询",
|
|
||||||
"type": 3,
|
|
||||||
"icon": null,
|
|
||||||
"order": 6,
|
|
||||||
"permission": "module_storage:workflow:flow:query",
|
|
||||||
"route_name": null,
|
|
||||||
"route_path": null,
|
|
||||||
"component_path": null,
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"title": "查询",
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"description": "查询",
|
|
||||||
"link": null,
|
|
||||||
"is_iframe": false,
|
|
||||||
"is_hide_tab": false,
|
|
||||||
"active_path": null,
|
|
||||||
"show_badge": false,
|
|
||||||
"show_text_badge": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"is_iframe": false,
|
"is_iframe": false,
|
||||||
"is_hide_tab": false,
|
"is_hide_tab": false,
|
||||||
@@ -3488,247 +3386,23 @@
|
|||||||
"show_text_badge": null
|
"show_text_badge": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "节点管理",
|
"name": "编辑",
|
||||||
"type": 2,
|
"type": 3,
|
||||||
"icon": "ri:hard-drive-3-line",
|
"icon": null,
|
||||||
"order": 2,
|
|
||||||
"permission": "module_storage:workflow:node:query",
|
|
||||||
"route_name": "WorkflowNode",
|
|
||||||
"route_path": "node",
|
|
||||||
"component_path": "module_storage/node/index",
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"title": "节点管理",
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"description": "节点即传输地址(存储源)",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"name": "新增",
|
|
||||||
"type": 3,
|
|
||||||
"icon": null,
|
|
||||||
"order": 1,
|
|
||||||
"permission": "module_storage:workflow:node:create",
|
|
||||||
"route_name": null,
|
|
||||||
"route_path": null,
|
|
||||||
"component_path": null,
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"title": "新增",
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"description": "新增",
|
|
||||||
"link": null,
|
|
||||||
"is_iframe": false,
|
|
||||||
"is_hide_tab": false,
|
|
||||||
"active_path": null,
|
|
||||||
"show_badge": false,
|
|
||||||
"show_text_badge": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "编辑",
|
|
||||||
"type": 3,
|
|
||||||
"icon": null,
|
|
||||||
"order": 2,
|
|
||||||
"permission": "module_storage:workflow:node:update",
|
|
||||||
"route_name": null,
|
|
||||||
"route_path": null,
|
|
||||||
"component_path": null,
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"title": "编辑",
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"description": "编辑",
|
|
||||||
"link": null,
|
|
||||||
"is_iframe": false,
|
|
||||||
"is_hide_tab": false,
|
|
||||||
"active_path": null,
|
|
||||||
"show_badge": false,
|
|
||||||
"show_text_badge": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "删除",
|
|
||||||
"type": 3,
|
|
||||||
"icon": null,
|
|
||||||
"order": 3,
|
|
||||||
"permission": "module_storage:workflow:node:delete",
|
|
||||||
"route_name": null,
|
|
||||||
"route_path": null,
|
|
||||||
"component_path": null,
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"title": "删除",
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"description": "删除",
|
|
||||||
"link": null,
|
|
||||||
"is_iframe": false,
|
|
||||||
"is_hide_tab": false,
|
|
||||||
"active_path": null,
|
|
||||||
"show_badge": false,
|
|
||||||
"show_text_badge": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "查询",
|
|
||||||
"type": 3,
|
|
||||||
"icon": null,
|
|
||||||
"order": 5,
|
|
||||||
"permission": "module_storage:workflow:node:query",
|
|
||||||
"route_name": null,
|
|
||||||
"route_path": null,
|
|
||||||
"component_path": null,
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"title": "查询",
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"description": "查询",
|
|
||||||
"link": null,
|
|
||||||
"is_iframe": false,
|
|
||||||
"is_hide_tab": false,
|
|
||||||
"active_path": null,
|
|
||||||
"show_badge": false,
|
|
||||||
"show_text_badge": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"link": null,
|
|
||||||
"is_iframe": false,
|
|
||||||
"is_hide_tab": false,
|
|
||||||
"active_path": null,
|
|
||||||
"show_badge": false,
|
|
||||||
"show_text_badge": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "文件浏览",
|
|
||||||
"type": 2,
|
|
||||||
"icon": "ri:folder-open-line",
|
|
||||||
"order": 3,
|
"order": 3,
|
||||||
"permission": "module_storage:browse:query",
|
"permission": "module_task:storage:flow:update",
|
||||||
"route_name": "WorkflowBrowse",
|
"route_name": null,
|
||||||
"route_path": "browse",
|
"route_path": null,
|
||||||
"component_path": "module_storage/browse/index",
|
"component_path": null,
|
||||||
"status": 0,
|
"status": 0,
|
||||||
"keep_alive": true,
|
"keep_alive": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"always_show": false,
|
"always_show": false,
|
||||||
"title": "文件浏览",
|
"title": "编辑",
|
||||||
"params": null,
|
"params": null,
|
||||||
"affix": false,
|
"affix": false,
|
||||||
"redirect": null,
|
"redirect": null,
|
||||||
"description": "查看各节点内部文件",
|
"description": "编辑",
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": 3,
|
|
||||||
"icon": null,
|
|
||||||
"route_name": null,
|
|
||||||
"route_path": null,
|
|
||||||
"component_path": null,
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"name": "查询",
|
|
||||||
"order": 1,
|
|
||||||
"permission": "module_storage:browse:query",
|
|
||||||
"title": "查询",
|
|
||||||
"description": "查询"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 3,
|
|
||||||
"icon": null,
|
|
||||||
"route_name": null,
|
|
||||||
"route_path": null,
|
|
||||||
"component_path": null,
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"name": "上传",
|
|
||||||
"order": 2,
|
|
||||||
"permission": "module_storage:browse:upload",
|
|
||||||
"title": "上传",
|
|
||||||
"description": "上传"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 3,
|
|
||||||
"icon": null,
|
|
||||||
"route_name": null,
|
|
||||||
"route_path": null,
|
|
||||||
"component_path": null,
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"name": "下载",
|
|
||||||
"order": 3,
|
|
||||||
"permission": "module_storage:browse:download",
|
|
||||||
"title": "下载",
|
|
||||||
"description": "下载"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 3,
|
|
||||||
"icon": null,
|
|
||||||
"route_name": null,
|
|
||||||
"route_path": null,
|
|
||||||
"component_path": null,
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"name": "删除",
|
|
||||||
"order": 4,
|
|
||||||
"permission": "module_storage:browse:delete",
|
|
||||||
"title": "删除",
|
|
||||||
"description": "删除"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 3,
|
|
||||||
"icon": null,
|
|
||||||
"route_name": null,
|
|
||||||
"route_path": null,
|
|
||||||
"component_path": null,
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"name": "修改",
|
|
||||||
"order": 5,
|
|
||||||
"permission": "module_storage:browse:update",
|
|
||||||
"title": "修改",
|
|
||||||
"description": "修改"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"is_iframe": false,
|
"is_iframe": false,
|
||||||
"is_hide_tab": false,
|
"is_hide_tab": false,
|
||||||
@@ -3737,107 +3411,406 @@
|
|||||||
"show_text_badge": null
|
"show_text_badge": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "传输任务",
|
"name": "删除流程",
|
||||||
"type": 2,
|
"type": 3,
|
||||||
"icon": "ri:swap-box-line",
|
"icon": null,
|
||||||
"order": 4,
|
"order": 4,
|
||||||
"permission": "module_storage:workflow:transfer:query",
|
"permission": "module_task:storage:flow:delete",
|
||||||
"route_name": "WorkflowTransfer",
|
"route_name": null,
|
||||||
"route_path": "transfer",
|
"route_path": null,
|
||||||
"component_path": "module_storage/transfer/index",
|
"component_path": null,
|
||||||
"status": 0,
|
"status": 0,
|
||||||
"keep_alive": true,
|
"keep_alive": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"always_show": false,
|
"always_show": false,
|
||||||
"title": "传输任务",
|
"title": "删除流程",
|
||||||
"params": null,
|
"params": null,
|
||||||
"affix": false,
|
"affix": false,
|
||||||
"redirect": null,
|
"redirect": null,
|
||||||
"description": "文件传输与流程执行",
|
"description": "删除流程",
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": 3,
|
|
||||||
"icon": null,
|
|
||||||
"route_name": null,
|
|
||||||
"route_path": null,
|
|
||||||
"component_path": null,
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"name": "查询",
|
|
||||||
"order": 1,
|
|
||||||
"permission": "module_storage:workflow:transfer:query",
|
|
||||||
"title": "查询",
|
|
||||||
"description": "查询"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 3,
|
|
||||||
"icon": null,
|
|
||||||
"route_name": null,
|
|
||||||
"route_path": null,
|
|
||||||
"component_path": null,
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"name": "新增",
|
|
||||||
"order": 2,
|
|
||||||
"permission": "module_storage:workflow:transfer:create",
|
|
||||||
"title": "新增",
|
|
||||||
"description": "新增"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 3,
|
|
||||||
"icon": null,
|
|
||||||
"route_name": null,
|
|
||||||
"route_path": null,
|
|
||||||
"component_path": null,
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"name": "修改",
|
|
||||||
"order": 3,
|
|
||||||
"permission": "module_storage:workflow:transfer:update",
|
|
||||||
"title": "修改",
|
|
||||||
"description": "修改"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 3,
|
|
||||||
"icon": null,
|
|
||||||
"route_name": null,
|
|
||||||
"route_path": null,
|
|
||||||
"component_path": null,
|
|
||||||
"status": 0,
|
|
||||||
"keep_alive": true,
|
|
||||||
"hidden": false,
|
|
||||||
"always_show": false,
|
|
||||||
"params": null,
|
|
||||||
"affix": false,
|
|
||||||
"redirect": null,
|
|
||||||
"name": "删除",
|
|
||||||
"order": 4,
|
|
||||||
"permission": "module_storage:workflow:transfer:delete",
|
|
||||||
"title": "删除",
|
|
||||||
"description": "删除"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"is_iframe": false,
|
"is_iframe": false,
|
||||||
"is_hide_tab": false,
|
"is_hide_tab": false,
|
||||||
"active_path": null,
|
"active_path": null,
|
||||||
"show_badge": false,
|
"show_badge": false,
|
||||||
"show_text_badge": null
|
"show_text_badge": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "查询",
|
||||||
|
"type": 3,
|
||||||
|
"icon": null,
|
||||||
|
"order": 6,
|
||||||
|
"permission": "module_task:storage:flow:query",
|
||||||
|
"route_name": null,
|
||||||
|
"route_path": null,
|
||||||
|
"component_path": null,
|
||||||
|
"status": 0,
|
||||||
|
"keep_alive": true,
|
||||||
|
"hidden": false,
|
||||||
|
"always_show": false,
|
||||||
|
"title": "查询",
|
||||||
|
"params": null,
|
||||||
|
"affix": false,
|
||||||
|
"redirect": null,
|
||||||
|
"description": "查询",
|
||||||
|
"link": null,
|
||||||
|
"is_iframe": false,
|
||||||
|
"is_hide_tab": false,
|
||||||
|
"active_path": null,
|
||||||
|
"show_badge": false,
|
||||||
|
"show_text_badge": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"link": null,
|
||||||
|
"is_iframe": false,
|
||||||
|
"is_hide_tab": false,
|
||||||
|
"active_path": null,
|
||||||
|
"show_badge": false,
|
||||||
|
"show_text_badge": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "节点管理",
|
||||||
|
"type": 2,
|
||||||
|
"icon": "ri:hard-drive-3-line",
|
||||||
|
"order": 2,
|
||||||
|
"permission": "module_task:storage:node:query",
|
||||||
|
"route_name": "WorkflowNode",
|
||||||
|
"route_path": "node",
|
||||||
|
"component_path": "module_task/storage/node/index",
|
||||||
|
"status": 0,
|
||||||
|
"keep_alive": true,
|
||||||
|
"hidden": false,
|
||||||
|
"always_show": false,
|
||||||
|
"title": "节点管理",
|
||||||
|
"params": null,
|
||||||
|
"affix": false,
|
||||||
|
"redirect": null,
|
||||||
|
"description": "节点即传输地址(存储源)",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "新增",
|
||||||
|
"type": 3,
|
||||||
|
"icon": null,
|
||||||
|
"order": 1,
|
||||||
|
"permission": "module_task:storage:node:create",
|
||||||
|
"route_name": null,
|
||||||
|
"route_path": null,
|
||||||
|
"component_path": null,
|
||||||
|
"status": 0,
|
||||||
|
"keep_alive": true,
|
||||||
|
"hidden": false,
|
||||||
|
"always_show": false,
|
||||||
|
"title": "新增",
|
||||||
|
"params": null,
|
||||||
|
"affix": false,
|
||||||
|
"redirect": null,
|
||||||
|
"description": "新增",
|
||||||
|
"link": null,
|
||||||
|
"is_iframe": false,
|
||||||
|
"is_hide_tab": false,
|
||||||
|
"active_path": null,
|
||||||
|
"show_badge": false,
|
||||||
|
"show_text_badge": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "编辑",
|
||||||
|
"type": 3,
|
||||||
|
"icon": null,
|
||||||
|
"order": 2,
|
||||||
|
"permission": "module_task:storage:node:update",
|
||||||
|
"route_name": null,
|
||||||
|
"route_path": null,
|
||||||
|
"component_path": null,
|
||||||
|
"status": 0,
|
||||||
|
"keep_alive": true,
|
||||||
|
"hidden": false,
|
||||||
|
"always_show": false,
|
||||||
|
"title": "编辑",
|
||||||
|
"params": null,
|
||||||
|
"affix": false,
|
||||||
|
"redirect": null,
|
||||||
|
"description": "编辑",
|
||||||
|
"link": null,
|
||||||
|
"is_iframe": false,
|
||||||
|
"is_hide_tab": false,
|
||||||
|
"active_path": null,
|
||||||
|
"show_badge": false,
|
||||||
|
"show_text_badge": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "删除",
|
||||||
|
"type": 3,
|
||||||
|
"icon": null,
|
||||||
|
"order": 3,
|
||||||
|
"permission": "module_task:storage:node:delete",
|
||||||
|
"route_name": null,
|
||||||
|
"route_path": null,
|
||||||
|
"component_path": null,
|
||||||
|
"status": 0,
|
||||||
|
"keep_alive": true,
|
||||||
|
"hidden": false,
|
||||||
|
"always_show": false,
|
||||||
|
"title": "删除",
|
||||||
|
"params": null,
|
||||||
|
"affix": false,
|
||||||
|
"redirect": null,
|
||||||
|
"description": "删除",
|
||||||
|
"link": null,
|
||||||
|
"is_iframe": false,
|
||||||
|
"is_hide_tab": false,
|
||||||
|
"active_path": null,
|
||||||
|
"show_badge": false,
|
||||||
|
"show_text_badge": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "查询",
|
||||||
|
"type": 3,
|
||||||
|
"icon": null,
|
||||||
|
"order": 5,
|
||||||
|
"permission": "module_task:storage:node:query",
|
||||||
|
"route_name": null,
|
||||||
|
"route_path": null,
|
||||||
|
"component_path": null,
|
||||||
|
"status": 0,
|
||||||
|
"keep_alive": true,
|
||||||
|
"hidden": false,
|
||||||
|
"always_show": false,
|
||||||
|
"title": "查询",
|
||||||
|
"params": null,
|
||||||
|
"affix": false,
|
||||||
|
"redirect": null,
|
||||||
|
"description": "查询",
|
||||||
|
"link": null,
|
||||||
|
"is_iframe": false,
|
||||||
|
"is_hide_tab": false,
|
||||||
|
"active_path": null,
|
||||||
|
"show_badge": false,
|
||||||
|
"show_text_badge": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"link": null,
|
||||||
|
"is_iframe": false,
|
||||||
|
"is_hide_tab": false,
|
||||||
|
"active_path": null,
|
||||||
|
"show_badge": false,
|
||||||
|
"show_text_badge": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "文件浏览",
|
||||||
|
"type": 2,
|
||||||
|
"icon": "ri:folder-open-line",
|
||||||
|
"order": 3,
|
||||||
|
"permission": "module_task:storage:browse:query",
|
||||||
|
"route_name": "WorkflowBrowse",
|
||||||
|
"route_path": "browse",
|
||||||
|
"component_path": "module_task/storage/browse/index",
|
||||||
|
"status": 0,
|
||||||
|
"keep_alive": true,
|
||||||
|
"hidden": false,
|
||||||
|
"always_show": false,
|
||||||
|
"title": "文件浏览",
|
||||||
|
"params": null,
|
||||||
|
"affix": false,
|
||||||
|
"redirect": null,
|
||||||
|
"description": "查看各节点内部文件",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": 3,
|
||||||
|
"icon": null,
|
||||||
|
"route_name": null,
|
||||||
|
"route_path": null,
|
||||||
|
"component_path": null,
|
||||||
|
"status": 0,
|
||||||
|
"keep_alive": true,
|
||||||
|
"hidden": false,
|
||||||
|
"always_show": false,
|
||||||
|
"params": null,
|
||||||
|
"affix": false,
|
||||||
|
"redirect": null,
|
||||||
|
"name": "查询",
|
||||||
|
"order": 1,
|
||||||
|
"permission": "module_task:storage:browse:query",
|
||||||
|
"title": "查询",
|
||||||
|
"description": "查询"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 3,
|
||||||
|
"icon": null,
|
||||||
|
"route_name": null,
|
||||||
|
"route_path": null,
|
||||||
|
"component_path": null,
|
||||||
|
"status": 0,
|
||||||
|
"keep_alive": true,
|
||||||
|
"hidden": false,
|
||||||
|
"always_show": false,
|
||||||
|
"params": null,
|
||||||
|
"affix": false,
|
||||||
|
"redirect": null,
|
||||||
|
"name": "上传",
|
||||||
|
"order": 2,
|
||||||
|
"permission": "module_task:storage:browse:upload",
|
||||||
|
"title": "上传",
|
||||||
|
"description": "上传"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 3,
|
||||||
|
"icon": null,
|
||||||
|
"route_name": null,
|
||||||
|
"route_path": null,
|
||||||
|
"component_path": null,
|
||||||
|
"status": 0,
|
||||||
|
"keep_alive": true,
|
||||||
|
"hidden": false,
|
||||||
|
"always_show": false,
|
||||||
|
"params": null,
|
||||||
|
"affix": false,
|
||||||
|
"redirect": null,
|
||||||
|
"name": "下载",
|
||||||
|
"order": 3,
|
||||||
|
"permission": "module_task:storage:browse:download",
|
||||||
|
"title": "下载",
|
||||||
|
"description": "下载"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 3,
|
||||||
|
"icon": null,
|
||||||
|
"route_name": null,
|
||||||
|
"route_path": null,
|
||||||
|
"component_path": null,
|
||||||
|
"status": 0,
|
||||||
|
"keep_alive": true,
|
||||||
|
"hidden": false,
|
||||||
|
"always_show": false,
|
||||||
|
"params": null,
|
||||||
|
"affix": false,
|
||||||
|
"redirect": null,
|
||||||
|
"name": "删除",
|
||||||
|
"order": 4,
|
||||||
|
"permission": "module_task:storage:browse:delete",
|
||||||
|
"title": "删除",
|
||||||
|
"description": "删除"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 3,
|
||||||
|
"icon": null,
|
||||||
|
"route_name": null,
|
||||||
|
"route_path": null,
|
||||||
|
"component_path": null,
|
||||||
|
"status": 0,
|
||||||
|
"keep_alive": true,
|
||||||
|
"hidden": false,
|
||||||
|
"always_show": false,
|
||||||
|
"params": null,
|
||||||
|
"affix": false,
|
||||||
|
"redirect": null,
|
||||||
|
"name": "修改",
|
||||||
|
"order": 5,
|
||||||
|
"permission": "module_task:storage:browse:update",
|
||||||
|
"title": "修改",
|
||||||
|
"description": "修改"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"link": null,
|
||||||
|
"is_iframe": false,
|
||||||
|
"is_hide_tab": false,
|
||||||
|
"active_path": null,
|
||||||
|
"show_badge": false,
|
||||||
|
"show_text_badge": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "传输任务",
|
||||||
|
"type": 2,
|
||||||
|
"icon": "ri:swap-box-line",
|
||||||
|
"order": 4,
|
||||||
|
"permission": "module_task:storage:transfer:query",
|
||||||
|
"route_name": "WorkflowTransfer",
|
||||||
|
"route_path": "transfer",
|
||||||
|
"component_path": "module_task/storage/transfer/index",
|
||||||
|
"status": 0,
|
||||||
|
"keep_alive": true,
|
||||||
|
"hidden": false,
|
||||||
|
"always_show": false,
|
||||||
|
"title": "传输任务",
|
||||||
|
"params": null,
|
||||||
|
"affix": false,
|
||||||
|
"redirect": null,
|
||||||
|
"description": "文件传输与流程执行",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": 3,
|
||||||
|
"icon": null,
|
||||||
|
"route_name": null,
|
||||||
|
"route_path": null,
|
||||||
|
"component_path": null,
|
||||||
|
"status": 0,
|
||||||
|
"keep_alive": true,
|
||||||
|
"hidden": false,
|
||||||
|
"always_show": false,
|
||||||
|
"params": null,
|
||||||
|
"affix": false,
|
||||||
|
"redirect": null,
|
||||||
|
"name": "查询",
|
||||||
|
"order": 1,
|
||||||
|
"permission": "module_task:storage:transfer:query",
|
||||||
|
"title": "查询",
|
||||||
|
"description": "查询"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 3,
|
||||||
|
"icon": null,
|
||||||
|
"route_name": null,
|
||||||
|
"route_path": null,
|
||||||
|
"component_path": null,
|
||||||
|
"status": 0,
|
||||||
|
"keep_alive": true,
|
||||||
|
"hidden": false,
|
||||||
|
"always_show": false,
|
||||||
|
"params": null,
|
||||||
|
"affix": false,
|
||||||
|
"redirect": null,
|
||||||
|
"name": "新增",
|
||||||
|
"order": 2,
|
||||||
|
"permission": "module_task:storage:transfer:create",
|
||||||
|
"title": "新增",
|
||||||
|
"description": "新增"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 3,
|
||||||
|
"icon": null,
|
||||||
|
"route_name": null,
|
||||||
|
"route_path": null,
|
||||||
|
"component_path": null,
|
||||||
|
"status": 0,
|
||||||
|
"keep_alive": true,
|
||||||
|
"hidden": false,
|
||||||
|
"always_show": false,
|
||||||
|
"params": null,
|
||||||
|
"affix": false,
|
||||||
|
"redirect": null,
|
||||||
|
"name": "修改",
|
||||||
|
"order": 3,
|
||||||
|
"permission": "module_task:storage:transfer:update",
|
||||||
|
"title": "修改",
|
||||||
|
"description": "修改"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 3,
|
||||||
|
"icon": null,
|
||||||
|
"route_name": null,
|
||||||
|
"route_path": null,
|
||||||
|
"component_path": null,
|
||||||
|
"status": 0,
|
||||||
|
"keep_alive": true,
|
||||||
|
"hidden": false,
|
||||||
|
"always_show": false,
|
||||||
|
"params": null,
|
||||||
|
"affix": false,
|
||||||
|
"redirect": null,
|
||||||
|
"name": "删除",
|
||||||
|
"order": 4,
|
||||||
|
"permission": "module_task:storage:transfer:delete",
|
||||||
|
"title": "删除",
|
||||||
|
"description": "删除"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"link": null,
|
"link": null,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "日志保留清理",
|
||||||
|
"code": "log_cleanup",
|
||||||
|
"jobstore": "default",
|
||||||
|
"executor": "threadpool",
|
||||||
|
"trigger": "cron",
|
||||||
|
"trigger_args": "0 0 3 * * ?",
|
||||||
|
"func": "def handler(**kwargs):\n import asyncio\n from app.modules.system.log.service import cleanup_expired_logs\n return asyncio.run(cleanup_expired_logs(days=int(kwargs.get(\"days\", 90))))",
|
||||||
|
"args": null,
|
||||||
|
"kwargs": "{\"days\": 90}",
|
||||||
|
"coalesce": true,
|
||||||
|
"max_instances": 1,
|
||||||
|
"status": 0,
|
||||||
|
"description": "每天 03:00 物理清理超过保留天数的操作日志与登录日志(默认保留 90 天,可改 kwargs.days)"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "本地上传目录",
|
||||||
|
"protocol": "local",
|
||||||
|
"host": "static/upload",
|
||||||
|
"port": 0,
|
||||||
|
"path_prefix": null,
|
||||||
|
"is_default": true,
|
||||||
|
"status": 0,
|
||||||
|
"description": "系统内置本地存储:backend/static/upload 目录(随应用启动自动创建)"
|
||||||
|
}
|
||||||
|
]
|
||||||
Generated
+25
-1
@@ -179,7 +179,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backend"
|
name = "backend"
|
||||||
version = "2.0.0"
|
version = "3.2.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "agno" },
|
{ name = "agno" },
|
||||||
@@ -198,6 +198,7 @@ dependencies = [
|
|||||||
{ name = "cryptography" },
|
{ name = "cryptography" },
|
||||||
{ name = "esdk-obs-python" },
|
{ name = "esdk-obs-python" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
|
{ name = "fastapi-limiter" },
|
||||||
{ name = "greenlet" },
|
{ name = "greenlet" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "jinja2" },
|
{ name = "jinja2" },
|
||||||
@@ -263,6 +264,7 @@ requires-dist = [
|
|||||||
{ name = "cryptography", specifier = "==50.0.0" },
|
{ name = "cryptography", specifier = "==50.0.0" },
|
||||||
{ name = "esdk-obs-python", specifier = "==3.26.6" },
|
{ name = "esdk-obs-python", specifier = "==3.26.6" },
|
||||||
{ name = "fastapi", specifier = "==0.138.2" },
|
{ name = "fastapi", specifier = "==0.138.2" },
|
||||||
|
{ name = "fastapi-limiter", specifier = "==0.2.0" },
|
||||||
{ name = "greenlet", specifier = "==3.5.2" },
|
{ name = "greenlet", specifier = "==3.5.2" },
|
||||||
{ name = "httpx", specifier = "==0.28.1" },
|
{ name = "httpx", specifier = "==0.28.1" },
|
||||||
{ name = "jinja2", specifier = "==3.1.6" },
|
{ name = "jinja2", specifier = "==3.1.6" },
|
||||||
@@ -854,6 +856,19 @@ wheels = [
|
|||||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/b3/38be2c074bdd0c986340db1d72d7b2321b805b1c5a68069aa00b5d31fd02/fastapi-0.138.2-py3-none-any.whl", hash = "sha256:db90c1ffb5517fba5d4a9f80e866daa008747e646310c9ce155c8c535f9d1615", size = 129271, upload-time = "2026-06-29T12:44:13.905Z" },
|
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/b3/38be2c074bdd0c986340db1d72d7b2321b805b1c5a68069aa00b5d31fd02/fastapi-0.138.2-py3-none-any.whl", hash = "sha256:db90c1ffb5517fba5d4a9f80e866daa008747e646310c9ce155c8c535f9d1615", size = 129271, upload-time = "2026-06-29T12:44:13.905Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastapi-limiter"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "fastapi" },
|
||||||
|
{ name = "pyrate-limiter" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/32/81f64b69a79345caca0a90d1601ae7c84c39438145793514ee82daac4fc6/fastapi_limiter-0.2.0.tar.gz", hash = "sha256:b042259f6280056b5b2b3f506edd76c8e0648d7a5ce86c3485216a4a0fd0ec14", size = 7401, upload-time = "2026-02-06T07:00:09.831Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/9b/35278a18254f187d55bb450ded6bc9a59c7b3010d164e1796f3dfb7aa73d/fastapi_limiter-0.2.0-py3-none-any.whl", hash = "sha256:e576ebb29cc9614c2f38471c60ed0e849024e9a1b7a2aeb4c2a31a6d708b4aea", size = 5219, upload-time = "2026-02-06T07:00:07.824Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gitdb"
|
name = "gitdb"
|
||||||
version = "4.0.12"
|
version = "4.0.12"
|
||||||
@@ -1599,6 +1614,15 @@ wheels = [
|
|||||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" },
|
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyrate-limiter"
|
||||||
|
version = "4.5.0"
|
||||||
|
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||||
|
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/43/48693393af06b9fffbaea6bb8fe03be3c3f17be5d1423dab347d1aad1dde/pyrate_limiter-4.5.0.tar.gz", hash = "sha256:098345fff3a52b84dee9bcf6973f184c8b3ef8d34e1f4f781ac0773e3984598b", size = 123240, upload-time = "2026-08-30T10:35:39.272Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/dc/02f88649feeabf4282c8c194a6f6060ca858041bab1204365b5d8ef8a95e/pyrate_limiter-4.5.0-py3-none-any.whl", hash = "sha256:e7320a715adf404cb7b2f399e05ceb59cae165275b21f7708eb1b9fd9608e91c", size = 57188, upload-time = "2026-08-30T10:35:38.069Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "9.0.2"
|
version = "9.0.2"
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# ==================== 配置 ====================
|
# ==================== 配置 ====================
|
||||||
|
# 部署方式:本地开发完成后将项目目录上传至服务器(scp/FTP/面板均可),
|
||||||
|
# 再在服务器上执行本脚本完成构建与启动——脚本不再从 git 拉取代码。
|
||||||
PROJECT_NAME="FastapiAdmin"
|
PROJECT_NAME="FastapiAdmin"
|
||||||
WORK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
WORK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
DOCKER_DIR="${WORK_DIR}/docker"
|
DOCKER_DIR="${WORK_DIR}/docker"
|
||||||
ENV_FILE="${DOCKER_DIR}/.env"
|
ENV_FILE="${DOCKER_DIR}/.env"
|
||||||
GIT_REPO="https://gitee.com/fastapiadmin/${PROJECT_NAME}.git"
|
|
||||||
|
|
||||||
COLOR_GREEN='\033[0;32m'; COLOR_BLUE='\033[0;34m'; COLOR_YELLOW='\033[0;33m'; COLOR_RED='\033[0;31m'; COLOR_RESET='\033[0m'
|
COLOR_GREEN='\033[0;32m'; COLOR_BLUE='\033[0;34m'; COLOR_YELLOW='\033[0;33m'; COLOR_RED='\033[0;31m'; COLOR_RESET='\033[0m'
|
||||||
|
|
||||||
@@ -28,6 +29,8 @@ load_env() {
|
|||||||
elif [ -f "${DOCKER_DIR}/.env.example" ]; then
|
elif [ -f "${DOCKER_DIR}/.env.example" ]; then
|
||||||
cp "${DOCKER_DIR}/.env.example" "${ENV_FILE}"
|
cp "${DOCKER_DIR}/.env.example" "${ENV_FILE}"
|
||||||
set -a; source "${ENV_FILE}"; set +a
|
set -a; source "${ENV_FILE}"; set +a
|
||||||
|
log "已从 .env.example 生成 ${ENV_FILE},请先填写密码等配置后重新执行" "WARN"
|
||||||
|
exit 1
|
||||||
else
|
else
|
||||||
log ".env 文件不存在" "ERROR"; exit 1
|
log ".env 文件不存在" "ERROR"; exit 1
|
||||||
fi
|
fi
|
||||||
@@ -35,11 +38,11 @@ load_env() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
check_deps() {
|
check_deps() {
|
||||||
for dir in "${DOCKER_DIR}/mysql/data" "${DOCKER_DIR}/redis/data"; do
|
for dir in "${DOCKER_DIR}/mysql/data" "${DOCKER_DIR}/redis/data" "${DOCKER_DIR}/mysql/init" "${DOCKER_DIR}/redis/conf" "${WORK_DIR}/backend/static/upload"; do
|
||||||
[ -d "$dir" ] || mkdir -p "$dir"
|
[ -d "$dir" ] || mkdir -p "$dir"
|
||||||
done
|
done
|
||||||
local missing=()
|
local missing=()
|
||||||
for cmd in git docker; do
|
for cmd in docker; do
|
||||||
command -v $cmd &>/dev/null || missing+=($cmd)
|
command -v $cmd &>/dev/null || missing+=($cmd)
|
||||||
done
|
done
|
||||||
if ! docker compose version &>/dev/null && ! docker-compose --version &>/dev/null; then
|
if ! docker compose version &>/dev/null && ! docker-compose --version &>/dev/null; then
|
||||||
@@ -51,43 +54,16 @@ check_deps() {
|
|||||||
log "✅ 依赖检查通过" "SUCCESS"
|
log "✅ 依赖检查通过" "SUCCESS"
|
||||||
}
|
}
|
||||||
|
|
||||||
pull_code() {
|
check_code() {
|
||||||
cd "${WORK_DIR}"
|
# 代码由用户上传,这里仅校验关键文件齐全
|
||||||
|
local missing=()
|
||||||
local script_md5=""
|
[ -f "${WORK_DIR}/backend/main.py" ] || missing+=("backend/main.py")
|
||||||
if command -v md5sum &>/dev/null; then
|
[ -f "${DOCKER_DIR}/backend/Dockerfile" ] || missing+=("docker/backend/Dockerfile")
|
||||||
script_md5=$(md5sum "$0" | awk '{print $1}')
|
[ -f "${DOCKER_DIR}/redis/conf/redis.conf" ] || missing+=("docker/redis/conf/redis.conf")
|
||||||
elif command -v md5 &>/dev/null; then
|
if [ ${#missing[@]} -gt 0 ]; then
|
||||||
script_md5=$(md5 -q "$0")
|
log "代码不完整,缺少: ${missing[*]}(请重新上传后重试)" "ERROR"; exit 1
|
||||||
fi
|
fi
|
||||||
if [ -d ".git" ]; then
|
log "✅ 代码完整性检查通过" "SUCCESS"
|
||||||
local branch
|
|
||||||
branch=$(git rev-parse --abbrev-ref HEAD)
|
|
||||||
log "分支: ${branch}"
|
|
||||||
local old_head
|
|
||||||
old_head=$(git rev-parse HEAD 2>/dev/null || echo "")
|
|
||||||
git fetch origin || true
|
|
||||||
git pull || { log "git pull 失败" "ERROR"; exit 1; }
|
|
||||||
local new_head
|
|
||||||
new_head=$(git rev-parse HEAD 2>/dev/null || echo "")
|
|
||||||
if [ -n "$old_head" ] && [ "$old_head" != "$new_head" ]; then
|
|
||||||
log "📋 本次拉取提交:"
|
|
||||||
git log --oneline --no-decorate "${old_head}..${new_head}" 2>/dev/null | sed 's/^/ /' || true
|
|
||||||
fi
|
|
||||||
# deploy.sh 自身有更新则重载
|
|
||||||
if [ -n "$script_md5" ]; then
|
|
||||||
local new_md5=""
|
|
||||||
command -v md5sum &>/dev/null && new_md5=$(md5sum "$0" | awk '{print $1}') || new_md5=$(md5 -q "$0")
|
|
||||||
if [ "$script_md5" != "$new_md5" ]; then
|
|
||||||
log "🔄 deploy.sh 已更新,重新执行..." "WARN"
|
|
||||||
exec "$0" "$@"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
git init && git remote add origin "${GIT_REPO}"
|
|
||||||
git pull origin master || git pull origin main || { log "拉取代码失败" "ERROR"; exit 1; }
|
|
||||||
fi
|
|
||||||
log "✅ 代码已更新至 $(git log -1 --oneline)" "SUCCESS"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
build_image() {
|
build_image() {
|
||||||
@@ -138,8 +114,10 @@ verify() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
docker system prune -a -f >/dev/null 2>&1 || true
|
# 仅清理悬空镜像/网络/构建缓存;-a 会删掉服务器上其他项目的未使用镜像,慎用
|
||||||
log "✅ 所有资源已清理" "SUCCESS"
|
docker image prune -f >/dev/null 2>&1 || true
|
||||||
|
docker builder prune -f >/dev/null 2>&1 || true
|
||||||
|
log "✅ 构建缓存已清理" "SUCCESS"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==================== 主流程 ====================
|
# ==================== 主流程 ====================
|
||||||
@@ -149,8 +127,7 @@ full_deploy() {
|
|||||||
log "时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
log "时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
load_env
|
load_env
|
||||||
check_deps
|
check_deps
|
||||||
stop_service
|
check_code
|
||||||
pull_code "$@"
|
|
||||||
build_image
|
build_image
|
||||||
start_service
|
start_service
|
||||||
verify
|
verify
|
||||||
@@ -172,7 +149,7 @@ case ${1:-} in
|
|||||||
clean) cleanup ;;
|
clean) cleanup ;;
|
||||||
help|-h|--help)
|
help|-h|--help)
|
||||||
echo "用法: $0 [命令]"
|
echo "用法: $0 [命令]"
|
||||||
echo " 无参数 完整部署(拉代码→构建→启动→清理)"
|
echo " 无参数 完整部署(校验代码→构建→启动→清理;代码需先上传至服务器)"
|
||||||
echo " start 启动服务"
|
echo " start 启动服务"
|
||||||
echo " stop 停止服务"
|
echo " stop 停止服务"
|
||||||
echo " restart 重启服务"
|
echo " restart 重启服务"
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ REDIS_PORT=6379
|
|||||||
|
|
||||||
# 后端配置
|
# 后端配置
|
||||||
BACKEND_PORT=8001
|
BACKEND_PORT=8001
|
||||||
|
BACKEND_IMAGE_TAG=3.0.0
|
||||||
|
# 日志级别(DEBUG/INFO/WARNING/ERROR)
|
||||||
|
LOGGER_LEVEL=INFO
|
||||||
|
|
||||||
# Nginx配置
|
# Nginx配置
|
||||||
HTTP_PORT=80
|
HTTP_PORT=80
|
||||||
|
|||||||
+3
-8
@@ -7,27 +7,22 @@
|
|||||||
```
|
```
|
||||||
docker/
|
docker/
|
||||||
├── backend/ # 后端服务配置
|
├── backend/ # 后端服务配置
|
||||||
│ └── Dockerfile # 后端 Dockerfile(多阶段构建)
|
│ └── Dockerfile # 后端 Dockerfile(依赖安装 + 代码复制,配合根 .dockerignore)
|
||||||
├── nginx/ # Nginx 配置
|
├── nginx/ # Nginx 配置
|
||||||
│ ├── nginx.conf # Nginx 配置文件
|
│ ├── nginx.conf # Nginx 配置文件
|
||||||
│ ├── ssl/ # SSL 证书目录(放置 server.key / server.pem)
|
│ ├── ssl/ # SSL 证书目录(放置 server.key / server.pem,.gitignore 已排除)
|
||||||
│ │ ├── server.key # SSL 私钥
|
|
||||||
│ │ └── server.pem # SSL 证书
|
|
||||||
│ ├── web/ # 前端静态文件(构建后自动放置 dist/)
|
│ ├── web/ # 前端静态文件(构建后自动放置 dist/)
|
||||||
│ └── app/ + docs/ # 移动端 H5 / 文档站点(按需启用)
|
│ └── app/ + docs/ # 移动端 H5 / 文档站点(按需启用)
|
||||||
├── mysql/ # MySQL 持久化 & 初始化
|
├── mysql/ # MySQL 持久化 & 初始化
|
||||||
│ ├── init/ # 首次启动时执行的 SQL 脚本
|
│ ├── init/ # 首次启动时执行的 SQL 脚本(可选)
|
||||||
│ │ └── init.sql
|
|
||||||
│ └── data/ # 数据库数据文件(自动生成)
|
│ └── data/ # 数据库数据文件(自动生成)
|
||||||
├── redis/ # Redis 持久化 & 配置
|
├── redis/ # Redis 持久化 & 配置
|
||||||
│ ├── conf/
|
│ ├── conf/
|
||||||
│ │ └── redis.conf # Redis 持久化配置文件
|
│ │ └── redis.conf # Redis 持久化配置文件
|
||||||
│ └── data/ # Redis 数据文件(自动生成)
|
│ └── data/ # Redis 数据文件(自动生成)
|
||||||
├── docker-compose.yaml # Docker Compose 编排文件
|
├── docker-compose.yaml # Docker Compose 编排文件
|
||||||
├── docker-compose-example.yaml # 编排文件示例
|
|
||||||
├── .env # 环境变量配置文件(.gitignore 已排除)
|
├── .env # 环境变量配置文件(.gitignore 已排除)
|
||||||
├── .env.example # 环境变量示例文件
|
├── .env.example # 环境变量示例文件
|
||||||
├── env.sh # 环境变量加载脚本
|
|
||||||
└── README.md # 本文档
|
└── README.md # 本文档
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 使用python:3.12-slim 镜像作为基础镜像
|
# 使用 python:3.12-slim 镜像作为基础镜像
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
# 使用 LABEL 替代 MAINTAINER
|
# 使用 LABEL 替代 MAINTAINER
|
||||||
@@ -10,12 +10,15 @@ ENV TZ Asia/Shanghai
|
|||||||
# 设置容器内工作目录
|
# 设置容器内工作目录
|
||||||
WORKDIR /home
|
WORKDIR /home
|
||||||
|
|
||||||
# 将当前主机目录全部文件复制至容器工作目录
|
# 先复制依赖清单并安装(利用 layer 缓存:代码变更时不重装依赖)
|
||||||
COPY ./backend/requirements.txt .
|
COPY ./backend/requirements.txt .
|
||||||
|
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
|
||||||
|
# 复制应用代码(backend/.venv、logs、缓存等由根 .dockerignore 排除)
|
||||||
|
COPY ./backend/ .
|
||||||
|
|
||||||
# 暴露端口
|
# 暴露端口
|
||||||
EXPOSE 8001
|
EXPOSE 8001
|
||||||
|
|
||||||
|
|||||||
@@ -117,11 +117,14 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${BACKEND_PORT:-8001}:8001"
|
- "${BACKEND_PORT:-8001}:8001"
|
||||||
volumes:
|
volumes:
|
||||||
# ── 注意 ──────────────────────────────────
|
# ── 开发热更新(可选)─────────────────────
|
||||||
# 以下挂载将宿主机代码覆盖容器内代码,实现热更新。
|
# 取消注释将宿主机代码覆盖容器内代码。生产环境保持注释,
|
||||||
# 生产环境建议注释掉该行,使用镜像内自带的代码。
|
# 使用镜像内自带的代码(Dockerfile 已 COPY backend)。
|
||||||
# ─────────────────────────────────────────
|
# ─────────────────────────────────────────
|
||||||
- ../backend:/home
|
# - ../backend:/home
|
||||||
|
# 本地存储源根目录持久化(种子节点 host=static/upload):
|
||||||
|
# 必须挂载,否则每次部署重建容器会丢失用户上传的文件
|
||||||
|
- ./backend/static/upload:/home/static/upload
|
||||||
depends_on:
|
depends_on:
|
||||||
mysql:
|
mysql:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -133,7 +136,7 @@ services:
|
|||||||
test:
|
test:
|
||||||
[
|
[
|
||||||
"CMD-SHELL",
|
"CMD-SHELL",
|
||||||
"python -c 'import urllib.request; urllib.request.urlopen(\"http://localhost:8001/monitor/health\")' || exit 1",
|
"python -c 'import urllib.request; urllib.request.urlopen(\"http://localhost:8001/api/v1/monitor/health/check\", timeout=5)' || exit 1",
|
||||||
]
|
]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
@@ -167,7 +170,6 @@ services:
|
|||||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
- ./nginx/web:/usr/share/nginx/html/web:ro
|
- ./nginx/web:/usr/share/nginx/html/web:ro
|
||||||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||||
# 按需启用(取消注释):
|
|
||||||
- ./nginx/app:/usr/share/nginx/html/app:ro
|
- ./nginx/app:/usr/share/nginx/html/app:ro
|
||||||
- ./nginx/docs:/usr/share/nginx/html/docs:ro
|
- ./nginx/docs:/usr/share/nginx/html/docs:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ http {
|
|||||||
|
|
||||||
# ==================== HTTPS Server ====================
|
# ==================== HTTPS Server ====================
|
||||||
server {
|
server {
|
||||||
listen 443 ssl http2;
|
listen 443 ssl;
|
||||||
|
http2 on;
|
||||||
server_name service.fastapiadmin.com;
|
server_name service.fastapiadmin.com;
|
||||||
|
|
||||||
# ==================== SSL 配置 ====================
|
# ==================== SSL 配置 ====================
|
||||||
@@ -113,6 +114,9 @@ http {
|
|||||||
|
|
||||||
# ==================== 后端 API 代理 ====================
|
# ==================== 后端 API 代理 ====================
|
||||||
location /api/v1 {
|
location /api/v1 {
|
||||||
|
limit_req zone=api_limit burst=60 nodelay;
|
||||||
|
limit_conn conn_limit 100;
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# FastapiAdmin Redis 配置(容器内路径 /usr/local/etc/redis/redis.conf)
|
||||||
|
# 密码由 docker-compose 的 command --requirepass 注入,勿写在此文件
|
||||||
|
|
||||||
|
# 持久化
|
||||||
|
appendonly yes
|
||||||
|
appendfsync everysec
|
||||||
|
save 900 1
|
||||||
|
save 300 10
|
||||||
|
save 60 10000
|
||||||
|
|
||||||
|
# 内存(与 compose 的 512m 容器限额匹配)
|
||||||
|
maxmemory 384mb
|
||||||
|
maxmemory-policy allkeys-lru
|
||||||
|
|
||||||
|
# 慢日志
|
||||||
|
slowlog-log-slower-than 10000
|
||||||
|
slowlog-max-len 128
|
||||||
@@ -169,10 +169,9 @@ const UserAPI = {
|
|||||||
|
|
||||||
export default UserAPI
|
export default UserAPI
|
||||||
|
|
||||||
/* 忘记密码表单(与后端 UserForgetPasswordSchema 一致,confirmPassword 为前端校验字段不提交) */
|
/* 忘记密码申请(与后端 UserForgetPasswordSchema 一致:仅用户名,后端不会直接改密) */
|
||||||
export interface ForgetPasswordForm {
|
export interface ForgetPasswordForm {
|
||||||
username: string
|
username: string
|
||||||
new_password: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 注册表单 */
|
/* 注册表单 */
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ const submitting = ref(false)
|
|||||||
const forgetFormRef = ref()
|
const forgetFormRef = ref()
|
||||||
const forgetForm = reactive({
|
const forgetForm = reactive({
|
||||||
username: '',
|
username: '',
|
||||||
new_password: '',
|
|
||||||
confirmPassword: '',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 与后端 UserForgetPasswordSchema 一致:字母开头,3-32 位,仅允许字母、数字、_ . - */
|
/** 与后端 UserForgetPasswordSchema 一致:字母开头,3-32 位,仅允许字母、数字、_ . - */
|
||||||
@@ -29,25 +27,15 @@ const forgetSchema: FormSchema = {
|
|||||||
validate: (model) => {
|
validate: (model) => {
|
||||||
const errors: Array<{ path: Array<string | number>, message: string }> = []
|
const errors: Array<{ path: Array<string | number>, message: string }> = []
|
||||||
const username = String(model.username ?? '').trim()
|
const username = String(model.username ?? '').trim()
|
||||||
const password = String(model.new_password ?? '')
|
|
||||||
const confirmPassword = String(model.confirmPassword ?? '')
|
|
||||||
if (!username)
|
if (!username)
|
||||||
errors.push({ path: ['username'], message: t('common.form.usernameRequired') })
|
errors.push({ path: ['username'], message: t('common.form.usernameRequired') })
|
||||||
else if (!USERNAME_REG.test(username))
|
else if (!USERNAME_REG.test(username))
|
||||||
errors.push({ path: ['username'], message: t('common.form.usernameFormat') })
|
errors.push({ path: ['username'], message: t('common.form.usernameFormat') })
|
||||||
if (!password)
|
|
||||||
errors.push({ path: ['new_password'], message: t('common.form.newPasswordRequired') })
|
|
||||||
else if (password.length < 6)
|
|
||||||
errors.push({ path: ['new_password'], message: t('common.form.passwordLength') })
|
|
||||||
if (!confirmPassword)
|
|
||||||
errors.push({ path: ['confirmPassword'], message: t('common.form.confirmNewRequired') })
|
|
||||||
else if (confirmPassword !== password)
|
|
||||||
errors.push({ path: ['confirmPassword'], message: t('common.form.mismatch') })
|
|
||||||
return errors
|
return errors
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重置密码成功后记住用户名,回登录页由用户用新密码登录 */
|
/** 提交忘记密码申请:后端不会直接改密,成功后提示联系管理员 */
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (submitting.value)
|
if (submitting.value)
|
||||||
return
|
return
|
||||||
@@ -58,13 +46,13 @@ async function handleSubmit() {
|
|||||||
const username = forgetForm.username.trim()
|
const username = forgetForm.username.trim()
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
await UserAPI.forgetPassword({ username, new_password: forgetForm.new_password })
|
await UserAPI.forgetPassword({ username })
|
||||||
Storage.set(REMEMBER_ME_KEY, { username, remember: true })
|
Storage.set(REMEMBER_ME_KEY, { username, remember: true })
|
||||||
toast.success(t('forget.success'))
|
toast.success(t('forget.success'))
|
||||||
uni.reLaunch({ url: '/pages/login/index' })
|
uni.reLaunch({ url: '/pages/login/index' })
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
// http 层已统一错误提示(如用户不存在)
|
// http 层已统一错误提示
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
@@ -94,26 +82,6 @@ function goLogin() {
|
|||||||
prefix-icon="user"
|
prefix-icon="user"
|
||||||
/>
|
/>
|
||||||
</wd-form-item>
|
</wd-form-item>
|
||||||
<wd-form-item prop="new_password" custom-style="margin-bottom: 14rpx; padding-left: 0; padding-right: 0;">
|
|
||||||
<wd-input
|
|
||||||
v-model="forgetForm.new_password"
|
|
||||||
:placeholder="t('common.form.newPasswordPlaceholder')"
|
|
||||||
show-password
|
|
||||||
clearable
|
|
||||||
:compact="false"
|
|
||||||
prefix-icon="lock"
|
|
||||||
/>
|
|
||||||
</wd-form-item>
|
|
||||||
<wd-form-item prop="confirmPassword" custom-style="margin-bottom: 14rpx; padding-left: 0; padding-right: 0;">
|
|
||||||
<wd-input
|
|
||||||
v-model="forgetForm.confirmPassword"
|
|
||||||
:placeholder="t('common.form.confirmNewPlaceholder')"
|
|
||||||
show-password
|
|
||||||
clearable
|
|
||||||
:compact="false"
|
|
||||||
prefix-icon="lock"
|
|
||||||
/>
|
|
||||||
</wd-form-item>
|
|
||||||
</wd-form>
|
</wd-form>
|
||||||
<wd-button type="primary" round block :loading="submitting" @click="handleSubmit">
|
<wd-button type="primary" round block :loading="submitting" @click="handleSubmit">
|
||||||
{{ submitting ? t('forget.submitting') : t('forget.submit') }}
|
{{ submitting ? t('forget.submitting') : t('forget.submit') }}
|
||||||
|
|||||||
@@ -141,8 +141,6 @@ export default UserAPI;
|
|||||||
|
|
||||||
export interface ForgetPasswordForm {
|
export interface ForgetPasswordForm {
|
||||||
username: string;
|
username: string;
|
||||||
new_password: string;
|
|
||||||
confirmPassword: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterForm {
|
export interface RegisterForm {
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
<el-link type="primary" :href="modelValue" target="_blank" :underline="false">
|
<el-link type="primary" :href="modelValue" target="_blank" :underline="false">
|
||||||
{{ fileName }}
|
{{ fileName }}
|
||||||
</el-link>
|
</el-link>
|
||||||
<el-icon class="file-item-action" title="移除" @click="handleRemove"><CircleCloseFilled /></el-icon>
|
<el-icon class="file-item-action" title="移除" @click="handleRemove"
|
||||||
|
><CircleCloseFilled
|
||||||
|
/></el-icon>
|
||||||
</div>
|
</div>
|
||||||
<el-button v-else type="primary" plain :loading="uploading" @click="fileInputRef?.click()">
|
<el-button v-else type="primary" plain :loading="uploading" @click="fileInputRef?.click()">
|
||||||
<el-icon class="mr-1"><UploadFilled /></el-icon>
|
<el-icon class="mr-1"><UploadFilled /></el-icon>
|
||||||
@@ -115,20 +117,20 @@ const doUpload = async (file: File) => {
|
|||||||
|
|
||||||
.fa-file-upload .file-item {
|
.fa-file-upload .file-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa-file-upload .file-item-icon {
|
.fa-file-upload .file-item-icon {
|
||||||
color: var(--el-color-primary);
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa-file-upload .file-item-action {
|
.fa-file-upload .file-item-action {
|
||||||
cursor: pointer;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa-file-upload .file-item-action:hover {
|
.fa-file-upload .file-item-action:hover {
|
||||||
@@ -138,7 +140,7 @@ const doUpload = async (file: File) => {
|
|||||||
.fa-file-upload .file-tip {
|
.fa-file-upload .file-tip {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -22,11 +22,12 @@
|
|||||||
>
|
>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
:prop="item.key"
|
:prop="item.key"
|
||||||
|
:label="typeof item.label === 'string' ? item.label : undefined"
|
||||||
:label-width="item.label ? item.labelWidth || labelWidth : undefined"
|
:label-width="item.label ? item.labelWidth || labelWidth : undefined"
|
||||||
>
|
>
|
||||||
<template #label v-if="item.label">
|
<!-- 字符串 label 走 ElFormItem 默认渲染使 label-suffix 生效;组件 label 保留插槽 -->
|
||||||
<component v-if="typeof item.label !== 'string'" :is="item.label" />
|
<template v-if="item.label && typeof item.label !== 'string'" #label>
|
||||||
<span v-else>{{ item.label }}</span>
|
<component :is="item.label" />
|
||||||
</template>
|
</template>
|
||||||
<slot :name="item.key" :item="item" :modelValue="modelValue">
|
<slot :name="item.key" :item="item" :modelValue="modelValue">
|
||||||
<component
|
<component
|
||||||
@@ -119,11 +120,12 @@
|
|||||||
>
|
>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
:prop="item.key"
|
:prop="item.key"
|
||||||
|
:label="typeof item.label === 'string' ? item.label : undefined"
|
||||||
:label-width="item.label ? item.labelWidth || labelWidth : undefined"
|
:label-width="item.label ? item.labelWidth || labelWidth : undefined"
|
||||||
>
|
>
|
||||||
<template #label v-if="item.label">
|
<!-- 字符串 label 走 ElFormItem 默认渲染使 label-suffix 生效;组件 label 保留插槽 -->
|
||||||
<component v-if="typeof item.label !== 'string'" :is="item.label" />
|
<template v-if="item.label && typeof item.label !== 'string'" #label>
|
||||||
<span v-else>{{ item.label }}</span>
|
<component :is="item.label" />
|
||||||
</template>
|
</template>
|
||||||
<slot :name="item.key" :item="item" :modelValue="modelValue">
|
<slot :name="item.key" :item="item" :modelValue="modelValue">
|
||||||
<component
|
<component
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
* `fetchDataQuiet` 吞掉 reject,避免页面重复 try/catch。
|
* `fetchDataQuiet` 吞掉 reject,避免页面重复 try/catch。
|
||||||
*
|
*
|
||||||
* 去重:
|
* 去重:
|
||||||
* - 同实例:`inFlightDedupePromise` + `inFlightDedupeKey`
|
* - 仅限单个 useTable 实例内部:`inFlightDedupePromise` + `inFlightDedupeKey`(同参进行中的请求复用 Promise)。
|
||||||
* - 跨实例(同参):模块级 `globalListNetworkInflight`(布局短时多挂载时合并为一条 HTTP)
|
* - 不做跨实例去重:key 仅由参数生成,不同接口的同参请求会碰撞串台(PR #516)。
|
||||||
*
|
*
|
||||||
* KeepAlive:`onDeactivated` 会取消请求并置 `tableViewActive`,失活期间 `fetchData` 直接返回当前快照不发 HTTP。
|
* KeepAlive:`onDeactivated` 会取消请求并置 `tableViewActive`,失活期间 `fetchData` 直接返回当前快照不发 HTTP。
|
||||||
*/
|
*/
|
||||||
@@ -44,9 +44,6 @@ import {
|
|||||||
} from "@utils";
|
} from "@utils";
|
||||||
import type { ColumnOption } from "@/types/component";
|
import type { ColumnOption } from "@/types/component";
|
||||||
|
|
||||||
/** 跨组件实例:同一 dedupeKey 仅一条进行中的网络请求 */
|
|
||||||
const globalListNetworkInflight = new Map<string, Promise<TableResponse<unknown>>>();
|
|
||||||
|
|
||||||
// --- 类型推导(由 apiFn / 响应类型反推记录类型) ---
|
// --- 类型推导(由 apiFn / 响应类型反推记录类型) ---
|
||||||
type InferApiParams<T> = T extends (params: infer P) => any ? P : never;
|
type InferApiParams<T> = T extends (params: infer P) => any ? P : never;
|
||||||
type UnwrapAxiosResponse<T> = T extends AxiosResponse<infer D> ? D : T;
|
type UnwrapAxiosResponse<T> = T extends AxiosResponse<infer D> ? D : T;
|
||||||
@@ -389,26 +386,6 @@ function useTableImpl<TApiFn extends (params: any) => Promise<any>>(
|
|||||||
return inFlightDedupePromise;
|
return inFlightDedupePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sharedGlobal = globalListNetworkInflight.get(dedupeKey);
|
|
||||||
if (sharedGlobal) {
|
|
||||||
loadingState.value = "loading";
|
|
||||||
error.value = null;
|
|
||||||
try {
|
|
||||||
const standardResponse = (await sharedGlobal) as TableResponse<TRecord>;
|
|
||||||
commitNetworkSuccess(standardResponse, requestParams, useCache);
|
|
||||||
return standardResponse;
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof Error && err.message === "请求已取消") {
|
|
||||||
loadingState.value = "idle";
|
|
||||||
return { records: [], total: 0, current: 1, size: 10 };
|
|
||||||
}
|
|
||||||
loadingState.value = "error";
|
|
||||||
data.value = [];
|
|
||||||
const tableError = handleError(err, "获取表格数据失败");
|
|
||||||
throw tableError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableViewActive) {
|
if (!tableViewActive) {
|
||||||
return {
|
return {
|
||||||
records: [...data.value],
|
records: [...data.value],
|
||||||
@@ -477,13 +454,6 @@ function useTableImpl<TApiFn extends (params: any) => Promise<any>>(
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
globalListNetworkInflight.set(dedupeKey, networkPromise as Promise<TableResponse<unknown>>);
|
|
||||||
networkPromise
|
|
||||||
.finally(() => {
|
|
||||||
globalListNetworkInflight.delete(dedupeKey);
|
|
||||||
})
|
|
||||||
.catch(() => {}); // 忽略取消/reject,仅用于清理全局 Map
|
|
||||||
|
|
||||||
inFlightDedupePromise = networkPromise;
|
inFlightDedupePromise = networkPromise;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -826,10 +796,8 @@ function useTableImpl<TApiFn extends (params: any) => Promise<any>>(
|
|||||||
removeColumn: columnConfig.removeColumn,
|
removeColumn: columnConfig.removeColumn,
|
||||||
/** 切换列显示状态 */
|
/** 切换列显示状态 */
|
||||||
toggleColumn: columnConfig.toggleColumn,
|
toggleColumn: columnConfig.toggleColumn,
|
||||||
/** 更新列配置 */
|
/** 更新列配置(支持单个或批量,批量传数组) */
|
||||||
updateColumn: columnConfig.updateColumn,
|
updateColumn: columnConfig.updateColumn,
|
||||||
/** 批量更新列配置 */
|
|
||||||
batchUpdateColumns: columnConfig.batchUpdateColumns,
|
|
||||||
/** 重新排序列 */
|
/** 重新排序列 */
|
||||||
reorderColumns: columnConfig.reorderColumns,
|
reorderColumns: columnConfig.reorderColumns,
|
||||||
/** 获取指定列配置 */
|
/** 获取指定列配置 */
|
||||||
|
|||||||
@@ -183,29 +183,29 @@ export const MANUAL_MODULES_AFTER_SYSTEM: ManualModuleSection[] = [
|
|||||||
{
|
{
|
||||||
anchor: "page-workflow",
|
anchor: "page-workflow",
|
||||||
title: "流程管理",
|
title: "流程管理",
|
||||||
path: "module_storage/workflow/index.vue",
|
path: "module_task/storage/workflow/index.vue",
|
||||||
notes: [
|
notes: [
|
||||||
"检索:FaSearchBar(可展开)。",
|
"检索:FaSearchBar(可展开)。",
|
||||||
"工具栏:新增、批量删除、刷新、列配置。",
|
"工具栏:新增、批量删除、刷新、列配置。",
|
||||||
"流程定义:源节点 + 目标节点列表(parallel 多目标 / chain 链式),目标路径随行配置。",
|
"流程定义:源节点 + 目标节点列表(parallel 多目标 / chain 链式),目标路径随行配置。",
|
||||||
"权限:`module_storage:workflow:flow:create|delete|update` + 行级操作。",
|
"权限:`module_task:storage:flow:create|delete|update` + 行级操作。",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
anchor: "page-workflow-node",
|
anchor: "page-workflow-node",
|
||||||
title: "节点管理",
|
title: "节点管理",
|
||||||
path: "module_storage/node/index.vue",
|
path: "module_task/storage/node/index.vue",
|
||||||
notes: [
|
notes: [
|
||||||
"检索:FaSearchBar(可展开)。",
|
"检索:FaSearchBar(可展开)。",
|
||||||
"节点即传输地址(存储源):FTP/FTPS/SFTP/S3/OBS/OSS/COS/本地目录。",
|
"节点即传输地址(存储源):FTP/FTPS/SFTP/S3/OBS/OSS/COS/本地目录。",
|
||||||
"行操作:打开(进入存储管理)、测试连接、详情、编辑、删除。",
|
"行操作:打开(进入存储管理)、测试连接、详情、编辑、删除。",
|
||||||
"权限:`module_storage:workflow:node:create|delete|update` + 行级操作。",
|
"权限:`module_task:storage:node:create|delete|update` + 行级操作。",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
anchor: "page-workflow-storage",
|
anchor: "page-workflow-storage",
|
||||||
title: "文件浏览",
|
title: "文件浏览",
|
||||||
path: "module_storage/browse/index.vue",
|
path: "module_task/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_storage/transfer/index.vue",
|
path: "module_task/storage/transfer/index.vue",
|
||||||
notes: [
|
notes: [
|
||||||
"创建传输任务:可选「从流程自动填充」源节点与多目标,或手动指定。",
|
"创建传输任务:可选「从流程自动填充」源节点与多目标,或手动指定。",
|
||||||
"实时进度:WebSocket 推送任务/步骤状态与速度。",
|
"实时进度:WebSocket 推送任务/步骤状态与速度。",
|
||||||
|
|||||||
+22
-3
@@ -408,7 +408,7 @@ const getInfo = async () => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
const response = await CacheAPI.getCacheInfo();
|
const response = await CacheAPI.getCacheInfo();
|
||||||
cache.value = response.data.data || { info: {}, command_stats: [], db_size: 0 };
|
cache.value = response.data.data || { info: {}, command_stats: [], db_size: 0 };
|
||||||
initCharts();
|
void initCharts();
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (import.meta.env.DEV) console.error("获取缓存监控数据失败:", error);
|
if (import.meta.env.DEV) console.error("获取缓存监控数据失败:", error);
|
||||||
ElMessage.error("获取缓存监控数据失败");
|
ElMessage.error("获取缓存监控数据失败");
|
||||||
@@ -417,8 +417,27 @@ const getInfo = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const initCharts = () => {
|
/** 等待容器完成布局(路由过渡/首次渲染时 clientWidth/Height 可能为 0),避免 ECharts "Can't get DOM width or height" 警告 */
|
||||||
|
const waitForSize = (el: HTMLElement): Promise<void> =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
const check = () => {
|
||||||
|
// 组件卸载后 template ref 置 null,此时 ref 不再指向 el,直接结束避免悬挂循环
|
||||||
|
if (
|
||||||
|
(commandstats.value !== el && usedmemory.value !== el) ||
|
||||||
|
(el.clientWidth > 0 && el.clientHeight > 0)
|
||||||
|
) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
requestAnimationFrame(check);
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
});
|
||||||
|
|
||||||
|
const initCharts = async () => {
|
||||||
if (!commandstats.value || !usedmemory.value) return;
|
if (!commandstats.value || !usedmemory.value) return;
|
||||||
|
await Promise.all([waitForSize(commandstats.value), waitForSize(usedmemory.value)]);
|
||||||
|
if (!commandstats.value || !usedmemory.value) return; // 等待期间组件被卸载
|
||||||
|
|
||||||
commandstatsInstance = echarts.init(commandstats.value, "macarons");
|
commandstatsInstance = echarts.init(commandstats.value, "macarons");
|
||||||
usedmemoryInstance = echarts.init(usedmemory.value, "macarons");
|
usedmemoryInstance = echarts.init(usedmemory.value, "macarons");
|
||||||
@@ -480,7 +499,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
watch(activeTab, (tab) => {
|
watch(activeTab, (tab) => {
|
||||||
if (tab === "0") {
|
if (tab === "0") {
|
||||||
nextTick(() => initCharts());
|
nextTick(() => void initCharts());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+2
-48
@@ -22,42 +22,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</ElInput>
|
</ElInput>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElTooltip :visible="isCapsLock" :content="$t('login.capsLock')" placement="right">
|
|
||||||
<ElFormItem prop="new_password">
|
|
||||||
<ElInput
|
|
||||||
v-model.trim="forgetForm.new_password"
|
|
||||||
class="custom-height"
|
|
||||||
type="password"
|
|
||||||
autocomplete="off"
|
|
||||||
show-password
|
|
||||||
clearable
|
|
||||||
:placeholder="$t('login.placeholder.password')"
|
|
||||||
@keyup="checkCapsLock"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<ElIcon><Lock /></ElIcon>
|
|
||||||
</template>
|
|
||||||
</ElInput>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElTooltip>
|
|
||||||
<ElTooltip :visible="isCapsLock" :content="$t('login.capsLock')" placement="right">
|
|
||||||
<ElFormItem prop="confirmPassword">
|
|
||||||
<ElInput
|
|
||||||
v-model.trim="forgetForm.confirmPassword"
|
|
||||||
class="custom-height"
|
|
||||||
type="password"
|
|
||||||
autocomplete="off"
|
|
||||||
show-password
|
|
||||||
clearable
|
|
||||||
:placeholder="$t('login.message.password.confirm')"
|
|
||||||
@keyup="checkCapsLock"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<ElIcon><Lock /></ElIcon>
|
|
||||||
</template>
|
|
||||||
</ElInput>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElTooltip>
|
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<ElButton
|
<ElButton
|
||||||
class="h-11 w-full min-w-0 rounded-lg! text-base font-medium"
|
class="h-11 w-full min-w-0 rounded-lg! text-base font-medium"
|
||||||
@@ -80,7 +44,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Lock, User } from "@element-plus/icons-vue";
|
import { User } from "@element-plus/icons-vue";
|
||||||
import type { ForgetPasswordForm } from "@/api/module_system/user";
|
import type { ForgetPasswordForm } from "@/api/module_system/user";
|
||||||
import type { FormRules } from "element-plus";
|
import type { FormRules } from "element-plus";
|
||||||
|
|
||||||
@@ -101,19 +65,9 @@ interface Emits {
|
|||||||
toLogin: [];
|
toLogin: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<Emits>();
|
defineEmits<Emits>();
|
||||||
|
|
||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
const isCapsLock = ref(false);
|
|
||||||
|
|
||||||
function checkCapsLock(event: KeyboardEvent) {
|
|
||||||
if (event instanceof KeyboardEvent) {
|
|
||||||
isCapsLock.value = event.getModifierState("CapsLock");
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
emit("submit");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
validate: () => formRef.value?.validate?.(),
|
validate: () => formRef.value?.validate?.(),
|
||||||
|
|||||||
@@ -381,8 +381,6 @@ const registerForm = reactive<RegisterForm>({
|
|||||||
|
|
||||||
const forgetForm = reactive<ForgetPasswordForm>({
|
const forgetForm = reactive<ForgetPasswordForm>({
|
||||||
username: "",
|
username: "",
|
||||||
new_password: "",
|
|
||||||
confirmPassword: "",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const validateRegisterPassword = (_rule: unknown, value: string, callback: (e?: Error) => void) => {
|
const validateRegisterPassword = (_rule: unknown, value: string, callback: (e?: Error) => void) => {
|
||||||
@@ -422,29 +420,8 @@ const registerRules = computed<FormRules<RegisterForm>>(() => ({
|
|||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const validateForgetConfirm = (_rule: unknown, value: string, callback: (e?: Error) => void) => {
|
|
||||||
if (!value) {
|
|
||||||
callback(new Error(t("login.message.password.required")));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (value !== forgetForm.new_password) {
|
|
||||||
callback(new Error(t("login.message.password.inconformity")));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
callback();
|
|
||||||
};
|
|
||||||
|
|
||||||
const forgetRules = computed<FormRules<ForgetPasswordForm>>(() => ({
|
const forgetRules = computed<FormRules<ForgetPasswordForm>>(() => ({
|
||||||
username: [{ required: true, message: t("login.message.username.required"), trigger: "blur" }],
|
username: [{ required: true, message: t("login.message.username.required"), trigger: "blur" }],
|
||||||
new_password: [
|
|
||||||
{ required: true, message: t("login.message.password.required"), trigger: "blur" },
|
|
||||||
{ min: 6, message: t("login.message.password.min"), trigger: "blur" },
|
|
||||||
],
|
|
||||||
confirmPassword: [
|
|
||||||
{ required: true, message: t("login.message.password.required"), trigger: "blur" },
|
|
||||||
{ min: 6, message: t("login.message.password.min"), trigger: "blur" },
|
|
||||||
{ validator: validateForgetConfirm, trigger: "blur" },
|
|
||||||
],
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const loginForm = reactive<LoginFormData>({
|
const loginForm = reactive<LoginFormData>({
|
||||||
@@ -671,11 +648,7 @@ async function submitForget() {
|
|||||||
await forgetPanelRef.value.validate?.();
|
await forgetPanelRef.value.validate?.();
|
||||||
forgetLoading.value = true;
|
forgetLoading.value = true;
|
||||||
await UserAPI.forgetPassword(forgetForm);
|
await UserAPI.forgetPassword(forgetForm);
|
||||||
loginForm.username = forgetForm.username;
|
|
||||||
loginForm.password = forgetForm.new_password;
|
|
||||||
forgetForm.username = "";
|
forgetForm.username = "";
|
||||||
forgetForm.new_password = "";
|
|
||||||
forgetForm.confirmPassword = "";
|
|
||||||
setAuthPanel("login");
|
setAuthPanel("login");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Login] forget password:", error);
|
console.error("[Login] forget password:", error);
|
||||||
|
|||||||
@@ -276,9 +276,10 @@ const jobStoreOptions: OptionItem[] = [
|
|||||||
{ label: "内存(Memory)", value: "memory" },
|
{ label: "内存(Memory)", value: "memory" },
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 执行器选项(对应 ap_scheduler executors 配置,不再依赖字典) */
|
/** 执行器选项(对应 ap_scheduler executors 配置:default=AsyncIOExecutor / threadpool / processpool) */
|
||||||
const jobExecutorOptions: OptionItem[] = [
|
const jobExecutorOptions: OptionItem[] = [
|
||||||
{ label: "线程池", value: "default" },
|
{ label: "异步协程(事件循环)", value: "default" },
|
||||||
|
{ label: "线程池", value: "threadpool" },
|
||||||
{ label: "进程池", value: "processpool" },
|
{ label: "进程池", value: "processpool" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -833,7 +834,7 @@ const initialFormData: Partial<NodeForm> = {
|
|||||||
id: undefined,
|
id: undefined,
|
||||||
name: "",
|
name: "",
|
||||||
code: undefined,
|
code: undefined,
|
||||||
jobstore: "sqlalchemy",
|
jobstore: "default",
|
||||||
executor: "default",
|
executor: "default",
|
||||||
func: defaultCodeBlock,
|
func: defaultCodeBlock,
|
||||||
args: undefined,
|
args: undefined,
|
||||||
@@ -931,7 +932,9 @@ async function handleSubmit() {
|
|||||||
? formData.value.start_date || undefined
|
? formData.value.start_date || undefined
|
||||||
: undefined,
|
: undefined,
|
||||||
end_date:
|
end_date:
|
||||||
trig === "cron" || trig === "interval" ? formData.value.end_date || undefined : undefined,
|
trig === "cron" || trig === "interval"
|
||||||
|
? formData.value.end_date || undefined
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
if (id) {
|
if (id) {
|
||||||
await NodeAPI.updateNode(id, submitData);
|
await NodeAPI.updateNode(id, submitData);
|
||||||
|
|||||||
+7
-7
@@ -27,8 +27,8 @@
|
|||||||
<template #left>
|
<template #left>
|
||||||
<FaTableHeaderLeft
|
<FaTableHeaderLeft
|
||||||
:remove-ids="selectedIds"
|
:remove-ids="selectedIds"
|
||||||
:perm-create="['module_storage:workflow:node:create']"
|
:perm-create="['module_task:storage:node:create']"
|
||||||
:perm-delete="['module_storage:workflow:node:delete']"
|
:perm-delete="['module_task:storage:node:delete']"
|
||||||
:delete-loading="batchDeleting"
|
:delete-loading="batchDeleting"
|
||||||
:create-loading="createLoading"
|
:create-loading="createLoading"
|
||||||
@add="handleAdd"
|
@add="handleAdd"
|
||||||
@@ -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_storage:workflow:node:query",
|
perm: "module_task:storage:node:query",
|
||||||
run: () => ctx.onTest(row.id!),
|
run: () => ctx.onTest(row.id!),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "detail",
|
key: "detail",
|
||||||
label: "详情",
|
label: "详情",
|
||||||
artType: "view",
|
artType: "view",
|
||||||
perm: "module_storage:workflow:node:query",
|
perm: "module_task:storage: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_storage:workflow:node:update",
|
perm: "module_task:storage: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_storage:workflow:node:delete",
|
perm: "module_task:storage: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/storage/workflow/browse",
|
path: "/task/storage/browse",
|
||||||
query: { source_id: row.id, name: row.name, protocol: row.protocol },
|
query: { source_id: row.id, name: row.name, protocol: row.protocol },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
+163
-132
@@ -34,121 +34,113 @@
|
|||||||
:class="sseConnected ? 'bg-(--el-color-success)' : 'bg-(--el-color-info)'"
|
:class="sseConnected ? 'bg-(--el-color-success)' : 'bg-(--el-color-info)'"
|
||||||
/>{{ sseConnected ? "实时连接" : "连接中…" }}
|
/>{{ sseConnected ? "实时连接" : "连接中…" }}
|
||||||
</span>
|
</span>
|
||||||
<el-button type="primary" @click="openCreate">新建传输任务</el-button>
|
<ElButton
|
||||||
|
v-hasPerm="['module_task:storage:transfer:create']"
|
||||||
|
type="primary"
|
||||||
|
@click="openCreate"
|
||||||
|
>新建传输任务</ElButton
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</FaTableHeader>
|
</FaTableHeader>
|
||||||
|
|
||||||
<div class="min-h-0 flex-1">
|
<FaTable
|
||||||
<FaTable
|
:data="tasks"
|
||||||
:data="tasks"
|
:columns="columns"
|
||||||
:columns="columns"
|
:pagination="{ current: page, size: pageSize, total }"
|
||||||
:pagination="{ current: page, size: pageSize, total }"
|
@pagination:size-change="onPageSizeChange"
|
||||||
stripe
|
@pagination:current-change="onPageChange"
|
||||||
border
|
>
|
||||||
height="100%"
|
<template #task_type="{ row }">
|
||||||
@pagination:size-change="onPageSizeChange"
|
<ElTag :type="row.task_type === 'chain' ? 'warning' : 'primary'" size="small">
|
||||||
@pagination:current-change="onPageChange"
|
{{ row.task_type === "chain" ? "链式" : "多目标" }}
|
||||||
>
|
</ElTag>
|
||||||
<template #task_type="{ row }">
|
</template>
|
||||||
<el-tag :type="row.task_type === 'chain' ? 'warning' : 'primary'" size="small">
|
<template #source="{ row }">
|
||||||
{{ row.task_type === "chain" ? "链式" : "多目标" }}
|
<div class="min-w-0">
|
||||||
</el-tag>
|
<div>
|
||||||
</template>
|
<ElTag v-if="row.source_type === 'local'" size="small" type="success">本地</ElTag>
|
||||||
<template #source="{ row }">
|
<ElTag v-else size="small" type="info">{{
|
||||||
<div class="min-w-0">
|
protoText(sourceById(row.source_id))
|
||||||
<div>
|
}}</ElTag>
|
||||||
<el-tag v-if="row.source_type === 'local'" size="small" type="success">本地</el-tag>
|
<span class="ml-1 text-(--el-text-color-regular)">{{
|
||||||
<el-tag v-else size="small" type="info">{{
|
row.source_type === "local"
|
||||||
protoText(sourceById(row.source_id))
|
? row.source_name || "本地文件"
|
||||||
}}</el-tag>
|
: sourceName(row.source_id)
|
||||||
<span class="ml-1 text-[#606266]">{{
|
}}</span>
|
||||||
row.source_type === "local"
|
|
||||||
? row.source_name || "本地文件"
|
|
||||||
: sourceName(row.source_id)
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="row.source_path" class="mt-0.5 truncate text-xs text-[#909399]">
|
|
||||||
{{ row.source_path }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div
|
||||||
<template #target="{ row }">
|
v-if="row.source_path"
|
||||||
<span class="text-[#606266]" :title="targetFullText(row as TransferTaskItem)">{{
|
class="mt-0.5 truncate text-xs text-(--el-text-color-secondary)"
|
||||||
targetShortText(row as TransferTaskItem)
|
>
|
||||||
}}</span>
|
{{ row.source_path }}
|
||||||
</template>
|
</div>
|
||||||
<template #status="{ row }">
|
</div>
|
||||||
<el-tag :type="statusTypeMap[row.status as TransferStatus] || 'info'">{{
|
</template>
|
||||||
statusText[row.status as TransferStatus] || row.status
|
<template #target="{ row }">
|
||||||
}}</el-tag>
|
<span
|
||||||
</template>
|
class="text-(--el-text-color-regular)"
|
||||||
<template #progress="{ row }">
|
:title="targetFullText(row as TransferTaskItem)"
|
||||||
<div class="flex flex-col gap-0.5">
|
>{{ targetShortText(row as TransferTaskItem) }}</span
|
||||||
<el-progress
|
>
|
||||||
:percentage="row.progress || 0"
|
</template>
|
||||||
:status="row.status === 'failed' ? 'exception' : undefined"
|
<template #status="{ row }">
|
||||||
/>
|
<ElTag :type="statusTypeMap[row.status as TransferStatus] || 'info'">{{
|
||||||
|
statusText[row.status as TransferStatus] || row.status
|
||||||
|
}}</ElTag>
|
||||||
|
</template>
|
||||||
|
<template #progress="{ row }">
|
||||||
|
<div class="flex flex-col gap-0.5">
|
||||||
|
<ElProgress
|
||||||
|
:percentage="row.progress || 0"
|
||||||
|
:status="row.status === 'failed' ? 'exception' : undefined"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="row.status === 'running' || row.speed > 0"
|
||||||
|
class="flex items-center gap-1.5 text-xs"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="shrink-0 rounded-[3px] bg-(--el-color-success) px-1.25 py-0.5 text-[11px] leading-none text-white"
|
||||||
|
>{{ formatSpeed(row.speed) }}</span
|
||||||
|
>
|
||||||
|
<span class="whitespace-nowrap text-(--el-text-color-secondary)"
|
||||||
|
>{{ formatSize(row.transferred_size) }} / {{ formatSize(row.total_size) }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #info="{ row }">
|
||||||
|
<div v-if="row.error_msg" class="flex min-w-0 items-center gap-1.5">
|
||||||
|
<ElPopover placement="top-start" width="360" trigger="hover">
|
||||||
<div
|
<div
|
||||||
v-if="row.status === 'running' || row.speed > 0"
|
class="max-h-75 overflow-auto break-all whitespace-pre-line text-(--el-color-danger) leading-[1.6]"
|
||||||
class="flex items-center gap-1.5 text-xs"
|
|
||||||
>
|
>
|
||||||
<span
|
{{ row.error_msg }}
|
||||||
class="shrink-0 rounded-[3px] bg-(--el-color-success) px-1.25 py-0.5 text-[11px] leading-none text-white"
|
|
||||||
>{{ formatSpeed(row.speed) }}</span
|
|
||||||
>
|
|
||||||
<span class="whitespace-nowrap text-(--el-text-color-secondary)"
|
|
||||||
>{{ formatSize(row.transferred_size) }} / {{ formatSize(row.total_size) }}</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<template #reference>
|
||||||
</template>
|
<span
|
||||||
<template #info="{ row }">
|
class="inline-block max-w-full truncate align-bottom text-(--el-color-danger)"
|
||||||
<div v-if="row.error_msg" class="flex min-w-0 items-center gap-1.5">
|
>{{ row.error_msg }}</span
|
||||||
<el-popover placement="top-start" width="360" trigger="hover">
|
|
||||||
<div
|
|
||||||
class="max-h-75 overflow-auto break-all whitespace-pre-line text-[#800000] leading-[1.6]"
|
|
||||||
>
|
>
|
||||||
{{ row.error_msg }}
|
</template>
|
||||||
</div>
|
</ElPopover>
|
||||||
<template #reference>
|
<ElButton
|
||||||
<span class="inline-block max-w-full truncate align-bottom text-[#800000]">{{
|
v-if="row.status === 'failed'"
|
||||||
row.error_msg
|
|
||||||
}}</span>
|
|
||||||
</template>
|
|
||||||
</el-popover>
|
|
||||||
<el-button
|
|
||||||
v-if="row.status === 'failed'"
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
link
|
|
||||||
@click="copyMessage(row.error_msg)"
|
|
||||||
>复制</el-button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<span v-else class="ml-1 text-[#606266]">{{
|
|
||||||
stepSummaryText(row as TransferTaskItem)
|
|
||||||
}}</span>
|
|
||||||
</template>
|
|
||||||
<template #actions="{ row }">
|
|
||||||
<el-button size="small" link type="primary" @click="openDetail(row as TransferTaskItem)"
|
|
||||||
>详情</el-button
|
|
||||||
>
|
|
||||||
<el-button
|
|
||||||
v-if="row.status === 'pending' || row.status === 'running'"
|
|
||||||
size="small"
|
size="small"
|
||||||
|
type="primary"
|
||||||
link
|
link
|
||||||
type="warning"
|
@click="copyMessage(row.error_msg)"
|
||||||
@click="cancelTask(row as TransferTaskItem)"
|
>复制</ElButton
|
||||||
>
|
>
|
||||||
取消
|
</div>
|
||||||
</el-button>
|
<span v-else class="ml-1 text-(--el-text-color-regular)">{{
|
||||||
<el-button size="small" link type="danger" @click="removeTask(row as TransferTaskItem)"
|
stepSummaryText(row as TransferTaskItem)
|
||||||
>删除</el-button
|
}}</span>
|
||||||
>
|
</template>
|
||||||
</template>
|
<template #actions="{ row }">
|
||||||
</FaTable>
|
<span v-html="formatTransferOperationCell(row as TransferTaskItem)" />
|
||||||
</div>
|
</template>
|
||||||
|
</FaTable>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
|
|
||||||
<!-- 新建传输任务 -->
|
<!-- 新建传输任务 -->
|
||||||
@@ -174,7 +166,7 @@
|
|||||||
>
|
>
|
||||||
<!-- 传输流程:具名插槽接管(需 @change 触发流程填充) -->
|
<!-- 传输流程:具名插槽接管(需 @change 触发流程填充) -->
|
||||||
<template #flow_id>
|
<template #flow_id>
|
||||||
<el-select
|
<ElSelect
|
||||||
v-model="selectedFlowId"
|
v-model="selectedFlowId"
|
||||||
placeholder="从已配置流程自动填充(可选,仍可手动调整)"
|
placeholder="从已配置流程自动填充(可选,仍可手动调整)"
|
||||||
clearable
|
clearable
|
||||||
@@ -182,18 +174,18 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@change="applyFlow"
|
@change="applyFlow"
|
||||||
>
|
>
|
||||||
<el-option
|
<ElOption
|
||||||
v-for="f in enabledFlows"
|
v-for="f in enabledFlows"
|
||||||
:key="f.id"
|
:key="f.id"
|
||||||
:value="f.id!"
|
:value="f.id!"
|
||||||
:label="flowOptionLabel(f)"
|
:label="flowOptionLabel(f)"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</ElSelect>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 本地文件:componentMap 无 upload 类型,具名插槽接管 -->
|
<!-- 本地文件:componentMap 无 upload 类型,具名插槽接管 -->
|
||||||
<template #local_file>
|
<template #local_file>
|
||||||
<el-upload
|
<ElUpload
|
||||||
drag
|
drag
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
:limit="1"
|
:limit="1"
|
||||||
@@ -203,35 +195,35 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<div class="el-upload__text">将文件拖到此处,或 <em>点击选择文件</em></div>
|
<div class="el-upload__text">将文件拖到此处,或 <em>点击选择文件</em></div>
|
||||||
</el-upload>
|
</ElUpload>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 目标列表:动态增删行,具名插槽接管 -->
|
<!-- 目标列表:动态增删行,具名插槽接管 -->
|
||||||
<template #targets>
|
<template #targets>
|
||||||
<div style="width: 100%">
|
<div style="width: 100%">
|
||||||
<div v-for="(t, idx) in form.targets" :key="idx" class="mb-2 flex items-center gap-2">
|
<div v-for="(t, idx) in form.targets" :key="idx" class="mb-2 flex items-center gap-2">
|
||||||
<el-tag
|
<ElTag
|
||||||
v-if="form.task_type === 'chain'"
|
v-if="form.task_type === 'chain'"
|
||||||
size="small"
|
size="small"
|
||||||
:type="idx === 0 ? 'primary' : 'warning'"
|
:type="idx === 0 ? 'primary' : 'warning'"
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
>
|
>
|
||||||
#{{ idx + 1 }}
|
#{{ idx + 1 }}
|
||||||
</el-tag>
|
</ElTag>
|
||||||
<el-select
|
<ElSelect
|
||||||
v-model="t.target_id"
|
v-model="t.target_id"
|
||||||
placeholder="目标存储源"
|
placeholder="目标存储源"
|
||||||
filterable
|
filterable
|
||||||
style="width: 250px"
|
style="width: 250px"
|
||||||
>
|
>
|
||||||
<el-option
|
<ElOption
|
||||||
v-for="s in enabledSources"
|
v-for="s in enabledSources"
|
||||||
:key="s.id"
|
:key="s.id"
|
||||||
:label="`${s.name}(${s.protocol}${s.host ? ' · ' + s.host : ''})`"
|
:label="`${s.name}(${s.protocol}${s.host ? ' · ' + s.host : ''})`"
|
||||||
:value="s.id!"
|
:value="s.id!"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</ElSelect>
|
||||||
<el-input
|
<ElInput
|
||||||
v-model="t.target_path"
|
v-model="t.target_path"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
selectedFlowId && !t.target_path
|
selectedFlowId && !t.target_path
|
||||||
@@ -240,15 +232,13 @@
|
|||||||
"
|
"
|
||||||
style="flex: 1"
|
style="flex: 1"
|
||||||
/>
|
/>
|
||||||
<el-button link type="danger" @click="removeTarget(idx)">删除</el-button>
|
<ElButton link type="danger" @click="removeTarget(idx)">删除</ElButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<el-button type="primary" plain size="small" @click="addTarget"
|
<ElButton type="primary" plain size="small" @click="addTarget">+ 添加目标</ElButton>
|
||||||
>+ 添加目标</el-button
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
v-if="form.task_type === 'chain' && form.targets.length > 1"
|
v-if="form.task_type === 'chain' && form.targets.length > 1"
|
||||||
class="text-[#909399]"
|
class="text-(--el-text-color-secondary)"
|
||||||
>
|
>
|
||||||
将按顺序执行:{{ flowPreview }}
|
将按顺序执行:{{ flowPreview }}
|
||||||
</span>
|
</span>
|
||||||
@@ -277,12 +267,12 @@
|
|||||||
max-height="70vh"
|
max-height="70vh"
|
||||||
>
|
>
|
||||||
<template #name="{ row }">
|
<template #name="{ row }">
|
||||||
<el-tooltip :content="String(row?.name ?? '')" placement="top" :show-after="100">
|
<ElTooltip :content="String(row?.name ?? '')" placement="top" :show-after="100">
|
||||||
<div class="max-w-45 truncate">{{ row?.name }}</div>
|
<div class="max-w-45 truncate">{{ row?.name }}</div>
|
||||||
</el-tooltip>
|
</ElTooltip>
|
||||||
</template>
|
</template>
|
||||||
<template #source="{ row }">
|
<template #source="{ row }">
|
||||||
<el-tooltip
|
<ElTooltip
|
||||||
:content="row ? sourceShortText(row as unknown as TransferTaskItem) : ''"
|
:content="row ? sourceShortText(row as unknown as TransferTaskItem) : ''"
|
||||||
placement="top"
|
placement="top"
|
||||||
:show-after="100"
|
:show-after="100"
|
||||||
@@ -290,7 +280,7 @@
|
|||||||
<div class="max-w-55 truncate">
|
<div class="max-w-55 truncate">
|
||||||
{{ row ? sourceShortText(row as unknown as TransferTaskItem) : "" }}
|
{{ row ? sourceShortText(row as unknown as TransferTaskItem) : "" }}
|
||||||
</div>
|
</div>
|
||||||
</el-tooltip>
|
</ElTooltip>
|
||||||
</template>
|
</template>
|
||||||
<template #total_size="{ row }">{{ formatSize(row?.total_size as number) }}</template>
|
<template #total_size="{ row }">{{ formatSize(row?.total_size as number) }}</template>
|
||||||
<template #speed="{ row }">{{ formatSpeed(row?.speed as number) }}</template>
|
<template #speed="{ row }">{{ formatSpeed(row?.speed as number) }}</template>
|
||||||
@@ -314,12 +304,10 @@
|
|||||||
class="rounded-md border border-(--el-border-color-lighter) bg-(--el-fill-color-lighter) px-3 py-2.5"
|
class="rounded-md border border-(--el-border-color-lighter) bg-(--el-fill-color-lighter) px-3 py-2.5"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<el-tag :type="idx === 0 ? 'primary' : 'warning'" size="small"
|
<ElTag :type="idx === 0 ? 'primary' : 'warning'" size="small">#{{ idx + 1 }}</ElTag>
|
||||||
>#{{ idx + 1 }}</el-tag
|
<ElTag :type="statusTypeMap[step.status] || 'info'" size="small">{{
|
||||||
>
|
|
||||||
<el-tag :type="statusTypeMap[step.status] || 'info'" size="small">{{
|
|
||||||
statusText[step.status] || step.status
|
statusText[step.status] || step.status
|
||||||
}}</el-tag>
|
}}</ElTag>
|
||||||
<span class="ml-auto text-xs text-(--el-text-color-secondary)"
|
<span class="ml-auto text-xs text-(--el-text-color-secondary)"
|
||||||
>{{ step.progress }}% · {{ formatSpeed(step.speed) }}</span
|
>{{ step.progress }}% · {{ formatSpeed(step.speed) }}</span
|
||||||
>
|
>
|
||||||
@@ -353,7 +341,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-else description="暂无步骤" :image-size="60" />
|
<ElEmpty v-else description="暂无步骤" :image-size="60" />
|
||||||
</template>
|
</template>
|
||||||
</FaDialog>
|
</FaDialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -370,6 +358,7 @@ import TransferAPI, {
|
|||||||
} from "@/api/module_storage/transfer";
|
} from "@/api/module_storage/transfer";
|
||||||
import NodeAPI, { type SourceTable } from "@/api/module_storage/node";
|
import NodeAPI, { type SourceTable } from "@/api/module_storage/node";
|
||||||
import FlowAPI, { type FlowTable } from "@/api/module_storage/workflow";
|
import FlowAPI, { type FlowTable } from "@/api/module_storage/workflow";
|
||||||
|
import { renderTableOperationCell, type TableOperationAction } from "@utils";
|
||||||
import FaDescriptions, {
|
import FaDescriptions, {
|
||||||
type DescriptionsItem,
|
type DescriptionsItem,
|
||||||
} from "@/components/display/fa-descriptions/index.vue";
|
} from "@/components/display/fa-descriptions/index.vue";
|
||||||
@@ -486,7 +475,13 @@ const columnChecks = ref<ColumnOption[]>([
|
|||||||
{ prop: "status", label: "状态", width: 92, useSlot: true },
|
{ prop: "status", label: "状态", width: 92, useSlot: true },
|
||||||
{ prop: "progress", label: "进度", minWidth: 240, useSlot: true },
|
{ prop: "progress", label: "进度", minWidth: 240, useSlot: true },
|
||||||
{ prop: "info", label: "信息", minWidth: 180, useSlot: true },
|
{ prop: "info", label: "信息", minWidth: 180, useSlot: true },
|
||||||
{ prop: "actions", label: "操作", width: 150, fixed: "right", useSlot: true },
|
{
|
||||||
|
prop: "actions",
|
||||||
|
label: "操作",
|
||||||
|
width: 150,
|
||||||
|
fixed: "right",
|
||||||
|
formatter: formatTransferOperationCell,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
/** 渲染列:仅保留列设置中可见的列 */
|
/** 渲染列:仅保留列设置中可见的列 */
|
||||||
const columns = computed(() => columnChecks.value.filter((c) => c.visible !== false));
|
const columns = computed(() => columnChecks.value.filter((c) => c.visible !== false));
|
||||||
@@ -811,10 +806,46 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── 取消 / 删除 ───────────────────────────────────────────────────────
|
// ── 取消 / 删除 ───────────────────────────────────────────────────────
|
||||||
|
/** 操作列(与 demo 案例统一:renderTableOperationCell + 权限码) */
|
||||||
|
function buildTransferRowActions(row: TransferTaskItem): TableOperationAction[] {
|
||||||
|
const all: TableOperationAction[] = [
|
||||||
|
{
|
||||||
|
key: "detail",
|
||||||
|
label: "详情",
|
||||||
|
artType: "view",
|
||||||
|
perm: "module_task:storage:transfer:query",
|
||||||
|
run: () => void openDetail(row),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (row.status === "pending" || row.status === "running") {
|
||||||
|
all.push({
|
||||||
|
key: "cancel",
|
||||||
|
label: "取消",
|
||||||
|
artType: "more",
|
||||||
|
icon: "ri:stop-circle-line",
|
||||||
|
iconColor: "var(--el-color-warning)",
|
||||||
|
perm: "module_task:storage:transfer:update",
|
||||||
|
run: () => void cancelTask(row),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
all.push({
|
||||||
|
key: "delete",
|
||||||
|
label: "删除",
|
||||||
|
artType: "delete",
|
||||||
|
perm: "module_task:storage:transfer:delete",
|
||||||
|
run: () => void removeTask(row),
|
||||||
|
});
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
function formatTransferOperationCell(row: TransferTaskItem) {
|
||||||
|
return renderTableOperationCell(buildTransferRowActions(row), {
|
||||||
|
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1",
|
||||||
|
});
|
||||||
|
}
|
||||||
async function cancelTask(row: TransferTaskItem) {
|
async function cancelTask(row: TransferTaskItem) {
|
||||||
if (!row.id) return;
|
if (!row.id) return;
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(`确认取消任务「${row.name}」?`, "提示", { type: "warning" });
|
await confirmDelete(`确认取消任务「${row.name}」?取消后任务将停止传输。`);
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -824,7 +855,7 @@ async function cancelTask(row: TransferTaskItem) {
|
|||||||
async function removeTask(row: TransferTaskItem) {
|
async function removeTask(row: TransferTaskItem) {
|
||||||
if (!row.id) return;
|
if (!row.id) return;
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(`确认删除任务「${row.name}」?`, "提示", { type: "warning" });
|
await confirmDelete(`确认删除任务「${row.name}」吗?此操作不可恢复!`);
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
+6
-6
@@ -27,8 +27,8 @@
|
|||||||
<template #left>
|
<template #left>
|
||||||
<FaTableHeaderLeft
|
<FaTableHeaderLeft
|
||||||
:remove-ids="selectedIds"
|
:remove-ids="selectedIds"
|
||||||
:perm-create="['module_storage:workflow:flow:create']"
|
:perm-create="['module_task:storage:flow:create']"
|
||||||
:perm-delete="['module_storage:workflow:flow:delete']"
|
:perm-delete="['module_task:storage:flow:delete']"
|
||||||
:delete-loading="batchDeleting"
|
:delete-loading="batchDeleting"
|
||||||
:create-loading="createLoading"
|
:create-loading="createLoading"
|
||||||
@add="handleAdd"
|
@add="handleAdd"
|
||||||
@@ -358,7 +358,7 @@ function buildFlowRowActions(
|
|||||||
key: "detail",
|
key: "detail",
|
||||||
label: "查看",
|
label: "查看",
|
||||||
artType: "view",
|
artType: "view",
|
||||||
perm: "module_storage:workflow:flow:query",
|
perm: "module_task:storage: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_storage:workflow:flow:update",
|
perm: "module_task:storage: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_storage:workflow:transfer:create",
|
perm: "module_task:storage: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_storage:workflow:flow:delete",
|
perm: "module_task:storage:flow:delete",
|
||||||
run: () => ctx.onDelete(row.id!, row.name ?? ""),
|
run: () => ctx.onDelete(row.id!, row.name ?? ""),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
Reference in New Issue
Block a user