diff --git a/alert_dashboard/docker-compose.yml b/alert_dashboard/docker-compose.yml index 6024926..925f088 100644 --- a/alert_dashboard/docker-compose.yml +++ b/alert_dashboard/docker-compose.yml @@ -4,17 +4,19 @@ services: volumes: - ./www:/usr/share/nginx/html - ./www/php-fpm/www.conf:/usr/local/etc/php-fpm.d/www.conf - command: > - sh -c "apk add --no-cache $PHPIZE_DEPS && docker-php-ext-install mysqli pdo_mysql && apk del $PHPIZE_DEPS && php-fpm" + - /var/run/docker.sock:/var/run/docker.sock + group_add: + - "994" + command: sh -c "apk add --no-cache docker-cli; addgroup -g 994 docker; addgroup www-data docker; docker-php-ext-install mysqli pdo_mysql; exec php-fpm" nginx: image: nginx:stable-alpine ports: - "8080:80" volumes: - - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf + - /var/run/docker.sock:/var/run/docker.sock - ./www:/usr/share/nginx/html depends_on: - php - # 使用nc探测tcp端口,不再用wget(wget不兼容fastcgi) command: ["/bin/sh", "-c", "until nc -z php 9000; do echo '等待php-fpm端口就绪...'; sleep 1; done; nginx -g 'daemon off;'"] diff --git a/alert_dashboard/docker-compose.yml.4 b/alert_dashboard/docker-compose.yml.4 new file mode 100644 index 0000000..bb1c1d8 --- /dev/null +++ b/alert_dashboard/docker-compose.yml.4 @@ -0,0 +1,24 @@ +services: + php: + image: php:7.4-fpm-alpine + volumes: + - ./www:/usr/share/nginx/html + - ./www/php-fpm/www.conf:/usr/local/etc/php-fpm.d/www.conf + - /var/run/docker.sock:/var/run/docker.sock + group_add: + - "994" + command: > + sh -c "apk add --no-cache $PHPIZE_DEPS && docker-php-ext-install mysqli pdo_mysql && apk del $PHPIZE_DEPS && php-fpm" + + nginx: + image: nginx:stable-alpine + ports: + - "8080:80" + volumes: + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf + - /var/run/docker.sock:/var/run/docker.sock + - ./www:/usr/share/nginx/html + depends_on: + - php + # 使用nc探测tcp端口,不再用wget(wget不兼容fastcgi) + command: ["/bin/sh", "-c", "until nc -z php 9000; do echo '等待php-fpm端口就绪...'; sleep 1; done; nginx -g 'daemon off;'"] diff --git a/alert_dashboard/webhook.log b/alert_dashboard/webhook.log index 36c4f15..3dd535a 100644 --- a/alert_dashboard/webhook.log +++ b/alert_dashboard/webhook.log @@ -203,3 +203,91 @@ nohup: ignoring input 10.150.117.190 - - [07/Jul/2026 11:46:03] "POST /alert HTTP/1.1" 200 - 10.150.117.190 - - [07/Jul/2026 12:12:58] "POST /alert HTTP/1.1" 200 - 10.150.117.190 - - [07/Jul/2026 12:13:48] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 13:37:13] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 13:42:58] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 13:43:23] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 13:45:58] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 13:46:48] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 13:54:28] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 13:54:38] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 14:20:43] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 14:21:23] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 14:25:43] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 14:25:43] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 14:25:53] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 14:27:58] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 14:28:38] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 14:28:53] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 14:40:58] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 14:43:38] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 14:45:49] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 14:45:49] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [07/Jul/2026 17:32:51] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 09:56:28] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 09:57:43] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 09:59:18] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 09:59:23] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 10:08:43] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 10:08:43] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 10:09:03] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 10:09:28] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 10:10:23] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 10:10:38] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 10:15:28] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 10:17:38] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 10:18:43] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 10:19:03] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 11:11:43] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 11:19:53] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 11:26:28] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 11:26:38] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:25:58] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:26:18] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:35:13] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:36:03] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:40:43] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:42:33] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:21] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:36] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 13:52:36] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 14:09:28] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 14:09:48] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 14:15:02] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 14:15:02] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 14:15:02] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 14:15:02] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 14:15:12] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 14:15:12] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 14:15:12] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 14:15:12] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 14:15:12] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 14:15:12] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 14:15:17] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 15:18:33] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 15:18:33] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 15:18:33] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 15:18:33] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 15:18:33] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 15:18:33] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 15:18:33] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 15:18:33] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 15:18:48] "POST /alert HTTP/1.1" 200 - +10.150.117.190 - - [08/Jul/2026 15:19:03] "POST /alert HTTP/1.1" 200 - diff --git a/alert_dashboard/www/alert_list.php b/alert_dashboard/www/alert_list.php index 408918a..94f2785 100644 --- a/alert_dashboard/www/alert_list.php +++ b/alert_dashboard/www/alert_list.php @@ -2,6 +2,7 @@ session_start(); header("Cache-Control: no-store, no-cache, must-revalidate"); header("Pragma: no-cache"); +require_once 'auth.php'; $expire = 1800; if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) { session_destroy(); @@ -80,6 +81,7 @@ td{padding:12px 10px;border-bottom:1px solid #F2F3F5;color:#1D2129;} diff --git a/alert_dashboard/www/api/__pycache__/docker.cpython-36.pyc b/alert_dashboard/www/api/__pycache__/docker.cpython-36.pyc new file mode 100644 index 0000000..03f306a Binary files /dev/null and b/alert_dashboard/www/api/__pycache__/docker.cpython-36.pyc differ diff --git a/alert_dashboard/www/api/docker.php b/alert_dashboard/www/api/docker.php new file mode 100644 index 0000000..607471d --- /dev/null +++ b/alert_dashboard/www/api/docker.php @@ -0,0 +1,165 @@ + session_id(), + "session_data" => $_SESSION +]; + +$expire = 1800; +if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) { + session_destroy(); + echo json_encode([ + "code"=>401, + "msg"=>"未登录", + "debug"=>$debug + ],JSON_UNESCAPED_UNICODE); + exit; +} + +$user = $_SESSION['user_id']; +$role = $_SESSION['role']; +$ts = time(); +$rawSignStr = $user . "|" . $role . "|" . $ts; +$sign = hash_hmac("sha256", $rawSignStr, $SECRET_RAW); +$clientIp = $_SERVER['REMOTE_ADDR']; + +$act = $_POST['act'] ?? $_GET['act'] ?? 'list'; +$cid = $_POST['container_id'] ?? $_GET['container_id'] ?? ''; +$imageId = $_POST['image_id'] ?? $_GET['image_id'] ?? ''; +$lines = $_POST['lines'] ?? $_GET['lines'] ?? 200; +$postRaw = file_get_contents("php://input"); +$apiBase = "http://10.150.117.190:8090"; + +$authHeaders = [ + "X-User: {$user}", + "X-Role: {$role}", + "X-Time: {$ts}", + "X-Sign: {$sign}", + "X-Client-Ip: {$clientIp}", + "Content-Type: application/json; charset=utf-8" +]; +$headerStr = implode("\r\n", $authHeaders); + +$getMap = [ + 'list' => '/list', + 'image_list' => '/image_list', + 'restart' => '/restart/%s', + 'stop' => '/stop/%s', + 'rm' => '/rm/%s', + 'remove_image' => '/remove_image/%s', + 'logs' => '/logs/%s/%d', + 'network' => '/network/%s', + 'stats' => '/stats/%s', + 'detail' => '/detail/%s' +]; + +$postMap = [ + 'exec' => '/exec/%s', + 'create' => '/create', + 'bind_repo' => '/bind_repo', +]; + +if (isset($getMap[$act])) { + $path = $getMap[$act]; + $url = ''; + switch ($act) { + case 'restart': + case 'stop': + case 'rm': + case 'network': + case 'stats': + case 'detail': + if (empty($cid)) { + echo json_encode(["code"=>400,"msg"=>"缺少容器ID","debug"=>$debug],JSON_UNESCAPED_UNICODE); + exit; + } + $url = $apiBase . sprintf($path, urlencode($cid)); + break; + case 'remove_image': + if (empty($imageId)) { + echo json_encode(["code"=>400,"msg"=>"缺少镜像ID","debug"=>$debug],JSON_UNESCAPED_UNICODE); + exit; + } + $url = $apiBase . sprintf($path, urlencode($imageId)); + break; + case 'logs': + if (empty($cid)) { + echo json_encode(["code"=>400,"msg"=>"缺少容器ID","debug"=>$debug],JSON_UNESCAPED_UNICODE); + exit; + } + $url = $apiBase . sprintf($path, urlencode($cid), (int)$lines); + break; + default: + $url = $apiBase . $path; + break; + } + $ctx = stream_context_create([ + 'http' => [ + 'method' => 'GET', + 'header' => $headerStr, + 'timeout' => 30 + ] + ]); + $resp = @file_get_contents($url, false, $ctx); + if ($resp === false) { + echo json_encode([ + "code" => 503, + "msg" => "后端Docker接口连接失败,请检查服务是否启动", + "debug" => $debug + ], JSON_UNESCAPED_UNICODE); + exit; + } + echo $resp; + exit; +} + +if (isset($postMap[$act])) { + $path = $postMap[$act]; + $url = ''; + if ($act === 'exec') { + if (empty($cid)) { + echo json_encode(["code"=>400,"msg"=>"缺少容器ID","debug"=>$debug],JSON_UNESCAPED_UNICODE); + exit; + } + $url = $apiBase . sprintf($path, urlencode($cid)); + } else { + $url = $apiBase . $path; + } + $ctx = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => $headerStr, + 'content' => $postRaw, + 'timeout' => 30 + ] + ]); + $resp = @file_get_contents($url, false, $ctx); + if ($resp === false) { + echo json_encode([ + "code" => 503, + "msg" => "后端Docker接口连接失败,请检查服务是否启动", + "debug" => $debug + ], JSON_UNESCAPED_UNICODE); + exit; + } + echo $resp; + exit; +} + +echo json_encode([ + "code"=>400, + "msg"=>"无效操作act参数", + "debug"=>$debug +],JSON_UNESCAPED_UNICODE); +exit; diff --git a/alert_dashboard/www/api/docker_api.py b/alert_dashboard/www/api/docker_api.py new file mode 100644 index 0000000..c925759 --- /dev/null +++ b/alert_dashboard/www/api/docker_api.py @@ -0,0 +1,752 @@ +import json +import shlex +import time +import hashlib +import hmac +import docker +from flask import Flask, jsonify, request +from flask_socketio import SocketIO + +# 全局签名密钥,前后端统一,生产环境使用环境变量注入 +SIGN_SECRET = b"DockerAdminPlatform2026SecretKey123456789" +# 签名有效期 300秒(5分钟) +SIGN_EXPIRE = 300 + +# 兼容新旧docker-py客户端 +try: + cli = docker.DockerClient(base_url='unix:///var/run/docker.sock') + api_cli = docker.APIClient(base_url='unix:///var/run/docker.sock') +except AttributeError: + cli = docker.Client(base_url='unix:///var/run/docker.sock') + api_cli = docker.APIClient(base_url='unix:///var/run/docker.sock') + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'docker-terminal-secret-2026' + +# WebSocket心跳保活,防止Nginx/反向代理断连 +socketio = SocketIO( + app, + cors_allowed_origins="*", + async_mode="threading", + ping_timeout=60, + ping_interval=25 +) + +# 全局会话存储 +session_exec_map = {} # sid => TTY终端流信息 +log_task_map = {} # sid => {"cid": "", "running": True} +ws_user_map = {} # 新增:sid 映射 WebSocket鉴权用户信息 +registry_config = { + "repo_name": "", + "repo_addr": "", + "repo_user": "", + "repo_pwd": "" +} +# 多系统shell兼容列表(Ubuntu/Alpine/标准Linux) +SHELL_CANDIDATES = [ + "/bin/bash", + "/usr/bin/bash", + "/bin/sh", + "/bin/ash" +] + +# ===================== 工具函数1:docker socket兼容recv ===================== +def docker_recv(sock, size=1024): + try: + return sock.recv(size) + except Exception: + try: + return sock._sock.recv(size) + except Exception: + return b"" + +# ===================== 工具函数2:操作审计日志(终端输入脱敏,不存明文密码) ===================== +def write_audit_log(user, role, action, target_name, target_id, result, client_ip): + """企业审计记录,可对接数据库docker_audit_log表""" + # 终端输入仅标记事件,不记录明文命令,规避密码泄露风险 + if action == "terminal_input": + result = "command_entered" + log_item = { + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), + "user": user, + "role": role, + "action": action, + "container_name": target_name, + "container_id": target_id, + "result": result, + "client_ip": client_ip + } + # 生产环境替换为mysql insert语句 + print(json.dumps(log_item, ensure_ascii=False)) + +# ===================== 工具函数3:HTTP Header HMAC签名鉴权(原有,HTTP接口专用) ===================== +def verify_auth_sign(): + """ + 校验前端PHP生成的HMAC签名,防止伪造X-Role:admin + 请求Header要求: + X-User: 用户名 + X-Role: admin/user/guest + X-Time: 当前时间戳(秒) + X-Sign: hmac-sha256签名 + 返回 (user, role, client_ip, True/False) + """ + user = request.headers.get("X-User", "anonymous") + role = request.headers.get("X-Role", "guest") + ts_str = request.headers.get("X-Time", "0") + sign = request.headers.get("X-Sign", "") + client_ip = request.headers.get("X-Client-Ip", request.remote_addr) + try: + ts = int(ts_str) + except ValueError: + return user, role, client_ip, False + # 校验签名有效期 + now = int(time.time()) + if abs(now - ts) > SIGN_EXPIRE: + return user, role, client_ip, False + # 校验HMAC签名 + raw_data = f"{user}|{role}|{ts}".encode("utf-8") + calc_sign = hmac.new(SIGN_SECRET, raw_data, hashlib.sha256).hexdigest() + if calc_sign != sign: + return user, role, client_ip, False + return user, role, client_ip, True + +# ===================== 新增工具函数4:WebSocket连接专用鉴权函数 ===================== +def verify_socket_auth(auth): + user = auth.get("user", "") + role = auth.get("role", "") + ts_str = auth.get("ts", "0") + sign = auth.get("sign", "") + try: + ts = int(ts_str) + except: + return user, role, "", False + if abs(int(time.time()) - ts) > SIGN_EXPIRE: + return user, role, "", False + raw_data = f"{user}|{role}|{ts}".encode("utf-8") + calc_sign = hmac.new( + SIGN_SECRET, + raw_data, + hashlib.sha256 + ).hexdigest() + if calc_sign != sign: + return user, role, "", False + return user, role, "", True + +# ===================== 工具函数5:环境变量脱敏,屏蔽密码/密钥 ===================== +def safe_env_filter(env_list): + safe_env = [] + sensitive_keywords = ["PASS", "PASSWORD", "TOKEN", "SECRET", "KEY", "CREDENTIAL"] + for item in env_list: + equal_pos = item.find("=") + if equal_pos == -1: + safe_env.append(item) + continue + key = item[:equal_pos] + val = item[equal_pos+1:] + if any(word in key.upper() for word in sensitive_keywords): + safe_env.append(f"{key}=******") + else: + safe_env.append(item) + return safe_env + +# ===================== 全部HTTP业务接口 ===================== +# 1. 容器列表(端口预拼接字符串,前端直接渲染,无需formatPorts) +@app.route("/list") +def api_list(): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "list_container", "", "", "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + arr = [] + for c in cli.containers.list(all=True): + img_name = c.image.tags[0] if c.image.tags else "" + port_str_arr = [] + ports_raw = c.ports + for c_port, host_list in ports_raw.items(): + if not host_list: + continue + for host_item in host_list: + host_port = host_item.get("HostPort", "") + port_str_arr.append(f"{host_port}:{c_port.split('/')[0]}") + port_display = ",".join(port_str_arr) if port_str_arr else "-" + arr.append({ + "id": c.short_id, + "full_id": c.id, + "name": c.name, + "image": img_name, + "status": c.status, + "ports": port_display + }) + write_audit_log(user, role, "list_container", "", "", "success", ip) + return jsonify({"code":0,"msg":"success","list":arr}) + +# 2. 镜像列表 +@app.route("/image_list") +def api_image_list(): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "list_image", "", "", "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + arr = [] + for img in cli.images.list(): + if not img.tags: + continue + full_tag = img.tags[0] + repo, tag = full_tag.split(":", 1) + size_mb = round(img.attrs["Size"] / 1024 / 1024, 1) + arr.append({ + "repo": repo, + "tag": tag, + "id": img.short_id, + "full_id": img.id, + "size": f"{size_mb}MB", + "created": img.attrs["Created"] + }) + write_audit_log(user, role, "list_image", "", "", "success", ip) + return jsonify({"code":0,"msg":"success","list":arr}) + +# 3. 删除镜像 +@app.route("/remove_image/") +def api_remove_image(image_id): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "remove_image", "", image_id, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + if role != "admin": + write_audit_log(user, role, "remove_image", "", image_id, "denied", ip) + return jsonify({"code":403,"msg":"权限不足,仅管理员可删除镜像"}) + try: + img = cli.images.get(image_id) + cli.images.remove(image_id, force=True) + write_audit_log(user, role, "remove_image", img.tags[0] if img.tags else "", image_id, "success", ip) + return jsonify({"code":0,"msg":"镜像已强制删除"}) + except Exception as e: + write_audit_log(user, role, "remove_image", "", image_id, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e)}) + +# 4. 重启容器 +@app.route("/restart/") +def api_restart(cid): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "restart", "", cid, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + if role != "admin": + write_audit_log(user, role, "restart", "", cid, "denied", ip) + return jsonify({"code":403,"msg":"权限不足,仅管理员可操作容器"}) + try: + container = cli.containers.get(cid) + container.restart() + write_audit_log(user, role, "restart", container.name, cid, "success", ip) + return jsonify({"code":0,"msg":"容器已重启"}) + except Exception as e: + write_audit_log(user, role, "restart", "", cid, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e)}) + +# 5. 停止容器 +@app.route("/stop/") +def api_stop(cid): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "stop", "", cid, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + if role != "admin": + write_audit_log(user, role, "stop", "", cid, "denied", ip) + return jsonify({"code":403,"msg":"权限不足,仅管理员可操作容器"}) + try: + container = cli.containers.get(cid) + container.stop() + write_audit_log(user, role, "stop", container.name, cid, "success", ip) + return jsonify({"code":0,"msg":"容器已停止"}) + except Exception as e: + write_audit_log(user, role, "stop", "", cid, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e)}) + +# 6. 删除容器 +@app.route("/rm/") +def api_rm(cid): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "rm", "", cid, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + if role != "admin": + write_audit_log(user, role, "rm", "", cid, "denied", ip) + return jsonify({"code":403,"msg":"权限不足,仅管理员可删除容器"}) + try: + container = cli.containers.get(cid) + container.remove(force=True) + write_audit_log(user, role, "rm", container.name, cid, "success", ip) + return jsonify({"code":0,"msg":"容器已强制删除"}) + except Exception as e: + write_audit_log(user, role, "rm", "", cid, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e)}) + +# 7. 一次性拉取日志(兼容旧页面单次查看) +@app.route("/logs//") +def api_logs(cid, tail_lines): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "read_log", "", cid, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + try: + container = cli.containers.get(cid) + log_bytes = container.logs(tail=tail_lines) + log_str = log_bytes.decode("utf-8", errors="replace") + write_audit_log(user, role, "read_log", container.name, cid, "success", ip) + return jsonify({"code":0,"msg":"success","data":log_str}) + except Exception as e: + write_audit_log(user, role, "read_log", "", cid, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e),"data":""}) + +# 8. 容器网络详情 +@app.route("/network/") +def api_network(cid): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "read_network", "", cid, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + try: + container = cli.containers.get(cid) + info = container.attrs["NetworkSettings"]["Networks"] + write_audit_log(user, role, "read_network", container.name, cid, "success", ip) + return jsonify({"code":0,"msg":"success","data":json.dumps(info,indent=2)}) + except Exception as e: + write_audit_log(user, role, "read_network", "", cid, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e),"data":""}) + +# 9. 短连接单次执行命令(旧弹窗兼容,非实时终端) +@app.route("/exec/", methods=["POST"]) +def api_exec(cid): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "single_exec", "", cid, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + if role != "admin": + write_audit_log(user, role, "single_exec", "", cid, "denied", ip) + return jsonify({"code":403,"msg":"权限不足,仅管理员可执行命令"}) + try: + data = request.get_json() + cmd = data.get("cmd", "") + if not cmd: + return jsonify({"code":400,"msg":"命令不能为空","data":""}) + cmd_list = shlex.split(cmd) + container = cli.containers.get(cid) + exec_obj = container.exec_run(cmd_list, stdout=True, stderr=True) + output = exec_obj.output.decode("utf-8", errors="replace") + write_audit_log(user, role, "single_exec", container.name, cid, "command_executed", ip) + return jsonify({"code":0,"msg":"success","data":output}) + except Exception as e: + write_audit_log(user, role, "single_exec", "", cid, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e),"data":""}) + +# 10. 创建容器(端口标准格式 + 前端可配置CPU/内存) +@app.route("/create", methods=["POST"]) +def api_create(): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "create_container", "", "", "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + if role != "admin": + write_audit_log(user, role, "create_container", "", "", "denied", ip) + return jsonify({"code":403,"msg":"权限不足,仅管理员可创建容器"}) + try: + data = request.get_json() + name = data.get("name", "") + image = data.get("image") + cmd = data.get("cmd") + ports_input = data.get("ports", {}) + # 修复:docker标准端口字典格式 {内部端口:宿主机端口} + port_bind = {} + for inner_port, host_port in ports_input.items(): + port_bind[f"{inner_port}/tcp"] = int(host_port) + # 前端可配置资源限制,默认1核512M + cpu_core = int(data.get("cpu", 1)) + memory_limit = data.get("memory", "512m") + nano_cpus = cpu_core * 1000000000 + container = cli.containers.run( + image=image, + name=name if name else None, + command=cmd if cmd else None, + ports=port_bind, + detach=True, + restart_policy={"Name": "always"}, + mem_limit=memory_limit, + nano_cpus=nano_cpus + ) + write_audit_log(user, role, "create_container", container.name, container.id, "success", ip) + return jsonify({"code":0,"msg":f"容器创建成功,ID:{container.short_id}"}) + except Exception as e: + write_audit_log(user, role, "create_container", "", "", f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e)}) + +# 11. 登录私有镜像仓库 +@app.route("/bind_repo", methods=["POST"]) +def api_bind_repo(): + global registry_config + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "repo_login", "", "", "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + if role != "admin": + write_audit_log(user, role, "repo_login", "", "", "denied", ip) + return jsonify({"code":403,"msg":"权限不足,仅管理员可登录镜像仓库"}) + try: + data = request.get_json() + repo_name = data.get("repo_name","") + repo_addr = data.get("repo_addr") + repo_user = data.get("repo_user","") + repo_pwd = data.get("repo_pwd","") + api_cli.login(username=repo_user, password=repo_pwd, registry=repo_addr) + registry_config["repo_name"] = repo_name + registry_config["repo_addr"] = repo_addr + registry_config["repo_user"] = repo_user + registry_config["repo_pwd"] = repo_pwd + write_audit_log(user, role, "repo_login", repo_addr, "", "success", ip) + return jsonify({"code":0,"msg":"镜像仓库登录绑定成功"}) + except Exception as e: + write_audit_log(user, role, "repo_login", repo_addr, "", f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e)}) + +# 修复:stats接口online_cpus兜底兼容,无KeyError +@app.route("/stats/") +def api_stats(cid): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "read_stats", "", cid, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + try: + container = cli.containers.get(cid) + raw = container.stats(stream=False) + # CPU兼容兜底,不存在online_cpus自动计算 + cpu_stats = raw["cpu_stats"] + online_cpus = cpu_stats.get("online_cpus", len(cpu_stats["cpu_usage"].get("percpu_usage", [])) or 1) + cpu_delta = cpu_stats["cpu_usage"]["total_usage"] - raw["precpu_stats"]["cpu_usage"]["total_usage"] + system_delta = cpu_stats["system_cpu_usage"] - raw["precpu_stats"]["system_cpu_usage"] + cpu_percent = 0.0 + if system_delta > 0 and cpu_delta > 0: + cpu_percent = round((cpu_delta / system_delta) * online_cpus * 100, 1) + # 内存计算 + mem_usage = raw["memory_stats"]["usage"] / 1024 / 1024 + mem_limit = raw["memory_stats"]["limit"] / 1024 / 1024 + mem_percent = round((mem_usage / mem_limit) * 100, 1) if mem_limit > 0 else 0 + # 网络流量 + net_rx = 0 + net_tx = 0 + for net in raw["networks"].values(): + net_rx += net["rx_bytes"] / 1024 / 1024 + net_tx += net["tx_bytes"] / 1024 / 1024 + res = { + "cpu": cpu_percent, + "mem_mb": round(mem_usage, 1), + "mem_limit_mb": round(mem_limit, 1), + "mem_percent": mem_percent, + "net_rx_mb": round(net_rx, 1), + "net_tx_mb": round(net_tx, 1) + } + write_audit_log(user, role, "read_stats", container.name, cid, "success", ip) + return jsonify({"code":0,"msg":"success","data":res}) + except Exception as e: + write_audit_log(user, role, "read_stats", "", cid, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e),"data":{}}) + +# 修复:detail接口ENV脱敏,多网卡IP兼容 +@app.route("/detail/") +def api_detail(cid): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "read_detail", "", cid, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + try: + container = cli.containers.get(cid) + attrs = container.attrs + nets = attrs["NetworkSettings"]["Networks"] + ip_map = {} + for net_name, net_info in nets.items(): + ip_map[net_name] = net_info.get("IPAddress", "") + # 敏感环境变量脱敏 + raw_env = attrs["Config"].get("Env", []) + safe_env = safe_env_filter(raw_env) + res = { + "network_ips": ip_map, + "mounts": attrs["Mounts"], + "env": safe_env, + "created": attrs["Created"], + "entrypoint": attrs["Config"]["Entrypoint"], + "cmd": attrs["Config"]["Cmd"], + "restart_policy": attrs["HostConfig"]["RestartPolicy"] + } + write_audit_log(user, role, "read_detail", container.name, cid, "success", ip) + return jsonify({"code":0,"msg":"success","data":res}) + except Exception as e: + write_audit_log(user, role, "read_detail", "", cid, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e),"data":{}}) + +# 根路由 +@app.route("/") +def index(): + return jsonify({"code":400,"msg":"无效操作","list":[],"data":""}) + +# ===================== WebSocket 连接鉴权事件(新增) ===================== +@socketio.on("connect") +def handle_connect(auth): + user, role, _, auth_ok = verify_socket_auth(auth) + if not auth_ok: + return False + ws_user_map[request.sid] = { + "user": user, + "role": role + } + print("WS Connected:", user, role) + +# ===================== WebSocket 交互式TTY实时终端 ===================== +def stream_terminal_output(sid, sock): + """后台异步读取docker终端输出,兼容两种socket接收方式""" + try: + while True: + chunk = docker_recv(sock, 1024) + if not chunk: + break + text = chunk.decode("utf-8", errors="replace") + socketio.emit("terminal_output", {"data": text}, room=sid) + except Exception: + pass + # 流结束自动清理会话 + if sid in session_exec_map: + try: + session_exec_map[sid]["socket"].close() + except Exception: + pass + del session_exec_map[sid] + +@socketio.on("terminal_start") +def handle_terminal_start(json_data): + sid = request.sid + # 新增调试输出 + socketio.emit( + "terminal_output", + { + "data":"[DEBUG] terminal_start reached\r\n" + }, + room=sid + ) + # 替换原verify_auth_sign(),从ws_user_map读取身份 + info = ws_user_map.get(request.sid) + if not info: + socketio.emit( + "terminal_output", + {"data":"身份认证失效\n"}, + room=request.sid + ) + return + user = info["user"] + role = info["role"] + client_ip = request.remote_addr + + # 权限拦截:非管理员禁止打开交互式终端 + if role != "admin": + socketio.emit("terminal_output", {"data": "Permission denied: 仅管理员可访问交互式终端\n"}, room=sid) + write_audit_log(user, role, "terminal_start", "", json_data.get("cid"), "denied", client_ip) + return + + cid = json_data.get("cid") + if not cid: + socketio.emit("terminal_output", {"data": "错误:容器ID不能为空\n"}, room=sid) + write_audit_log(user, role, "terminal_start", "", "", "empty_cid", client_ip) + return + + # 自动探测容器可用shell,兼容全版本docker-py(exec_create返回dict取Id) + target_shell = None + for shell in SHELL_CANDIDATES: + try: + ret = api_cli.exec_create( + container=cid, + cmd=[shell, "-c", "echo ok"] + ) + test_exec_id = ret["Id"] + test_stream = api_cli.exec_start(test_exec_id) + test_stream.read() + target_shell = shell + break + except Exception: + continue + if not target_shell: + socketio.emit("terminal_output", {"data": "错误:容器内无可用shell(bash/sh/ash)\n"}, room=sid) + write_audit_log(user, role, "terminal_start", "", cid, "no_shell", client_ip) + return + + try: + # 创建交互式TTY会话,统一提取Id兼容所有SDK版本 + exec_ret = api_cli.exec_create( + container=cid, + cmd=target_shell, + stdin=True, + stdout=True, + stderr=True, + tty=True + ) + exec_id = exec_ret["Id"] + sock = api_cli.exec_start(exec_id=exec_id, tty=True, socket=True, detach=False) + session_exec_map[sid] = { + "cid": cid, + "exec_id": exec_id, + "socket": sock + } + socketio.start_background_task(stream_terminal_output, sid, sock) + container_name = cli.containers.get(cid).name + write_audit_log(user, role, "terminal_start", container_name, cid, "success", client_ip) + except Exception as e: + socketio.emit("terminal_output", {"data": f"启动终端失败:{str(e)}\n"}, room=sid) + write_audit_log(user, role, "terminal_start", "", cid, f"fail:{str(e)}", client_ip) + +@socketio.on("terminal_input") +def handle_terminal_input(json_data): + """前端输入字符写入容器标准输入,审计脱敏不记录明文""" + sid = request.sid + # 替换原verify_auth_sign() + info = ws_user_map.get(request.sid) + if not info: + return + user = info["user"] + role = info["role"] + client_ip = request.remote_addr + + if sid not in session_exec_map: + socketio.emit("terminal_output", {"data": "终端会话已断开,请重新打开\n"}, room=sid) + return + input_text = json_data.get("data", "") + sock = session_exec_map[sid]["socket"] + sock.send(input_text.encode("utf-8")) + cid = session_exec_map[sid]["cid"] + # 审计仅标记事件,不再记录明文命令,规避密码泄露 + write_audit_log(user, role, "terminal_input", "", cid, "command_entered", client_ip) + +@socketio.on("terminal_resize") +def handle_terminal_resize(json_data): + """前端xterm窗口大小自适应""" + sid = request.sid + if sid not in session_exec_map: + return + exec_id = session_exec_map[sid]["exec_id"] + height = int(json_data.get("height", 24)) + width = int(json_data.get("width", 80)) + try: + api_cli.exec_resize(exec_id=exec_id, height=height, width=width) + except Exception: + pass + +# 新增主动关闭终端事件(解决切换容器残留TTY) +@socketio.on("terminal_close") +def handle_terminal_close(): + sid = request.sid + info = ws_user_map.get(request.sid) + if not info: + return + user = info["user"] + role = info["role"] + client_ip = request.remote_addr + + if sid in session_exec_map: + cid = session_exec_map[sid]["cid"] + try: + session_exec_map[sid]["socket"].close() + except Exception: + pass + del session_exec_map[sid] + write_audit_log(user, role, "terminal_close", "", cid, "manual_close", client_ip) + +@socketio.on("disconnect") +def handle_disconnect(): + """前端断开连接,释放docker socket、清理会话,防止句柄泄漏""" + sid = request.sid + # 清理ws_user_map会话 + if sid in ws_user_map: + del ws_user_map[sid] + info = ws_user_map.get(sid) + if not info: + user = "" + role = "" + client_ip = request.remote_addr + else: + user = info["user"] + role = info["role"] + client_ip = request.remote_addr + + # 清理TTY终端 + if sid in session_exec_map: + cid = session_exec_map[sid]["cid"] + try: + session_exec_map[sid]["socket"].close() + except Exception: + pass + del session_exec_map[sid] + write_audit_log(user, role, "terminal_close", "", cid, "disconnect", client_ip) + # 清理实时日志标记,终止日志循环 + if sid in log_task_map: + del log_task_map[sid] + +# ===================== WebSocket 实时流式日志 docker logs -f(生成器主动释放) ===================== +def stream_follow_log(sid, cid): + generator = None + try: + container = cli.containers.get(cid) + generator = container.logs(stream=True, follow=True) + log_task_map[sid] = {"cid": cid, "running": True} + for line in generator: + task_info = log_task_map.get(sid, {}) + if not task_info.get("running"): + break + txt = line.decode("utf-8", errors="replace") + socketio.emit("log_output", {"data": txt}, room=sid) + except Exception: + pass + finally: + # 主动关闭日志生成器,释放docker daemon长连接 + if generator is not None: + generator.close() + if sid in log_task_map: + del log_task_map[sid] + +@socketio.on("log_start") +def handle_log_start(json_data): + sid = request.sid + # 替换verify_auth_sign,读取ws_user_map + info = ws_user_map.get(request.sid) + if not info: + socketio.emit("log_output", {"data": "身份校验失败"}, room=sid) + write_audit_log("", "", "log_start", "", "", "auth_failed", request.remote_addr) + return + user = info["user"] + role = info["role"] + client_ip = request.remote_addr + + cid = json_data.get("cid") + if not cid: + socketio.emit("log_output", {"data": "容器ID不能为空"}, room=sid) + write_audit_log(user, role, "log_start", "", "", "empty_cid", client_ip) + return + if role != "admin": + socketio.emit("log_output", {"data": "权限不足,仅管理员可查看实时日志"}, room=sid) + write_audit_log(user, role, "log_start", "", cid, "denied", client_ip) + return + socketio.start_background_task(stream_follow_log, sid, cid) + container_name = cli.containers.get(cid).name + write_audit_log(user, role, "log_start", container_name, cid, "success", client_ip) + +@socketio.on("log_stop") +def handle_log_stop(): + sid = request.sid + info = ws_user_map.get(request.sid) + if not info: + return + user = info["user"] + role = info["role"] + client_ip = request.remote_addr + + if sid in log_task_map: + log_task_map[sid]["running"] = False + cid = log_task_map[sid]["cid"] + write_audit_log(user, role, "log_stop", "", cid, "manual_close", client_ip) + +# 启动入口(eventlet异步,必须socketio.run) +if __name__ == "__main__": + socketio.run(app, host="0.0.0.0", port=8090, debug=False, allow_unsafe_werkzeug=True) diff --git a/alert_dashboard/www/api/docker_api.py.bak b/alert_dashboard/www/api/docker_api.py.bak new file mode 100644 index 0000000..b607643 --- /dev/null +++ b/alert_dashboard/www/api/docker_api.py.bak @@ -0,0 +1,100 @@ +import json +import docker +from flask import Flask, jsonify + +# 兼容新旧版本 +try: + cli = docker.DockerClient(base_url='unix:///var/run/docker.sock') +except AttributeError: + cli = docker.Client(base_url='unix:///var/run/docker.sock') + +app = Flask(__name__) + +# 容器列表 +@app.route("/list") +def api_list(): + arr = [] + for c in cli.containers.list(all=True): + img_name = c.image.tags[0] if c.image.tags else "" + arr.append({ + "id": c.short_id, + "full_id": c.id, + "name": c.name, + "image": img_name, + "status": c.status, + "ports": str(c.ports) + }) + return jsonify({"code":0,"msg":"success","list":arr}) + +# 镜像列表 +@app.route("/image_list") +def api_image_list(): + arr = [] + for img in cli.images.list(): + if not img.tags: + continue + full_tag = img.tags[0] + repo, tag = full_tag.split(":", 1) + size_mb = round(img.attrs["Size"] / 1024 / 1024, 1) + arr.append({ + "repo": repo, + "tag": tag, + "id": img.short_id, + "full_id": img.id, + "size": f"{size_mb}MB", + "created": img.attrs["Created"] + }) + return jsonify({"code":0,"msg":"success","list":arr}) + +# 重启容器 +@app.route("/restart/") +def api_restart(cid): + try: + cli.containers.get(cid).restart() + return jsonify({"code":0,"msg":"容器已重启"}) + except Exception as e: + return jsonify({"code":500,"msg":str(e)}) + +# 停止容器 +@app.route("/stop/") +def api_stop(cid): + try: + cli.containers.get(cid).stop() + return jsonify({"code":0,"msg":"容器已停止"}) + except Exception as e: + return jsonify({"code":500,"msg":str(e)}) + +# 删除容器 +@app.route("/rm/") +def api_rm(cid): + try: + cli.containers.get(cid).remove(force=True) + return jsonify({"code":0,"msg":"容器已强制删除"}) + except Exception as e: + return jsonify({"code":500,"msg":str(e)}) + +# 容器日志 +@app.route("/logs//") +def api_logs(cid, tail_lines): + try: + log_bytes = cli.containers.get(cid).logs(tail=tail_lines) + log_str = log_bytes.decode("utf-8", errors="ignore") + return jsonify({"code":0,"msg":"success","data":log_str}) + except Exception as e: + return jsonify({"code":500,"msg":str(e),"data":""}) + +# 网络信息 +@app.route("/network/") +def api_network(cid): + try: + info = cli.containers.get(cid).attrs["NetworkSettings"]["Networks"] + return jsonify({"code":0,"msg":"success","data":info}) + except Exception as e: + return jsonify({"code":500,"msg":str(e),"data":""}) + +@app.route("/") +def index(): + return jsonify({"code":400,"msg":"无效操作","list":[],"data":""}) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8090, debug=False) diff --git a/alert_dashboard/www/api/docker_api.py1 b/alert_dashboard/www/api/docker_api.py1 new file mode 100644 index 0000000..b514e30 --- /dev/null +++ b/alert_dashboard/www/api/docker_api.py1 @@ -0,0 +1,669 @@ +# 【强制置顶】eventlet猴子补丁,必须放在所有导入最前面 +import json +import shlex +import time +import hashlib +import hmac +import docker +from flask import Flask, jsonify, request +from flask_socketio import SocketIO + +# 全局签名密钥,前后端统一,生产环境使用环境变量注入 +SIGN_SECRET = b"DockerAdminPlatform2026SecretKey123456789" +# 签名有效期 300秒(5分钟) +SIGN_EXPIRE = 300 + +# 兼容新旧docker-py客户端 +try: + cli = docker.DockerClient(base_url='unix:///var/run/docker.sock') + api_cli = docker.APIClient(base_url='unix:///var/run/docker.sock') +except AttributeError: + cli = docker.Client(base_url='unix:///var/run/docker.sock') + api_cli = docker.APIClient(base_url='unix:///var/run/docker.sock') + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'docker-terminal-secret-2026' + +# WebSocket心跳保活,防止Nginx/反向代理断连 +socketio = SocketIO( + app, + cors_allowed_origins="*", + async_mode="threading", + ping_timeout=60, + ping_interval=25 +) + +# 全局会话存储 +session_exec_map = {} # sid => TTY终端流信息 +log_task_map = {} # sid => {"cid": "", "running": True} +registry_config = { + "repo_name": "", + "repo_addr": "", + "repo_user": "", + "repo_pwd": "" +} +# 多系统shell兼容列表(Ubuntu/Alpine/标准Linux) +SHELL_CANDIDATES = [ + "/bin/bash", + "/usr/bin/bash", + "/bin/sh", + "/bin/ash" +] + +# ===================== 工具函数1:docker socket兼容recv ===================== +def docker_recv(sock, size=1024): + try: + return sock.recv(size) + except Exception: + try: + return sock._sock.recv(size) + except Exception: + return b"" + +# ===================== 工具函数2:操作审计日志(终端输入脱敏,不存明文密码) ===================== +def write_audit_log(user, role, action, target_name, target_id, result, client_ip): + """企业审计记录,可对接数据库docker_audit_log表""" + # 终端输入仅标记事件,不记录明文命令,规避密码泄露风险 + if action == "terminal_input": + result = "command_entered" + log_item = { + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), + "user": user, + "role": role, + "action": action, + "container_name": target_name, + "container_id": target_id, + "result": result, + "client_ip": client_ip + } + # 生产环境替换为mysql insert语句 + print(json.dumps(log_item, ensure_ascii=False)) + +# ===================== 工具函数3:HMAC签名鉴权,防Header伪造 ===================== +def verify_auth_sign(): + """ + 校验前端PHP生成的HMAC签名,防止伪造X-Role:admin + 请求Header要求: + X-User: 用户名 + X-Role: admin/user/guest + X-Time: 当前时间戳(秒) + X-Sign: hmac-sha256签名 + 返回 (user, role, client_ip, True/False) + """ + user = request.headers.get("X-User", "anonymous") + role = request.headers.get("X-Role", "guest") + ts_str = request.headers.get("X-Time", "0") + sign = request.headers.get("X-Sign", "") + client_ip = request.headers.get("X-Client-Ip", request.remote_addr) + try: + ts = int(ts_str) + except ValueError: + return user, role, client_ip, False + # 校验签名有效期 + now = int(time.time()) + if abs(now - ts) > SIGN_EXPIRE: + return user, role, client_ip, False + # 校验HMAC签名 + raw_data = f"{user}|{role}|{ts}".encode("utf-8") + calc_sign = hmac.new(SIGN_SECRET, raw_data, hashlib.sha256).hexdigest() + if calc_sign != sign: + return user, role, client_ip, False + return user, role, client_ip, True + +# ===================== 工具函数4:环境变量脱敏,屏蔽密码/密钥 ===================== +def safe_env_filter(env_list): + safe_env = [] + sensitive_keywords = ["PASS", "PASSWORD", "TOKEN", "SECRET", "KEY", "CREDENTIAL"] + for item in env_list: + equal_pos = item.find("=") + if equal_pos == -1: + safe_env.append(item) + continue + key = item[:equal_pos] + val = item[equal_pos+1:] + if any(word in key.upper() for word in sensitive_keywords): + safe_env.append(f"{key}=******") + else: + safe_env.append(item) + return safe_env + +# ===================== 全部HTTP业务接口 ===================== +# 1. 容器列表(端口预拼接字符串,前端直接渲染,无需formatPorts) +@app.route("/list") +def api_list(): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "list_container", "", "", "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + arr = [] + for c in cli.containers.list(all=True): + img_name = c.image.tags[0] if c.image.tags else "" + port_str_arr = [] + ports_raw = c.ports + for c_port, host_list in ports_raw.items(): + if not host_list: + continue + for host_item in host_list: + host_port = host_item.get("HostPort", "") + port_str_arr.append(f"{host_port}:{c_port.split('/')[0]}") + port_display = ",".join(port_str_arr) if port_str_arr else "-" + arr.append({ + "id": c.short_id, + "full_id": c.id, + "name": c.name, + "image": img_name, + "status": c.status, + "ports": port_display + }) + write_audit_log(user, role, "list_container", "", "", "success", ip) + return jsonify({"code":0,"msg":"success","list":arr}) + +# 2. 镜像列表 +@app.route("/image_list") +def api_image_list(): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "list_image", "", "", "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + arr = [] + for img in cli.images.list(): + if not img.tags: + continue + full_tag = img.tags[0] + repo, tag = full_tag.split(":", 1) + size_mb = round(img.attrs["Size"] / 1024 / 1024, 1) + arr.append({ + "repo": repo, + "tag": tag, + "id": img.short_id, + "full_id": img.id, + "size": f"{size_mb}MB", + "created": img.attrs["Created"] + }) + write_audit_log(user, role, "list_image", "", "", "success", ip) + return jsonify({"code":0,"msg":"success","list":arr}) + +# 3. 删除镜像 +@app.route("/remove_image/") +def api_remove_image(image_id): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "remove_image", "", image_id, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + if role != "admin": + write_audit_log(user, role, "remove_image", "", image_id, "denied", ip) + return jsonify({"code":403,"msg":"权限不足,仅管理员可删除镜像"}) + try: + img = cli.images.get(image_id) + cli.images.remove(image_id, force=True) + write_audit_log(user, role, "remove_image", img.tags[0] if img.tags else "", image_id, "success", ip) + return jsonify({"code":0,"msg":"镜像已强制删除"}) + except Exception as e: + write_audit_log(user, role, "remove_image", "", image_id, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e)}) + +# 4. 重启容器 +@app.route("/restart/") +def api_restart(cid): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "restart", "", cid, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + if role != "admin": + write_audit_log(user, role, "restart", "", cid, "denied", ip) + return jsonify({"code":403,"msg":"权限不足,仅管理员可操作容器"}) + try: + container = cli.containers.get(cid) + container.restart() + write_audit_log(user, role, "restart", container.name, cid, "success", ip) + return jsonify({"code":0,"msg":"容器已重启"}) + except Exception as e: + write_audit_log(user, role, "restart", "", cid, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e)}) + +# 5. 停止容器 +@app.route("/stop/") +def api_stop(cid): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "stop", "", cid, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + if role != "admin": + write_audit_log(user, role, "stop", "", cid, "denied", ip) + return jsonify({"code":403,"msg":"权限不足,仅管理员可操作容器"}) + try: + container = cli.containers.get(cid) + container.stop() + write_audit_log(user, role, "stop", container.name, cid, "success", ip) + return jsonify({"code":0,"msg":"容器已停止"}) + except Exception as e: + write_audit_log(user, role, "stop", "", cid, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e)}) + +# 6. 删除容器 +@app.route("/rm/") +def api_rm(cid): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "rm", "", cid, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + if role != "admin": + write_audit_log(user, role, "rm", "", cid, "denied", ip) + return jsonify({"code":403,"msg":"权限不足,仅管理员可删除容器"}) + try: + container = cli.containers.get(cid) + container.remove(force=True) + write_audit_log(user, role, "rm", container.name, cid, "success", ip) + return jsonify({"code":0,"msg":"容器已强制删除"}) + except Exception as e: + write_audit_log(user, role, "rm", "", cid, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e)}) + +# 7. 一次性拉取日志(兼容旧页面单次查看) +@app.route("/logs//") +def api_logs(cid, tail_lines): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "read_log", "", cid, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + try: + container = cli.containers.get(cid) + log_bytes = container.logs(tail=tail_lines) + log_str = log_bytes.decode("utf-8", errors="replace") + write_audit_log(user, role, "read_log", container.name, cid, "success", ip) + return jsonify({"code":0,"msg":"success","data":log_str}) + except Exception as e: + write_audit_log(user, role, "read_log", "", cid, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e),"data":""}) + +# 8. 容器网络详情 +@app.route("/network/") +def api_network(cid): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "read_network", "", cid, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + try: + container = cli.containers.get(cid) + info = container.attrs["NetworkSettings"]["Networks"] + write_audit_log(user, role, "read_network", container.name, cid, "success", ip) + return jsonify({"code":0,"msg":"success","data":json.dumps(info,indent=2)}) + except Exception as e: + write_audit_log(user, role, "read_network", "", cid, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e),"data":""}) + +# 9. 短连接单次执行命令(旧弹窗兼容,非实时终端) +@app.route("/exec/", methods=["POST"]) +def api_exec(cid): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "single_exec", "", cid, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + if role != "admin": + write_audit_log(user, role, "single_exec", "", cid, "denied", ip) + return jsonify({"code":403,"msg":"权限不足,仅管理员可执行命令"}) + try: + data = request.get_json() + cmd = data.get("cmd", "") + if not cmd: + return jsonify({"code":400,"msg":"命令不能为空","data":""}) + cmd_list = shlex.split(cmd) + container = cli.containers.get(cid) + exec_obj = container.exec_run(cmd_list, stdout=True, stderr=True) + output = exec_obj.output.decode("utf-8", errors="replace") + write_audit_log(user, role, "single_exec", container.name, cid, "command_executed", ip) + return jsonify({"code":0,"msg":"success","data":output}) + except Exception as e: + write_audit_log(user, role, "single_exec", "", cid, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e),"data":""}) + +# 10. 创建容器(端口标准格式 + 前端可配置CPU/内存) +@app.route("/create", methods=["POST"]) +def api_create(): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "create_container", "", "", "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + if role != "admin": + write_audit_log(user, role, "create_container", "", "", "denied", ip) + return jsonify({"code":403,"msg":"权限不足,仅管理员可创建容器"}) + try: + data = request.get_json() + name = data.get("name", "") + image = data.get("image") + cmd = data.get("cmd") + ports_input = data.get("ports", {}) + # 修复:docker标准端口字典格式 {内部端口:宿主机端口} + port_bind = {} + for inner_port, host_port in ports_input.items(): + port_bind[f"{inner_port}/tcp"] = int(host_port) + # 前端可配置资源限制,默认1核512M + cpu_core = int(data.get("cpu", 1)) + memory_limit = data.get("memory", "512m") + nano_cpus = cpu_core * 1000000000 + container = cli.containers.run( + image=image, + name=name if name else None, + command=cmd if cmd else None, + ports=port_bind, + detach=True, + restart_policy={"Name": "always"}, + mem_limit=memory_limit, + nano_cpus=nano_cpus + ) + write_audit_log(user, role, "create_container", container.name, container.id, "success", ip) + return jsonify({"code":0,"msg":f"容器创建成功,ID:{container.short_id}"}) + except Exception as e: + write_audit_log(user, role, "create_container", "", "", f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e)}) + +# 11. 登录私有镜像仓库 +@app.route("/bind_repo", methods=["POST"]) +def api_bind_repo(): + global registry_config + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "repo_login", "", "", "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + if role != "admin": + write_audit_log(user, role, "repo_login", "", "", "denied", ip) + return jsonify({"code":403,"msg":"权限不足,仅管理员可登录镜像仓库"}) + try: + data = request.get_json() + repo_name = data.get("repo_name","") + repo_addr = data.get("repo_addr") + repo_user = data.get("repo_user","") + repo_pwd = data.get("repo_pwd","") + api_cli.login(username=repo_user, password=repo_pwd, registry=repo_addr) + registry_config["repo_name"] = repo_name + registry_config["repo_addr"] = repo_addr + registry_config["repo_user"] = repo_user + registry_config["repo_pwd"] = repo_pwd + write_audit_log(user, role, "repo_login", repo_addr, "", "success", ip) + return jsonify({"code":0,"msg":"镜像仓库登录绑定成功"}) + except Exception as e: + write_audit_log(user, role, "repo_login", repo_addr, "", f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e)}) + +# 修复:stats接口online_cpus兜底兼容,无KeyError +@app.route("/stats/") +def api_stats(cid): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "read_stats", "", cid, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + try: + container = cli.containers.get(cid) + raw = container.stats(stream=False) + # CPU兼容兜底,不存在online_cpus自动计算 + cpu_stats = raw["cpu_stats"] + online_cpus = cpu_stats.get("online_cpus", len(cpu_stats["cpu_usage"].get("percpu_usage", [])) or 1) + cpu_delta = cpu_stats["cpu_usage"]["total_usage"] - raw["precpu_stats"]["cpu_usage"]["total_usage"] + system_delta = cpu_stats["system_cpu_usage"] - raw["precpu_stats"]["system_cpu_usage"] + cpu_percent = 0.0 + if system_delta > 0 and cpu_delta > 0: + cpu_percent = round((cpu_delta / system_delta) * online_cpus * 100, 1) + # 内存计算 + mem_usage = raw["memory_stats"]["usage"] / 1024 / 1024 + mem_limit = raw["memory_stats"]["limit"] / 1024 / 1024 + mem_percent = round((mem_usage / mem_limit) * 100, 1) if mem_limit > 0 else 0 + # 网络流量 + net_rx = 0 + net_tx = 0 + for net in raw["networks"].values(): + net_rx += net["rx_bytes"] / 1024 / 1024 + net_tx += net["tx_bytes"] / 1024 / 1024 + res = { + "cpu": cpu_percent, + "mem_mb": round(mem_usage, 1), + "mem_limit_mb": round(mem_limit, 1), + "mem_percent": mem_percent, + "net_rx_mb": round(net_rx, 1), + "net_tx_mb": round(net_tx, 1) + } + write_audit_log(user, role, "read_stats", container.name, cid, "success", ip) + return jsonify({"code":0,"msg":"success","data":res}) + except Exception as e: + write_audit_log(user, role, "read_stats", "", cid, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e),"data":{}}) + +# 修复:detail接口ENV脱敏,多网卡IP兼容 +@app.route("/detail/") +def api_detail(cid): + user, role, ip, auth_ok = verify_auth_sign() + if not auth_ok: + write_audit_log(user, role, "read_detail", "", cid, "auth_failed", ip) + return jsonify({"code":403,"msg":"身份校验失败,非法访问"}) + try: + container = cli.containers.get(cid) + attrs = container.attrs + nets = attrs["NetworkSettings"]["Networks"] + ip_map = {} + for net_name, net_info in nets.items(): + ip_map[net_name] = net_info.get("IPAddress", "") + # 敏感环境变量脱敏 + raw_env = attrs["Config"].get("Env", []) + safe_env = safe_env_filter(raw_env) + res = { + "network_ips": ip_map, + "mounts": attrs["Mounts"], + "env": safe_env, + "created": attrs["Created"], + "entrypoint": attrs["Config"]["Entrypoint"], + "cmd": attrs["Config"]["Cmd"], + "restart_policy": attrs["HostConfig"]["RestartPolicy"] + } + write_audit_log(user, role, "read_detail", container.name, cid, "success", ip) + return jsonify({"code":0,"msg":"success","data":res}) + except Exception as e: + write_audit_log(user, role, "read_detail", "", cid, f"fail:{str(e)}", ip) + return jsonify({"code":500,"msg":str(e),"data":{}}) + +# 根路由 +@app.route("/") +def index(): + return jsonify({"code":400,"msg":"无效操作","list":[],"data":""}) + +# ===================== WebSocket 交互式TTY实时终端 ===================== +def stream_terminal_output(sid, sock): + """后台异步读取docker终端输出,兼容两种socket接收方式""" + try: + while True: + chunk = docker_recv(sock, 1024) + if not chunk: + break + text = chunk.decode("utf-8", errors="replace") + socketio.emit("terminal_output", {"data": text}, room=sid) + except Exception: + pass + # 流结束自动清理会话 + if sid in session_exec_map: + try: + session_exec_map[sid]["socket"].close() + except Exception: + pass + del session_exec_map[sid] + +@socketio.on("terminal_start") +def handle_terminal_start(json_data): + sid = request.sid + user, role, client_ip, auth_ok = verify_auth_sign() + if not auth_ok: + socketio.emit("terminal_output", {"data": "Permission denied: 身份校验失败\n"}, room=sid) + write_audit_log(user, role, "terminal_start", "", json_data.get("cid"), "auth_failed", client_ip) + return + # 权限拦截:非管理员禁止打开交互式终端 + if role != "admin": + socketio.emit("terminal_output", {"data": "Permission denied: 仅管理员可访问交互式终端\n"}, room=sid) + write_audit_log(user, role, "terminal_start", "", json_data.get("cid"), "denied", client_ip) + return + + cid = json_data.get("cid") + if not cid: + socketio.emit("terminal_output", {"data": "错误:容器ID不能为空\n"}, room=sid) + write_audit_log(user, role, "terminal_start", "", "", "empty_cid", client_ip) + return + + # 自动探测容器可用shell,兼容全版本docker-py(exec_create返回dict取Id) + target_shell = None + for shell in SHELL_CANDIDATES: + try: + ret = api_cli.exec_create( + container=cid, + cmd=[shell, "-c", "echo ok"] + ) + test_exec_id = ret["Id"] + test_stream = api_cli.exec_start(test_exec_id) + test_stream.read() + target_shell = shell + break + except Exception: + continue + if not target_shell: + socketio.emit("terminal_output", {"data": "错误:容器内无可用shell(bash/sh/ash)\n"}, room=sid) + write_audit_log(user, role, "terminal_start", "", cid, "no_shell", client_ip) + return + + try: + # 创建交互式TTY会话,统一提取Id兼容所有SDK版本 + exec_ret = api_cli.exec_create( + container=cid, + cmd=target_shell, + stdin=True, + stdout=True, + stderr=True, + tty=True + ) + exec_id = exec_ret["Id"] + sock = api_cli.exec_start(exec_id=exec_id, tty=True, socket=True, detach=False) + session_exec_map[sid] = { + "cid": cid, + "exec_id": exec_id, + "socket": sock + } + socketio.start_background_task(stream_terminal_output, sid, sock) + container_name = cli.containers.get(cid).name + write_audit_log(user, role, "terminal_start", container_name, cid, "success", client_ip) + except Exception as e: + socketio.emit("terminal_output", {"data": f"启动终端失败:{str(e)}\n"}, room=sid) + write_audit_log(user, role, "terminal_start", "", cid, f"fail:{str(e)}", client_ip) + +@socketio.on("terminal_input") +def handle_terminal_input(json_data): + """前端输入字符写入容器标准输入,审计脱敏不记录明文""" + sid = request.sid + user, role, client_ip, auth_ok = verify_auth_sign() + if not auth_ok: + socketio.emit("terminal_output", {"data": "身份校验失败\n"}, room=sid) + return + if sid not in session_exec_map: + socketio.emit("terminal_output", {"data": "终端会话已断开,请重新打开\n"}, room=sid) + return + input_text = json_data.get("data", "") + sock = session_exec_map[sid]["socket"] + sock.send(input_text.encode("utf-8")) + cid = session_exec_map[sid]["cid"] + # 审计仅标记事件,不再记录明文命令,规避密码泄露 + write_audit_log(user, role, "terminal_input", "", cid, "command_entered", client_ip) + +@socketio.on("terminal_resize") +def handle_terminal_resize(json_data): + """前端xterm窗口大小自适应""" + sid = request.sid + if sid not in session_exec_map: + return + exec_id = session_exec_map[sid]["exec_id"] + height = int(json_data.get("height", 24)) + width = int(json_data.get("width", 80)) + try: + api_cli.exec_resize(exec_id=exec_id, height=height, width=width) + except Exception: + pass + +# 新增主动关闭终端事件(解决切换容器残留TTY) +@socketio.on("terminal_close") +def handle_terminal_close(): + sid = request.sid + user, role, client_ip, auth_ok = verify_auth_sign() + if sid in session_exec_map: + cid = session_exec_map[sid]["cid"] + try: + session_exec_map[sid]["socket"].close() + except Exception: + pass + del session_exec_map[sid] + write_audit_log(user, role, "terminal_close", "", cid, "manual_close", client_ip) + +@socketio.on("disconnect") +def handle_disconnect(): + """前端断开连接,释放docker socket、清理会话,防止句柄泄漏""" + sid = request.sid + user, role, client_ip, auth_ok = verify_auth_sign() + # 清理TTY终端 + if sid in session_exec_map: + cid = session_exec_map[sid]["cid"] + try: + session_exec_map[sid]["socket"].close() + except Exception: + pass + del session_exec_map[sid] + write_audit_log(user, role, "terminal_close", "", cid, "disconnect", client_ip) + # 清理实时日志标记,终止日志循环 + if sid in log_task_map: + del log_task_map[sid] + +# ===================== WebSocket 实时流式日志 docker logs -f(生成器主动释放) ===================== +def stream_follow_log(sid, cid): + generator = None + try: + container = cli.containers.get(cid) + generator = container.logs(stream=True, follow=True) + log_task_map[sid] = {"cid": cid, "running": True} + for line in generator: + task_info = log_task_map.get(sid, {}) + if not task_info.get("running"): + break + txt = line.decode("utf-8", errors="replace") + socketio.emit("log_output", {"data": txt}, room=sid) + except Exception: + pass + finally: + # 主动关闭日志生成器,释放docker daemon长连接 + if generator is not None: + generator.close() + if sid in log_task_map: + del log_task_map[sid] + +@socketio.on("log_start") +def handle_log_start(json_data): + sid = request.sid + user, role, client_ip, auth_ok = verify_auth_sign() + if not auth_ok: + socketio.emit("log_output", {"data": "身份校验失败"}, room=sid) + write_audit_log(user, role, "log_start", "", "", "auth_failed", client_ip) + return + cid = json_data.get("cid") + if not cid: + socketio.emit("log_output", {"data": "容器ID不能为空"}, room=sid) + write_audit_log(user, role, "log_start", "", "", "empty_cid", client_ip) + return + if role != "admin": + socketio.emit("log_output", {"data": "权限不足,仅管理员可查看实时日志"}, room=sid) + write_audit_log(user, role, "log_start", "", cid, "denied", client_ip) + return + socketio.start_background_task(stream_follow_log, sid, cid) + container_name = cli.containers.get(cid).name + write_audit_log(user, role, "log_start", container_name, cid, "success", client_ip) + +@socketio.on("log_stop") +def handle_log_stop(): + sid = request.sid + user, role, client_ip, auth_ok = verify_auth_sign() + if sid in log_task_map: + log_task_map[sid]["running"] = False + cid = log_task_map[sid]["cid"] + write_audit_log(user, role, "log_stop", "", cid, "manual_close", client_ip) + +# 启动入口(eventlet异步,必须socketio.run) +if __name__ == "__main__": + socketio.run(app, host="0.0.0.0", port=8090, debug=False, allow_unsafe_werkzeug=True) diff --git a/alert_dashboard/www/api/docker_api.sh b/alert_dashboard/www/api/docker_api.sh new file mode 100644 index 0000000..f6b244a --- /dev/null +++ b/alert_dashboard/www/api/docker_api.sh @@ -0,0 +1,9 @@ +#!/bin/bash +while true; do +nc -l 127.0.0.1 9999 <400,"msg"=>"日期格式错误,请使用 Y-m-d,例如 2026-07-01"], JSON_UNESCAPED_UNICODE); - exit; + + $singleDate = $_GET['date'] ?? ''; + $startDate = $_GET['start'] ?? ''; + $endDate = $_GET['end'] ?? ''; + $where = ""; + $fileName = ""; + + if (!empty($startDate) && !empty($endDate)) { + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) { + header("Content-Type:application/json;charset=utf-8"); + echo json_encode(["code"=>400,"msg"=>"日期格式错误"], JSON_UNESCAPED_UNICODE); + exit; + } + if ($startDate > $endDate) { + header("Content-Type:application/json;charset=utf-8"); + echo json_encode(["code"=>400,"msg"=>"开始日期不能大于结束日期"], JSON_UNESCAPED_UNICODE); + exit; + } + $where = "receive_time >= '$startDate 00:00:00' AND receive_time <= '$endDate 23:59:59'"; + $fileName = "告警报表_{$startDate}_至_{$endDate}.csv"; + } elseif (!empty($singleDate)) { + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $singleDate)) { + header("Content-Type:application/json;charset=utf-8"); + echo json_encode(["code"=>400,"msg"=>"日期格式错误"], JSON_UNESCAPED_UNICODE); + exit; + } + $where = "DATE(receive_time) = '$singleDate'"; + $fileName = "告警报表_{$singleDate}.csv"; + } else { + $today = date("Y-m-d"); + $where = "DATE(receive_time) = '$today'"; + $fileName = "告警报表_今日_{$today}.csv"; } + // 调试:打印where条件,访问接口先看这个 + // echo "WHERE条件:".$where;exit; + $sql = "SELECT alert_type,alert_name,instance,severity,starts_at,receive_time - FROM alert_log WHERE DATE(receive_time) = '$targetDate' ORDER BY receive_time DESC"; + FROM alert_log WHERE {$where} ORDER BY receive_time DESC"; $res = mysqli_query($conn, $sql); ob_clean(); header("Content-Type: text/csv; charset=utf-8"); - header("Content-Disposition: attachment; filename=告警报表_" . $targetDate . ".csv"); + header("Content-Disposition: attachment; filename={$fileName}"); echo "\xEF\xBB\xBF"; $header = ["告警状态", "告警名称", "实例", "级别", "故障开始时间", "接收时间"]; echo implode(",", $header) . "\r\n"; @@ -69,7 +99,6 @@ if ($act === "export_csv") { mysqli_close($conn); exit; } - // 所有JSON接口统一返回头 header("Content-Type:application/json;charset=utf-8"); $conn = getDbConn(); diff --git a/alert_dashboard/www/api/panel.php b/alert_dashboard/www/api/panel.php index 5483cf1..1ba07ee 100644 --- a/alert_dashboard/www/api/panel.php +++ b/alert_dashboard/www/api/panel.php @@ -322,13 +322,25 @@ button.normal:hover{
-

