mdbq 4.0.92__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.
- {mdbq-4.0.92 → mdbq-4.0.94}/PKG-INFO +1 -1
- mdbq-4.0.94/mdbq/__version__.py +1 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/auth/auth_backend.py +258 -2
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/route/analytics.py +4 -3
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/route/monitor.py +1 -2
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/route/routes.py +211 -2
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq.egg-info/PKG-INFO +1 -1
- mdbq-4.0.92/mdbq/__version__.py +0 -1
- {mdbq-4.0.92 → mdbq-4.0.94}/README.txt +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/__init__.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/auth/__init__.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/auth/rate_limiter.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/js/__init__.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/js/jc.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/log/__init__.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/log/mylogger.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/myconf/__init__.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/myconf/myconf.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/mysql/__init__.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/mysql/deduplicator.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/mysql/mysql.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/mysql/s_query.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/mysql/unique_.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/mysql/uploader.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/other/__init__.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/other/download_sku_picture.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/other/error_handler.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/other/otk.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/other/pov_city.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/other/ua_sj.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/pbix/__init__.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/pbix/pbix_refresh.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/pbix/refresh_all.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/redis/__init__.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/redis/getredis.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/route/__init__.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/selenium/__init__.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/selenium/get_driver.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq/spider/__init__.py +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq.egg-info/SOURCES.txt +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq.egg-info/dependency_links.txt +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/mdbq.egg-info/top_level.txt +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/setup.cfg +0 -0
- {mdbq-4.0.92 → mdbq-4.0.94}/setup.py +0 -0
|
@@ -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':
|
|
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 {
|
|
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)}'}
|
|
@@ -915,6 +927,35 @@ class StandaloneAuthManager:
|
|
|
915
927
|
cursor.close()
|
|
916
928
|
conn.close()
|
|
917
929
|
|
|
930
|
+
def get_user_masked_email(self, username):
|
|
931
|
+
"""根据用户名获取脱敏邮箱(用于忘记密码提示)"""
|
|
932
|
+
conn = self.pool.connection()
|
|
933
|
+
cursor = conn.cursor()
|
|
934
|
+
|
|
935
|
+
try:
|
|
936
|
+
# 查找用户邮箱
|
|
937
|
+
cursor.execute('''
|
|
938
|
+
SELECT email, is_active
|
|
939
|
+
FROM users WHERE username = %s
|
|
940
|
+
''', (username,))
|
|
941
|
+
|
|
942
|
+
user = cursor.fetchone()
|
|
943
|
+
if not user or not user['is_active']:
|
|
944
|
+
return {'success': False, 'message': '用户不存在'}
|
|
945
|
+
|
|
946
|
+
# 返回脱敏邮箱
|
|
947
|
+
masked_email = self._mask_email(user['email'])
|
|
948
|
+
return {
|
|
949
|
+
'success': True,
|
|
950
|
+
'masked_email': masked_email
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
except Exception as e:
|
|
954
|
+
return {'success': False, 'message': f'查询用户邮箱失败: {str(e)}'}
|
|
955
|
+
finally:
|
|
956
|
+
cursor.close()
|
|
957
|
+
conn.close()
|
|
958
|
+
|
|
918
959
|
def request_password_reset(self, username, email):
|
|
919
960
|
"""请求密码重置 - 需要同时验证用户名和邮箱"""
|
|
920
961
|
conn = self.pool.connection()
|
|
@@ -1272,6 +1313,221 @@ class StandaloneAuthManager:
|
|
|
1272
1313
|
|
|
1273
1314
|
return f"{masked_local}@{masked_domain}"
|
|
1274
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
|
+
|
|
1275
1531
|
|
|
1276
1532
|
# Flask集成装饰器
|
|
1277
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
|
-
提供完整的监控数据查看和管理功能的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():
|
mdbq-4.0.92/mdbq/__version__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = '4.0.92'
|
|
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
|
|
File without changes
|
|
File without changes
|