mdbq 4.0.93__tar.gz → 4.0.94__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mdbq might be problematic. Click here for more details.

Files changed (44) hide show
  1. {mdbq-4.0.93 → mdbq-4.0.94}/PKG-INFO +1 -1
  2. mdbq-4.0.94/mdbq/__version__.py +1 -0
  3. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/auth/auth_backend.py +229 -2
  4. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/route/analytics.py +4 -3
  5. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/route/monitor.py +1 -2
  6. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/route/routes.py +211 -2
  7. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq.egg-info/PKG-INFO +1 -1
  8. mdbq-4.0.93/mdbq/__version__.py +0 -1
  9. {mdbq-4.0.93 → mdbq-4.0.94}/README.txt +0 -0
  10. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/__init__.py +0 -0
  11. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/auth/__init__.py +0 -0
  12. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/auth/rate_limiter.py +0 -0
  13. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/js/__init__.py +0 -0
  14. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/js/jc.py +0 -0
  15. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/log/__init__.py +0 -0
  16. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/log/mylogger.py +0 -0
  17. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/myconf/__init__.py +0 -0
  18. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/myconf/myconf.py +0 -0
  19. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/mysql/__init__.py +0 -0
  20. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/mysql/deduplicator.py +0 -0
  21. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/mysql/mysql.py +0 -0
  22. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/mysql/s_query.py +0 -0
  23. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/mysql/unique_.py +0 -0
  24. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/mysql/uploader.py +0 -0
  25. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/other/__init__.py +0 -0
  26. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/other/download_sku_picture.py +0 -0
  27. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/other/error_handler.py +0 -0
  28. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/other/otk.py +0 -0
  29. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/other/pov_city.py +0 -0
  30. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/other/ua_sj.py +0 -0
  31. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/pbix/__init__.py +0 -0
  32. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/pbix/pbix_refresh.py +0 -0
  33. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/pbix/refresh_all.py +0 -0
  34. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/redis/__init__.py +0 -0
  35. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/redis/getredis.py +0 -0
  36. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/route/__init__.py +0 -0
  37. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/selenium/__init__.py +0 -0
  38. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/selenium/get_driver.py +0 -0
  39. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq/spider/__init__.py +0 -0
  40. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq.egg-info/SOURCES.txt +0 -0
  41. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq.egg-info/dependency_links.txt +0 -0
  42. {mdbq-4.0.93 → mdbq-4.0.94}/mdbq.egg-info/top_level.txt +0 -0
  43. {mdbq-4.0.93 → mdbq-4.0.94}/setup.cfg +0 -0
  44. {mdbq-4.0.93 → mdbq-4.0.94}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdbq
3
- Version: 4.0.93
3
+ Version: 4.0.94
4
4
  Home-page: https://pypi.org/project/mdbq
5
5
  Author: xigua,
6
6
  Author-email: 2587125111@qq.com
@@ -0,0 +1 @@
1
+ VERSION = '4.0.94'
@@ -116,7 +116,7 @@ class StandaloneAuthManager:
116
116
  'session_expires_hours': 24, # 会话过期时间
117
117
  'max_login_attempts': 5, # 最大登录尝试次数
118
118
  'lockout_duration': 15 * 60, # 锁定时长
119
- 'max_concurrent_devices': 10, # 最大并发设备数
119
+ 'max_concurrent_devices': 20, # 最大并发设备数
120
120
  'device_session_expires_days': 30, # 设备会话过期时间
121
121
  'ip_max_attempts': 10, # IP最大尝试次数
122
122
  'ip_window_minutes': 30, # IP限制时间窗口
@@ -824,6 +824,14 @@ class StandaloneAuthManager:
824
824
  try:
825
825
  current_time_utc = datetime.now(timezone.utc)
826
826
 
827
+ # 先查询要登出的设备数量
828
+ cursor.execute('''
829
+ SELECT COUNT(*) as device_count FROM device_sessions
830
+ WHERE user_id = %s AND is_active = 1
831
+ ''', (user_id,))
832
+
833
+ device_count = cursor.fetchone()['device_count']
834
+
827
835
  # 撤销用户的所有刷新令牌