9. 导出告警CSV报表(支持历史日期)

-
- +

9. 导出告警CSV报表

+ + +
+ - +
-
不选择日期默认导出今日告警;选择历史日期可下载过往报表
+
不选择日期默认导出今日告警;选择历史日期下载单日报表
+ + +
+ + + + + +
+
填写开始、结束日期,导出该时间段全部告警数据
@@ -397,8 +409,8 @@ function simpleReq(act){ }) } -// 导出CSV 支持日历选择日期 -function exportCsv(){ +// 单日导出 +function exportCsvSingle(){ const dateVal = document.getElementById("csv_date").value.trim(); let url = `${apiUrl}?act=export_csv`; if(dateVal){ @@ -406,6 +418,23 @@ function exportCsv(){ } window.open(url,"_blank"); } + +// 区间导出 +function exportCsvRange(){ + const startDate = document.getElementById("csv_start").value.trim(); + const endDate = document.getElementById("csv_end").value.trim(); + if(!startDate || !endDate){ + alert("请同时选择开始日期和结束日期"); + return; + } + if(startDate > endDate){ + alert("开始日期不能晚于结束日期"); + return; + } + let url = `${apiUrl}?act=export_csv`; + url += `&start=${encodeURIComponent(startDate)}&end=${encodeURIComponent(endDate)}`; + window.open(url,"_blank"); +} diff --git a/alert_dashboard/www/api/work_order_api.php b/alert_dashboard/www/api/work_order_api.php index 63e3a97..f71add0 100644 --- a/alert_dashboard/www/api/work_order_api.php +++ b/alert_dashboard/www/api/work_order_api.php @@ -9,7 +9,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { } session_start(); $expire = 1800; -if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) { +if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time']) > $expire) { echo json_encode(['code' => 401, 'msg' => '登录失效']); exit; } @@ -42,7 +42,8 @@ mysqli_set_charset($connConf, "utf8mb4"); // 获取当前用户角色权限 $isAdmin = 0; $permList = ""; -$roleSql = "SELECT r.is_admin, r.perm_list FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?"; +$roleId = 0; +$roleSql = "SELECT r.is_admin, r.perm_list, u.role_id FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?"; $stmtRole = mysqli_prepare($connWork, $roleSql); mysqli_stmt_bind_param($stmtRole, 's', $loginUid); mysqli_stmt_execute($stmtRole); @@ -51,12 +52,98 @@ $roleRow = $roleRes ? mysqli_fetch_assoc($roleRes) : []; if ($roleRow) { $isAdmin = intval($roleRow['is_admin']); $permList = $roleRow['perm_list']; + $roleId = intval($roleRow['role_id'] ?? 0); +} + +// 读取当前角色页面权限列表(用于分发工单权限判断) +$allowPages = []; +if ($roleId > 0) { + $permSql = "SELECT page_key FROM sys_role_permission WHERE role_id = ?"; + $stmtPerm = mysqli_prepare($connWork, $permSql); + mysqli_stmt_bind_param($stmtPerm, 'i', $roleId); + mysqli_stmt_execute($stmtPerm); + $pRes = mysqli_stmt_get_result($stmtPerm); + while ($p = mysqli_fetch_assoc($pRes)) { + $allowPages[] = $p['page_key']; + } } $action = $_REQUEST['action'] ?? ''; $post = json_decode(file_get_contents("php://input"), true) ?: []; -// ===================== 新增1:根据告警ID查询关联工单 ===================== +// ===================== 工单列表查询 ===================== +if ($action === "get_list") { + $page = intval($_GET['page'] ?? 1); + $size = intval($_GET['size'] ?? 20); + $offset = ($page - 1) * $size; + $status = trim($_GET['status'] ?? ''); + $keyword = trim($_GET['keyword'] ?? ''); + + $where = []; + $param = []; + $paramType = ''; + + // 全部用户查看全部工单,不再按uid过滤 + $whereSql = "1=1"; + + if ($status !== '') { + $where[] = "status = ?"; + $param[] = $status; + $paramType .= 'i'; + } + if ($keyword !== '') { + $where[] = "(title LIKE ? OR content LIKE ?)"; + $param[] = "%$keyword%"; + $param[] = "%$keyword%"; + $paramType .= 'ss'; + } + + if (!empty($where)) { + $whereSql = "WHERE " . implode(" AND ", $where); + } + + // 分页列表,LEFT JOIN 认领人姓名 + $listSql = "SELECT wo.*, uu.real_name claim_name +FROM work_order wo +LEFT JOIN sys_user uu ON wo.claim_uid = uu.uid +$whereSql ORDER BY create_time DESC LIMIT ?,?"; + $param[] = $offset; + $param[] = $size; + $paramType .= 'ii'; + + $stmtList = mysqli_prepare($connWork, $listSql); + mysqli_stmt_bind_param($stmtList, $paramType, ...$param); + mysqli_stmt_execute($stmtList); + $res = mysqli_stmt_get_result($stmtList); + $list = []; + while ($row = mysqli_fetch_assoc($res)) { + $list[] = $row; + } + + // 总数统计 + $countSql = "SELECT COUNT(id) total FROM work_order $whereSql"; + $stmtCount = mysqli_prepare($connWork, $countSql); + array_pop($param); + array_pop($param); + $paramTypeCount = rtrim($paramType, 'ii'); + if ($paramTypeCount) { + mysqli_stmt_bind_param($stmtCount, $paramTypeCount, ...$param); + } + mysqli_stmt_execute($stmtCount); + $countRow = mysqli_fetch_assoc(mysqli_stmt_get_result($stmtCount)); + $total = intval($countRow['total']); + + echo json_encode([ + 'code' => 0, + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'size' => $size + ], JSON_UNESCAPED_UNICODE); + exit; +} + +// ===================== 根据告警ID查询关联工单 ===================== if ($action === "get_workorder_by_alertid") { $alertId = trim($_GET['alert_id'] ?? ''); if (empty($alertId)) { @@ -77,9 +164,8 @@ if ($action === "get_workorder_by_alertid") { exit; } -// ===================== 新增2:自动同步告警状态,批量更新工单为已完成 ===================== +// ===================== 自动同步告警状态,批量更新工单为已完成 ===================== if ($action === "auto_sync_workorder_status") { - // 1. 查询所有已恢复的告警(alert_type=2),去重获取告警ID $resolveSql = "SELECT DISTINCT CONCAT(alert_name,'_',instance) as alert_id FROM alert_log WHERE alert_type=2"; $resolveRes = mysqli_query($connConf, $resolveSql); $resolveAlertIds = []; @@ -91,11 +177,9 @@ if ($action === "auto_sync_workorder_status") { exit; } - // 2. 拼接IN条件,查询未完成、关联该告警的工单 $inStr = implode("','", array_map(function($v) use ($connWork) { return mysqli_real_escape_string($connWork, $v); }, $resolveAlertIds)); - // status=1待处理 / status=2处理中,统一改为3已完成 $updateSql = "UPDATE work_order SET status=3,update_time=NOW() WHERE relate_alert_id IN ('$inStr') AND status IN (1,2)"; mysqli_query($connWork, $updateSql); $updateCnt = mysqli_affected_rows($connWork); @@ -122,7 +206,7 @@ if ($action === "get_firing_alert") { exit; } -// 2、新建工单(关联告警、多渠道通知 + 自动判断告警是否已恢复) +// 2、新建工单 if ($action === "create") { $title = trim($post['title'] ?? ''); $content = trim($post['content'] ?? ''); @@ -130,7 +214,6 @@ if ($action === "create") { $relateAlertId = trim($post['relate_alert_id'] ?? ''); $notifyUser = trim($post['notify_user'] ?? ''); $notifyChannel = trim($post['notify_channel'] ?? ''); - // 默认待处理1;若关联告警已恢复,直接置完成3 $status = 1; if (!$title || !$content) { @@ -138,7 +221,6 @@ if ($action === "create") { exit; } - // 关联了告警,判断该告警是否存在恢复记录 if (!empty($relateAlertId)) { $alertEsc = mysqli_real_escape_string($connConf, $relateAlertId); $checkResolveSql = "SELECT 1 FROM alert_log WHERE CONCAT(alert_name,'_',instance) = ? AND alert_type=2 LIMIT 1"; @@ -151,8 +233,8 @@ if ($action === "create") { } } - // 新增工单默认未读 is_read=0 - $sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,notify_user,notify_channel,status,is_read,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,0,NOW(),NOW())"; + // 新建工单默认无认领人 claim_uid='' + $sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,notify_user,notify_channel,status,is_read,claim_uid,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,?, '',NOW(),NOW())"; $stmt = mysqli_prepare($connWork, $sql); mysqli_stmt_bind_param($stmt, 'ssssssss', $title, $content, $loginUid, $assign, $relateAlertId, $notifyUser, $notifyChannel, $status); $ok = mysqli_stmt_execute($stmt); @@ -166,7 +248,37 @@ if ($action === "create") { exit; } -// 3、编辑工单 +// ========== 新增:工单认领接口 claim_work ========== +if ($action === "claim_work") { + $id = intval($post['id'] ?? 0); + $uid = trim($post['uid'] ?? ''); + if ($id <= 0 || empty($uid)) { + echo json_encode(["code" => 400, "msg" => "参数错误"]); + exit; + } + // 校验工单未被认领 + $chkSql = "SELECT id FROM work_order WHERE id=? AND (claim_uid IS NULL OR claim_uid = '')"; + $stmtChk = mysqli_prepare($connWork, $chkSql); + mysqli_stmt_bind_param($stmtChk, 'i', $id); + mysqli_stmt_execute($stmtChk); + $chkRes = mysqli_stmt_get_result($stmtChk); + if(mysqli_num_rows($chkRes) === 0){ + echo json_encode(["code" => 400, "msg" => "该工单已被他人认领,无法重复认领"]); + exit; + } + $updateSql = "UPDATE work_order SET claim_uid=?, claim_time=NOW() WHERE id=?"; + $stmt = mysqli_prepare($connWork, $updateSql); + mysqli_stmt_bind_param($stmt, 'si', $uid, $id); + $ok = mysqli_stmt_execute($stmt); + if($ok){ + echo json_encode(["code" => 0, "msg" => "工单认领成功,现在你可以编辑处理"]); + }else{ + echo json_encode(["code" => 500, "msg" => "认领失败:".mysqli_error($connWork)]); + } + exit; +} + +// 3、编辑工单(权限:管理员 或 当前用户是认领人) if ($action === "edit") { $id = intval($post['id'] ?? 0); $title = trim($post['title'] ?? ''); @@ -179,15 +291,15 @@ if ($action === "edit") { echo json_encode(['code' => 400, 'msg' => '参数不能为空']); exit; } - // 非管理员权限校验 + // 权限重写 if ($isAdmin !== 1) { - $chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)"; + $chkSql = "SELECT id FROM work_order WHERE id=? AND claim_uid=?"; $stmtChk = mysqli_prepare($connWork, $chkSql); - mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid); + mysqli_stmt_bind_param($stmtChk, 'is', $id, $loginUid); mysqli_stmt_execute($stmtChk); $chkRes = mysqli_stmt_get_result($stmtChk); if (mysqli_num_rows($chkRes) === 0) { - echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']); + echo json_encode(['code' => 403, 'msg' => '仅工单认领人可编辑']); exit; } } @@ -200,7 +312,7 @@ if ($action === "edit") { exit; } -// 4、修改工单状态 +// 4、修改工单状态(权限:管理员 或 当前用户是认领人) if ($action === "update_status") { $id = intval($post['id'] ?? 0); $status = intval($post['status'] ?? 1); @@ -209,17 +321,16 @@ if ($action === "update_status") { exit; } if ($isAdmin !== 1) { - $chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)"; + $chkSql = "SELECT id FROM work_order WHERE id=? AND claim_uid=?"; $stmtChk = mysqli_prepare($connWork, $chkSql); - mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid); + mysqli_stmt_bind_param($stmtChk, 'is', $id, $loginUid); mysqli_stmt_execute($stmtChk); $chkRes = mysqli_stmt_get_result($stmtChk); if (mysqli_num_rows($chkRes) === 0) { - echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']); + echo json_encode(['code' => 403, 'msg' => '仅工单认领人可修改状态']); exit; } } - // 修改状态自动标记已读 $sql = "UPDATE work_order SET status=?,is_read=1,update_time=NOW() WHERE id=?"; $stmt = mysqli_prepare($connWork, $sql); mysqli_stmt_bind_param($stmt, 'ii', $status, $id); @@ -228,15 +339,14 @@ if ($action === "update_status") { exit; } -// 5、管理员分发工单(前端统一action=assign,废弃assign_order) +// 5、管理员/拥有system_manage权限角色 分发工单 if ($action === "assign") { - if ($isAdmin !== 1) { - echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']); + if ($isAdmin !== 1 && !in_array("system_manage", $allowPages)) { + echo json_encode(['code' => 403, 'msg' => '无工单分发权限,请联系管理员']); exit; } $id = intval($post['id'] ?? 0); $assignUid = trim($post['assign_uid'] ?? ''); - // 修复BUG:仅按工单唯一ID更新,不再使用标题模糊匹配,选中哪一行只修改该行工单 $sql = "UPDATE work_order SET assign_uid=?,is_read=0,update_time=NOW() WHERE id=?"; $stmt = mysqli_prepare($connWork, $sql); mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id); @@ -267,7 +377,7 @@ if ($action === 'save_lang') { exit; } -// 新增:同步Prometheus实时告警,自动清理恢复告警(完全独立,无需index.php) +// 同步Prometheus实时告警,自动清理恢复告警 if ($action === "sync_prom_alerts") { $promUrl = "http://10.150.117.190:9090/api/v1/alerts"; $resp = @file_get_contents($promUrl); @@ -281,10 +391,8 @@ if ($action === "sync_prom_alerts") { exit; } - // 1. 先将表内所有告警标记为已恢复 mysqli_query($connConf,"UPDATE alert_firing SET status='resolved'"); - // 2. 遍历当前Prometheus活跃告警,写入/更新 $alerts = $promData['data']['alerts'] ?? []; foreach($alerts as $item){ $alertName = $item['labels']['alertname'] ?? "unknown"; @@ -297,7 +405,6 @@ if ($action === "sync_prom_alerts") { } $syncNum = count($alerts); - // 同步完成后自动执行工单状态更新 $syncWorkUrl = $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . "?action=auto_sync_workorder_status"; @file_get_contents($syncWorkUrl); @@ -311,19 +418,16 @@ if ($action === "sync_prom_alerts") { // 定时同步:告警恢复自动完工单 if($action === "sync_alert_auto_close"){ - // 1、拉取仪表盘实时未恢复告警 $alertApi = "./index.php?act=active_firing"; $raw = @file_get_contents($alertApi); $activeList = json_decode($raw, true) ?: []; $activeKeys = []; foreach($activeList as $item){ - $activeKeys[] = $item['alert_name'] . "|" . $item['instance']; + $activeKeys[] = $item['alert_name'] . "_" . $item['instance']; } - // 2、查询所有未完成、绑定告警的工单 $sql = "SELECT id, relate_alert_id FROM work_order WHERE status IN(1,2) AND relate_alert_id <> ''"; $res = mysqli_query($connWork, $sql); while($row = mysqli_fetch_assoc($res)){ - // 工单关联告警不在当前活跃列表=已恢复,自动设为完成 if(!in_array($row['relate_alert_id'], $activeKeys)){ $updateSql = "UPDATE work_order SET status=3, update_time=NOW() WHERE id = ".(int)$row['id']; mysqli_query($connWork, $updateSql); @@ -340,12 +444,10 @@ function sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUserStr, $ch $userArr = explode(",", $notifyUserStr); $channelArr = explode(",", $channelStr); - // 读取完整通知配置(邮箱+钉钉+企微) $cfgRes = mysqli_query($connConf, "SELECT * FROM sys_smtp_config WHERE id=1"); $notifyCfg = $cfgRes ? mysqli_fetch_assoc($cfgRes) : []; if(empty($notifyCfg)) return; - // 获取工单完整信息 $orderRes = mysqli_query($connWork, "SELECT * FROM work_order WHERE id=$orderId"); $order = $orderRes ? mysqli_fetch_assoc($orderRes) : []; $title = $order['title']; @@ -389,7 +491,6 @@ function sendMailNotify($smtp, $connWork, $uidList, $title, $content, $relateId,

