修复权限管理,角色认证,更新工单认领系统,增加表格导出支持区间选择,增加docker管理(开发中
This commit is contained in:
@@ -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;'"]
|
||||
|
||||
@@ -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;'"]
|
||||
@@ -203,3 +203,91 @@ nohup: ignoring input
|
||||
10.150.117.190 - - [07/Jul/2026 11:46:03] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 12:12:58] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 12:13:48] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 13:37:13] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 13:42:58] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 13:43:23] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 13:45:58] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 13:46:48] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 13:54:28] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 13:54:38] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 14:20:43] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 14:21:23] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 14:25:43] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 14:25:43] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 14:25:53] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 14:27:58] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 14:28:38] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 14:28:53] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 14:40:58] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 14:43:38] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 14:45:49] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 14:45:49] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [07/Jul/2026 17:32:51] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 09:56:28] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 09:57:43] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 09:59:18] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 09:59:23] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 10:08:43] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 10:08:43] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 10:09:03] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 10:09:28] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 10:10:23] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 10:10:38] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 10:15:28] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 10:17:38] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 10:18:43] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 10:19:03] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 11:11:43] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 11:19:53] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 11:26:28] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 11:26:38] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:25:58] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:26:18] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:35:13] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:36:03] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:40:43] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:42:33] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:21] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 13:52:36] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 14:09:28] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 14:09:48] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 14:15:02] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 14:15:02] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 14:15:02] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 14:15:02] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 14:15:12] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 14:15:12] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 14:15:12] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 14:15:12] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 14:15:12] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 14:15:12] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 14:15:17] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 15:18:33] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 15:18:33] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 15:18:33] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 15:18:33] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 15:18:33] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 15:18:33] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 15:18:33] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 15:18:33] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 15:18:48] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
10.150.117.190 - - [08/Jul/2026 15:19:03] "[37mPOST /alert HTTP/1.1[0m" 200 -
|
||||
|
||||
@@ -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;}
|
||||
<div class="nav-right">
|
||||
<a href="alert_subscribe.php" class="btn btn-outline"><i class="fa fa-bell"></i> 告警订阅管理</a>
|
||||
<span style="color:#4E5969;"><i class="fa fa-user"></i> <?php echo $userName; ?></span>
|
||||
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?>(<?php echo $roleRealName; ?>)</span>
|
||||
<button class="btn btn-danger" id="logoutBtn"><i class="fa fa-sign-out"></i> 退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
session_start();
|
||||
header("Access-Control-Allow-Credentials: true");
|
||||
header("Content-Type:application/json;charset=utf-8");
|
||||
|
||||
$SECRET_RAW = "DockerAdminPlatform2026SecretKey123456789";
|
||||
|
||||
$uidGet = $_GET['uid'] ?? '';
|
||||
if (!empty($uidGet) && empty($_SESSION['user_id'])) {
|
||||
$_SESSION['user_id'] = $uidGet;
|
||||
$_SESSION['login_time'] = time();
|
||||
}
|
||||
|
||||
$debug = [
|
||||
"sid" => 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;
|
||||
@@ -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/<image_id>")
|
||||
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/<cid>")
|
||||
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/<cid>")
|
||||
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/<cid>")
|
||||
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/<cid>/<int:tail_lines>")
|
||||
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/<cid>")
|
||||
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/<cid>", 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/<cid>")
|
||||
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/<cid>")
|
||||
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)
|
||||
@@ -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/<cid>")
|
||||
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/<cid>")
|
||||
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/<cid>")
|
||||
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/<cid>/<int:tail_lines>")
|
||||
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/<cid>")
|
||||
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)
|
||||
@@ -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/<image_id>")
|
||||
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/<cid>")
|
||||
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/<cid>")
|
||||
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/<cid>")
|
||||
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/<cid>/<int:tail_lines>")
|
||||
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/<cid>")
|
||||
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/<cid>", 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/<cid>")
|
||||
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/<cid>")
|
||||
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)
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
while true; do
|
||||
nc -l 127.0.0.1 9999 <<EOF
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
$(docker ps -a --format '{{.Names}}|{{.ID}}|{{.Image}}|{{.Status}}|{{.Ports}}')
|
||||
EOF
|
||||
done
|
||||
@@ -25,28 +25,58 @@ function getDbConn() {
|
||||
|
||||
// CSV导出接口(支持指定历史日期下载,不输出JSON头)
|
||||
if ($act === "export_csv") {
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
$conn = getDbConn();
|
||||
mysqli_query($conn, "SET time_zone = '+08:00'");
|
||||
if (!$conn) {
|
||||
header("Content-Type:text/html;charset=utf-8");
|
||||
echo "数据库连接失败:" . mysqli_connect_error();
|
||||
exit;
|
||||
}
|
||||
// 接收自定义日期参数,不传则取今日
|
||||
$targetDate = $_GET['date'] ?? date("Y-m-d");
|
||||
// 简单日期格式校验 Y-m-d
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $targetDate)) {
|
||||
header("Content-Type:application/json;charset=utf-8");
|
||||
echo json_encode(["code"=>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();
|
||||
|
||||
@@ -322,13 +322,25 @@ button.normal:hover{
|
||||
|
||||
<!-- 9 导出CSV报表(带日历选择) -->
|
||||
<div class="card">
|
||||
<h3><i class="fa fa-download"></i>9. 导出告警CSV报表(支持历史日期)</h3>
|
||||
<div class="row">
|
||||
<label>选择日期:</label>
|
||||
<h3><i class="fa fa-download"></i>9. 导出告警CSV报表</h3>
|
||||
|
||||
<!-- 第一组:单日导出(原有功能保留) -->
|
||||
<div class="row" style="margin-bottom:12px;gap:10px;align-items:center;flex-wrap:wrap;">
|
||||
<label>单日导出:</label>
|
||||
<input type="date" id="csv_date" placeholder="留空导出今日">
|
||||
<button class="btn-success" onclick="exportCsv()"><i class="fa fa-download"></i>下载对应日期报表</button>
|
||||
<button class="btn-success" onclick="exportCsvSingle()"><i class="fa fa-download"></i>下载单日报表</button>
|
||||
</div>
|
||||
<div class="tip">不选择日期默认导出今日告警;选择历史日期可下载过往报表</div>
|
||||
<div class="tip" style="margin-bottom:16px;">不选择日期默认导出今日告警;选择历史日期下载单日报表</div>
|
||||
|
||||
<!-- 第二组:区间导出(新增) -->
|
||||
<div class="row" style="gap:10px;align-items:center;flex-wrap:wrap;">
|
||||
<label>区间导出:</label>
|
||||
<input type="date" id="csv_start">
|
||||
<span>~</span>
|
||||
<input type="date" id="csv_end">
|
||||
<button class="btn-primary" onclick="exportCsvRange()"><i class="fa fa-download"></i>下载区间报表</button>
|
||||
</div>
|
||||
<div class="tip">填写开始、结束日期,导出该时间段全部告警数据</div>
|
||||
</div>
|
||||
|
||||
<!-- 返回结果输出 -->
|
||||
@@ -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");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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,
|
||||
<p>关联告警ID:$relateId</p>
|
||||
<p>标题:$title</p>
|
||||
<pre style='background:#f5f5f5;padding:10px'>$content</pre>";
|
||||
// 此处接入PHPMailer发送逻辑
|
||||
}
|
||||
|
||||
// 钉钉机器人推送
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=utf-8");
|
||||
header("Access-Control-Allow-Origin: *");
|
||||
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
|
||||
header("Access-Control-Allow-Headers: Content-Type");
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
echo json_encode(['code' => 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 = "<h3>工单 #$orderId</h3>
|
||||
<p>关联告警ID:$relateId</p>
|
||||
<p>标题:$title</p>
|
||||
<pre style='background:#f5f5f5;padding:10px'>$content</pre>";
|
||||
// 此处接入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;
|
||||
?>
|
||||
@@ -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 "<script>alert('无访问权限!');history.back();</script>";
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// 读取右上角未读消息数量(修复第79/84/86行报错)
|
||||
// 2. 获取当前访问页面文件名(去掉.php后缀)
|
||||
$currentPage = basename($_SERVER['SCRIPT_NAME'], '.php');
|
||||
|
||||
// 3. 非管理员自动拦截无权限页面(全局统一校验,无需每个页面重复写)
|
||||
if ($isAdmin !== 1 && !in_array($currentPage, $allowPages)) {
|
||||
echo "<script>alert('无访问权限!');history.back();</script>";
|
||||
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】
|
||||
?>
|
||||
|
||||
@@ -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}
|
||||
</div>
|
||||
<div class="sidebar-menu">
|
||||
<div class="menu-title"><?php echo $t['monitor_panel']; ?></div>
|
||||
|
||||
<?php if ($isAdmin === 1 || in_array("dashboard", $allowPages)): ?>
|
||||
<div class="menu-item active" data-page="alertPage">
|
||||
<i class="fa fa-line-chart"></i>
|
||||
<span><?php echo $t['alert_overview']; ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isAdmin === 1 || in_array("disk", $allowPages)): ?>
|
||||
<div class="menu-item" data-page="diskPage">
|
||||
<i class="fa fa-hdd-o"></i>
|
||||
<span><?php echo $t['disk_capacity']; ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isAdmin === 1 || in_array("docker_mgr", $allowPages)): ?>
|
||||
<div class="menu-item" onclick="location.href='docker_mgr.php'">
|
||||
<i class="fa fa-cubes"></i>
|
||||
<span><?php echo $t['docker_mgr']; ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 告警分页:去掉window.open,同页跳转 -->
|
||||
<?php if ($isAdmin === 1 || in_array("history_query", $allowPages)): ?>
|
||||
<div class="menu-item" onclick="location.href='alert_list.php'">
|
||||
<i class="fa fa-list"></i>
|
||||
<span><?php echo $t['alert_list_page']; ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 工单 图标 fa-ticket -->
|
||||
<?php if ($isAdmin === 1 || in_array("work_order", $allowPages)): ?>
|
||||
<div class="menu-item" onclick="location.href='work_order.php'" data-page="workOrderPage">
|
||||
<i class="fa fa-ticket"></i>
|
||||
<span><?php echo $t['work_order_mgr']; ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 后台配置快捷外链自动渲染 -->
|
||||
<?php if (!empty($sidebarLinkList)): ?>
|
||||
<div class="menu-title"><?php echo $t['quick_link']; ?></div>
|
||||
@@ -664,16 +689,23 @@ tbody tr:nth-child(even){background:#fbfcfe}
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="menu-title"><?php echo $t['system_manage']; ?></div>
|
||||
|
||||
<?php if ($isAdmin === 1 || in_array("smtp_config", $allowPages)): ?>
|
||||
<div class="menu-item" onclick="location.href='admin.php'">
|
||||
<i class="fa fa-cog"></i>
|
||||
<span><?php echo $t['system_config']; ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 新增角色管理菜单 -->
|
||||
<?php if ($isAdmin === 1 || in_array("system_manage", $allowPages)): ?>
|
||||
<div class="menu-item" onclick="location.href='system_manage.php'">
|
||||
<i class="fa fa-users"></i>
|
||||
<span><?php echo $t['system_manager']; ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧主内容区 -->
|
||||
@@ -686,7 +718,7 @@ tbody tr:nth-child(even){background:#fbfcfe}
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?>(<?php echo $roleText; ?>)</span>
|
||||
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?>(<?php echo $roleRealName; ?>)</span>
|
||||
<span class="time-info"><i class="fa fa-clock-o"></i><?php echo $t['sys_time']; ?><span id="updateTime">--</span></span>
|
||||
<!-- 语言切换下拉框 -->
|
||||
<div class="lang-box">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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") {
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?></span>
|
||||
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?>(<?php echo $roleRealName; ?>)</span>
|
||||
<span class="time-info"><i class="fa fa-clock-o"></i><span id="nowTime">--</span></span>
|
||||
<div class="lang-box">
|
||||
<span><?php echo $t['lang_text']; ?></span>
|
||||
|
||||
@@ -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容器管理'],
|
||||
];
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
@@ -259,7 +261,7 @@ input[type="number"]{-moz-appearance:textfield;}
|
||||
<i class="fa fa-bell-o"></i>
|
||||
<?php if($unreadMsgCount>0):?><span><?php echo $unreadMsgCount; ?></span><?php endif;?>
|
||||
</span>
|
||||
<span class="user-info"><i class="fa fa-user-circle-o"></i><?php echo $t['current_user']; ?><?php echo $userName; ?></span>
|
||||
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?>(<?php echo $roleRealName; ?>)</span>
|
||||
<span class="time-info"><i class="fa fa-clock-o"></i><span id="nowTime">--</span></span>
|
||||
<div class="lang-box">
|
||||
<select id="langSwitch">
|
||||
|
||||
@@ -4,6 +4,8 @@ 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) {
|
||||
@@ -21,6 +23,8 @@ $_SESSION['login_time'] = time();
|
||||
$userName = htmlspecialchars($_SESSION['username']);
|
||||
$userRoleId = $_SESSION['role_id'];
|
||||
$loginUid = $_SESSION['user_id'];
|
||||
// 修复:auth.php 必须输出 $roleRealName,若无则临时赋值避免报错
|
||||
$roleRealName = $_SESSION['roleRealName'] ?? '';
|
||||
|
||||
// 全局配置库连接
|
||||
$dbConfigHost = "10.150.117.190";
|
||||
@@ -116,7 +120,9 @@ $langMap = [
|
||||
'no_user_data' => '无可用用户',
|
||||
'perm_deny' => '权限不足,禁止访问',
|
||||
'unread_ticket' => '未读工单',
|
||||
'mark_all_read' => '全部标记已读'
|
||||
'mark_all_read' => '全部标记已读',
|
||||
'claim_ticket' => '认领工单',
|
||||
'claim_man' => '认领人'
|
||||
],
|
||||
'en' => [
|
||||
'html_lang' => 'en',
|
||||
@@ -177,7 +183,9 @@ $langMap = [
|
||||
'no_user_data' => 'No available users',
|
||||
'perm_deny' => 'Permission denied',
|
||||
'unread_ticket' => 'Unread Tickets',
|
||||
'mark_all_read' => 'Mark All Read'
|
||||
'mark_all_read' => 'Mark All Read',
|
||||
'claim_ticket' => 'Claim Ticket',
|
||||
'claim_man' => 'Claimer'
|
||||
]
|
||||
];
|
||||
$t = $langMap[$currentLang];
|
||||
@@ -210,7 +218,9 @@ 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 = ?";
|
||||
$roleId = 0;
|
||||
$allowPages = [];
|
||||
$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);
|
||||
if($stmtRole){
|
||||
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
|
||||
@@ -221,10 +231,23 @@ if($stmtRole){
|
||||
if ($roleRow) {
|
||||
$isAdmin = intval($roleRow['is_admin']);
|
||||
$permList = $roleRow['perm_list'];
|
||||
$roleId = intval($roleRow['role_id'] ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 读取当前角色页面权限列表(修复分发下拉判断缺失变量)
|
||||
if ($roleId > 0) {
|
||||
$permSql = "SELECT page_key FROM sys_role_perm 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'];
|
||||
}
|
||||
}
|
||||
|
||||
// 权限校验临时注释,如需开启取消注释
|
||||
//$hasWorkPerm = str_contains($permList, "work_order");
|
||||
//if (!$hasWorkPerm) {
|
||||
@@ -258,22 +281,16 @@ $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 = ?) ";
|
||||
}
|
||||
// 所有人查看全部工单,不再按uid过滤
|
||||
$whereSql = " 1=1 ";
|
||||
$bindType = "ii";
|
||||
$bindData = [$offset, $pageSize];
|
||||
|
||||
// 统计工单总数
|
||||
$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) {
|
||||
@@ -283,21 +300,20 @@ if($stmtCnt){
|
||||
}
|
||||
$totalPage = $pageSize <= 0 ? 1 : ceil($total / $pageSize);
|
||||
|
||||
// 查询工单列表(带出is_read未读标记)
|
||||
$listSql = "SELECT wo.*, uc.real_name create_name, ua.real_name assign_name
|
||||
// 查询工单列表,带出认领人信息
|
||||
$listSql = "SELECT wo.*,
|
||||
uc.real_name create_name,
|
||||
ua.real_name assign_name,
|
||||
uu.real_name claim_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
|
||||
LEFT JOIN sys_user uu ON wo.claim_uid = uu.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_bind_param($stmtList, $bindType, ...$bindData);
|
||||
mysqli_stmt_execute($stmtList);
|
||||
$listResult = mysqli_stmt_get_result($stmtList);
|
||||
if ($listResult instanceof mysqli_result) {
|
||||
@@ -453,7 +469,7 @@ tr.unread-row{background:#fff3f3 !important;}
|
||||
<i class="fa fa-bell-o"></i>
|
||||
<?php if($unreadCount > 0):?><span><?php echo $unreadCount; ?></span><?php endif;?>
|
||||
</span>
|
||||
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?></span>
|
||||
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?>(<?php echo $roleRealName; ?>)</span>
|
||||
<span class="time-info"><i class="fa fa-clock-o"></i><span id="nowTime">--</span></span>
|
||||
<div class="lang-box">
|
||||
<select id="langSwitch">
|
||||
@@ -486,13 +502,14 @@ tr.unread-row{background:#fff3f3 !important;}
|
||||
<th><i class="fa fa-tag"></i><?php echo $t['work_status']; ?></th>
|
||||
<th><i class="fa fa-calendar-o"></i><?php echo $t['create_time']; ?></th>
|
||||
<th><i class="fa fa-clock-o"></i><?php echo $t['update_time']; ?></th>
|
||||
<th><i class="fa fa-user-circle-o"></i><?php echo $t['claim_man']; ?></th>
|
||||
<th>OP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (count($orderList) === 0): ?>
|
||||
<tr>
|
||||
<td colspan="9">
|
||||
<td colspan="10">
|
||||
<div class="empty-tip">
|
||||
<i class="fa fa-ticket"></i>
|
||||
<div><?php echo $t['no_work_data']; ?></div>
|
||||
@@ -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);
|
||||
?>
|
||||
<tr class="<?php echo $rowClass; ?>"
|
||||
data-id="<?php echo $item['id']; ?>"
|
||||
@@ -529,20 +548,33 @@ tr.unread-row{background:#fff3f3 !important;}
|
||||
<td><span class="status-tag <?php echo $statusClass; ?>"><?php echo $statusText; ?></span></td>
|
||||
<td><?php echo $item['create_time']; ?></td>
|
||||
<td><?php echo $item['update_time'] ?? '-'; ?></td>
|
||||
<td style="display:flex;gap:6px;align-items:center;">
|
||||
<?php if ($isAdmin === 1): ?>
|
||||
<!-- 认领人列 -->
|
||||
<td><?php echo $item['claim_name'] ?? '-'; ?></td>
|
||||
<!-- 操作栏 -->
|
||||
<td style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
|
||||
<?php if ($isAdmin === 1 || in_array("system_manage", $allowPages)): ?>
|
||||
<select class="assign-user select-sm">
|
||||
<option value="">--</option>
|
||||
<?php echo $userOptions; ?>
|
||||
</select>
|
||||
<?php endif; ?>
|
||||
<select class="status-select select-sm">
|
||||
|
||||
<?php if (empty($item['claim_uid'])): ?>
|
||||
<button class="btn-base btn-sm btn-success claim-order" data-id="<?php echo $item['id']; ?>">
|
||||
<i class="fa fa-hand-paper-o"></i><?php echo $t['claim_ticket']; ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<select class="status-select select-sm" <?php if(!$canOperate) echo "disabled"; ?>>
|
||||
<option value="1" <?php echo $item['status']==1?'selected':''; ?>><?php echo $t['status_1']; ?></option>
|
||||
<option value="2" <?php echo $item['status']==2?'selected':''; ?>><?php echo $t['status_2']; ?></option>
|
||||
<option value="3" <?php echo $item['status']==3?'selected':''; ?>><?php echo $t['status_3']; ?></option>
|
||||
<option value="4" <?php echo $item['status']==4?'selected':''; ?>><?php echo $t['status_4']; ?></option>
|
||||
</select>
|
||||
<button class="btn-base btn-outline btn-sm edit-order"><i class="fa fa-edit"></i><?php echo $t['work_edit']; ?></button>
|
||||
|
||||
<button class="btn-base btn-outline btn-sm edit-order" <?php if(!$canOperate) echo "disabled"; ?>>
|
||||
<i class="fa fa-edit"></i><?php echo $t['work_edit']; ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
@@ -734,7 +766,7 @@ function loadFiringAlert(selSelector, bindId=""){
|
||||
const sel = $(selSelector);
|
||||
sel.html(`<option value="">-- <?php echo $t['no_alert_option']; ?> --</option>`);
|
||||
list.forEach(item=>{
|
||||
const aid = `${item.alert_name}|${item.instance}`;
|
||||
const aid = `${item.alert_name}_${item.instance}`;
|
||||
sel.append(`<option value="${aid}" data-name="${item.alert_name}" data-ip="${item.instance}">
|
||||
[${item.instance}] ${item.alert_name}
|
||||
</option>`);
|
||||
@@ -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({
|
||||
|
||||
@@ -0,0 +1,955 @@
|
||||
<?php
|
||||
session_start();
|
||||
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) {
|
||||
return $needle === '' || strpos($haystack, $needle) !== false;
|
||||
}
|
||||
}
|
||||
|
||||
$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']);
|
||||
$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 "<script>alert('".$t['perm_deny']."');location.href='dashboard.php';</script>";
|
||||
// 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 .= '<option value="' . $u['uid'] . '">' . $u['real_name'] . '(' . $u['username'] . ')</option>';
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 修复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);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $t['html_lang']; ?>">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo $pageTitle; ?> - Advantest <?php echo $t['work_order_mgr']; ?></title>
|
||||
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
|
||||
<script src="/static/js/echarts.min.js"></script>
|
||||
<script src="/static/js/jquery.min.js"></script>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{background:#f9fafc;font-family:"Inter","PingFang SC","Microsoft YaHei";color:#1d2939;line-height:1.6;display:flex;flex-direction:column;height:100vh;overflow:hidden;}
|
||||
.wrap-main{display:flex;flex:1;overflow:hidden;}
|
||||
.sidebar{width:240px;background:#152c5b;color:#e2e8f0;flex-shrink:0;overflow-y:auto;}
|
||||
.sidebar-brand{padding:26px 24px;border-bottom:1px solid rgba(255,255,255,0.1);}
|
||||
.sidebar-brand-en{font-size:20px;font-weight:700;letter-spacing:1px;color:#fff;}
|
||||
.sidebar-brand-cn{font-size:13px;color:#a5b4fc;margin-top:4px;}
|
||||
.sidebar-menu{padding:16px 0;}
|
||||
.menu-title{padding:10px 24px;font-size:12px;color:#94a3b8;letter-spacing:1px;}
|
||||
.menu-item{display:flex;align-items:center;gap:12px;padding:14px 24px;cursor:pointer;font-size:14px;transition:0.2s;color:#cbd5e1;}
|
||||
.menu-item i{font-size:16px;width:20px;text-align:center;}
|
||||
.menu-item.active{background:rgba(255,255,255,0.12);color:#fff;border-left:4px solid #3b82f6;}
|
||||
.menu-item:hover:not(.active){background:rgba(255,255,255,0.06);color:#fff;}
|
||||
.main-content{flex:1;overflow-y:auto;display:flex;flex-direction:column;}
|
||||
.header{background:#ffffff;border-bottom:1px solid #eef2fb;color:#152c5b;padding:20px 40px;display:flex;justify-content:space-between;align-items:center;box-shadow:0 2px 12px rgba(21,44,91,0.05);flex-shrink:0;}
|
||||
.header-left{display:flex;align-items:center;gap:14px;}
|
||||
.header-brand-en{font-size:24px;font-weight:700;letter-spacing:2px;color:#152c5b;}
|
||||
.header-brand-cn{font-size:15px;color:#64748b;}
|
||||
.header-right{display:flex;align-items:center;gap:18px;font-size:14px;flex-wrap:wrap;}
|
||||
.user-info,.time-info,.lang-box{color:#475569;display:flex;align-items:center;gap:6px;}
|
||||
.lang-box select{padding:4px 8px;border:1px solid #cbd5e1;border-radius:6px;}
|
||||
/* 消息铃铛角标样式,与dashboard统一 */
|
||||
.msg-badge{position:relative;cursor:pointer;}
|
||||
.msg-badge span{position:absolute;top:-5px;right:-5px;background:#ef4444;color:#fff;font-size:10px;border-radius:99px;width:14px;height:14px;text-align:center;line-height:14px;}
|
||||
.btn-base{border:none;padding:9px 20px;border-radius:8px;cursor:pointer;font-size:14px;display:flex;align-items:center;gap:6px;transition:all 0.2s ease;height:38px;}
|
||||
.btn-sm{padding:6px 10px;height:32px;font-size:13px;}
|
||||
.btn-outline{background:#ffffff;color:#152c5b;border:1px solid #cbd5e1;}
|
||||
.btn-outline:hover{background:#f7f8fc;border-color:#152c5b;}
|
||||
.btn-primary{background:#152c5b;color:#fff;}
|
||||
.btn-primary:hover{background:#0f2347;}
|
||||
.btn-success{background:#10b981;color:#fff;}
|
||||
.btn-success:hover{background:#059669;}
|
||||
.mask{position:fixed;left:0;top:0;width:100%;height:100%;background:rgba(255,255,255,0.85);display:none;z-index:1;align-items:center;justify-content:center;}
|
||||
.mask-box{background:#fff;padding:30px 40px;border-radius:14px;font-size:15px;display:flex;gap:14px;align-items:center;box-shadow:0 8px 30px rgba(21,44,91,0.12);border:1px solid #eef2fb;}
|
||||
.loading-spin{display:inline-block;width:20px;height:20px;border:2px solid #e2e8f0;border-top-color:#152c5b;border-radius:50%;animation:spin 0.8s linear infinite;}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
.container{padding:36px 40px;max-width:100%;flex:1;}
|
||||
.panel-card{background:#fff;border-radius:16px;padding:32px;box-shadow:0 3px 18px rgba(21,44,91,0.06);border:1px solid #eef2fb;margin-bottom:36px;}
|
||||
.panel-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:26px;padding-bottom:16px;border-bottom:1px solid #eef2fb;}
|
||||
.panel-title{display:flex;align-items:center;gap:12px;font-size:19px;font-weight:600;color:#152c5b;}
|
||||
.panel-title i{color:#3b82f6;font-size:21px}
|
||||
.table-wrap{overflow-x:auto}
|
||||
table{width:100%;border-collapse:collapse;font-size:14px}
|
||||
th{background:#f9fafc;color:#475569;font-weight:600;padding:16px 18px;text-align:left;border-bottom:2px solid #e2e8f0;white-space:nowrap;}
|
||||
th i{margin-right:6px;font-size:13px;color:#94a3b8}
|
||||
td{padding:18px;border-bottom:1px solid #f1f5f9;color:#334155;vertical-align:middle;}
|
||||
tbody tr{transition:background 0.2s}
|
||||
tbody tr:hover{background:#f7f8fc}
|
||||
tbody tr:nth-child(even){background:#fbfcfe}
|
||||
/* 未读行高亮 */
|
||||
tr.unread-row{background:#fff3f3 !important;}
|
||||
.empty-tip{text-align:center;padding:80px 20px;color:#94a3b8;font-size:15px;}
|
||||
.empty-tip i{font-size:56px;margin-bottom:18px;opacity:0.35;}
|
||||
.modal-mask{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.4);display:none;align-items:center;justify-content:center;z-index:999;pointer-events:none;}
|
||||
.modal-box{width:720px;background:#fff;border-radius:16px;padding:32px;box-shadow:0 8px 30px rgba(21,44,91,0.12);max-height:90vh;overflow-y:auto;pointer-events:auto;}
|
||||
.modal-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:26px;padding-bottom:16px;border-bottom:1px solid #eef2fb;}
|
||||
.modal-close{cursor:pointer;font-size:22px;color:#94a3b8;}
|
||||
.form-row{margin-bottom:20px;}
|
||||
.form-row label{display:block;margin-bottom:8px;color:#475569;font-weight:500;}
|
||||
.form-row input,.form-row select,.form-row textarea{width:100%;padding:10px 14px;border:1px solid #cbd5e1;border-radius:8px;font-size:14px;height:40px;}
|
||||
.form-row textarea{height:140px;resize:none;}
|
||||
.btn-group{display:flex;gap:14px;margin-top:10px;}
|
||||
.select-sm{padding:6px 8px;border:1px solid #cbd5e1;border-radius:6px;font-size:13px;height:32px;}
|
||||
.status-tag{display:inline-flex;align-items:center;gap:5px;padding:6px 14px;border-radius:24px;font-size:13px;font-weight:500;}
|
||||
.tag-pending{background:#ffedd5;color:#ea580c}
|
||||
.tag-processing{background:#dbeafe;color:#2563eb}
|
||||
.tag-finished{background:#dcfce7;color:#16a34a}
|
||||
.tag-closed{background:#f1f5f9;color:#64748b}
|
||||
.pagination{display:flex;justify-content:space-between;align-items:center;margin-top:20px;flex-wrap:wrap;gap:16px;}
|
||||
.page-btn{padding:8px 16px;border:1px solid #cbd5e1;background:#fff;border-radius:8px;cursor:pointer;font-size:14px;min-width:40px;text-align:center;transition:0.2s;}
|
||||
.page-btn:hover{background:#f7f8fc;border-color:#152c5b;}
|
||||
.page-btn.active{background:#152c5b;color:#fff;border-color:#152c5b;}
|
||||
.page-btn:disabled{opacity:0.5;cursor:not-allowed;}
|
||||
.checkbox-wrap{display:flex;gap:16px;flex-wrap:wrap;padding:8px 0;}
|
||||
.checkbox-item{display:flex;align-items:center;gap:6px;padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;background:#fafafa;min-width:130px;}
|
||||
.checkbox-item input{width:auto;height:auto;}
|
||||
.input-readonly{background:#f7f8fc;color:#64748b;}
|
||||
.divider-top{margin:24px 0 8px;padding-top:16px;border-top:1px solid #eef2fb;}
|
||||
@media (max-width:768px){
|
||||
.wrap-main{flex-direction:column;}
|
||||
.sidebar{width:100%;height:auto}
|
||||
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
|
||||
.header-right{gap:8px;flex-wrap:wrap;}
|
||||
.container{padding:12px 14px;}
|
||||
.panel-card{padding:24px}
|
||||
.pagination{flex-direction:column;align-items:flex-start;}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="mask" id="loadingMask">
|
||||
<div class="mask-box"><span class="loading-spin"></span><span></span></div>
|
||||
</div>
|
||||
<div class="wrap-main">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<div class="sidebar-brand-en">Advantest</div>
|
||||
<div class="sidebar-brand-cn"><?php echo $t['page_subtitle']; ?></div>
|
||||
</div>
|
||||
<div class="sidebar-menu">
|
||||
<div class="menu-title"><?php echo $t['monitor_panel']; ?></div>
|
||||
<div class="menu-item" onclick="location.href='dashboard.php'">
|
||||
<i class="fa fa-line-chart"></i><span><?php echo $t['alert_overview']; ?></span>
|
||||
</div>
|
||||
<div class="menu-item active">
|
||||
<i class="fa fa-ticket"></i><span><?php echo $t['work_order_mgr']; ?></span>
|
||||
</div>
|
||||
<?php if (!empty($sidebarLinkList)): ?>
|
||||
<div class="menu-title"><?php echo $t['quick_link']; ?></div>
|
||||
<?php foreach ($sidebarLinkList as $link): ?>
|
||||
<div class="menu-item" onclick="window.open('<?php echo htmlspecialchars($link['url']); ?>','_blank')">
|
||||
<i class="fa fa-external-link"></i><span><?php echo htmlspecialchars($link['name']); ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<div class="menu-title"><?php echo $t['system_manage']; ?></div>
|
||||
<div class="menu-item" onclick="location.href='smtp_config.php'">
|
||||
<i class="fa fa-bell"></i><span><?php echo $t['notify_config']; ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-content">
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<div>
|
||||
<div class="header-brand-en">Advantest</div>
|
||||
<div class="header-brand-cn"><?php echo $t['page_subtitle']; ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<!-- 右上角未读工单铃铛角标,与dashboard统一 -->
|
||||
<span class="msg-badge" onclick="location.href='work_order.php'">
|
||||
<i class="fa fa-bell-o"></i>
|
||||
<?php if($unreadCount > 0):?><span><?php echo $unreadCount; ?></span><?php endif;?>
|
||||
</span>
|
||||
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?>(<?php echo $roleRealName; ?>)</span>
|
||||
<span class="time-info"><i class="fa fa-clock-o"></i><span id="nowTime">--</span></span>
|
||||
<div class="lang-box">
|
||||
<select id="langSwitch">
|
||||
<option value="zh" <?php echo $currentLang==='zh'?'selected':''; ?>>中文</option>
|
||||
<option value="en" <?php echo $currentLang==='en'?'selected':''; ?>>EN</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-base btn-outline" onclick="location.reload()"><i class="fa fa-refresh"></i><?php echo $t['refresh_all']; ?></button>
|
||||
<button class="btn-base btn-primary" onclick="location.href='logout.php'"><i class="fa fa-sign-out"></i><?php echo $t['logout']; ?></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="panel-card">
|
||||
<div class="panel-head">
|
||||
<div class="panel-title"><i class="fa fa-ticket"></i><?php echo $t['all_work_order']; ?></div>
|
||||
<div style="display:flex;gap:12px;align-items:center;">
|
||||
<button class="btn-base btn-outline btn-sm" id="markAllRead"><i class="fa fa-check"></i><?php echo $t['mark_all_read']; ?></button>
|
||||
<button class="btn-base btn-success" id="openCreateModal"><i class="fa fa-plus"></i><?php echo $t['work_create']; ?></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th><i class="fa fa-link"></i><?php echo $t['relate_alert_id']; ?></th>
|
||||
<th><i class="fa fa-file-text-o"></i><?php echo $t['work_title']; ?></th>
|
||||
<th><i class="fa fa-user"></i><?php echo $t['work_creator']; ?></th>
|
||||
<th><i class="fa fa-user-o"></i><?php echo $t['work_handler']; ?></th>
|
||||
<th><i class="fa fa-tag"></i><?php echo $t['work_status']; ?></th>
|
||||
<th><i class="fa fa-calendar-o"></i><?php echo $t['create_time']; ?></th>
|
||||
<th><i class="fa fa-clock-o"></i><?php echo $t['update_time']; ?></th>
|
||||
<th>OP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (count($orderList) === 0): ?>
|
||||
<tr>
|
||||
<td colspan="9">
|
||||
<div class="empty-tip">
|
||||
<i class="fa fa-ticket"></i>
|
||||
<div><?php echo $t['no_work_data']; ?></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($orderList as $item): ?>
|
||||
<?php
|
||||
$statusText = ''; $statusClass = '';
|
||||
switch ($item['status']) {
|
||||
case 1:$statusText=$t['status_1'];$statusClass='tag-pending';break;
|
||||
case 2:$statusText=$t['status_2'];$statusClass='tag-processing';break;
|
||||
case 3:$statusText=$t['status_3'];$statusClass='tag-finished';break;
|
||||
case 4:$statusText=$t['status_4'];$statusClass='tag-closed';break;
|
||||
}
|
||||
$rowClass = $item['is_read'] == 0 && $item['assign_uid'] == $loginUid ? 'unread-row' : '';
|
||||
?>
|
||||
<tr class="<?php echo $rowClass; ?>"
|
||||
data-id="<?php echo $item['id']; ?>"
|
||||
data-title="<?php echo htmlspecialchars($item['title']); ?>"
|
||||
data-content="<?php echo htmlspecialchars($item['content']); ?>"
|
||||
data-assign="<?php echo $item['assign_uid']; ?>"
|
||||
data-status="<?php echo $item['status']; ?>"
|
||||
data-relate="<?php echo htmlspecialchars($item['relate_alert_id'] ?? ''); ?>"
|
||||
data-notifyuser="<?php echo htmlspecialchars($item['notify_user'] ?? ''); ?>"
|
||||
data-notifychannel="<?php echo htmlspecialchars($item['notify_channel'] ?? ''); ?>"
|
||||
data-isread="<?php echo $item['is_read']; ?>">
|
||||
<td><?php echo $item['id']; ?></td>
|
||||
<td><?php echo htmlspecialchars($item['relate_alert_id'] ?? '-'); ?></td>
|
||||
<td><?php echo htmlspecialchars($item['title']); ?></td>
|
||||
<td><?php echo $item['create_name'] ?? '-'; ?></td>
|
||||
<td><?php echo $item['assign_name'] ?? '-'; ?></td>
|
||||
<td><span class="status-tag <?php echo $statusClass; ?>"><?php echo $statusText; ?></span></td>
|
||||
<td><?php echo $item['create_time']; ?></td>
|
||||
<td><?php echo $item['update_time'] ?? '-'; ?></td>
|
||||
<td style="display:flex;gap:6px;align-items:center;">
|
||||
<?php if ($isAdmin === 1 || in_array("system_manage", $allowPages)): ?>
|
||||
<select class="assign-user select-sm">
|
||||
<option value="">--</option>
|
||||
<?php echo $userOptions; ?>
|
||||
</select>
|
||||
<?php endif; ?>
|
||||
<select class="status-select select-sm">
|
||||
<option value="1" <?php echo $item['status']==1?'selected':''; ?>><?php echo $t['status_1']; ?></option>
|
||||
<option value="2" <?php echo $item['status']==2?'selected':''; ?>><?php echo $t['status_2']; ?></option>
|
||||
<option value="3" <?php echo $item['status']==3?'selected':''; ?>><?php echo $t['status_3']; ?></option>
|
||||
<option value="4" <?php echo $item['status']==4?'selected':''; ?>><?php echo $t['status_4']; ?></option>
|
||||
</select>
|
||||
<button class="btn-base btn-outline btn-sm edit-order"><i class="fa fa-edit"></i><?php echo $t['work_edit']; ?></button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<div><?php echo $currentLang==='zh'?'共 ':'Total '; ?><?php echo $total; ?> <?php echo $currentLang==='zh'?'条':'records'; ?></div>
|
||||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
|
||||
<?php if ($page > 1): ?>
|
||||
<button class="page-btn" onclick="location.href='work_order.php?page=1'"><?php echo $currentLang==='zh'?'首页':'First' ?></button>
|
||||
<button class="page-btn" onclick="location.href='work_order.php?page=<?php echo $page-1; ?>'"><?php echo $currentLang==='zh'?'上一页':'Prev' ?></button>
|
||||
<?php else: ?>
|
||||
<button class="page-btn" disabled><?php echo $currentLang==='zh'?'首页':'First' ?></button>
|
||||
<button class="page-btn" disabled><?php echo $currentLang==='zh'?'上一页':'Prev' ?></button>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$start = max(1, $page - 2);
|
||||
$end = min($totalPage, $page + 2);
|
||||
for ($i = $start; $i <= $end; $i++):
|
||||
?>
|
||||
<button class="page-btn <?php echo $i == $page ? 'active' : ''; ?>" onclick="location.href='work_order.php?page=<?php echo $i; ?>'"><?php echo $i; ?></button>
|
||||
<?php endfor; ?>
|
||||
<?php if ($page < $totalPage): ?>
|
||||
<button class="page-btn" onclick="location.href='work_order.php?page=<?php echo $page+1; ?>'"><?php echo $currentLang==='zh'?'下一页':'Next' ?></button>
|
||||
<button class="page-btn" onclick="location.href='work_order.php?page=<?php echo $totalPage; ?>'"><?php echo $currentLang==='zh'?'末页':'Last' ?></button>
|
||||
<?php else: ?>
|
||||
<button class="page-btn" disabled><?php echo $currentLang==='zh'?'下一页':'Next' ?></button>
|
||||
<button class="page-btn" disabled><?php echo $currentLang==='zh'?'末页':'Last' ?></button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 新建工单弹窗 -->
|
||||
<div class="modal-mask" id="createModal">
|
||||
<div class="modal-box">
|
||||
<div class="modal-head">
|
||||
<div class="panel-title"><i class="fa fa-plus"></i><?php echo $t['work_create']; ?></div>
|
||||
<span class="modal-close" id="closeCreateModal">×</span>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label><?php echo $t['relate_alert']; ?></label>
|
||||
<select id="relateAlertSelect">
|
||||
<option value="">-- <?php echo $t['no_alert_option']; ?> --</option>
|
||||
</select>
|
||||
<div class="form-desc"><?php echo $currentLang==='zh'?'选择后自动填充工单标题与告警基础信息':'Auto fill title and alert info after select'; ?></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label><?php echo $t['work_title']; ?></label>
|
||||
<input type="text" id="orderTitle" placeholder="<?php echo $t['title_placeholder']; ?>">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label><?php echo $t['work_content']; ?></label>
|
||||
<textarea id="orderContent" placeholder="<?php echo $t['content_placeholder']; ?>"></textarea>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label><?php echo $t['work_handler']; ?></label>
|
||||
<select id="orderAssign">
|
||||
<option value=""><?php echo $t['select_handler']; ?></option>
|
||||
<?php echo $userOptions; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="divider-top"></div>
|
||||
<div class="panel-title" style="font-size:16px;margin-bottom:12px;"><i class="fa fa-bell"></i><?php echo $t['notify_title']; ?></div>
|
||||
<div class="form-row">
|
||||
<label><?php echo $t['notify_user']; ?></label>
|
||||
<select id="notifyUserSelect" multiple style="height:120px;">
|
||||
<?php echo $userOptions; ?>
|
||||
</select>
|
||||
<div class="form-desc"><?php echo $currentLang==='zh'?'按住Ctrl多选通知人员':'Hold Ctrl multi-select users'; ?></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label><?php echo $t['notify_channel']; ?></label>
|
||||
<div class="checkbox-wrap">
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" id="chMail" value="mail">
|
||||
<label for="chMail"><i class="fa fa-envelope-o"></i> <?php echo $t['channel_mail']; ?></label>
|
||||
</div>
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" id="chDing" value="dingtalk">
|
||||
<label for="chDing"><i class="fa fa-comment"></i> <?php echo $t['channel_ding']; ?></label>
|
||||
</div>
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" id="chWecom" value="wecom">
|
||||
<label for="chWecom"><i class="fa fa-wechat"></i> <?php echo $t['channel_wecom']; ?></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn-base btn-primary" id="submitOrder"><i class="fa fa-save"></i><?php echo $t['save']; ?></button>
|
||||
<button class="btn-base btn-outline" id="resetForm"><i class="fa fa-refresh"></i><?php echo $t['cancel']; ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑工单弹窗 -->
|
||||
<div class="modal-mask" id="editModal">
|
||||
<div class="modal-box">
|
||||
<div class="modal-head">
|
||||
<div class="panel-title"><i class="fa fa-edit"></i><?php echo $t['work_edit']; ?></div>
|
||||
<span class="modal-close" id="closeEditModal">×</span>
|
||||
</div>
|
||||
<input type="hidden" id="editOrderId">
|
||||
<input type="hidden" id="editRelateAlertId">
|
||||
<div class="form-row">
|
||||
<label><?php echo $t['relate_alert']; ?></label>
|
||||
<select id="editRelateAlertSelect" style="width:100%;">
|
||||
<option value="">-- <?php echo $t['no_alert_option']; ?> --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>当前绑定告警ID(只读)</label>
|
||||
<input type="text" id="editRelateAlertShow" readonly class="input-readonly">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label><?php echo $t['work_title']; ?></label>
|
||||
<input type="text" id="editTitle">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label><?php echo $t['work_content']; ?></label>
|
||||
<textarea id="editContent"></textarea>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label><?php echo $t['work_handler']; ?></label>
|
||||
<select id="editAssign">
|
||||
<option value="">--</option>
|
||||
<?php echo $userOptions; ?>
|
||||
</select>
|
||||
<script>$("select[name=role_id]").val(<?php echo $editUser['role_id']; ?>)</script>
|
||||
</div>
|
||||
<div class="divider-top"></div>
|
||||
<div class="panel-title" style="font-size:16px;margin-bottom:12px;"><i class="fa fa-bell"></i><?php echo $t['notify_title']; ?></div>
|
||||
<div class="form-row">
|
||||
<label><?php echo $t['notify_user']; ?></label>
|
||||
<select id="editNotifyUser" multiple style="height:120px;">
|
||||
<?php echo $userOptions; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label><?php echo $t['notify_channel']; ?></label>
|
||||
<div class="checkbox-wrap">
|
||||
<div class="checkbox-item"><input type="checkbox" id="editChMail" value="mail"><label for="editChMail"><i class="fa fa-envelope-o"></i> <?php echo $t['channel_mail']; ?></label></div>
|
||||
<div class="checkbox-item"><input type="checkbox" id="editChDing" value="dingtalk"><label for="editChDing"><i class="fa fa-comment"></i> <?php echo $t['channel_ding']; ?></label></div>
|
||||
<div class="checkbox-item"><input type="checkbox" id="editChWecom" value="wecom"><label for="editChWecom"><i class="fa fa-wechat"></i> <?php echo $t['channel_wecom']; ?></label></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn-base btn-primary" id="saveEdit"><i class="fa fa-save"></i><?php echo $t['save']; ?></button>
|
||||
<button class="btn-base btn-outline" id="cancelEdit"><?php echo $t['cancel']; ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const apiAlert = "./api/index.php";
|
||||
const apiOrder = "./api/work_order_api.php";
|
||||
const loginUid = "<?php echo $loginUid; ?>";
|
||||
const isAdmin = <?php echo $isAdmin; ?>;
|
||||
const currentLang = "<?php echo $currentLang; ?>";
|
||||
let activeAlertList = [];
|
||||
|
||||
$("#loadingMask").hide();
|
||||
|
||||
function updateNowTime() {
|
||||
const d = new Date();
|
||||
const Y = d.getFullYear();
|
||||
const M = String(d.getMonth()+1).padStart(2,'0');
|
||||
const D = String(d.getDate()).padStart(2,'0');
|
||||
const h = String(d.getHours()).padStart(2,'0');
|
||||
const m = String(d.getMinutes()).padStart(2,'0');
|
||||
const s = String(d.getSeconds()).padStart(2,'0');
|
||||
$("#nowTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
|
||||
}
|
||||
setInterval(updateNowTime,1000);
|
||||
updateNowTime();
|
||||
|
||||
$("#langSwitch").change(function(){
|
||||
const lang = $(this).val();
|
||||
$.post(apiOrder, {action:"save_lang",lang:lang}, ()=>location.reload());
|
||||
})
|
||||
|
||||
function loadFiringAlert(selSelector, bindId=""){
|
||||
$.getJSON(apiAlert+"?act=active_firing", function(list){
|
||||
activeAlertList = list;
|
||||
const sel = $(selSelector);
|
||||
sel.html(`<option value="">-- <?php echo $t['no_alert_option']; ?> --</option>`);
|
||||
list.forEach(item=>{
|
||||
const aid = `${item.alert_name}_${item.instance}`;
|
||||
sel.append(`<option value="${aid}" data-name="${item.alert_name}" data-ip="${item.instance}">
|
||||
[${item.instance}] ${item.alert_name}
|
||||
</option>`);
|
||||
})
|
||||
if(bindId) sel.val(bindId);
|
||||
}).fail(()=>{
|
||||
$(selSelector).html(`<option value=""><?php echo $t['no_alert_data']; ?></option>`);
|
||||
})
|
||||
}
|
||||
|
||||
$("#relateAlertSelect").change(function(){
|
||||
const opt = $(this).find("option:selected");
|
||||
const aid = opt.val();
|
||||
const aname = opt.data("name");
|
||||
const ip = opt.data("ip");
|
||||
if(!aid){
|
||||
$("#orderTitle").val("");
|
||||
$("#orderContent").val("");
|
||||
return;
|
||||
}
|
||||
const tip = currentLang === "zh"
|
||||
? `关联告警ID:${aid}\n实例IP:${ip}\n告警名称:${aname}\n=====故障处理备注=====\n`
|
||||
: `Relate Alert ID: ${aid}\nInstance IP: ${ip}\nAlert Name: ${aname}\n=====Handling Remark=====\n`;
|
||||
$("#orderTitle").val(aname);
|
||||
$("#orderContent").val(tip);
|
||||
});
|
||||
|
||||
$("#openCreateModal").on("click", function(){
|
||||
$("#createModal").css("display","flex");
|
||||
loadFiringAlert("#relateAlertSelect");
|
||||
});
|
||||
|
||||
$("#closeCreateModal,#resetForm").click(function(){
|
||||
$("#relateAlertSelect").val("");
|
||||
$("#orderTitle").val("");
|
||||
$("#orderContent").val("");
|
||||
$("#orderAssign").val("");
|
||||
$("#notifyUserSelect").val([]);
|
||||
$("#chMail,#chDing,#chWecom").prop("checked",false);
|
||||
$("#createModal").hide();
|
||||
});
|
||||
|
||||
$("#closeEditModal,#cancelEdit").click(function(){
|
||||
$("#editModal").hide();
|
||||
});
|
||||
|
||||
$(document).on("click", ".edit-order", function(){
|
||||
const tr = $(this).closest("tr");
|
||||
$("#editOrderId").val(tr.data("id"));
|
||||
$("#editTitle").val(tr.data("title"));
|
||||
$("#editContent").val(tr.data("content"));
|
||||
$("#editAssign").val(tr.data("assign"));
|
||||
const relateAid = tr.data("relate") || "";
|
||||
$("#editRelateAlertId").val(relateAid);
|
||||
$("#editRelateAlertShow").val(relateAid || (currentLang==="zh"?"无关联告警":"No related alert"));
|
||||
const userRaw = tr.data("notifyuser") || "";
|
||||
const userArr = userRaw.split(",").filter(x=>x);
|
||||
$("#editNotifyUser").val(userArr);
|
||||
const chRaw = tr.data("notifychannel") || "";
|
||||
const chArr = chRaw.split(",").filter(x=>x);
|
||||
$("#editChMail").prop("checked", chArr.includes("mail"));
|
||||
$("#editChDing").prop("checked", chArr.includes("dingtalk"));
|
||||
$("#editChWecom").prop("checked", chArr.includes("wecom"));
|
||||
loadFiringAlert("#editRelateAlertSelect", relateAid);
|
||||
$("#editModal").css("display","flex");
|
||||
});
|
||||
$("#submitOrder").click(function(){
|
||||
const title = $("#orderTitle").val().trim();
|
||||
const content = $("#orderContent").val().trim();
|
||||
const assign = $("#orderAssign").val();
|
||||
const relateAlertId = $("#relateAlertSelect").val();
|
||||
const notifyUserArr = $("#notifyUserSelect").val() || [];
|
||||
const notifyUser = notifyUserArr.join(",");
|
||||
let channelArr = [];
|
||||
if($("#chMail").prop("checked")) channelArr.push("mail");
|
||||
if($("#chDing").prop("checked")) channelArr.push("dingtalk");
|
||||
if($("#chWecom").prop("checked")) channelArr.push("wecom");
|
||||
const notifyChannel = channelArr.join(",");
|
||||
if(!title) return alert(currentLang==='zh'?"请填写工单标题":"Fill order title");
|
||||
if(!content) return alert(currentLang==='zh'?"请填写工单内容":"Fill order content");
|
||||
$.ajax({
|
||||
url: apiOrder + "?action=create",
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({
|
||||
title,
|
||||
content,
|
||||
assign_uid: assign,
|
||||
relate_alert_id: relateAlertId,
|
||||
notify_user: notifyUser,
|
||||
notify_channel: notifyChannel
|
||||
}),
|
||||
dataType: "json",
|
||||
success(res){
|
||||
alert(res.msg);
|
||||
if(res.code === 0) location.reload();
|
||||
},
|
||||
error(){
|
||||
alert(currentLang==='zh'?"提交失败,接口异常":"Submit failed, api error")
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// 编辑保存
|
||||
$("#saveEdit").click(function(){
|
||||
const id = $("#editOrderId").val();
|
||||
const title = $("#editTitle").val().trim();
|
||||
const content = $("#editContent").val().trim();
|
||||
const assign = $("#editAssign").val();
|
||||
const newRelateAid = $("#editRelateAlertSelect").val();
|
||||
const notifyUserArr = $("#editNotifyUser").val() || [];
|
||||
const notifyUser = notifyUserArr.join(",");
|
||||
let channelArr = [];
|
||||
if($("#editChMail").prop("checked")) channelArr.push("mail");
|
||||
if($("#editChDing").prop("checked")) channelArr.push("dingtalk");
|
||||
if($("#editChWecom").prop("checked")) channelArr.push("wecom");
|
||||
const notifyChannel = channelArr.join(",");
|
||||
if(!title || !content) return alert(currentLang==='zh'?"标题和内容不能为空":"Title and content required");
|
||||
$.ajax({
|
||||
url: apiOrder + "?action=edit",
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({
|
||||
id: id,
|
||||
title: title,
|
||||
content: content,
|
||||
assign_uid: assign,
|
||||
relate_alert_id: newRelateAid,
|
||||
notify_user: notifyUser,
|
||||
notify_channel: notifyChannel
|
||||
}),
|
||||
dataType: "json",
|
||||
success(res){
|
||||
alert(res.msg);
|
||||
if(res.code === 0) location.reload();
|
||||
},
|
||||
error(){
|
||||
alert(currentLang==='zh'?"保存失败,接口请求异常":"Save failed, api request error")
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 修改工单状态
|
||||
$(document).on("change", ".status-select", function(){
|
||||
const tr = $(this).closest("tr");
|
||||
const orderId = tr.data("id");
|
||||
const status = $(this).val();
|
||||
$.ajax({
|
||||
url: apiOrder + "?action=update_status",
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({id:orderId,status:status}),
|
||||
dataType: "json",
|
||||
success(res){
|
||||
alert(res.msg);
|
||||
if(res.code === 0) location.reload();
|
||||
},
|
||||
error(){
|
||||
alert(currentLang==='zh'?"状态更新失败":"Status update failed")
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 管理员分发工单(修复只修改当前行单条ID,不再匹配标题批量更新)
|
||||
$(document).on("change", ".assign-user", function(){
|
||||
const tr = $(this).closest("tr");
|
||||
const orderId = tr.data("id");
|
||||
const assignUid = $(this).val();
|
||||
if(!assignUid) return;
|
||||
$.ajax({
|
||||
url: apiOrder + "?action=assign",
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({id:orderId,assign_uid:assignUid}),
|
||||
dataType: "json",
|
||||
success(res){
|
||||
alert(res.msg);
|
||||
if(res.code === 0) location.reload();
|
||||
},
|
||||
error(){
|
||||
alert(currentLang==='zh'?"工单分发失败":"Assign order failed")
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 一键标记全部当前用户待处理工单为已读
|
||||
$("#markAllRead").click(function(){
|
||||
$.ajax({
|
||||
url: apiOrder + "?action=mark_all_read",
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({uid:loginUid}),
|
||||
dataType: "json",
|
||||
success(res){
|
||||
alert(res.msg);
|
||||
if(res.code === 0) location.reload();
|
||||
},
|
||||
error(){
|
||||
alert(currentLang==='zh'?"标记已读失败":"Mark read failed")
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// 点击弹窗空白区域关闭弹窗
|
||||
$(".modal-mask").click(function(e){
|
||||
if($(e.target).is(".modal-mask")) $(this).hide();
|
||||
});
|
||||
|
||||
// 页面初始化加载告警下拉数据
|
||||
$(function(){
|
||||
loadFiringAlert("#relateAlertSelect");
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user