828
836
  cursor.execute('''
829
837
  UPDATE refresh_tokens
@@ -838,7 +846,11 @@ class StandaloneAuthManager:
838
846
  WHERE user_id = %s AND is_active = 1
839
847
  ''', (user_id,))
840
848
 
841
- return {'success': True, 'message': '已成功登出所有设备'}
849
+ return {
850
+ 'success': True,
851
+ 'message': '已成功登出所有设备',
852
+ 'logged_out_devices': device_count
853
+ }
842
854
 
843
855
  except Exception as e:
844
856
  return {'success': False, 'message': f'登出失败: {str(e)}'}
@@ -1301,6 +1313,221 @@ class StandaloneAuthManager:
1301
1313
 
1302
1314
  return f"{masked_local}@{masked_domain}"
1303
1315
 
1316
+ def get_user_devices(self, user_id, current_device_fingerprint=None):
1317
+ """获取用户的所有设备"""
1318
+ conn = self.pool.connection()
1319
+ cursor = conn.cursor()
1320
+
1321
+ try:
1322
+ cursor.execute('''
1323
+ SELECT device_id, device_fingerprint, device_name, device_type, platform, browser,
1324
+ ip_address, last_activity, created_at, is_active
1325
+ FROM device_sessions
1326
+ WHERE user_id = %s AND is_active = 1
1327
+ ORDER BY last_activity DESC
1328
+ ''', (user_id,))
1329
+
1330
+ devices = cursor.fetchall()
1331
+ current_device_id = None
1332
+
1333
+ devices_list = []
1334
+ for device in devices:
1335
+ is_current = device['device_fingerprint'] == current_device_fingerprint
1336
+ if is_current:
1337
+ current_device_id = device['device_id']
1338
+
1339
+ devices_list.append({
1340
+ 'device_id': device['device_id'],
1341
+ 'device_name': device['device_name'],
1342
+ 'device_type': device['device_type'],
1343
+ 'platform': device['platform'],
1344
+ 'browser': device['browser'],
1345
+ 'ip_address': device['ip_address'],
1346
+ 'last_activity': device['last_activity'].isoformat() if device['last_activity'] else None,
1347
+ 'created_at': device['created_at'].isoformat() if device['created_at'] else None,
1348
+ 'is_current': is_current
1349
+ })
1350
+
1351
+ return {
1352
+ 'devices': devices_list,
1353
+ 'total_count': len(devices_list),
1354
+ 'current_device_id': current_device_id
1355
+ }
1356
+
1357
+ finally:
1358
+ cursor.close()
1359
+ conn.close()
1360
+
1361
+ def logout_device(self, user_id, device_id):
1362
+ """登出指定设备"""
1363
+ conn = self.pool.connection()
1364
+ cursor = conn.cursor()
1365
+
1366
+ try:
1367
+ current_time_utc = datetime.now(timezone.utc)
1368
+
1369
+ # 查找设备
1370
+ cursor.execute('''
1371
+ SELECT id, device_name FROM device_sessions
1372
+ WHERE user_id = %s AND device_id = %s AND is_active = 1
1373
+ ''', (user_id, device_id))
1374
+
1375
+ device = cursor.fetchone()
1376
+
1377
+ if not device:
1378
+ return {'success': False, 'message': '设备不存在或已登出'}
1379
+
1380
+ device_session_id = device['id']
1381
+ device_name = device['device_name']
1382
+
1383
+ # 撤销该设备的刷新令牌
1384
+ cursor.execute('''
1385
+ UPDATE refresh_tokens
1386
+ SET is_revoked = 1, revoked_at = %s, revoked_reason = 'single_device_logout'
1387
+ WHERE device_session_id = %s AND is_revoked = 0
1388
+ ''', (current_time_utc, device_session_id))
1389
+
1390
+ # 停用设备会话
1391
+ cursor.execute('''
1392
+ UPDATE device_sessions
1393
+ SET is_active = 0
1394
+ WHERE id = %s
1395
+ ''', (device_session_id,))
1396
+
1397
+ return {'success': True, 'message': f'设备 "{device_name}" 已成功登出'}
1398
+
1399
+ except Exception as e:
1400
+ return {'success': False, 'message': f'登出设备失败: {str(e)}'}
1401
+ finally:
1402
+ cursor.close()
1403
+ conn.close()
1404
+
1405
+ def get_user_profile_stats(self, user_id):
1406
+ """获取用户资料统计信息"""
1407
+ conn = self.pool.connection()
1408
+ cursor = conn.cursor()
1409
+
1410
+ try:
1411
+ # 获取用户基本信息
1412
+ cursor.execute('''
1413
+ SELECT username, email, role, permissions, created_at, last_login, is_active
1414
+ FROM users WHERE id = %s
1415
+ ''', (user_id,))
1416
+
1417
+ user_info = cursor.fetchone()
1418
+
1419
+ if not user_info:
1420
+ return {'error': '用户不存在'}
1421
+
1422
+ # 获取活跃设备数
1423
+ cursor.execute('''
1424
+ SELECT COUNT(*) as active_devices FROM device_sessions
1425
+ WHERE user_id = %s AND is_active = 1
1426
+ ''', (user_id,))
1427
+
1428
+ active_devices = cursor.fetchone()['active_devices']
1429
+
1430
+ # 获取登录次数(成功的登录)
1431
+ cursor.execute('''
1432
+ SELECT COUNT(*) as login_count FROM login_logs
1433
+ WHERE user_id = %s AND login_result = 'success'
1434
+ ''', (user_id,))
1435
+
1436
+ login_count = cursor.fetchone()['login_count']
1437
+
1438
+ # 获取最近登录记录
1439
+ cursor.execute('''
1440
+ SELECT ip_address, user_agent, login_time FROM login_logs
1441
+ WHERE user_id = %s AND login_result = 'success'
1442
+ ORDER BY login_time DESC LIMIT 5
1443
+ ''', (user_id,))
1444
+
1445
+ recent_logins = cursor.fetchall()
1446
+
1447
+ return {
1448
+ 'user_info': {
1449
+ 'username': user_info['username'],
1450
+ 'email': user_info['email'],
1451
+ 'role': user_info['role'],
1452
+ 'permissions': self._safe_json_parse(user_info['permissions']),
1453
+ 'created_at': user_info['created_at'].isoformat() if user_info['created_at'] else None,
1454
+ 'last_login': user_info['last_login'].isoformat() if user_info['last_login'] else None,
1455
+ 'is_active': user_info['is_active']
1456
+ },
1457
+ 'statistics': {
1458
+ 'active_devices': active_devices,
1459
+ 'total_login_count': login_count,
1460
+ 'max_concurrent_devices': self.auth_config['max_concurrent_devices']
1461
+ },
1462
+ 'recent_logins': [
1463
+ {
1464
+ 'ip_address': login['ip_address'],
1465
+ 'user_agent': login['user_agent'],
1466
+ 'login_time': login['login_time'].isoformat() if login['login_time'] else None
1467
+ }
1468
+ for login in recent_logins
1469
+ ]
1470
+ }
1471
+
1472
+ except Exception as e:
1473
+ return {'error': f'获取用户统计信息失败: {str(e)}'}
1474
+ finally:
1475
+ cursor.close()
1476
+ conn.close()
1477
+
1478
+ def cleanup_inactive_devices(self, user_id, days_threshold=30):
1479
+ """清理不活跃设备"""
1480
+ conn = self.pool.connection()
1481
+ cursor = conn.cursor()
1482
+
1483
+ try:
1484
+ current_time_utc = datetime.now(timezone.utc)
1485
+ threshold_time = current_time_utc - timedelta(days=days_threshold)
1486
+
1487
+ # 查找不活跃的设备
1488
+ cursor.execute('''
1489
+ SELECT id, device_name FROM device_sessions
1490
+ WHERE user_id = %s AND is_active = 1
1491
+ AND last_activity < %s
1492
+ ''', (user_id, threshold_time))
1493
+
1494
+ inactive_devices = cursor.fetchall()
1495
+ cleaned_count = len(inactive_devices)
1496
+
1497
+ if cleaned_count > 0:
1498
+ # 撤销这些设备的令牌
1499
+ device_session_ids = [device['id'] for device in inactive_devices]
1500
+ cursor.execute('''
1501
+ UPDATE refresh_tokens
1502
+ SET is_revoked = 1, revoked_at = %s, revoked_reason = 'inactive_cleanup'
1503
+ WHERE device_session_id IN ({})
1504
+ AND is_revoked = 0
1505
+ '''.format(','.join(['%s'] * len(device_session_ids))),
1506
+ [current_time_utc] + device_session_ids)
1507
+
1508
+ # 停用设备会话
1509
+ cursor.execute('''
1510
+ UPDATE device_sessions
1511
+ SET is_active = 0
1512
+ WHERE id IN ({})
1513
+ '''.format(','.join(['%s'] * len(device_session_ids))), device_session_ids)
1514
+
1515
+ return {
1516
+ 'success': True,
1517
+ 'cleaned_count': cleaned_count,
1518
+ 'message': f'成功清理 {cleaned_count} 个不活跃设备'
1519
+ }
1520
+
1521
+ except Exception as e:
1522
+ return {
1523
+ 'success': False,
1524
+ 'cleaned_count': 0,
1525
+ 'message': f'清理不活跃设备失败: {str(e)}'
1526
+ }
1527
+ finally:
1528
+ cursor.close()
1529
+ conn.close()
1530
+
1304
1531
 
