Initial commit: 投注游戏平台初始化

This commit is contained in:
li
2026-02-25 01:26:58 +08:00
commit 77ca2cc8b3
275 changed files with 237479 additions and 0 deletions
+682
View File
@@ -0,0 +1,682 @@
<?php
namespace App\Core;
class PluginManager {
protected $db;
protected $pluginDir;
protected $loadedPlugins = [];
protected $router;
protected $logFile;
protected $installedPluginsCache = null;
protected $registeredRoutes = [];
protected $plugins = null;
protected $enabledPlugins = [];
protected $systemRoutes = [];
protected $enabledRoutes = [];
private $lastError = '';
private function setError(string $msg) {
$this->lastError = $msg;
$this->log("[ERROR] $msg");
}
public function getLastError(): string {
return $this->lastError;
}
/**
* 构造函数
* @param object $db 数据库对象
* @param object $router 路由对象
* @param string $pluginDir 插件目录
*/
public function __construct($db, $router, string $pluginDir) {
$this->db = $db;
$this->router = $router;
$this->pluginDir = rtrim($pluginDir, '/');
$rootDir = realpath(__DIR__ . '/../../');
$logDir = $rootDir . '/Storage/log';
// 确保日志目录(如不存在则创建)
if (!is_dir($logDir)) {
if (!mkdir($logDir, 0755, true) && !is_dir($logDir)) {
throw new \RuntimeException("无法创建日志目录: $logDir ,请检查权限");
}
}
// 检查目录可写性
if (!is_writable($logDir)) {
throw new \RuntimeException("日志目录不可写: $logDir ,请检查权限");
}
$this->logFile = $logDir . '/plugin_manager.log';
// 检查日志文件大小并自动清理(大于2MB时)
$this->cleanupLogFile(2); // 传入最大允许的MB数
}
/**
* 清理日志日志文件清理
* @param int $maxSizeMB 最大允许的文件大小(MB)
*/
private function cleanupLogFile(int $maxSizeMB) {
// 检查文件是否存在
if (!file_exists($this->logFile)) {
return;
}
// 转换MB为字节
$maxSizeBytes = $maxSizeMB * 1024 * 1024;
// 获取当前文件大小
$currentSize = filesize($this->logFile);
// 如果文件大小超过限制,清空文件
if ($currentSize > $maxSizeBytes) {
// 先备份当前日志内容(可选)
$backupFile = $this->logFile . '.bak_' . date('YmdHis');
copy($this->logFile, $backupFile);
// 清空日志文件
file_put_contents($this->logFile, '');
// 记录清理日志
$message = "[" . date('Y-m-d H:i:s') . "] 日志文件超过{$maxSizeMB}MB,已自动清理\n";
file_put_contents($this->logFile, $message, FILE_APPEND);
}
}
/**
* 设置系统核心路由
* @param array $routes 格式: [['GET', '/path', 'handler'], ...]
*/
public function setSystemRoutes(array $routes): void {
$this->systemRoutes = [];
foreach ($routes as $route) {
if (count($route) < 2) continue;
[$method, $path] = $route;
$key = strtoupper(trim($method)) . ' ' . trim($path);
$this->systemRoutes[$key] = true;
}
}
/**
* 获取所有已注册路由(系统+已启用插件)
* @return array 路由键名数组
*/
public function getAllRegisteredRoutes(): array {
return array_merge(
array_keys($this->systemRoutes),
array_keys($this->enabledRoutes)
);
}
public function getDB() {
return $this->db;
}
protected function log(string $msg, string $level = 'INFO'): void {
$date = date('Y-m-d H:i:s');
$logMsg = "[$date] [$level] $msg\n";
$logDir = dirname($this->logFile);
try {
if (is_dir($logDir) && is_writable($logDir)) {
file_put_contents($this->logFile, $logMsg, FILE_APPEND);
} else {
error_log("PluginManager log directory not writable: $logDir");
}
} catch (\Throwable $e) {
error_log("Failed to write plugin log: " . $e->getMessage());
}
}
/**
* 获取所有扫描到的插件信息
* @return array
*/
public function getAllPlugins(): array {
$this->scanPlugins();
return $this->plugins;
}
public function scanPlugins() {
if ($this->plugins !== null) {
return $this->plugins;
}
$this->log("scanPlugins called");
$this->plugins = [];
if (!is_dir($this->pluginDir)) {
mkdir($this->pluginDir, 0755, true);
$this->log("Plugin directory created: {$this->pluginDir}");
return $this->plugins;
}
$dirs = scandir($this->pluginDir);
foreach ($dirs as $dir) {
if ($dir === '.' || $dir === '..') continue;
$pluginPath = $this->pluginDir . '/' . $dir;
$pluginFile = $pluginPath . '/mian.php';
if (is_dir($pluginPath) && is_file($pluginFile)) {
$pluginConfig = $this->getPluginInfo($pluginFile, $dir);
if (empty($pluginConfig)) {
$this->log("[WARN] 插件 {$dir} 不规范,已跳过");
continue;
}
$this->plugins[$dir] = array_merge([
'dir' => $dir,
'path' => $pluginPath,
], $pluginConfig);
$this->log("Plugin found: $dir ({$pluginConfig['name']})");
}
}
return $this->plugins;
}
public function getAllPluginIcons(): array
{
$pluginDirs = array_filter(scandir(PLUGIN_PATH . '/'), function($dir) {
return $dir !== '.' && $dir !== '..';
});
$icons = [];
foreach ($pluginDirs as $pluginDir) {
$pluginFile = PLUGIN_PATH . "/{$pluginDir}/mian.php";
if (file_exists($pluginFile)) {
$info = include $pluginFile;
$icons[$pluginDir] = $info['menus'][0]['icon'] ?? 'fa fa-plug';
}
}
return $icons;
}
/**
* 从插件文件头部注释获取插件信息
*/
private function getPluginInfo(string $pluginFile, string $pluginDirName): array {
$info = [];
$arrayConfig = @include $pluginFile;
if (!is_array($arrayConfig)) {
$this->setError("[ERROR] Plugin file {$pluginFile} must return an array");
return [];
}
$lines = file($pluginFile);
if (!$lines) {
$this->setError("[ERROR] Cannot read plugin file: {$pluginFile}");
return [];
}
// 读取前 30 行,兼容大部分注释头
$header = implode('', array_slice($lines, 0, 30));
//$this->log("Header of {$pluginFile}:\n" . $header);
if (preg_match_all('/^\s*\*\s*([A-Za-z ]+):\s*(.+)$/m', $header, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$key = strtolower(str_replace(' ', '_', trim($match[1])));
$value = trim($match[2]);
$info[$key] = $value;
}
}
// 必填字段列表
$requiredFields = ['plugin_name', 'version', 'description', 'author', 'plugin_url'];
// 检查必要字段是否存在且非空
foreach ($requiredFields as $field) {
if (empty($info[$field])) {
$this->setError("[ERROR] Plugin {$pluginFile} 缺少必要字段或为空: {$field}");
return [];
}
}
// 插件文件夹名必须和插件名一致
if ($pluginDirName !== $info['plugin_name']) {
$this->setError("[ERROR] 插件目录名 {$pluginDirName} 与插件名 {$info['plugin_name']} 不一致");
return [];
}
if (isset($info['plugin_name'])) {
$arrayConfig['name'] = $info['plugin_name'];
}
if (isset($info['description'])) {
$arrayConfig['description'] = $info['description'];
}
if (isset($info['version'])) {
$arrayConfig['version'] = $info['version'];
}
if (isset($info['author'])) {
$arrayConfig['author'] = $info['author'];
}
if (isset($info['plugin_url'])) {
$arrayConfig['url'] = $info['plugin_url'];
}
return $arrayConfig;
}
public function clearCache() {
$this->plugins = null;
}
public function getEnabledPluginMenus(): array {
$menus = [];
foreach ($this->enabledPlugins as $plugin) {
if (isset($plugin['menus']) && is_array($plugin['menus'])) {
$menus = array_merge($menus, $plugin['menus']);
}
}
return $menus;
}
/**
* 获取已安装插件列表
* @return array
*/
public function getInstalledPlugins(): array {
// 如果数据库对象不存在或未初始化,直接返回空数组
if (!$this->db || !$this->db->medoo) {
return [];
}
if ($this->installedPluginsCache !== null) {
return $this->installedPluginsCache;
}
try {
$installed = $this->db->select('plugins', '*');
if (!is_array($installed)) {
$installed = [];
}
} catch (\Exception $e) {
// 捕获数据库异常,返回空数组
$installed = [];
}
$installedPlugins = [];
foreach ($installed as $row) {
$installedPlugins[$row['name']] = $row;
}
$this->installedPluginsCache = $installedPlugins;
return $installedPlugins;
}
/**
* 获取所有插件状态信息
* @return array
*/
public function getPluginStatusList(): array {
$this->scanPlugins();
$installedPlugins = $this->getInstalledPlugins();
$result = [];
foreach ($this->plugins as $name => $plugin) {
$installed = isset($installedPlugins[$name]);
$status = $installed ? (int)$installedPlugins[$name]['status'] : 0;
$result[] = [
'name' => $name,
'installed' => $installed ? 1 : 0,
'status' => $status,
'path' => $plugin['path'],
'title' => $installed ? $installedPlugins[$name]['title'] : ($plugin['title'] ?? $name),
'version' => $installed ? $installedPlugins[$name]['version'] : ($plugin['version'] ?? ''),
'description' => $installed ? $installedPlugins[$name]['description'] : ($plugin['description'] ?? ''),
'author' => $installed ? $installedPlugins[$name]['author'] : ($plugin['author'] ?? ''),
'url' => $installed ? $installedPlugins[$name]['url'] : ($plugin['url'] ?? ''),
];
}
return $result;
}
/**
* 获取启用的插件名称列表
* @return array
*/
public function getEnabledPlugins(): array {
$enabled = [];
$installed = $this->getInstalledPlugins();
foreach ($this->plugins as $name => $plugin) {
if (isset($installed[$name]) && $installed[$name]['status'] == 1) {
$enabled[] = $name;
}
}
return $enabled;
}
/**
* 递归加载控制器目录内所有PHP文件
* @param string $dir
*/
protected function loadControllersRecursively(string $dir): void {
if (!is_dir($dir)) {
return;
}
$files = scandir($dir);
foreach ($files as $file) {
if ($file === '.' || $file === '..') continue;
$fullPath = $dir . DIRECTORY_SEPARATOR . $file;
if (is_dir($fullPath)) {
$this->loadControllersRecursively($fullPath);
} elseif (is_file($fullPath) && pathinfo($file, PATHINFO_EXTENSION) === 'php') {
require_once $fullPath;
$this->log("Loaded controller: $fullPath");
}
}
}
private function isAssoc(array $arr): bool {
return array_keys($arr) !== range(0, count($arr) - 1);
}
public function loadEnabledPlugins(): void {
static $loaded = false;
if ($loaded) {
return;
}
$loaded = true;
$this->scanPlugins();
$installed = $this->getInstalledPlugins();
$this->enabledPlugins = [];
$this->registeredRoutes = [];
$this->enabledRoutes = [];
foreach ($this->plugins as $pluginName => $pluginData) {
if (isset($installed[$pluginName]) && $installed[$pluginName]['status'] == 1) {
// 激活插件(调用init
if (!empty($pluginData['init']) && is_callable($pluginData['init'])) {
try {
call_user_func($pluginData['init']);
//$this->log("Initialized plugin $pluginName");
} catch (\Throwable $e) {
$this->log("[ERROR] Exception in init of plugin $pluginName: " . $e->getMessage());
}
}
// 注册普通路由
if (!empty($pluginData['routes']) && is_array($pluginData['routes'])) {
foreach ($pluginData['routes'] as $route) {
if (count($route) < 3) {
$this->log("[WARNING] Invalid route config for plugin $pluginName");
continue;
}
[$method, $path, $handler] = $route;
$method = strtoupper($method);
$routeKey = $method . ' ' . $path;
if (in_array($routeKey, $this->registeredRoutes, true)) {
$this->log("[WARNING] Route conflict: [$method] $path");
continue;
}
$this->router->add($method, $path, $handler);
$this->registeredRoutes[] = $routeKey;
$this->enabledRoutes[$routeKey] = true;
//$this->log("Registered route [$method] $path for plugin $pluginName");
}
}
// 注册路由组
if (!empty($pluginData['route_group'])) {
$groups = $this->isAssoc($pluginData['route_group'])
? [$pluginData['route_group']]
: $pluginData['route_group'];
foreach ($groups as $group) {
if (isset($group['prefix'], $group['namespace'], $group['routes']) && is_array($group['routes'])) {
$this->router->group(
$group['prefix'],
$group['namespace'],
$group['routes']
);
//$this->log("Registered route group [prefix={$group['prefix']}] for plugin $pluginName");
} else {
$this->log("[WARNING] Invalid route_group config in plugin $pluginName");
}
}
}
$this->enabledPlugins[] = $pluginData;
}
}
}
/**
* 安装插件
* @param string $name
* @return bool
*/
public function installPlugin(string $name): bool|string {
if (!isset($this->plugins[$name])) {
$this->log("Install failed: plugin $name not found");
return false;
}
$plugin = $this->plugins[$name];
// 1. 路由冲突检测
if (!empty($plugin['route_group']) && is_array($plugin['route_group'])) {
$prefixes = [];
foreach ($plugin['route_group'] as $group) {
if (isset($group['prefix'])) {
$prefixes[] = $group['prefix'];
}
}
$prefixes = array_unique($prefixes);
if (!$this->checkRouteConflicts($prefixes)) {
return false; // 路由冲突阻止安装
}
}
// 2. 表冲突检测
if (!empty($plugin['tables']) && is_array($plugin['tables'])) {
$conflictTables = [];
foreach ($plugin['tables'] as $table) {
$stmt = $this->db->query("SHOW TABLES LIKE '{$table}'");
if ($stmt && $stmt->fetch()) {
$conflictTables[] = $table;
}
}
if (!empty($conflictTables)) {
$conflictList = implode(', ', $conflictTables);
$errorMsg = "插件 {$name} 安装失败:以下数据表已存在 -> {$conflictList}";
$this->log("[ERROR] $errorMsg");
return $errorMsg; // 返回错误消息字符串阻止安装
}
}
// 3. 插件元信息写入数据库
$pluginData = [
'name' => $name,
'title' => $plugin['title'] ?? $name,
'description' => $plugin['description'] ?? '',
'version' => $plugin['version'] ?? '',
'author' => $plugin['author'] ?? '',
'url' => $plugin['url'] ?? '',
'status' => 1,
];
$count = $this->db->count('plugins', '*', ['name' => $name]);
if ($count > 0) {
$this->db->update('plugins', $pluginData, ['name' => $name]);
} else {
$this->db->insert('plugins', $pluginData);
}
$this->installedPluginsCache = null;
return true;
}
/**
* 路由冲突检测
*/
public function checkRouteConflicts(array $newPrefixes): bool {
$installedPlugins = $this->getInstalledPlugins();
$installedPrefixes = [];
foreach ($this->plugins as $pluginName => $plugin) {
if (!isset($installedPlugins[$pluginName])) continue;
if (!empty($plugin['route_group']) && is_array($plugin['route_group'])) {
if (isset($plugin['route_group'][0]) && is_array($plugin['route_group'][0])) {
foreach ($plugin['route_group'] as $group) {
$prefix = $group['prefix'] ?? '';
if ($prefix) $installedPrefixes[] = $prefix;
}
} else {
$prefix = $plugin['route_group']['prefix'] ?? '';
if ($prefix) $installedPrefixes[] = $prefix;
}
}
}
$installedPrefixes = array_unique($installedPrefixes);
$conflictNewPrefixes = [];
$conflictInstalledPrefixes = [];
foreach ($newPrefixes as $newPrefix) {
foreach ($installedPrefixes as $installedPrefix) {
if ($this->isPrefixConflict($newPrefix, $installedPrefix)) {
$conflictNewPrefixes[] = $newPrefix;
$conflictInstalledPrefixes[] = $installedPrefix;
}
}
}
if (!empty($conflictNewPrefixes)) {
echo json_encode([
'success' => false,
'message' => "路由前缀冲突:新插件的前缀(" . implode(', ', array_unique($conflictNewPrefixes)) . ")与已安装插件的前缀(" . implode(', ', array_unique($conflictInstalledPrefixes)) . ")重复,请修改后再安装。"
]);
exit;
}
return true;
}
/**
* 判断两个路由前缀是否冲突
*/
protected function isPrefixConflict(string $prefixA, string $prefixB): bool {
$prefixA = rtrim(trim($prefixA), '/');
$prefixB = rtrim(trim($prefixB), '/');
// 两个都是空字符串,视为冲突
if ($prefixA === '' && $prefixB === '') {
return true;
}
// 完全相等才算冲突
if ($prefixA === $prefixB) {
return true;
}
// 不再判断包含关系为冲突,直接返回不冲突
return false;
}
/**
* 卸载插件
* @param string $name
* @return bool
*/
public function uninstallPlugin(string $name): bool {
if (!isset($this->plugins[$name])) {
$this->log("Uninstall failed: plugin $name not found");
return false;
}
$plugin = $this->plugins[$name];
// 删除插件相关的数据库表
if (!empty($plugin['tables']) && is_array($plugin['tables'])) {
foreach ($plugin['tables'] as $table) {
$exists = $this->db->query("SHOW TABLES LIKE '{$table}'")->fetch();
if ($exists) {
try {
$this->db->query("DROP TABLE IF EXISTS `{$table}`");
$this->log("Dropped table: {$table}");
} catch (\Throwable $e) {
$this->log("[ERROR] Failed to drop table {$table}: " . $e->getMessage());
}
}
}
}
// 删除插件记录
$this->db->delete('plugins', ['name' => $name]);
// 清除已安装插件缓存
$this->installedPluginsCache = null;
$this->log("Uninstalled plugin {$name}");
return true;
}
/**
* 启用插件
* @param string $name
* @return bool
*/
public function enablePlugin(string $name): bool {
$count = $this->db->count('plugins', '*', ['name' => $name]);
if ($count == 0) {
$this->log("Enable failed: plugin $name not installed");
return false;
}
$this->db->update('plugins', ['status' => 1], ['name' => $name]);
$this->installedPluginsCache = null;
$this->log("Enabled plugin $name");
return true;
}
/**
* 禁用插件
* @param string $name
* @return bool
*/
public function disablePlugin(string $name): bool {
$count = $this->db->count('plugins', '*', ['name' => $name]);
if ($count == 0) {
$this->log("Disable failed: plugin $name not installed");
return false;
}
$this->db->update('plugins', ['status' => 0], ['name' => $name]);
$this->installedPluginsCache = null;
$this->log("Disabled plugin $name");
return true;
}
}