初始化本地仓库
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
namespace core\plugin\traits;
|
||||
|
||||
/**
|
||||
* 配置管理 Trait
|
||||
* 实现配置的获取、合并和覆盖机制
|
||||
*/
|
||||
trait ConfigTrait
|
||||
{
|
||||
/**
|
||||
* 获取应用配置
|
||||
*/
|
||||
protected function getAppConfig(): array
|
||||
{
|
||||
static $config = null;
|
||||
|
||||
if ($config === null) {
|
||||
// Trait 中 __DIR__ 是 core/plugin/traits,需要两级目录才能到达 core/plugin
|
||||
$configFile = dirname(dirname(__DIR__)) . '/plugin/config/app.php';
|
||||
$config = file_exists($configFile) ? include $configFile : [];
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置项(合并 appConfig + info.php 配置)
|
||||
* info.php 中的配置优先级更高
|
||||
*/
|
||||
protected function getConfig(string $key, mixed $default = null): mixed
|
||||
{
|
||||
// 先从 appConfig 获取
|
||||
$appConfigValue = $this->getAppConfigValue($key);
|
||||
|
||||
// 再从插件 info.php 获取(覆盖 appConfig)
|
||||
$pluginConfigValue = $this->getPluginConfigValue($key);
|
||||
|
||||
// 插件配置优先
|
||||
if ($pluginConfigValue !== null) {
|
||||
return $pluginConfigValue;
|
||||
}
|
||||
|
||||
return $appConfigValue !== null ? $appConfigValue : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 appConfig 获取配置值
|
||||
*/
|
||||
private function getAppConfigValue(string $key): mixed
|
||||
{
|
||||
$keys = explode('.', $key);
|
||||
$value = $this->appConfig;
|
||||
|
||||
foreach ($keys as $k) {
|
||||
if (!isset($value[$k])) {
|
||||
return null;
|
||||
}
|
||||
$value = $value[$k];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从插件 info.php 获取配置值
|
||||
*/
|
||||
private function getPluginConfigValue(string $key): mixed
|
||||
{
|
||||
static $pluginInfo = null;
|
||||
|
||||
if ($pluginInfo === null) {
|
||||
$infoFile = $this->getPluginInfoFile();
|
||||
$pluginInfo = file_exists($infoFile) ? include $infoFile : [];
|
||||
}
|
||||
|
||||
if (empty($pluginInfo)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$keys = explode('.', $key);
|
||||
$value = $pluginInfo;
|
||||
|
||||
foreach ($keys as $k) {
|
||||
if (!isset($value[$k])) {
|
||||
return null;
|
||||
}
|
||||
$value = $value[$k];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件信息配置文件路径
|
||||
*/
|
||||
protected function getPluginInfoFile(): string
|
||||
{
|
||||
return $this->pluginPath . '/config/info.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件信息(包含隐式默认配置)
|
||||
* info.php 中的配置会覆盖默认配置
|
||||
*/
|
||||
protected function getPluginInfo(): array
|
||||
{
|
||||
// 先获取显式配置(info.php)
|
||||
$infoFile = $this->getPluginInfoFile();
|
||||
$explicitConfig = [];
|
||||
|
||||
if (file_exists($infoFile)) {
|
||||
$explicitConfig = include $infoFile;
|
||||
$explicitConfig = is_array($explicitConfig) ? $explicitConfig : [];
|
||||
}
|
||||
|
||||
// 合并隐式默认配置
|
||||
return array_merge($this->getImplicitConfig(), $explicitConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取隐式默认配置
|
||||
*/
|
||||
protected function getImplicitConfig(): array
|
||||
{
|
||||
return [
|
||||
'dependencies' => [],
|
||||
'require' => [
|
||||
'composer' => [],
|
||||
],
|
||||
'npm_require' => [
|
||||
'admin' => [],
|
||||
'web' => [],
|
||||
],
|
||||
'supports' => [
|
||||
'backend' => true,
|
||||
'frontend' => true,
|
||||
'admin' => true,
|
||||
'web' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件配置文件路径
|
||||
*/
|
||||
protected function getPluginConfigFile(): string
|
||||
{
|
||||
return $this->pluginPath . '/config/installed.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存插件安装状态
|
||||
*/
|
||||
protected function savePluginConfig(string $version): void
|
||||
{
|
||||
$configFile = $this->getPluginConfigFile();
|
||||
$configDir = dirname($configFile);
|
||||
|
||||
if (!is_dir($configDir)) {
|
||||
mkdir($configDir, 0755, true);
|
||||
}
|
||||
|
||||
$existingConfig = [];
|
||||
if (file_exists($configFile)) {
|
||||
$existingConfig = include $configFile;
|
||||
$existingConfig = is_array($existingConfig) ? $existingConfig : [];
|
||||
}
|
||||
|
||||
$config = array_merge($existingConfig, [
|
||||
'name' => $this->getPluginName(),
|
||||
'version' => $version,
|
||||
'installed_at' => date('Y-m-d H:i:s'),
|
||||
'status' => 'installed',
|
||||
]);
|
||||
|
||||
$content = "<?php\n\n/**\n * 插件安装状态配置\n * 此文件由系统自动生成,请勿手动修改\n */\n\nreturn " . var_export($config, true) . ";\n";
|
||||
|
||||
file_put_contents($configFile, $content);
|
||||
|
||||
$this->output("📝 Plugin config saved: {$configFile}");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件安装状态
|
||||
*/
|
||||
protected function getPluginConfig(): ?array
|
||||
{
|
||||
$configFile = $this->getPluginConfigFile();
|
||||
|
||||
if (!file_exists($configFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$config = include $configFile;
|
||||
return is_array($config) ? $config : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查插件是否已安装
|
||||
*/
|
||||
public function isInstalled(): bool
|
||||
{
|
||||
$config = $this->getPluginConfig();
|
||||
return $config !== null && ($config['status'] ?? '') === 'installed';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件版本
|
||||
*/
|
||||
public function getVersion(): ?string
|
||||
{
|
||||
$config = $this->getPluginConfig();
|
||||
return $config['version'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除插件配置文件
|
||||
*/
|
||||
protected function removePluginConfig(): void
|
||||
{
|
||||
$configFile = $this->getPluginConfigFile();
|
||||
|
||||
if (file_exists($configFile)) {
|
||||
unlink($configFile);
|
||||
$this->output("📝 Plugin config removed: {$configFile}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
namespace core\plugin\traits;
|
||||
|
||||
/**
|
||||
* 依赖安装 Trait
|
||||
*/
|
||||
trait DependencyTrait
|
||||
{
|
||||
/**
|
||||
* 合并 Composer 依赖(后端)
|
||||
*/
|
||||
protected function installComposerDeps(): void
|
||||
{
|
||||
$pluginInfo = $this->getPluginInfo();
|
||||
$require = $pluginInfo['require']['composer'] ?? [];
|
||||
|
||||
if (empty($require)) {
|
||||
$this->output("📦 No Composer dependencies to merge");
|
||||
return;
|
||||
}
|
||||
|
||||
$config = $this->getConfig('dependencies.backend', []);
|
||||
if (empty($config['enabled'])) {
|
||||
$this->output("📦 Composer dependency merge disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output("📦 Merging Composer dependencies...");
|
||||
|
||||
$composerFile = base_path('composer.json');
|
||||
if (!file_exists($composerFile)) {
|
||||
$this->output(" ⚠️ composer.json not found");
|
||||
return;
|
||||
}
|
||||
|
||||
$composerContent = file_get_contents($composerFile);
|
||||
$composer = json_decode($composerContent, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$this->output(" ⚠️ Invalid composer.json format");
|
||||
return;
|
||||
}
|
||||
|
||||
$merged = false;
|
||||
|
||||
// 合并 require
|
||||
if (!empty($config['merge_require'])) {
|
||||
foreach ($require as $package => $version) {
|
||||
if (!isset($composer['require'][$package])) {
|
||||
$composer['require'][$package] = $version;
|
||||
$merged = true;
|
||||
$this->output(" ➕ Added to require: {$package}:{$version}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存
|
||||
if ($merged) {
|
||||
file_put_contents($composerFile, json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
|
||||
$this->output(" ✅ Composer dependencies merged. Run 'composer install' manually.");
|
||||
} else {
|
||||
$this->output(" ℹ️ No new dependencies to merge");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并 NPM 依赖(前端)
|
||||
*/
|
||||
protected function installNpmDeps(): void
|
||||
{
|
||||
$pluginInfo = $this->getPluginInfo();
|
||||
$npmRequire = $pluginInfo['npm_require'] ?? [];
|
||||
|
||||
if (empty($npmRequire)) {
|
||||
$this->output("📦 No NPM dependencies to merge");
|
||||
return;
|
||||
}
|
||||
|
||||
$projectRoot = $this->getProjectRoot();
|
||||
|
||||
// 合并 Admin 端依赖(前端代码统一在 frontend 目录下)
|
||||
$adminConfig = $this->getConfig('dependencies.admin', []);
|
||||
if (!empty($adminConfig['enabled'])) {
|
||||
$adminPath = $this->getFrontendProjectPath('admin');
|
||||
$this->mergeNpmDeps($npmRequire['admin'] ?? [], $adminPath, 'Admin', $adminConfig);
|
||||
$this->mergeNpmDeps($npmRequire['common'] ?? [], $adminPath, 'Admin (common)', $adminConfig);
|
||||
}
|
||||
|
||||
// 合并 Web 端依赖
|
||||
$webConfig = $this->getConfig('dependencies.web', []);
|
||||
if (!empty($webConfig['enabled'])) {
|
||||
$webPath = $this->getFrontendProjectPath('web');
|
||||
$this->mergeNpmDeps($npmRequire['web'] ?? [], $webPath, 'Web', $webConfig);
|
||||
$this->mergeNpmDeps($npmRequire['common'] ?? [], $webPath, 'Web (common)', $webConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并 NPM 依赖到 package.json
|
||||
*/
|
||||
protected function mergeNpmDeps(array $packages, string $workDir, string $endpoint, array $config): void
|
||||
{
|
||||
if (empty($packages) || !is_dir($workDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$packageFile = $workDir . '/package.json';
|
||||
if (!file_exists($packageFile)) {
|
||||
$this->output(" ⚠️ {$endpoint}: package.json not found");
|
||||
return;
|
||||
}
|
||||
|
||||
$packageContent = file_get_contents($packageFile);
|
||||
$package = json_decode($packageContent, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$this->output(" ⚠️ {$endpoint}: Invalid package.json format");
|
||||
return;
|
||||
}
|
||||
|
||||
$merged = false;
|
||||
|
||||
// 合并 dependencies
|
||||
if (!empty($config['merge_prod'])) {
|
||||
foreach ($packages as $pkg => $version) {
|
||||
if (!isset($package['dependencies'][$pkg])) {
|
||||
$package['dependencies'][$pkg] = $version;
|
||||
$merged = true;
|
||||
$this->output(" ➕ {$endpoint}: Added {$pkg}@{$version}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 合并 devDependencies
|
||||
if (!empty($config['merge_dev'])) {
|
||||
foreach ($packages as $pkg => $version) {
|
||||
if (!isset($package['devDependencies'][$pkg])) {
|
||||
$package['devDependencies'][$pkg] = $version;
|
||||
$merged = true;
|
||||
$this->output(" ➕ {$endpoint}: Added dev {$pkg}@{$version}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存
|
||||
if ($merged) {
|
||||
file_put_contents($packageFile, json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
|
||||
$this->output(" ✅ {$endpoint}: Dependencies merged. Run 'npm install' manually.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除合并的依赖
|
||||
*/
|
||||
protected function removeMergedDependencies(): void
|
||||
{
|
||||
$this->output("🗑️ Removing merged dependencies...");
|
||||
|
||||
$pluginInfo = $this->getPluginInfo();
|
||||
|
||||
// 移除 Composer 依赖
|
||||
$composerRequire = $pluginInfo['require']['composer'] ?? [];
|
||||
if (!empty($composerRequire)) {
|
||||
$this->removeComposerDeps($composerRequire);
|
||||
}
|
||||
|
||||
// 移除 NPM 依赖
|
||||
$npmRequire = $pluginInfo['npm_require'] ?? [];
|
||||
if (!empty($npmRequire)) {
|
||||
// 前端代码统一在 frontend 目录下
|
||||
$this->removeNpmDeps($npmRequire['admin'] ?? [], $this->getFrontendProjectPath('admin'), 'Admin');
|
||||
$this->removeNpmDeps($npmRequire['common'] ?? [], $this->getFrontendProjectPath('admin'), 'Admin (common)');
|
||||
|
||||
$this->removeNpmDeps($npmRequire['web'] ?? [], $this->getFrontendProjectPath('web'), 'Web');
|
||||
$this->removeNpmDeps($npmRequire['common'] ?? [], $this->getFrontendProjectPath('web'), 'Web (common)');
|
||||
}
|
||||
|
||||
$this->output(" ✅ Dependencies removed. Run 'composer install' or 'npm install' to update.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除 Composer 依赖
|
||||
*/
|
||||
protected function removeComposerDeps(array $packages): void
|
||||
{
|
||||
$composerFile = base_path('composer.json');
|
||||
if (!file_exists($composerFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$composer = json_decode(file_get_contents($composerFile), true);
|
||||
|
||||
$removed = false;
|
||||
|
||||
foreach ($packages as $package => $version) {
|
||||
if (isset($composer['require'][$package])) {
|
||||
unset($composer['require'][$package]);
|
||||
$removed = true;
|
||||
$this->output(" ➖ Removed composer: {$package}");
|
||||
}
|
||||
}
|
||||
|
||||
if ($removed) {
|
||||
file_put_contents($composerFile, json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除 NPM 依赖
|
||||
*/
|
||||
protected function removeNpmDeps(array $packages, string $workDir, string $endpoint): void
|
||||
{
|
||||
if (empty($packages) || !is_dir($workDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$packageFile = $workDir . '/package.json';
|
||||
if (!file_exists($packageFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$package = json_decode(file_get_contents($packageFile), true);
|
||||
|
||||
$removed = false;
|
||||
|
||||
foreach ($packages as $pkg => $version) {
|
||||
// 移除 dependencies
|
||||
if (isset($package['dependencies'][$pkg])) {
|
||||
unset($package['dependencies'][$pkg]);
|
||||
$removed = true;
|
||||
$this->output(" ➖ {$endpoint}: Removed {$pkg}");
|
||||
}
|
||||
// 移除 devDependencies
|
||||
if (isset($package['devDependencies'][$pkg])) {
|
||||
unset($package['devDependencies'][$pkg]);
|
||||
$removed = true;
|
||||
$this->output(" ➖ {$endpoint}: Removed dev {$pkg}");
|
||||
}
|
||||
}
|
||||
|
||||
if ($removed) {
|
||||
file_put_contents($packageFile, json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 Shell 命令
|
||||
*/
|
||||
protected function runShellCommand(string $command, string $workDir, string $label): void
|
||||
{
|
||||
if (!is_dir($workDir)) {
|
||||
$this->output(" ⚠️ {$label}: Directory not found: {$workDir}");
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output(" 🔧 {$label}: Running '{$command}'");
|
||||
|
||||
try {
|
||||
$descriptorspec = [
|
||||
0 => ['pipe', 'r'],
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
];
|
||||
|
||||
$process = proc_open($command, $descriptorspec, $pipes, $workDir);
|
||||
|
||||
if (is_resource($process)) {
|
||||
stream_get_contents($pipes[1]);
|
||||
$error = stream_get_contents($pipes[2]);
|
||||
|
||||
foreach ($pipes as $pipe) {
|
||||
fclose($pipe);
|
||||
}
|
||||
|
||||
$exitCode = proc_close($process);
|
||||
|
||||
if ($exitCode === 0) {
|
||||
$this->output(" ✅ {$label}: Completed successfully");
|
||||
} else {
|
||||
$this->output(" ❌ {$label}: Failed with exit code {$exitCode}");
|
||||
if (!empty($error)) {
|
||||
$this->output(" Error: {$error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->output(" ❌ {$label}: Error - {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
<?php
|
||||
|
||||
namespace core\plugin\traits;
|
||||
|
||||
use core\uuid\Snowflake;
|
||||
use support\Db;
|
||||
|
||||
/**
|
||||
* 菜单操作 Trait
|
||||
*/
|
||||
trait MenuTrait
|
||||
{
|
||||
/**
|
||||
* 预加载系统菜单的 code 映射(用于智能关联父级)
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $_admin_menu_codes = [];
|
||||
protected array $_web_menu_codes = [];
|
||||
|
||||
/**
|
||||
* 加载菜单文件
|
||||
*/
|
||||
protected function loadMenus(): void
|
||||
{
|
||||
$menuResourceDir = $this->getConfig('resource.menu', 'menu');
|
||||
$menuDir = $this->pluginPath . '/resource/' . $menuResourceDir;
|
||||
|
||||
$this->output("📂 Loading menus from: {$menuDir}");
|
||||
|
||||
if (!is_dir($menuDir)) {
|
||||
$this->output("📂 No menu directory found, skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
$files = scandir($menuDir);
|
||||
$this->output("📂 Menu files found: " . implode(', ', array_diff($files, ['.', '..'])));
|
||||
|
||||
// 预加载系统菜单的 code 映射(用于智能关联父级)
|
||||
$this->preloadSystemMenuCodes('admin');
|
||||
$this->preloadSystemMenuCodes('web');
|
||||
|
||||
// 只加载 admin 和 web 两种类型的菜单
|
||||
$this->loadMenuFile($menuDir . '/admin.php', 'admin');
|
||||
$this->loadMenuFile($menuDir . '/web.php', 'web');
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载系统菜单的 code 映射
|
||||
* 用于插件菜单关联到系统已有菜单
|
||||
*
|
||||
* @param string $type 菜单类型 (admin/web)
|
||||
*/
|
||||
protected function preloadSystemMenuCodes(string $type): void
|
||||
{
|
||||
$tableName = $type === 'admin' ? 'sys_menu' : 'web_menu';
|
||||
$connection = $this->connection ?? null;
|
||||
|
||||
try {
|
||||
// 获取系统中非插件来源的菜单 code -> id 映射
|
||||
$menus = Db::connection($connection)
|
||||
->table($tableName)
|
||||
->where('source', 'system')
|
||||
->whereNotNull('code')
|
||||
->where('code', '!=', '')
|
||||
->select(['id', 'code', 'pid', 'type'])
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
$key = $type === 'admin' ? '_admin_menu_codes' : '_web_menu_codes';
|
||||
$this->$key = [];
|
||||
|
||||
foreach ($menus as $menu) {
|
||||
$code = $menu->code;
|
||||
$this->$key[$code] = [
|
||||
'id' => $menu->id,
|
||||
'pid' => $menu->pid,
|
||||
'type' => $menu->type,
|
||||
];
|
||||
}
|
||||
|
||||
$this->output(" 📋 Preloaded {$type} system menus: " . count($this->$key) . " codes");
|
||||
} catch (\Throwable $e) {
|
||||
$this->output(" ⚠️ Failed to preload {$type} system menus: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 pid_code 查找父级 ID
|
||||
*
|
||||
* @param string|null $pidCode 父级菜单的 code
|
||||
* @param string $type 菜单类型
|
||||
* @return int 父级 ID,0 表示顶级菜单
|
||||
*/
|
||||
protected function findParentIdByCode(?string $pidCode, string $type): int
|
||||
{
|
||||
if (empty($pidCode)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$key = $type === 'admin' ? '_admin_menu_codes' : '_web_menu_codes';
|
||||
$codes = $this->$key ?? [];
|
||||
|
||||
if (!isset($codes[$pidCode])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$parentInfo = $codes[$pidCode];
|
||||
|
||||
// 如果父级是目录类型(type=1),则返回其 ID
|
||||
// 否则作为顶级菜单
|
||||
if ($type === 'admin') {
|
||||
// admin: 1=目录 2=菜单 3=按钮
|
||||
return in_array($parentInfo['type'], [1]) ? $parentInfo['id'] : 0;
|
||||
} else {
|
||||
// web 菜单类型判断
|
||||
return $parentInfo['id'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载单个菜单文件
|
||||
*/
|
||||
protected function loadMenuFile(string $filePath, string $type): void
|
||||
{
|
||||
if (!file_exists($filePath)) {
|
||||
$this->output(" ⚠️ {$type}: Menu file not found");
|
||||
return;
|
||||
}
|
||||
|
||||
$menu = include $filePath;
|
||||
|
||||
if (empty($menu)) {
|
||||
$this->output(" ⚠️ {$type}: Empty menu file");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_array($menu)) {
|
||||
$this->output(" ❌ {$type}: Invalid menu format");
|
||||
return;
|
||||
}
|
||||
|
||||
$this->saveMenuToSystem($type, $menu);
|
||||
|
||||
$this->output(" ✅ {$type}: Menu loaded (" . count($menu) . " items)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存菜单到系统
|
||||
*/
|
||||
protected function saveMenuToSystem(string $type, array $menu): void
|
||||
{
|
||||
if ($type === 'admin') {
|
||||
$this->saveAdminMenu($menu);
|
||||
} else {
|
||||
$this->saveWebMenu($menu);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存后台菜单
|
||||
*/
|
||||
protected function saveAdminMenu(array $menu): void
|
||||
{
|
||||
$tableName = 'sys_menu';
|
||||
|
||||
$this->output(" 📝 Saving admin menu to {$tableName}...");
|
||||
|
||||
try {
|
||||
$connection = $this->connection ?? null;
|
||||
$hasTable = Db::connection($connection)->getSchemaBuilder()->hasTable($tableName);
|
||||
|
||||
if (!$hasTable) {
|
||||
$this->output(" ⚠️ Table {$tableName} does not exist, skipping menu import");
|
||||
return;
|
||||
}
|
||||
|
||||
$this->insertAdminMenuItems($tableName, $menu, $this->pluginName);
|
||||
|
||||
$this->output(" ✅ Admin menu saved to {$tableName}");
|
||||
} catch (\Throwable $e) {
|
||||
$this->output(" ❌ Failed to save admin menu: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入后台菜单项
|
||||
*
|
||||
* @param string $tableName 表名
|
||||
* @param array $items 菜单项
|
||||
* @param string $plugin 插件标识
|
||||
* @param int $parentId 父级ID
|
||||
* @param int $level 当前层级
|
||||
*/
|
||||
protected function insertAdminMenuItems(string $tableName, array $items, string $plugin, int $parentId = 0, int $level = 1): void
|
||||
{
|
||||
$connection = $this->connection ?? null;
|
||||
|
||||
foreach ($items as $index => $item) {
|
||||
$menuId = (int)Snowflake::generate();
|
||||
|
||||
// 智能获取父级 ID(仅顶级菜单使用 pid_code)
|
||||
// 子菜单通过 parentId(children 嵌套)确定父级
|
||||
if ($parentId === 0 && isset($item['pid_code'])) {
|
||||
$parentId = $this->findParentIdByCode($item['pid_code'], 'admin');
|
||||
}
|
||||
|
||||
// 规范化菜单数据:自动生成缺失的 code
|
||||
$normalizedItem = $this->normalizeMenuItem($item, $plugin, $level, $index);
|
||||
|
||||
$data = [
|
||||
'id' => $menuId,
|
||||
'pid' => $parentId,
|
||||
'app' => $item['app'] ?? $plugin,
|
||||
'source' => 'plugin',
|
||||
'title' => $normalizedItem['title'],
|
||||
'code' => $normalizedItem['code'],
|
||||
'level' => $level,
|
||||
'type' => $normalizedItem['type'],
|
||||
'sort' => $normalizedItem['sort'],
|
||||
'path' => $normalizedItem['path'],
|
||||
'component' => $normalizedItem['component'],
|
||||
'icon' => $normalizedItem['icon'],
|
||||
'is_show' => $normalizedItem['is_show'],
|
||||
'is_link' => $normalizedItem['is_link'],
|
||||
'is_cache' => $normalizedItem['is_cache'],
|
||||
'is_sync' => $normalizedItem['is_sync'],
|
||||
'created_at' => time(),
|
||||
'updated_at' => time(),
|
||||
];
|
||||
|
||||
Db::connection($connection)->table($tableName)->insert($data);
|
||||
|
||||
// 记录本次插入菜单的 code -> id 映射(供子菜单使用)
|
||||
if ($normalizedItem['code']) {
|
||||
$this->_admin_menu_codes[$normalizedItem['code']] = [
|
||||
'id' => $menuId,
|
||||
'pid' => $parentId,
|
||||
'type' => $data['type'],
|
||||
];
|
||||
}
|
||||
|
||||
// 递归处理子菜单,层级 +1
|
||||
if (!empty($item['children'])) {
|
||||
$this->insertAdminMenuItems($tableName, $item['children'], $plugin, $menuId, $level + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化菜单项数据
|
||||
* 确保必要字段存在,自动生成缺失的 code
|
||||
*
|
||||
* @param array $item 菜单项
|
||||
* @param string $plugin 插件标识
|
||||
* @param int $level 当前层级
|
||||
* @param int $index 当前索引
|
||||
* @return array 规范化后的菜单项
|
||||
*/
|
||||
protected function normalizeMenuItem(array $item, string $plugin, int $level, int $index): array
|
||||
{
|
||||
// 获取菜单名称
|
||||
$title = $item['name'] ?? $item['title'] ?? '';
|
||||
// 获取路径
|
||||
$path = $item['path'] ?? '';
|
||||
|
||||
// 自动生成 code:如果没有 code 字段,根据插件名和路径生成
|
||||
$code = $item['code'] ?? '';
|
||||
if (empty($code)) {
|
||||
// 从 path 生成 code: /test-plugin/index -> test-plugin:index
|
||||
if (!empty($path)) {
|
||||
$code = trim($path, '/');
|
||||
$code = str_replace('/', ':', $code);
|
||||
}
|
||||
// 如果路径也为空,使用插件名+层级+索引生成唯一 code
|
||||
if (empty($code)) {
|
||||
$code = $plugin . ':level' . $level . '_' . $index;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => $title,
|
||||
'code' => $code,
|
||||
'type' => $item['type'] ?? ($level === 1 ? 1 : 2), // 1=目录 2=菜单
|
||||
'sort' => $item['sort'] ?? 0,
|
||||
'path' => $path,
|
||||
'component' => $item['component'] ?? ($level === 1 ? '/layout' : ''),
|
||||
'icon' => $item['icon'] ?? '',
|
||||
'is_show' => $item['is_show'] ?? 1,
|
||||
'is_link' => $item['is_link'] ?? 0,
|
||||
'is_cache' => $item['is_cache'] ?? 0,
|
||||
'is_sync' => $item['is_sync'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前台菜单
|
||||
*/
|
||||
protected function saveWebMenu(array $menu): void
|
||||
{
|
||||
$tableName = 'web_menu';
|
||||
|
||||
$this->output(" 📝 Saving web menu to {$tableName}...");
|
||||
|
||||
try {
|
||||
$connection = $this->connection ?? null;
|
||||
$hasTable = Db::connection($connection)->getSchemaBuilder()->hasTable($tableName);
|
||||
|
||||
if (!$hasTable) {
|
||||
$this->output(" ⚠️ Table {$tableName} does not exist, skipping menu import");
|
||||
return;
|
||||
}
|
||||
|
||||
$this->insertWebMenuItems($tableName, $menu, $this->pluginName);
|
||||
|
||||
$this->output(" ✅ Web menu saved to {$tableName}");
|
||||
} catch (\Throwable $e) {
|
||||
$this->output(" ❌ Failed to save web menu: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入前台菜单项
|
||||
*
|
||||
* @param string $tableName 表名
|
||||
* @param array $items 菜单项
|
||||
* @param string $plugin 插件标识
|
||||
* @param int $parentId 父级ID
|
||||
* @param int $level 当前层级
|
||||
*/
|
||||
protected function insertWebMenuItems(string $tableName, array $items, string $plugin, int $parentId = 0, int $level = 1): void
|
||||
{
|
||||
$connection = $this->connection ?? null;
|
||||
|
||||
foreach ($items as $index => $item) {
|
||||
$menuId = (int)Snowflake::generate();
|
||||
|
||||
// 智能获取父级 ID(仅顶级菜单使用 pid_code)
|
||||
// 子菜单通过 parentId(children 嵌套)确定父级
|
||||
if ($parentId === 0 && isset($item['pid_code'])) {
|
||||
$parentId = $this->findParentIdByCode($item['pid_code'], 'web');
|
||||
}
|
||||
|
||||
// 规范化菜单数据:自动生成缺失的 code
|
||||
$normalizedItem = $this->normalizeWebMenuItem($item, $plugin, $level, $index);
|
||||
|
||||
// target: 1=当前窗口 2=新窗口
|
||||
$targetValue = 1; // 默认当前窗口
|
||||
if (isset($item['target'])) {
|
||||
$targetValue = $item['target'] === '_self' || $item['target'] === 1 || $item['target'] === '1' ? 1 : 2;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'id' => $menuId,
|
||||
'app' => $item['app'] ?? $plugin,
|
||||
'category' => $item['category'] ?? '1',
|
||||
'source' => 'plugin',
|
||||
'code' => $normalizedItem['code'],
|
||||
'name' => $normalizedItem['name'],
|
||||
'url' => $normalizedItem['url'],
|
||||
'pid' => $parentId,
|
||||
'level' => $level,
|
||||
'type' => $normalizedItem['type'],
|
||||
'sort' => $normalizedItem['sort'],
|
||||
'target' => $targetValue,
|
||||
'icon' => $normalizedItem['icon'],
|
||||
'is_show' => $normalizedItem['is_show'],
|
||||
'enabled' => $normalizedItem['enabled'],
|
||||
'created_at' => time(),
|
||||
'updated_at' => time(),
|
||||
];
|
||||
|
||||
Db::connection($connection)->table($tableName)->insert($data);
|
||||
|
||||
// 记录本次插入菜单的 code -> id 映射(供子菜单使用)
|
||||
if ($normalizedItem['code']) {
|
||||
$this->_web_menu_codes[$normalizedItem['code']] = [
|
||||
'id' => $menuId,
|
||||
'pid' => $parentId,
|
||||
'type' => $data['type'],
|
||||
];
|
||||
}
|
||||
|
||||
// 递归处理子菜单,层级 +1
|
||||
if (!empty($item['children'])) {
|
||||
$this->insertWebMenuItems($tableName, $item['children'], $plugin, $menuId, $level + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化前台菜单项数据
|
||||
* 确保必要字段存在,自动生成缺失的 code
|
||||
*
|
||||
* @param array $item 菜单项
|
||||
* @param string $plugin 插件标识
|
||||
* @param int $level 当前层级
|
||||
* @param int $index 当前索引
|
||||
* @return array 规范化后的菜单项
|
||||
*/
|
||||
protected function normalizeWebMenuItem(array $item, string $plugin, int $level, int $index): array
|
||||
{
|
||||
// 获取菜单名称
|
||||
$name = $item['name'] ?? $item['title'] ?? '';
|
||||
// 获取 URL
|
||||
$url = $item['path'] ?? $item['url'] ?? '';
|
||||
|
||||
// 自动生成 code:如果没有 code 字段,根据插件名和 URL 生成
|
||||
$code = $item['code'] ?? '';
|
||||
if (empty($code)) {
|
||||
// 从 url 生成 code
|
||||
if (!empty($url)) {
|
||||
$code = trim($url, '/');
|
||||
$code = str_replace('/', ':', $code);
|
||||
}
|
||||
// 如果 URL 也为空,使用插件名+层级+索引生成唯一 code
|
||||
if (empty($code)) {
|
||||
$code = $plugin . ':level' . $level . '_' . $index;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'code' => $code,
|
||||
'url' => $url,
|
||||
'type' => $item['type'] ?? 1,
|
||||
'sort' => $item['sort'] ?? 0,
|
||||
'icon' => $item['icon'] ?? '',
|
||||
'is_show' => $item['is_show'] ?? 1,
|
||||
'enabled' => $item['enabled'] ?? 1,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除插件菜单
|
||||
*/
|
||||
protected function clearPluginMenus(string $plugin): void
|
||||
{
|
||||
$this->clearMenusByTable('sys_menu', $plugin);
|
||||
$this->clearMenusByTable('web_menu', $plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据表清除菜单
|
||||
*/
|
||||
protected function clearMenusByTable(string $tableName, string $plugin): void
|
||||
{
|
||||
try {
|
||||
$connection = $this->connection ?? null;
|
||||
if (!Db::connection($connection)->getSchemaBuilder()->hasTable($tableName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$query = Db::connection($connection)
|
||||
->table($tableName)
|
||||
->where('source', 'plugin')
|
||||
->where(function ($query) use ($tableName, $plugin) {
|
||||
$query->where('app', $plugin)
|
||||
->orWhere('code', 'like', $plugin . ':%');
|
||||
|
||||
if ($tableName === 'sys_menu') {
|
||||
$query->orWhere('path', 'like', '/' . $plugin . '%')
|
||||
->orWhere('component', 'like', '/' . $plugin . '/%');
|
||||
} else {
|
||||
$query->orWhere('url', 'like', '/' . $plugin . '%');
|
||||
}
|
||||
});
|
||||
|
||||
$deleted = $query->delete();
|
||||
|
||||
if ($deleted > 0) {
|
||||
$this->output(" 🗑️ Cleared {$deleted} menus from {$tableName}");
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->output(" ⚠️ Failed to clear {$tableName}: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入插件菜单(快捷方法)
|
||||
*/
|
||||
public function importPluginMenus(): void
|
||||
{
|
||||
$this->importMenus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载插件菜单(快捷方法)
|
||||
*/
|
||||
public function uninstallPluginMenus(): void
|
||||
{
|
||||
$this->clearMenus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入菜单(子类可重写)
|
||||
*/
|
||||
protected function importMenus(): void
|
||||
{
|
||||
$this->loadMenus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除菜单(子类可重写)
|
||||
*/
|
||||
protected function clearMenus(): void
|
||||
{
|
||||
$this->clearPluginMenus($this->pluginName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,668 @@
|
||||
<?php
|
||||
|
||||
namespace core\plugin\traits;
|
||||
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
use support\Db;
|
||||
|
||||
/**
|
||||
* 迁移操作 Trait
|
||||
*/
|
||||
trait MigrationTrait
|
||||
{
|
||||
/**
|
||||
* 插件临时目录路径
|
||||
*/
|
||||
const RUNTIME_PLUGIN_PATH = 'install/plugin';
|
||||
|
||||
/**
|
||||
* 运行迁移
|
||||
*/
|
||||
public function runMigrations(): void
|
||||
{
|
||||
$migrationsDir = $this->findMigrationsDir();
|
||||
|
||||
if (!$migrationsDir) {
|
||||
$this->output("⚠️ No migrations directory found");
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output("📁 Migrations dir: {$migrationsDir}");
|
||||
|
||||
// 获取已执行的迁移
|
||||
$executed = $this->getExecutedMigrations();
|
||||
|
||||
// 获取所有迁移文件
|
||||
$files = glob($migrationsDir . '/*.php');
|
||||
|
||||
$pending = [];
|
||||
foreach ($files as $file) {
|
||||
$basename = basename($file, '.php');
|
||||
if (!in_array($basename, $executed)) {
|
||||
$pending[] = [$file, $basename];
|
||||
}
|
||||
}
|
||||
|
||||
sort($pending);
|
||||
|
||||
if (empty($pending)) {
|
||||
$this->output("✅ No pending migrations");
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output("📋 Pending migrations: " . count($pending));
|
||||
|
||||
$batch = $this->getNextBatch();
|
||||
$schema = $this->getSchemaBuilder();
|
||||
|
||||
// 直接执行迁移
|
||||
foreach ($pending as $migration) {
|
||||
$this->runMigration($migration[0], $migration[1], $schema);
|
||||
}
|
||||
|
||||
// 记录批次
|
||||
$this->recordBatch($batch, $pending);
|
||||
|
||||
$this->output("✅ Migrations completed (batch: {$batch})");
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行单个迁移
|
||||
*/
|
||||
protected function runMigration(string $file, string $name, Builder $schema): void
|
||||
{
|
||||
$this->output(" 📝 Migrating: {$name}");
|
||||
|
||||
try {
|
||||
$migration = require $file;
|
||||
$migration->up($schema);
|
||||
$this->output(" ✅ Migrated: {$name}");
|
||||
} catch (\Throwable $e) {
|
||||
$this->output(" ❌ Error: {$e->getMessage()}");
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚迁移
|
||||
* @param bool $dropTables 是否删除表
|
||||
*/
|
||||
public function rollbackMigrations(bool $dropTables = false): void
|
||||
{
|
||||
if ($dropTables) {
|
||||
$this->dropAllTables();
|
||||
$this->saveMigrationRows([]);
|
||||
$this->output("✅ All plugin tables dropped");
|
||||
return;
|
||||
}
|
||||
|
||||
// 不删除表,只清理迁移日志
|
||||
$this->saveMigrationRows([]);
|
||||
$this->output("📝 Migration records cleared (tables preserved)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除插件所有表
|
||||
*/
|
||||
protected function dropAllTables(): void
|
||||
{
|
||||
$tables = $this->getPluginTables();
|
||||
if (empty($tables)) {
|
||||
$this->output(" ⚠️ No tables to drop");
|
||||
return;
|
||||
}
|
||||
|
||||
$schema = $this->getSchemaBuilder();
|
||||
foreach ($tables as $table) {
|
||||
$this->output(" 🗑️ Dropping table: {$table}");
|
||||
$schema->dropIfExists($table);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件相关的所有表
|
||||
*/
|
||||
protected function getPluginTables(): array
|
||||
{
|
||||
$tables = [];
|
||||
$migrationsDir = $this->findMigrationsDir();
|
||||
|
||||
// 从迁移文件解析表名
|
||||
if (is_dir($migrationsDir)) {
|
||||
$files = glob($migrationsDir . '/*.php');
|
||||
foreach ($files as $file) {
|
||||
$content = file_get_contents($file);
|
||||
if (preg_match_all('/->create\([\'"](\w+)[\'"]/', $content, $matches)) {
|
||||
$tables = array_merge($tables, $matches[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从数据库查询以插件名前缀的表
|
||||
$prefix = $this->pluginName . '_';
|
||||
try {
|
||||
$connection = $this->connection ?? null;
|
||||
$db = Db::connection($connection)->getDoctrineConnection();
|
||||
$tablesList = $db->getSchemaManager()->listTableNames();
|
||||
|
||||
foreach ($tablesList as $tableName) {
|
||||
if (strpos($tableName, $prefix) === 0) {
|
||||
$tables[] = $tableName;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 忽略查询错误
|
||||
}
|
||||
|
||||
return array_unique($tables);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚单个迁移
|
||||
*/
|
||||
protected function rollbackMigration(string $name, Builder $schema): void
|
||||
{
|
||||
$this->output(" 📝 Rolling back: {$name}");
|
||||
|
||||
$migrationsDir = $this->findMigrationsDir();
|
||||
$file = $migrationsDir . '/' . $name . '.php';
|
||||
|
||||
if (!file_exists($file)) {
|
||||
$this->output(" ⚠️ Migration file not found: {$file}");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$migration = require $file;
|
||||
$migration->down($schema);
|
||||
$this->output(" ✅ Rolled back: {$name}");
|
||||
} catch (\Throwable $e) {
|
||||
$this->output(" ❌ Error: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找迁移目录
|
||||
*/
|
||||
protected function findMigrationsDir(): ?string
|
||||
{
|
||||
$resourceDir = $this->getConfig('resource.migration', 'database/migrations');
|
||||
|
||||
$dirs = [
|
||||
$this->pluginPath . '/resource/database/migrations',
|
||||
$this->pluginPath . '/' . $resourceDir,
|
||||
$this->pluginPath . '/install/migrations',
|
||||
base_path("resource/database/migrations/plugin/{$this->pluginName}"),
|
||||
];
|
||||
|
||||
foreach ($dirs as $dir) {
|
||||
if (is_dir($dir)) {
|
||||
return $dir;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Schema Builder
|
||||
*/
|
||||
protected function getSchemaBuilder(): Builder
|
||||
{
|
||||
return Db::connection($this->connection ?? null)->getSchemaBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取迁移日志文件
|
||||
*/
|
||||
protected function getMigrationLogFile(): string
|
||||
{
|
||||
return runtime_path(self::RUNTIME_PLUGIN_PATH . '/' . $this->pluginName . '-migrations.log');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已执行的迁移
|
||||
*/
|
||||
protected function getExecutedMigrations(): array
|
||||
{
|
||||
$logFile = $this->getMigrationLogFile();
|
||||
|
||||
if (!file_exists($logFile)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$content = trim(file_get_contents($logFile));
|
||||
|
||||
if (empty($content)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_filter(explode(PHP_EOL, $content));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取迁移记录行
|
||||
*/
|
||||
protected function getMigrationRows(): array
|
||||
{
|
||||
$logFile = $this->getMigrationLogFile();
|
||||
|
||||
if (!file_exists($logFile)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$content = trim(file_get_contents($logFile));
|
||||
|
||||
if (empty($content)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach (explode(PHP_EOL, $content) as $line) {
|
||||
$parts = explode(',', $line, 2);
|
||||
if (count($parts) === 2) {
|
||||
$rows[] = $parts;
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存迁移记录行
|
||||
*/
|
||||
protected function saveMigrationRows(array $rows): void
|
||||
{
|
||||
$logFile = $this->getMigrationLogFile();
|
||||
|
||||
$logDir = dirname($logFile);
|
||||
if (!is_dir($logDir)) {
|
||||
mkdir($logDir, 0755, true);
|
||||
}
|
||||
|
||||
if (empty($rows)) {
|
||||
@unlink($logFile);
|
||||
return;
|
||||
}
|
||||
|
||||
$lines = array_map(fn($r) => "{$r[0]},{$r[1]}", $rows);
|
||||
file_put_contents($logFile, implode(PHP_EOL, $lines) . PHP_EOL, LOCK_EX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一批次号
|
||||
*/
|
||||
protected function getNextBatch(): int
|
||||
{
|
||||
$rows = $this->getMigrationRows();
|
||||
|
||||
if (empty($rows)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$batches = array_map(fn($row) => (int)$row[0], $rows);
|
||||
|
||||
return max($batches) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录批次
|
||||
*/
|
||||
protected function recordBatch(int $batch, array $migrations): void
|
||||
{
|
||||
$logFile = $this->getMigrationLogFile();
|
||||
|
||||
$logDir = dirname($logFile);
|
||||
if (!is_dir($logDir)) {
|
||||
mkdir($logDir, 0755, true);
|
||||
}
|
||||
|
||||
$lines = array_map(fn($m) => "{$batch},{$m[1]}", $migrations);
|
||||
file_put_contents($logFile, PHP_EOL . implode(PHP_EOL, $lines), FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理日志
|
||||
*/
|
||||
protected function clearLogs(): void
|
||||
{
|
||||
@unlink($this->getMigrationLogFile());
|
||||
@unlink($this->getSeedLogFile());
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出 SQL(从迁移文件生成)
|
||||
*
|
||||
* @param bool $withData 是否包含数据
|
||||
* @param string|null $outputFile 输出文件路径(null 则输出到插件目录)
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public function exportSql(bool $withData = false, ?string $outputFile = null): bool
|
||||
{
|
||||
$migrationsDir = $this->findMigrationsDir();
|
||||
|
||||
if (!$migrationsDir || !is_dir($migrationsDir)) {
|
||||
$this->output("⚠️ No migrations directory found");
|
||||
return false;
|
||||
}
|
||||
|
||||
$files = glob($migrationsDir . '/*.php');
|
||||
|
||||
if (empty($files)) {
|
||||
$this->output("⚠️ No migration files found");
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->output("📤 Exporting SQL from " . count($files) . " migration file(s)...");
|
||||
|
||||
// 从迁移文件生成 SQL
|
||||
$sql = $this->generateSqlFromMigrations($files, $withData);
|
||||
|
||||
// 默认输出到插件的 resource/database 目录
|
||||
if (!$outputFile) {
|
||||
$outputDir = $this->pluginPath . '/resource/database';
|
||||
if (!is_dir($outputDir)) {
|
||||
mkdir($outputDir, 0755, true);
|
||||
}
|
||||
$filename = $withData ? 'install.sql' : 'structure.sql';
|
||||
$outputFile = $outputDir . '/' . $filename;
|
||||
}
|
||||
|
||||
$dir = dirname($outputFile);
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
file_put_contents($outputFile, $sql);
|
||||
|
||||
$this->output("✅ SQL exported to: {$outputFile}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从迁移文件生成 SQL
|
||||
*/
|
||||
protected function generateSqlFromMigrations(array $files, bool $withData): string
|
||||
{
|
||||
$sql = "-- Plugin: {$this->pluginName}\n";
|
||||
$sql .= "-- Exported at: " . date('Y-m-d H:i:s') . "\n";
|
||||
$sql .= "-- Generated from migration files\n\n";
|
||||
|
||||
$sql .= "SET FOREIGN_KEY_CHECKS = 0;\n\n";
|
||||
|
||||
// 按文件名排序
|
||||
sort($files);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$basename = basename($file, '.php');
|
||||
$this->output(" 📤 Processing: {$basename}");
|
||||
|
||||
$sql .= $this->generateSqlFromMigrationFile($file, $withData);
|
||||
$sql .= "\n";
|
||||
}
|
||||
|
||||
$sql .= "SET FOREIGN_KEY_CHECKS = 1;\n";
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从单个迁移文件生成 SQL
|
||||
*/
|
||||
protected function generateSqlFromMigrationFile(string $file, bool $withData): string
|
||||
{
|
||||
$sql = "";
|
||||
$content = file_get_contents($file);
|
||||
|
||||
// 提取所有表名
|
||||
preg_match_all('/->create\([\'"](\w+)[\'"]\s*,/', $content, $tableMatches);
|
||||
|
||||
if (empty($tableMatches[1])) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
foreach ($tableMatches[1] as $tableName) {
|
||||
$sql .= "-- ----------------------------\n";
|
||||
$sql .= "-- Table structure for {$tableName}\n";
|
||||
$sql .= "-- ----------------------------\n";
|
||||
$sql .= "DROP TABLE IF EXISTS `{$tableName}`;\n";
|
||||
|
||||
// 从迁移代码提取 CREATE TABLE 部分
|
||||
// 匹配 ->create('table_name', function (Blueprint $table) { ... });
|
||||
$pattern = '/->create\([\'"]' . preg_quote($tableName, '/') . '[\'"]\s*,\s*function\s*\([^)]*\)\s*\{([\s\S]*?)\}\s*\)/';
|
||||
|
||||
if (preg_match($pattern, $content, $matches)) {
|
||||
$body = $matches[1];
|
||||
$sql .= $this->generateCreateTableFromBlueprint($tableName, $body);
|
||||
}
|
||||
|
||||
$sql .= ";\n\n";
|
||||
|
||||
// 如果需要数据,尝试从数据库获取(如果表存在)
|
||||
if ($withData) {
|
||||
$sql .= $this->generateInsertDataSql($tableName);
|
||||
$sql .= "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Blueprint 代码生成 CREATE TABLE
|
||||
*/
|
||||
protected function generateCreateTableFromBlueprint(string $tableName, string $body): string
|
||||
{
|
||||
$columns = [];
|
||||
$indexes = [];
|
||||
|
||||
// 匹配 $table->列方法
|
||||
// $table->id();
|
||||
// $table->string('name', 100);
|
||||
// $table->text('content')->nullable();
|
||||
// $table->foreignId('user_id')->references('id')->on('users');
|
||||
|
||||
// 匹配各种列定义
|
||||
$lines = explode("\n", $body);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (empty($line) || $line === '$table->') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 移除 $table-> 前缀和结尾分号
|
||||
$line = ltrim($line, '$table->');
|
||||
$line = rtrim($line, ';');
|
||||
|
||||
// 匹配方法调用
|
||||
if (preg_match('/^(\w+)\s*\(([^)]*)\)(.*)/', $line, $matches)) {
|
||||
$method = $matches[1];
|
||||
$args = $matches[2];
|
||||
$modifiers = $matches[3] ?? '';
|
||||
|
||||
$colDef = $this->parseBlueprintMethod($method, $args, $modifiers);
|
||||
|
||||
if ($colDef) {
|
||||
$columns[] = $colDef;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($columns)) {
|
||||
return "CREATE TABLE `{$tableName}` ()";
|
||||
}
|
||||
|
||||
return "CREATE TABLE `{$tableName}` (\n " . implode(",\n ", $columns) . "\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Blueprint 方法
|
||||
*/
|
||||
protected function parseBlueprintMethod(string $method, string $args, string $modifiers): ?string
|
||||
{
|
||||
// 解析参数
|
||||
$argList = [];
|
||||
if ($args) {
|
||||
// 简单解析逗号分隔的参数
|
||||
$depth = 0;
|
||||
$current = '';
|
||||
for ($i = 0; $i < strlen($args); $i++) {
|
||||
$char = $args[$i];
|
||||
if ($char === '(') {
|
||||
$depth++;
|
||||
$current .= $char;
|
||||
} elseif ($char === ')') {
|
||||
$depth--;
|
||||
$current .= $char;
|
||||
} elseif ($char === ',' && $depth === 0) {
|
||||
$argList[] = trim($current);
|
||||
$current = '';
|
||||
} else {
|
||||
$current .= $char;
|
||||
}
|
||||
}
|
||||
if ($current) {
|
||||
$argList[] = trim($current);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理修饰符
|
||||
$nullable = strpos($modifiers, '->nullable()') !== false;
|
||||
$default = null;
|
||||
if (preg_match('/->default\(([^)]+)\)/', $modifiers, $dm)) {
|
||||
$default = $dm[1];
|
||||
}
|
||||
$autoIncrement = strpos($modifiers, '->autoIncrement()') !== false
|
||||
|| strpos($modifiers, '->autoIncrements()') !== false;
|
||||
$primary = strpos($modifiers, '->primary()') !== false;
|
||||
|
||||
// 根据方法名生成列定义
|
||||
return match ($method) {
|
||||
'id' => $this->formatColumn('id', 'bigint', $nullable, $default, $autoIncrement, $primary),
|
||||
'bigIncrements' => $this->formatColumn(null, 'bigint', false, null, true, true),
|
||||
'bigInteger' => $this->formatColumn($argList[0] ?? null, 'bigint', $nullable, $default, $autoIncrement, $primary),
|
||||
'integer', 'increments', 'mediumIncrements', 'smallIncrements', 'tinyIncrements' =>
|
||||
$this->formatColumn($argList[0] ?? null, 'int', $nullable, $default, $autoIncrement, $primary),
|
||||
'bigInteger' => $this->formatColumn($argList[0] ?? null, 'bigint', $nullable, $default, $autoIncrement, $primary),
|
||||
'string' => $this->formatColumn($argList[0] ?? null, 'varchar(' . ($argList[1] ?? 255) . ')', $nullable, $default, false, false),
|
||||
'text' => $this->formatColumn($argList[0] ?? null, 'text', $nullable, $default, false, false),
|
||||
'longText' => $this->formatColumn($argList[0] ?? null, 'longtext', $nullable, $default, false, false),
|
||||
'mediumText' => $this->formatColumn($argList[0] ?? null, 'mediumtext', $nullable, $default, false, false),
|
||||
'json' => $this->formatColumn($argList[0] ?? null, 'json', $nullable, $default, false, false),
|
||||
'boolean' => $this->formatColumn($argList[0] ?? null, 'tinyint(1)', $nullable, $default, false, false),
|
||||
'date' => $this->formatColumn($argList[0] ?? null, 'date', $nullable, $default, false, false),
|
||||
'datetime' => $this->formatColumn($argList[0] ?? null, 'datetime', $nullable, $default, false, false),
|
||||
'timestamp' => $this->formatColumn($argList[0] ?? null, 'timestamp', $nullable, $default, false, false),
|
||||
'time' => $this->formatColumn($argList[0] ?? null, 'time', $nullable, $default, false, false),
|
||||
'float' => $this->formatColumn($argList[0] ?? null, 'float', $nullable, $default, false, false),
|
||||
'double' => $this->formatColumn($argList[0] ?? null, 'double', $nullable, $default, false, false),
|
||||
'decimal' => $this->formatColumn($argList[0] ?? null, 'decimal(' . ($argList[1] ?? '10,2') . ')', $nullable, $default, false, false),
|
||||
'enum' => $this->formatColumn($argList[0] ?? null, 'enum(' . ($argList[1] ?? '') . ')', $nullable, $default, false, false),
|
||||
'uuid' => $this->formatColumn($argList[0] ?? null, 'char(36)', $nullable, $default, false, false),
|
||||
'foreignId', 'foreignIdFor' => $this->formatColumn($argList[0] ?? null, 'bigint', $nullable, $default, false, false),
|
||||
'foreignUuid' => $this->formatColumn($argList[0] ?? null, 'char(36)', $nullable, $default, false, false),
|
||||
'rememberToken' => $this->formatColumn('remember_token', 'varchar(100)', true, null, false, false),
|
||||
'timestamps' => "`created_at` datetime NULL,\n `updated_at` datetime NULL",
|
||||
'timestampable' => "`created_at` datetime NULL,\n `updated_at` datetime NULL",
|
||||
'softDeletes' => "`deleted_at` datetime NULL",
|
||||
'softDeletesTz' => "`deleted_at` datetime NULL",
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化列定义
|
||||
*/
|
||||
protected function formatColumn(?string $name, string $type, bool $nullable, ?string $default, bool $autoIncrement, bool $primary): ?string
|
||||
{
|
||||
if (!$name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$col = "`{$name}` {$type}";
|
||||
|
||||
if ($nullable) {
|
||||
$col .= " NULL";
|
||||
} else {
|
||||
$col .= " NOT NULL";
|
||||
}
|
||||
|
||||
if ($autoIncrement) {
|
||||
$col .= " AUTO_INCREMENT";
|
||||
}
|
||||
|
||||
if ($default !== null) {
|
||||
$col .= " DEFAULT " . $default;
|
||||
}
|
||||
|
||||
if ($primary) {
|
||||
$col .= ", PRIMARY KEY (`{$name}`)";
|
||||
}
|
||||
|
||||
return $col;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 INSERT DATA SQL(从数据库)
|
||||
*/
|
||||
protected function generateInsertDataSql(string $table): string
|
||||
{
|
||||
$sql = "-- ----------------------------\n";
|
||||
$sql .= "-- Data for {$table}\n";
|
||||
$sql .= "-- ----------------------------\n";
|
||||
|
||||
try {
|
||||
$connection = $this->connection ?? null;
|
||||
$pdo = Db::connection($connection)->getPdo();
|
||||
|
||||
// 检查表是否存在
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE '{$table}'");
|
||||
if ($stmt->rowCount() === 0) {
|
||||
$sql .= "-- Table does not exist, skipping data\n";
|
||||
return $sql;
|
||||
}
|
||||
|
||||
$stmt = $pdo->query("SELECT * FROM `{$table}`");
|
||||
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($rows)) {
|
||||
$sql .= "-- No data\n";
|
||||
return $sql;
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$values = array_map([$this, 'quoteValue'], array_values($row));
|
||||
$sql .= "INSERT INTO `{$table}` (`" . implode('`, `', array_keys($row)) . "`) VALUES (" . implode(', ', $values) . ");\n";
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$sql .= "-- Error: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义值
|
||||
*/
|
||||
protected function quoteValue(mixed $value): string
|
||||
{
|
||||
if ($value === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
|
||||
if (is_bool($value)) {
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
if (is_int($value) || is_float($value)) {
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
// 使用 PDO 的 quote
|
||||
$connection = $this->connection ?? null;
|
||||
$pdo = Db::connection($connection)->getPdo();
|
||||
return $pdo->quote($value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace core\plugin\traits;
|
||||
|
||||
/**
|
||||
* 种子操作 Trait
|
||||
*/
|
||||
trait SeedTrait
|
||||
{
|
||||
/**
|
||||
* 插件临时目录路径
|
||||
*/
|
||||
const RUNTIME_PLUGIN_PATH = 'install/plugin';
|
||||
|
||||
/**
|
||||
* 运行种子
|
||||
*/
|
||||
public function runSeeds(): void
|
||||
{
|
||||
$seedsDir = $this->findSeedsDir();
|
||||
|
||||
if (!$seedsDir) {
|
||||
$this->output("⚠️ No seeds directory found");
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output("📁 Seeds dir: {$seedsDir}");
|
||||
|
||||
$executed = $this->getExecutedSeeds();
|
||||
|
||||
$files = glob($seedsDir . '/*.php');
|
||||
|
||||
$pending = [];
|
||||
foreach ($files as $file) {
|
||||
$basename = basename($file, '.php');
|
||||
$seedName = str_replace('Seeder', '', $basename);
|
||||
if (!in_array($seedName, $executed)) {
|
||||
$pending[] = [$file, $basename];
|
||||
}
|
||||
}
|
||||
|
||||
sort($pending);
|
||||
|
||||
if (empty($pending)) {
|
||||
$this->output("✅ No pending seeds");
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output("📋 Pending seeds: " . count($pending));
|
||||
|
||||
foreach ($pending as $seed) {
|
||||
$this->runSeed($seed[0], $seed[1]);
|
||||
}
|
||||
|
||||
$this->output("✅ Seeds completed");
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行单个种子
|
||||
*/
|
||||
protected function runSeed(string $file, string $className): void
|
||||
{
|
||||
$this->output(" 📝 Seeding: {$className}");
|
||||
|
||||
require_once $file;
|
||||
|
||||
$seeder = null;
|
||||
|
||||
$classes = get_declared_classes();
|
||||
foreach ($classes as $cls) {
|
||||
if (str_ends_with($cls, '\\' . $className) || $cls === $className) {
|
||||
$seeder = new $cls();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$seeder) {
|
||||
$this->output(" ❌ Seeder class not found: {$className}");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (method_exists($seeder, 'run')) {
|
||||
$seeder->run();
|
||||
}
|
||||
$this->output(" ✅ Seeded: {$className}");
|
||||
|
||||
$this->recordSeed(str_replace('Seeder', '', $className));
|
||||
} catch (\Throwable $e) {
|
||||
$this->output(" ❌ Error: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找种子目录
|
||||
*/
|
||||
protected function findSeedsDir(): ?string
|
||||
{
|
||||
$resourceDir = $this->getConfig('resource.seed', 'database/seeds');
|
||||
|
||||
$dirs = [
|
||||
$this->pluginPath . '/resource/database/seeds',
|
||||
$this->pluginPath . '/' . $resourceDir,
|
||||
$this->pluginPath . '/install/seeds',
|
||||
base_path("resource/database/seeds/plugin/{$this->pluginName}"),
|
||||
];
|
||||
|
||||
foreach ($dirs as $dir) {
|
||||
if (is_dir($dir)) {
|
||||
return $dir;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取种子日志文件
|
||||
*/
|
||||
protected function getSeedLogFile(): string
|
||||
{
|
||||
return runtime_path(self::RUNTIME_PLUGIN_PATH . '/' . $this->pluginName . '-seeds.log');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已执行的种子
|
||||
*/
|
||||
protected function getExecutedSeeds(): array
|
||||
{
|
||||
$logFile = $this->getSeedLogFile();
|
||||
|
||||
if (!file_exists($logFile)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$content = trim(file_get_contents($logFile));
|
||||
|
||||
if (empty($content)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_filter(explode(PHP_EOL, $content));
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录种子
|
||||
*/
|
||||
protected function recordSeed(string $name): void
|
||||
{
|
||||
$logFile = $this->getSeedLogFile();
|
||||
|
||||
$logDir = dirname($logFile);
|
||||
if (!is_dir($logDir)) {
|
||||
mkdir($logDir, 0755, true);
|
||||
}
|
||||
|
||||
file_put_contents($logFile, PHP_EOL . $name, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace core\plugin\traits;
|
||||
|
||||
/**
|
||||
* 模板资源操作 Trait
|
||||
*/
|
||||
trait TemplateTrait
|
||||
{
|
||||
/**
|
||||
* 复制模板资源到目标目录
|
||||
*/
|
||||
protected function copyTemplates(): void
|
||||
{
|
||||
$templateDir = $this->pluginPath . '/resource/template';
|
||||
|
||||
if (!is_dir($templateDir)) {
|
||||
$this->output("📂 No template directory found, skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output("📂 Copying templates from {$templateDir}...");
|
||||
|
||||
$this->copyTemplateDir('admin');
|
||||
$this->copyTemplateDir('web');
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制单个端的模板
|
||||
*/
|
||||
protected function copyTemplateDir(string $endpoint): void
|
||||
{
|
||||
$resourceDir = $this->getConfig('resource.template', 'template');
|
||||
$targetBase = $this->getConfig("template.{$endpoint}");
|
||||
|
||||
$sourceDir = $this->pluginPath . '/resource/' . $resourceDir . '/' . $endpoint;
|
||||
$targetDir = $this->getProjectRoot() . '/' . $targetBase . '/' . $this->pluginName;
|
||||
|
||||
if (!is_dir($sourceDir)) {
|
||||
$this->output(" ⚠️ {$endpoint}: Template source not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_dir(dirname($targetDir))) {
|
||||
mkdir(dirname($targetDir), 0755, true);
|
||||
}
|
||||
|
||||
$this->recurseCopy($sourceDir, $targetDir);
|
||||
|
||||
$this->output(" ✅ {$endpoint}: Templates copied to {$targetDir}");
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归复制目录
|
||||
*/
|
||||
protected function recurseCopy(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;
|
||||
}
|
||||
|
||||
$sourcePath = $source . '/' . $file;
|
||||
$targetPath = $target . '/' . $file;
|
||||
|
||||
if (is_dir($sourcePath)) {
|
||||
$this->recurseCopy($sourcePath, $targetPath);
|
||||
} else {
|
||||
copy($sourcePath, $targetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除插件模板资源
|
||||
*/
|
||||
protected function deleteTemplates(): void
|
||||
{
|
||||
if ($this->preserveTemplatesDuringUninstall) {
|
||||
$this->output('📦 Template resources are preserved during uninstall.');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output("🗑️ Deleting plugin templates...");
|
||||
|
||||
$this->deleteTemplateDir('admin');
|
||||
$this->deleteTemplateDir('web');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除单个端模板
|
||||
*/
|
||||
protected function deleteTemplateDir(string $endpoint): void
|
||||
{
|
||||
$targetBase = $this->getConfig("template.{$endpoint}");
|
||||
|
||||
$targetDir = $this->getProjectRoot() . '/' . $targetBase . '/' . $this->pluginName;
|
||||
|
||||
if (!is_dir($targetDir)) {
|
||||
$this->output(" ⚠️ {$endpoint}: Template directory not found");
|
||||
return;
|
||||
}
|
||||
|
||||
$this->recurseDelete($targetDir);
|
||||
|
||||
$this->output(" ✅ {$endpoint}: Templates deleted");
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归删除目录
|
||||
*/
|
||||
protected function recurseDelete(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->recurseDelete($path);
|
||||
} else {
|
||||
unlink($path);
|
||||
}
|
||||
}
|
||||
|
||||
rmdir($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入模板(子类可重写)
|
||||
*/
|
||||
protected function importTemplates(): void
|
||||
{
|
||||
$this->copyTemplates();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目根目录
|
||||
*/
|
||||
protected function getProjectRoot(): string
|
||||
{
|
||||
return dirname(base_path());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取前端基础目录
|
||||
* 前端代码统一在 frontend 目录下
|
||||
*/
|
||||
protected function getFrontendPath(): string
|
||||
{
|
||||
return $this->getProjectRoot() . DIRECTORY_SEPARATOR . 'frontend';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取前端指定项目目录
|
||||
*
|
||||
* @param string $frontendType 前端类型(admin/web/uni-app)
|
||||
* @return string 前端项目目录
|
||||
*/
|
||||
protected function getFrontendProjectPath(string $frontendType): string
|
||||
{
|
||||
return $this->getFrontendPath() . DIRECTORY_SEPARATOR . $frontendType;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user