1305
1532
  # Flask集成装饰器
1306
1533
  def require_auth(auth_manager):
@@ -1,6 +1,5 @@
1
1
  """
2
- 路由监控数据分析工具
3
- 提供专业的监控数据查询、分析和报告功能
2
+ 数据分析工具
4
3
 
5
4
  主要功能:
6
5
  1. 实时监控数据查询
@@ -23,8 +22,9 @@ from mdbq.myconf import myconf
23
22
  class MonitorAnalytics:
24
23
  """监控数据分析类"""
25
24
 
26
- def __init__(self):
25
+ def __init__(self, database='api_monitor_logs'):
27
26
  """初始化分析工具"""
27
+ self.database = database
28
28
  self.init_database_pool()
29
29
 
30
30
  def init_database_pool(self):
@@ -49,6 +49,7 @@ class MonitorAnalytics:
49
49
  port=int(port),
50
50
  user=username,
51
51
  password=password,
52
+ database=self.database,
52
53
  ping=1,
53
54
  charset='utf8mb4',
54
55
  cursorclass=pymysql.cursors.DictCursor,
@@ -1,6 +1,5 @@
1
1
  """
2
- 路由监控系统
3
- 专业的API接口访问监控、记录和统计系统
2
+ 核心监控模块
4
3
 
