VPSHUB/mofangidc.php
2026-05-30 15:06:24 +08:00

813 lines
27 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
require_once __DIR__ . '/app/db_helper.php';
require_once __DIR__ . '/app/logger.php';
define('TOKEN_CACHE_FILE', __DIR__ . '/app/token.php');
define('TOKEN_EXPIRE_TIME', 7200);
/**
* 获取缓存的Token
*/
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到缓存文件
*/
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
*/
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) {
Logger::error("魔方登录请求失败HTTP状态码: {$httpCode}", 'mofangLogin');
return null;
}
$result = json_decode($response, true);
if (isset($result['status']) && $result['status'] === 200 && isset($result['jwt'])) {
return $result['jwt'];
} else {
Logger::error("魔方登录失败: " . ($result['msg'] ?? '未知错误'), 'mofangLogin');
return null;
}
} catch (Exception $e) {
Logger::error("魔方登录异常: " . $e->getMessage(), 'mofangLogin');
return null;
}
}
/**
* 获取有效的Token(先查缓存,没有则重新登录)
*/
function getValidToken($configId) {
// 先尝试从缓存获取
$token = getCachedToken($configId);
if ($token) {
return $token;
}
// 缓存不存在或已过期,重新获取
$db = getVpsDB();
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
if (!$config) {
Logger::error("配置ID {$configId} 不存在", 'getValidToken');
return null;
}
$token = mofangLogin($config['site_url'], $config['account'], $config['api_key']);
if ($token) {
saveToken($configId, $token);
}
return $token;
}
/**
* 发送魔方API请求
*/
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列表
*/
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状态
*/
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'];
$rawStatus = $statusData['status'] ?? 'unknown';
// 状态映射:将魔方平台的原始状态转换为标准状态
$statusMap = [
'on' => 'on', // 开机
'off' => 'off', // 关机
'running' => 'on', // 运行中
'stopped' => 'off', // 已停止
'process' => 'process', // 处理中(开机/关机/重启过程中)
'pending' => 'process', // 等待中
'installing' => 'process', // 安装中
'rebooting' => 'process', // 重启中
'unknown' => 'unknown' // 未知
];
$status = $statusMap[$rawStatus] ?? $rawStatus;
$listDb = getVpsListDB();
// 先获取该VPS的IP地址用于精确匹配
$vpsInfo = $listDb->queryOne(
'SELECT id, ip_address FROM vps_list WHERE config_id = ? AND vps_id = ?',
[$configId, $vpsId]
);
if ($vpsInfo) {
// 记录存在执行UPDATE
$success = $listDb->execute(
'UPDATE vps_list SET status = ?, last_check = CURRENT_TIMESTAMP WHERE id = ?',
[$status, $vpsInfo['id']]
);
if ($success) {
Logger::info("[mofangGetVpsStatus] VPS {$vpsId} (IP: {$vpsInfo['ip_address']}) 状态已更新为: {$status}", 'mofangGetVpsStatus');
} else {
Logger::error("[mofangGetVpsStatus] VPS {$vpsId} 状态更新失败", 'mofangGetVpsStatus');
}
} else {
// 记录不存在,插入新记录
$success = $listDb->execute(
'INSERT INTO vps_list (config_id, vps_id, status, last_check) VALUES (?, ?, ?, CURRENT_TIMESTAMP)',
[$configId, $vpsId, $status]
);
if ($success) {
Logger::info("[mofangGetVpsStatus] VPS {$vpsId} 新记录已插入,状态: {$status}", 'mofangGetVpsStatus');
} else {
Logger::error("[mofangGetVpsStatus] VPS {$vpsId} 新记录插入失败", 'mofangGetVpsStatus');
}
}
} elseif ($updateDb && (!$result || !isset($result['status']) || $result['status'] !== 200)) {
// API调用失败不更新数据库保留原有状态
Logger::warning("[mofangGetVpsStatus] VPS {$vpsId} API调用失败保留原有状态", 'mofangGetVpsStatus');
}
return $result;
}
/**
* 获取VPS详细信息
*/
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['status'])) {
$rawStatus = $host['status'];
// 状态映射:将魔方平台的原始状态转换为标准状态
$statusMap = [
'on' => 'on', // 开机
'off' => 'off', // 关机
'running' => 'on', // 运行中
'stopped' => 'off', // 已停止
'process' => 'process', // 处理中(开机/关机/重启过程中)
'pending' => 'process', // 等待中
'installing' => 'process', // 安装中
'rebooting' => 'process', // 重启中
'unknown' => 'unknown' // 未知
];
$status = $statusMap[$rawStatus] ?? $rawStatus;
$updates[] = 'status = ?';
$values[] = $status;
}
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
);
Logger::info("[mofangGetVpsDetails] VPS {$vpsId} 详细信息已更新" . (isset($status) ? ",状态: {$status}" : ""), 'mofangGetVpsDetails');
} 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
);
Logger::info("[mofangGetVpsDetails] VPS {$vpsId} 新记录已插入", 'mofangGetVpsDetails');
}
}
}
}
return $result;
}
/**
* VPS开机
*/
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";
$result = mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
// 开机操作后立即获取状态并更新数据库
if ($result && isset($result['status']) && $result['status'] === 200) {
sleep(2); // 等待2秒让状态生效
mofangGetVpsStatus($configId, $vpsId, true);
}
return $result;
}
/**
* VPS关机
*/
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";
$result = mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
// 关机操作后立即获取状态并更新数据库
if ($result && isset($result['status']) && $result['status'] === 200) {
sleep(2); // 等待2秒让状态生效
mofangGetVpsStatus($configId, $vpsId, true);
}
return $result;
}
/**
* VPS硬重启
*/
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";
$result = mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
// 硬重启操作后立即获取状态并更新数据库
if ($result && isset($result['status']) && $result['status'] === 200) {
sleep(3); // 等待3秒让重启生效
mofangGetVpsStatus($configId, $vpsId, true);
}
return $result;
}
/**
* 刷新指定配置的VPS列表并保存到数据库
*/
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) {
Logger::error("获取VPS列表失败: " . ($result['msg'] ?? '未知错误'), 'refreshVpsListForConfig');
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;
}
}
}
// 基于vps_id和ip_address去重
$existing = $listDb->queryOne(
'SELECT id, status FROM vps_list WHERE vps_id = ? AND ip_address = ?',
[$host['id'], $host['dedicatedip'] ?? null]
);
if ($existing) {
// 记录已存在只更新非status字段保留原有status
$listDb->execute(
'UPDATE vps_list SET domain = ?, product_name = ?, cpu_cores = ?, memory_size = ?, disk_size = ?, bandwidth = ?, os_type = ?, amount = ?, nextduedate = ?, section = ?, last_check = CURRENT_TIMESTAMP WHERE id = ?',
[
$host['domain'] ?? null,
$host['product_name'] ?? null,
$cpuCores,
$memorySize,
$diskSize,
$bandwidth,
$osType,
$host['amount'] ?? null,
$host['nextduedate'] ?? null,
$config['auto_monitor'] ? 1 : 0,
$existing['id']
]
);
} else {
// 新记录插入所有字段status设为null稍后通过状态接口获取
$listDb->execute(
'INSERT INTO vps_list (config_id, vps_id, domain, ip_address, product_name, cpu_cores, memory_size, disk_size, bandwidth, os_type, amount, nextduedate, status, section, last_check) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, CURRENT_TIMESTAMP)',
[
$configId,
$host['id'],
$host['domain'] ?? null,
$host['dedicatedip'] ?? null,
$host['product_name'] ?? null,
$cpuCores,
$memorySize,
$diskSize,
$bandwidth,
$osType,
$host['amount'] ?? null,
$host['nextduedate'] ?? null,
$config['auto_monitor'] ? 1 : 0
]
);
}
}
// 刷新列表后批量获取每个VPS的状态
Logger::info("[refreshVpsListForConfig] 开始批量获取 {$configId} 配置下 " . count($hosts) . " 台VPS的状态", 'refreshVpsListForConfig');
foreach ($hosts as $host) {
// 调用专门的状态接口获取开关机状态
mofangGetVpsStatus($configId, $host['id'], true);
// 避免频繁请求每次请求间隔0.5秒
usleep(500000); // 500毫秒
}
Logger::info("[refreshVpsListForConfig] 配置 {$configId} 的VPS列表和状态刷新完成", 'refreshVpsListForConfig');
return true;
}
/**
* 刷新所有配置的VPS列表
*/
function refreshAllVpsLists() {
$db = getVpsDB();
$configs = $db->query('SELECT * FROM configs');
$successCount = 0;
foreach ($configs as $config) {
if (refreshVpsListForConfig($config['id'])) {
$successCount++;
}
}
return $successCount;
}
/**
* 获取单个VPS的状态并更新到数据库
*/
function updateVpsStatusToDb($configId, $vpsId) {
$db = getVpsDB();
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
if (!$config) {
Logger::error("配置ID {$configId} 不存在", 'updateVpsStatusToDb');
return null;
}
// 调用API获取VPS状态不自动更新数据库由本函数统一处理
$result = mofangGetVpsStatus($configId, $vpsId, false);
if (!$result || !isset($result['status']) || $result['status'] !== 200) {
Logger::error("获取VPS {$vpsId} 状态失败,保留原有状态: " . ($result['msg'] ?? '未知错误'), 'updateVpsStatusToDb');
return null;
}
$statusData = $result['data'];
$rawStatus = $statusData['status'] ?? 'unknown';
$des = $statusData['des'] ?? '未知';
// 状态映射:将魔方平台的原始状态转换为标准状态
$statusMap = [
'on' => 'on', // 开机
'off' => 'off', // 关机
'running' => 'on', // 运行中
'stopped' => 'off', // 已停止
'process' => 'process', // 处理中(开机/关机/重启过程中)
'pending' => 'process', // 等待中
'installing' => 'process', // 安装中
'rebooting' => 'process', // 重启中
'unknown' => 'unknown' // 未知
];
$status = $statusMap[$rawStatus] ?? $rawStatus;
// 检查记录是否存在
$listDb = getVpsListDB();
$vpsInfo = $listDb->queryOne(
'SELECT id, ip_address FROM vps_list WHERE config_id = ? AND vps_id = ?',
[$configId, $vpsId]
);
if ($vpsInfo) {
// 记录存在执行UPDATE
$success = $listDb->execute(
'UPDATE vps_list SET status = ?, last_check = CURRENT_TIMESTAMP WHERE id = ?',
[$status, $vpsInfo['id']]
);
if ($success) {
Logger::info("[updateVpsStatusToDb] VPS {$vpsId} (IP: {$vpsInfo['ip_address']}) 状态已更新为: {$status}", 'updateVpsStatusToDb');
} else {
Logger::error("[updateVpsStatusToDb] VPS {$vpsId} 状态更新失败,保留原有状态", 'updateVpsStatusToDb');
return null;
}
} else {
// 记录不存在,插入新记录
$success = $listDb->execute(
'INSERT INTO vps_list (config_id, vps_id, status, last_check) VALUES (?, ?, ?, CURRENT_TIMESTAMP)',
[$configId, $vpsId, $status]
);
if ($success) {
Logger::info("[updateVpsStatusToDb] VPS {$vpsId} 新记录已插入,状态: {$status}", 'updateVpsStatusToDb');
} else {
Logger::error("[updateVpsStatusToDb] VPS {$vpsId} 新记录插入失败", 'updateVpsStatusToDb');
return null;
}
}
return [
'vps_id' => $vpsId,
'status' => $status,
'des' => $des,
'updated' => true
];
}
/**
* 批量获取VPS状态并更新到数据库
*/
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 = [];
$listDb = getVpsListDB();
$listDb->getConnection()->beginTransaction();
try {
foreach ($vpsIds as $vpsId) {
$result = updateVpsStatusToDb($configId, $vpsId);
if ($result) {
$successCount++;
$results[] = $result;
} else {
$failedCount++;
$results[] = [
'vps_id' => $vpsId,
'status' => 'error',
'des' => '获取状态失败',
'updated' => false
];
}
}
$listDb->getConnection()->commit();
Logger::info("[batchUpdateVpsStatus] 批量更新完成: 成功{$successCount}, 失败{$failedCount}", 'batchUpdateVpsStatus');
} catch (Exception $e) {
$listDb->getConnection()->rollBack();
Logger::error("[batchUpdateVpsStatus] 批量更新失败,已回滚: " . $e->getMessage(), 'batchUpdateVpsStatus');
return ['success' => 0, 'failed' => count($vpsIds), 'error' => '批量更新失败: ' . $e->getMessage()];
}
return [
'success' => $successCount,
'failed' => $failedCount,
'total' => count($vpsIds),
'results' => $results
];
}
?>