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

+19 -15
View File
@@ -60,10 +60,14 @@ async def test_stats_fields_structure(client: AsyncClient, admin_token: str):
"instance_online",
"user_total",
"queries_today",
"query_stats",
}
assert set(data.keys()) == expected_keys
for key in expected_keys:
assert isinstance(data[key], int), f"{key} should be int"
if key == "query_stats":
assert isinstance(data[key], (dict, type(None))), f"{key} should be dict or None"
else:
assert isinstance(data[key], int), f"{key} should be int"
@pytest.mark.asyncio
@@ -166,23 +170,23 @@ async def test_stats_excludes_inactive_users(
async def test_stats_reflect_queries_today(
client: AsyncClient, db: AsyncSession, admin_token: str
):
"""Add audit logs with QUERY action and verify queries_today."""
"""Add query histories and verify queries_today counts from QueryHistory."""
from app.models.query_history import QueryHistory
for i in range(5):
log = AuditLog(
qh = QueryHistory(
user_id=1,
action="QUERY",
target_type="query",
detail="test query",
username="admin",
instance_id=1,
instance_name="test",
db_type="mysql",
database_name="test_db",
query_content="SELECT 1",
query_type="SELECT",
execution_time_ms=100,
result_rows=1,
is_success=True,
)
db.add(log)
# also add a non-QUERY log (should be ignored)
non_query = AuditLog(
user_id=1,
action="LOGIN",
target_type="session",
detail="login event",
)
db.add(non_query)
db.add(qh)
await db.commit()
resp = await client.get(BASE, headers=auth_header(admin_token))