更改
This commit is contained in:
parent
28cb40ad28
commit
2110411712
@ -28,12 +28,14 @@
|
||||

|
||||
|
||||
#### Web端
|
||||
部署到服务器上,分配域名或直接IP访问即可
|
||||
将web目录部分部署到服务器上,分配域名或直接IP访问即可
|
||||
|
||||
首次使用会进入配置页,依次输入API_PASS,ACCOUNT,API_KEY(其中API_PASS是您自定义的网站访问密码,支持大小写字母以及数字)
|
||||
|
||||
随后访问`http://example.com/?pass=API_PASS`即可
|
||||
|
||||
切记:切勿将app部分放置于网站目录下
|
||||
|
||||
#### 监控端
|
||||
|
||||
**安装:**
|
||||
|
||||
191
app/install.sh
Normal file
191
app/install.sh
Normal file
@ -0,0 +1,191 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 核云IDC服务商VPS自动监测重启程序 - 安装脚本
|
||||
# 该脚本会创建一个systemd服务,持续化运行当前目录下的python main.py命令
|
||||
|
||||
# 获取当前脚本所在目录的绝对路径
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# 定义服务名称
|
||||
SERVICE_NAME="idc-monitor"
|
||||
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||||
|
||||
echo "正在安装 ${SERVICE_NAME} 服务..."
|
||||
|
||||
# 检查是否以root权限运行
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "错误: 请以root权限运行此脚本 (sudo ./install.sh)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查Python是否已安装
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
if command -v python &> /dev/null; then
|
||||
PYTHON_CMD="python"
|
||||
else
|
||||
echo "错误: 未找到Python,请先安装Python"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
PYTHON_CMD="python3"
|
||||
fi
|
||||
|
||||
echo "检测到Python: ${PYTHON_CMD}"
|
||||
|
||||
# 检查并安装pip
|
||||
install_pip() {
|
||||
echo "正在安装pip..."
|
||||
|
||||
# 尝试使用系统包管理器安装pip
|
||||
if command -v apt-get &> /dev/null; then
|
||||
apt-get update && apt-get install -y python3-pip
|
||||
PIP_CMD="pip3"
|
||||
elif command -v yum &> /dev/null; then
|
||||
yum install -y python3-pip
|
||||
PIP_CMD="pip3"
|
||||
elif command -v dnf &> /dev/null; then
|
||||
dnf install -y python3-pip
|
||||
PIP_CMD="pip3"
|
||||
else
|
||||
# 如果包管理器不可用,使用get-pip.py
|
||||
echo "未检测到常用包管理器,尝试使用get-pip.py安装..."
|
||||
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
|
||||
${PYTHON_CMD} get-pip.py
|
||||
rm -f get-pip.py
|
||||
PIP_CMD="pip"
|
||||
fi
|
||||
|
||||
# 验证pip安装
|
||||
if command -v pip3 &> /dev/null; then
|
||||
PIP_CMD="pip3"
|
||||
elif command -v pip &> /dev/null; then
|
||||
PIP_CMD="pip"
|
||||
else
|
||||
echo "错误: pip安装失败"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "✅ pip安装成功: ${PIP_CMD}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 检查pip是否存在
|
||||
if command -v pip3 &> /dev/null; then
|
||||
PIP_CMD="pip3"
|
||||
echo "检测到pip: ${PIP_CMD}"
|
||||
elif command -v pip &> /dev/null; then
|
||||
PIP_CMD="pip"
|
||||
echo "检测到pip: ${PIP_CMD}"
|
||||
else
|
||||
echo "警告: 未检测到pip"
|
||||
read -p "是否现在安装pip?(Y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Nn]$ ]]; then
|
||||
echo "跳过pip安装"
|
||||
PIP_CMD=""
|
||||
else
|
||||
if ! install_pip; then
|
||||
echo "错误: pip安装失败,无法继续"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# 安装Python依赖
|
||||
if [ -f "${PROJECT_DIR}/requirements.txt" ]; then
|
||||
if [ -n "${PIP_CMD}" ]; then
|
||||
echo "正在安装Python依赖包..."
|
||||
cd ${PROJECT_DIR}
|
||||
if ${PIP_CMD} install -r requirements.txt; then
|
||||
echo "✅ Python依赖包安装成功"
|
||||
else
|
||||
echo "❌ Python依赖包安装失败"
|
||||
read -p "是否继续安装服务?(y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "安装已取消"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "警告: 跳过依赖包安装(pip不可用)"
|
||||
fi
|
||||
else
|
||||
echo "警告: ${PROJECT_DIR}/requirements.txt 文件不存在,跳过依赖安装"
|
||||
fi
|
||||
|
||||
# 检查main.py是否存在
|
||||
if [ ! -f "${PROJECT_DIR}/main.py" ]; then
|
||||
echo "警告: ${PROJECT_DIR}/main.py 文件不存在"
|
||||
echo "请确保main.py文件位于项目根目录下"
|
||||
read -p "是否继续安装?(y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "安装已取消"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ... existing code ...
|
||||
|
||||
# 创建systemd服务文件
|
||||
cat > ${SERVICE_FILE} << EOF
|
||||
[Unit]
|
||||
Description=Heyun IDC Monitor Service
|
||||
After=network.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=${PROJECT_DIR}
|
||||
ExecStart=${PYTHON_CMD} ${PROJECT_DIR}/main.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=${SERVICE_NAME}
|
||||
|
||||
# 资源限制
|
||||
LimitNOFILE=65536
|
||||
|
||||
# 安全设置
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=${PROJECT_DIR}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# ... existing code ...
|
||||
|
||||
# 重新加载systemd配置
|
||||
systemctl daemon-reload
|
||||
|
||||
# 启用服务(开机自启)
|
||||
systemctl enable ${SERVICE_NAME}
|
||||
|
||||
# 启动服务
|
||||
systemctl start ${SERVICE_NAME}
|
||||
|
||||
# 检查服务状态
|
||||
if systemctl is-active --quiet ${SERVICE_NAME}; then
|
||||
echo "✅ ${SERVICE_NAME} 服务已成功安装并启动"
|
||||
echo "服务状态: 运行中"
|
||||
else
|
||||
echo "❌ ${SERVICE_NAME} 服务启动失败"
|
||||
echo "请检查日志: journalctl -u ${SERVICE_NAME} -f"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "常用命令:"
|
||||
echo " 查看服务状态: systemctl status ${SERVICE_NAME}"
|
||||
echo " 查看实时日志: journalctl -u ${SERVICE_NAME} -f"
|
||||
echo " 停止服务: systemctl stop ${SERVICE_NAME}"
|
||||
echo " 重启服务: systemctl restart ${SERVICE_NAME}"
|
||||
echo " 卸载服务: ./uninstall.sh"
|
||||
echo ""
|
||||
echo "安装完成!"
|
||||
0
app/monitor.py
Normal file
0
app/monitor.py
Normal file
72
app/uninstall.sh
Normal file
72
app/uninstall.sh
Normal file
@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 核云IDC服务商VPS自动监测重启程序 - 卸载脚本
|
||||
# 该脚本会移除systemd服务并清理相关文件
|
||||
|
||||
# 定义服务名称
|
||||
SERVICE_NAME="idc-monitor"
|
||||
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||||
|
||||
echo "正在卸载 ${SERVICE_NAME} 服务..."
|
||||
|
||||
# 检查是否以root权限运行
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "错误: 请以root权限运行此脚本 (sudo ./uninstall.sh)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查服务是否存在
|
||||
if [ ! -f "${SERVICE_FILE}" ]; then
|
||||
echo "警告: 服务文件 ${SERVICE_FILE} 不存在"
|
||||
echo "可能服务未安装或已被卸载"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 停止服务
|
||||
echo "正在停止服务..."
|
||||
if systemctl is-active --quiet ${SERVICE_NAME}; then
|
||||
systemctl stop ${SERVICE_NAME}
|
||||
echo "✅ 服务已停止"
|
||||
else
|
||||
echo "服务未运行,跳过停止步骤"
|
||||
fi
|
||||
|
||||
# 禁用服务(取消开机自启)
|
||||
echo "正在禁用服务..."
|
||||
if systemctl is-enabled --quiet ${SERVICE_NAME} 2>/dev/null; then
|
||||
systemctl disable ${SERVICE_NAME}
|
||||
echo "✅ 服务已禁用"
|
||||
else
|
||||
echo "服务未启用,跳过禁用步骤"
|
||||
fi
|
||||
|
||||
# 重新加载systemd配置
|
||||
echo "正在清理systemd配置..."
|
||||
systemctl daemon-reload
|
||||
systemctl reset-failed ${SERVICE_NAME} 2>/dev/null
|
||||
|
||||
# 删除服务文件
|
||||
echo "正在删除服务文件..."
|
||||
rm -f ${SERVICE_FILE}
|
||||
echo "✅ 服务文件已删除: ${SERVICE_FILE}"
|
||||
|
||||
# 清理journal日志(可选)
|
||||
echo ""
|
||||
read -p "是否同时清理该服务的历史日志?(y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
journalctl --rotate
|
||||
journalctl --vacuum-time=1s 2>/dev/null
|
||||
echo "✅ 日志已清理"
|
||||
else
|
||||
echo "跳过日志清理"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ ${SERVICE_NAME} 服务已成功卸载"
|
||||
echo ""
|
||||
echo "注意:"
|
||||
echo " - Python依赖包未被卸载,如需清理请手动执行: pip uninstall -r requirements.txt"
|
||||
echo " - 项目文件未被删除,如需删除请手动清理项目目录"
|
||||
echo ""
|
||||
echo "卸载完成!"
|
||||
494
index.php
494
index.php
@ -1,494 +0,0 @@
|
||||
<?php
|
||||
// ==================== 全局配置区域 ====================
|
||||
define('CONFIG_FILE', __DIR__ . '/config.php'); // 配置文件路径
|
||||
define('TOKEN_CACHE_FILE', __DIR__ . '/token_cache.php'); // Token缓存文件路径
|
||||
define('TOKEN_EXPIRE_TIME', 3600); // Token过期时间(秒),默认1小时
|
||||
|
||||
// 加载配置文件
|
||||
if (file_exists(CONFIG_FILE)) {
|
||||
require_once CONFIG_FILE;
|
||||
}
|
||||
|
||||
// 检查是否需要显示配置页面
|
||||
$needConfig = !defined('API_PASS') || !defined('ACCOUNT') || !defined('API_KEY') ||
|
||||
empty(API_PASS) || empty(ACCOUNT) || empty(API_KEY);
|
||||
|
||||
// 处理配置提交
|
||||
if ($needConfig && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['setup_config'])) {
|
||||
$apiPass = trim($_POST['api_pass']);
|
||||
$account = trim($_POST['account']);
|
||||
$apiKey = trim($_POST['api_key']);
|
||||
|
||||
if (empty($apiPass) || empty($account) || empty($apiKey)) {
|
||||
$configError = '所有字段都不能为空!';
|
||||
} else {
|
||||
// 生成配置文件内容
|
||||
$configContent = "<?php\n";
|
||||
$configContent .= "// 核云IDC VPS管理面板配置文件\n";
|
||||
$configContent .= "// 生成时间: " . date('Y-m-d H:i:s') . "\n\n";
|
||||
$configContent .= "define('API_PASS', '" . addslashes($apiPass) . "'); // API访问密码\n";
|
||||
$configContent .= "define('ACCOUNT', '" . addslashes($account) . "'); // 核云IDC账号(手机号或邮箱)\n";
|
||||
$configContent .= "define('API_KEY', '" . addslashes($apiKey) . "'); // 核云IDC API KEY\n";
|
||||
$configContent .= "define('BASE_URL', 'https://www.heyunidc.cn/v1'); // API基础URL\n";
|
||||
|
||||
// 写入配置文件
|
||||
if (file_put_contents(CONFIG_FILE, $configContent)) {
|
||||
// 重新加载配置
|
||||
require_once CONFIG_FILE;
|
||||
$needConfig = false;
|
||||
} else {
|
||||
$configError = '配置文件写入失败,请检查目录权限!';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果仍需配置,显示配置页面
|
||||
if ($needConfig) {
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="./static/favicon.ico" type="image/x-icon">
|
||||
<link href="./static/initial.css" rel="stylesheet" type="text/css">
|
||||
<title>初始配置 - VPS管理面板</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="config-container">
|
||||
<div class="config-header">
|
||||
<h1>🔧 初始配置</h1>
|
||||
<p>请填写以下信息以开始使用</p>
|
||||
</div>
|
||||
|
||||
<?php if (isset($configError)): ?>
|
||||
<div class="error-message">
|
||||
❌ <?php echo htmlspecialchars($configError); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="info-box">
|
||||
💡 提示:这些信息将保存在 <code>config.php</code> 文件中,请妥善保管。<br>
|
||||
若您需要重置或更改配置,请删除 <code>config.php</code> 文件并重新运行。<br>
|
||||
配置完成后请以该格式访问服务:example.com/?pass=API_PASS
|
||||
</div>
|
||||
|
||||
<form method="POST" onsubmit="return validatePassword()">
|
||||
<div class="form-group">
|
||||
<label for="api_pass">访问密码 (API_PASS)</label>
|
||||
<input type="text" id="api_pass" name="api_pass" required placeholder="设置一个安全的访问密码" pattern="[a-zA-Z0-9]+" title="只允许字母和数字">
|
||||
<div class="help-text">用于保护管理面板的访问权限<br>可选:字母(a-z,A-Z)和数字(0-9)</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="account">核云IDC账号 (ACCOUNT)</label>
|
||||
<input type="text" id="account" name="account" required placeholder="手机号或邮箱">
|
||||
<div class="help-text">您的核云IDC登录账号</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="api_key">API密钥 (API_KEY)</label>
|
||||
<input type="password" id="api_key" name="api_key" required placeholder="输入API KEY">
|
||||
<div class="help-text">在核云IDC控制台获取的API密钥</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" name="setup_config" class="btn-submit">
|
||||
✅ 保存配置并开始使用
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function validatePassword() {
|
||||
var password = document.getElementById('api_pass').value;
|
||||
var pattern = /^[a-zA-Z0-9]+$/;
|
||||
|
||||
if (!pattern.test(password)) {
|
||||
alert('格式不正确!\n\n密码仅允许包含:\n• 字母 (a-z, A-Z)\n• 数字 (0-9)\n\n请勿使用特殊符号或空格');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
alert('至少输入6个字符!');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
exit;
|
||||
}
|
||||
|
||||
// ==================== Token管理函数 ====================
|
||||
|
||||
/**
|
||||
* 从PHP缓存文件读取Token
|
||||
*/
|
||||
function getCachedToken() {
|
||||
if (!file_exists(TOKEN_CACHE_FILE)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 包含文件获取变量
|
||||
include TOKEN_CACHE_FILE;
|
||||
|
||||
if (!isset($cached_token) || !isset($cached_timestamp)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查Token是否过期(1小时内有效)
|
||||
$currentTime = time();
|
||||
$tokenAge = $currentTime - $cached_timestamp;
|
||||
|
||||
if ($tokenAge < TOKEN_EXPIRE_TIME) {
|
||||
return $cached_token;
|
||||
}
|
||||
|
||||
// Token已过期,清除缓存
|
||||
@unlink(TOKEN_CACHE_FILE);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存Token到PHP缓存文件
|
||||
*/
|
||||
function saveTokenToCache($token) {
|
||||
$timestamp = time();
|
||||
$expireAt = date('Y-m-d H:i:s', $timestamp + TOKEN_EXPIRE_TIME);
|
||||
|
||||
$content = "<?php\n";
|
||||
$content .= "// Token缓存文件 - 自动生成\n";
|
||||
$content .= "// 生成时间: " . date('Y-m-d H:i:s', $timestamp) . "\n";
|
||||
$content .= "// 过期时间: {$expireAt}\n";
|
||||
$content .= "// 警告: 不要手动修改此文件\n\n";
|
||||
$content .= "\$cached_token = '" . addslashes($token) . "';\n";
|
||||
$content .= "\$cached_timestamp = {$timestamp};\n";
|
||||
|
||||
file_put_contents(TOKEN_CACHE_FILE, $content);
|
||||
|
||||
// 设置文件权限(如果可能)
|
||||
if (function_exists('chmod')) {
|
||||
@chmod(TOKEN_CACHE_FILE, 0600);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录Token(带缓存)
|
||||
*/
|
||||
function getLoginToken() {
|
||||
// 首先尝试从缓存获取
|
||||
$cachedToken = getCachedToken();
|
||||
if ($cachedToken) {
|
||||
return $cachedToken;
|
||||
}
|
||||
|
||||
// 缓存不存在或已过期,重新获取Token
|
||||
$url = BASE_URL . '/login_api';
|
||||
$data = [
|
||||
'account' => ACCOUNT,
|
||||
'password' => API_KEY
|
||||
];
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = json_decode($response, true);
|
||||
$token = isset($result['jwt']) ? $result['jwt'] : null;
|
||||
|
||||
// 保存新Token到缓存
|
||||
if ($token) {
|
||||
saveTokenToCache($token);
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
// ==================== 功能实现区域 ====================
|
||||
|
||||
/**
|
||||
* 发送API请求
|
||||
*/
|
||||
function sendApiRequest($endpoint, $method = 'GET', $data = []) {
|
||||
$token = getLoginToken();
|
||||
if (!$token) {
|
||||
return ['status' => 500, 'msg' => '获取Token失败,请检查账号和密码配置'];
|
||||
}
|
||||
|
||||
$url = BASE_URL . $endpoint;
|
||||
$headers = [
|
||||
'Authorization: JWT ' . $token,
|
||||
'Content-Type: application/json'
|
||||
];
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
if ($method === 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
} elseif ($method === 'PUT') {
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
}
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取VPS列表
|
||||
*/
|
||||
function getHostList($page = 1, $limit = 100) {
|
||||
return sendApiRequest("/hosts?page={$page}&limit={$limit}", 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取VPS状态
|
||||
*/
|
||||
function getHostStatus($hostId) {
|
||||
return sendApiRequest("/hosts/{$hostId}/module/status?type=host", 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作VPS
|
||||
*/
|
||||
function operateHost($hostId, $operation) {
|
||||
return sendApiRequest("/hosts/{$hostId}/module/{$operation}", 'PUT');
|
||||
}
|
||||
|
||||
// ==================== 主逻辑区域 ====================
|
||||
|
||||
// 验证pass参数
|
||||
$pass = isset($_GET['pass']) ? $_GET['pass'] : '';
|
||||
|
||||
if ($pass !== API_PASS) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['status' => 403, 'msg' => '访问密码错误或者未输入密码,请拼接?pass=API_PASS后再尝试访问'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 处理操作请求
|
||||
$action = isset($_POST['action']) ? $_POST['action'] : '';
|
||||
$hostId = isset($_POST['host_id']) ? intval($_POST['host_id']) : 0;
|
||||
|
||||
$result = null;
|
||||
if ($action && $hostId > 0) {
|
||||
if ($action === 'reboot') {
|
||||
$result = operateHost($hostId, 'hard_reboot');
|
||||
} elseif ($action === 'on') {
|
||||
$result = operateHost($hostId, 'on');
|
||||
} elseif ($action === 'off') {
|
||||
$result = operateHost($hostId, 'off');
|
||||
}
|
||||
}
|
||||
|
||||
// 获取VPS列表和状态
|
||||
$hosts = [];
|
||||
$statusMap = [];
|
||||
$domainstatus = [];
|
||||
$totalCount = 0;
|
||||
$errorMsg = '';
|
||||
|
||||
$listResult = getHostList();
|
||||
if (isset($listResult['status']) && $listResult['status'] === 200) {
|
||||
$data = $listResult['data'];
|
||||
$hosts = $data['host'];
|
||||
$domainstatus = $data['domainstatus'];
|
||||
$totalCount = $data['total'];
|
||||
|
||||
// 批量获取所有VPS的状态
|
||||
foreach ($hosts as $host) {
|
||||
$statusResult = getHostStatus($host['id']);
|
||||
if (isset($statusResult['status']) && $statusResult['status'] === 200) {
|
||||
$statusMap[$host['id']] = $statusResult['data'];
|
||||
} else {
|
||||
$statusMap[$host['id']] = ['status' => 'unknown', 'des' => '未知'];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$errorMsg = isset($listResult['msg']) ? $listResult['msg'] : '获取VPS列表失败';
|
||||
}
|
||||
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="./static/favicon.ico" type="image/x-icon">
|
||||
<link href="./static/style.css" rel="stylesheet" type="text/css">
|
||||
<title>VPS管理面板</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🖥️ 核云IDC VPS管理面板</h1>
|
||||
<p>实时查看和管理您的云服务器</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<?php if (!empty($errorMsg)): ?>
|
||||
<div class="error-message">
|
||||
<h3>❌ 加载失败</h3>
|
||||
<p><?php echo htmlspecialchars($errorMsg); ?></p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
||||
<?php if (!empty($result)): ?>
|
||||
<?php if (isset($result['status']) && $result['status'] === 200): ?>
|
||||
<div class="success-message">
|
||||
<h3>✅ 操作成功</h3>
|
||||
<p>VPS #<?php echo $hostId; ?> 已成功执行操作</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="error-message">
|
||||
<h3>❌ 操作失败</h3>
|
||||
<p><?php echo isset($result['msg']) ? htmlspecialchars($result['msg']) : '未知错误'; ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$cachedToken = getCachedToken();
|
||||
if ($cachedToken):
|
||||
// 从PHP文件读取时间戳
|
||||
include TOKEN_CACHE_FILE;
|
||||
$remainingTime = TOKEN_EXPIRE_TIME - (time() - $cached_timestamp);
|
||||
$remainingMinutes = floor($remainingTime / 60);
|
||||
?>
|
||||
<div class="token-info">
|
||||
✅ <strong>Token缓存生效中</strong> | 剩余有效期:约 <?php echo $remainingMinutes; ?> 分钟
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="stats-bar">
|
||||
<div class="stats-item">
|
||||
总计:<strong><?php echo $totalCount; ?></strong> 台服务器
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
运行中:<strong><?php
|
||||
$runningCount = 0;
|
||||
foreach ($statusMap as $status) {
|
||||
if ($status['status'] === 'on') $runningCount++;
|
||||
}
|
||||
echo $runningCount;
|
||||
?></strong> 台
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
已关机:<strong><?php
|
||||
$offCount = 0;
|
||||
foreach ($statusMap as $status) {
|
||||
if ($status['status'] === 'off') $offCount++;
|
||||
}
|
||||
echo $offCount;
|
||||
?></strong> 台
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-grid">
|
||||
<?php foreach ($hosts as $host):
|
||||
$statusKey = $host['domainstatus'];
|
||||
$statusInfo = isset($domainstatus[$statusKey]) ? $domainstatus[$statusKey] : ['name' => $statusKey, 'color' => '#999'];
|
||||
$powerStatus = isset($statusMap[$host['id']]) ? $statusMap[$host['id']] : ['status' => 'unknown', 'des' => '未知'];
|
||||
|
||||
$regDate = date('Y-m-d', $host['regdate']);
|
||||
$nextDueDate = date('Y-m-d', $host['nextduedate']);
|
||||
|
||||
$powerClass = 'power-unknown';
|
||||
if ($powerStatus['status'] === 'on') {
|
||||
$powerClass = 'power-on';
|
||||
} elseif ($powerStatus['status'] === 'off') {
|
||||
$powerClass = 'power-off';
|
||||
}
|
||||
?>
|
||||
<div class="vps-card">
|
||||
<div class="card-header">
|
||||
<div class="vps-id">#<?php echo $host['id']; ?></div>
|
||||
<span class="status-badge" style="background-color: <?php echo $statusInfo['color']; ?>">
|
||||
<?php echo htmlspecialchars($statusInfo['name']); ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="info-row">
|
||||
<span class="info-label">域名</span>
|
||||
<span class="info-value"><?php echo htmlspecialchars($host['domain']); ?></span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">IP地址</span>
|
||||
<span class="info-value"><?php echo htmlspecialchars($host['dedicatedip']); ?></span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">产品名称</span>
|
||||
<span class="info-value"><?php echo htmlspecialchars($host['product_name']); ?></span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">注册日期</span>
|
||||
<span class="info-value"><?php echo $regDate; ?></span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">到期日期</span>
|
||||
<span class="info-value"><?php echo $nextDueDate; ?></span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">金额</span>
|
||||
<span class="info-value">¥<?php echo htmlspecialchars($host['amount']); ?></span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">计费周期</span>
|
||||
<span class="info-value"><?php echo htmlspecialchars($host['billingcycle']); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="power-status">
|
||||
<span class="power-indicator <?php echo $powerClass; ?>"></span>
|
||||
<span class="power-text"><?php echo htmlspecialchars($powerStatus['des']); ?></span>
|
||||
</div>
|
||||
|
||||
<form method="POST" style="margin: 0;">
|
||||
<input type="hidden" name="host_id" value="<?php echo $host['id']; ?>">
|
||||
<div class="card-actions">
|
||||
<button type="submit" name="action" value="on" class="btn btn-success" onclick="return confirm('确认开机?')">
|
||||
⚡ 开机
|
||||
</button>
|
||||
<button type="submit" name="action" value="off" class="btn btn-danger" onclick="return confirm('确认关机?')">
|
||||
🔴 关机
|
||||
</button>
|
||||
<button type="submit" name="action" value="reboot" class="btn btn-warning" onclick="return confirm('确认硬重启?')" style="grid-column: span 2;">
|
||||
🔄 硬重启
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
static/api.png
BIN
static/api.png
Binary file not shown.
|
Before Width: | Height: | Size: 367 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
@ -1,121 +0,0 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.config-container {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
padding: 40px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.config-header h1 {
|
||||
color: #2d3748;
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.config-header p {
|
||||
color: #718096;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
color: #4a5568;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.form-group .help-text {
|
||||
color: #a0aec0;
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #fed7d7;
|
||||
border-left: 4px solid #f56565;
|
||||
color: #c53030;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-submit:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-submit:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #ebf8ff;
|
||||
border-left: 4px solid #4299e1;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
color: #2c5282;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.config-container {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
}
|
||||
338
static/style.css
338
static/style.css
@ -1,338 +0,0 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
|
||||
min-height: 100vh;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 25px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.token-info {
|
||||
background: linear-gradient(135deg, #e0f7fa 0%, #b2ebf2 100%);
|
||||
border-left: 4px solid #00bcd4;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
color: #006064;
|
||||
}
|
||||
|
||||
.token-info strong {
|
||||
color: #00838f;
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
font-size: 14px;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.stats-item strong {
|
||||
color: #667eea;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.vps-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.vps-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.vps-id {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 6px 14px;
|
||||
border-radius: 20px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f7fafc;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #718096;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #2d3748;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
max-width: 60%;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.power-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
background: #f7fafc;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.power-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.power-on {
|
||||
background: #48bb78;
|
||||
box-shadow: 0 0 8px #48bb78;
|
||||
}
|
||||
|
||||
.power-off {
|
||||
background: #e53e3e;
|
||||
box-shadow: 0 0 8px #e53e3e;
|
||||
}
|
||||
|
||||
.power-unknown {
|
||||
background: #a0aec0;
|
||||
}
|
||||
|
||||
.power-text {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
transition: all 0.3s;
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%);
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: linear-gradient(135deg, #4299e1 0%, #3182ce 100%);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: linear-gradient(135deg, #fed7d7 0%, #feb2b2 100%);
|
||||
border-left: 4px solid #f56565;
|
||||
color: #c53030;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background: linear-gradient(135deg, #c6f6d5 0%, #9ae6b4 100%);
|
||||
border-left: 4px solid #48bb78;
|
||||
color: #22543d;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #e2e8f0;
|
||||
border-top-color: #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 20px 15px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.vps-card {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
max-width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.header h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.vps-id {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
1
web
Submodule
1
web
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 28cb40ad280dbca46ae1fe0254f8910eb178ce6f
|
||||
Loading…
Reference in New Issue
Block a user