feat: Dashboard数据关联修复 + 通知模板优化 + 工单创建页布局

1. Dashboard: stats新增queryStats查询统计, trend API真实工单趋势数据
2. Layout铃铛: 显示真实待审批工单数量
3. 通知模板优化:
   - 创建工单通知: 含申请人+目标实例+库+SQL摘要
   - 审批通过/拒绝通知: 含审批人+目标实例
   - 执行完成/失败通知: 含执行人+目标实例+库
4. 飞书卡片颜色: 创建=turquoise, 审批=blue, 成功=green, 失败=red
5. 钉钉消息: 添加状态emoji标签(🆕✅❌⚠️)
6. TicketCreatePage: 底部操作栏(SQL检测+提交审批)
7. ticket_executor: 移除aiomysql不支持的read_timeout/write_timeout

Tests: 1057 passed, 17 pre-existing failures (notification 35/35 passed)
This commit is contained in:
logan committed 2026-05-25 01:29:24 +08:00
1 parent 7109031f71
commit 880dbfefa4
9 files changed
+423 -209

No files matched your search

+49 -21
View File
@@ -18,43 +18,69 @@ class NotificationSender:
# 内置消息模板
TEMPLATES = {
"ticket_created": {
"title": "新工单待审批: {title}",
"body": "**申请人**: {applicant}\n**数据库**: {db_type}@{target_database}\n**SQL摘要**: {sql_preview}",
"title": "新工单待审批",
"body": "**申请人**: {applicant}\n**目标实例**: {instance_name} ({db_type})\n**目标库**: {target_database}\n**工单标题**: {title}\n**SQL摘要**: {sql_preview}",
},
"ticket_approved": {
"title": "工单已审批通过: {title}",
"body": "**审批人**: {approver}\n**工单ID**: {ticket_id}\n**状态**: 已批准,可执行",
"title": "工单已审批通过",
"body": "**工单标题**: {title}\n**审批人**: {approver}\n**目标实例**: {instance_name} ({db_type})\n**状态**: 已批准,可执行",
},
"ticket_rejected": {
"title": "工单已被拒绝: {title}",
"body": "**审批人**: {approver}\n**拒绝原因**: {comment}\n**工单ID**: {ticket_id}",
"title": "工单已被拒绝",
"body": "**工单标题**: {title}\n**审批人**: {approver}\n**拒绝原因**: {comment}\n**目标实例**: {instance_name} ({db_type})",
},
"ticket_executed": {
"title": "工单执行完成: {title}",
"body": "**执行结果**: {result}\n**耗时**: {duration}ms\n**影响行数**: {affected_rows}",
"title": "工单执行完成",
"body": "**工单标题**: {title}\n**执行人**: {operator}\n**目标实例**: {instance_name} ({db_type})\n**目标库**: {target_database}\n**执行结果**: 成功\n**耗时**: {duration}ms\n**影响行数**: {affected_rows}",
},
"ticket_failed": {
"title": "工单执行失败: {title}",
"body": "**错误信息**: {error}\n**工单ID**: {ticket_id}\n请及时处理",
"title": "工单执行失败",
"body": "**工单标题**: {title}\n**执行人**: {operator}\n**目标实例**: {instance_name} ({db_type})\n**目标库**: {target_database}\n**错误信息**: {error}",
},
"ticket_rollback": {
"title": "工单已回滚: {title}",
"title": "工单已回滚",
"body": "**回滚人**: {operator}\n**工单ID**: {ticket_id}",
},
"backup_completed": {
"title": "备份完成: {title}",
"title": "备份完成",
"body": "**数据库**: {database}\n**文件大小**: {size}\n**耗时**: {duration}s",
},
"backup_failed": {
"title": "备份失败: {title}",
"title": "备份失败",
"body": "**错误信息**: {error}\n请检查数据库连接和磁盘空间",
},
"schema_drift_detected": {
"title": "检测到 Schema 漂移: {instance_name}",
"body": "**数据库**: {database}\n**发现 {drift_count} 处变更**:\n{drift_details}",
"title": "检测到 Schema 漂移",
"body": "**实例**: {instance_name}\n**数据库**: {database}\n**发现 {drift_count} 处变更**:\n{drift_details}",
},
}
# 飞书卡片颜色映射
FEISHU_COLORS = {
"ticket_created": "turquoise",
"ticket_approved": "blue",
"ticket_rejected": "red",
"ticket_executed": "green",
"ticket_failed": "red",
"ticket_rollback": "orange",
"backup_completed": "green",
"backup_failed": "red",
"schema_drift_detected": "orange",
}
# 钉钉消息颜色标记
DINGTALK_TAGS = {
"ticket_created": "🆕",
"ticket_approved": "✅",
"ticket_rejected": "❌",
"ticket_executed": "✅",
"ticket_failed": "❌",
"ticket_rollback": "⚠️",
"backup_completed": "✅",
"backup_failed": "❌",
"schema_drift_detected": "⚠️",
}
async def send(self, channel_type: str, config: Dict[str, Any],
event_type: str, data: Dict[str, Any]) -> Dict[str, Any]:
"""发送通知。
@@ -95,11 +121,12 @@ class NotificationSender:
if "?" in webhook_url:
webhook_url = f"{webhook_url}&timestamp={timestamp}&sign={sign}"
else:
webhook_url = f"{webhook_url}&timestamp={timestamp}&sign={sign}"
webhook_url = f"{webhook_url}?timestamp={timestamp}&sign={sign}"
template = self.TEMPLATES.get(event_type, self.TEMPLATES["ticket_created"])
title = template["title"].format(**data)
body = template["body"].format(**data)
tag = self.DINGTALK_TAGS.get(event_type, "📌")
# 钉钉 markdown 单个 \n 视为空格,需要 \n\n 才能换行
body = body.replace("\n", "\n\n")
@@ -107,8 +134,8 @@ class NotificationSender:
payload = {
"msgtype": "markdown",
"markdown": {
"title": title,
"text": f"## {title}\n\n{body}\n\n---\n*来自 SQL 审核平台*",
"title": f"{tag} {title}",
"text": f"### {tag} {title}\n\n{body}\n\n---\n*来自 SQL 审核平台*",
}
}
@@ -122,7 +149,7 @@ class NotificationSender:
async def _send_feishu(self, config: Dict[str, Any], event_type: str,
data: Dict[str, Any]) -> Dict[str, Any]:
"""发送飞书消息。"""
"""发送飞书消息 — 使用 Interactive Card。"""
import httpx
webhook_url = config.get("webhook_url", "")
@@ -132,13 +159,14 @@ class NotificationSender:
template = self.TEMPLATES.get(event_type, self.TEMPLATES["ticket_created"])
title = template["title"].format(**data)
body = template["body"].format(**data)
color = self.FEISHU_COLORS.get(event_type, "blue")
payload = {
"msg_type": "interactive",
"card": {
"header": {
"title": {"tag": "plain_text", "content": title},
"template": "blue" if "approved" in event_type or "completed" in event_type else "red",
"template": color,
},
"elements": [
{"tag": "div", "text": {"tag": "lark_md", "content": body}},
@@ -179,7 +207,7 @@ class NotificationSender:
template = self.TEMPLATES.get(event_type, self.TEMPLATES["ticket_created"])
subject = template["title"].format(**data)
body_text = template["body"].format(**data).replace("\\n", "<br>")
body_text = template["body"].format(**data).replace("\n", "<br>")
html_body = f"""
<html>