关联告警ID:$relateId

标题:$title

$content
"; - // 此处接入PHPMailer发送逻辑 } // 钉钉机器人推送 diff --git a/alert_dashboard/www/api/work_order_api.php.9.0 b/alert_dashboard/www/api/work_order_api.php.9.0 new file mode 100644 index 0000000..e2dfc88 --- /dev/null +++ b/alert_dashboard/www/api/work_order_api.php.9.0 @@ -0,0 +1,515 @@ + 0, 'msg' => 'ok']); + exit; +} +session_start(); +$expire = 1800; +if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time']) > $expire) { + echo json_encode(['code' => 401, 'msg' => '登录失效']); + exit; +} +$loginUid = $_SESSION['user_id']; + +// 工单业务库 monitor +$dbWorkHost = '10.150.117.190'; +$dbWorkUser = 'root'; +$dbWorkPwd = 'hp93000'; +$dbWorkName = 'monitor'; +$connWork = mysqli_connect($dbWorkHost, $dbWorkUser, $dbWorkPwd, $dbWorkName); +if (!$connWork) { + echo json_encode(['code' => 500, 'msg' => '工单数据库连接失败:' . mysqli_connect_error()]); + exit; +} +mysqli_set_charset($connWork, 'utf8mb4'); + +// 配置库 alert_mail_stat(告警、通知配置统一此处) +$dbConfHost = "10.150.117.190"; +$dbConfUser = "root"; +$dbConfPwd = "hp93000"; +$dbConfName = "alert_mail_stat"; +$connConf = mysqli_connect($dbConfHost, $dbConfUser, $dbConfPwd, $dbConfName); +if(!$connConf){ + echo json_encode(['code' => 500, 'msg' => '配置库连接失败']); + exit; +} +mysqli_set_charset($connConf, "utf8mb4"); + +// 获取当前用户角色权限 +$isAdmin = 0; +$permList = ""; +$roleId = 0; +$roleSql = "SELECT r.is_admin, r.perm_list, u.role_id FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?"; +$stmtRole = mysqli_prepare($connWork, $roleSql); +mysqli_stmt_bind_param($stmtRole, 's', $loginUid); +mysqli_stmt_execute($stmtRole); +$roleRes = mysqli_stmt_get_result($stmtRole); +$roleRow = $roleRes ? mysqli_fetch_assoc($roleRes) : []; +if ($roleRow) { + $isAdmin = intval($roleRow['is_admin']); + $permList = $roleRow['perm_list']; + $roleId = intval($roleRow['role_id'] ?? 0); +} + +// 读取当前角色页面权限列表(用于分发工单权限判断) +$allowPages = []; +if ($roleId > 0) { + $permSql = "SELECT page_key FROM sys_role_permission WHERE role_id = ?"; + $stmtPerm = mysqli_prepare($connWork, $permSql); + mysqli_stmt_bind_param($stmtPerm, 'i', $roleId); + mysqli_stmt_execute($stmtPerm); + $pRes = mysqli_stmt_get_result($stmtPerm); + while ($p = mysqli_fetch_assoc($pRes)) { + $allowPages[] = $p['page_key']; + } +} + +$action = $_REQUEST['action'] ?? ''; +$post = json_decode(file_get_contents("php://input"), true) ?: []; + +// ===================== 工单列表查询(新增,解决数据可见权限隔离) ===================== +if ($action === "get_list") { + $page = intval($_GET['page'] ?? 1); + $size = intval($_GET['size'] ?? 20); + $offset = ($page - 1) * $size; + $status = trim($_GET['status'] ?? ''); + $keyword = trim($_GET['keyword'] ?? ''); + + $where = []; + $param = []; + $paramType = ''; + + // 普通用户:仅能查看自己创建 或 分配给自己的工单;管理员无限制查看全部 + if ($isAdmin !== 1) { + $where[] = "(create_uid = ? OR assign_uid = ?)"; + $param[] = $loginUid; + $param[] = $loginUid; + $paramType .= 'ss'; + } + + if ($status !== '') { + $where[] = "status = ?"; + $param[] = $status; + $paramType .= 'i'; + } + if ($keyword !== '') { + $where[] = "(title LIKE ? OR content LIKE ?)"; + $param[] = "%$keyword%"; + $param[] = "%$keyword%"; + $paramType .= 'ss'; + } + + $whereSql = $where ? "WHERE " . implode(" AND ", $where) : ""; + + // 分页列表 + $listSql = "SELECT * FROM work_order $whereSql ORDER BY create_time DESC LIMIT ?,?"; + $param[] = $offset; + $param[] = $size; + $paramType .= 'ii'; + + $stmtList = mysqli_prepare($connWork, $listSql); + mysqli_stmt_bind_param($stmtList, $paramType, ...$param); + mysqli_stmt_execute($stmtList); + $res = mysqli_stmt_get_result($stmtList); + $list = []; + while ($row = mysqli_fetch_assoc($res)) { + $list[] = $row; + } + + // 总数统计 + $countSql = "SELECT COUNT(id) total FROM work_order $whereSql"; + $stmtCount = mysqli_prepare($connWork, $countSql); + array_pop($param); + array_pop($param); + $paramTypeCount = rtrim($paramType, 'ii'); + if ($paramTypeCount) { + mysqli_stmt_bind_param($stmtCount, $paramTypeCount, ...$param); + } + mysqli_stmt_execute($stmtCount); + $countRow = mysqli_fetch_assoc(mysqli_stmt_get_result($stmtCount)); + $total = intval($countRow['total']); + + echo json_encode([ + 'code' => 0, + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'size' => $size + ], JSON_UNESCAPED_UNICODE); + exit; +} + +// ===================== 根据告警ID查询关联工单 ===================== +if ($action === "get_workorder_by_alertid") { + $alertId = trim($_GET['alert_id'] ?? ''); + if (empty($alertId)) { + echo json_encode(['code' => 400, 'msg' => '缺少告警ID参数']); + exit; + } + $alertEsc = mysqli_real_escape_string($connWork, $alertId); + $sql = "SELECT id,title,status,assign_uid,create_time FROM work_order WHERE relate_alert_id = ? ORDER BY create_time DESC"; + $stmt = mysqli_prepare($connWork, $sql); + mysqli_stmt_bind_param($stmt, 's', $alertEsc); + mysqli_stmt_execute($stmt); + $res = mysqli_stmt_get_result($stmt); + $list = []; + while ($row = mysqli_fetch_assoc($res)) { + $list[] = $row; + } + echo json_encode(['code' => 0, 'list' => $list]); + exit; +} + +// ===================== 自动同步告警状态,批量更新工单为已完成 ===================== +if ($action === "auto_sync_workorder_status") { + // 1. 查询所有已恢复的告警(alert_type=2),去重获取告警ID + $resolveSql = "SELECT DISTINCT CONCAT(alert_name,'_',instance) as alert_id FROM alert_log WHERE alert_type=2"; + $resolveRes = mysqli_query($connConf, $resolveSql); + $resolveAlertIds = []; + while ($row = mysqli_fetch_assoc($resolveRes)) { + $resolveAlertIds[] = $row['alert_id']; + } + if (empty($resolveAlertIds)) { + echo json_encode(['code' => 0, 'msg' => '暂无已恢复告警,无需更新工单', 'update_count' => 0]); + exit; + } + + // 2. 拼接IN条件,查询未完成、关联该告警的工单 + $inStr = implode("','", array_map(function($v) use ($connWork) { + return mysqli_real_escape_string($connWork, $v); + }, $resolveAlertIds)); + // status=1待处理 / status=2处理中,统一改为3已完成 + $updateSql = "UPDATE work_order SET status=3,update_time=NOW() WHERE relate_alert_id IN ('$inStr') AND status IN (1,2)"; + mysqli_query($connWork, $updateSql); + $updateCnt = mysqli_affected_rows($connWork); + + echo json_encode([ + 'code' => 0, + 'msg' => '工单状态自动同步完成,已恢复告警绑定工单全部置为已完成', + 'update_count' => $updateCnt + ], JSON_UNESCAPED_UNICODE); + exit; +} + +// 1、获取实时活跃告警(alert_mail_stat.alert_firing 数据源) +if ($action === "get_firing_alert") { + $alertSql = "SELECT alert_id,alert_name,instance FROM alert_firing WHERE status='firing' ORDER BY receive_time DESC LIMIT 100"; + $res = mysqli_query($connConf, $alertSql); + $list = []; + if($res instanceof mysqli_result){ + while ($row = mysqli_fetch_assoc($res)) { + $list[] = $row; + } + } + echo json_encode(['code' => 0, 'list' => $list]); + exit; +} + +// 2、新建工单(关联告警、多渠道通知 + 自动判断告警是否已恢复) +if ($action === "create") { + $title = trim($post['title'] ?? ''); + $content = trim($post['content'] ?? ''); + $assign = trim($post['assign_uid'] ?? ''); + $relateAlertId = trim($post['relate_alert_id'] ?? ''); + $notifyUser = trim($post['notify_user'] ?? ''); + $notifyChannel = trim($post['notify_channel'] ?? ''); + // 默认待处理1;若关联告警已恢复,直接置完成3 + $status = 1; + + if (!$title || !$content) { + echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']); + exit; + } + + // 关联了告警,判断该告警是否存在恢复记录 + if (!empty($relateAlertId)) { + $alertEsc = mysqli_real_escape_string($connConf, $relateAlertId); + $checkResolveSql = "SELECT 1 FROM alert_log WHERE CONCAT(alert_name,'_',instance) = ? AND alert_type=2 LIMIT 1"; + $stmtCheck = mysqli_prepare($connConf, $checkResolveSql); + mysqli_stmt_bind_param($stmtCheck, 's', $alertEsc); + mysqli_stmt_execute($stmtCheck); + $checkRes = mysqli_stmt_get_result($stmtCheck); + if (mysqli_num_rows($checkRes) > 0) { + $status = 3; + } + } + + // 新增工单默认未读 is_read=0 + $sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,notify_user,notify_channel,status,is_read,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,0,NOW(),NOW())"; + $stmt = mysqli_prepare($connWork, $sql); + mysqli_stmt_bind_param($stmt, 'ssssssss', $title, $content, $loginUid, $assign, $relateAlertId, $notifyUser, $notifyChannel, $status); + $ok = mysqli_stmt_execute($stmt); + if (!$ok) { + echo json_encode(['code' => 500, 'msg' => '创建失败:' . mysqli_error($connWork)]); + exit; + } + $orderId = mysqli_insert_id($connWork); + sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUser, $notifyChannel); + echo json_encode(['code' => 0, 'msg' => '工单提交成功,通知已下发', 'init_status' => $status]); + exit; +} + +// 3、编辑工单 +if ($action === "edit") { + $id = intval($post['id'] ?? 0); + $title = trim($post['title'] ?? ''); + $content = trim($post['content'] ?? ''); + $assign = trim($post['assign_uid'] ?? ''); + $relateAlertId = trim($post['relate_alert_id'] ?? ''); + $notifyUser = trim($post['notify_user'] ?? ''); + $notifyChannel = trim($post['notify_channel'] ?? ''); + if ($id <= 0 || empty($title) || empty($content)) { + echo json_encode(['code' => 400, 'msg' => '参数不能为空']); + exit; + } + // 非管理员权限校验 + if ($isAdmin !== 1) { + $chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)"; + $stmtChk = mysqli_prepare($connWork, $chkSql); + mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid); + mysqli_stmt_execute($stmtChk); + $chkRes = mysqli_stmt_get_result($stmtChk); + if (mysqli_num_rows($chkRes) === 0) { + echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']); + exit; + } + } + $sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,relate_alert_id=?,notify_user=?,notify_channel=?,update_time=NOW() WHERE id=?"; + $stmt = mysqli_prepare($connWork, $sql); + mysqli_stmt_bind_param($stmt, 'ssssssi', $title, $content, $assign, $relateAlertId, $notifyUser, $notifyChannel, $id); + mysqli_stmt_execute($stmt); + sendWorkOrderNotify($connConf, $connWork, $id, $notifyUser, $notifyChannel); + echo json_encode(['code' => 0, 'msg' => '工单保存成功,通知已更新下发']); + exit; +} + +// 4、修改工单状态 +if ($action === "update_status") { + $id = intval($post['id'] ?? 0); + $status = intval($post['status'] ?? 1); + if ($id <= 0) { + echo json_encode(['code' => 400, 'msg' => '工单ID错误']); + exit; + } + if ($isAdmin !== 1) { + $chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)"; + $stmtChk = mysqli_prepare($connWork, $chkSql); + mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid); + mysqli_stmt_execute($stmtChk); + $chkRes = mysqli_stmt_get_result($stmtChk); + if (mysqli_num_rows($chkRes) === 0) { + echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']); + exit; + } + } + // 修改状态自动标记已读 + $sql = "UPDATE work_order SET status=?,is_read=1,update_time=NOW() WHERE id=?"; + $stmt = mysqli_prepare($connWork, $sql); + mysqli_stmt_bind_param($stmt, 'ii', $status, $id); + mysqli_stmt_execute($stmt); + echo json_encode(['code' => 0, 'msg' => '状态更新成功']); + exit; +} + +// 5、管理员/拥有system_manage权限角色 分发工单 +if ($action === "assign") { + // 修复:超级管理员 或 拥有system_manage页面权限均可分发工单 + if ($isAdmin !== 1 && !in_array("system_manage", $allowPages)) { + echo json_encode(['code' => 403, 'msg' => '无工单分发权限,请联系管理员']); + exit; + } + $id = intval($post['id'] ?? 0); + $assignUid = trim($post['assign_uid'] ?? ''); + // 修复BUG:仅按工单唯一ID更新,不再使用标题模糊匹配,选中哪一行只修改该行工单 + $sql = "UPDATE work_order SET assign_uid=?,is_read=0,update_time=NOW() WHERE id=?"; + $stmt = mysqli_prepare($connWork, $sql); + mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id); + mysqli_stmt_execute($stmt); + echo json_encode(['code' => 0, 'msg' => '工单分发成功']); + exit; +} + +// 一键标记当前用户全部待处理工单为已读 +if ($action === 'mark_all_read') { + $uid = $post['uid'] ?? ''; + $sql = "UPDATE work_order SET is_read=1 WHERE assign_uid=? AND status=1"; + $stmt = mysqli_prepare($connWork, $sql); + mysqli_stmt_bind_param($stmt, "s", $uid); + mysqli_stmt_execute($stmt); + echo json_encode(['code' => 0, 'msg' => '已全部标记为已读']); + exit; +} + +// 语言切换 +if ($action === 'save_lang') { + $lang = $post['lang'] ?? 'zh'; + $sql = "UPDATE sys_config SET v=? WHERE k='lang'"; + $stmt = mysqli_prepare($connConf, $sql); + mysqli_stmt_bind_param($stmt, "s", $lang); + mysqli_stmt_execute($stmt); + echo json_encode(['code' => 0, 'msg' => '成功']); + exit; +} + +// 同步Prometheus实时告警,自动清理恢复告警 +if ($action === "sync_prom_alerts") { + $promUrl = "http://10.150.117.190:9090/api/v1/alerts"; + $resp = @file_get_contents($promUrl); + if($resp === false){ + echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus接口"],JSON_UNESCAPED_UNICODE); + exit; + } + $promData = json_decode($resp,true); + if(!isset($promData['data']['alerts'])){ + echo json_encode(["code"=>500,"msg"=>"Prometheus返回数据异常"],JSON_UNESCAPED_UNICODE); + exit; + } + + // 1. 先将表内所有告警标记为已恢复 + mysqli_query($connConf,"UPDATE alert_firing SET status='resolved'"); + + // 2. 遍历当前Prometheus活跃告警,写入/更新 + $alerts = $promData['data']['alerts'] ?? []; + foreach($alerts as $item){ + $alertName = $item['labels']['alertname'] ?? "unknown"; + $instance = $item['labels']['instance'] ?? "unknown"; + $alertId = $alertName."_".$instance; + $sql = "INSERT INTO alert_firing(alert_id,alert_name,instance,status,receive_time) VALUES (?,?,?,'firing',NOW()) ON DUPLICATE KEY UPDATE status='firing',receive_time=NOW()"; + $stmt = mysqli_prepare($connConf,$sql); + mysqli_stmt_bind_param($stmt,"sss",$alertId,$alertName,$instance); + mysqli_stmt_execute($stmt); + } + $syncNum = count($alerts); + + // 同步完成后自动执行工单状态更新 + $syncWorkUrl = $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . "?action=auto_sync_workorder_status"; + @file_get_contents($syncWorkUrl); + + echo json_encode([ + "code"=>0, + "msg"=>"Prometheus告警同步完成,已自动同步工单状态", + "active_alert_total"=>$syncNum + ],JSON_UNESCAPED_UNICODE); + exit; +} + +// 定时同步:告警恢复自动完工单 +if($action === "sync_alert_auto_close"){ + // 1、拉取仪表盘实时未恢复告警 + $alertApi = "./index.php?act=active_firing"; + $raw = @file_get_contents($alertApi); + $activeList = json_decode($raw, true) ?: []; + $activeKeys = []; + foreach($activeList as $item){ + $activeKeys[] = $item['alert_name'] . "|" . $item['instance']; + } + // 2、查询所有未完成、绑定告警的工单 + $sql = "SELECT id, relate_alert_id FROM work_order WHERE status IN(1,2) AND relate_alert_id <> ''"; + $res = mysqli_query($connWork, $sql); + while($row = mysqli_fetch_assoc($res)){ + // 工单关联告警不在当前活跃列表=已恢复,自动设为完成 + if(!in_array($row['relate_alert_id'], $activeKeys)){ + $updateSql = "UPDATE work_order SET status=3, update_time=NOW() WHERE id = ".(int)$row['id']; + mysqli_query($connWork, $updateSql); + } + } + echo json_encode(['code'=>0, 'msg'=>'同步完成']); + exit; +} + +// ==================== 全局通知统一函数 ==================== +function sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUserStr, $channelStr) +{ + if (empty($notifyUserStr) || empty($channelStr)) return; + $userArr = explode(",", $notifyUserStr); + $channelArr = explode(",", $channelStr); + + // 读取完整通知配置(邮箱+钉钉+企微) + $cfgRes = mysqli_query($connConf, "SELECT * FROM sys_smtp_config WHERE id=1"); + $notifyCfg = $cfgRes ? mysqli_fetch_assoc($cfgRes) : []; + if(empty($notifyCfg)) return; + + // 获取工单完整信息 + $orderRes = mysqli_query($connWork, "SELECT * FROM work_order WHERE id=$orderId"); + $order = $orderRes ? mysqli_fetch_assoc($orderRes) : []; + $title = $order['title']; + $content = $order['content']; + $relateId = $order['relate_alert_id'] ?: "无"; + + $msgText = "【运维工单通知】\n工单ID:$orderId\n关联告警:$relateId\n标题:$title\n详情:$content"; + + foreach ($channelArr as $ch) { + $ch = trim($ch); + switch ($ch) { + case "mail": + sendMailNotify($notifyCfg, $connWork, $userArr, $title, $content, $relateId, $orderId); + break; + case "dingtalk": + sendDingNotify($notifyCfg, $msgText); + break; + case "wecom": + sendWecomNotify($notifyCfg, $msgText); + break; + } + } +} + +// 邮件发送 +function sendMailNotify($smtp, $connWork, $uidList, $title, $content, $relateId, $orderId) +{ + if (empty($smtp['smtp_host']) || empty($smtp['smtp_account'])) return; + $emailList = []; + foreach ($uidList as $uid) { + $uid = trim($uid); + if (empty($uid)) continue; + $res = mysqli_query($connWork, "SELECT email FROM sys_user WHERE uid='$uid'"); + $u = mysqli_fetch_assoc($res); + if (!empty($u['email'])) $emailList[] = $u['email']; + } + if (empty($emailList)) return; + $to = implode(",", array_unique($emailList)); + $subject = "【工单通知】#$orderId $title"; + $body = "

