923 lines
30 KiB
PHP
923 lines
30 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
/**
|
||
*+------------------
|
||
* madong
|
||
*+------------------
|
||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||
*+------------------
|
||
* Author: Mr. April (405784684@qq.com)
|
||
*+------------------
|
||
* Official Website: https://madong.tech
|
||
*/
|
||
|
||
namespace app\service\admin\plugin;
|
||
|
||
use app\dao\plugin\PluginDao;
|
||
use app\model\plugin\Plugin;
|
||
use app\process\Monitor;
|
||
use core\base\BaseService;
|
||
use core\exception\handler\AdminException;
|
||
use core\uuid\Snowflake;
|
||
use support\Container;
|
||
use Workerman\Timer;
|
||
|
||
/**
|
||
* Plugin服务层
|
||
*
|
||
* @author Mr.April
|
||
* @since 1.0
|
||
*/
|
||
class PluginDevelopService extends BaseService
|
||
{
|
||
private const MONITOR_RESUME_DELAY = 3;
|
||
|
||
private string $pluginDir;
|
||
private string $adminDir;
|
||
private string $webDir;
|
||
|
||
public function __construct(PluginDao $dao)
|
||
{
|
||
$this->dao = $dao;
|
||
$basePath = base_path();
|
||
$projectPath = dirname($basePath);
|
||
$frontendPath = $projectPath . '/frontend';
|
||
|
||
$this->pluginDir = $basePath . '/plugin';
|
||
$this->adminDir = $frontendPath . '/admin/src/apps';
|
||
$this->webDir = $frontendPath . '/web/app/apps';
|
||
}
|
||
|
||
/**
|
||
* 获取插件列表(扫描插件目录并同步到数据库)
|
||
*
|
||
* @param array $where 查询条件(标准filters格式)
|
||
* @param int $page 页码
|
||
* @param int $limit 每页数量
|
||
*
|
||
* @return array
|
||
* @throws \Exception
|
||
*/
|
||
public function getList(array $where = [], int $page = 1, int $limit = 15): array
|
||
{
|
||
// 先扫描插件目录,将新插件同步到数据库
|
||
$this->syncPlugins();
|
||
return $this->dao->getList($where, $page, $limit);
|
||
}
|
||
|
||
/**
|
||
* 扫描插件目录并同步到数据库
|
||
*
|
||
* @return void
|
||
* @throws \Exception
|
||
*/
|
||
private function syncPlugins(): void
|
||
{
|
||
// 获取插件根目录下所有子目录
|
||
if (!is_dir($this->pluginDir)) {
|
||
return;
|
||
}
|
||
|
||
$dirs = scandir($this->pluginDir);
|
||
$pluginsToInsert = [];
|
||
$existingPluginKeys = [];
|
||
|
||
foreach ($dirs as $dir) {
|
||
// 跳过.和..目录
|
||
if ($dir === '.' || $dir === '..') {
|
||
continue;
|
||
}
|
||
|
||
// 优先读取 info.php 获取插件基本信息
|
||
$infoPath = $this->pluginDir . '/' . $dir . '/config/info.php';
|
||
if (!file_exists($infoPath)) {
|
||
continue;
|
||
}
|
||
|
||
// 加载插件 info 配置
|
||
$infoConfig = include $infoPath;
|
||
if (!is_array($infoConfig)) {
|
||
continue;
|
||
}
|
||
|
||
// 检查插件归属类型:madong(官方)、third(第三方)、custom(自定义)
|
||
$pluginType = $infoConfig['type'] ?? 'madong';
|
||
|
||
// 检查是否为madong类型的插件
|
||
if (!str_starts_with($pluginType, 'madong:') && $pluginType !== 'madong') {
|
||
continue;
|
||
}
|
||
|
||
$pluginKey = $infoConfig['name'] ?? $dir;
|
||
$existingPluginKeys[] = $pluginKey;
|
||
|
||
// 检查 installed.php 判断是否已安装
|
||
$installedPath = $this->pluginDir . '/' . $dir . '/config/installed.php';
|
||
$isInstalled = file_exists($installedPath);
|
||
$installedAt = 0;
|
||
$status = 0; // 默认未安装
|
||
|
||
if ($isInstalled) {
|
||
$installedConfig = include $installedPath;
|
||
if (is_array($installedConfig) && !empty($installedConfig['installed_at'])) {
|
||
$installedAt = strtotime($installedConfig['installed_at']);
|
||
} else {
|
||
$installedAt = time();
|
||
}
|
||
$status = 1; // 已安装
|
||
}
|
||
|
||
// 检查插件是否已存在于数据库
|
||
$existingPlugin = $this->dao->findByKey($pluginKey);
|
||
if ($existingPlugin) {
|
||
// 如果数据库存在但安装状态不同,同步更新
|
||
if ($existingPlugin->installed_at != $installedAt || $existingPlugin->status != $status) {
|
||
$existingPlugin->installed_at = $installedAt;
|
||
$existingPlugin->status = $status;
|
||
$existingPlugin->save();
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// 读取icon和cover图片(base64或路径)
|
||
$icon = $this->getPluginImageBase64($dir, 'icon');
|
||
$cover = $this->getPluginImageBase64($dir, 'cover');
|
||
|
||
// 使用 info.php 的字段
|
||
$pluginsToInsert[] = [
|
||
'id' => Snowflake::generate(),
|
||
'title' => $infoConfig['description'] ?? $pluginKey,
|
||
'key' => $pluginKey,
|
||
'desc' => $infoConfig['description'] ?? '',
|
||
'author' => $infoConfig['author'] ?? '',
|
||
'version' => $infoConfig['version'] ?? '1.0.0',
|
||
'type' => $pluginType,
|
||
'icon' => $icon,
|
||
'cover' => $cover,
|
||
'status' => $status,
|
||
'support_app' => 'admin',
|
||
'created_at' => time(),
|
||
'updated_at' => time(),
|
||
'installed_at' => $installedAt,
|
||
];
|
||
}
|
||
// 批量插入新插件
|
||
if (!empty($pluginsToInsert)) {
|
||
$this->dao->batchInsert($pluginsToInsert);
|
||
}
|
||
// 移除数据库中已不存在的插件
|
||
$this->removeNonExistingPlugins($existingPluginKeys);
|
||
}
|
||
|
||
/**
|
||
* 移除数据库中已不存在的插件
|
||
*
|
||
* @param array $existingPluginKeys 现有的插件key列表
|
||
*
|
||
* @return void
|
||
* @throws \Exception
|
||
*/
|
||
private function removeNonExistingPlugins(array $existingPluginKeys): void
|
||
{
|
||
$allDbPluginKeys = $this->dao->getAllKeys();
|
||
$keysToDelete = array_diff($allDbPluginKeys, $existingPluginKeys);
|
||
|
||
if (!empty($keysToDelete)) {
|
||
// 批量删除不存在的插件
|
||
foreach ($keysToDelete as $key) {
|
||
$plugin = $this->dao->findByKey($key);
|
||
if ($plugin) {
|
||
$plugin->delete();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取插件图片的base64编码
|
||
*
|
||
* @param string $pluginName 插件名称
|
||
* @param string $imageName 图片名称(icon或cover)
|
||
*
|
||
* @return string|null
|
||
*/
|
||
private function getPluginImageBase64(string $pluginName, string $imageName): ?string
|
||
{
|
||
$imagePath = $this->pluginDir . '/' . $pluginName . '/public/' . $imageName . '.png';
|
||
if (!file_exists($imagePath)) {
|
||
// 尝试查找resource目录下的图片
|
||
$resourcePath = $this->pluginDir . '/' . $pluginName . '/resource/public/' . $imageName . '.png';
|
||
if (!file_exists($resourcePath)) {
|
||
return null;
|
||
}
|
||
$imagePath = $resourcePath;
|
||
}
|
||
$imageData = file_get_contents($imagePath);
|
||
if ($imageData === false) {
|
||
return null;
|
||
}
|
||
return 'data:image/png;base64,' . base64_encode($imageData);
|
||
}
|
||
|
||
/**
|
||
* 获取插件详情
|
||
*
|
||
* @param string|int $id 插件ID
|
||
*
|
||
* @return Plugin|null
|
||
* @throws \Exception
|
||
*/
|
||
public function read(string|int $id): ?Plugin
|
||
{
|
||
$plugin = $this->dao->get($id);
|
||
if (!$plugin) {
|
||
return null;
|
||
}
|
||
$pluginConfigPath = $this->pluginDir . '/' . $plugin->key . '/config/app.php';
|
||
if (file_exists($pluginConfigPath)) {
|
||
$config = include $pluginConfigPath;
|
||
if (is_array($config)) {
|
||
// 更新插件信息
|
||
$plugin->title = $config['title'] ?? $plugin->title;
|
||
$plugin->desc = $config['description'] ?? $plugin->desc;
|
||
$plugin->author = $config['author'] ?? $plugin->author;
|
||
$plugin->version = $config['version'] ?? $plugin->version;
|
||
$plugin->type = $config['type'] ?? $plugin->type;
|
||
$plugin->icon = $this->getPluginImageBase64($plugin->key, 'icon');
|
||
$plugin->cover = $this->getPluginImageBase64($plugin->key, 'cover');
|
||
}
|
||
}
|
||
return $plugin;
|
||
}
|
||
|
||
/**
|
||
* 创建插件
|
||
*
|
||
* @param array $data
|
||
*
|
||
* @return void
|
||
* @throws \Throwable
|
||
* @throws \core\exception\handler\AdminException
|
||
*/
|
||
public function store(array $data): void
|
||
{
|
||
$this->transaction(function () use ($data) {
|
||
try {
|
||
if (empty($data['icon'])) {
|
||
$data['icon'] = $this->generateDefaultIcon($data['title'] ?? 'Plugin');
|
||
}
|
||
if (empty($data['cover'])) {
|
||
$data['cover'] = $this->generateDefaultCover($data['title'] ?? 'Plugin');
|
||
}
|
||
$model = $this->dao->save($data);
|
||
if (empty($model)) {
|
||
throw new AdminException('插件创建失败');
|
||
}
|
||
|
||
$monitor_support_pause = method_exists(Monitor::class, 'pause');
|
||
if ($monitor_support_pause) {
|
||
Monitor::pause();
|
||
}
|
||
//生成插件模板
|
||
/**@var \app\service\core\plugin\PluginDevelopService $service */
|
||
$service = Container::make(\app\service\core\plugin\PluginDevelopService::class);
|
||
$pluginName = $model->key;
|
||
$pluginTitle = $model->title;
|
||
$pluginDescription = $model->desc ?? '';
|
||
$frontendType = $data['frontend_type'] ?? 'admin';
|
||
$result = $service->generatePluginTemplate($pluginName, $pluginTitle, $pluginDescription, $frontendType);
|
||
if ($result['code'] !== 200) {
|
||
throw new AdminException($result['message']);
|
||
}
|
||
|
||
// 生成插件 info.php 配置文件
|
||
$this->createPluginInfoConfig($pluginName, $data);
|
||
|
||
//添加图片到对应插件位置
|
||
$publicPath = $this->pluginDir . '/' . $pluginName . '/public';
|
||
// 确保public目录存在
|
||
if (!is_dir($publicPath)) {
|
||
mkdir($publicPath, 0755, true);
|
||
}
|
||
$iconPath = $publicPath . '/icon.png';
|
||
$coverPath = $publicPath . '/cover.png';
|
||
$this->saveBase64Image($data['icon'], $iconPath);
|
||
$this->saveBase64Image($data['cover'], $coverPath);
|
||
if ($monitor_support_pause) {
|
||
Timer::add(self::MONITOR_RESUME_DELAY, static function () {
|
||
Monitor::resume();
|
||
}, null, false);
|
||
}
|
||
} catch (\Exception $e) {
|
||
throw new AdminException($e->getMessage());
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 打包插件
|
||
*
|
||
* @param string $pluginKey 插件标识
|
||
* @param bool $updateTemplate 是否更新前端模板到resource/template目录(默认true)
|
||
*
|
||
* @return array
|
||
* @throws \core\exception\handler\AdminException
|
||
*/
|
||
public function buildPlugin(string $pluginKey, bool $updateTemplate = true): array
|
||
{
|
||
try {
|
||
// 查询插件信息
|
||
$plugin = $this->dao->findByKey($pluginKey);
|
||
if (!$plugin) {
|
||
throw new AdminException('插件不存在');
|
||
}
|
||
|
||
// 定义路径
|
||
$backendPath = $this->pluginDir . '/' . $pluginKey;
|
||
$adminPath = $this->adminDir . '/' . $pluginKey;
|
||
$webPath = $this->webDir . '/' . $pluginKey;
|
||
$resourcePath = $backendPath . '/resource/template';
|
||
|
||
// 验证后端插件目录是否存在
|
||
if (!is_dir($backendPath)) {
|
||
throw new AdminException("插件后端目录不存在: {$backendPath}");
|
||
}
|
||
|
||
// 是否更新前端模板到 resource/template 目录
|
||
if ($updateTemplate) {
|
||
// 清理并创建template目录
|
||
if (is_dir($resourcePath)) {
|
||
$this->removeDirectory($resourcePath);
|
||
}
|
||
|
||
$resourceCount = 0;
|
||
|
||
// 复制后台前端到template目录(如果存在)
|
||
if (is_dir($adminPath)) {
|
||
$this->copyDirectory($adminPath, $resourcePath . '/admin');
|
||
$resourceCount++;
|
||
}
|
||
|
||
// 复制前台前端到resource目录(如果存在)
|
||
if (is_dir($webPath)) {
|
||
$this->copyDirectory($webPath, $resourcePath . '/web');
|
||
$resourceCount++;
|
||
}
|
||
|
||
if ($resourceCount === 0) {
|
||
// 没有前端资源,清空resource目录
|
||
if (is_dir($resourcePath)) {
|
||
rmdir($resourcePath);
|
||
}
|
||
}
|
||
} else {
|
||
// 不更新时检查是否已有模板
|
||
$resourceCount = (is_dir($resourcePath . '/admin') || is_dir($resourcePath . '/web')) ? 1 : 0;
|
||
}
|
||
|
||
// 调用核心服务的 build 方法打包插件
|
||
$pluginsDir = base_path() . '/runtime/plugins';
|
||
/** @var \app\service\core\plugin\PluginDevelopService $coreService */
|
||
$coreService = Container::make(\app\service\core\plugin\PluginDevelopService::class);
|
||
$result = $coreService->build($pluginKey, $pluginsDir);
|
||
|
||
if ($result['code'] !== 200) {
|
||
throw new AdminException($result['message']);
|
||
}
|
||
|
||
return [
|
||
'plugin_key' => $pluginKey,
|
||
'zip_path' => $result['data']['zip_file_path'],
|
||
'has_frontend' => $resourceCount > 0,
|
||
];
|
||
} catch (\Exception $e) {
|
||
throw new AdminException('插件打包失败: ' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 复制目录
|
||
*
|
||
* @param string $source 源目录
|
||
* @param string $target 目标目录
|
||
*
|
||
* @return void
|
||
*/
|
||
private function copyDirectory(string $source, string $target): void
|
||
{
|
||
if (!is_dir($source)) {
|
||
return;
|
||
}
|
||
|
||
if (!is_dir($target)) {
|
||
mkdir($target, 0755, true);
|
||
}
|
||
|
||
$files = scandir($source);
|
||
foreach ($files as $file) {
|
||
if ($file === '.' || $file === '..') {
|
||
continue;
|
||
}
|
||
|
||
$sourceFile = $source . '/' . $file;
|
||
$targetFile = $target . '/' . $file;
|
||
|
||
if (is_dir($sourceFile)) {
|
||
$this->copyDirectory($sourceFile, $targetFile);
|
||
} else {
|
||
copy($sourceFile, $targetFile);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除目录及其内容
|
||
*
|
||
* @param string $dir 目录路径
|
||
*
|
||
* @return void
|
||
*/
|
||
private function removeDirectory(string $dir): void
|
||
{
|
||
if (!is_dir($dir)) {
|
||
return;
|
||
}
|
||
|
||
$files = scandir($dir);
|
||
foreach ($files as $file) {
|
||
if ($file === '.' || $file === '..') {
|
||
continue;
|
||
}
|
||
|
||
$path = $dir . '/' . $file;
|
||
if (is_dir($path)) {
|
||
$this->removeDirectory($path);
|
||
} else {
|
||
unlink($path);
|
||
}
|
||
}
|
||
rmdir($dir);
|
||
}
|
||
|
||
/**
|
||
* 更新插件信息
|
||
*
|
||
* @param int|string $id 插件ID
|
||
* @param array $data 更新数据
|
||
*
|
||
* @return void
|
||
* @throws \Throwable
|
||
* @throws \core\exception\handler\AdminException
|
||
*/
|
||
public function update(int|string $id, array $data): void
|
||
{
|
||
$this->transaction(function () use ($id, $data) {
|
||
$model = $this->dao->get($id);
|
||
if (!$model) {
|
||
throw new AdminException('插件不存在');
|
||
}
|
||
|
||
$pluginKey = $model->getAttribute('key');
|
||
|
||
// 更新数据库 - 排除key字段,不允许修改插件标识
|
||
$updateData = $data;
|
||
unset($updateData['key']);
|
||
$model->fill($updateData);
|
||
$model->save();
|
||
|
||
// 更新插件配置文件 app.php
|
||
$this->updatePluginConfig($pluginKey, $data);
|
||
|
||
// 添加图片到对应插件位置(仅当提供了新的base64图片时)
|
||
if (isset($data['icon']) || isset($data['cover'])) {
|
||
$pluginDirName = $this->findPluginDirectory($pluginKey);
|
||
if ($pluginDirName) {
|
||
$publicPath = $this->pluginDir . '/' . $pluginDirName . '/public';
|
||
// 确保public目录存在
|
||
if (!is_dir($publicPath)) {
|
||
mkdir($publicPath, 0755, true);
|
||
}
|
||
|
||
if (!empty($data['icon'])) {
|
||
$iconPath = $publicPath . '/icon.png';
|
||
$this->saveBase64Image($data['icon'], $iconPath);
|
||
}
|
||
|
||
if (!empty($data['cover'])) {
|
||
$coverPath = $publicPath . '/cover.png';
|
||
$this->saveBase64Image($data['cover'], $coverPath);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 根据插件key查找实际的目录名
|
||
*
|
||
* @param string $pluginKey 插件key
|
||
*
|
||
* @return string|null
|
||
*/
|
||
private function findPluginDirectory(string $pluginKey): ?string
|
||
{
|
||
if (!is_dir($this->pluginDir)) {
|
||
return null;
|
||
}
|
||
|
||
$dirs = scandir($this->pluginDir);
|
||
foreach ($dirs as $dir) {
|
||
if ($dir === '.' || $dir === '..') {
|
||
continue;
|
||
}
|
||
|
||
// 优先使用 info.php 查找插件key
|
||
$infoPath = $this->pluginDir . '/' . $dir . '/config/info.php';
|
||
if (file_exists($infoPath)) {
|
||
$infoConfig = include $infoPath;
|
||
if (is_array($infoConfig) && isset($infoConfig['name']) && $infoConfig['name'] === $pluginKey) {
|
||
return $dir;
|
||
}
|
||
}
|
||
|
||
// 如果 info.php 不存在或找不到,尝试从 app.php 查找(旧版本兼容)
|
||
$configPath = $this->pluginDir . '/' . $dir . '/config/app.php';
|
||
if (!file_exists($configPath)) {
|
||
continue;
|
||
}
|
||
|
||
$config = include $configPath;
|
||
if (!is_array($config)) {
|
||
continue;
|
||
}
|
||
|
||
if (($config['name'] ?? '') === $pluginKey) {
|
||
return $dir;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 更新插件配置文件
|
||
*
|
||
* @param string $pluginKey 插件标识
|
||
* @param array $data 更新数据
|
||
*
|
||
* @return void
|
||
* @throws \core\exception\handler\AdminException
|
||
*/
|
||
private function updatePluginConfig(string $pluginKey, array $data): void
|
||
{
|
||
// 查找实际的插件目录名
|
||
$pluginDirName = $this->findPluginDirectory($pluginKey);
|
||
if (!$pluginDirName) {
|
||
throw new AdminException('插件目录不存在: ' . $pluginKey);
|
||
}
|
||
$configPath = $this->pluginDir . '/' . $pluginDirName . '/config/app.php';
|
||
// 检查配置文件是否存在
|
||
if (!file_exists($configPath)) {
|
||
throw new AdminException('插件配置文件不存在: ' . $configPath);
|
||
}
|
||
// 加载当前配置
|
||
$config = include $configPath;
|
||
if (!is_array($config)) {
|
||
throw new AdminException('插件配置文件格式错误');
|
||
}
|
||
// 更新配置项
|
||
$fieldMapping = [
|
||
'title' => 'title',
|
||
'desc' => 'description',
|
||
'author' => 'author',
|
||
'version' => 'version',
|
||
'type' => 'type',
|
||
'status' => 'enable',
|
||
];
|
||
|
||
foreach ($fieldMapping as $dbField => $configField) {
|
||
if (isset($data[$dbField])) {
|
||
if ($dbField === 'status') {
|
||
// status字段转换为enable布尔值
|
||
$config[$configField] = (bool)$data[$dbField];
|
||
} else {
|
||
$config[$configField] = $data[$dbField];
|
||
}
|
||
}
|
||
}
|
||
|
||
// 生成PHP配置文件内容
|
||
$phpContent = "<?php\n\nreturn " . $this->arrayToPhpCode($config) . ";\n";
|
||
|
||
// 写入配置文件
|
||
file_put_contents($configPath, $phpContent);
|
||
|
||
// 同时更新 info.php 配置文件
|
||
$this->updatePluginInfoConfig($pluginDirName, $data);
|
||
}
|
||
|
||
/**
|
||
* 创建插件 info.php 配置文件
|
||
*
|
||
* @param string $pluginKey 插件标识
|
||
* @param array $data 插件数据
|
||
*
|
||
* @return void
|
||
* @throws \core\exception\handler\AdminException
|
||
*/
|
||
private function createPluginInfoConfig(string $pluginKey, array $data): void
|
||
{
|
||
$pluginDir = $this->pluginDir . '/' . $pluginKey;
|
||
$configDir = $pluginDir . '/config';
|
||
|
||
// 确保 config 目录存在
|
||
if (!is_dir($configDir)) {
|
||
mkdir($configDir, 0755, true);
|
||
}
|
||
|
||
$infoConfig = [
|
||
'name' => $pluginKey,
|
||
'identifier' => $pluginKey,
|
||
'type' => $data['type'] ?? 'madong', // 插件归属类型:madong(官方)、third(第三方)、custom(自定义)
|
||
'version' => $data['version'] ?? '1.0.0',
|
||
'description' => $data['desc'] ?? '',
|
||
'author' => $data['author'] ?? '',
|
||
'author_email' => $data['author_email'] ?? '',
|
||
'website' => $data['website'] ?? '',
|
||
'uninstall' => [
|
||
'drop_tables' => false,
|
||
'remove_dependencies' => false,
|
||
],
|
||
];
|
||
|
||
$phpContent = "<?php\n\n/**\n * 插件信息配置\n */\n\nreturn " . $this->arrayToPhpCode($infoConfig) . ";\n";
|
||
file_put_contents($configDir . '/info.php', $phpContent);
|
||
}
|
||
|
||
/**
|
||
* 更新插件 info.php 配置文件
|
||
*
|
||
* @param string $pluginDirName 插件目录名
|
||
* @param array $data 更新数据
|
||
*
|
||
* @return void
|
||
* @throws \core\exception\handler\AdminException
|
||
*/
|
||
private function updatePluginInfoConfig(string $pluginDirName, array $data): void
|
||
{
|
||
$infoPath = $this->pluginDir . '/' . $pluginDirName . '/config/info.php';
|
||
|
||
// 如果 info.php 不存在,则创建
|
||
if (!file_exists($infoPath)) {
|
||
$this->createPluginInfoConfig($pluginDirName, $data);
|
||
return;
|
||
}
|
||
|
||
// 加载当前配置
|
||
$infoConfig = include $infoPath;
|
||
if (!is_array($infoConfig)) {
|
||
$infoConfig = [];
|
||
}
|
||
|
||
// 更新配置项
|
||
$fieldMapping = [
|
||
'type' => 'type',
|
||
'version' => 'version',
|
||
'desc' => 'description',
|
||
'author' => 'author',
|
||
'author_email' => 'author_email',
|
||
'website' => 'website',
|
||
];
|
||
|
||
foreach ($fieldMapping as $dbField => $infoField) {
|
||
if (isset($data[$dbField])) {
|
||
$infoConfig[$infoField] = $data[$dbField];
|
||
}
|
||
}
|
||
|
||
// 确保必要的字段存在
|
||
if (!isset($infoConfig['name'])) {
|
||
$infoConfig['name'] = $pluginDirName;
|
||
}
|
||
if (!isset($infoConfig['identifier'])) {
|
||
$infoConfig['identifier'] = $pluginDirName;
|
||
}
|
||
if (!isset($infoConfig['type'])) {
|
||
$infoConfig['type'] = 'madong'; // 默认归属 madong
|
||
}
|
||
if (!isset($infoConfig['uninstall'])) {
|
||
$infoConfig['uninstall'] = [
|
||
'drop_tables' => false,
|
||
'remove_dependencies' => false,
|
||
];
|
||
}
|
||
|
||
// 生成PHP配置文件内容
|
||
$phpContent = "<?php\n\n/**\n * 插件信息配置\n */\n\nreturn " . $this->arrayToPhpCode($infoConfig) . ";\n";
|
||
|
||
// 写入配置文件
|
||
file_put_contents($infoPath, $phpContent);
|
||
}
|
||
|
||
/**
|
||
* 将数组转换为PHP代码格式
|
||
*
|
||
* @param array $array 数组
|
||
* @param int $level 缩进级别
|
||
*
|
||
* @return string
|
||
*/
|
||
private function arrayToPhpCode(array $array, int $level = 1): string
|
||
{
|
||
$indent = str_repeat(' ', $level);
|
||
$nextIndent = str_repeat(' ', $level + 1);
|
||
|
||
$result = "[\n";
|
||
|
||
foreach ($array as $key => $value) {
|
||
if (is_numeric($key)) {
|
||
$result .= $nextIndent;
|
||
} else {
|
||
$result .= $nextIndent . "'{$key}' => ";
|
||
}
|
||
|
||
if (is_array($value)) {
|
||
$result .= $this->arrayToPhpCode($value, $level + 1);
|
||
} elseif (is_bool($value)) {
|
||
$result .= $value ? 'true' : 'false';
|
||
} elseif (is_null($value)) {
|
||
$result .= 'null';
|
||
} elseif (is_string($value)) {
|
||
// 转义单引号
|
||
$escaped = str_replace("'", "\\'", $value);
|
||
$result .= "'{$escaped}'";
|
||
} else {
|
||
$result .= $value;
|
||
}
|
||
|
||
$result .= ",\n";
|
||
}
|
||
|
||
$result .= $indent . ']';
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 保存base64图片到文件
|
||
*
|
||
* @param string $base64 base64编码的图片
|
||
* @param string $targetPath 目标路径
|
||
*
|
||
* @return void
|
||
*/
|
||
private function saveBase64Image(string $base64, string $targetPath): void
|
||
{
|
||
// 提取base64数据
|
||
if (preg_match('/^data:image\/(\w+);base64,/', $base64, $matches)) {
|
||
$imageData = substr($base64, strpos($base64, ',') + 1);
|
||
$imageData = base64_decode($imageData);
|
||
if ($imageData !== false) {
|
||
file_put_contents($targetPath, $imageData);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成默认图标(64x64)
|
||
*
|
||
* @param string $text 文字
|
||
*
|
||
* @return string base64编码的图片
|
||
*/
|
||
private function generateDefaultIcon(string $text): string
|
||
{
|
||
// 提取首字母
|
||
$firstChar = mb_substr($text, 0, 1, 'UTF-8');
|
||
|
||
// 创建64x64的图像
|
||
$size = 64;
|
||
$image = imagecreatetruecolor($size, $size);
|
||
|
||
// 分配颜色
|
||
$bgColor = imagecolorallocate($image, 64, 158, 255); // 蓝色背景 #409EFF
|
||
$textColor = imagecolorallocate($image, 255, 255, 255); // 白色文字
|
||
|
||
// 填充背景
|
||
imagefill($image, 0, 0, $bgColor);
|
||
|
||
// 绘制圆角矩形
|
||
$radius = 12;
|
||
$corners = [
|
||
[0, 0], [$size - 1, 0],
|
||
[0, $size - 1], [$size - 1, $size - 1],
|
||
];
|
||
|
||
foreach ($corners as $corner) {
|
||
imagefilledarc($image, $corner[0] + $radius, $corner[1] + $radius, $radius * 2, $radius * 2, 0, 360, $bgColor, IMG_ARC_PIE);
|
||
}
|
||
|
||
// 添加文字
|
||
$fontSize = 24;
|
||
$fontFile = $this->getFontFile();
|
||
if ($fontFile && file_exists($fontFile)) {
|
||
$bbox = imagettfbbox($fontSize, 0, $fontFile, $firstChar);
|
||
$textWidth = $bbox[2] - $bbox[0];
|
||
$textHeight = $bbox[7] - $bbox[1];
|
||
$x = ($size - $textWidth) / 2 - $bbox[0];
|
||
$y = ($size - $textHeight) / 2 - $bbox[1];
|
||
imagettftext($image, $fontSize, 0, $x, $y, $textColor, $fontFile, $firstChar);
|
||
} else {
|
||
// 如果没有字体文件,使用内置字体
|
||
imagestring($image, 5, ($size - imagefontwidth(5)) / 2, ($size - imagefontheight(5)) / 2, $firstChar, $textColor);
|
||
}
|
||
|
||
// 转换为base64
|
||
ob_start();
|
||
imagepng($image);
|
||
$imageData = ob_get_clean();
|
||
imagedestroy($image);
|
||
|
||
return 'data:image/png;base64,' . base64_encode($imageData);
|
||
}
|
||
|
||
/**
|
||
* 生成默认封面(200x120)
|
||
*
|
||
* @param string $text 文字
|
||
*
|
||
* @return string base64编码的图片
|
||
*/
|
||
private function generateDefaultCover(string $text): string
|
||
{
|
||
// 创建200x120的图像
|
||
$width = 200;
|
||
$height = 120;
|
||
$image = imagecreatetruecolor($width, $height);
|
||
|
||
// 分配颜色
|
||
$bgColor = imagecolorallocate($image, 240, 242, 245); // 浅灰色背景 #F0F2F5
|
||
$lineColor = imagecolorallocate($image, 220, 223, 230); // 线条颜色 #DCDFE6
|
||
$textColor = imagecolorallocate($image, 144, 147, 153); // 文字颜色 #909399
|
||
|
||
// 填充背景
|
||
imagefill($image, 0, 0, $bgColor);
|
||
|
||
// 绘制圆角
|
||
$radius = 8;
|
||
$corners = [
|
||
[0, 0], [$width - 1, 0],
|
||
[0, $height - 1], [$width - 1, $height - 1],
|
||
];
|
||
|
||
foreach ($corners as $corner) {
|
||
imagefilledarc($image, $corner[0] + $radius, $corner[1] + $radius, $radius * 2, $radius * 2, 0, 360, $bgColor, IMG_ARC_PIE);
|
||
}
|
||
|
||
// 绘制虚线边框
|
||
imagesetthickness($image, 1);
|
||
imagerectangle($image, 1, 1, $width - 2, $height - 2, $lineColor);
|
||
|
||
// 绘制中间的+号
|
||
$plusSize = 24;
|
||
$centerX = $width / 2;
|
||
$centerY = $height / 2;
|
||
|
||
imagesetthickness($image, 2);
|
||
imageline($image, $centerX - $plusSize, $centerY, $centerX + $plusSize, $centerY, $textColor);
|
||
imageline($image, $centerX, $centerY - $plusSize, $centerX, $centerY + $plusSize, $textColor);
|
||
|
||
// 转换为base64
|
||
ob_start();
|
||
imagepng($image);
|
||
$imageData = ob_get_clean();
|
||
imagedestroy($image);
|
||
|
||
return 'data:image/png;base64,' . base64_encode($imageData);
|
||
}
|
||
|
||
/**
|
||
* 获取字体文件路径
|
||
*
|
||
* @return string|null
|
||
*/
|
||
private function getFontFile(): ?string
|
||
{
|
||
$fontPaths = [
|
||
'/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', // Linux
|
||
'C:/Windows/Fonts/msyh.ttc', // Windows 微软雅黑
|
||
'C:/Windows/Fonts/simhei.ttf', // Windows 黑体
|
||
'/System/Library/Fonts/PingFang.ttc', // macOS
|
||
];
|
||
|
||
foreach ($fontPaths as $path) {
|
||
if (file_exists($path)) {
|
||
return $path;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
}
|