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

+108 -34
View File
@@ -447,6 +447,12 @@ async def create_ticket(
await db.commit()
await db.refresh(ticket)
# 9. 发送创建通知
try:
await _send_ticket_notification(db, ticket, "ticket_created")
except Exception as notify_err:
logger.error("创建通知发送失败: %s", notify_err)
result = await _enrich_ticket(ticket, db)
if v2_report:
result["audit_report"] = v2_report.to_dict()
@@ -663,6 +669,17 @@ async def review_ticket(
await db.commit()
await db.refresh(ticket)
# 发送审批通知
try:
event_type = "ticket_approved" if data.action == "approve" else "ticket_rejected"
await _send_ticket_notification(db, ticket, event_type, {
"approver": current_user.display_name,
"comment": data.comment or ("审批通过" if data.action == "approve" else "审批拒绝"),
})
except Exception as notify_err:
logger.error("审批通知发送失败: %s", notify_err)
return await _enrich_ticket(ticket, db)
@@ -1027,65 +1044,122 @@ async def execute_ticket(
return ticket_data
async def _send_execution_notification(
db: AsyncSession, ticket: Ticket, report: "ExecutionReport"
) -> None:
"""发送工单执行完成通知。
async def _get_user_name(db: AsyncSession, user_id: Optional[int]) -> str:
"""获取用户显示名称。"""
if not user_id:
return "系统"
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
return user.display_name if user else "未知"
查询已启用的通知渠道并发送执行结果通知。
async def _get_instance_name(db: AsyncSession, instance_id: Optional[int]) -> str:
"""获取实例名称。"""
if not instance_id:
return "未知实例"
result = await db.execute(select(Instance).where(Instance.id == instance_id))
inst = result.scalar_one_or_none()
return inst.name if inst else "未知实例"
async def _get_enabled_channels(db: AsyncSession) -> list:
"""获取已启用的通知渠道。"""
from app.models.notification import NotificationChannel
result = await db.execute(
select(NotificationChannel).where(NotificationChannel.is_enabled == True)
)
return result.scalars().all()
async def _send_ticket_notification(
db: AsyncSession, ticket: Ticket, event_type: str,
extra_data: Optional[Dict[str, Any]] = None,
) -> None:
"""通用工单通知发送。
Args:
db: 数据库会话
ticket: 工单对象
report: 执行报告
event_type: 事件类型 (ticket_created / ticket_approved / ticket_rejected / ticket_executed / ticket_failed)
extra_data: 额外模板数据 (如 approver, operator, error 等)
"""
from app.models.notification import NotificationChannel
from app.services.notification_sender import NotificationSender
result = await db.execute(
select(NotificationChannel).where(NotificationChannel.is_enabled == True)
)
channels = result.scalars().all()
channels = await _get_enabled_channels(db)
if not channels:
return
sender = NotificationSender()
# 确定事件类型
if report.status == "success":
event_type = "ticket_executed"
else:
event_type = "ticket_failed"
# 构建 SQL 预览(前 100 字符)
# 获取关联信息
applicant_name = await _get_user_name(db, ticket.applicant_id)
instance_name = await _get_instance_name(db, ticket.instance_id)
sql_preview = ticket.sql_content[:100] + "..." if len(ticket.sql_content) > 100 else ticket.sql_content
# 获取申请人信息
applicant_name = "未知"
if ticket.applicant_id:
result = await db.execute(
select(User).where(User.id == ticket.applicant_id)
)
applicant = result.scalar_one_or_none()
if applicant:
applicant_name = applicant.display_name
notification_data = {
"title": ticket.title,
"ticket_id": ticket.id,
"ticket_no": ticket.ticket_no or str(ticket.id),
"db_type": ticket.db_type,
"target_database": ticket.target_database,
"target_database": ticket.target_database or "",
"instance_name": instance_name,
"sql_preview": sql_preview,
"priority": ticket.priority,
"applicant": applicant_name,
"approver": "未知",
"operator": "系统",
"comment": "",
"result": "",
"duration": 0,
"affected_rows": 0,
"error": "",
}
# 合并额外数据
if extra_data:
notification_data.update(extra_data)
sender = NotificationSender()
await sender.notify_ticket_event(
ticket_id=ticket.id,
event_type=event_type,
channels=channels,
ticket_data=notification_data,
)
async def _send_execution_notification(
db: AsyncSession, ticket: Ticket, report: "ExecutionReport"
) -> None:
"""发送工单执行完成通知(兼容旧调用)。"""
from app.services.notification_sender import NotificationSender
channels = await _get_enabled_channels(db)
if not channels:
return
event_type = "ticket_executed" if report.status == "success" else "ticket_failed"
operator_name = await _get_user_name(db, ticket.approver_id)
instance_name = await _get_instance_name(db, ticket.instance_id)
applicant_name = await _get_user_name(db, ticket.applicant_id)
sql_preview = ticket.sql_content[:100] + "..." if len(ticket.sql_content) > 100 else ticket.sql_content
notification_data = {
"title": ticket.title,
"ticket_id": ticket.id,
"ticket_no": ticket.ticket_no or str(ticket.id),
"db_type": ticket.db_type,
"target_database": ticket.target_database or "",
"instance_name": instance_name,
"sql_preview": sql_preview,
"applicant": applicant_name,
"operator": operator_name,
"result": report.status,
"duration": report.duration_ms,
"affected_rows": report.affected_rows,
"error": "; ".join(report.errors[:3]) if report.errors else "",
"operator": "dba",
"approver": operator_name,
"comment": "",
}
sender = NotificationSender()
await sender.notify_ticket_event(
ticket_id=ticket.id,
event_type=event_type,