Files
MrleeAdmin/backend/app/service/core/plugin/PluginService.php
T
2026-08-31 11:33:59 +08:00

561 lines
22 KiB
PHP
Raw 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
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
* Official Website: http://www.madong.tech
*/
namespace app\service\core\plugin;
use app\dao\plugin\PluginDao;
use support\Container;
/**
* 插件服务类
*
* @package app\service\core\plugin
*/
final class PluginService extends PluginBaseService
{
/**
* 插件根目录
*/
protected string $plugin_path;
/**
* 项目根目录(前端和后端的父级目录)
*/
protected string $project_path;
/**
* 后端根目录
*/
protected string $server_path;
public function __construct(PluginDao $dao)
{
// 初始化基础路径
$this->plugin_path = base_path('plugin');
$this->project_path = dirname(base_path()); // 项目根目录(前端和后端的父级)
$this->server_path = base_path(); // 后端根目录
$this->dao = $dao;
}
/**
* 获取插件列表
*
* @param string|null $category 插件分类(all/installed/un_installed/purchased/updatable)
* @param string|null $type 插件类型
* @param string|null $keyword 搜索关键词
* @param int $page 页码
* @param int $limit 每页数量
*
* @return array 插件列表
*/
public function getList(string|null $category = 'all', string|null $type = null, string|null $keyword = null, int $page = 1, int $limit = 9999): array
{
// 获取授权配置
$config = [
'auth_code' => config('madong.auth_code', ''),
'auth_secret' => config('madong.auth_secret', ''),
'page' => $page,
'limit' => $limit,
'market_host' => config('madong.market_host', 'https://madong.tech'),
'name' => $keyword,
];
// 获取本地插件列表(原始数据)
$localModules = $this->getLocalModules($config['auth_code']);
/** @var PluginRemoteService $pluginRemoteService */
// 获取已购买的插件列表(原始数据)
$pluginRemoteService = Container::make(PluginRemoteService::class);
$purchasedModules = $pluginRemoteService->getPurchasedModules($config);
// 从本地插件中派生出已安装的模块
$installedModules = array_filter($localModules, function ($module) {
return $module['is_installed'] === true;
});
// 构建已安装插件的映射
$installedMap = array_column($installedModules, null, 'name');
// 合并本地和远程市场的插件
$allModules = $this->mergeModules($localModules, $purchasedModules);
// 根据不同类型处理数据
$processedItems = [];
switch ($category) {
case 'installed':
// 已安装(本地已安装状态)
$processedItems = $this->getInstalledModulesData($allModules, $installedMap, $installedModules);
break;
case 'un_installed':
// 未安装(本地未安装状态)
$processedItems = $this->getUninstalledModulesData($localModules, $installedMap);
break;
case 'purchased':
// 已购买(远程市场返回的所有)
$processedItems = $this->getPurchasedModulesData($purchasedModules, $installedMap);
break;
case 'updatable':
// 可更新(远程市场跟本地安装对比 市场版本大于安装版本的状态)
$processedItems = $this->getUpdatableModulesData($allModules, $installedMap, $installedModules);
break;
case 'all':
default:
// 全部(本地+市场)
$processedItems = $this->getAllModulesData($allModules, $installedMap);
break;
}
// 应用通用过滤
$filteredItems = [];
foreach ($processedItems as $module) {
if ($type !== null && (string)$module['status'] !== (string)$type) {
continue;
}
if ($keyword && !$this->matchKeyword($module, $keyword)) {
continue;
}
$filteredItems[] = $module;
}
// 分页处理
$total = count($filteredItems);
$items = array_slice($filteredItems, ($page - 1) * $limit, $limit);
return compact('page', 'limit', 'total', 'items');
}
/**
* 合并本地和远程市场的插件
*
* @param array $localModules 本地插件列表
* @param array $purchasedModules 已购买的插件列表
*
* @return array 合并后的插件列表
*/
private function mergeModules(array $localModules, array $purchasedModules): array
{
$merged = [];
$moduleMap = [];
// 先添加本地插件
foreach ($localModules as $module) {
$merged[] = $module;
$moduleMap[$module['name']] = count($merged) - 1;
}
// 再添加远程市场的插件(如果本地没有)
foreach ($purchasedModules as $module) {
if (!isset($moduleMap[$module['name']])) {
$merged[] = $module;
}
}
return $merged;
}
/**
* 获取本地插件列表(从 plugin 目录扫描)
*
* @param string $authCode 授权码
*
* @return array
*/
public function getLocalModules(string $authCode = ''): array
{
$modules = [];
$pluginPath = $this->plugin_path;
if (!is_dir($pluginPath)) {
return $modules;
}
$dirItems = scandir($pluginPath);
foreach ($dirItems as $key => $item) {
if ($item === '.' || $item === '..' || !is_dir($pluginPath . DIRECTORY_SEPARATOR . $item)) {
continue;
}
$itemPath = $pluginPath . DIRECTORY_SEPARATOR . $item;
$infoPath = $itemPath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'info.php';
$appConfigPath = $itemPath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'app.php';
$installedPath = $itemPath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'installed.php';
// 只读取 info.php
if (!file_exists($infoPath)) {
continue;
}
$config = include $infoPath;
if (!is_array($config)) {
continue;
}
// 检查插件归属类型
$pluginType = $config['type'] ?? 'madong';
if (!str_starts_with($pluginType, 'madong:') && $pluginType !== 'madong') {
continue;
}
$pluginKey = $config['name'] ?? $item;
$existingPlugin = $this->dao->findByKey($pluginKey);
$appConfig = [];
if (file_exists($appConfigPath)) {
$loadedAppConfig = include $appConfigPath;
if (is_array($loadedAppConfig)) {
$appConfig = $loadedAppConfig;
}
}
// 检查安装状态 - 通过 installed.php 判断
$isInstalled = file_exists($installedPath);
$installedAt = 0;
if ($isInstalled) {
$installedConfig = include $installedPath;
if (is_array($installedConfig) && !empty($installedConfig['installed_at'])) {
$installedAt = strtotime($installedConfig['installed_at']);
} else {
$installedAt = time();
}
}
$icon = $this->getPluginImageBase64($item, 'icon');
$cover = $this->getPluginImageBase64($item, 'cover');
$title = '';
$description = $appConfig['description'] ?? ($config['description'] ?? '');
$author = $appConfig['author'] ?? ($config['author'] ?? '');
$version = $appConfig['version'] ?? ($config['version'] ?? '1.0.0');
$type = $appConfig['type'] ?? ($config['type'] ?? 'madong');
if (!empty($appConfig['title'])) {
$title = (string)$appConfig['title'];
}
if ($existingPlugin) {
$title = $title ?: (string)($existingPlugin->title ?? '');
$description = (string)($existingPlugin->desc ?? $description);
$author = (string)($existingPlugin->author ?? $author);
$icon = $icon ?: (string)($existingPlugin->icon ?? '');
$cover = $cover ?: (string)($existingPlugin->cover ?? '');
}
if ($title === '') {
$title = $description !== '' ? $description : $pluginKey;
}
$modules[] = [
'id' => $key,
'code' => $config['name'] ?? $item,
'name' => $config['name'] ?? $item,
'title' => $title,
'type' => $type,
'version' => $version,
'status' => $isInstalled ? 1 : 0,
'description' => $description,
'author' => $author,
'price' => 0,
'cover' => $cover,
'icon' => $icon,
'downloads' => 0,
'rating' => 0,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'auth_code' => $authCode,
'is_installed' => $isInstalled,
'installed_at' => $installedAt,
'is_local' => 1,
'undeletable' => isset($config['uninstall']['undeletable']) && $config['uninstall']['undeletable'] === true ? 1 : 0,
];
}
return $modules;
}
private function getPluginImageBase64(string $pluginName, string $imageName): string
{
$imagePath = $this->plugin_path . DIRECTORY_SEPARATOR . $pluginName . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR . $imageName . '.png';
if (!file_exists($imagePath)) {
$resourcePath = $this->plugin_path . DIRECTORY_SEPARATOR . $pluginName . DIRECTORY_SEPARATOR . 'resource' . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR . $imageName . '.png';
if (!file_exists($resourcePath)) {
return '';
}
$imagePath = $resourcePath;
}
$imageData = file_get_contents($imagePath);
if ($imageData === false) {
return '';
}
return 'data:image/png;base64,' . base64_encode($imageData);
}
/**
* 获取已安装模块数据
*/
private function getInstalledModulesData(array $allModules, array $installedMap, array $installedModules): array
{
$result = [];
foreach ($installedModules as $localModule) {
// 从localModules派生的installedModules可能没有enable字段,默认为true
$enable = $localModule['enable'] ?? true;
if (!$enable) continue;
$name = $localModule['name'];
$remoteModule = $this->findModuleByName($allModules, $name);
$result[] = $this->buildModuleItem(
$remoteModule ?: $this->createFallbackModule($localModule),
$localModule,
$installedMap
);
}
return $result;
}
/**
* 获取未安装模块数据
*/
private function getUninstalledModulesData(array $localModules, array $installedMap): array
{
$result = [];
foreach ($localModules as $module) {
if (isset($installedMap[$module['name']])) continue;
$result[] = $this->buildModuleItem($module, null, $installedMap);
}
return $result;
}
/**
* 获取已购买模块数据
*/
private function getPurchasedModulesData(array $purchasedModules, array $installedMap): array
{
$result = [];
foreach ($purchasedModules as $module) {
$localModule = $installedMap[$module['name']] ?? null;
$result[] = $this->buildModuleItem($module, $localModule, $installedMap);
}
return $result;
}
/**
* 获取可更新模块数据
*/
private function getUpdatableModulesData(array $allModules, array $installedMap, array $installedModules): array
{
$result = [];
foreach ($installedModules as $localModule) {
// 从localModules派生的installedModules可能没有enable字段,默认为true
$enable = $localModule['enable'] ?? true;
if (!$enable) continue;
$name = $localModule['name'];
$remoteModule = $this->findModuleByName($allModules, $name);
if (!$remoteModule) continue;
if (version_compare($remoteModule['version'], $localModule['version'], '>')) {
$result[] = $this->buildModuleItem($remoteModule, $localModule, $installedMap);
}
}
return $result;
}
/**
* 获取所有模块数据
*/
private function getAllModulesData(array $allModules, array $installedMap): array
{
$result = [];
foreach ($allModules as $module) {
$localModule = $installedMap[$module['name']] ?? null;
$result[] = $this->buildModuleItem($module, $localModule, $installedMap);
}
return $result;
}
/**
* 构建模块数据项(包含安装状态、版本差异等)
*/
private function buildModuleItem(?array $remoteModule, ?array $localModule, array $installedMap): array
{
$name = $remoteModule['name'] ?? $localModule['name'] ?? '';
$title = $remoteModule['title'] ?? $localModule['title'] ?? $name;
$isInstalled = $localModule !== null;
$localVersion = $isInstalled ? $localModule['version'] : null;
$remoteVersion = $remoteModule['version'] ?? null;
// 获取本地插件标志
$isLocal = ($localModule && isset($localModule['is_local'])) ? $localModule['is_local'] : 0;
// 获取不可删除标志(优先从 localModule 获取)
$undeletable = 0;
if ($localModule && isset($localModule['undeletable'])) {
$undeletable = $localModule['undeletable'];
}
// 计算可更新状态
$hasUpdate = false;
$versionComparison = null;
if ($isInstalled && $remoteVersion) {
$hasUpdate = version_compare($remoteVersion, $localVersion, '>');
$versionComparison = $hasUpdate ? "{$remoteVersion} > {$localVersion}" : null;
}
// 计算购买状态
$isPurchased = false;
if ($remoteModule) {
$isPurchased = $remoteModule['price'] == 0 ||
in_array($remoteModule['name'], ['sms-verification', 'wechat-integration']);
}
// 统一数据结构 - 使用专业命名
return [
'id' => $remoteModule['id'] ?? $localModule['name'] ?? '',
'code' => $remoteModule['code'] ?? $localModule['name'] ?? '',
'name' => $name,
'title' => $title,
'type' => $remoteModule['type'] ?? ($localModule['type'] ?? 'module'),
'version' => $remoteVersion ?? $localVersion ?? '',
'status' => $remoteModule['status'] ?? 1,
'description' => $remoteModule['description'] ?? ($localModule['description'] ?? ''),
'detail_description' => $remoteModule['detail_description'] ?? '',
'author' => $remoteModule['author'] ?? ($localModule['author'] ?? ''),
'icon' => $remoteModule['icon'] ?? ($localModule['icon'] ?? ''),
'cover' => $remoteModule['cover'] ?? ($localModule['cover'] ?? ''),
'poster' => $remoteModule['poster'] ?? ($localModule['poster'] ?? ($localModule['cover'] ?? '')),
'price' => $remoteModule['price'] ?? 0,
'downloads' => $remoteModule['downloads'] ?? 0,
'rating' => $remoteModule['rating'] ?? 0,
'created_at' => $remoteModule['created_at'] ?? date('Y-m-d H:i:s'),
'updated_at' => $remoteModule['updated_at'] ?? date('Y-m-d H:i:s'),
'update_time' => $remoteModule['update_time'] ?? '',
'update_logs' => $remoteModule['update_logs'] ?? [],
'category' => $remoteModule['category'] ?? null,
'category_name' => $remoteModule['category_name'] ?? '',
'tags' => $remoteModule['tags'] ?? [],
'is_new' => (int)($remoteModule['is_new'] ?? 0),
'is_hot' => (int)($remoteModule['is_hot'] ?? 0),
'purchased' => (int)($remoteModule['purchased'] ?? 0),
'installed' => (int)($remoteModule['installed'] ?? 0),
'manual_uninstall' => (int)($remoteModule['manual_uninstall'] ?? 0),
'composer_dependencies' => $remoteModule['composer_dependencies'] ?? [],
'npm_dependencies' => $remoteModule['npm_dependencies'] ?? [],
// 状态标识 - 使用更专业的命名
'is_installed' => (int)$isInstalled,
'installed_version' => $localVersion,
'has_update' => (int)$hasUpdate,
'is_purchased' => (int)$isPurchased,
'is_downloaded' => (int)($isInstalled || in_array($name, ['data-export'])),
'can_uninstall' => (int)($isInstalled && $name !== 'user-management'),
'can_download' => (int)$isPurchased,
'is_local' => (int)$isLocal,
'undeletable' => $undeletable,
// 专业化的版本信息结构
'version_info' => [
'remote' => [
'version' => $remoteVersion,
'release_date' => $remoteModule['updated_at'] ?? null,
],
'local' => [
'version' => $localVersion,
'install_date' => $localModule['created_at'] ?? null,
],
'comparison' => [
'needs_update' => (int)$hasUpdate,
'is_latest' => (int)($isInstalled && $remoteVersion && version_compare($remoteVersion, $localVersion, '<=')),
'version_difference' => $versionComparison,
],
]
];
}
/**
* 通过名称查找模块
*/
private function findModuleByName(array $modules, string $name): ?array
{
foreach ($modules as $module) {
if ($module['name'] === $name) {
return $module;
}
}
return null;
}
/**
* 创建回退模块(当远程模块不存在时使用)
*/
private function createFallbackModule(array $localModule): array
{
return [
'name' => $localModule['name'],
'title' => $localModule['title'] ?? $localModule['name'],
'type' => 'module',
'version' => $localModule['version'],
'status' => 1,
'description' => $localModule['description'] ?? '',
'author' => $localModule['author'] ?? '',
'price' => 0,
'cover' => $localModule['cover'] ?? '',
'icon' => $localModule['icon'] ?? '',
'downloads' => 0,
'rating' => 0,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'undeletable' => $localModule['undeletable'] ?? 0,
];
}
/**
* 匹配模块名称、描述和作者是否包含关键词
*/
private function matchKeyword(array $module, string $keyword): bool
{
$lowerKeyword = strtolower($keyword);
$title = strtolower((string)($module['title'] ?? ''));
return str_contains(strtolower($module['name']), $lowerKeyword) ||
str_contains($title, $lowerKeyword) ||
str_contains(strtolower($module['description']), $lowerKeyword) ||
str_contains(strtolower($module['author']), $lowerKeyword);
}
/**
* 获取模块升级日志
*
* @param string $moduleName 模块名称
*
* @return array 升级日志列表
*/
public function getUpgradeLogs(string $moduleName): array
{
try {
// 获取授权配置
$config = [
'auth_code' => config('madong.auth_code', ''),
'auth_secret' => config('madong.auth_secret', ''),
'market_host' => config('madong.market_host', 'https://madong.tech'),
];
/** @var PluginRemoteService $pluginRemoteService */
$pluginRemoteService = Container::make(PluginRemoteService::class);
return $pluginRemoteService->getRemoteUpdateLogs(
$config['auth_code'],
$config['auth_secret'],
$moduleName
);
} catch (\Throwable $e) {
return [];
}
}
}