- 新增重试和失败告警功能(支持自动重试N次,失败后钉钉/企微通知) - 新增密钥管理(安全存储API Key等敏感信息) - 新增脚本模板库(预置常用脚本模板) - 新增脚本版本管理(自动保存历史版本,支持回滚) - 新增执行统计(成功率、平均耗时、7日趋势) - SDK 新增多租户遍历能力(get_tenants/get_tenant_config/get_all_tenant_configs) - SDK 新增密钥读取方法(get_secret)
This commit is contained in:
@@ -31,10 +31,81 @@ def get_db_session() -> Session:
|
||||
return SessionLocal()
|
||||
|
||||
|
||||
async def send_alert(webhook: str, task_name: str, error_message: str):
|
||||
"""发送失败告警通知"""
|
||||
try:
|
||||
# 自动判断钉钉或企微
|
||||
if "dingtalk" in webhook or "oapi.dingtalk.com" in webhook:
|
||||
data = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"title": "定时任务执行失败",
|
||||
"text": f"### ⚠️ 定时任务执行失败\n\n**任务名称**:{task_name}\n\n**错误信息**:{error_message[:500]}\n\n**时间**:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
}
|
||||
}
|
||||
else:
|
||||
# 企微格式
|
||||
data = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"content": f"### ⚠️ 定时任务执行失败\n\n**任务名称**:{task_name}\n\n**错误信息**:{error_message[:500]}\n\n**时间**:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
}
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
await client.post(webhook, json=data)
|
||||
logger.info(f"Alert sent for task {task_name}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to send alert: {e}")
|
||||
|
||||
|
||||
async def execute_task_with_retry(task_id: int, retry_count: int = 0, max_retries: int = 0, retry_interval: int = 60):
|
||||
"""带重试的任务执行"""
|
||||
success = await execute_task_once(task_id)
|
||||
|
||||
if not success and retry_count < max_retries:
|
||||
logger.info(f"Task {task_id} failed, scheduling retry {retry_count + 1}/{max_retries} in {retry_interval}s")
|
||||
await asyncio.sleep(retry_interval)
|
||||
await execute_task_with_retry(task_id, retry_count + 1, max_retries, retry_interval)
|
||||
elif not success:
|
||||
# 所有重试都失败,发送告警
|
||||
db = get_db_session()
|
||||
try:
|
||||
result = db.execute(
|
||||
text("SELECT task_name, alert_on_failure, alert_webhook, last_run_message FROM platform_scheduled_tasks WHERE id = :id"),
|
||||
{"id": task_id}
|
||||
)
|
||||
task = result.mappings().first()
|
||||
if task and task["alert_on_failure"] and task["alert_webhook"]:
|
||||
await send_alert(task["alert_webhook"], task["task_name"], task["last_run_message"] or "未知错误")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
async def execute_task(task_id: int):
|
||||
"""执行定时任务"""
|
||||
"""执行定时任务入口(处理重试配置)"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
result = db.execute(
|
||||
text("SELECT retry_count, retry_interval FROM platform_scheduled_tasks WHERE id = :id"),
|
||||
{"id": task_id}
|
||||
)
|
||||
task = result.mappings().first()
|
||||
if task:
|
||||
max_retries = task.get("retry_count", 0) or 0
|
||||
retry_interval = task.get("retry_interval", 60) or 60
|
||||
await execute_task_with_retry(task_id, 0, max_retries, retry_interval)
|
||||
else:
|
||||
await execute_task_once(task_id)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
async def execute_task_once(task_id: int) -> bool:
|
||||
"""执行一次定时任务,返回是否成功"""
|
||||
db = get_db_session()
|
||||
log_id = None
|
||||
success = False
|
||||
|
||||
try:
|
||||
# 1. 查询任务配置
|
||||
@@ -46,7 +117,7 @@ async def execute_task(task_id: int):
|
||||
|
||||
if not task:
|
||||
logger.warning(f"Task {task_id} not found or disabled")
|
||||
return
|
||||
return True # 不需要重试
|
||||
|
||||
# 2. 更新任务状态为运行中
|
||||
db.execute(
|
||||
@@ -161,9 +232,11 @@ async def execute_task(task_id: int):
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Task {task_id} executed with status: {status}")
|
||||
success = (status == "success")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Task {task_id} execution error: {str(e)}")
|
||||
success = False
|
||||
|
||||
# 更新失败状态
|
||||
try:
|
||||
@@ -190,6 +263,8 @@ async def execute_task(task_id: int):
|
||||
pass
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
return success
|
||||
|
||||
|
||||
def add_task_to_scheduler(task: Dict[str, Any]):
|
||||
|
||||
@@ -24,6 +24,8 @@ class ScriptSDK:
|
||||
- HTTP 请求
|
||||
- 变量存储(跨执行持久化)
|
||||
- 日志记录
|
||||
- 多租户遍历
|
||||
- 密钥管理
|
||||
"""
|
||||
|
||||
def __init__(self, tenant_id: str, task_id: int, trace_id: str = None):
|
||||
@@ -32,6 +34,7 @@ class ScriptSDK:
|
||||
self.trace_id = trace_id or f"script_{task_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||
self._logs: List[str] = []
|
||||
self._db: Optional[Session] = None
|
||||
self._tenants_cache: Optional[List[Dict]] = None # 租户列表缓存
|
||||
|
||||
def _get_db(self) -> Session:
|
||||
"""获取数据库会话"""
|
||||
@@ -393,6 +396,171 @@ class ScriptSDK:
|
||||
self.log(f"删除变量失败: {str(e)}", level="ERROR")
|
||||
return False
|
||||
|
||||
# ============ 多租户遍历 ============
|
||||
|
||||
def get_tenants(self, app_code: str = None) -> List[Dict]:
|
||||
"""
|
||||
获取租户列表(用于多租户任务遍历)
|
||||
|
||||
Args:
|
||||
app_code: 应用代码(可选),筛选订阅了该应用的租户
|
||||
|
||||
Returns:
|
||||
租户列表 [{"code": "xxx", "name": "租户名", "custom_configs": {...}}]
|
||||
"""
|
||||
db = self._get_db()
|
||||
try:
|
||||
if app_code:
|
||||
# 筛选订阅了指定应用的租户
|
||||
result = db.execute(
|
||||
text("""
|
||||
SELECT DISTINCT t.code, t.name, ta.custom_configs
|
||||
FROM platform_tenants t
|
||||
INNER JOIN platform_tenant_apps ta ON t.code = ta.tenant_id
|
||||
WHERE ta.app_code = :app_code AND t.status = 1
|
||||
"""),
|
||||
{"app_code": app_code}
|
||||
)
|
||||
else:
|
||||
# 获取所有启用的租户
|
||||
result = db.execute(
|
||||
text("SELECT code, name FROM platform_tenants WHERE status = 1")
|
||||
)
|
||||
|
||||
tenants = []
|
||||
for row in result.mappings().all():
|
||||
tenant = dict(row)
|
||||
# 解析 custom_configs
|
||||
if "custom_configs" in tenant and tenant["custom_configs"]:
|
||||
try:
|
||||
tenant["custom_configs"] = json.loads(tenant["custom_configs"])
|
||||
except:
|
||||
pass
|
||||
tenants.append(tenant)
|
||||
|
||||
self.log(f"获取租户列表成功,共 {len(tenants)} 个租户")
|
||||
return tenants
|
||||
except Exception as e:
|
||||
self.log(f"获取租户列表失败: {str(e)}", level="ERROR")
|
||||
return []
|
||||
|
||||
def get_tenant_config(self, tenant_id: str, app_code: str, key: str = None) -> Any:
|
||||
"""
|
||||
获取指定租户的应用配置
|
||||
|
||||
Args:
|
||||
tenant_id: 租户ID
|
||||
app_code: 应用代码
|
||||
key: 配置项键名(可选,不传返回全部配置)
|
||||
|
||||
Returns:
|
||||
配置值或配置字典
|
||||
"""
|
||||
db = self._get_db()
|
||||
try:
|
||||
result = db.execute(
|
||||
text("""
|
||||
SELECT custom_configs FROM platform_tenant_apps
|
||||
WHERE tenant_id = :tenant_id AND app_code = :app_code
|
||||
"""),
|
||||
{"tenant_id": tenant_id, "app_code": app_code}
|
||||
)
|
||||
row = result.first()
|
||||
|
||||
if not row or not row[0]:
|
||||
return None if key else {}
|
||||
|
||||
try:
|
||||
configs = json.loads(row[0])
|
||||
except:
|
||||
configs = {}
|
||||
|
||||
if key:
|
||||
return configs.get(key)
|
||||
return configs
|
||||
except Exception as e:
|
||||
self.log(f"获取租户配置失败: {str(e)}", level="ERROR")
|
||||
return None if key else {}
|
||||
|
||||
def get_all_tenant_configs(self, app_code: str) -> List[Dict]:
|
||||
"""
|
||||
获取所有租户的应用配置(便捷方法,用于批量操作)
|
||||
|
||||
Args:
|
||||
app_code: 应用代码
|
||||
|
||||
Returns:
|
||||
[{"tenant_id": "xxx", "tenant_name": "租户名", "configs": {...}}]
|
||||
"""
|
||||
db = self._get_db()
|
||||
try:
|
||||
result = db.execute(
|
||||
text("""
|
||||
SELECT t.code as tenant_id, t.name as tenant_name, ta.custom_configs
|
||||
FROM platform_tenants t
|
||||
INNER JOIN platform_tenant_apps ta ON t.code = ta.tenant_id
|
||||
WHERE ta.app_code = :app_code AND t.status = 1
|
||||
"""),
|
||||
{"app_code": app_code}
|
||||
)
|
||||
|
||||
tenants = []
|
||||
for row in result.mappings().all():
|
||||
configs = {}
|
||||
if row["custom_configs"]:
|
||||
try:
|
||||
configs = json.loads(row["custom_configs"])
|
||||
except:
|
||||
pass
|
||||
tenants.append({
|
||||
"tenant_id": row["tenant_id"],
|
||||
"tenant_name": row["tenant_name"],
|
||||
"configs": configs
|
||||
})
|
||||
|
||||
self.log(f"获取 {app_code} 应用的租户配置,共 {len(tenants)} 个")
|
||||
return tenants
|
||||
except Exception as e:
|
||||
self.log(f"获取租户配置失败: {str(e)}", level="ERROR")
|
||||
return []
|
||||
|
||||
# ============ 密钥管理 ============
|
||||
|
||||
def get_secret(self, key: str) -> Optional[str]:
|
||||
"""
|
||||
获取密钥(优先读取租户级密钥,其次读取全局密钥)
|
||||
|
||||
Args:
|
||||
key: 密钥名称
|
||||
|
||||
Returns:
|
||||
密钥值(如不存在返回 None)
|
||||
"""
|
||||
db = self._get_db()
|
||||
try:
|
||||
# 优先查询租户级密钥
|
||||
result = db.execute(
|
||||
text("""
|
||||
SELECT secret_value FROM platform_secrets
|
||||
WHERE (tenant_id = :tenant_id OR tenant_id IS NULL)
|
||||
AND secret_key = :key
|
||||
ORDER BY tenant_id DESC
|
||||
LIMIT 1
|
||||
"""),
|
||||
{"tenant_id": self.tenant_id, "key": key}
|
||||
)
|
||||
row = result.first()
|
||||
|
||||
if row:
|
||||
self.log(f"获取密钥成功: {key}")
|
||||
return row[0]
|
||||
|
||||
self.log(f"密钥不存在: {key}", level="WARN")
|
||||
return None
|
||||
except Exception as e:
|
||||
self.log(f"获取密钥失败: {str(e)}", level="ERROR")
|
||||
return None
|
||||
|
||||
# ============ 日志 ============
|
||||
|
||||
def log(self, message: str, level: str = "INFO"):
|
||||
|
||||
Reference in New Issue
Block a user