V1.0正式版更新

This commit is contained in:
MasonLiu 2026-05-29 23:09:58 +08:00
parent 4d1a9111b6
commit 0d81e72ef1
18 changed files with 4146 additions and 1203 deletions

595
README.md
View File

@ -1,51 +1,580 @@
## 核云IDC服务商VPS自动监测重启程序
# VPS Hub - 多平台VPS监控与管理系统
### API接口信息
<div align="center">
![License](https://img.shields.io/badge/license-MIT-red)
- **获取登录Token**
`https://www.heyunidc.cn/v1/login_api?account={phone number/email acc}&password={API KEY}`
备注POST方法
**一个强大的多平台VPS监控、管理和自动重启系统**
- **获取VPS列表**
`https://www.heyunidc.cn/v1/hosts?page=1&limit=100`
备注GET方法使用"Authorization: JWT {Token}"进行认证
注意若您的VPS数量超过100需要更改limit数量
[功能特性](#-功能特性) • [快速开始](#-快速开始) • [架构设计](#-架构设计) • [API文档](#-api文档) • [常见问题](#-常见问题)
- **获取VPS状态**
`https://www.heyunidc.cn/v1/hosts/{id}/module/status?type=host`
备注GET方法使用"Authorization: JWT {Token}"进行认证
</div>
- **操作VPS**
`PUT https://www.heyunidc.cn/v1/hosts/:id/module/hard_reboot`
备注PUT方法使用"Authorization: JWT {Token}"进行认证
操作类型hard_reboot - 硬重启on - 开机off - 关机reboot - 重启
---
### 使用方法
## 📋 目录
#### 获取API_KEY
如图所示:
- [功能特性](#-功能特性)
- [系统架构](#-系统架构)
- [技术栈](#-技术栈)
- [快速开始](#-快速开始)
- [配置说明](#-配置说明)
- [使用指南](#-使用指南)
- [数据库设计](#-数据库设计)
- [API文档](#-api文档)
- [监控服务](#-监控服务)
- [安全说明](#-安全说明)
- [常见问题](#-常见问题)
- [更新日志](#-更新日志)
- [许可证](#-许可证)
---
## ✨ 功能特性
### 🎯 核心功能
- **多平台支持**支持魔方平台智简魔方、阿里云、腾讯云等主流VPS平台
- **实时监控**每5分钟自动Ping检测所有VPS的可用性
- **自动开机**检测到VPS关机时自动执行开机操作
- **状态管理**实时显示VPS的运行状态开机/关机/未知)
- **详细信息**展示CPU、内存、磁盘、带宽、操作系统等配置信息
### 🔧 管理功能
- **Web管理面板**美观的响应式Web界面
- **手动刷新**一键刷新VPS列表和详细信息
- **远程控制**:支持开机、关机、硬重启操作
- **配置管理**轻松添加和管理多个VPS平台配置
- **统计信息**实时统计VPS总数、运行中数量、已关机数量
### 📊 监控功能
- **Ping检测**定期检测VPS网络可达性
- **延迟记录**记录每次Ping的延迟时间
- **状态持久化**将VPS状态保存到SQLite数据库
- **历史数据**保留30天的Ping历史记录
- **每日摘要**每天0点生成前一天的可用性统计报告
### 🚀 智能优化
- **API调用优化**Ping成功时不调用API减少不必要的请求
- **Token缓存**JWT Token缓存2小时避免频繁登录
- **动态更新**:只更新实际变化的字段,避免数据覆盖
- **防重复机制**:使用唯一约束防止重复数据
- **错误重试**:开机失败后自动重试并记录真实状态
---
## 🏗️ 系统架构
```
┌─────────────────────────────────────────────────────┐
│ Web前端 (index.php) │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ VPS列表 │ │ 操作控制 │ │ 统计信息展示 │ │
│ └──────────┘ └──────────┘ └──────────────────┘ │
└──────────────────────┬──────────────────────────────┘
│ HTTP POST/GET
┌──────────────────────▼──────────────────────────────┐
│ PHP后端 (mofangidc.php) │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ API封装 │ │Token管理 │ │ 数据库操作 │ │
│ └──────────┘ └──────────┘ └──────────────────┘ │
└──────────────────────┬──────────────────────────────┘
│ SQLite
┌──────────────────────▼──────────────────────────────┐
│ 数据库层 (SQLite) │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ vps.db │ │vpslist.db│ │ status.db │ │
│ │(配置) │ │ (VPS列表)│ │ (监控记录) │ │
│ └──────────┘ └──────────┘ └──────────────────┘ │
└──────────────────────┬──────────────────────────────┘
┌──────────────────────▼──────────────────────────────┐
│ Python监控服务 (monitor.py) │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Ping检测 │ │ API调用 │ │ 定时任务 │ │
│ └──────────┘ └──────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────┘
```
### 架构特点
1. **前后端分离**PHP负责Web界面和API调用Python负责后台监控
2. **多数据库设计**三个独立的SQLite数据库职责清晰
3. **混合架构**PHP处理用户交互Python处理定时任务
4. **适配器模式**:支持多平台扩展,易于添加新平台
---
## 💻 技术栈
### 后端
- **PHP 7.4+**Web服务器端逻辑
- **Python 3.6+**:后台监控服务
- **SQLite 3**:轻量级数据库
### 前端
- **HTML5/CSS3**:页面结构和样式
- **JavaScript**:交互逻辑
- **响应式设计**:支持移动端访问
### 依赖库
- **PHP**: cURL, PDO-SQLite
- **Python**: requests, schedule, sqlite3
---
## 🚀 快速开始
### 环境要求
- PHP 7.4 或更高版本
- Python 3.6 或更高版本
- SQLite 3
- Linux/Windows/macOS
### 安装步骤
#### 1. 克隆项目
```bash
git clone https://git.masonliu.com/MasonLiu/VPSHUB.git
cd VPSHUB
```
#### 2. 配置Web
直接将VPSHUB文件夹下的所有文件放置于网站路径下即可
**温馨提示**
请将app路径设置为禁止外网访问或添加Basic认证
#### 3. 一键快速启动
```bash
chmod 755 app/install.sh
./app/install.sh
```
#### 4. 访问Web界面
浏览器访问:`http://your-domain.com/`
首次访问会自动跳转到配置页面添加第一个VPS平台配置。
---
## ⚙️ 配置说明
### 添加VPS平台配置
1. 访问 `config_add.php?pass=YOUR_API_PASS`
2. 填写配置信息:
- **API标识**:自定义名称(唯一)
- **网站类型**:选择平台(目前支持魔方平台)
- **网站链接**API根域名不要包含路径
- **账户**:登录账号
- **API密钥**登录密码或API Key
- **自动监控**:是否启用自动监控
3. 点击"保存配置"
### 配置示例
**魔方平台配置**
```
API标识: 核云IDC
网站类型: 魔方平台
网站链接: https://www.heyunidc.cn
账户: 邮箱或电话号码
API密钥: your_api_key
自动监控: ✓
```
![API_KEY](./src/api.png)
#### Web端
将web目录部分部署到服务器上分配域名或直接IP访问即可
⚠️ **重要提示**
- 网站链接只填写根域名,不要包含 `/v1``/api` 等路径
- API标识必须唯一不能重复
- 确保账户和密码正确
首次使用会进入配置页依次输入API_PASSACCOUNTAPI_KEY其中API_PASS是您自定义的网站访问密码支持大小写字母以及数字
---
随后访问`http://example.com/?pass=API_PASS`即可
## 📖 使用指南
切记切勿将app部分放置于网站目录下
### Web管理面板
#### 监控端
#### 查看VPS列表
**安装:**
1. chmod +x ./install.sh
2. ./install.sh
访问 `index.php?pass=YOUR_API_PASS`,可以看到:
- 按配置分组的VPS列表
- 每个VPS的状态、IP、配置信息
- 实时统计数据
系统将自动注册名为idc-monitor的system服务
#### 操作VPS
注意若您的服务器必须要求禁ping程序可能无法正常运行在安装脚本前请手动将config.yml中的WAY改为http然后再启动安装脚本
每个VPS卡片提供三个操作按钮
- **⚡ 开机**启动VPS
- **🔴 关机**关闭VPS
- **🔄 硬重启**强制重启VPS
**卸载:**
1. chmod +x ./uninstall.sh
2. ./uninstall.sh
操作后会显示成功/失败提示5秒后自动消失。
#### 刷新VPS列表
点击顶部工具栏的 "🔄 手动刷新VPS列表" 按钮:
1. 从API获取最新的VPS列表
2. 更新数据库中的VPS信息
3. 获取每个VPS的详细配置CPU、内存、磁盘、带宽、系统
### 监控服务
#### 启动监控
```bash
sudo systemctl start idc_monitor
```
#### 查看状态
```bash
sudo systemctl status idc_monitor
```
#### 停止监控
```bash
sudo systemctl stop idc_monitor
```
#### 重启监控
```bash
sudo systemctl restart idc_monitor
```
### 监控流程
```
每5分钟执行一次监控循环
1. Ping所有VPS
├─ Ping成功 → 标记status='on'不调用API
└─ Ping失败 → 标记为abnormal等待下一步
2. 检查Ping失败的VPS
├─ 全部正常 → 跳过
└─ 有失败的 → 调用API查询真实状态
├─ 状态为'on' → 可能是禁Ping更新数据库
└─ 状态为'off' → 执行开机操作
├─ 等待60秒
└─ 验证开机结果最多重试2次
3. 清理30天前的旧数据
等待5分钟后下一次循环
```
---
## 🗄️ 数据库设计
### 数据库文件
| 文件名 | 用途 | 主要表 |
|--------|------|--------|
| `vps.db` | 存储平台配置 | `configs` |
| `vpslist.db` | 存储VPS列表 | `vps_list` |
| `status.db` | 存储监控记录 | `ping_status`, `vps_summary` |
### 表结构
#### configs 表 (vps.db)
```sql
CREATE TABLE configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
api_label TEXT NOT NULL UNIQUE, -- API标识
site_type TEXT NOT NULL, -- 平台类型
site_url TEXT, -- API地址
account TEXT NOT NULL, -- 账户
api_key TEXT NOT NULL, -- API密钥
auto_monitor BOOLEAN DEFAULT 1, -- 是否自动监控
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
#### vps_list 表 (vpslist.db)
```sql
CREATE TABLE vps_list (
id INTEGER PRIMARY KEY AUTOINCREMENT,
config_id INTEGER NOT NULL, -- 配置ID
vps_id INTEGER NOT NULL, -- VPS平台ID
domain TEXT, -- 域名
ip_address TEXT, -- IP地址
product_name TEXT, -- 产品名称
cpu_cores INTEGER, -- CPU核数
memory_size TEXT, -- 内存大小
disk_size TEXT, -- 磁盘大小
bandwidth TEXT, -- 带宽
os_type TEXT, -- 操作系统
status TEXT, -- 状态(on/off/unknown)
section BOOLEAN DEFAULT 0, -- 是否监控
last_check TIMESTAMP, -- 最后检查时间
FOREIGN KEY (config_id) REFERENCES configs(id),
UNIQUE(config_id, vps_id)
);
```
#### ping_status 表 (status.db)
```sql
CREATE TABLE ping_status (
id INTEGER PRIMARY KEY AUTOINCREMENT,
vps_id INTEGER NOT NULL, -- VPS ID
target TEXT NOT NULL, -- 目标(IP或域名)
status TEXT NOT NULL, -- 状态(normal/abnormal)
latency_ms REAL, -- 延迟(ms)
check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
#### vps_summary 表 (status.db)
```sql
CREATE TABLE vps_summary (
id INTEGER PRIMARY KEY AUTOINCREMENT,
vps_id INTEGER NOT NULL, -- VPS ID
date DATE NOT NULL, -- 日期
avg_latency_ms REAL, -- 平均延迟
max_latency_ms REAL, -- 最大延迟
min_latency_ms REAL, -- 最小延迟
count_under_100 INTEGER, -- <100ms次数
count_100_to_300 INTEGER, -- 100-300ms次数
count_300_to_500 INTEGER, -- 300-500ms次数
count_abnormal INTEGER, -- 异常次数
availability TEXT, -- 可用性评分
UNIQUE(vps_id, date)
);
```
---
## 📡 API文档
### 魔方平台API
- `POST /login_api?account={acc}&password={api key}` - 登录获取JWT Token
以下接口通过请求头进行认证
Authorization: JWT {token}
- `GET /hosts?page=&limit=` - 获取VPS列表
- `GET /hosts/{id}` - 获取VPS详情
- `GET /hosts/{id}/module/status?type=host` - 获取VPS状态
- `PUT /hosts/{id}/module/on` - 开机
- `PUT /hosts/{id}/module/off` - 关机
- `PUT /hosts/{id}/module/hard_reboot` - 硬重启
## 🔍 监控服务
### 配置文件
监控服务配置文件位于:`/etc/systemd/system/idc_monitor.service`
```ini
[Unit]
Description=VPS Hub Monitor Service
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/path/to/VPSHUB/app
ExecStart=/usr/bin/python3 monitor.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
```
### 日志文件
- **正常日志**`app/logs/monitor.log`
- **错误日志**`app/logs/error.log`
### 定时任务
- **监控循环**每5分钟执行一次
- **每日摘要**每天00:00生成前一天的统计报告
- **数据清理**每次循环清理30天前的Ping记录
---
## 🔒 安全说明
### 访问控制
- **API密码保护**:所有页面都需要通过 `?pass=API_PASS` 验证
- **密码文件**:存储在 `app/pass.php`,权限设置为 600
- **数据库保护**Nginx配置禁止直接访问 `.db` 文件或者限制访问app路径需要basic认证
### Token安全
- **Token缓存**JWT Token缓存2小时减少登录次数
- **自动刷新**Token失效时自动重新登录
- **缓存文件**`app/token.php`,权限设置为 600
### 最佳实践
1. **修改默认密码**首次使用后立即修改API密码
2. **HTTPS**生产环境务必使用HTTPS
3. **防火墙**限制访问IP范围
4. **定期备份**:定期备份数据库文件
5. **日志审计**:定期检查日志文件
---
## ❓ 常见问题
### Q1: 页面显示"0台VPS"怎么办?
**A**:
1. 点击"手动刷新VPS列表"按钮
2. 检查配置是否正确特别是site_url不要包含路径
3. 查看PHP错误日志`tail -f /var/log/php/error.log`
---
### Q2: 监控服务无法启动?
**A**:
```bash
# 检查服务状态
sudo systemctl status idc_monitor
# 查看详细日志
journalctl -u idc_monitor -n 50
# 常见原因:
# 1. Python依赖未安装pip install -r requirements.txt
# 2. 数据库文件权限问题chown www-data:www-data app/db/*.db
# 3. 配置文件路径错误检查service文件中的WorkingDirectory
```
---
### Q3: exec函数被禁用怎么办
**A**:
系统已经做了兼容处理:
- 如果exec被禁用会显示友好提示
- 需要手动执行安装脚本:`sudo bash app/install.sh`
- 监控服务仍然可以正常运行
---
### Q4: VPS状态显示"未知"
**A**:
可能原因:
1. 数据库中还没有该VPS的记录
2. 多次调用导致状态被覆盖(已修复)
3. API返回异常
解决方法:
- 手动刷新VPS列表
- 检查日志确认API调用是否正常
- 等待下一次监控循环
---
### Q5: 如何添加新的VPS平台
**A**:
1. 在 `app/monitor.py` 中创建新的适配器类
2. 继承 `PlatformAdapter` 基类
3. 实现必要的方法login, get_vps_list, get_vps_status等
4. 在 `load_configs_and_create_adapters()` 中添加判断逻辑
示例:
```python
class AliyunAdapter(PlatformAdapter):
def login(self):
# 实现阿里云登录逻辑
pass
def get_vps_list(self):
# 实现获取VPS列表逻辑
pass
```
---
### Q6: 数据库文件在哪里?
**A**:
- `app/db/vps.db` - 配置数据库
- `app/db/vpslist.db` - VPS列表数据库
- `app/db/status.db` - 监控状态数据库
可以使用SQLite工具查看
```bash
sqlite3 app/db/vpslist.db
.tables
SELECT * FROM vps_list;
```
---
### Q7: 如何备份数据?
**A**:
```bash
# 备份所有数据库
cp app/db/*.db backup/$(date +%Y%m%d)/
# 或使用sqlite3导出
sqlite3 app/db/vpslist.db ".dump" > backup/vpslist.sql
```
---
## 📄 许可证
本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件
---
## 📧 联系方式
- **项目地址**: [https://git.masonliu.com/MasonLiu/VPSHUB](https://git.masonliu.com/MasonLiu/VPSHUB)
- **作者**: MasonLiu
- **邮箱**: admin@masonliu.com
---
## 🙏 致谢
感谢以下开源项目:
- [PHP](https://www.php.net/)
- [Python](https://www.python.org/)
- [SQLite](https://www.sqlite.org/)
- [智简魔方财务系统](https://www.idcsmart.com/)
---
<div align="center">
**⭐ 如果这个项目对您有帮助请给个Star**
Made with ❤️ by MasonLiu
</div>

View File

@ -1,7 +0,0 @@
ACCOUNT: '' # 填写核云IDC账号手机号或邮箱
API_KEY: '' # 填写核云IDC API密钥
WAY: ping # 填写检测方式ping 或 http默认为ping
DOMAIN: '' # 当WAY为http时填写要检测的域名多个域名用英文逗号分隔
SPAN: 300 # 监控间隔时间默认300秒5分钟
EXCEPTION_IPS: [] # 例外IP列表这些IP关机时不会自动开机例如["1.2.3.4", "5.6.7.8"]
JWT: '' # JWT Token自动管理无需手动填写

253
app/db_helper.php Normal file
View File

@ -0,0 +1,253 @@
<?php
/**
* VPS Hub 数据库辅助类
* 提供SQLite数据库的统一访问接口
*/
class DBHelper {
private $db;
private $dbPath;
/**
* 构造函数
* @param string $dbPath 数据库文件路径
*/
public function __construct($dbPath) {
$this->dbPath = $dbPath;
// 确保目录存在
$dir = dirname($dbPath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
try {
$this->db = new PDO("sqlite:" . $dbPath);
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
throw new Exception("数据库连接失败: " . $e->getMessage());
}
}
/**
* 执行查询并返回所有结果
* @param string $sql SQL语句
* @param array $params 参数数组
* @return array 结果数组
*/
public function query($sql, $params = []) {
try {
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
} catch (PDOException $e) {
error_log("数据库查询错误: " . $e->getMessage() . " | SQL: " . $sql);
return [];
}
}
/**
* 执行查询并返回单条结果
* @param string $sql SQL语句
* @param array $params 参数数组
* @return array|false 结果数组或false
*/
public function queryOne($sql, $params = []) {
try {
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $stmt->fetch();
} catch (PDOException $e) {
error_log("数据库查询错误: " . $e->getMessage() . " | SQL: " . $sql);
return false;
}
}
/**
* 执行插入、更新、删除操作
* @param string $sql SQL语句
* @param array $params 参数数组
* @return bool 是否成功
*/
public function execute($sql, $params = []) {
try {
$stmt = $this->db->prepare($sql);
return $stmt->execute($params);
} catch (PDOException $e) {
error_log("数据库执行错误: " . $e->getMessage() . " | SQL: " . $sql);
return false;
}
}
/**
* 插入数据并返回最后插入的ID
* @param string $sql SQL语句
* @param array $params 参数数组
* @return int|false 最后插入的ID或false
*/
public function insert($sql, $params = []) {
try {
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $this->db->lastInsertId();
} catch (PDOException $e) {
error_log("数据库插入错误: " . $e->getMessage() . " | SQL: " . $sql);
return false;
}
}
/**
* 开始事务
*/
public function beginTransaction() {
return $this->db->beginTransaction();
}
/**
* 提交事务
*/
public function commit() {
return $this->db->commit();
}
/**
* 回滚事务
*/
public function rollBack() {
return $this->db->rollBack();
}
/**
* 获取数据库连接对象(用于高级操作)
* @return PDO
*/
public function getConnection() {
return $this->db;
}
}
// 便捷函数获取vps.db的DBHelper实例
function getVpsDB() {
static $db = null;
if ($db === null) {
$dbPath = __DIR__ . '/db/vps.db';
$db = new DBHelper($dbPath);
// 初始化表结构
initVpsTables($db);
}
return $db;
}
// 便捷函数获取vpslist.db的DBHelper实例
function getVpsListDB() {
static $db = null;
if ($db === null) {
$dbPath = __DIR__ . '/db/vpslist.db';
$db = new DBHelper($dbPath);
initVpsListTables($db);
}
return $db;
}
// 便捷函数获取status.db的DBHelper实例
function getStatusDB() {
static $db = null;
if ($db === null) {
$dbPath = __DIR__ . '/db/status.db';
$db = new DBHelper($dbPath);
// 初始化表结构
initStatusTables($db);
}
return $db;
}
/**
* 初始化vps.db表结构
*/
function initVpsTables($db) {
$db->execute("
CREATE TABLE IF NOT EXISTS configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
api_label TEXT NOT NULL UNIQUE,
site_type TEXT NOT NULL,
site_url TEXT,
account TEXT NOT NULL,
api_key TEXT NOT NULL,
auto_monitor BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
");
}
/**
* 初始化vpslist.db表结构
*/
function initVpsListTables($db) {
$db->execute("
CREATE TABLE IF NOT EXISTS vps_list (
id INTEGER PRIMARY KEY AUTOINCREMENT,
config_id INTEGER NOT NULL,
vps_id INTEGER NOT NULL,
domain TEXT,
ip_address TEXT,
product_name TEXT,
cpu_cores INTEGER,
memory_size TEXT,
disk_size TEXT,
bandwidth TEXT,
os_type TEXT,
status TEXT,
section BOOLEAN DEFAULT 0,
last_check TIMESTAMP,
FOREIGN KEY (config_id) REFERENCES configs(id),
UNIQUE(config_id, vps_id)
)
");
$db->execute('CREATE INDEX IF NOT EXISTS idx_vps_config ON vps_list(config_id)');
$db->execute('CREATE INDEX IF NOT EXISTS idx_vps_vps_id ON vps_list(vps_id)');
}
/**
* 初始化status.db表结构
*/
function initStatusTables($db) {
// Ping状态记录表
$db->execute("
CREATE TABLE IF NOT EXISTS ping_status (
id INTEGER PRIMARY KEY AUTOINCREMENT,
vps_id INTEGER NOT NULL,
target TEXT NOT NULL,
status TEXT NOT NULL,
latency_ms REAL,
check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
");
// VPS摘要统计表
$db->execute("
CREATE TABLE IF NOT EXISTS vps_summary (
id INTEGER PRIMARY KEY AUTOINCREMENT,
vps_id INTEGER NOT NULL,
date DATE NOT NULL,
avg_latency_ms REAL,
max_latency_ms REAL,
min_latency_ms REAL,
count_under_100 INTEGER DEFAULT 0,
count_100_to_300 INTEGER DEFAULT 0,
count_300_to_500 INTEGER DEFAULT 0,
count_abnormal INTEGER DEFAULT 0,
availability TEXT,
UNIQUE(vps_id, date)
)
");
// 创建索引
$db->execute('CREATE INDEX IF NOT EXISTS idx_ping_vps ON ping_status(vps_id)');
$db->execute('CREATE INDEX IF NOT EXISTS idx_ping_time ON ping_status(check_time)');
$db->execute('CREATE INDEX IF NOT EXISTS idx_summary_vps ON vps_summary(vps_id)');
$db->execute('CREATE INDEX IF NOT EXISTS idx_summary_date ON vps_summary(date)');
}
?>

582
app/db_manager.py Normal file
View File

@ -0,0 +1,582 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
VPS Hub 数据库管理模块
负责初始化和管理三个SQLite数据库: vps.db, vpslist.db, status.db
"""
import os
import sqlite3
from datetime import datetime
class DatabaseManager:
"""数据库管理器"""
def __init__(self, db_dir=None):
"""初始化数据库管理器
Args:
db_dir: 数据库文件目录,默认为app/db/
"""
if db_dir is None:
db_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'db')
self.db_dir = db_dir
os.makedirs(db_dir, exist_ok=True)
# 数据库文件路径
self.vps_db = os.path.join(db_dir, 'vps.db')
self.vpslist_db = os.path.join(db_dir, 'vpslist.db')
self.status_db = os.path.join(db_dir, 'status.db')
# 初始化所有数据库
self.init_vps_db()
self.init_vpslist_db()
self.init_status_db()
def get_connection(self, db_path):
"""获取数据库连接
Args:
db_path: 数据库文件路径
Returns:
SQLite连接对象
"""
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row # 使结果可以通过列名访问
return conn
def init_vps_db(self):
"""初始化vps.db - VPS配置表"""
conn = self.get_connection(self.vps_db)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
api_label TEXT NOT NULL UNIQUE,
site_type TEXT NOT NULL,
site_url TEXT,
account TEXT NOT NULL,
api_key TEXT NOT NULL,
auto_monitor BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
def init_vpslist_db(self):
"""初始化vpslist.db - VPS列表缓存表"""
conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS vps_list (
id INTEGER PRIMARY KEY AUTOINCREMENT,
config_id INTEGER NOT NULL,
vps_id INTEGER NOT NULL,
domain TEXT,
ip_address TEXT,
product_name TEXT,
cpu_cores INTEGER,
memory_size TEXT,
disk_size TEXT,
bandwidth TEXT,
os_type TEXT,
status TEXT,
section BOOLEAN DEFAULT 0,
last_check TIMESTAMP,
FOREIGN KEY (config_id) REFERENCES configs(id),
UNIQUE(config_id, vps_id)
)
''')
# 创建索引以提高查询性能
cursor.execute('CREATE INDEX IF NOT EXISTS idx_vps_config ON vps_list(config_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_vps_vps_id ON vps_list(vps_id)')
conn.commit()
conn.close()
def init_status_db(self):
"""初始化status.db - Ping状态记录和摘要统计表"""
conn = self.get_connection(self.status_db)
cursor = conn.cursor()
# Ping状态记录表
cursor.execute('''
CREATE TABLE IF NOT EXISTS ping_status (
id INTEGER PRIMARY KEY AUTOINCREMENT,
vps_id INTEGER NOT NULL,
target TEXT NOT NULL,
status TEXT NOT NULL,
latency_ms REAL,
check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# VPS摘要统计表
cursor.execute('''
CREATE TABLE IF NOT EXISTS vps_summary (
id INTEGER PRIMARY KEY AUTOINCREMENT,
vps_id INTEGER NOT NULL,
date DATE NOT NULL,
avg_latency_ms REAL,
max_latency_ms REAL,
min_latency_ms REAL,
count_under_100 INTEGER DEFAULT 0,
count_100_to_300 INTEGER DEFAULT 0,
count_300_to_500 INTEGER DEFAULT 0,
count_abnormal INTEGER DEFAULT 0,
availability TEXT,
UNIQUE(vps_id, date)
)
''')
# 创建索引
cursor.execute('CREATE INDEX IF NOT EXISTS idx_ping_vps ON ping_status(vps_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_ping_time ON ping_status(check_time)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_summary_vps ON vps_summary(vps_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_summary_date ON vps_summary(date)')
conn.commit()
conn.close()
# ==================== vps.db 操作 ====================
def add_config(self, api_label, site_type, account, api_key, site_url=None, auto_monitor=True):
"""添加VPS配置
Args:
api_label: API标识必填唯一
site_type: 网站类型 (mofang/aliyun/tencent)
account: 账户
api_key: API密钥
site_url: 网站链接
auto_monitor: 是否开启自动监控
Returns:
新配置的ID
"""
conn = self.get_connection(self.vps_db)
cursor = conn.cursor()
try:
cursor.execute('''
INSERT INTO configs (api_label, site_type, site_url, account, api_key, auto_monitor)
VALUES (?, ?, ?, ?, ?, ?)
''', (api_label, site_type, site_url, account, api_key, auto_monitor))
config_id = cursor.lastrowid
conn.commit()
return config_id
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
def get_all_configs(self):
"""获取所有配置
Returns:
配置列表
"""
conn = self.get_connection(self.vps_db)
cursor = conn.cursor()
cursor.execute('SELECT * FROM configs ORDER BY id')
configs = [dict(row) for row in cursor.fetchall()]
conn.close()
return configs
def get_config_by_id(self, config_id):
"""根据ID获取配置
Args:
config_id: 配置ID
Returns:
配置字典或None
"""
conn = self.get_connection(self.vps_db)
cursor = conn.cursor()
cursor.execute('SELECT * FROM configs WHERE id = ?', (config_id,))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
def update_config(self, config_id, **kwargs):
"""更新配置
Args:
config_id: 配置ID
**kwargs: 要更新的字段
Returns:
是否成功
"""
if not kwargs:
return False
conn = self.get_connection(self.vps_db)
cursor = conn.cursor()
# 构建UPDATE语句
fields = ', '.join([f"{key} = ?" for key in kwargs.keys()])
values = list(kwargs.values())
values.append(config_id)
cursor.execute(f'UPDATE configs SET {fields}, updated_at = CURRENT_TIMESTAMP WHERE id = ?', values)
affected = cursor.rowcount
conn.commit()
conn.close()
return affected > 0
def delete_config(self, config_id):
"""删除配置
Args:
config_id: 配置ID
Returns:
是否成功
"""
conn = self.get_connection(self.vps_db)
cursor = conn.cursor()
cursor.execute('DELETE FROM configs WHERE id = ?', (config_id,))
affected = cursor.rowcount
conn.commit()
conn.close()
return affected > 0
# ==================== vpslist.db 操作 ====================
def add_vps(self, config_id, vps_id, domain=None, ip_address=None,
product_name=None, section=False):
"""添加VPS到列表
Args:
config_id: 配置ID
vps_id: VPS在平台的ID
domain: 域名
ip_address: IP地址
product_name: 产品名称
section: 是否标记为需要监控
Returns:
新记录的ID
"""
conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO vps_list (config_id, vps_id, domain, ip_address, product_name, section, last_check)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
''', (config_id, vps_id, domain, ip_address, product_name, section))
record_id = cursor.lastrowid
conn.commit()
conn.close()
return record_id
def batch_add_vps(self, vps_list):
"""批量添加VPS
Args:
vps_list: VPS信息列表,每个元素是字典
"""
conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor()
for vps in vps_list:
cursor.execute('''
INSERT OR REPLACE INTO vps_list
(config_id, vps_id, domain, ip_address, product_name, cpu_cores, memory_size, disk_size, bandwidth, os_type, section, last_check)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
''', (
vps['config_id'],
vps['vps_id'],
vps.get('domain'),
vps.get('ip_address'),
vps.get('product_name'),
vps.get('cpu_cores'),
vps.get('memory_size'),
vps.get('disk_size'),
vps.get('bandwidth'),
vps.get('os_type'),
vps.get('section', False)
))
conn.commit()
conn.close()
def get_all_vps(self):
"""获取所有VPS
Returns:
VPS列表
"""
conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor()
cursor.execute('SELECT * FROM vps_list ORDER BY config_id, vps_id')
vps_list = [dict(row) for row in cursor.fetchall()]
conn.close()
return vps_list
def get_vps_by_config(self, config_id):
"""根据配置ID获取VPS列表
Args:
config_id: 配置ID
Returns:
VPS列表
"""
conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor()
cursor.execute('SELECT * FROM vps_list WHERE config_id = ? ORDER BY vps_id', (config_id,))
vps_list = [dict(row) for row in cursor.fetchall()]
conn.close()
return vps_list
def update_vps_details(self, vps_id, **kwargs):
"""更新VPS详细信息
Args:
vps_id: VPS ID
**kwargs: 要更新的字段
Returns:
是否成功
"""
if not kwargs:
return False
conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor()
fields = ', '.join([f"{key} = ?" for key in kwargs.keys()])
values = list(kwargs.values())
values.append(vps_id)
cursor.execute(f'UPDATE vps_list SET {fields}, last_check = CURRENT_TIMESTAMP WHERE vps_id = ?', values)
affected = cursor.rowcount
conn.commit()
conn.close()
return affected > 0
def update_vps_status(self, vps_id, status):
"""更新VPS状态
Args:
vps_id: VPS ID
status: 状态 (on/off/unknown)
Returns:
是否成功
"""
return self.update_vps_details(vps_id, status=status)
def get_monitored_vps(self):
"""获取所有标记为需要监控的VPS
Returns:
VPS列表
"""
conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor()
cursor.execute('SELECT * FROM vps_list WHERE section = 1 ORDER BY config_id, vps_id')
vps_list = [dict(row) for row in cursor.fetchall()]
conn.close()
return vps_list
# ==================== status.db 操作 ====================
def save_ping_status(self, vps_id, target, status, latency_ms=None):
"""保存Ping状态记录
Args:
vps_id: VPS ID
target: 目标(IP或域名)
status: 状态 (normal/abnormal)
latency_ms: 延迟(ms)
"""
conn = self.get_connection(self.status_db)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO ping_status (vps_id, target, status, latency_ms)
VALUES (?, ?, ?, ?)
''', (vps_id, target, status, latency_ms))
conn.commit()
conn.close()
def get_ping_records(self, vps_id, date=None):
"""获取Ping记录
Args:
vps_id: VPS ID
date: 日期 (YYYY-MM-DD),为空则获取所有记录
Returns:
Ping记录列表
"""
conn = self.get_connection(self.status_db)
cursor = conn.cursor()
if date:
cursor.execute('''
SELECT * FROM ping_status
WHERE vps_id = ? AND DATE(check_time) = ?
ORDER BY check_time
''', (vps_id, date))
else:
cursor.execute('''
SELECT * FROM ping_status
WHERE vps_id = ?
ORDER BY check_time DESC
''', (vps_id,))
records = [dict(row) for row in cursor.fetchall()]
conn.close()
return records
def cleanup_old_ping_records(self, days=30):
"""清理旧的Ping记录
Args:
days: 保留天数,默认30天
"""
from datetime import timedelta
conn = self.get_connection(self.status_db)
cursor = conn.cursor()
cutoff_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d %H:%M:%S')
cursor.execute('DELETE FROM ping_status WHERE check_time < ?', (cutoff_date,))
deleted = cursor.rowcount
conn.commit()
conn.close()
return deleted
def save_vps_summary(self, vps_id, date, avg_latency, max_latency, min_latency,
count_under_100, count_100_to_300, count_300_to_500,
count_abnormal, availability):
"""保存VPS摘要统计
Args:
vps_id: VPS ID
date: 日期 (YYYY-MM-DD)
avg_latency: 平均延迟
max_latency: 最大延迟
min_latency: 最小延迟
count_under_100: <100ms次数
count_100_to_300: 100-300ms次数
count_300_to_500: 300-500ms次数
count_abnormal: 异常次数
availability: 可用性评分
"""
conn = self.get_connection(self.status_db)
cursor = conn.cursor()
cursor.execute('''
INSERT OR REPLACE INTO vps_summary
(vps_id, date, avg_latency_ms, max_latency_ms, min_latency_ms,
count_under_100, count_100_to_300, count_300_to_500, count_abnormal, availability)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (vps_id, date, avg_latency, max_latency, min_latency,
count_under_100, count_100_to_300, count_300_to_500,
count_abnormal, availability))
conn.commit()
conn.close()
def get_vps_summary(self, vps_id, date=None):
"""获取VPS摘要统计
Args:
vps_id: VPS ID
date: 日期,为空则获取最新一条
Returns:
摘要统计字典或None
"""
conn = self.get_connection(self.status_db)
cursor = conn.cursor()
if date:
cursor.execute('SELECT * FROM vps_summary WHERE vps_id = ? AND date = ?', (vps_id, date))
else:
cursor.execute('SELECT * FROM vps_summary WHERE vps_id = ? ORDER BY date DESC LIMIT 1', (vps_id,))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
def get_all_summaries(self, date=None):
"""获取所有VPS的摘要统计
Args:
date: 日期,为空则获取最新
Returns:
摘要统计列表
"""
conn = self.get_connection(self.status_db)
cursor = conn.cursor()
if date:
cursor.execute('SELECT * FROM vps_summary WHERE date = ? ORDER BY vps_id', (date,))
else:
# 获取每个VPS的最新摘要
cursor.execute('''
SELECT vs.* FROM vps_summary vs
INNER JOIN (
SELECT vps_id, MAX(date) as max_date
FROM vps_summary
GROUP BY vps_id
) latest ON vs.vps_id = latest.vps_id AND vs.date = latest.max_date
ORDER BY vs.vps_id
''')
summaries = [dict(row) for row in cursor.fetchall()]
conn.close()
return summaries
if __name__ == '__main__':
# 测试代码
db = DatabaseManager()
print("数据库初始化完成")
print(f"vps.db: {db.vps_db}")
print(f"vpslist.db: {db.vpslist_db}")
print(f"status.db: {db.status_db}")

View File

@ -1,7 +1,7 @@
#!/bin/bash
# 核云IDC服务商VPS自动监测重启程序 - 安装脚本
# 该脚本会检查配置并创建systemd服务持续化运行monitor.py
# VPS Hub 监控程序 - 安装脚本
# 该脚本会创建systemd服务持续化运行monitor.py
# 颜色定义
RED='\033[0;31m'
@ -13,12 +13,11 @@ NC='\033[0m' # No Color
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 定义服务名称
SERVICE_NAME="idc-monitor"
SERVICE_NAME="idc_monitor"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
CONFIG_FILE="${SCRIPT_DIR}/config.yml"
echo "=========================================="
echo " 核云IDC VPS监控程序 - 安装向导"
echo " VPS Hub 监控程序 - 安装向导"
echo "=========================================="
echo ""
@ -81,189 +80,12 @@ fi
echo -e "${GREEN}${NC} 检测到pip: ${PIP_CMD}"
# 检查配置文件是否存在
if [ ! -f "${CONFIG_FILE}" ]; then
echo -e "${RED}错误: 配置文件不存在: ${CONFIG_FILE}${NC}"
exit 1
fi
# 读取现有配置
echo ""
echo "正在检查配置文件..."
# 使用grep解析YAML配置
ACCOUNT=$(grep "^ACCOUNT:" "${CONFIG_FILE}" | sed 's/ACCOUNT:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
API_KEY=$(grep "^API_KEY:" "${CONFIG_FILE}" | sed 's/API_KEY:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
WAY=$(grep "^WAY:" "${CONFIG_FILE}" | sed 's/WAY:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
DOMAIN=$(grep "^DOMAIN:" "${CONFIG_FILE}" | sed 's/DOMAIN:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
SPAN=$(grep "^SPAN:" "${CONFIG_FILE}" | sed 's/SPAN:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
EXCEPTION_IPS=$(grep "^EXCEPTION_IPS:" "${CONFIG_FILE}" | sed 's/EXCEPTION_IPS:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d "'" | tr -d '"')
NEED_UPDATE=false
# 检查ACCOUNT
if [ -z "$ACCOUNT" ]; then
echo ""
echo "=================================================="
echo -e -n "${RED}请输入核云IDC账号手机号或邮箱: ${NC}"
read ACCOUNT
if [ -z "$ACCOUNT" ]; then
echo -e "${RED}错误: 账号不能为空${NC}"
exit 1
fi
NEED_UPDATE=true
fi
# 检查API_KEY
if [ -z "$API_KEY" ]; then
echo ""
echo "=================================================="
echo -e -n "${RED}请输入核云IDC API密钥: ${NC}"
read API_KEY
if [ -z "$API_KEY" ]; then
echo -e "${RED}错误: API密钥不能为空${NC}"
exit 1
fi
NEED_UPDATE=true
fi
# 检查WAY
if [ -z "$WAY" ]; then
echo ""
echo "=================================================="
echo -e "${RED}请选择检测方式:${NC}"
echo " 1. ping - Ping检测IP地址默认"
echo " 2. http - HTTP检测域名"
echo -e -n "${RED}请输入选项 1/2直接回车默认为1: ${NC}"
read WAY_CHOICE
if [ "$WAY_CHOICE" = "2" ]; then
WAY="http"
else
WAY="ping"
fi
NEED_UPDATE=true
fi
# 如果WAY为http检查DOMAIN
if [ "$WAY" = "http" ] && [ -z "$DOMAIN" ]; then
echo ""
echo "=================================================="
echo -e "${RED}请输入要检测的域名(多个域名用英文逗号分隔):${NC}"
echo "例如: example.com,test.com,demo.com"
echo -e -n "${RED}域名: ${NC}"
read DOMAIN
if [ -z "$DOMAIN" ]; then
echo -e "${RED}错误: 域名不能为空${NC}"
exit 1
fi
NEED_UPDATE=true
fi
# 检查SPAN
if [ -z "$SPAN" ]; then
echo ""
echo "=================================================="
echo -e -n "${RED}请输入监控间隔时间直接回车默认300秒: ${NC}"
read SPAN_INPUT
if [ -n "$SPAN_INPUT" ]; then
if [[ "$SPAN_INPUT" =~ ^[0-9]+$ ]]; then
if [ "$SPAN_INPUT" -lt 60 ]; then
echo -e "${RED}警告: 间隔时间过短建议至少60秒${NC}"
fi
SPAN=$SPAN_INPUT
else
echo -e "${RED}警告: 输入无效使用默认值300秒${NC}"
SPAN=300
fi
else
SPAN=300
fi
NEED_UPDATE=true
fi
# 询问是否设置例外IP
echo ""
echo "=================================================="
echo -e "${RED}是否设置例外IP列表这些IP关机时不会自动开机${NC}"
echo -e -n "${RED}请输入例外IP多个IP用英文逗号分隔直接回车跳过: ${NC}"
read EXCEPTION_INPUT
if [ -n "$EXCEPTION_INPUT" ]; then
EXCEPTION_IPS="$EXCEPTION_INPUT"
NEED_UPDATE=true
else
if [ -z "$EXCEPTION_IPS" ]; then
EXCEPTION_IPS=""
fi
fi
# 保存配置
if [ "$NEED_UPDATE" = true ]; then
echo ""
echo "=================================================="
# 使用Python更新YAML文件
${PYTHON_CMD} << PYEOF
import yaml
config_file = "${CONFIG_FILE}"
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
except:
config = {}
config['ACCOUNT'] = "${ACCOUNT}"
config['API_KEY'] = "${API_KEY}"
config['WAY'] = "${WAY}"
if "${WAY}" == "http":
config['DOMAIN'] = "${DOMAIN}"
config['SPAN'] = ${SPAN}
# 处理例外IP
exception_ips_str = "${EXCEPTION_IPS}"
if exception_ips_str:
config['EXCEPTION_IPS'] = [ip.strip() for ip in exception_ips_str.split(',') if ip.strip()]
else:
config['EXCEPTION_IPS'] = []
# 清除JWT字段重新生成
if 'JWT' in config:
del config['JWT']
with open(config_file, 'w', encoding='utf-8') as f:
yaml.dump(config, f, allow_unicode=True, default_flow_style=False)
print("\033[32m✅ 配置已保存\033[0m")
PYEOF
if [ $? -ne 0 ]; then
echo -e "${RED}错误: 配置保存失败${NC}"
exit 1
fi
else
echo -e "${GREEN}${NC} 配置检查完成,无需更新"
fi
echo ""
echo "当前配置:"
echo " 账号: ${ACCOUNT}"
echo " 检测方式: ${WAY}"
if [ "$WAY" = "http" ]; then
echo " 域名: ${DOMAIN}"
fi
echo " 监控间隔: ${SPAN}"
if [ -n "$EXCEPTION_IPS" ]; then
echo " 例外IP: ${EXCEPTION_IPS}"
else
echo " 例外IP: 无"
fi
echo ""
echo "注意: VPS配置请通过网页 config_add.php 进行管理"
# 安装Python依赖
if [ -f "${SCRIPT_DIR}/requirements.txt" ]; then
echo ""
echo "正在安装Python依赖包..."
cd ${SCRIPT_DIR}
if ${PIP_CMD} install -r requirements.txt; then
@ -283,10 +105,11 @@ if [ ! -f "${SCRIPT_DIR}/monitor.py" ]; then
exit 1
fi
# 创建日志目录
# 创建日志目录和数据库目录
LOG_DIR="${SCRIPT_DIR}/logs"
mkdir -p ${LOG_DIR}
chmod 755 ${LOG_DIR}
DB_DIR="${SCRIPT_DIR}/db"
mkdir -p ${LOG_DIR} ${DB_DIR}
chmod 755 ${LOG_DIR} ${DB_DIR}
echo ""
echo "正在创建systemd服务..."
@ -294,7 +117,7 @@ echo "正在创建systemd服务..."
# 创建systemd服务文件
cat > ${SERVICE_FILE} << EOF
[Unit]
Description=Heyun IDC VPS Monitor Service
Description=VPS Hub Monitor Service
After=network.target
Wants=network-online.target
@ -359,6 +182,7 @@ echo -e " ${YELLOW}停止服务:${NC} systemctl stop ${SERVICE_NAME}"
echo -e " ${YELLOW}重启服务:${NC} systemctl restart ${SERVICE_NAME}"
echo -e " ${YELLOW}卸载服务:${NC} cd ${SCRIPT_DIR} && ./uninstall.sh"
echo ""
echo "配置文件位置: ${CONFIG_FILE}"
echo "配置文件位置: 通过网页 config_add.php 管理"
echo "日志文件位置: ${LOG_DIR}"
echo "数据库文件位置: ${DB_DIR}"
echo ""

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +1,3 @@
requests
pyyaml
schedule

View File

@ -1,6 +1,6 @@
#!/bin/bash
# 核云IDC服务商VPS自动监测重启程序 - 卸载脚本
# VPS Hub 监控程序 - 卸载脚本
# 该脚本会移除systemd服务
# 颜色定义
@ -13,7 +13,7 @@ NC='\033[0m' # No Color
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 定义服务名称
SERVICE_NAME="idc-monitor"
SERVICE_NAME="idc_monitor"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
echo "=========================================="

View File

@ -1,39 +0,0 @@
### 程序运行
1. 用户需先向程序config内添加以下几项定义量
ACCOUNTAPI_KEYWAY
2. 用户运行install.sh脚本
脚本会先检测config.yml中的配置文件
缺少ACCOUNTAPI_KEYWAY向用户询问填写什么同时WAY默认为ping问句以红色展示
若用户填写http则让用户填写域名可以访问的路径通过英文逗号分隔域名
随后用户可添加例外参数脚本检测到例外内的IP主机关机时直接跳过不进行开机
3. 脚本运行
创建名为idc_monitor的systemd服务持续化运行当前路径下的main.py
### 程序逻辑
1. 1. 隔一段时间SPAN便根据WAY来探测目标存活
若为ping则直接ping一遍所有的IP地址
若为域名则使用HEAD方法探测相应域名是否正常响应
1. 2. 若存在不通的情况,进行如下操作
使用config.yml中的JWT请求VPS列表并挨个查询VPS状态是否为on开机
若返回响应码为405即响应报文为
```
{
"status": 405,
"msg": "请登陆后再试"
}
```
重新请求JWT并存储随后再次请求VPS列表并查询状态
1. 3. 若均为开机状态,记录日志:什么时间点 - 发生了什么情况实际没有机器关机可能是禁ping或者服务器网站状态异常
1. 4. 若发现有机器关机则使用开机接口on进行开机
1. 5. 所有机器都操作完成后隔60秒再次查询刚才尝试开机操作的几台机器是否开机成功若不超过则再来一遍
1. 6. 尝试开机操作两次后还是非开机状态则中断本次循环进入SPAN间隔准备下一次循环
1. 7. 以上所有操作均需记录为日志,分为正常日志和异常日志(例如:未发现存在机器关机,开机失败等)

424
config_add.php Normal file
View File

@ -0,0 +1,424 @@
<?php
/**
* VPS配置管理页面
* 用于添加和管理多平台VPS配置
*/
require_once __DIR__ . '/app/db_helper.php';
require_once __DIR__ . '/mofangidc.php';
// Token文件路径(存储API_PASS)
$tokenFile = __DIR__ . '/app/token_pass.php';
// 检查是否首次访问(需要设置API_PASS)
$needSetup = !file_exists($tokenFile);
// 处理API_PASS设置
if ($needSetup && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['setup_pass'])) {
$apiPass = trim($_POST['api_pass']);
if (empty($apiPass)) {
$setupError = '密码不能为空!';
} elseif (strlen($apiPass) < 8) {
$setupError = '密码至少8位';
} elseif (!preg_match('/^[a-zA-Z0-9]+$/', $apiPass)) {
$setupError = '密码只能包含字母和数字!';
} else {
// 保存API_PASS
$content = "<?php\n";
$content .= "// API访问密码 - 自动生成\n";
$content .= "// 警告: 不要手动修改此文件\n\n";
$content .= "define('API_PASS', '" . addslashes($apiPass) . "');\n";
$content .= "?>\n";
if (file_put_contents($tokenFile, $content)) {
chmod($tokenFile, 0600);
header('Location: config_add.php?pass=' . urlencode($apiPass));
exit;
} else {
$setupError = '保存失败,请检查目录权限!';
}
}
}
// 如果需要设置密码,显示设置页面
if ($needSetup) {
?>
<!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 Hub</title>
</head>
<body>
<div class="config-container">
<div class="config-header">
<h1>🔐 初始设置</h1>
<p>请设置管理面板的访问密码</p>
</div>
<?php if (isset($setupError)): ?>
<div class="error-message">
<?php echo htmlspecialchars($setupError); ?>
</div>
<?php endif; ?>
<div class="info-box">
💡 提示:此密码用于保护配置管理页面的访问权限。<br>
密码要求至少8位只能包含字母(a-z,A-Z)和数字(0-9)
</div>
<form method="POST" onsubmit="return validatePassword()">
<div class="form-group">
<label for="api_pass">设置访问密码</label>
<input type="password" id="api_pass" name="api_pass" required
placeholder="至少8位字母和数字"
pattern="[a-zA-Z0-9]{8,}"
title="至少8位只能包含字母和数字">
</div>
<button type="submit" name="setup_pass" class="btn-submit">
保存并继续
</button>
</form>
</div>
<script>
function validatePassword() {
var password = document.getElementById('api_pass').value;
if (password.length < 8) {
alert('密码至少需要8位');
return false;
}
if (!/^[a-zA-Z0-9]+$/.test(password)) {
alert('密码只能包含字母和数字!');
return false;
}
return true;
}
</script>
</body>
</html>
<?php
exit;
}
// 加载API_PASS
if (file_exists($tokenFile)) {
include $tokenFile;
}
// 验证pass参数
$pass = isset($_GET['pass']) ? $_GET['pass'] : '';
if (!defined('API_PASS') || $pass !== API_PASS) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['status' => 403, 'msg' => '访问密码错误'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
exit;
}
// 处理添加配置
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['add_config'])) {
$apiLabel = trim($_POST['api_label']);
$siteType = trim($_POST['site_type']);
$siteUrl = trim($_POST['site_url']);
$account = trim($_POST['account']);
$apiKey = trim($_POST['api_key']);
$autoMonitor = isset($_POST['auto_monitor']) ? 1 : 0;
if (empty($apiLabel) || empty($siteType) || empty($siteUrl) || empty($account) || empty($apiKey)) {
$message = '<div class="error-message">❌ API标识、网站类型、网站链接、账户和密钥不能为空</div>';
} else {
// 验证URL格式不允许包含路径
$parsedUrl = parse_url($siteUrl);
if (!$parsedUrl || !isset($parsedUrl['scheme']) || !isset($parsedUrl['host'])) {
$message = '<div class="error-message">❌ 网站链接格式错误请输入完整的URL例如: https://www.example.com</div>';
} elseif (isset($parsedUrl['path']) && $parsedUrl['path'] !== '/') {
$message = '<div class="error-message">❌ 网站链接严禁包含路径部分!只允许输入根域名(例如: https://www.example.com不要添加 /v1、/api 等路径</div>';
} else {
$db = getVpsDB();
// 检查API标识是否已存在
$existingConfig = $db->queryOne('SELECT id FROM configs WHERE api_label = ?', [$apiLabel]);
if ($existingConfig) {
$message = '<div class="error-message">❌ API标识 "' . htmlspecialchars($apiLabel) . '" 已存在,请使用其他标识!</div>';
} else {
$configId = $db->insert(
'INSERT INTO configs (api_label, site_type, site_url, account, api_key, auto_monitor) VALUES (?, ?, ?, ?, ?, ?)',
[$apiLabel, $siteType, $siteUrl, $account, $apiKey, $autoMonitor]
);
if ($configId) {
$message = '<div class="success-message">✅ 配置添加成功ID: ' . $configId . '</div>';
// 如果是首次添加配置提示运行install.sh
$configCount = $db->queryOne('SELECT COUNT(*) as count FROM configs')['count'];
if ($configCount == 1) {
$message .= '<div class="info-box">💡 这是第一个配置,请运行 <code>app/install.sh</code> 安装监控服务</div>';
// 尝试自动执行install.sh需要PHP有执行权限
$installScript = __DIR__ . '/app/install.sh';
if (file_exists($installScript)) {
// 检查exec函数是否可用
if (!function_exists('exec')) {
$message .= '<div class="warning-message">⚠️ exec函数被禁用无法自动安装监控服务。请手动执行: sudo bash app/install.sh</div>';
} else {
// 先设置可执行权限
chmod($installScript, 0755);
// 执行安装脚本
$output = [];
$returnVar = 0;
exec("bash {$installScript} 2>&1", $output, $returnVar);
if ($returnVar === 0) {
$message .= '<div class="success-message">✅ 监控服务已自动安装并启动</div>';
} else {
$message .= '<div class="error-message">⚠️ 自动安装失败,请手动执行: sudo bash app/install.sh</div>';
// 输出错误信息以便调试
if (!empty($output)) {
$message .= '<details><summary>查看错误详情</summary><pre>' . htmlspecialchars(implode("\n", $output)) . '</pre></details>';
}
}
}
}
}
// 自动刷新VPS列表
if ($siteType === 'mofang') {
refreshVpsListForConfig($configId);
$message .= '<div class="info-box">✅ 已自动获取VPS列表</div>';
}
} else {
$message = '<div class="error-message">❌ 配置添加失败!</div>';
}
}
}
}
} elseif (isset($_POST['delete_config'])) {
$configId = intval($_POST['config_id']);
$db = getVpsDB();
$db->execute('DELETE FROM configs WHERE id = ?', [$configId]);
$message = '<div class="success-message">✅ 配置已删除</div>';
}
}
// 获取所有配置
$db = getVpsDB();
$configs = $db->query('SELECT * FROM configs ORDER BY id');
?>
<!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 Hub</title>
<style>
.config-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.config-table th, .config-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.config-table th {
background-color: #f5f5f5;
font-weight: bold;
}
.config-table tr:hover {
background-color: #f9f9f9;
}
.btn-delete {
background-color: #dc3545;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
}
.btn-delete:hover {
background-color: #c82333;
}
.form-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.form-row {
display: flex;
gap: 15px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.form-group-inline {
flex: 1;
min-width: 200px;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>⚙️ VPS配置管理</h1>
<p>添加和管理多平台VPS配置</p>
<a href="index.php?pass=<?php echo urlencode($pass); ?>" class="btn" style="float: right; margin-top: -60px;">返回首页</a>
</div>
<div class="content">
<?php echo $message; ?>
<!-- 添加配置表单 -->
<div class="form-section">
<h2> 添加新配置</h2>
<form method="POST">
<div class="form-row">
<div class="form-group-inline">
<label>API标识 * <small>(例如: 核云、阿里云主账号)</small></label>
<input type="text" name="api_label" required placeholder="输入唯一标识" pattern="[a-zA-Z0-9\u4e00-\u9fa5_\-]+" title="只能包含字母、数字、中文、下划线和连字符">
</div>
</div>
<div class="form-row">
<div class="form-group-inline">
<label>网站类型 *</label>
<select name="site_type" required onchange="toggleSiteUrl(this.value)">
<option value="mofang">魔方平台</option>
<option value="aliyun">阿里云(暂不支持)</option>
<option value="tencent">腾讯云(暂不支持)</option>
</select>
</div>
<div class="form-group-inline" id="site_url_group">
<label>网站链接/API地址 *</label>
<input type="url" name="site_url" required placeholder="https://www.example.com" id="site_url_input">
<small style="color: #dc3545;">⚠️ 严禁包含路径部分(如 /v1、/api只允许根域名</small>
</div>
</div>
<div class="form-row">
<div class="form-group-inline">
<label>账户 * <small>(邮箱或手机号)</small></label>
<input type="text" name="account" required placeholder="输入账户">
</div>
<div class="form-group-inline">
<label>API密钥 *</label>
<input type="password" name="api_key" required placeholder="输入API密钥">
</div>
</div>
<div class="form-row">
<div class="checkbox-group">
<input type="checkbox" name="auto_monitor" id="auto_monitor" checked>
<label for="auto_monitor">开启自动开机监控</label>
</div>
</div>
<button type="submit" name="add_config" class="btn btn-success">
添加配置
</button>
</form>
</div>
<!-- 配置列表 -->
<div class="form-section">
<h2>📋 现有配置</h2>
<?php if (empty($configs)): ?>
<p style="color: #999; text-align: center; padding: 40px;">暂无配置,请添加第一个配置</p>
<?php else: ?>
<table class="config-table">
<thead>
<tr>
<th>序号</th>
<th>API标识</th>
<th>网站类型</th>
<th>网站链接</th>
<th>账户</th>
<th>自动监控</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($configs as $config): ?>
<tr>
<td><?php echo $config['id']; ?></td>
<td><strong><?php echo htmlspecialchars($config['api_label']); ?></strong></td>
<td>
<?php
$typeMap = [
'mofang' => '魔方',
'aliyun' => '阿里云',
'tencent' => '腾讯云'
];
echo $typeMap[$config['site_type']] ?? $config['site_type'];
?>
</td>
<td><?php echo htmlspecialchars($config['site_url'] ?? '-'); ?></td>
<td><?php echo htmlspecialchars($config['account']); ?></td>
<td>
<?php echo $config['auto_monitor'] ? '✅ 开启' : '❌ 关闭'; ?>
</td>
<td><?php echo date('Y-m-d H:i', strtotime($config['created_at'])); ?></td>
<td>
<form method="POST" style="display: inline;"
onsubmit="return confirm('确认删除此配置?')">
<input type="hidden" name="config_id" value="<?php echo $config['id']; ?>">
<button type="submit" name="delete_config" class="btn-delete">
🗑️ 删除
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
<center><div class="copyright">
© 2026 Gaming Master Cybersecurity |
<a href="https://git.masonliu.com/MasonLiu/VPSHUB" target="_blank">MasonLiu In Gitea</a>
</div></center>
</div>
<script>
function toggleSiteUrl(siteType) {
const urlGroup = document.getElementById('site_url_group');
const urlInput = document.getElementById('site_url_input');
if (siteType === 'mofang') {
urlInput.placeholder = '例如: https://www.heyunidc.cn';
} else if (siteType === 'aliyun') {
urlInput.placeholder = '例如: https://ecs.aliyuncs.com';
} else if (siteType === 'tencent') {
urlInput.placeholder = '例如: https://cvm.tencentcloudapi.com';
} else {
urlInput.placeholder = '例如: https://www.example.com';
}
}
</script>
</body>
</html>

540
index.php Normal file
View File

@ -0,0 +1,540 @@
<?php
require_once __DIR__ . '/app/db_helper.php';
require_once __DIR__ . '/mofangidc.php';
// 存储API_PASS
$tokenFile = __DIR__ . '/app/pass.php';
// 检查是否已设置API_PASS
if (!file_exists($tokenFile)) {
header('Location: config_add.php');
exit;
}
// 加载API_PASS
include $tokenFile;
// 验证pass参数
$pass = isset($_GET['pass']) ? $_GET['pass'] : '';
if (!defined('API_PASS') || $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;
}
// 处理操作请求
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
$hostId = intval($_POST['host_id'] ?? 0);
$configId = intval($_POST['config_id'] ?? 0);
if ($action && $hostId > 0 && $configId > 0) {
$result = null;
if ($action === 'reboot') {
$result = mofangHardReboot($configId, $hostId);
} elseif ($action === 'on') {
$result = mofangPowerOn($configId, $hostId);
} elseif ($action === 'off') {
$result = mofangPowerOff($configId, $hostId);
}
if ($result && isset($result['status']) && $result['status'] === 200) {
$message = '<div class="success-message"><h3>✅ 操作成功</h3><p>VPS #' . $hostId . ' 已成功执行操作</p></div>';
} else {
$errorMsg = $result['msg'] ?? '未知错误';
$message = '<div class="error-message"><h3>❌ 操作失败</h3><p>' . htmlspecialchars($errorMsg) . '</p></div>';
}
} elseif ($action === 'refresh_vps_list') {
// 手动刷新VPS列表
$count = refreshAllVpsLists();
$message = '<div class="success-message"><h3>✅ 刷新完成</h3><p>已成功刷新 ' . $count . ' 个配置的VPS列表</p></div>';
// 获取每个VPS的详细信息并更新数据库
$listDb = getVpsListDB();
$configDb = getVpsDB();
$configs = $configDb->query('SELECT * FROM configs');
foreach ($configs as $config) {
if ($config['site_type'] !== 'mofang') {
continue; // 目前只支持魔方平台
}
// 获取该配置下的所有VPS
$vpsItems = $listDb->query('SELECT vps_id FROM vps_list WHERE config_id = ?', [$config['id']]);
foreach ($vpsItems as $vpsItem) {
$vpsId = $vpsItem['vps_id'];
// 获取VPS详细信息
$details = mofangGetVpsDetails($config['id'], $vpsId);
if ($details && isset($details['status']) && $details['status'] === 200 && isset($details['data']['host'])) {
$host = $details['data']['host'];
// 解析CPU和内存信息
$cpuCores = null;
$memorySize = null;
if (isset($host['config_option']) && is_array($host['config_option'])) {
foreach ($host['config_option'] as $option) {
if ($option['key'] === 'cpu' && isset($option['value'])) {
// 提取数字,例如 "16核" -> 16
preg_match('/(\d+)/', $option['value'], $matches);
if (!empty($matches)) {
$cpuCores = intval($matches[1]);
}
}
if ($option['key'] === 'memory' && isset($option['value'])) {
$memorySize = $option['value']; // 例如 "16G"
}
}
}
// 更新数据库
$listDb->execute(
'UPDATE vps_list SET cpu_cores = ?, memory_size = ?, last_check = CURRENT_TIMESTAMP WHERE config_id = ? AND vps_id = ?',
[$cpuCores, $memorySize, $config['id'], $vpsId]
);
}
}
}
$message .= '<div class="success-message">✅ 已更新VPS详细信息(CPU、内存等)</div>';
}
}
// 获取所有配置
$db = getVpsDB();
$configs = $db->query('SELECT * FROM configs ORDER BY id');
// 构建配置ID到配置的映射
$configMap = [];
foreach ($configs as $config) {
$configMap[$config['id']] = $config;
}
// 获取所有VPS列表只从vpslist.db查询
$listDb = getVpsListDB();
$vpsList = $listDb->query('SELECT * FROM vps_list ORDER BY config_id, vps_id');
// 为每个VPS添加site_type和site_url信息
foreach ($vpsList as &$vps) {
$configId = $vps['config_id'];
if (isset($configMap[$configId])) {
$vps['site_type'] = $configMap[$configId]['site_type'];
// 修正site_url移除末尾的/v1或/v1/
$siteUrl = $configMap[$configId]['site_url'];
$siteUrl = rtrim($siteUrl, '/'); // 移除末尾的 /
if (substr($siteUrl, -3) === '/v1') {
$siteUrl = substr($siteUrl, 0, -3); // 移除 /v1
}
$vps['site_url'] = $siteUrl;
} else {
$vps['site_type'] = 'unknown';
$vps['site_url'] = '';
}
}
unset($vps); // 解除引用
// 调试信息(临时)
error_log("Configs count: " . count($configs));
error_log("VPS List count: " . count($vpsList));
if (!empty($vpsList)) {
error_log("First VPS: " . json_encode($vpsList[0]));
}
// 按配置分组
$vpsByConfig = [];
foreach ($vpsList as $vps) {
$configId = $vps['config_id'];
if (!isset($vpsByConfig[$configId])) {
$vpsByConfig[$configId] = [];
}
$vpsByConfig[$configId][] = $vps;
}
// 统计信息
$totalCount = count($vpsList);
$runningCount = 0;
$offCount = 0;
foreach ($vpsList as $vps) {
if ($vps['status'] === 'on') {
$runningCount++;
} elseif ($vps['status'] === 'off') {
$offCount++;
}
}
?>
<!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管理面板 - VPS Hub</title>
<style>
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 15px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.btn-refresh {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-refresh:hover {
background-color: #0056b3;
}
/* 消息自动消失动画 */
.success-message, .error-message {
transition: opacity 0.5s ease-out;
}
.fade-out {
opacity: 0;
}
.config-section {
margin-bottom: 30px;
}
.config-title {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 20px;
border-radius: 8px 8px 0 0;
font-size: 18px;
font-weight: bold;
}
.vps-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: 0 0 8px 8px;
}
.vps-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
.vps-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.card-header {
padding: 15px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.vps-id {
font-size: 18px;
font-weight: bold;
color: #333;
}
.status-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
color: white;
}
.card-body {
padding: 15px;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
color: #666;
font-size: 14px;
}
.info-value {
color: #333;
font-weight: 500;
font-size: 14px;
}
.power-status {
padding: 10px 15px;
background: #f8f9fa;
display: flex;
align-items: center;
gap: 8px;
}
.power-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
}
.power-on {
background-color: #28a745;
box-shadow: 0 0 8px #28a745;
}
.power-off {
background-color: #dc3545;
}
.power-unknown {
background-color: #6c757d;
}
.power-text {
font-size: 14px;
color: #666;
}
.card-actions {
padding: 15px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-success:hover {
background-color: #218838;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
.btn-warning {
background-color: #ffc107;
color: #333;
grid-column: span 2;
}
.btn-warning:hover {
background-color: #e0a800;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state h3 {
font-size: 24px;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🖥️ VPS Hub 管理面板</h1>
<p>实时查看和管理您的云服务器</p>
</div>
<div class="content">
<?php echo $message; ?>
<!-- 工具栏 -->
<div class="toolbar">
<form method="POST" style="display: inline;">
<button type="submit" name="action" value="refresh_vps_list" class="btn-refresh">
🔄 手动刷新VPS列表
</button>
</form>
<a href="config_add.php?pass=<?php echo urlencode($pass); ?>" class="btn btn-success">
添加配置源
</a>
</div>
<!-- 统计信息 -->
<div class="stats-bar">
<div class="stats-item">
总计:<strong><?php echo $totalCount; ?></strong> 台服务器
</div>
<div class="stats-item">
运行中:<strong><?php echo $runningCount; ?></strong> 台
</div>
<div class="stats-item">
已关机:<strong><?php echo $offCount; ?></strong> 台
</div>
</div>
<?php if (empty($configs)): ?>
<div class="empty-state">
<h3>📭 暂无配置</h3>
<p>请先添加VPS配置才能开始管理</p>
<a href="config_add.php?pass=<?php echo urlencode($pass); ?>" class="btn btn-success" style="margin-top: 20px;">
添加第一个配置
</a>
</div>
<?php else: ?>
<!-- 按配置分组显示VPS -->
<?php foreach ($configs as $config):
$configVps = $vpsByConfig[$config['id']] ?? [];
$typeMap = [
'mofang' => '魔方平台',
'aliyun' => '阿里云',
'tencent' => '腾讯云'
];
$typeName = $typeMap[$config['site_type']] ?? $config['site_type'];
?>
<div class="config-section">
<div class="config-title">
<?php echo htmlspecialchars($config['api_label']); ?>
(<?php echo htmlspecialchars($typeName); ?>)
- <?php echo count($configVps); ?> 台VPS
</div>
<?php if (empty($configVps)): ?>
<div class="empty-state" style="background: white; padding: 40px;">
<p>此配置下暂无VPS请点击"手动刷新VPS列表"获取</p>
</div>
<?php else: ?>
<div class="vps-grid">
<?php foreach ($configVps as $vps):
$statusClass = 'power-unknown';
$statusText = '未知';
$statusColor = '#999';
if ($vps['status'] === 'on') {
$statusClass = 'power-on';
$statusText = '运行中';
$statusColor = '#28a745';
} elseif ($vps['status'] === 'off') {
$statusClass = 'power-off';
$statusText = '已关机';
$statusColor = '#dc3545';
}
?>
<div class="vps-card">
<div class="card-header">
<div class="vps-id">#<?php echo $vps['vps_id']; ?></div>
<span class="status-badge" style="background-color: <?php echo $statusColor; ?>">
<?php echo $statusText; ?>
</span>
</div>
<div class="card-body">
<?php if ($vps['ip_address']): ?>
<div class="info-row">
<span class="info-label">IP地址</span>
<span class="info-value"><?php echo htmlspecialchars($vps['ip_address']); ?></span>
</div>
<?php endif; ?>
<?php if ($vps['product_name']): ?>
<div class="info-row">
<span class="info-label">产品名称</span>
<span class="info-value"><?php echo htmlspecialchars($vps['product_name']); ?></span>
</div>
<?php endif; ?>
<?php if ($vps['cpu_cores'] || $vps['memory_size']): ?>
<div class="info-row">
<span class="info-label">CPU/内存</span>
<span class="info-value">
<?php
$parts = [];
if ($vps['cpu_cores']) {
$parts[] = $vps['cpu_cores'] . ' 核';
}
if ($vps['memory_size']) {
$parts[] = htmlspecialchars($vps['memory_size']);
}
echo implode(' / ', $parts);
?>
</span>
</div>
<?php endif; ?>
<?php if ($vps['disk_size'] || $vps['bandwidth'] || $vps['os_type']): ?>
<div class="info-row">
<span class="info-label">磁盘/带宽/系统</span>
<span class="info-value"><?php echo htmlspecialchars($vps['disk_size']) . ' / ' . htmlspecialchars($vps['bandwidth']) . ' / ' . htmlspecialchars($vps['os_type']); ?></span>
</div>
<?php endif; ?>
</div>
<div class="power-status">
<span class="power-indicator <?php echo $statusClass; ?>"></span>
<span class="power-text"><?php echo $statusText; ?></span>
</div>
<form method="POST" style="margin: 0;">
<input type="hidden" name="host_id" value="<?php echo $vps['vps_id']; ?>">
<input type="hidden" name="config_id" value="<?php echo $vps['config_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('确认硬重启?')">
🔄 硬重启
</button>
</div>
</form>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<center><div class="copyright">
© 2026 Gaming Master Cybersecurity |
<a href="https://git.masonliu.com/MasonLiu/VPSHUB" target="_blank">MasonLiu In Gitea</a>
</div></center>
</div>
<script>
// 操作成功提示5秒后自动消失
document.addEventListener('DOMContentLoaded', function() {
const messages = document.querySelectorAll('.success-message, .error-message');
messages.forEach(function(message) {
setTimeout(function() {
message.classList.add('fade-out');
// 等待动画完成后移除元素
setTimeout(function() {
message.style.display = 'none';
}, 500);
}, 5000); // 5秒后开始淡出
});
});
</script>
</body>
</html>

723
mofangidc.php Normal file
View File

@ -0,0 +1,723 @@
<?php
/**
* 魔方平台API接口封装
* 所有智简魔方的操作API以及相关函数存储在此文件中
*/
require_once __DIR__ . '/app/db_helper.php';
// Token缓存文件路径
define('TOKEN_CACHE_FILE', __DIR__ . '/app/token.php');
define('TOKEN_EXPIRE_TIME', 7200); // Token过期时间2小时
/**
* 获取缓存的Token
* @param int $configId 配置ID
* @return string|null Token字符串或null
*/
function getCachedToken($configId) {
if (!file_exists(TOKEN_CACHE_FILE)) {
return null;
}
include TOKEN_CACHE_FILE;
if (!isset($cached_tokens[$configId])) {
return null;
}
$cache = $cached_tokens[$configId];
$currentTime = time();
$tokenAge = $currentTime - $cache['timestamp'];
if ($tokenAge < TOKEN_EXPIRE_TIME) {
return $cache['token'];
}
// Token已过期清除该配置的缓存
unset($cached_tokens[$configId]);
saveTokensFile($cached_tokens);
return null;
}
/**
* 保存Token到缓存文件
* @param int $configId 配置ID
* @param string $token Token字符串
*/
function saveToken($configId, $token) {
$cached_tokens = [];
if (file_exists(TOKEN_CACHE_FILE)) {
include TOKEN_CACHE_FILE;
}
$cached_tokens[$configId] = [
'token' => $token,
'timestamp' => time()
];
saveTokensFile($cached_tokens);
}
/**
* 保存tokens数组到文件
* @param array $cached_tokens tokens数组
*/
function saveTokensFile($cached_tokens) {
$content = "<?php\n";
$content .= "// Token缓存文件 - 自动生成\n";
$content .= "// 警告: 不要手动修改此文件\n\n";
$content .= '$cached_tokens = ' . var_export($cached_tokens, true) . ";\n";
$content .= "?>\n";
file_put_contents(TOKEN_CACHE_FILE, $content);
chmod(TOKEN_CACHE_FILE, 0600);
}
/**
* 魔方平台登录获取JWT Token
* @param string $siteUrl API基础URL
* @param string $account 账户
* @param string $apiKey API密钥
* @return string|null JWT Token或null
*/
function mofangLogin($siteUrl, $account, $apiKey) {
try {
$url = rtrim($siteUrl, '/') . '/v1/login_api';
$data = [
'account' => $account,
'password' => $apiKey
];
$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);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
error_log("魔方登录请求失败HTTP状态码: {$httpCode}");
return null;
}
$result = json_decode($response, true);
if (isset($result['status']) && $result['status'] === 200 && isset($result['jwt'])) {
return $result['jwt'];
} else {
error_log("魔方登录失败: " . ($result['msg'] ?? '未知错误'));
return null;
}
} catch (Exception $e) {
error_log("魔方登录异常: " . $e->getMessage());
return null;
}
}
/**
* 获取有效的Token(先查缓存,没有则重新登录)
* @param int $configId 配置ID
* @return string|null Token或null
*/
function getValidToken($configId) {
// 先尝试从缓存获取
$token = getCachedToken($configId);
if ($token) {
return $token;
}
// 缓存不存在或已过期,重新获取
$db = getVpsDB();
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
if (!$config) {
error_log("配置ID {$configId} 不存在");
return null;
}
$token = mofangLogin($config['site_url'], $config['account'], $config['api_key']);
if ($token) {
saveToken($configId, $token);
}
return $token;
}
/**
* 发送魔方API请求
* @param string $siteUrl API基础URL
* @param string $endpoint API端点
* @param string $method HTTP方法 (GET/POST/PUT)
* @param array $data 请求数据
* @param int $configId 配置ID
* @return array|null 响应数据或null
*/
function mofangApiRequest($siteUrl, $endpoint, $method = 'GET', $data = [], $configId = null) {
// 获取Token
if ($configId) {
$token = getValidToken($configId);
} else {
// 如果没有configId尝试从第一个配置获取
$db = getVpsDB();
$configs = $db->query('SELECT * FROM configs LIMIT 1');
if (!$configs) {
return null;
}
$config = $configs[0];
$token = getValidToken($config['id']);
$siteUrl = $config['site_url'];
}
if (!$token) {
return ['status' => 400, 'msg' => '获取Token失败'];
}
$url = rtrim($siteUrl, '/') . $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);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
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);
if ($httpCode !== 200) {
return ['status' => $httpCode, 'msg' => 'HTTP请求失败'];
}
$result = json_decode($response, true);
// 如果Token失效(405),清除缓存并重试
if (isset($result['status']) && $result['status'] === 405 && $configId) {
// 清除缓存
$cached_tokens = [];
if (file_exists(TOKEN_CACHE_FILE)) {
include TOKEN_CACHE_FILE;
}
unset($cached_tokens[$configId]);
saveTokensFile($cached_tokens);
// 重试一次
return mofangApiRequest($siteUrl, $endpoint, $method, $data, $configId);
}
return $result;
}
/**
* 获取VPS列表
* @param int $configId 配置ID
* @param int $page 页码
* @param int $limit 每页数量
* @return array|null VPS列表数据或null
*/
function mofangGetVpsList($configId, $page = 1, $limit = 100) {
$db = getVpsDB();
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
if (!$config) {
return null;
}
$endpoint = "/v1/hosts?page={$page}&limit={$limit}";
return mofangApiRequest($config['site_url'], $endpoint, 'GET', [], $configId);
}
/**
* 获取VPS状态
* @param int $configId 配置ID
* @param int $vpsId VPS ID
* @param bool $updateDb 是否更新数据库默认true
* @return array|null 状态数据或null
*/
function mofangGetVpsStatus($configId, $vpsId, $updateDb = true) {
$db = getVpsDB();
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
if (!$config) {
return null;
}
$endpoint = "/v1/hosts/{$vpsId}/module/status?type=host";
$result = mofangApiRequest($config['site_url'], $endpoint, 'GET', [], $configId);
// 如果获取成功且需要更新数据库
if ($updateDb && $result && isset($result['status']) && $result['status'] === 200 && isset($result['data'])) {
$statusData = $result['data'];
$status = $statusData['status'] ?? 'unknown';
// 只更新status字段不影响其他字段
$listDb = getVpsListDB();
// 先检查记录是否存在
$existing = $listDb->queryOne(
'SELECT id FROM vps_list WHERE config_id = ? AND vps_id = ?',
[$configId, $vpsId]
);
if ($existing) {
// 记录存在执行UPDATE
$listDb->execute(
'UPDATE vps_list SET status = ?, last_check = CURRENT_TIMESTAMP WHERE config_id = ? AND vps_id = ?',
[$status, $configId, $vpsId]
);
error_log("[mofangGetVpsStatus] VPS {$vpsId} 状态已更新为: {$status}");
} else {
// 记录不存在,插入新记录
$listDb->execute(
'INSERT INTO vps_list (config_id, vps_id, status, last_check) VALUES (?, ?, ?, CURRENT_TIMESTAMP)',
[$configId, $vpsId, $status]
);
error_log("[mofangGetVpsStatus] VPS {$vpsId} 新记录已插入,状态: {$status}");
}
}
return $result;
}
/**
* 获取VPS详细信息
* @param int $configId 配置ID
* @param int $vpsId VPS ID
* @param bool $updateDb 是否更新数据库默认true
* @return array|null 详细信息或null
*/
function mofangGetVpsDetails($configId, $vpsId, $updateDb = true) {
$db = getVpsDB();
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
if (!$config) {
return null;
}
$endpoint = "/v1/hosts/{$vpsId}";
$result = mofangApiRequest($config['site_url'], $endpoint, 'GET', [], $configId);
// 如果获取成功且需要更新数据库
if ($updateDb && $result && isset($result['status']) && $result['status'] === 200 && isset($result['data']['host'])) {
$host = $result['data']['host'];
// 解析详细信息
$updates = []; // 存储要更新的字段
$values = [];
if (isset($host['config_option']) && is_array($host['config_option'])) {
foreach ($host['config_option'] as $option) {
switch ($option['key']) {
case 'cpu':
if (isset($option['value'])) {
preg_match('/(\d+)/', $option['value'], $matches);
if (!empty($matches)) {
$updates[] = 'cpu_cores = ?';
$values[] = intval($matches[1]);
}
}
break;
case 'memory':
if (isset($option['value'])) {
$updates[] = 'memory_size = ?';
$values[] = $option['value'];
}
break;
case 'system_disk_size':
if (isset($option['value'])) {
$updates[] = 'disk_size = ?';
$values[] = $option['value'];
}
break;
case 'bw':
if (isset($option['value'])) {
$updates[] = 'bandwidth = ?';
$values[] = $option['value'];
}
break;
case 'os':
if (isset($option['value'])) {
$updates[] = 'os_type = ?';
$values[] = $option['value'];
}
break;
}
}
}
// 只有当有字段需要更新时才执行UPDATE
if (!empty($updates)) {
// 添加last_check更新
$updates[] = 'last_check = CURRENT_TIMESTAMP';
// 构建动态UPDATE语句
$setClause = implode(', ', $updates);
$values[] = $configId;
$values[] = $vpsId;
$listDb = getVpsListDB();
// 先检查记录是否存在
$existing = $listDb->queryOne(
'SELECT id FROM vps_list WHERE config_id = ? AND vps_id = ?',
[$configId, $vpsId]
);
if ($existing) {
// 记录存在执行UPDATE
$listDb->execute(
"UPDATE vps_list SET {$setClause} WHERE config_id = ? AND vps_id = ?",
$values
);
error_log("[mofangGetVpsDetails] VPS {$vpsId} 详细信息已更新");
} else {
// 记录不存在,插入新记录(只插入获取到的字段)
$columns = [];
$insertValues = [];
$placeholders = [];
// 解析SET子句中的字段
foreach ($updates as $update) {
if (strpos($update, 'last_check') === false) {
list($column, $value) = explode(' = ', $update);
$columns[] = $column;
$insertValues[] = array_shift($values); // 从$values中取出对应的值
$placeholders[] = '?';
}
}
// 添加config_id和vps_id
$columns[] = 'config_id';
$columns[] = 'vps_id';
$insertValues[] = $configId;
$insertValues[] = $vpsId;
if (!empty($columns)) {
$columnStr = implode(', ', $columns);
$placeholderStr = implode(', ', $placeholders);
$listDb->execute(
"INSERT INTO vps_list ({$columnStr}) VALUES ({$placeholderStr})",
$insertValues
);
error_log("[mofangGetVpsDetails] VPS {$vpsId} 新记录已插入");
}
}
}
}
return $result;
}
/**
* VPS开机
* @param int $configId 配置ID
* @param int $vpsId VPS ID
* @return array|null 响应数据或null
*/
function mofangPowerOn($configId, $vpsId) {
$db = getVpsDB();
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
if (!$config) {
return null;
}
$endpoint = "/v1/hosts/{$vpsId}/module/on";
return mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
}
/**
* VPS关机
* @param int $configId 配置ID
* @param int $vpsId VPS ID
* @return array|null 响应数据或null
*/
function mofangPowerOff($configId, $vpsId) {
$db = getVpsDB();
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
if (!$config) {
return null;
}
$endpoint = "/v1/hosts/{$vpsId}/module/off";
return mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
}
/**
* VPS硬重启
* @param int $configId 配置ID
* @param int $vpsId VPS ID
* @return array|null 响应数据或null
*/
function mofangHardReboot($configId, $vpsId) {
$db = getVpsDB();
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
if (!$config) {
return null;
}
$endpoint = "/v1/hosts/{$vpsId}/module/hard_reboot";
return mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
}
/**
* 刷新指定配置的VPS列表并保存到数据库
* @param int $configId 配置ID
* @return bool 是否成功
*/
function refreshVpsListForConfig($configId) {
$db = getVpsDB();
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
if (!$config) {
return false;
}
// 获取VPS列表
$result = mofangGetVpsList($configId);
if (!$result || !isset($result['status']) || $result['status'] !== 200) {
error_log("获取VPS列表失败: " . ($result['msg'] ?? '未知错误'));
return false;
}
$hosts = $result['data']['host'] ?? [];
if (empty($hosts)) {
return true; // 没有VPS也算成功
}
// 批量保存到数据库
$listDb = getVpsListDB();
foreach ($hosts as $host) {
// 解析详细信息
$cpuCores = null;
$memorySize = null;
$diskSize = null;
$bandwidth = null;
$osType = null;
if (isset($host['config_option']) && is_array($host['config_option'])) {
foreach ($host['config_option'] as $option) {
switch ($option['key']) {
case 'cpu':
if (isset($option['value'])) {
// 提取数字,例如 "16核" -> 16
preg_match('/(\d+)/', $option['value'], $matches);
if (!empty($matches)) {
$cpuCores = intval($matches[1]);
}
}
break;
case 'memory':
if (isset($option['value'])) {
$memorySize = $option['value']; // 例如 "16G"
}
break;
case 'system_disk_size':
if (isset($option['value'])) {
$diskSize = $option['value']; // 例如 "Lin50G,Win50G"
}
break;
case 'bw':
if (isset($option['value'])) {
$bandwidth = $option['value']; // 例如 "70Mbps"
}
break;
case 'os':
if (isset($option['value'])) {
$osType = $option['value']; // 例如 "Debian-12.0_x64"
}
break;
}
}
}
// 使用INSERT OR REPLACE防止重复数据
// 注意需要保留原有的status字段避免被覆盖为NULL
$existing = $listDb->queryOne(
'SELECT status FROM vps_list WHERE config_id = ? AND vps_id = ?',
[$configId, $host['id']]
);
$currentStatus = $existing ? $existing['status'] : null;
$listDb->execute(
'INSERT OR REPLACE INTO vps_list (config_id, vps_id, domain, ip_address, product_name, cpu_cores, memory_size, disk_size, bandwidth, os_type, status, section, last_check) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)',
[
$configId,
$host['id'],
$host['domain'] ?? null,
$host['dedicatedip'] ?? null,
$host['product_name'] ?? null,
$cpuCores,
$memorySize,
$diskSize,
$bandwidth,
$osType,
$currentStatus, // 保留原有状态
$config['auto_monitor'] ? 1 : 0
]
);
}
return true;
}
/**
* 刷新所有配置的VPS列表
* @return int 成功刷新的配置数量
*/
function refreshAllVpsLists() {
$db = getVpsDB();
$configs = $db->query('SELECT * FROM configs');
$successCount = 0;
foreach ($configs as $config) {
if (refreshVpsListForConfig($config['id'])) {
$successCount++;
}
}
return $successCount;
}
/**
* 获取单个VPS的状态并更新到数据库
* @param int $configId 配置ID
* @param int $vpsId VPS ID
* @return array|null 状态信息或null
*/
function updateVpsStatusToDb($configId, $vpsId) {
$db = getVpsDB();
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
if (!$config) {
error_log("配置ID {$configId} 不存在");
return null;
}
// 调用API获取VPS状态不自动更新数据库由本函数统一处理
$result = mofangGetVpsStatus($configId, $vpsId, false);
if (!$result || !isset($result['status']) || $result['status'] !== 200) {
error_log("获取VPS {$vpsId} 状态失败: " . ($result['msg'] ?? '未知错误'));
return null;
}
$statusData = $result['data'];
$status = $statusData['status'] ?? 'unknown';
$des = $statusData['des'] ?? '未知';
// 检查记录是否存在
$listDb = getVpsListDB();
$existing = $listDb->queryOne(
'SELECT id FROM vps_list WHERE config_id = ? AND vps_id = ?',
[$configId, $vpsId]
);
if ($existing) {
// 记录存在执行UPDATE
$listDb->execute(
'UPDATE vps_list SET status = ?, last_check = CURRENT_TIMESTAMP WHERE config_id = ? AND vps_id = ?',
[$status, $configId, $vpsId]
);
error_log("[updateVpsStatusToDb] VPS {$vpsId} 状态已更新为: {$status}");
} else {
// 记录不存在,插入新记录
$listDb->execute(
'INSERT INTO vps_list (config_id, vps_id, status, last_check) VALUES (?, ?, ?, CURRENT_TIMESTAMP)',
[$configId, $vpsId, $status]
);
error_log("[updateVpsStatusToDb] VPS {$vpsId} 新记录已插入,状态: {$status}");
}
return [
'vps_id' => $vpsId,
'status' => $status,
'des' => $des,
'updated' => true
];
}
/**
* 批量获取VPS状态并更新到数据库
* @param int $configId 配置ID
* @param array $vpsIds VPS ID数组为空则更新该配置下所有VPS
* @return array 更新结果统计
*/
function batchUpdateVpsStatus($configId, $vpsIds = []) {
$db = getVpsDB();
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
if (!$config) {
return ['success' => 0, 'failed' => 0, 'error' => '配置不存在'];
}
// 如果未指定VPS IDs获取该配置下的所有VPS
if (empty($vpsIds)) {
$listDb = getVpsListDB();
$vpsList = $listDb->query('SELECT vps_id FROM vps_list WHERE config_id = ?', [$configId]);
$vpsIds = array_column($vpsList, 'vps_id');
}
if (empty($vpsIds)) {
return ['success' => 0, 'failed' => 0, 'error' => '没有VPS需要更新'];
}
$successCount = 0;
$failedCount = 0;
$results = [];
foreach ($vpsIds as $vpsId) {
$result = updateVpsStatusToDb($configId, $vpsId);
if ($result) {
$successCount++;
$results[] = $result;
} else {
$failedCount++;
$results[] = [
'vps_id' => $vpsId,
'status' => 'error',
'des' => '获取状态失败',
'updated' => false
];
}
}
return [
'success' => $successCount,
'failed' => $failedCount,
'total' => count($vpsIds),
'results' => $results
];
}
?>

205
static/config.js Normal file
View File

@ -0,0 +1,205 @@
/**
* VPS Hub 配置管理页面交互脚本
*/
/**
* 切换网站URL输入框的提示和默认值
* @param {string} siteType - 网站类型
*/
function toggleSiteUrl(siteType) {
const urlGroup = document.getElementById('site_url_group');
const urlInput = document.getElementById('site_url_input');
if (!urlInput) return;
switch(siteType) {
case 'mofang':
urlInput.placeholder = '留空使用默认值: https://www.heyunidc.cn/v1';
urlInput.required = false;
break;
case 'aliyun':
urlInput.placeholder = '例如: https://ecs.aliyuncs.com';
urlInput.required = true;
break;
case 'tencent':
urlInput.value = 'https://cvm.tencentcloudapi.com/';
urlInput.placeholder = '腾讯云API地址';
urlInput.required = true;
break;
default:
urlInput.placeholder = '输入API地址';
urlInput.required = true;
}
}
/**
* 验证密码格式
* @returns {boolean} 是否通过验证
*/
function validatePassword() {
const passwordInput = document.getElementById('api_pass');
if (!passwordInput) return true;
const password = passwordInput.value;
if (password.length < 8) {
alert('密码至少需要8位');
return false;
}
if (!/^[a-zA-Z0-9]+$/.test(password)) {
alert('密码只能包含字母和数字!');
return false;
}
return true;
}
/**
* 确认删除操作
* @param {string} message - 确认消息
* @returns {boolean} 是否确认
*/
function confirmDelete(message) {
return confirm(message || '确认删除此配置?');
}
/**
* 显示加载状态
* @param {HTMLElement} button - 按钮元素
*/
function showLoading(button) {
if (!button) return;
const originalText = button.textContent;
button.disabled = true;
button.textContent = '加载中...';
button.dataset.originalText = originalText;
}
/**
* 隐藏加载状态
* @param {HTMLElement} button - 按钮元素
*/
function hideLoading(button) {
if (!button) return;
button.disabled = false;
button.textContent = button.dataset.originalText || '提交';
}
/**
* AJAX提交表单
* @param {HTMLFormElement} form - 表单元素
* @param {Function} successCallback - 成功回调
* @param {Function} errorCallback - 失败回调
*/
function submitFormAjax(form, successCallback, errorCallback) {
const formData = new FormData(form);
const submitButton = form.querySelector('button[type="submit"]');
showLoading(submitButton);
fetch(form.action || window.location.href, {
method: form.method || 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
hideLoading(submitButton);
if (data.status === 200 || data.success) {
if (successCallback) successCallback(data);
} else {
if (errorCallback) errorCallback(data);
else alert('操作失败: ' + (data.msg || '未知错误'));
}
})
.catch(error => {
hideLoading(submitButton);
console.error('请求失败:', error);
if (errorCallback) errorCallback(error);
else alert('网络错误,请稍后重试');
});
}
/**
* 刷新VPS列表
*/
function refreshVpsList() {
const button = document.querySelector('.btn-refresh');
if (!button) return;
showLoading(button);
// 创建临时表单提交
const form = document.createElement('form');
form.method = 'POST';
form.action = window.location.href;
const actionInput = document.createElement('input');
actionInput.type = 'hidden';
actionInput.name = 'action';
actionInput.value = 'refresh_vps_list';
form.appendChild(actionInput);
document.body.appendChild(form);
form.submit();
}
/**
* 初始化页面
*/
document.addEventListener('DOMContentLoaded', function() {
// 自动聚焦第一个输入框
const firstInput = document.querySelector('input:not([type="hidden"]):not([type="checkbox"])');
if (firstInput && !firstInput.value) {
firstInput.focus();
}
// 为所有删除按钮添加确认对话框
const deleteButtons = document.querySelectorAll('.btn-delete');
deleteButtons.forEach(button => {
button.addEventListener('click', function(e) {
if (!confirmDelete('确认删除此配置?此操作不可恢复!')) {
e.preventDefault();
}
});
});
// 为所有操作按钮添加确认对话框
const actionButtons = document.querySelectorAll('.btn-success, .btn-danger, .btn-warning');
actionButtons.forEach(button => {
if (button.type === 'submit' && button.name === 'action') {
button.addEventListener('click', function(e) {
let message = '';
switch(this.value) {
case 'on':
message = '确认开机?';
break;
case 'off':
message = '确认关机?';
break;
case 'reboot':
message = '确认硬重启?';
break;
}
if (message && !confirm(message)) {
e.preventDefault();
}
});
}
});
console.log('VPS Hub 页面已加载');
});
// 导出函数供全局使用
window.VPSHub = {
toggleSiteUrl,
validatePassword,
confirmDelete,
refreshVpsList,
showLoading,
hideLoading
};

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -336,3 +336,124 @@ body {
font-size: 12px;
}
}
/* VPS Hub 新增样式 */
.config-container {
max-width: 600px;
margin: 50px auto;
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
}
.config-header {
text-align: center;
margin-bottom: 30px;
}
.config-header h1 {
color: #667eea;
margin-bottom: 10px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
}
.form-group input,
.form-group select {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #667eea;
}
.help-text {
font-size: 12px;
color: #999;
margin-top: 5px;
}
.btn-submit {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
transition: transform 0.2s;
}
.btn-submit:hover {
transform: translateY(-2px);
}
.error-message {
background: #fee;
border-left: 4px solid #dc3545;
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 20px;
color: #dc3545;
}
.success-message {
background: #efe;
border-left: 4px solid #28a745;
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 20px;
color: #28a745;
}
.info-box {
background: #e7f3ff;
border-left: 4px solid #007bff;
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 20px;
color: #0056b3;
font-size: 14px;
}
.info-box code {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
}
.copyright {
margin-top: 30px;
padding: 20px;
color: #999;
font-size: 14px;
}
.copyright a {
color: #667eea;
text-decoration: none;
}
.copyright a:hover {
text-decoration: underline;
}

View File

@ -1,505 +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>
<center><div class="copyright">
© 2026 Gaming Master Cybersecurity |
<a href="https://git.masonliu.com/MasonLiu/HeyunIDC" target="_blank">MasonLiu In Gitea</a>
</div></center>
</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>
<center><div class="copyright">
© 2026 Gaming Master Cybersecurity |
<a href="https://git.masonliu.com/MasonLiu/HeyunIDC" target="_blank">MasonLiu In Gitea</a>
</div></center>
</div>
</div>
</body>
</html>

69
新开发文档.md Normal file
View File

@ -0,0 +1,69 @@
### WEB运行
#### 配置文件 config_add.php
用户可访问config_add.php添加VPS配置项
初次访问该php页面时用户需设置网页自定义访问密码API_PASS运行输入大小写字母以及数字最低8位数
存在API_PASS时需跟参数?pass=API_PASS才允许访问
用户点击添加配置按钮,新列出一列,分别是:
旧数据也会读取并展示
序号自动生成网站类型魔方、阿里云、腾讯云等网站链接若为魔方则设置为可填腾讯云为https://cvm.tencentcloudapi.com/阿里云后面再写账户魔方账户邮箱或手机号其他云后续添加密钥魔方输入API KEY是否开启自动开机监控魔方默认开启其他云默认关闭
1. 用户点击添加数据后,在./app/db/文件夹内生成sqlite数据库 vps.db用于存储该类数据
2. 首次添加后自动调用所有API进行VPS列表查询查询结果记录在./app/db中的vpslist.db中之后此类查询每天仅需查询一次是否开启自动开机监控标记为开启时vps的最后一列section也标记为True
3. 进行ping访问测试后将结果记录于./app/db/中的status.db中保留最近三十天数据
记录项目标IP地址或域名状态正常或异常延时单位ms 毫秒)
每天0点时总结前一天中各VPS的ping响应数据记录在该数据库中的新表 vps_summary 中。该表需包含以下字段VPS标识vps_id、统计日期date、平均延迟avg_latency_ms、最大延迟max_latency_ms、最小延迟min_latency_ms。同时分级记录延迟分布情况延迟低于100ms的次数count_under_100、100ms~300ms的次数count_100_to_300、300ms~500ms的次数count_300_to_500以及延迟超过500ms或ping异常的丢包/超时次数count_abnormal。根据延迟表现计算高可用评分100ms以内权重最高300ms以内为良好500ms以内为可用超过500ms标记为低可用异常则标记为不可用。记录为新字段服务器可用性优秀/良好/一般/不可用)
4. 首次使用网页并添加数据后,调用./app/install.sh进行system服务注册
#### 首页 index.php
1. 查询是否存在API_PASS若存在则需跟参数?pass=API_PASS才能访问
2. 若不存在则跳转至config_add.php页
获取到的认证token存储于./app/token.php中记录为两小时后续调用API时超时或提示失效再重新获取
3. 右上角放置“添加配置源”的按钮
4. 查询vps.db并依次获取vps列表并查询其状态信息使用v1/hosts/:id接口获取VPS的CPU核数操作系统内存大小系统盘大小以及带宽大小提供开机、关机、硬重启按钮
5. 左上角新建一个按钮用于手动再次获取vps列表以及信息不再自动获取信息。
#### 魔方页 mofangidc.php
所有智简魔方的操作API以及相关函数存储在此网页下
### Python运行
#### 安装
通过install.sh自动安装
功能点:
1. 探测当前系统是否存在idc_monitor的系统服务若不存在则进入第二步若存在则重启一下该服务
2. 读取python版本并探测是否存在pip自动下载python-pip
3. pip下载所需环境和依赖项
4. 新建一个idc_system的系统服务持续化运行当前路径下的monitor.py脚本
#### 读取配置
1. 读取vpslist中的数据则对其进行ping测试
2. 测试结果保存至status.db中
获取到的认证token存储于./app/token.php中记录为两小时
3. 若section为True且ping测试失败调用API对该机器进行状态查询若返回状态为关机则尝试开机隔60秒后再进行状态查询并再次尝试开机60秒后查询还是为异常的话则记录异常进入下一个循环多台机器关机时依次进行状态查询和开机然后间隔60秒再进行操作不同的机器不需要间隔