工单 #$orderId

+

关联告警ID:$relateId

+

标题:$title

+
$content
"; + // 此处接入PHPMailer发送逻辑 +} + +// 钉钉机器人推送 +function sendDingNotify($cfg, $text) +{ + $webhook = $cfg['ding_webhook'] ?? ''; + $secret = $cfg['ding_secret'] ?? ''; + if(empty($webhook)) return; + $data = json_encode([ + "msgtype" => "text", + "text" => ["content" => $text] + ]); + $opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]]; + file_get_contents($webhook, false, stream_context_create($opts)); +} + +// 企业微信机器人推送 +function sendWecomNotify($cfg, $text) +{ + $webhook = $cfg['wecom_webhook'] ?? ''; + if(empty($webhook)) return; + $data = json_encode([ + "msgtype" => "text", + "text" => ["content" => $text] + ]); + $opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]]; + file_get_contents($webhook, false, stream_context_create($opts)); +} + +// 无匹配动作 +echo json_encode(['code' => 400, 'msg' => '无效请求动作']); +mysqli_close($connWork); +mysqli_close($connConf); +exit; +?> diff --git a/alert_dashboard/www/auth.php b/alert_dashboard/www/auth.php index 8db6b8f..126661c 100644 --- a/alert_dashboard/www/auth.php +++ b/alert_dashboard/www/auth.php @@ -26,7 +26,7 @@ $userName = htmlspecialchars($_SESSION['username'] ?? ''); $isAdmin = intval($_SESSION['is_admin'] ?? 0); $userRoleId = intval($_SESSION['role_id'] ?? 0); -// ====================== 双库固定连接(修复null根源) ====================== +// ====================== 双库固定连接 ====================== $dbHost = "10.150.117.190"; $dbUser = "root"; $dbPass = "hp93000"; @@ -48,27 +48,43 @@ if (!$connMonitor) { mysqli_set_charset($connMonitor, "utf8mb4"); // ======================================================================== -// 获取当前访问页面文件名(去掉.php后缀) -$currentPage = basename($_SERVER['SCRIPT_NAME'], '.php'); +// ========== 新增:读取角色真实名称 ========== +$roleRealName = "未知角色"; +if ($userRoleId > 0) { + $getRoleSql = "SELECT role_name FROM sys_role WHERE id = $userRoleId"; + $roleResult = mysqli_query($connMonitor, $getRoleSql); + if ($roleResult && $roleRow = mysqli_fetch_assoc($roleResult)) { + $roleRealName = $roleRow['role_name']; + } +} +// 超级管理员强制覆盖文字 +if ($isAdmin === 1) { + $roleRealName = "超级管理员"; +} +// =========================================== -// 非超级管理员校验页面权限 -if ($isAdmin !== 1) { +// 1. 登录时一次性加载当前角色全部权限,存入全局变量(侧边栏共用) +$allowPages = []; +if ($userRoleId > 0) { $permSql = "SELECT page_key FROM sys_role_permission WHERE role_id = $userRoleId"; $permRes = mysqli_query($connMonitor, $permSql); - $allowPages = []; if ($permRes) { while ($row = mysqli_fetch_assoc($permRes)) { $allowPages[] = $row['page_key']; } } - // 无权限拦截 - if (!in_array($currentPage, $allowPages)) { - echo ""; - exit; - } } -// 读取右上角未读消息数量(修复第79/84/86行报错) +// 2. 获取当前访问页面文件名(去掉.php后缀) +$currentPage = basename($_SERVER['SCRIPT_NAME'], '.php'); + +// 3. 非管理员自动拦截无权限页面(全局统一校验,无需每个页面重复写) +if ($isAdmin !== 1 && !in_array($currentPage, $allowPages)) { + echo ""; + exit; +} + +// 读取右上角未读消息数量 $unreadMsgCount = 0; $msgSql = "SELECT COUNT(id) cnt FROM sys_user_msg WHERE user_id = $loginUserId AND is_read = 0"; $msgRes = mysqli_query($connMonitor, $msgSql); @@ -142,4 +158,6 @@ if (!empty($sidebarRawLinks)) { } } } + +// 【已删除冲突无用函数 hasPagePerm】 ?> diff --git a/alert_dashboard/www/dashboard.php b/alert_dashboard/www/dashboard.php index 142b238..53ec939 100644 --- a/alert_dashboard/www/dashboard.php +++ b/alert_dashboard/www/dashboard.php @@ -6,6 +6,9 @@ header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0"); header("Pragma: no-cache"); header("Expires: Thu, 01 Jan 1970 00:00:00 GMT"); + +require_once 'auth.php'; +// 权限拦截 // 会话超时:30分钟无操作自动登出,与admin后台统一 $expire = 1800; if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) { @@ -51,6 +54,7 @@ $langMap = [ 'monitor_panel' => '监控面板', 'alert_overview' => '告警监控总览', 'disk_capacity' => '服务器磁盘容量', + 'docker_mgr' => 'Docker容器管理', 'alert_list_page' => '历史告警查询', 'work_order_mgr' => '工单系统', 'system_manager' => '角色管理', @@ -124,6 +128,7 @@ $langMap = [ 'monitor_panel' => 'Monitor Panel', 'alert_overview' => 'Alert Overview', 'disk_capacity' => 'Server Disk Capacity', + 'docker_mgr' => 'Docker Manager', 'alert_list_page' => 'Alert Pagination List', 'work_order_mgr' => 'Work Order System', 'system_manager' => 'Role & Permission Manage', @@ -636,24 +641,44 @@ tbody tr:nth-child(even){background:#fbfcfe}
+ + + + + + + @@ -686,7 +718,7 @@ tbody tr:nth-child(even){background:#fbfcfe}
- + --
diff --git a/alert_dashboard/www/dashboard.php.1.0.3 b/alert_dashboard/www/dashboard.php.1.0.3 new file mode 100644 index 0000000..3bb861d --- /dev/null +++ b/alert_dashboard/www/dashboard.php.1.0.3 @@ -0,0 +1,1175 @@ +alert('无访问权限');location.href='login.php';"; + exit; +} + +// 会话超时:30分钟无操作自动登出,与admin后台统一 +$expire = 1800; +if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) { + session_destroy(); + header("Location: login.php"); + exit; +} +// 刷新登录计时 +$_SESSION['login_time'] = time(); +$userName = htmlspecialchars($_SESSION['username']); +$userRole = $_SESSION['role']; +$roleText = $userRole === 'admin' ? '管理员' : '只读用户'; + +// 读取全局系统配置(admin后台保存后同步生效) +$dbHost = "10.150.117.190"; +$dbUser = "root"; +$dbPwd = "hp93000"; +$dbName = "alert_mail_stat"; +$connConfig = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName); +mysqli_set_charset($connConfig, "utf8mb4"); +$configSql = "SELECT k,v FROM sys_config"; +$configRes = mysqli_query($connConfig, $configSql); +$sysConfig = []; +while ($row = mysqli_fetch_assoc($configRes)) { + $sysConfig[$row['k']] = $row['v']; +} +mysqli_close($connConfig); + +// 赋值全局配置 +$pageTitle = htmlspecialchars($sysConfig['page_title'] ?? "告警监控系统"); +$themeColor = htmlspecialchars($sysConfig['theme_color'] ?? "#409eff"); +$pageSize = $sysConfig['page_size'] ?? "20"; +$footerText = htmlspecialchars($sysConfig['footer_text'] ?? ""); +$sidebarRawLinks = $sysConfig['sidebar_links'] ?? ""; +// 读取语言配置 zh=中文 en=英文 +$currentLang = $sysConfig['lang'] ?? "zh"; + +// 全局多语言词典 +$langMap = [ + 'zh' => [ + 'html_lang' => 'zh-CN', + 'page_subtitle' => '爱德万测试 · 运维告警监控平台', + 'monitor_panel' => '监控面板', + 'alert_overview' => '告警监控总览', + 'disk_capacity' => '服务器磁盘容量', + 'docker_mgr' => 'Docker容器管理', + 'alert_list_page' => '历史告警查询', + 'work_order_mgr' => '工单系统', + 'system_manager' => '角色管理', + 'work_order_title' => '工单管理', + 'work_create' => '新建工单', + 'work_edit' => '编辑工单', + 'work_assign' => '分发工单', + 'work_status' => '工单状态', + 'work_creator' => '创建人', + 'work_handler' => '处理人', + 'work_content' => '工单内容/备注', + 'work_title' => '工单标题', + 'work_create_time' => '创建时间', + 'work_update_time' => '更新时间', + 'work_all_list' => '全部工单列表', + 'work_no_data' => '暂无工单记录', + 'work_save' => '保存', + 'work_cancel' => '取消', + 'work_placeholder_title' => '填写故障/需求标题', + 'work_placeholder_content' => '详细描述故障、操作步骤、处理备注', + 'work_select_handler' => '选择处理人(留空暂不分配)', + 'quick_link' => '快捷外链', + 'system_manage' => '系统管理', + 'system_config' => '后台配置管理', + 'current_user' => '当前用户:', + 'sys_time' => '系统时间:', + 'refresh_all' => '刷新全部数据', + 'export_csv' => '导出告警CSV', + 'logout' => '退出登录', + 'lang_text' => '语言:', + 'today_fire' => '今日触发告警', + 'unresolved_now' => '当前未恢复告警', + 'resolved_today' => '今日恢复告警', + 'instance_total' => '故障实例总数', + 'recovery_rate' => '告警恢复率', + 'today_fire_sub' => 'Total Firing Today', + 'unresolved_sub' => 'Unresolved Now', + 'resolved_sub' => 'Resolved Today', + 'instance_sub' => 'Affected Host Instance', + 'rate_sub' => 'Daily Recovery Rate', + 'top10_alert' => '今日告警频次 TOP 10', + 'active_alert' => '当前未恢复活跃告警', + 'alert_count' => '条未恢复故障', + 'all_alert_log' => '今日全量告警明细列表', + 'total_alert' => '条告警记录', + 'tag' => '告警状态', + 'alert_name' => '告警名称', + 'instance_addr' => '实例地址', + 'severity' => '告警级别', + 'occur_time' => '故障发生时间', + 'empty_normal' => '当前无未恢复告警,系统运行正常', + 'empty_no_data' => '暂无今日告警数据', + 'disk_stat' => '用户目录磁盘占用统计', + 'filter_ip' => '筛选实例IP:', + 'export_disk' => '导出当前表格CSV', + 'all_instance' => '全部实例', + 'disk_loading' => '磁盘数据加载中...', + 'no_disk_data' => '暂无磁盘指标数据', + 'no_ip_data' => '该实例下无目录数据', + 'csv_tip' => '正在生成CSV报表,请稍候...', + 'alert_axis' => '告警次数', + 'data_fail' => '数据加载失败,请重新登录', + 'server_fail' => '接口数据加载失败,请检查后端服务', + 'disk_api_fail' => '磁盘接口异常:', + 'disk_request_fail' => '请求df.php接口失败,请检查服务', + 'no_export_data' => '无数据可导出' + ], + 'en' => [ + 'html_lang' => 'en', + 'page_subtitle' => 'Advantest · O&M Alert Monitor Platform', + 'monitor_panel' => 'Monitor Panel', + 'alert_overview' => 'Alert Overview', + 'disk_capacity' => 'Server Disk Capacity', + 'docker_mgr' => 'Docker Manager', + 'alert_list_page' => 'Alert Pagination List', + 'work_order_mgr' => 'Work Order System', + 'system_manager' => 'Role & Permission Manage', + 'work_order_title' => 'Work Order Manage', + 'work_create' => 'Create Order', + 'work_edit' => 'Edit Order', + 'work_assign' => 'Assign Order', + 'work_status' => 'Status', + 'work_creator' => 'Creator', + 'work_handler' => 'Handler', + 'work_content' => 'Content / Remark', + 'work_title' => 'Order Title', + 'work_create_time' => 'Create Time', + 'work_update_time' => 'Update Time', + 'work_all_list' => 'All Work Order List', + 'work_no_data' => 'No order data', + 'work_save' => 'Save', + 'work_cancel' => 'Cancel', + 'work_placeholder_title' => 'Fill fault or demand title', + 'work_placeholder_content' => 'Describe fault, steps and handling remark', + 'work_select_handler' => 'Select handler (empty for unassigned)', + 'quick_link' => 'Quick Links', + 'system_manage' => 'System Manage', + 'system_config' => 'System Config', + 'current_user' => 'User: ', + 'sys_time' => 'System Time: ', + 'refresh_all' => 'Refresh All Data', + 'export_csv' => 'Export Alert CSV', + 'logout' => 'Logout', + 'lang_text' => 'Lang: ', + 'today_fire' => 'Today Triggered Alerts', + 'unresolved_now' => 'Unresolved Alerts', + 'resolved_today' => 'Resolved Today', + 'instance_total' => 'Affected Instances', + 'recovery_rate' => 'Daily Recovery Rate', + 'today_fire_sub' => 'Total Firing Today', + 'unresolved_sub' => 'Unresolved Now', + 'resolved_sub' => 'Resolved Today', + 'instance_sub' => 'Affected Host Instance', + 'rate_sub' => 'Daily Recovery Rate', + 'top10_alert' => 'Top 10 Alert Frequency Today', + 'active_alert' => 'Active Unresolved Alerts', + 'alert_count' => ' unresolved alerts', + 'all_alert_log' => 'Today Full Alert List', + 'total_alert' => ' alert records', + 'tag' => 'Status', + 'alert_name' => 'Alert Name', + 'instance_addr' => 'Instance IP', + 'severity' => 'Severity', + 'occur_time' => 'Occur Time', + 'empty_normal' => 'No active alerts, system normal', + 'empty_no_data' => 'No alert data today', + 'disk_stat' => 'User Directory Disk Usage', + 'filter_ip' => 'Filter IP: ', + 'export_disk' => 'Export Table CSV', + 'all_instance' => 'All Instances', + 'disk_loading' => 'Loading disk data...', + 'no_disk_data' => 'No disk data available', + 'no_ip_data' => 'No directory data for this instance', + 'csv_tip' => 'Generating CSV file, please wait...', + 'alert_axis' => 'Alert Count', + 'data_fail' => 'Data load failed, please relogin', + 'server_fail' => 'API request failed, check backend service', + 'disk_api_fail' => 'Disk API Error: ', + 'disk_request_fail' => 'Request df.php failed, check service', + 'no_export_data' => 'No data to export' + ] +]; +$t = $langMap[$currentLang]; + +// 解析侧边外链列表 +$sidebarLinkList = []; +if (!empty($sidebarRawLinks)) { + $lines = explode("\n", $sidebarRawLinks); + foreach ($lines as $line) { + $line = trim($line); + if (empty($line)) continue; + $item = explode("|", $line, 2); + if (count($item) === 2) { + $sidebarLinkList[] = [ + "name" => trim($item[0]), + "url" => trim($item[1]) + ]; + } + } +} +?> + + + + + +<?php echo $pageTitle; ?> - Advantest <?php echo $t['page_subtitle']; ?> + + + + + + +
+
+ + +
+
+
+ + + +
+
+
+
+
Advantest
+
+
+
+
+ + -- + +
+ + +
+ +
+ + +
+ +
+
+ +
+ +
+
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
0%
+
+
+
+ + +
+
+
+
+
+
+
+
+ + +
+
+
+
+
0
+
+
+ + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
0
+
+
+ + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+ + + +
+
+
+ + + + + + + + + + + + + + +
IPUsernamePathSize(GB)
+
+
+
+
+
+
+
+
+
+
+ + + + diff --git a/alert_dashboard/www/dashboard.php.1.0.4 b/alert_dashboard/www/dashboard.php.1.0.4 new file mode 100644 index 0000000..53ec939 --- /dev/null +++ b/alert_dashboard/www/dashboard.php.1.0.4 @@ -0,0 +1,1193 @@ + $expire)) { + session_destroy(); + header("Location: login.php"); + exit; +} +// 刷新登录计时 +$_SESSION['login_time'] = time(); +$userName = htmlspecialchars($_SESSION['username']); +$userRole = $_SESSION['role']; +$roleText = $userRole === 'admin' ? '管理员' : '只读用户'; + +// 读取全局系统配置(admin后台保存后同步生效) +$dbHost = "10.150.117.190"; +$dbUser = "root"; +$dbPwd = "hp93000"; +$dbName = "alert_mail_stat"; +$connConfig = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName); +mysqli_set_charset($connConfig, "utf8mb4"); +$configSql = "SELECT k,v FROM sys_config"; +$configRes = mysqli_query($connConfig, $configSql); +$sysConfig = []; +while ($row = mysqli_fetch_assoc($configRes)) { + $sysConfig[$row['k']] = $row['v']; +} +mysqli_close($connConfig); + +// 赋值全局配置 +$pageTitle = htmlspecialchars($sysConfig['page_title'] ?? "告警监控系统"); +$themeColor = htmlspecialchars($sysConfig['theme_color'] ?? "#409eff"); +$pageSize = $sysConfig['page_size'] ?? "20"; +$footerText = htmlspecialchars($sysConfig['footer_text'] ?? ""); +$sidebarRawLinks = $sysConfig['sidebar_links'] ?? ""; +// 读取语言配置 zh=中文 en=英文 +$currentLang = $sysConfig['lang'] ?? "zh"; + +// 全局多语言词典 +$langMap = [ + 'zh' => [ + 'html_lang' => 'zh-CN', + 'page_subtitle' => '爱德万测试 · 运维告警监控平台', + 'monitor_panel' => '监控面板', + 'alert_overview' => '告警监控总览', + 'disk_capacity' => '服务器磁盘容量', + 'docker_mgr' => 'Docker容器管理', + 'alert_list_page' => '历史告警查询', + 'work_order_mgr' => '工单系统', + 'system_manager' => '角色管理', + 'work_order_title' => '工单管理', + 'work_create' => '新建工单', + 'work_edit' => '编辑工单', + 'work_assign' => '分发工单', + 'work_status' => '工单状态', + 'work_creator' => '创建人', + 'work_handler' => '处理人', + 'work_content' => '工单内容/备注', + 'work_title' => '工单标题', + 'work_create_time' => '创建时间', + 'work_update_time' => '更新时间', + 'work_all_list' => '全部工单列表', + 'work_no_data' => '暂无工单记录', + 'work_save' => '保存', + 'work_cancel' => '取消', + 'work_placeholder_title' => '填写故障/需求标题', + 'work_placeholder_content' => '详细描述故障、操作步骤、处理备注', + 'work_select_handler' => '选择处理人(留空暂不分配)', + 'quick_link' => '快捷外链', + 'system_manage' => '系统管理', + 'system_config' => '后台配置管理', + 'current_user' => '当前用户:', + 'sys_time' => '系统时间:', + 'refresh_all' => '刷新全部数据', + 'export_csv' => '导出告警CSV', + 'logout' => '退出登录', + 'lang_text' => '语言:', + 'today_fire' => '今日触发告警', + 'unresolved_now' => '当前未恢复告警', + 'resolved_today' => '今日恢复告警', + 'instance_total' => '故障实例总数', + 'recovery_rate' => '告警恢复率', + 'today_fire_sub' => 'Total Firing Today', + 'unresolved_sub' => 'Unresolved Now', + 'resolved_sub' => 'Resolved Today', + 'instance_sub' => 'Affected Host Instance', + 'rate_sub' => 'Daily Recovery Rate', + 'top10_alert' => '今日告警频次 TOP 10', + 'active_alert' => '当前未恢复活跃告警', + 'alert_count' => '条未恢复故障', + 'all_alert_log' => '今日全量告警明细列表', + 'total_alert' => '条告警记录', + 'tag' => '告警状态', + 'alert_name' => '告警名称', + 'instance_addr' => '实例地址', + 'severity' => '告警级别', + 'occur_time' => '故障发生时间', + 'empty_normal' => '当前无未恢复告警,系统运行正常', + 'empty_no_data' => '暂无今日告警数据', + 'disk_stat' => '用户目录磁盘占用统计', + 'filter_ip' => '筛选实例IP:', + 'export_disk' => '导出当前表格CSV', + 'all_instance' => '全部实例', + 'disk_loading' => '磁盘数据加载中...', + 'no_disk_data' => '暂无磁盘指标数据', + 'no_ip_data' => '该实例下无目录数据', + 'csv_tip' => '正在生成CSV报表,请稍候...', + 'alert_axis' => '告警次数', + 'data_fail' => '数据加载失败,请重新登录', + 'server_fail' => '接口数据加载失败,请检查后端服务', + 'disk_api_fail' => '磁盘接口异常:', + 'disk_request_fail' => '请求df.php接口失败,请检查服务', + 'no_export_data' => '无数据可导出' + ], + 'en' => [ + 'html_lang' => 'en', + 'page_subtitle' => 'Advantest · O&M Alert Monitor Platform', + 'monitor_panel' => 'Monitor Panel', + 'alert_overview' => 'Alert Overview', + 'disk_capacity' => 'Server Disk Capacity', + 'docker_mgr' => 'Docker Manager', + 'alert_list_page' => 'Alert Pagination List', + 'work_order_mgr' => 'Work Order System', + 'system_manager' => 'Role & Permission Manage', + 'work_order_title' => 'Work Order Manage', + 'work_create' => 'Create Order', + 'work_edit' => 'Edit Order', + 'work_assign' => 'Assign Order', + 'work_status' => 'Status', + 'work_creator' => 'Creator', + 'work_handler' => 'Handler', + 'work_content' => 'Content / Remark', + 'work_title' => 'Order Title', + 'work_create_time' => 'Create Time', + 'work_update_time' => 'Update Time', + 'work_all_list' => 'All Work Order List', + 'work_no_data' => 'No order data', + 'work_save' => 'Save', + 'work_cancel' => 'Cancel', + 'work_placeholder_title' => 'Fill fault or demand title', + 'work_placeholder_content' => 'Describe fault, steps and handling remark', + 'work_select_handler' => 'Select handler (empty for unassigned)', + 'quick_link' => 'Quick Links', + 'system_manage' => 'System Manage', + 'system_config' => 'System Config', + 'current_user' => 'User: ', + 'sys_time' => 'System Time: ', + 'refresh_all' => 'Refresh All Data', + 'export_csv' => 'Export Alert CSV', + 'logout' => 'Logout', + 'lang_text' => 'Lang: ', + 'today_fire' => 'Today Triggered Alerts', + 'unresolved_now' => 'Unresolved Alerts', + 'resolved_today' => 'Resolved Today', + 'instance_total' => 'Affected Instances', + 'recovery_rate' => 'Daily Recovery Rate', + 'today_fire_sub' => 'Total Firing Today', + 'unresolved_sub' => 'Unresolved Now', + 'resolved_sub' => 'Resolved Today', + 'instance_sub' => 'Affected Host Instance', + 'rate_sub' => 'Daily Recovery Rate', + 'top10_alert' => 'Top 10 Alert Frequency Today', + 'active_alert' => 'Active Unresolved Alerts', + 'alert_count' => ' unresolved alerts', + 'all_alert_log' => 'Today Full Alert List', + 'total_alert' => ' alert records', + 'tag' => 'Status', + 'alert_name' => 'Alert Name', + 'instance_addr' => 'Instance IP', + 'severity' => 'Severity', + 'occur_time' => 'Occur Time', + 'empty_normal' => 'No active alerts, system normal', + 'empty_no_data' => 'No alert data today', + 'disk_stat' => 'User Directory Disk Usage', + 'filter_ip' => 'Filter IP: ', + 'export_disk' => 'Export Table CSV', + 'all_instance' => 'All Instances', + 'disk_loading' => 'Loading disk data...', + 'no_disk_data' => 'No disk data available', + 'no_ip_data' => 'No directory data for this instance', + 'csv_tip' => 'Generating CSV file, please wait...', + 'alert_axis' => 'Alert Count', + 'data_fail' => 'Data load failed, please relogin', + 'server_fail' => 'API request failed, check backend service', + 'disk_api_fail' => 'Disk API Error: ', + 'disk_request_fail' => 'Request df.php failed, check service', + 'no_export_data' => 'No data to export' + ] +]; +$t = $langMap[$currentLang]; + +// 解析侧边外链列表 +$sidebarLinkList = []; +if (!empty($sidebarRawLinks)) { + $lines = explode("\n", $sidebarRawLinks); + foreach ($lines as $line) { + $line = trim($line); + if (empty($line)) continue; + $item = explode("|", $line, 2); + if (count($item) === 2) { + $sidebarLinkList[] = [ + "name" => trim($item[0]), + "url" => trim($item[1]) + ]; + } + } +} +?> + + + + + +<?php echo $pageTitle; ?> - Advantest <?php echo $t['page_subtitle']; ?> + + + + + + +
+
+ + +
+
+
+ + + +
+
+
+
+
Advantest
+
+
+
+
+ + -- + +
+ + +
+ +
+ + +
+ +
+
+ +
+ +
+
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
0%
+
+
+
+ + +
+
+
+
+
+
+
+
+ + +
+
+
+
+
0
+
+
+ + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
0
+
+
+ + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+ + + +
+
+
+ + + + + + + + + + + + + + +
IPUsernamePathSize(GB)
+
+
+
+
+
+
+
+
+
+
+ + + + diff --git a/alert_dashboard/www/docker_mgr.php b/alert_dashboard/www/docker_mgr.php new file mode 100644 index 0000000..037a2d9 --- /dev/null +++ b/alert_dashboard/www/docker_mgr.php @@ -0,0 +1,1469 @@ + $expire)) { + session_destroy(); + header("Location: login.php"); + exit; +} +$_SESSION['login_time'] = time(); +$userName = htmlspecialchars($_SESSION['username']); +$userRole = $_SESSION['role']; +$roleText = $userRole === 'admin' ? '管理员' : '超级管理员'; + +$dbHost = "10.150.117.190"; +$dbUser = "root"; +$dbPwd = "hp93000"; +$dbName = "alert_mail_stat"; +$connConfig = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName); +mysqli_set_charset($connConfig, "utf8mb4"); +$configSql = "SELECT k,v FROM sys_config"; +$configRes = mysqli_query($connConfig, $configSql); +$sysConfig = []; +while ($row = mysqli_fetch_assoc($configRes)) { + $sysConfig[$row['k']] = $row['v']; +} +mysqli_close($connConfig); + +$pageTitle = htmlspecialchars($sysConfig['page_title'] ?? "告警监控系统"); +$themeColor = htmlspecialchars($sysConfig['theme_color'] ?? "#409eff"); +$pageSize = $sysConfig['page_size'] ?? "20"; +$footerText = htmlspecialchars($sysConfig['footer_text'] ?? ""); +$sidebarRawLinks = $sysConfig['sidebar_links'] ?? ""; +$currentLang = $sysConfig['lang'] ?? "zh"; + +$langMap = [ + 'zh' => [ + 'html_lang' => 'zh-CN', + 'page_subtitle' => '爱德万测试 · 运维告警监控平台', + 'monitor_panel' => '监控面板', + 'alert_overview' => '告警监控总览', + 'alert_list_page' => '历史告警查询', + 'work_order_mgr' => '工单系统', + 'docker_mgr' => 'Docker容器管理', + 'system_manager' => '角色管理', + 'quick_link' => '快捷外链', + 'system_manage' => '系统管理', + 'system_config' => '后台配置管理', + 'current_user' => '当前用户:', + 'sys_time' => '系统时间:', + 'refresh_all' => '刷新全部数据', + 'logout' => '退出登录', + 'lang_text' => '语言:', + 'container_list' => '容器列表', + 'image_list' => '镜像列表', + 'container_log' => '容器日志', + 'network_info' => '网络详情', + 'exec_container' => '进入容器执行命令', + 'restart' => '重启', + 'stop' => '停止', + 'remove' => '强制删除', + 'remove_image' => '删除镜像', + 'bind_repo' => '绑定镜像仓库', + 'input_cmd' => '仅允许 ls/pwd/cat/ps/df/free/top/du/whoami/env,禁止rm/wget/curl/chmod等高危指令', + 'execute' => '执行', + 'close' => '关闭', + 'loading' => '加载中...', + 'no_container' => '暂无容器', + 'no_image' => '无本地镜像', + 'request_fail' => '接口请求失败', + 'container_id' => '容器ID', + 'container_name' => '容器名称', + 'image_name' => '镜像', + 'status' => '运行状态', + 'port_mapping' => '端口映射', + 'operate' => '操作', + 'repo' => '镜像仓库', + 'tag' => '镜像标签', + 'image_id' => '镜像ID', + 'size' => '镜像大小', + 'create_time' => '创建时间', + 'confirm_remove' => '确认强制删除容器?操作不可恢复!', + 'confirm_remove_img' => '确认删除本地镜像?不可恢复!', + 'op_success' => '操作成功,已刷新状态', + 'op_failed' => '操作执行失败', + 'loading_mask' => '数据加载中,请稍候...', + 'create_container' => '新建容器', + 'batch_restart' => '批量重启', + 'batch_stop' => '批量停止', + 'batch_remove' => '批量删除', + 'search_placeholder' => '搜索容器名称/镜像', + 'op_menu' => '操作 ▼', + 'view_log' => '查看日志', + 'view_network' => '查看网络', + 'enter_exec' => '交互式终端', + 'stat_total' => '容器总数', + 'stat_running' => '运行中', + 'stat_stopped' => '已停止', + 'stat_image' => '本地镜像', + 'tip_exec_once' => '单次命令执行', + 'tip_websocket' => '实时交互式TTY终端,支持输入、复制、窗口自适应', + 'repo_addr' => '仓库地址', + 'repo_user' => '仓库账号', + 'repo_pwd' => '仓库密码', + 'repo_name' => '仓库名称', + 'bind_repo_title' => '绑定私有镜像仓库' + ], + 'en' => [ + 'html_lang' => 'en', + 'page_subtitle' => 'Advantest · O&M Alert Monitor Platform', + 'monitor_panel' => 'Monitor Panel', + 'alert_overview' => 'Alert Overview', + 'alert_list_page' => 'Alert Pagination List', + 'work_order_mgr' => 'Work Order System', + 'docker_mgr' => 'Docker Manager', + 'system_manager' => 'Role & Permission Manage', + 'quick_link' => 'Quick Links', + 'system_manage' => 'System Manage', + 'system_config' => 'System Config', + 'current_user' => 'User: ', + 'sys_time' => 'System Time: ', + 'refresh_all' => 'Refresh All Data', + 'logout' => 'Logout', + 'lang_text' => 'Lang: ', + 'container_list' => 'Container List', + 'image_list' => 'Image List', + 'container_log' => 'Container Logs', + 'network_info' => 'Network Info', + 'exec_container' => 'Exec Container Command', + 'restart' => 'Restart', + 'stop' => 'Stop', + 'remove' => 'Force Remove', + 'remove_image' => 'Remove Image', + 'bind_repo' => 'Bind Registry', + 'input_cmd' => 'Only allow ls/pwd/cat/ps/df/free/top/du/whoami/env, ban rm/wget/curl/chmod', + 'execute' => 'Run', + 'close' => 'Close', + 'loading' => 'Loading...', + 'no_container' => 'No Containers', + 'no_image' => 'No Local Images', + 'request_fail' => 'API Request Failed', + 'container_id' => 'Container ID', + 'container_name' => 'Name', + 'image_name' => 'Image', + 'status' => 'Status', + 'port_mapping' => 'Ports', + 'operate' => 'Operate', + 'repo' => 'Repository', + 'tag' => 'Tag', + 'image_id' => 'Image ID', + 'size' => 'Size', + 'create_time' => 'Created At', + 'confirm_remove' => 'Confirm force delete container? Irreversible!', + 'confirm_remove_img' => 'Confirm delete local image? Irreversible!', + 'op_success' => 'Operation succeeded, refreshed status', + 'op_failed' => 'Operation failed', + 'loading_mask' => 'Loading data, please wait...', + 'create_container' => 'Create Container', + 'batch_restart' => 'Batch Restart', + 'batch_stop' => 'Batch Stop', + 'batch_remove' => 'Batch Remove', + 'search_placeholder' => 'Search container / image', + 'op_menu' => 'Operate ▼', + 'view_log' => 'Logs', + 'view_network' => 'Network', + 'enter_exec' => 'Interactive Terminal', + 'stat_total' => 'Total Containers', + 'stat_running' => 'Running', + 'stat_stopped' => 'Stopped', + 'stat_image' => 'Local Images', + 'tip_exec_once' => 'Single command execution', + 'tip_websocket' => 'Real-time interactive TTY terminal, input & resize support', + 'repo_addr' => 'Registry Address', + 'repo_user' => 'Username', + 'repo_pwd' => 'Password', + 'repo_name' => 'Registry Name', + 'bind_repo_title' => 'Bind Private Registry' + ] +]; +$t = $langMap[$currentLang]; + +$sidebarLinkList = []; +if (!empty($sidebarRawLinks)) { + $lines = explode("\n", $sidebarRawLinks); + foreach ($lines as $line) { + $line = trim($line); + if (empty($line)) continue; + $item = explode("|", $line, 2); + if (count($item) === 2) { + $sidebarLinkList[] = [ + "name" => trim($item[0]), + "url" => trim($item[1]) + ]; + } + } +} +$pageSid = session_id(); + +// WebSocket HMAC 签名参数(和docker_api.py密钥完全一致) +$WS_SECRET = "DockerAdminPlatform2026SecretKey123456789"; +$wsUser = $_SESSION['user_id']; +$wsRole = $_SESSION['role']; +$wsTs = time(); +$wsRawSign = $wsUser . "|" . $wsRole . "|" . $wsTs; +$wsSign = hash_hmac("sha256", $wsRawSign, $WS_SECRET); +$wsClientIp = $_SERVER['REMOTE_ADDR']; +?> + + + + + +<?php echo $pageTitle; ?> - Advantest <?php echo $t['page_subtitle']; ?> + + + + + + + + +
+ + +
+ +
+ + +
+
+
+
+
Advantest
+
+
+
+
+ + -- +
+ + +
+ + +
+
+ +
+ 当前页面SID:  |  接口SID:(SID一致,会话互通) +
+ +
+
+
+
+
+ + +
+
+ +
+
+
+
0
+
+
+
+
0
+
+
+
+
0
+
+
+
+
0
+
+
+ +
+
+
+
+ +
+
+ +
+ + + +
+
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + diff --git a/alert_dashboard/www/docker_mgr.php.bak b/alert_dashboard/www/docker_mgr.php.bak new file mode 100644 index 0000000..64c8e4a --- /dev/null +++ b/alert_dashboard/www/docker_mgr.php.bak @@ -0,0 +1,1483 @@ + $expire)) { + session_destroy(); + header("Location: login.php"); + exit; +} +$_SESSION['login_time'] = time(); +$userName = htmlspecialchars($_SESSION['username']); +$userRole = $_SESSION['role']; +$roleText = $userRole === 'admin' ? '管理员' : '只读用户'; + +$dbHost = "10.150.117.190"; +$dbUser = "root"; +$dbPwd = "hp93000"; +$dbName = "alert_mail_stat"; +$connConfig = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName); +mysqli_set_charset($connConfig, "utf8mb4"); +$configSql = "SELECT k,v FROM sys_config"; +$configRes = mysqli_query($connConfig, $configSql); +$sysConfig = []; +while ($row = mysqli_fetch_assoc($configRes)) { + $sysConfig[$row['k']] = $row['v']; +} +mysqli_close($connConfig); + +$pageTitle = htmlspecialchars($sysConfig['page_title'] ?? "告警监控系统"); +$themeColor = htmlspecialchars($sysConfig['theme_color'] ?? "#409eff"); +$pageSize = $sysConfig['page_size'] ?? "20"; +$footerText = htmlspecialchars($sysConfig['footer_text'] ?? ""); +$sidebarRawLinks = $sysConfig['sidebar_links'] ?? ""; +$currentLang = $sysConfig['lang'] ?? "zh"; + +$langMap = [ + 'zh' => [ + 'html_lang' => 'zh-CN', + 'page_subtitle' => '爱德万测试 · 运维告警监控平台', + 'monitor_panel' => '监控面板', + 'alert_overview' => '告警监控总览', + 'alert_list_page' => '历史告警查询', + 'work_order_mgr' => '工单系统', + 'docker_mgr' => 'Docker容器管理', + 'system_manager' => '角色管理', + 'quick_link' => '快捷外链', + 'system_manage' => '系统管理', + 'system_config' => '后台配置管理', + 'current_user' => '当前用户:', + 'sys_time' => '系统时间:', + 'refresh_all' => '刷新全部数据', + 'logout' => '退出登录', + 'lang_text' => '语言:', + 'container_list' => '容器列表', + 'image_list' => '镜像列表', + 'container_log' => '容器日志', + 'network_info' => '网络详情', + 'exec_container' => '进入容器执行命令', + 'restart' => '重启', + 'stop' => '停止', + 'remove' => '强制删除', + 'input_cmd' => '仅允许 ls/pwd/cat/ps/df/free/top/du/whoami/env,禁止rm/wget/curl/chmod等高危指令', + 'execute' => '执行', + 'close' => '关闭', + 'loading' => '加载中...', + 'no_container' => '暂无容器', + 'no_image' => '无本地镜像', + 'request_fail' => '接口请求失败', + 'container_id' => '容器ID', + 'container_name' => '容器名称', + 'image_name' => '镜像', + 'status' => '运行状态', + 'port_mapping' => '端口映射', + 'operate' => '操作', + 'repo' => '镜像仓库', + 'tag' => '镜像标签', + 'image_id' => '镜像ID', + 'size' => '镜像大小', + 'create_time' => '创建时间', + 'confirm_remove' => '确认强制删除容器?操作不可恢复!', + 'op_success' => '操作成功,已刷新状态', + 'op_failed' => '操作执行失败', + 'loading_mask' => '数据加载中,请稍候...', + 'create_container' => '新建容器', + 'batch_restart' => '批量重启', + 'batch_stop' => '批量停止', + 'batch_remove' => '批量删除', + 'search_placeholder' => '搜索容器名称/镜像', + 'op_menu' => '操作 ▼', + 'view_log' => '查看日志', + 'view_network' => '查看网络', + 'enter_exec' => '进入容器', + 'stat_total' => '容器总数', + 'stat_running' => '运行中', + 'stat_stopped' => '已停止', + 'stat_image' => '本地镜像', + 'tip_exec_once' => '当前为单次命令执行,如需实时交互式终端需后端WebSocket扩展' + ], + 'en' => [ + 'html_lang' => 'en', + 'page_subtitle' => 'Advantest · O&M Alert Monitor Platform', + 'monitor_panel' => 'Monitor Panel', + 'alert_overview' => 'Alert Overview', + 'alert_list_page' => 'Alert Pagination List', + 'work_order_mgr' => 'Work Order System', + 'docker_mgr' => 'Docker Manager', + 'system_manager' => 'Role & Permission Manage', + 'quick_link' => 'Quick Links', + 'system_manage' => 'System Manage', + 'system_config' => 'System Config', + 'current_user' => 'User: ', + 'sys_time' => 'System Time: ', + 'refresh_all' => 'Refresh All Data', + 'logout' => 'Logout', + 'lang_text' => 'Lang: ', + 'container_list' => 'Container List', + 'image_list' => 'Image List', + 'container_log' => 'Container Logs', + 'network_info' => 'Network Info', + 'exec_container' => 'Exec Container Command', + 'restart' => 'Restart', + 'stop' => 'Stop', + 'remove' => 'Force Remove', + 'input_cmd' => 'Only allow ls/pwd/cat/ps/df/free/top/du/whoami/env, ban rm/wget/curl/chmod', + 'execute' => 'Run', + 'close' => 'Close', + 'loading' => 'Loading...', + 'no_container' => 'No Containers', + 'no_image' => 'No Local Images', + 'request_fail' => 'API Request Failed', + 'container_id' => 'Container ID', + 'container_name' => 'Name', + 'image_name' => 'Image', + 'status' => 'Status', + 'port_mapping' => 'Ports', + 'operate' => 'Operate', + 'repo' => 'Repository', + 'tag' => 'Tag', + 'image_id' => 'Image ID', + 'size' => 'Size', + 'create_time' => 'Created At', + 'confirm_remove' => 'Confirm force delete container? Irreversible!', + 'op_success' => 'Operation succeeded, refreshed status', + 'op_failed' => 'Operation failed', + 'loading_mask' => 'Loading data, please wait...', + 'create_container' => 'Create Container', + 'batch_restart' => 'Batch Restart', + 'batch_stop' => 'Batch Stop', + 'batch_remove' => 'Batch Remove', + 'search_placeholder' => 'Search container / image', + 'op_menu' => 'Operate ▼', + 'view_log' => 'Logs', + 'view_network' => 'Network', + 'enter_exec' => 'Exec', + 'stat_total' => 'Total Containers', + 'stat_running' => 'Running', + 'stat_stopped' => 'Stopped', + 'stat_image' => 'Local Images', + 'tip_exec_once' => 'Single command execution only, real-time terminal requires WebSocket backend' + ] +]; +$t = $langMap[$currentLang]; + +$sidebarLinkList = []; +if (!empty($sidebarRawLinks)) { + $lines = explode("\n", $sidebarRawLinks); + foreach ($lines as $line) { + $line = trim($line); + if (empty($line)) continue; + $item = explode("|", $line, 2); + if (count($item) === 2) { + $sidebarLinkList[] = [ + "name" => trim($item[0]), + "url" => trim($item[1]) + ]; + } + } +} +$pageSid = session_id(); +?> + + + + + +<?php echo $pageTitle; ?> - Advantest <?php echo $t['page_subtitle']; ?> + + + + + +
+ + +
+ +
+ + +
+
+
+
+
Advantest
+
+
+
+
+ + -- +
+ + +
+ + +
+
+ +
+ 当前页面SID:  |  接口SID:(SID一致,会话互通) +
+ +
+
+
+
+
+ +
+
+ + +
+
+
+
0
+
+
+
+
0
+
+
+
+
0
+
+
+
+
0
+
+
+ +
+
+
+
+ + +
+
+ +
+ + + +
+
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+ + +
+
+ + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + diff --git a/alert_dashboard/www/smtp_config.php b/alert_dashboard/www/smtp_config.php index 2e83703..95b678b 100644 --- a/alert_dashboard/www/smtp_config.php +++ b/alert_dashboard/www/smtp_config.php @@ -4,6 +4,9 @@ header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0"); header("Pragma: no-cache"); header("Expires: Thu, 01 Jan 1970 00:00:00 GMT"); + +require_once 'auth.php'; + // 兼容低PHP版本 str_contains if (!function_exists('str_contains')) { function str_contains($haystack, $needle) { @@ -418,7 +421,7 @@ if ($_POST['act'] === "save_all") {
- + --
diff --git a/alert_dashboard/www/system_manage.php b/alert_dashboard/www/system_manage.php index 4134340..5663e0d 100644 --- a/alert_dashboard/www/system_manage.php +++ b/alert_dashboard/www/system_manage.php @@ -3,6 +3,7 @@ $pageTitle = "系统权限管理"; $activeTab = $_GET['tab'] ?? 'role'; $conn = $connMonitor; +require_once 'auth.php'; // ========== 角色操作逻辑 ========== if ($_POST['act'] == "save_role") { $roleName = trim($_POST['role_name']); @@ -123,6 +124,7 @@ $allPagePerms = [ ['key'=>'history_query','name'=>'历史查询'], ['key'=>'smtp_config','name'=>'消息通知配置'], ['key'=>'system_manage','name'=>'系统权限管理'], + ['key'=>'docker_mgr','name'=>'Docker容器管理'], ]; ?> @@ -259,7 +261,7 @@ input[type="number"]{-moz-appearance:textfield;} 0):?> - + --
@@ -486,13 +502,14 @@ tr.unread-row{background:#fff3f3 !important;} + OP - +
@@ -510,6 +527,8 @@ tr.unread-row{background:#fff3f3 !important;} case 4:$statusText=$t['status_4'];$statusClass='tag-closed';break; } $rowClass = $item['is_read'] == 0 && $item['assign_uid'] == $loginUid ? 'unread-row' : ''; + // 操作权限:管理员 或 当前登录用户是认领人 + $canOperate = ($isAdmin === 1 || $item['claim_uid'] === $loginUid); ?> - - + + + + + - > - + + @@ -734,7 +766,7 @@ function loadFiringAlert(selSelector, bindId=""){ const sel = $(selSelector); sel.html(``); list.forEach(item=>{ - const aid = `${item.alert_name}|${item.instance}`; + const aid = `${item.alert_name}_${item.instance}`; sel.append(``); @@ -781,6 +813,7 @@ $("#closeEditModal,#cancelEdit").click(function(){ $("#editModal").hide(); }); +// 编辑工单弹窗填充 $(document).on("click", ".edit-order", function(){ const tr = $(this).closest("tr"); $("#editOrderId").val(tr.data("id")); @@ -801,6 +834,8 @@ $(document).on("click", ".edit-order", function(){ loadFiringAlert("#editRelateAlertSelect", relateAid); $("#editModal").css("display","flex"); }); + +// 新建工单提交 $("#submitOrder").click(function(){ const title = $("#orderTitle").val().trim(); const content = $("#orderContent").val().trim(); @@ -898,7 +933,7 @@ $(document).on("change", ".status-select", function(){ }); }); -// 管理员分发工单(修复只修改当前行单条ID,不再匹配标题批量更新) +// 管理员分发工单 $(document).on("change", ".assign-user", function(){ const tr = $(this).closest("tr"); const orderId = tr.data("id"); @@ -920,6 +955,29 @@ $(document).on("change", ".assign-user", function(){ }); }); +// 工单认领 +$(document).on("click", ".claim-order", function(){ + const orderId = $(this).data("id"); + if(!confirm(currentLang==="zh"?"确认认领该工单?":"Confirm claim this order?")) return; + $.ajax({ + url: apiOrder + "?action=claim_work", + method: "POST", + contentType: "application/json", + data: JSON.stringify({ + id: orderId, + uid: loginUid + }), + dataType: "json", + success(res){ + alert(res.msg); + if(res.code === 0) location.reload(); + }, + error(){ + alert(currentLang==='zh'?"认领请求失败":"Claim failed"); + } + }); +}); + // 一键标记全部当前用户待处理工单为已读 $("#markAllRead").click(function(){ $.ajax({ diff --git a/alert_dashboard/www/work_order.php.1.0.3 b/alert_dashboard/www/work_order.php.1.0.3 new file mode 100644 index 0000000..572f55f --- /dev/null +++ b/alert_dashboard/www/work_order.php.1.0.3 @@ -0,0 +1,955 @@ + $expire)) { + session_destroy(); + header("Location: login.php"); + exit; +} +$_SESSION['login_time'] = time(); +$userName = htmlspecialchars($_SESSION['username']); +$userRoleId = $_SESSION['role_id']; +$loginUid = $_SESSION['user_id']; + +// 全局配置库连接 +$dbConfigHost = "10.150.117.190"; +$dbConfigUser = "root"; +$dbConfigPwd = "hp93000"; +$dbConfigName = "alert_mail_stat"; +$connConfig = mysqli_connect($dbConfigHost, $dbConfigUser, $dbConfigPwd, $dbConfigName); +if(!$connConfig){ + die("配置库连接失败"); +} +mysqli_set_charset($connConfig, "utf8mb4"); +$configSql = "SELECT k,v FROM sys_config"; +$configRes = mysqli_query($connConfig, $configSql); +$sysConfig = []; +while ($row = mysqli_fetch_assoc($configRes)) { + $sysConfig[$row['k']] = $row['v']; +} + +// 修复SMTP查询警告 +$smtpRow = []; +$sqlSmtp = "SELECT * FROM sys_smtp_config WHERE id=1"; +if (!empty(trim($sqlSmtp))) { + $smtpQuery = mysqli_query($connConfig, $sqlSmtp); + if ($smtpQuery !== false && $smtpQuery instanceof mysqli_result) { + $smtpRow = mysqli_fetch_assoc($smtpQuery) ?: []; + } +} +$smtpConfig = $smtpRow ?? []; + +$pageTitle = htmlspecialchars($sysConfig['page_title'] ?? "告警监控系统"); +$currentLang = $sysConfig['lang'] ?? "zh"; +$sidebarRawLinks = $sysConfig['sidebar_links'] ?? ""; +mysqli_close($connConfig); + +// 多语言词典(新增未读、待处理文案) +$langMap = [ + 'zh' => [ + 'html_lang' => 'zh-CN', + 'page_subtitle' => '爱德万测试 · 运维告警监控平台', + 'monitor_panel' => '监控面板', + 'alert_overview' => '告警总览', + 'quick_link' => '快捷外链', + 'system_manage' => '系统管理', + 'system_config' => '系统参数', + 'notify_config' => '工单消息通知配置', + 'current_user' => '当前用户:', + 'sys_time' => '系统时间:', + 'refresh_all' => '刷新页面', + 'export_csv' => '导出CSV', + 'logout' => '退出登录', + 'lang_text' => '语言:', + 'work_order_mgr' => '工单系统', + 'work_create' => '新建工单', + 'work_edit' => '编辑工单', + 'work_assign' => '分发工单', + 'work_status' => '工单状态', + 'work_creator' => '创建人', + 'work_handler' => '处理人', + 'work_content' => '工单内容/备注', + 'work_title' => '工单标题', + 'create_time' => '创建时间', + 'update_time' => '更新时间', + 'all_work_order' => '全部工单列表', + 'no_work_data' => '暂无工单数据', + 'save' => '保存', + 'cancel' => '取消', + 'title_placeholder' => '填写故障/需求标题', + 'content_placeholder' => '详细描述故障现象、操作步骤、处理方案', + 'select_handler' => '选择处理人(留空暂不分配)', + 'status_1' => '待处理', + 'status_2' => '处理中', + 'status_3' => '已完成', + 'status_4' => '已关闭', + 'no_alert_option' => '不关联任何告警', + 'relate_alert' => '关联告警(可选)', + 'relate_alert_id' => '关联告警ID', + 'notify_title' => '通知配置', + 'notify_user' => '通知接收人', + 'notify_channel' => '通知渠道', + 'channel_mail' => '邮箱', + 'channel_ding' => '钉钉', + 'channel_wecom' => '企业微信', + 'smtp_host' => 'SMTP服务器', + 'smtp_port' => '端口', + 'smtp_ssl' => '开启SSL/TLS', + 'smtp_account' => '发件账号', + 'smtp_pwd' => '授权密码', + 'smtp_from' => '发件邮箱', + 'save_smtp' => '保存通知配置', + 'alert_loading' => '加载活跃告警...', + 'no_alert_data' => '当前无正在触发的告警', + 'user_loading' => '加载用户列表', + 'no_user_data' => '无可用用户', + 'perm_deny' => '权限不足,禁止访问', + 'unread_ticket' => '未读工单', + 'mark_all_read' => '全部标记已读' + ], + 'en' => [ + 'html_lang' => 'en', + 'page_subtitle' => 'Advantest · O&M Alert Monitor Platform', + 'monitor_panel' => 'Monitor Panel', + 'alert_overview' => 'Alert Overview', + 'quick_link' => 'Quick Links', + 'system_manage' => 'System Manage', + 'system_config' => 'System Config', + 'notify_config' => 'Work Order Notification Config', + 'current_user' => 'User: ', + 'sys_time' => 'System Time: ', + 'refresh_all' => 'Refresh', + 'export_csv' => 'Export CSV', + 'logout' => 'Logout', + 'lang_text' => 'Lang: ', + 'work_order_mgr' => 'Work Order System', + 'work_create' => 'Create Order', + 'work_edit' => 'Edit Order', + 'work_assign' => 'Assign Order', + 'work_status' => 'Status', + 'work_creator' => 'Creator', + 'work_handler' => 'Handler', + 'work_content' => 'Content / Remark', + 'work_title' => 'Order Title', + 'create_time' => 'Create Time', + 'update_time' => 'Update Time', + 'all_work_order' => 'All Work Orders', + 'no_work_data' => 'No work orders', + 'save' => 'Save', + 'cancel' => 'Cancel', + 'title_placeholder' => 'Fill fault or demand title', + 'content_placeholder' => 'Describe fault, steps and handling plan', + 'select_handler' => 'Select handler (empty for unassigned)', + 'status_1' => 'Pending', + 'status_2' => 'Processing', + 'status_3' => 'Finished', + 'status_4' => 'Closed', + 'no_alert_option' => 'No related alert', + 'relate_alert' => 'Relate Alert (Optional)', + 'relate_alert_id' => 'Relate Alert ID', + 'notify_title' => 'Notification Config', + 'notify_user' => 'Notify Users', + 'notify_channel' => 'Notify Channel', + 'channel_mail' => 'Email', + 'channel_ding' => 'DingTalk', + 'channel_wecom' => 'WeCom', + 'smtp_host' => 'SMTP Host', + 'smtp_port' => 'Port', + 'smtp_ssl' => 'Enable SSL/TLS', + 'smtp_account' => 'SMTP Account', + 'smtp_pwd' => 'Auth Password', + 'smtp_from' => 'Sender Email', + 'save_smtp' => 'Save Notification Config', + 'alert_loading' => 'Loading active alerts...', + 'no_alert_data' => 'No active firing alerts', + 'user_loading' => 'Loading user list...', + 'no_user_data' => 'No available users', + 'perm_deny' => 'Permission denied', + 'unread_ticket' => 'Unread Tickets', + 'mark_all_read' => 'Mark All Read' + ] +]; +$t = $langMap[$currentLang]; + +// 解析外链菜单 +$sidebarLinkList = []; +if (!empty($sidebarRawLinks)) { + $lines = explode("\n", $sidebarRawLinks); + foreach ($lines as $line) { + $line = trim($line); + if (empty($line)) continue; + $item = explode("|", $line, 2); + if (count($item) === 2) { + $sidebarLinkList[] = ["name" => trim($item[0]), "url" => trim($item[1])]; + } + } +} + +// 工单业务库 monitor +$dbWorkHost = "10.150.117.190"; +$dbWorkUser = "root"; +$dbWorkPwd = "hp93000"; +$dbWorkName = "monitor"; +$connWork = mysqli_connect($dbWorkHost, $dbWorkUser, $dbWorkPwd, $dbWorkName); +if (!$connWork) { + die("工单数据库 monitor 连接失败"); +} +mysqli_set_charset($connWork, "utf8mb4"); + +// 获取用户角色权限 +$isAdmin = 0; +$permList = ""; +$roleSql = "SELECT r.is_admin, r.perm_list FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?"; +$stmtRole = mysqli_prepare($connWork, $roleSql); +if($stmtRole){ + mysqli_stmt_bind_param($stmtRole, 's', $loginUid); + mysqli_stmt_execute($stmtRole); + $roleRes = mysqli_stmt_get_result($stmtRole); + if ($roleRes instanceof mysqli_result) { + $roleRow = mysqli_fetch_assoc($roleRes); + if ($roleRow) { + $isAdmin = intval($roleRow['is_admin']); + $permList = $roleRow['perm_list']; + } + } +} + +// 权限校验临时注释,如需开启取消注释 +//$hasWorkPerm = str_contains($permList, "work_order"); +//if (!$hasWorkPerm) { +// echo ""; +// exit; +//} + +// 加载全部系统用户下拉 +$userOptions = ""; +$userAll = mysqli_query($connWork, "SELECT uid, real_name, username FROM sys_user WHERE enable = 1 ORDER BY real_name ASC"); +if ($userAll instanceof mysqli_result) { + while ($u = mysqli_fetch_assoc($userAll)) { + $userOptions .= ''; + } +} + +// ========== 修复1:统计当前用户未读待处理工单(右上角角标数字) ========== +$unreadCount = 0; +$unreadSql = "SELECT COUNT(*) cnt FROM work_order WHERE assign_uid = ? AND status = 1 AND is_read = 0"; +$stmtUnread = mysqli_prepare($connWork, $unreadSql); +mysqli_stmt_bind_param($stmtUnread, 's', $loginUid); +mysqli_stmt_execute($stmtUnread); +$unreadRes = mysqli_stmt_get_result($stmtUnread); +if($unreadRes){ + $unreadRow = mysqli_fetch_assoc($unreadRes); + $unreadCount = intval($unreadRow['cnt']); +} + +// 分页参数 +$page = isset($_GET['page']) ? intval($_GET['page']) : 1; +$pageSize = intval($sysConfig['page_size'] ?? 20); +if ($pageSize < 1) $pageSize = 20; +$offset = ($page - 1) * $pageSize; + +// ========== 修复1:数据权限过滤 普通用户仅展示创建/分配给自己工单,参数绑定修复 ========== +if ($isAdmin === 1) { + $whereSql = " 1=1 "; +} else { + $whereSql = " (create_uid = ? OR assign_uid = ?) "; +} + +// 统计工单总数 +$cntSql = "SELECT COUNT(*) total FROM work_order WHERE " . $whereSql; +$stmtCnt = mysqli_prepare($connWork, $cntSql); +$total = 0; +if($stmtCnt){ + if ($isAdmin !== 1) { + mysqli_stmt_bind_param($stmtCnt, 'ss', $loginUid, $loginUid); + } + mysqli_stmt_execute($stmtCnt); + $cntRes = mysqli_stmt_get_result($stmtCnt); + if ($cntRes instanceof mysqli_result) { + $cntRow = mysqli_fetch_assoc($cntRes); + $total = intval($cntRow['total'] ?? 0); + } +} +$totalPage = $pageSize <= 0 ? 1 : ceil($total / $pageSize); + +// 查询工单列表(带出is_read未读标记) +$listSql = "SELECT wo.*, uc.real_name create_name, ua.real_name assign_name +FROM work_order wo +LEFT JOIN sys_user uc ON wo.create_uid = uc.uid +LEFT JOIN sys_user ua ON wo.assign_uid = ua.uid +WHERE " . $whereSql . " ORDER BY create_time DESC LIMIT ?, ?"; +$stmtList = mysqli_prepare($connWork, $listSql); +$orderList = []; +if($stmtList){ + if ($isAdmin === 1) { + mysqli_stmt_bind_param($stmtList, 'ii', $offset, $pageSize); + } else { + // 修复参数绑定:2个字符串uid + 2个数字分页参数 + mysqli_stmt_bind_param($stmtList, 'ssii', $loginUid, $loginUid, $offset, $pageSize); + } + mysqli_stmt_execute($stmtList); + $listResult = mysqli_stmt_get_result($stmtList); + if ($listResult instanceof mysqli_result) { + while ($row = mysqli_fetch_assoc($listResult)) { + $orderList[] = $row; + } + } +} +mysqli_close($connWork); +?> + + + + + +<?php echo $pageTitle; ?> - Advantest <?php echo $t['work_order_mgr']; ?> + + + + + + +
+
+
+
+ +
+
+
+
+
Advantest
+
+
+
+
+ + + + 0):?> + + + -- +
+ +
+ + +
+
+
+
+
+
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDOP
+
+ +
+
+
+ + + + + +
+
+ +
+
+
+
+ + + + + + + + + diff --git a/alert_dashboard78.tar.gz b/alert_dashboard78.tar.gz new file mode 100644 index 0000000..a69ef6e Binary files /dev/null and b/alert_dashboard78.tar.gz differ