5
4
  主要功能:
6
5
  1. 监控所有路由接口的访问请求
@@ -1,6 +1,5 @@
1
1
  """
2
- 路由监控管理界面
3
- 提供完整的监控数据查看和管理功能的Flask路由
2
+ 管理路由API
4
3
 
5
4
  主要功能:
6
5
  1. 实时监控面板
@@ -24,6 +23,216 @@ import json
24
23
  monitor_bp = Blueprint('monitor', __name__, url_prefix='/admin/monitor')
25
24
 
26
25
 
26
+ @monitor_bp.route('/', methods=['GET'])
27
+ @monitor_request
28
+ def monitor_ui():
29
+ """监控面板可视化界面(前端页面)"""
30
+ return render_template_string("""
31
+ <!doctype html>
32
+ <html lang=\"zh-CN\">
33
+ <head>
34
+ <meta charset=\"utf-8\" />
35
+ <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
36
+ <title>API监控面板</title>
37
+ <style>
38
+ :root { --bg:#0b1220; --card:#111a2b; --text:#e6edf3; --sub:#a0a8b3; --ok:#16a34a; --warn:#f59e0b; --err:#ef4444; --muted:#22304a; }
39
+ * { box-sizing: border-box; }
40
+ body { margin:0; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial; background: var(--bg); color: var(--text); }
41
+ header { padding: 16px 24px; border-bottom: 1px solid var(--muted); display:flex; align-items:center; justify-content:space-between; }
42
+ header h1 { margin: 0; font-size: 18px; }
43
+ .wrap { padding: 20px; max-width: 1200px; margin: 0 auto; }
44
+ .grid { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 16px; }
45
+ .card { background: var(--card); border: 1px solid var(--muted); border-radius: 12px; padding: 16px; }
46
+ .kpi { font-size: 12px; color: var(--sub); margin-bottom: 6px; }
47
+ .val { font-size: 22px; font-weight: 700; }
48
+ .row { display:flex; gap: 16px; margin-top: 16px; }
49
+ .row .card { flex: 1; }
50
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
51
+ th, td { border-bottom: 1px solid var(--muted); padding: 8px 6px; text-align: left; }
52
+ th { color: var(--sub); font-weight: 600; }
53
+ .badge { display:inline-block; padding:2px 8px; border-radius: 999px; font-size: 12px; }
54
+ .badge.ok { background:#14351f; color:#4ade80; }
55
+ .badge.warn { background:#3a2f16; color:#facc15; }
56
+ .badge.err { background:#3b1112; color:#fda4af; }
57
+ .muted { color: var(--sub); }
58
+ .controls { display:flex; gap:10px; align-items:center; }
59
+ input, select, button { background: #0e1626; color: var(--text); border:1px solid var(--muted); padding:8px 10px; border-radius: 8px; }
60
+ button.primary { background: #1f2937; border-color:#2b3a55; cursor:pointer; }
61
+ button.primary:hover { background:#263349; }
62
+ .footer { color: var(--sub); font-size: 12px; margin-top: 12px; }
63
+ @media (max-width: 960px) { .grid { grid-template-columns: repeat(2, minmax(0,1fr)); } }
64
+ @media (max-width: 640px) { .grid { grid-template-columns: 1fr; } }
65
+ </style>
66
+ </head>
67
+ <body>
68
+ <header>
69
+ <h1>API监控面板</h1>
70
+ <div class=\"controls\">
71
+ <label class=\"muted\">自动刷新</label>
72
+ <select id=\"autoRefresh\">
73
+ <option value=\"0\">关闭</option>
74
+ <option value=\"15\">15s</option>
75
+ <option value=\"30\" selected>30s</option>
76
+ <option value=\"60\">60s</option>
77
+ </select>
78
+ <button class=\"primary\" id=\"refreshBtn\">立即刷新</button>
79
+ </div>
80
+ </header>
81
+ <div class=\"wrap\">
82
+ <section class=\"grid\">
83
+ <div class=\"card\"><div class=\"kpi\">近1小时请求数</div><div class=\"val\" id=\"k_requests_hour\">-</div></div>
84
+ <div class=\"card\"><div class=\"kpi\">近24小时请求数</div><div class=\"val\" id=\"k_requests_day\">-</div></div>
85
+ <div class=\"card\"><div class=\"kpi\">平均响应时间(ms)</div><div class=\"val\" id=\"k_avg_rt\">-</div></div>
86
+ <div class=\"card\"><div class=\"kpi\">错误率(%)</div><div class=\"val\" id=\"k_err_rate\">-</div></div>
87
+ </section>
88
+
89
+ <div class=\"row\">
90
+ <div class=\"card\">
91
+ <h3 class=\"muted\">热门端点(近1小时)</h3>
92
+ <table id=\"tbl_top_endpoints\"><thead><tr><th>端点</th><th>请求数</th><th>平均耗时(ms)</th></tr></thead><tbody></tbody></table>
93
+ </div>
94
+ <div class=\"card\">
95
+ <h3 class=\"muted\">告警</h3>
96
+ <table id=\"tbl_alerts\"><thead><tr><th>类型</th><th>级别</th><th>消息</th><th>时间</th></tr></thead><tbody></tbody></table>
97
+ </div>
98
+ </div>
99
+
100
+ <div class=\"card\" style=\"margin-top:16px;\">
101
+ <div style=\"display:flex; align-items:center; justify-content:space-between;\">
102
+ <h3 class=\"muted\">流量趋势(日)</h3>
103
+ <div class=\"controls\">
104
+ <label class=\"muted\">天数</label>
105
+ <select id=\"days\">
106
+ <option value=\"7\" selected>7</option>
107
+ <option value=\"14\">14</option>
108
+ <option value=\"30\">30</option>
109
+ </select>
110
+ </div>
111
+ </div>
112
+ <table id=\"tbl_daily\"><thead><tr><th>日期</th><th>请求数</th><th>唯一IP</th><th>平均响应(ms)</th><th>错误数</th></tr></thead><tbody></tbody></table>
113
+ </div>
114
+
115
+ <div class=\"card\" style=\"margin-top:16px;\">
116
+ <h3 class=\"muted\">请求搜索</h3>
117
+ <div class=\"controls\" style=\"margin-bottom:10px; flex-wrap:wrap;\">
118
+ <input id=\"q_endpoint\" placeholder=\"端点包含...\" style=\"min-width:200px;\" />
119
+ <input id=\"q_client_ip\" placeholder=\"客户端IP\" />
120
+ <select id=\"q_method\"><option value=\"\">方法</option><option>GET</option><option>POST</option><option>PUT</option><option>DELETE</option></select>
121
+ <input id=\"q_status\" placeholder=\"状态码\" type=\"number\" style=\"width:120px;\" />
122
+ <input id=\"q_min_rt\" placeholder=\"最小耗时(ms)\" type=\"number\" style=\"width:140px;\" />
123
+ <button class=\"primary\" id=\"btn_search\">搜索</button>
124
+ </div>
125
+ <table id=\"tbl_requests\"><thead><tr><th>时间</th><th>请求ID</th><th>方法</th><th>端点</th><th>状态</th><th>耗时(ms)</th><th>IP</th></tr></thead><tbody></tbody></table>
126
+ <div class=\"footer\" id=\"pg_info\"></div>
127
+ </div>
128
+ </div>
129
+
130
+ <script>
131
+ async function fetchJSON(url, options) {
132
+ const res = await fetch(url, options);
133
+ if (!res.ok) throw new Error('请求失败');
134
+ const data = await res.json();
135
+ if (data && data.data) return data.data;
136
+ return data;
137
+ }
138
+
139
+ function setText(id, val) { document.getElementById(id).textContent = val ?? '-'; }
140
+
141
+ async function loadRealtime() {
142
+ const data = await fetchJSON('/admin/monitor/metrics/realtime');
143
+ const m = data.realtime_metrics || {};
144
+ setText('k_requests_hour', m.requests_per_hour ?? '-');
145
+ setText('k_requests_day', m.requests_per_day ?? '-');
146
+ setText('k_avg_rt', m.avg_response_time ?? '-');
147
+ setText('k_err_rate', m.error_rate ?? '-');
148
+ const tbody = document.querySelector('#tbl_top_endpoints tbody');
149
+ tbody.innerHTML = '';
150
+ (data.top_endpoints || []).forEach(row => {
151
+ const tr = document.createElement('tr');
152
+ tr.innerHTML = `<td>${row.endpoint || '-'}</td><td>${row.request_count || 0}</td><td>${(row.avg_time||0).toFixed ? (row.avg_time||0).toFixed(2) : row.avg_time||0}</td>`;
153
+ tbody.appendChild(tr);
154
+ });
155
+ }
156
+
157
+ async function loadAlerts() {
158
+ const data = await fetchJSON('/admin/monitor/alerts');
159
+ const tbody = document.querySelector('#tbl_alerts tbody');
160
+ tbody.innerHTML = '';
161
+ (data.alerts || []).forEach(a => {
162
+ const tr = document.createElement('tr');
163
+ const sev = (a.severity||'').toUpperCase();
164
+ const cls = sev === 'HIGH' ? 'err' : (sev === 'MEDIUM' ? 'warn' : 'ok');
165
+ tr.innerHTML = `<td>${a.type||'-'}</td><td><span class=\"badge ${cls}\">${sev}</span></td><td>${a.message||'-'}</td><td>${a.timestamp||''}</td>`;
166
+ tbody.appendChild(tr);
167
+ });
168
+ }
169
+
170
+ async function loadTrend() {
171
+ const days = document.getElementById('days').value || 7;
172
+ const data = await fetchJSON(`/admin/monitor/traffic/trend?days=${days}`);
173
+ const tbody = document.querySelector('#tbl_daily tbody');
174
+ tbody.innerHTML = '';
175
+ (data.daily_data || []).forEach(r => {
176
+ const tr = document.createElement('tr');
177
+ tr.innerHTML = `<td>${r.date}</td><td>${r.requests}</td><td>${r.unique_ips}</td><td>${Number(r.avg_response_time||0).toFixed(2)}</td><td>${r.errors}</td>`;
178
+ tbody.appendChild(tr);
179
+ });
180
+ }
181
+
182
+ async function searchRequests(page=1, page_size=20) {
183
+ const body = {
184
+ page, page_size,
185
+ filters: {
186
+ endpoint: document.getElementById('q_endpoint').value || undefined,
187
+ client_ip: document.getElementById('q_client_ip').value || undefined,
188
+ method: document.getElementById('q_method').value || undefined,
189
+ status_code: document.getElementById('q_status').value ? Number(document.getElementById('q_status').value) : undefined,
190
+ min_response_time: document.getElementById('q_min_rt').value ? Number(document.getElementById('q_min_rt').value) : undefined,
191
+ }
192
+ };
193
+ const data = await fetchJSON('/admin/monitor/requests/search', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
194
+ const list = (data.requests || []);
195
+ const tbody = document.querySelector('#tbl_requests tbody');
196
+ tbody.innerHTML = '';
197
+ list.forEach(x => {
198
+ const tr = document.createElement('tr');
199
+ tr.innerHTML = `<td>${x.timestamp || ''}</td><td>${x.request_id || ''}</td><td>${x.method || ''}</td><td>${x.endpoint || ''}</td><td>${x.response_status || ''}</td><td>${x.process_time || ''}</td><td>${x.client_ip || ''}</td>`;
200
+ tbody.appendChild(tr);
201
+ });
202
+ const pg = data.pagination || {};
203
+ document.getElementById('pg_info').textContent = `第 ${pg.current_page||1}/${pg.total_pages||1} 页,共 ${pg.total_count||0} 条`;
204
+ }
205
+
206
+ async function refreshAll() {
207
+ await Promise.all([
208
+ loadRealtime(),
209
+ loadAlerts(),
210
+ loadTrend(),
211
+ searchRequests(1, 20)
212
+ ]);
213
+ }
214
+
215
+ let timer = null;
216
+ function applyAutoRefresh() {
217
+ if (timer) { clearInterval(timer); timer = null; }
218
+ const sec = Number(document.getElementById('autoRefresh').value || 0);
219
+ if (sec > 0) timer = setInterval(refreshAll, sec * 1000);
220
+ }
221
+
222
+ document.getElementById('refreshBtn').addEventListener('click', refreshAll);
223
+ document.getElementById('autoRefresh').addEventListener('change', () => { applyAutoRefresh(); refreshAll(); });
224
+ document.getElementById('days').addEventListener('change', loadTrend);
225
+ document.getElementById('btn_search').addEventListener('click', () => searchRequests(1, 20));
226
+
227
+ // 首次加载
228
+ refreshAll().catch(console.error);
229
+ applyAutoRefresh();
230
+ </script>
231
+ </body>
232
+ </html>
233
+ """)
234
+
235
+
27
236
  @monitor_bp.route('/dashboard', methods=['GET'])
28
237
  @monitor_request
29
238
  def dashboard():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdbq
3
- Version: 4.0.93
3
+ Version: 4.0.94
4
4
  Home-page: https://pypi.org/project/mdbq
5
5
  Author: xigua,
6
6
  Author-email: 2587125111@qq.com
@@ -1 +0,0 @@
1
- VERSION = '4.0.93'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes