chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,972 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 插件健康检查命令(合并版)
|
||||
*
|
||||
* 融合原 addon:health(人工排查 + --fix 自动修复)与 addon:health-check
|
||||
* (系统环境 / 缓存系统检查 + JSON/HTML 输出 + 告警 + 报告存档)的能力。
|
||||
*
|
||||
* php think addon:health # 系统级 + 全部插件健康检查
|
||||
* php think addon:health <name> # 单独检查某个插件(含详情)
|
||||
* php think addon:health --detail # 显示详细信息
|
||||
* php think addon:health --fix # 尝试自动修复问题(清缓存/修权限)
|
||||
* php think addon:health --output=json # 输出 JSON(机器消费)
|
||||
* php think addon:health --output=html # 输出 HTML 报告
|
||||
* php think addon:health --send-alert # 异常时发送告警
|
||||
* php think addon:health --webhook=URL # 告警 Webhook 地址
|
||||
* php think addon:health --email=ADDR # 告警邮箱
|
||||
* php think addon:health --save-report # 存档检查报告(runtime 保留 30 天)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ywxapp\command;
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\Log;
|
||||
use think\facade\Config;
|
||||
use think\facade\Cache;
|
||||
use ywxapp\service\AddonPerformanceMonitor;
|
||||
|
||||
/**
|
||||
* AddonHealth 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class AddonHealth extends Command
|
||||
{
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('addon:health')
|
||||
->addArgument('addon', Argument::OPTIONAL, '插件名称(留空则检查全部插件 + 系统环境)')
|
||||
->addOption('detail', 'd', Option::VALUE_NONE, '显示详细信息')
|
||||
->addOption('fix', 'f', Option::VALUE_NONE, '尝试自动修复问题(清缓存 / 修权限)')
|
||||
->addOption('output', 'o', Option::VALUE_OPTIONAL, '输出格式 (text, json, html)', 'text')
|
||||
->addOption('send-alert', 'a', Option::VALUE_NONE, '异常时发送告警通知')
|
||||
->addOption('webhook', 'w', Option::VALUE_OPTIONAL, '告警 Webhook URL')
|
||||
->addOption('email', 'e', Option::VALUE_OPTIONAL, '告警邮箱')
|
||||
->addOption('save-report', 's', Option::VALUE_NONE, '保存检查报告')
|
||||
->setDescription('插件健康检查(含系统环境 / 缓存 / 文件 / 性能 / 依赖 / 告警)');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*
|
||||
* @param Input $input 输入
|
||||
* @param Output $output 输出
|
||||
* @return int 退出码(0 健康 / 1 异常)
|
||||
*/
|
||||
protected function execute(Input $input, Output $output): int
|
||||
{
|
||||
$addon = $input->getArgument('addon');
|
||||
$detail = $input->getOption('detail');
|
||||
$fix = $input->getOption('fix');
|
||||
$outputFormat = $input->getOption('output') ?: 'text';
|
||||
$sendAlert = $input->getOption('send-alert');
|
||||
$webhookUrl = $input->getOption('webhook');
|
||||
$email = $input->getOption('email');
|
||||
$saveReport = $input->getOption('save-report');
|
||||
|
||||
// 仅文本模式打印标题横幅,避免污染 JSON/HTML 输出(JSON/HTML 模式下保持纯输出)
|
||||
if ($outputFormat === 'text') {
|
||||
$output->writeln('<info>=== 插件健康检查 ===</info>');
|
||||
$output->writeln('');
|
||||
}
|
||||
|
||||
// 构建检查报告(始终含系统环境 + 缓存系统检查)
|
||||
$report = $this->buildReport($addon, $detail);
|
||||
|
||||
// 按格式输出
|
||||
switch ($outputFormat) {
|
||||
case 'json':
|
||||
$this->outputJson($report, $output);
|
||||
break;
|
||||
case 'html':
|
||||
$this->outputHtml($report, $output);
|
||||
break;
|
||||
default:
|
||||
$this->outputText($report, $output);
|
||||
// 单插件 + --detail 时,补充版本 / 作者 / 依赖等详细信息
|
||||
if ($addon && $detail) {
|
||||
$this->outputSingleDetail($addon, $report['addon'][$addon] ?? [], $output);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 自动修复
|
||||
if ($fix) {
|
||||
$this->runAutoFix($report, $output);
|
||||
}
|
||||
|
||||
// 发送告警
|
||||
if ($sendAlert && !$report['healthy']) {
|
||||
$this->sendAlert($report, $webhookUrl, $email, $output);
|
||||
}
|
||||
|
||||
// 保存报告
|
||||
if ($saveReport) {
|
||||
$this->saveReport($report, $output);
|
||||
}
|
||||
|
||||
return $report['healthy'] ? 0 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建健康检查报告
|
||||
*/
|
||||
private function buildReport(?string $addon, bool $detail): array
|
||||
{
|
||||
$report = [
|
||||
'healthy' => true,
|
||||
'timestamp' => time(),
|
||||
'checks' => [],
|
||||
'issues' => [],
|
||||
'warnings' => [],
|
||||
'addon' => []
|
||||
];
|
||||
|
||||
// 1/2. 系统环境 + 缓存系统(始终检查,属全局健康)
|
||||
$report['checks']['system'] = $this->checkSystemEnvironment();
|
||||
$report['checks']['cache'] = $this->checkCacheSystem();
|
||||
|
||||
if ($addon) {
|
||||
// 单插件深检
|
||||
$report['addon'][$addon] = $this->getAddonHealth($addon);
|
||||
// --detail 时把性能采样并入报告,使 JSON/HTML 输出也包含详情
|
||||
// (version/author/dependencies 等已在 getAddonHealth 返回中,此处补性能)
|
||||
if ($detail && !empty($report['addon'][$addon]['enabled'])) {
|
||||
$stats = AddonPerformanceMonitor::getPerformanceStats($addon);
|
||||
if ($stats['total_calls'] > 0) {
|
||||
$report['addon'][$addon]['performance'] = $stats;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 全量:补充文件 / 性能 / 依赖 / 状态聚合检查
|
||||
$report['checks']['files'] = $this->checkAddonFiles();
|
||||
$report['checks']['performance'] = $this->checkAddonPerformance();
|
||||
$report['checks']['dependencies'] = $this->checkDependencies();
|
||||
$report['checks']['status'] = $this->checkaddontatus();
|
||||
|
||||
$addon = Config::get('addon', []);
|
||||
foreach ($addon as $a) {
|
||||
if (!isset($a['name'])) {
|
||||
continue;
|
||||
}
|
||||
$report['addon'][$a['name']] = $this->getAddonHealth($a['name']);
|
||||
}
|
||||
}
|
||||
|
||||
// 聚合 checks 中的问题 / 警告
|
||||
foreach ($report['checks'] as $checkResult) {
|
||||
if (!$checkResult['healthy']) {
|
||||
$report['healthy'] = false;
|
||||
$report['issues'] = array_merge($report['issues'], $checkResult['issues']);
|
||||
}
|
||||
if (!empty($checkResult['warnings'])) {
|
||||
$report['warnings'] = array_merge($report['warnings'], $checkResult['warnings']);
|
||||
}
|
||||
}
|
||||
|
||||
// 插件个体健康状态影响整体
|
||||
foreach ($report['addon'] as $health) {
|
||||
if (!$health['healthy']) {
|
||||
$report['healthy'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $report;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个插件健康状态(丰富信息:目录 / 文件 / info / 依赖 / 扩展 / 主类 / 性能)
|
||||
*/
|
||||
private function getAddonHealth(string $addon): array
|
||||
{
|
||||
$health = [
|
||||
'healthy' => true,
|
||||
'issues' => [],
|
||||
'warnings' => [],
|
||||
'enabled' => false,
|
||||
'install_time' => null
|
||||
];
|
||||
|
||||
$addonPath = ADDON_PATH . $addon;
|
||||
|
||||
// 检查插件目录
|
||||
if (!is_dir($addonPath)) {
|
||||
$health['healthy'] = false;
|
||||
$health['issues'][] = "插件目录不存在: {$addonPath}";
|
||||
return $health;
|
||||
}
|
||||
|
||||
// 检查必需文件
|
||||
$requiredFiles = ['info.php', 'Addon.php'];
|
||||
foreach ($requiredFiles as $file) {
|
||||
if (!is_file($addonPath . DIRECTORY_SEPARATOR . $file)) {
|
||||
$health['healthy'] = false;
|
||||
$health['issues'][] = "缺少必需文件: {$file}";
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 info.php
|
||||
$infoFile = $addonPath . DIRECTORY_SEPARATOR . 'info.php';
|
||||
if (is_file($infoFile)) {
|
||||
$info = include $infoFile;
|
||||
|
||||
$health['enabled'] = ($info['state'] ?? false) == 1;
|
||||
$health['install_time'] = $info['install_time'] ?? null;
|
||||
$health['version'] = $info['version'] ?? null;
|
||||
$health['author'] = $info['author'] ?? null;
|
||||
$health['description'] = $info['intro'] ?? null;
|
||||
|
||||
// 检查依赖插件
|
||||
if (isset($info['dependencies']) && !empty($info['dependencies'])) {
|
||||
$health['dependencies'] = $info['dependencies'];
|
||||
foreach ($info['dependencies'] as $dep) {
|
||||
if (!$this->isAddonInstalled($dep)) {
|
||||
$health['healthy'] = false;
|
||||
$health['issues'][] = "依赖插件 {$dep} 未安装";
|
||||
} elseif (!$this->isAddonEnabled($dep)) {
|
||||
$health['healthy'] = false;
|
||||
$health['issues'][] = "依赖插件 {$dep} 未启用";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 PHP 扩展
|
||||
if (isset($info['extensions']) && !empty($info['extensions'])) {
|
||||
$health['extensions'] = $info['extensions'];
|
||||
foreach ($info['extensions'] as $ext) {
|
||||
if (!extension_loaded($ext)) {
|
||||
$health['healthy'] = false;
|
||||
$health['issues'][] = "缺少PHP扩展: {$ext}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查插件主类与必需方法
|
||||
$addonClass = '\\addon\\' . $addon . '\\Addon';
|
||||
if (class_exists($addonClass)) {
|
||||
try {
|
||||
$instance = app()->make($addonClass);
|
||||
|
||||
foreach (['install', 'uninstall'] as $method) {
|
||||
if (!method_exists($instance, $method)) {
|
||||
$health['healthy'] = false;
|
||||
$health['issues'][] = "缺少必需方法: {$method}()";
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$health['healthy'] = false;
|
||||
$health['issues'][] = "插件类初始化失败: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// 性能检查
|
||||
if ($health['enabled']) {
|
||||
$stats = AddonPerformanceMonitor::getPerformanceStats($addon);
|
||||
if ($stats['total_calls'] > 10) {
|
||||
if ($stats['avg_execution_time'] > AddonPerformanceMonitor::PERFORMANCE_THRESHOLD) {
|
||||
$health['warnings'][] = "执行时间过长: " . number_format($stats['avg_execution_time'], 2) . "秒";
|
||||
}
|
||||
if ($stats['avg_memory_usage'] > AddonPerformanceMonitor::MEMORY_THRESHOLD) {
|
||||
$health['warnings'][] = "内存使用过多: " . number_format($stats['avg_memory_usage'] / 1024 / 1024, 2) . "MB";
|
||||
}
|
||||
if ($stats['success_rate'] < 90) {
|
||||
$health['healthy'] = false;
|
||||
$health['issues'][] = "成功率过低: " . number_format($stats['success_rate'], 2) . "%";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $health;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查系统环境(PHP 版本 / 必需扩展 / runtime 可写 / 磁盘空间)
|
||||
*/
|
||||
private function checkSystemEnvironment(): array
|
||||
{
|
||||
$result = [
|
||||
'healthy' => true,
|
||||
'issues' => [],
|
||||
'warnings' => [],
|
||||
'details' => []
|
||||
];
|
||||
|
||||
// PHP 版本
|
||||
$phpVersion = PHP_VERSION;
|
||||
$result['details']['php_version'] = $phpVersion;
|
||||
if (version_compare($phpVersion, '8.0.0', '<')) {
|
||||
$result['healthy'] = false;
|
||||
$result['issues'][] = "PHP版本过低 ({$phpVersion}),要求 >= 8.0.0";
|
||||
}
|
||||
|
||||
// 必需扩展
|
||||
foreach (['json', 'mbstring', 'curl', 'zip'] as $ext) {
|
||||
$loaded = extension_loaded($ext);
|
||||
$result['details'][$ext] = $loaded;
|
||||
if (!$loaded) {
|
||||
$result['healthy'] = false;
|
||||
$result['issues'][] = "缺少PHP扩展: {$ext}";
|
||||
}
|
||||
}
|
||||
|
||||
// runtime 目录可写
|
||||
$runtimePath = root_path() . 'runtime';
|
||||
if (!is_writable($runtimePath)) {
|
||||
$result['healthy'] = false;
|
||||
$result['issues'][] = "runtime目录不可写: {$runtimePath}";
|
||||
}
|
||||
|
||||
// 磁盘空间
|
||||
$freeSpace = disk_free_space(root_path());
|
||||
$result['details']['disk_free_space'] = $this->formatBytes((int) $freeSpace);
|
||||
if ($freeSpace < 104857600) { // 小于 100MB
|
||||
$result['warnings'][] = "磁盘空间不足,剩余: " . $this->formatBytes((int) $freeSpace);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存系统
|
||||
*/
|
||||
private function checkCacheSystem(): array
|
||||
{
|
||||
$result = [
|
||||
'healthy' => true,
|
||||
'issues' => [],
|
||||
'warnings' => [],
|
||||
'details' => []
|
||||
];
|
||||
|
||||
try {
|
||||
$testKey = 'health_check_test_' . time();
|
||||
$testValue = ['test' => true, 'timestamp' => time()];
|
||||
|
||||
Cache::set($testKey, $testValue, 60);
|
||||
$retrieved = Cache::get($testKey);
|
||||
|
||||
if ($retrieved === false) {
|
||||
$result['healthy'] = false;
|
||||
$result['issues'][] = "缓存写入/读取失败";
|
||||
} else {
|
||||
$result['details']['cache_test'] = 'passed';
|
||||
}
|
||||
|
||||
Cache::delete($testKey);
|
||||
|
||||
// 插件缓存命中情况(仅警告,不影响健康)
|
||||
$addon = Config::get('addon', []);
|
||||
foreach ($addon as $addon) {
|
||||
if (!isset($addon['name']) || !($addon['state'] ?? false)) {
|
||||
continue;
|
||||
}
|
||||
if (Cache::get('addon_config_' . $addon['name']) === false) {
|
||||
$result['warnings'][] = "插件 {$addon['name']} 缓存未命中,可能影响性能";
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$result['healthy'] = false;
|
||||
$result['issues'][] = "缓存系统异常: " . $e->getMessage();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查插件文件完整性(全量)
|
||||
*/
|
||||
private function checkAddonFiles(): array
|
||||
{
|
||||
$result = [
|
||||
'healthy' => true,
|
||||
'issues' => [],
|
||||
'warnings' => [],
|
||||
'details' => []
|
||||
];
|
||||
|
||||
$addon = Config::get('addon', []);
|
||||
foreach ($addon as $addon) {
|
||||
if (!isset($addon['name'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$addonPath = ADDON_PATH . $addon['name'];
|
||||
$result['details'][$addon['name']] = [];
|
||||
|
||||
if (!is_dir($addonPath)) {
|
||||
$result['healthy'] = false;
|
||||
$result['issues'][] = "插件 {$addon['name']} 目录不存在";
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (['info.php', 'Addon.php'] as $file) {
|
||||
$filePath = $addonPath . DIRECTORY_SEPARATOR . $file;
|
||||
$exists = is_file($filePath);
|
||||
$result['details'][$addon['name']][$file] = $exists;
|
||||
if (!$exists) {
|
||||
$result['healthy'] = false;
|
||||
$result['issues'][] = "插件 {$addon['name']} 缺少必需文件: {$file}";
|
||||
}
|
||||
}
|
||||
|
||||
// 文件权限(不可读即警告)
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($addonPath),
|
||||
\RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
$permissionIssues = [];
|
||||
foreach ($iterator as $item) {
|
||||
if ($item->isFile() && ($item->getPerms() & 0x0004) === 0) {
|
||||
$permissionIssues[] = $item->getPathname();
|
||||
}
|
||||
}
|
||||
if (!empty($permissionIssues)) {
|
||||
$result['warnings'][] = "插件 {$addon['name']} 有文件权限问题";
|
||||
$result['details'][$addon['name']]['permission_issues'] = count($permissionIssues);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查插件性能(全量)
|
||||
*/
|
||||
private function checkAddonPerformance(): array
|
||||
{
|
||||
$result = [
|
||||
'healthy' => true,
|
||||
'issues' => [],
|
||||
'warnings' => [],
|
||||
'details' => []
|
||||
];
|
||||
|
||||
$problematicaddon = AddonPerformanceMonitor::getProblematicaddon();
|
||||
|
||||
if (!empty($problematicaddon)) {
|
||||
$result['healthy'] = false;
|
||||
foreach ($problematicaddon as $addon => $info) {
|
||||
$result['issues'][] = "插件 {$addon} 性能异常: " . implode(', ', $info['issues']);
|
||||
$result['details'][$addon] = $info['issues'];
|
||||
}
|
||||
}
|
||||
|
||||
$overview = AddonPerformanceMonitor::getAllPerformanceOverview();
|
||||
$totalCalls = $totalTime = $totalSuccess = 0;
|
||||
foreach ($overview as $stats) {
|
||||
$totalCalls += $stats['total_calls'];
|
||||
$totalTime += $stats['avg_execution_time'] * $stats['total_calls'];
|
||||
$totalSuccess += ($stats['success_rate'] / 100) * $stats['total_calls'];
|
||||
}
|
||||
if ($totalCalls > 0) {
|
||||
$avgTime = $totalTime / $totalCalls;
|
||||
$successRate = ($totalSuccess / $totalCalls) * 100;
|
||||
$result['details']['total_calls'] = $totalCalls;
|
||||
$result['details']['avg_execution_time'] = $avgTime;
|
||||
$result['details']['overall_success_rate'] = $successRate;
|
||||
|
||||
if ($avgTime > 1.0) {
|
||||
$result['warnings'][] = "总体平均执行时间过长: " . number_format($avgTime * 1000, 2) . " ms";
|
||||
}
|
||||
if ($successRate < 95) {
|
||||
$result['healthy'] = false;
|
||||
$result['issues'][] = "总体成功率过低: " . number_format($successRate, 2) . "%";
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查依赖关系(全量)
|
||||
*/
|
||||
private function checkDependencies(): array
|
||||
{
|
||||
$result = [
|
||||
'healthy' => true,
|
||||
'issues' => [],
|
||||
'warnings' => [],
|
||||
'details' => []
|
||||
];
|
||||
|
||||
$addon = Config::get('addon', []);
|
||||
$installedaddon = [];
|
||||
foreach ($addon as $addon) {
|
||||
if (isset($addon['name'])) {
|
||||
$installedaddon[$addon['name']] = $addon['state'] ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($addon as $addon) {
|
||||
if (!isset($addon['name'])) {
|
||||
continue;
|
||||
}
|
||||
$addonName = $addon['name'];
|
||||
$result['details'][$addonName] = [];
|
||||
|
||||
$addonClass = '\\addon\\' . $addonName . '\\Addon';
|
||||
if (!class_exists($addonClass)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$instance = app()->make($addonClass);
|
||||
|
||||
if (method_exists($instance, 'getDependencies')) {
|
||||
$dependencies = $instance->getDependencies();
|
||||
$result['details'][$addonName]['dependencies'] = $dependencies;
|
||||
foreach ($dependencies as $dep) {
|
||||
if (!isset($installedaddon[$dep])) {
|
||||
$result['healthy'] = false;
|
||||
$result['issues'][] = "插件 {$addonName} 依赖插件 {$dep} 未安装";
|
||||
} elseif (!$installedaddon[$dep]) {
|
||||
$result['healthy'] = false;
|
||||
$result['issues'][] = "插件 {$addonName} 依赖插件 {$dep} 未启用";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (method_exists($instance, 'getExtensions')) {
|
||||
$extensions = $instance->getExtensions();
|
||||
$result['details'][$addonName]['extensions'] = $extensions;
|
||||
foreach ($extensions as $ext) {
|
||||
if (!extension_loaded($ext)) {
|
||||
$result['healthy'] = false;
|
||||
$result['issues'][] = "插件 {$addonName} 需要PHP扩展: {$ext}";
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$result['warnings'][] = "无法检查插件 {$addonName} 的依赖: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查插件状态(全量)
|
||||
*/
|
||||
private function checkaddontatus(): array
|
||||
{
|
||||
$result = [
|
||||
'healthy' => true,
|
||||
'issues' => [],
|
||||
'warnings' => [],
|
||||
'details' => []
|
||||
];
|
||||
|
||||
$addon = Config::get('addon', []);
|
||||
$enabledCount = $disabledCount = $errorCount = 0;
|
||||
|
||||
foreach ($addon as $addon) {
|
||||
if (!isset($addon['name'])) {
|
||||
continue;
|
||||
}
|
||||
$addonName = $addon['name'];
|
||||
$isEnabled = ($addon['state'] ?? false) == 1;
|
||||
|
||||
if ($isEnabled) {
|
||||
$enabledCount++;
|
||||
$addonClass = '\\addon\\' . $addonName . '\\Addon';
|
||||
if (class_exists($addonClass)) {
|
||||
try {
|
||||
app()->make($addonClass);
|
||||
$result['details'][$addonName] = 'healthy';
|
||||
} catch (\Exception $e) {
|
||||
$errorCount++;
|
||||
$result['healthy'] = false;
|
||||
$result['issues'][] = "插件 {$addonName} 加载失败: " . $e->getMessage();
|
||||
$result['details'][$addonName] = 'error';
|
||||
}
|
||||
} else {
|
||||
$errorCount++;
|
||||
$result['healthy'] = false;
|
||||
$result['issues'][] = "插件 {$addonName} 主类不存在";
|
||||
$result['details'][$addonName] = 'missing';
|
||||
}
|
||||
} else {
|
||||
$disabledCount++;
|
||||
$result['details'][$addonName] = 'disabled';
|
||||
}
|
||||
}
|
||||
|
||||
$result['details']['enabled_count'] = $enabledCount;
|
||||
$result['details']['disabled_count'] = $disabledCount;
|
||||
$result['details']['error_count'] = $errorCount;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出文本格式
|
||||
*/
|
||||
private function outputText(array $report, Output $output): void
|
||||
{
|
||||
$output->writeln('检查时间: ' . date('Y-m-d H:i:s', $report['timestamp']));
|
||||
$output->writeln('整体状态: ' . ($report['healthy'] ? '<info>健康</info>' : '<error>异常</error>'));
|
||||
$output->writeln('');
|
||||
|
||||
// 检查项
|
||||
foreach ($report['checks'] as $checkName => $checkResult) {
|
||||
$status = $checkResult['healthy'] ? '<info>✓</info>' : '<error>✗</error>';
|
||||
$output->writeln("{$status} {$this->getCheckName($checkName)}");
|
||||
|
||||
foreach ($checkResult['issues'] ?? [] as $issue) {
|
||||
$output->writeln(" <error>✗</error> {$issue}");
|
||||
}
|
||||
foreach ($checkResult['warnings'] ?? [] as $warning) {
|
||||
$output->writeln(" <comment>⚠</comment> {$warning}");
|
||||
}
|
||||
}
|
||||
|
||||
// 插件状态
|
||||
if (!empty($report['addon'])) {
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>插件状态:</comment>');
|
||||
foreach ($report['addon'] as $addon => $health) {
|
||||
$status = $health['healthy'] ? '<info>✓</info>' : '<error>✗</error>';
|
||||
$output->writeln("{$status} {$addon}" .
|
||||
($health['enabled'] ? ' <comment>(已启用)</comment>' : ' <comment>(已禁用)</comment>'));
|
||||
|
||||
foreach ($health['issues'] ?? [] as $issue) {
|
||||
$output->writeln(" <error>✗</error> {$issue}");
|
||||
}
|
||||
foreach ($health['warnings'] ?? [] as $warning) {
|
||||
$output->writeln(" <comment>⚠</comment> {$warning}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 汇总
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>=== 检查汇总 ===</info>');
|
||||
$output->writeln('总问题数: ' . count($report['issues']));
|
||||
$output->writeln('总警告数: ' . count($report['warnings']));
|
||||
$output->writeln('插件总数: ' . count($report['addon']));
|
||||
$output->writeln('健康插件: ' . count(array_filter($report['addon'], fn($a) => $a['healthy'])));
|
||||
}
|
||||
|
||||
/**
|
||||
* 单插件详细信息(--detail 时补充)
|
||||
*/
|
||||
private function outputSingleDetail(string $addon, array $health, Output $output): void
|
||||
{
|
||||
if (empty($health)) {
|
||||
return;
|
||||
}
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>详细信息:</info>');
|
||||
$output->writeln("版本: " . ($health['version'] ?? '未知'));
|
||||
$output->writeln("作者: " . ($health['author'] ?? '未知'));
|
||||
$output->writeln("描述: " . ($health['description'] ?? '无'));
|
||||
$output->writeln("启用状态: " . ($health['enabled'] ? '已启用' : '已禁用'));
|
||||
$output->writeln("安装时间: " . (isset($health['install_time']) ? date('Y-m-d H:i:s', $health['install_time']) : '未知'));
|
||||
if (isset($health['dependencies'])) {
|
||||
$output->writeln("依赖插件: " . implode(', ', $health['dependencies']));
|
||||
}
|
||||
if (isset($health['extensions'])) {
|
||||
$output->writeln("PHP扩展: " . implode(', ', $health['extensions']));
|
||||
}
|
||||
|
||||
if ($health['enabled']) {
|
||||
$stats = $health['performance'] ?? AddonPerformanceMonitor::getPerformanceStats($addon);
|
||||
if (!empty($stats) && $stats['total_calls'] > 0) {
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>性能状态:</info>');
|
||||
$output->writeln(" 总调用次数: {$stats['total_calls']}");
|
||||
$output->writeln(" 平均执行时间: " . number_format($stats['avg_execution_time'] * 1000, 2) . " ms");
|
||||
$output->writeln(" 成功率: " . number_format($stats['success_rate'], 2) . "%");
|
||||
if ($stats['avg_execution_time'] > AddonPerformanceMonitor::PERFORMANCE_THRESHOLD) {
|
||||
$output->writeln(' <error>⚠ 执行时间过长</error>');
|
||||
}
|
||||
if ($stats['success_rate'] < 90) {
|
||||
$output->writeln(' <error>⚠ 成功率过低</error>');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出 JSON 格式
|
||||
*/
|
||||
private function outputJson(array $report, Output $output): void
|
||||
{
|
||||
echo json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出 HTML 格式
|
||||
*/
|
||||
private function outputHtml(array $report, Output $output): void
|
||||
{
|
||||
$html = '<!DOCTYPE html><html><head><meta charset="utf-8"><title>插件健康检查报告</title>';
|
||||
$html .= '<style>body{font-family:Arial,sans-serif;margin:20px;}';
|
||||
$html .= '.healthy{color:#5fb878;}.error{color:#ff5722;}.warning{color:#ffb800;}';
|
||||
$html .= '.section{margin:20px 0;padding:15px;background:#f9f9f9;border-radius:4px;}';
|
||||
$html .= '.check{margin:10px 0;padding:10px;background:#fff;border-radius:4px;}';
|
||||
$html .= '</style></head><body>';
|
||||
$html .= '<h1>插件健康检查报告</h1>';
|
||||
$html .= '<p>检查时间: ' . date('Y-m-d H:i:s', $report['timestamp']) . '</p>';
|
||||
$html .= '<p>整体状态: <span class="' . ($report['healthy'] ? 'healthy' : 'error') . '">' . ($report['healthy'] ? '健康' : '异常') . '</span></p>';
|
||||
|
||||
foreach ($report['checks'] as $checkName => $checkResult) {
|
||||
$html .= '<div class="section">';
|
||||
$html .= '<h2>' . $this->getCheckName($checkName) . '</h2>';
|
||||
foreach ($checkResult['issues'] ?? [] as $issue) {
|
||||
$html .= '<div class="check error">✗ ' . htmlspecialchars($issue) . '</div>';
|
||||
}
|
||||
foreach ($checkResult['warnings'] ?? [] as $warning) {
|
||||
$html .= '<div class="check warning">⚠ ' . htmlspecialchars($warning) . '</div>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
}
|
||||
$html .= '</body></html>';
|
||||
echo $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检查项中文名
|
||||
*/
|
||||
private function getCheckName(string $checkKey): string
|
||||
{
|
||||
$names = [
|
||||
'system' => '系统环境检查',
|
||||
'cache' => '缓存系统检查',
|
||||
'files' => '插件文件检查',
|
||||
'performance' => '插件性能检查',
|
||||
'dependencies' => '依赖关系检查',
|
||||
'status' => '插件状态检查'
|
||||
];
|
||||
return $names[$checkKey] ?? $checkKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试自动修复所有问题(清缓存 / 修权限)
|
||||
*/
|
||||
private function runAutoFix(array $report, Output $output): void
|
||||
{
|
||||
// 遍历插件问题(缓存测试键已由 checkCacheSystem 自身清理,无需此处重复)
|
||||
foreach ($report['addon'] as $addon => $health) {
|
||||
$this->tryAutoFix($addon, $health['issues'] ?? [], $output);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试自动修复单个插件问题
|
||||
*/
|
||||
private function tryAutoFix(string $addon, array $issues, Output $output): void
|
||||
{
|
||||
$fixedCount = 0;
|
||||
|
||||
foreach ($issues as $issue) {
|
||||
// 缓存类问题
|
||||
if (strpos($issue, '缓存') !== false) {
|
||||
try {
|
||||
Cache::delete('addon_config_' . $addon);
|
||||
Cache::delete('addon_config_version_' . $addon);
|
||||
$output->writeln("<info> ✓ 已清除插件 {$addon} 缓存</info>");
|
||||
$fixedCount++;
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln("<comment> ⚠ 清除缓存失败: {$e->getMessage()}</comment>");
|
||||
}
|
||||
}
|
||||
|
||||
// 权限类问题(含 runtime 不可写)
|
||||
if (strpos($issue, '权限') !== false || strpos($issue, 'runtime') !== false) {
|
||||
try {
|
||||
if (strpos($issue, 'runtime') !== false) {
|
||||
$this->fixPermissions(root_path() . 'runtime');
|
||||
}
|
||||
$this->fixPermissions(ADDON_PATH . $addon);
|
||||
$output->writeln("<info> ✓ 已修复文件权限 ({$addon})</info>");
|
||||
$fixedCount++;
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln("<comment> ⚠ 修复权限失败: {$e->getMessage()}</comment>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($fixedCount > 0) {
|
||||
$output->writeln("<info>已自动修复 {$fixedCount} 个问题({$addon})</info>");
|
||||
} else {
|
||||
$output->writeln("<comment>插件 {$addon} 无可通过 --fix 自动修复的问题</comment>");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复文件权限(递归 chmod)
|
||||
*/
|
||||
private function fixPermissions(string $path): void
|
||||
{
|
||||
if (!is_dir($path)) {
|
||||
return;
|
||||
}
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($path),
|
||||
\RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
foreach ($iterator as $item) {
|
||||
chmod($item->getPathname(), $item->isDir() ? 0755 : 0644);
|
||||
}
|
||||
chmod($path, 0755);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送告警
|
||||
*/
|
||||
private function sendAlert(array $report, ?string $webhookUrl, ?string $email, Output $output): void
|
||||
{
|
||||
$message = "插件系统健康检查发现异常!\n\n";
|
||||
$message .= "检查时间: " . date('Y-m-d H:i:s', $report['timestamp']) . "\n";
|
||||
$message .= "总问题数: " . count($report['issues']) . "\n";
|
||||
$message .= "总警告数: " . count($report['warnings']) . "\n\n";
|
||||
|
||||
if (!empty($report['issues'])) {
|
||||
$message .= "问题列表:\n";
|
||||
foreach ($report['issues'] as $issue) {
|
||||
$message .= "- {$issue}\n";
|
||||
}
|
||||
$message .= "\n";
|
||||
}
|
||||
|
||||
Log::error('插件健康检查异常', $report);
|
||||
|
||||
if ($webhookUrl) {
|
||||
try {
|
||||
$this->sendWebhook($webhookUrl, $report);
|
||||
$output->writeln('<info>已发送Webhook告警</info>');
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>Webhook发送失败: ' . $e->getMessage() . '</error>');
|
||||
}
|
||||
}
|
||||
|
||||
if ($email) {
|
||||
try {
|
||||
$this->sendEmail($email, '插件系统健康检查告警', $message);
|
||||
$output->writeln('<info>已发送邮件告警</info>');
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>邮件发送失败: ' . $e->getMessage() . '</error>');
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln('<error>告警消息: ' . str_replace("\n", " ", $message) . '</error>');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 Webhook
|
||||
*/
|
||||
private function sendWebhook(string $url, array $data): void
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
throw new \Exception("Webhook返回错误: HTTP {$httpCode}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮件
|
||||
*/
|
||||
private function sendEmail(string $to, string $subject, string $message): void
|
||||
{
|
||||
$headers = "From: noreply@yourdomain.com\r\n";
|
||||
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
||||
|
||||
if (!mail($to, $subject, $message, $headers)) {
|
||||
throw new \Exception("邮件发送失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存检查报告(runtime/addon_health_reports,保留 30 天)
|
||||
*/
|
||||
private function saveReport(array $report, Output $output): void
|
||||
{
|
||||
$reportDir = root_path() . 'runtime' . DIRECTORY_SEPARATOR . 'addon_health_reports';
|
||||
if (!is_dir($reportDir)) {
|
||||
mkdir($reportDir, 0755, true);
|
||||
}
|
||||
|
||||
$reportFile = $reportDir . DIRECTORY_SEPARATOR . 'health_report_' . date('YmdHis') . '.json';
|
||||
file_put_contents($reportFile, json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$output->writeln("<info>检查报告已保存: {$reportFile}</info>");
|
||||
$this->cleanupOldReports($reportDir, 30);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理旧报告
|
||||
*/
|
||||
private function cleanupOldReports(string $dir, int $days): void
|
||||
{
|
||||
$files = glob($dir . DIRECTORY_SEPARATOR . 'health_report_*.json');
|
||||
$cutoffTime = time() - ($days * 86400);
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (filemtime($file) < $cutoffTime) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化字节
|
||||
*/
|
||||
private function formatBytes(int $bytes): string
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
$bytes /= pow(1024, $pow);
|
||||
return round($bytes, 2) . ' ' . $units[$pow];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查插件是否已安装
|
||||
*/
|
||||
private function isAddonInstalled(string $addon): bool
|
||||
{
|
||||
return is_dir(ADDON_PATH . $addon) && is_file(ADDON_PATH . $addon . DIRECTORY_SEPARATOR . 'info.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查插件是否已启用
|
||||
*/
|
||||
private function isAddonEnabled(string $addon): bool
|
||||
{
|
||||
$infoFile = ADDON_PATH . $addon . DIRECTORY_SEPARATOR . 'info.php';
|
||||
if (!is_file($infoFile)) {
|
||||
return false;
|
||||
}
|
||||
$info = include $infoFile;
|
||||
return isset($info['state']) && $info['state'] == 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace ywxapp\command;
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use ywxapp\service\AddonService;
|
||||
|
||||
/**
|
||||
* 运行期授权巡检命令(Discuz 式:已启用付费插件定期复查授权,过期自动禁用)。
|
||||
* 用法:php think addon:license-check
|
||||
*/
|
||||
class AddonLicenseCheck extends Command
|
||||
{
|
||||
protected $name = 'addon:license-check';
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName($this->name)
|
||||
->setDescription('巡检已启用付费插件的授权状态,过期自动禁用');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$disabled = AddonService::checkLicenses();
|
||||
if (empty($disabled)) {
|
||||
$output->info('授权巡检完成:未发现需要禁用的插件。');
|
||||
return 0;
|
||||
}
|
||||
$output->warning('以下插件授权未通过,已自动禁用:');
|
||||
foreach ($disabled as $name) {
|
||||
$output->writeln(' - ' . $name);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace ywxapp\command;
|
||||
|
||||
use think\console\command\Make;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
/**
|
||||
* Addon 统一脚手架 / 生成器命令
|
||||
*
|
||||
* 合并原 addon:build / addon:clear / addon:controller / addon:model /
|
||||
* addon:middleware / addon:validate / addon:event / addon:listener /
|
||||
* addon:service / addon:subscribe 十条命令为单入口:
|
||||
* php think addon:make <插件名> -a <action> [--name=<类>] [动作选项]
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class AddonMake extends Make
|
||||
{
|
||||
/**
|
||||
* 当前生成动作对应的类类型(首字母大写),供 getStub/buildClass 使用
|
||||
* @var string
|
||||
*/
|
||||
protected $currentType = '';
|
||||
|
||||
/**
|
||||
* 当前生成动作对应的 stub 关键字
|
||||
* @var string
|
||||
*/
|
||||
protected $currentStub = '';
|
||||
|
||||
/**
|
||||
* 应用基础目录
|
||||
* @var string
|
||||
*/
|
||||
protected $basePath;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('addon:make')
|
||||
->addArgument('addon', Argument::OPTIONAL, 'addon name, e.g. demo (required for build & generators)')
|
||||
->addOption('action', 'a', Option::VALUE_REQUIRED, 'build|clear|controller|model|middleware|validate|event|listener|service|subscribe')
|
||||
->addOption('name', 'n', Option::VALUE_OPTIONAL, 'class name for generator actions, e.g. User')
|
||||
->addOption('api', null, Option::VALUE_NONE, 'controller: generate an api controller class.')
|
||||
->addOption('plain', null, Option::VALUE_NONE, 'controller: generate an empty controller class.')
|
||||
->addOption('cache', 'c', Option::VALUE_NONE, 'clear: clear cache file')
|
||||
->addOption('log', 'l', Option::VALUE_NONE, 'clear: clear log file')
|
||||
->addOption('dir', 'r', Option::VALUE_NONE, 'clear: clear empty dir')
|
||||
->setDescription('Unified addon scaffold & class generator (build / clear / controller / model / middleware / validate / event / listener / service / subscribe)');
|
||||
}
|
||||
|
||||
|
||||
protected function execute(Input $input, Output $output): int
|
||||
{
|
||||
$addon = trim((string) $input->getArgument('addon'));
|
||||
$action = strtolower(trim((string) $input->getOption('action')));
|
||||
|
||||
if ($action === '') {
|
||||
$output->writeln('<error>Please specify an action via -a/--action, e.g. -a build, -a controller.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
// clear 允许插件名为空(清理根 runtime)
|
||||
if ($action === 'clear') {
|
||||
return $this->doClear($addon, $input, $output);
|
||||
}
|
||||
|
||||
// build 与生成器均要求合法的插件名
|
||||
if ($addon === '' || ! preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $addon)) {
|
||||
$output->writeln('<error>Please provide a valid addon name, e.g. php think addon demo -a ' . $action . '</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'build':
|
||||
return $this->doBuild($addon, $output);
|
||||
case 'controller':
|
||||
case 'model':
|
||||
case 'middleware':
|
||||
case 'validate':
|
||||
case 'event':
|
||||
case 'listener':
|
||||
case 'service':
|
||||
case 'subscribe':
|
||||
return $this->doGenerate($action, $addon, $input, $output);
|
||||
default:
|
||||
$output->writeln('<error>Unknown action: ' . $action . '</error>');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------- 生成器动作 ----------------------------- */
|
||||
|
||||
/**
|
||||
* 生成各类插件类文件(controller/model/middleware/validate/event/listener/service/subscribe)
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function doGenerate(string $action, string $addon, Input $input, Output $output): int
|
||||
{
|
||||
$this->currentType = ucfirst($action);
|
||||
$this->currentStub = $action;
|
||||
|
||||
$class = trim((string) ($input->getOption('name') ?: ''));
|
||||
if ($class === '' || ! preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $class)) {
|
||||
$output->writeln('<error>Please provide a valid class name via --name, e.g. --name=User</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$name = 'addon' . '\\' . $addon . '\\' . $action . '\\' . $class;
|
||||
$pathname = $this->app->getRootPath() . ltrim(str_replace('\\', '/', $name), '/') . '.php';
|
||||
|
||||
if (is_file($pathname)) {
|
||||
$output->writeln('<error>' . $this->currentType . ':' . $name . ' already exists!</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (! is_dir(dirname($pathname))) {
|
||||
mkdir(dirname($pathname), 0755, true);
|
||||
}
|
||||
|
||||
file_put_contents($pathname, $this->buildClass($name));
|
||||
|
||||
$output->writeln('<info>' . $this->currentType . ':' . $name . ' created successfully.</info>');
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 stub 构建类内容(覆盖 Make::buildClass 以使用插件命名空间)
|
||||
*
|
||||
* @param string $name 完整类名 addon\<addon>\<type>\<Class>
|
||||
* @return string
|
||||
*/
|
||||
protected function buildClass(string $name): string
|
||||
{
|
||||
$stub = file_get_contents($this->getStub());
|
||||
|
||||
$namespace = trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\');
|
||||
$class = str_replace($namespace . '\\', '', $name);
|
||||
|
||||
return str_replace(
|
||||
['{%className%}', '{%actionSuffix%}', '{%namespace%}', '{%app_namespace%}'],
|
||||
[
|
||||
$class,
|
||||
$this->app->config->get('route.action_suffix'),
|
||||
$namespace,
|
||||
$this->app->getNamespace(),
|
||||
],
|
||||
$stub
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前动作与 --api/--plain 选项返回 stub 路径
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getStub(): string
|
||||
{
|
||||
$stubPath = __DIR__ . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR;
|
||||
|
||||
if ($this->currentStub === 'controller') {
|
||||
if ($this->input->getOption('api')) {
|
||||
return $stubPath . 'controller.api.stub';
|
||||
}
|
||||
if ($this->input->getOption('plain')) {
|
||||
return $stubPath . 'controller.plain.stub';
|
||||
}
|
||||
return $stubPath . 'controller.stub';
|
||||
}
|
||||
|
||||
return $stubPath . $this->currentStub . '.stub';
|
||||
}
|
||||
|
||||
/* ------------------------------- build -------------------------------- */
|
||||
|
||||
/**
|
||||
* 脚手架生成插件骨架
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function doBuild(string $app, Output $output): int
|
||||
{
|
||||
$this->basePath = $this->app->getRootPath() . 'addon' . DIRECTORY_SEPARATOR;
|
||||
|
||||
if (is_dir($this->basePath . $app)) {
|
||||
$output->writeln("<comment>Addon '{$app}' already exists, ensuring skeleton only.</comment>");
|
||||
}
|
||||
|
||||
$this->buildAddon($app, []);
|
||||
$output->writeln("<info>Addon '{$app}' scaffold ready at addon/{$app}/</info>");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建应用
|
||||
*
|
||||
* @param string $app 应用名
|
||||
* @param array $list 目录结构
|
||||
* @return void
|
||||
*/
|
||||
protected function buildAddon(string $app, array $list = []): void
|
||||
{
|
||||
$appPath = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '');
|
||||
$namespace = 'addon' . ($app ? '\\' . $app : '');
|
||||
if (! is_dir($appPath)) {
|
||||
mkdir($appPath);
|
||||
}
|
||||
$this->buildAddonFile($app, $namespace);
|
||||
$this->buildInfoFile($app);
|
||||
$this->buildCommonFile($app);
|
||||
$this->buildConfigFile($app);
|
||||
if (! is_dir($appPath . 'controller')) {
|
||||
mkdir($appPath . 'controller');
|
||||
mkdir($appPath . 'controller/backend');
|
||||
mkdir($appPath . 'controller/member');
|
||||
}
|
||||
$this->buildIndexController($app, $namespace);
|
||||
if (! is_dir($appPath . 'lang')) {
|
||||
mkdir($appPath . 'lang');
|
||||
}
|
||||
if (! is_dir($appPath . 'model')) {
|
||||
mkdir($appPath . 'model');
|
||||
}
|
||||
if (! is_dir($appPath . 'view')) {
|
||||
mkdir($appPath . 'view');
|
||||
mkdir($appPath . 'view/frontend');
|
||||
mkdir($appPath . 'view/backend');
|
||||
mkdir($appPath . 'view/member');
|
||||
}
|
||||
if (! is_dir($appPath . 'route')) {
|
||||
mkdir($appPath . 'route');
|
||||
}
|
||||
$this->buildRouteFile($app, $namespace);
|
||||
$this->buildMenuJson($app);
|
||||
$appPublicPath = public_path() . 'static' . DIRECTORY_SEPARATOR . ($app ? $app . DIRECTORY_SEPARATOR : '');
|
||||
if (! is_dir($appPublicPath)) {
|
||||
mkdir($appPublicPath, 0755, true);
|
||||
mkdir($appPublicPath . 'img', 0755, true);
|
||||
mkdir($appPublicPath . 'css', 0755, true);
|
||||
mkdir($appPublicPath . 'js', 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建插件主类文件
|
||||
*
|
||||
* @param string $app 应用名
|
||||
* @param string $namespace 类库命名空间
|
||||
* @return void
|
||||
*/
|
||||
protected function buildAddonFile(string $app, string $namespace): void
|
||||
{
|
||||
$appPath = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '');
|
||||
if (! is_file($appPath . 'Addon.php')) {
|
||||
$content = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . 'addon.stub');
|
||||
$content = str_replace(['{%namespace%}', '{%addon%}', '{%className%}'], [$namespace, $app, 'Addon'], $content);
|
||||
file_put_contents($appPath . 'Addon.php', $content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建插件信息文件
|
||||
*
|
||||
* @param string $app 目录
|
||||
* @return void
|
||||
*/
|
||||
protected function buildInfoFile(string $app): void
|
||||
{
|
||||
$filename = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '') . 'info.php';
|
||||
if (! is_file($filename)) {
|
||||
$content = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . 'info.stub');
|
||||
$content = str_replace(['{%app%}'], [$app], $content);
|
||||
$this->checkDirBuild(dirname($filename));
|
||||
file_put_contents($filename, $content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建菜单 JSON
|
||||
*
|
||||
* @param string $app 应用名
|
||||
* @return void
|
||||
*/
|
||||
protected function buildMenuJson(string $app): void
|
||||
{
|
||||
$filename = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '') . 'menu.json';
|
||||
if (! is_file($filename)) {
|
||||
$menu = ["frontend" => [], "member" => [], "backend" => []];
|
||||
$content = json_encode($menu, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
file_put_contents($filename, $content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建公共文件
|
||||
*
|
||||
* @param string $app 目录
|
||||
* @return void
|
||||
*/
|
||||
public function buildCommonFile(string $app): void
|
||||
{
|
||||
$appPath = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '');
|
||||
if (! is_file($appPath . 'common.php')) {
|
||||
file_put_contents($appPath . 'common.php', "<?php" . PHP_EOL . "// 这是系统自动生成的公共文件" . PHP_EOL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建插件配置文件(后台「配置」表单数据源)
|
||||
*
|
||||
* @param string $app 应用名
|
||||
* @return void
|
||||
*/
|
||||
protected function buildConfigFile(string $app): void
|
||||
{
|
||||
$filename = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '') . 'config.php';
|
||||
if (! is_file($filename)) {
|
||||
file_put_contents($filename, "<?php" . PHP_EOL
|
||||
. "// 插件配置项(后台「配置」表单数据源)" . PHP_EOL
|
||||
. "return [];" . PHP_EOL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建首页控制器
|
||||
*
|
||||
* @param string $app 应用名
|
||||
* @param string $namespace 类库命名空间
|
||||
* @return void
|
||||
*/
|
||||
public function buildIndexController(string $app, string $namespace): void
|
||||
{
|
||||
$suffix = $this->app->config->get('route.controller_suffix') ? 'Controller' : '';
|
||||
$filename = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '') . 'controller' . DIRECTORY_SEPARATOR . 'Index' . $suffix . '.php';
|
||||
|
||||
if (! is_file($filename)) {
|
||||
$content = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . 'controller.stub');
|
||||
$content = str_replace(['{%className%}', '{%actionSuffix%}', '{%namespace%}', '{%app_namespace%}'], ['Index', $suffix, $namespace . '\\controller', $namespace], $content);
|
||||
$this->checkDirBuild(dirname($filename));
|
||||
file_put_contents($filename, $content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建路由文件
|
||||
*
|
||||
* @param string $app 应用名
|
||||
* @param string $namespace 类库命名空间
|
||||
* @return void
|
||||
*/
|
||||
public function buildRouteFile(string $app, string $namespace): void
|
||||
{
|
||||
$filename = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '') . 'route' . DIRECTORY_SEPARATOR . 'app.php';
|
||||
if (! is_file($filename)) {
|
||||
$this->checkDirBuild(dirname($filename));
|
||||
file_put_contents($filename, "<?php" . PHP_EOL . "use think\\facade\\Route;" . PHP_EOL . PHP_EOL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建目录
|
||||
*
|
||||
* @param string $dirname 目录名称
|
||||
* @return void
|
||||
*/
|
||||
public function checkDirBuild(string $dirname): void
|
||||
{
|
||||
if (! is_dir($dirname)) {
|
||||
mkdir($dirname, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------- clear -------------------------------- */
|
||||
|
||||
/**
|
||||
* 清理 runtime 文件
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function doClear(string $app, Input $input, Output $output): int
|
||||
{
|
||||
$runtimePath = $this->app->getRootPath() . 'runtime' . DIRECTORY_SEPARATOR . ($app ? $app . DIRECTORY_SEPARATOR : '');
|
||||
|
||||
if ($input->getOption('cache')) {
|
||||
$path = $runtimePath . 'cache';
|
||||
} elseif ($input->getOption('log')) {
|
||||
$path = $runtimePath . 'log';
|
||||
} else {
|
||||
$path = $runtimePath;
|
||||
}
|
||||
|
||||
$rmdir = $input->getOption('dir') ? true : false;
|
||||
$this->clear(rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, $rmdir);
|
||||
|
||||
$output->writeln("<info>Clear Successed</info>");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归清理目录
|
||||
*
|
||||
* @param string $path 目录
|
||||
* @param bool $rmdir 是否删除空目录
|
||||
* @return void
|
||||
*/
|
||||
protected function clear(string $path, bool $rmdir): void
|
||||
{
|
||||
$files = is_dir($path) ? scandir($path) : [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ('.' != $file && '..' != $file && is_dir($path . $file)) {
|
||||
array_map('unlink', glob($path . $file . DIRECTORY_SEPARATOR . '*.*'));
|
||||
if ($rmdir) {
|
||||
rmdir($path . $file);
|
||||
}
|
||||
} elseif ('.gitignore' != $file && is_file($path . $file)) {
|
||||
unlink($path . $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace ywxapp\command;
|
||||
|
||||
use Exception;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\File;
|
||||
use ywxapp\exception\AddonException;
|
||||
use ywxapp\service\AddonService;
|
||||
use ywxapp\service\AddonHotReload;
|
||||
|
||||
/**
|
||||
* 插件统一管理命令(合并 develop / install / reload)
|
||||
*
|
||||
* 用法:
|
||||
* php think addon:manage <插件名> -a develop
|
||||
* php think addon:manage <插件名> -a install [--local=<zip>] [--force]
|
||||
* php think addon:manage [<插件名>] -a reload [--force] [--status]
|
||||
*
|
||||
* 说明:
|
||||
* - develop / install / reload 逻辑内联于此;
|
||||
* - 菜单重载 / 数据库自愈 / 配置补全已由 addon:repair 统一负责(含 --menu 单项),
|
||||
* 故本命令不再保留 refresh-menu;
|
||||
* - health 逻辑体量大且复用 AddonHealth,故委托 ywxapp\command\AddonHealth 执行(长选项形式传参,规避短选项冲突)。
|
||||
*/
|
||||
class AddonManage extends Command
|
||||
{
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('addon:manage')
|
||||
->addArgument('addon', Argument::OPTIONAL, '插件名(部分动作需要;留空语义见各动作)')
|
||||
->addOption('action', 'a', Option::VALUE_REQUIRED, '动作: develop|install|reload')
|
||||
// install
|
||||
->addOption('local', 'l', Option::VALUE_REQUIRED, '离线 zip 路径(install 离线安装)')
|
||||
->addOption('force', 'f', Option::VALUE_NONE, '强制(install 在线强制覆盖 / reload 强制重载)')
|
||||
// reload
|
||||
->addOption('status', 's', Option::VALUE_NONE, '查看重载状态(reload)')
|
||||
->setDescription('插件统一管理:开发安装 / 安装 / 重导菜单 / 热重载');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令:按 -a 分发
|
||||
*/
|
||||
protected function execute(Input $input, Output $output): int
|
||||
{
|
||||
$action = strtolower(trim((string) $input->getOption('action')));
|
||||
if ($action === '') {
|
||||
$output->writeln('<error>请通过 -a/--action 指定动作:develop|install|reload</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'develop':
|
||||
return $this->doDevelop($input, $output);
|
||||
case 'install':
|
||||
return $this->doInstall($input, $output);
|
||||
case 'reload':
|
||||
return $this->doReload($input, $output);
|
||||
default:
|
||||
$output->writeln('<error>未知动作: ' . $action . '(支持 develop|install|reload;菜单重载请用 addon:repair --menu)</error>');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开发者模式安装(源码已在 addon/<name>,原地建表/注入菜单/启用,免打包)
|
||||
*/
|
||||
private function doDevelop(Input $input, Output $output): int
|
||||
{
|
||||
$name = trim((string) $input->getArgument('addon'));
|
||||
if ($name === '') {
|
||||
$output->writeln('<error>请指定插件名,例如:php think addon:manage demo -a develop</error>');
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
$info = AddonService::instance($name)->developInstall();
|
||||
$version = $info['version'] ?? '';
|
||||
$output->writeln("<info>开发模式安装成功:{$name} v{$version}</info>");
|
||||
$output->writeln('<info>已建表、注入菜单并启用,可直接测试。</info>');
|
||||
} catch (AddonException $e) {
|
||||
$output->writeln('<error>' . $e->getMessage() . '</error>');
|
||||
return 1;
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>开发模式安装失败:' . $e->getMessage() . '</error>');
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装插件:在线下载安装(默认)或离线 zip 安装
|
||||
*/
|
||||
private function doInstall(Input $input, Output $output): int
|
||||
{
|
||||
$name = trim((string) $input->getArgument('addon'));
|
||||
if ($name === '' || !preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $name)) {
|
||||
$output->writeln('<error>请提供合法的插件名,例如:php think addon:manage demo -a install</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$extend = [
|
||||
'install_user' => 0,
|
||||
'install_ip' => 'cli',
|
||||
'install_time' => time(),
|
||||
];
|
||||
|
||||
try {
|
||||
$localOpt = (string) $input->getOption('local');
|
||||
if ($localOpt !== '') {
|
||||
$local = realpath($localOpt);
|
||||
if ($local === false || !is_file($local)) {
|
||||
$output->writeln("<error>Local zip not found: {$localOpt}</error>");
|
||||
return 1;
|
||||
}
|
||||
$output->writeln("<comment>Offline install from '{$local}' (addon='{$name}')...</comment>");
|
||||
$file = new File($local);
|
||||
AddonService::instance($name)->local($file, $extend);
|
||||
$output->writeln("<info>Addon '{$name}' offline-installed successfully.</info>");
|
||||
return 0;
|
||||
}
|
||||
|
||||
$force = (bool) $input->getOption('force');
|
||||
$output->writeln("<comment>Online install '{$name}' (force=" . ($force ? 'yes' : 'no') . ")...</comment>");
|
||||
AddonService::instance($name)->install($force, $extend);
|
||||
$output->writeln("<info>Addon '{$name}' online-installed successfully.</info>");
|
||||
return 0;
|
||||
} catch (AddonException $e) {
|
||||
$output->writeln('<error>Install failed: ' . $e->getMessage() . '</error>');
|
||||
return 1;
|
||||
} catch (Exception $e) {
|
||||
$output->writeln('<error>Install failed: ' . $e->getMessage() . '</error>');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件热重载(需 app_debug)
|
||||
*/
|
||||
private function doReload(Input $input, Output $output): int
|
||||
{
|
||||
if (!config('app.app_debug')) {
|
||||
$output->error('热重载功能仅在开发环境可用');
|
||||
return 1;
|
||||
}
|
||||
$addon = trim((string) $input->getArgument('addon'));
|
||||
$force = (bool) $input->getOption('force');
|
||||
$showStatus = (bool) $input->getOption('status');
|
||||
|
||||
if ($showStatus) {
|
||||
return $this->reloadStatus($addon, $output);
|
||||
}
|
||||
if ($addon) {
|
||||
return $this->reloadSingle($addon, $force, $output);
|
||||
}
|
||||
return $this->reloadAll($output);
|
||||
}
|
||||
|
||||
private function reloadSingle(string $addon, bool $force, Output $output): int
|
||||
{
|
||||
$output->writeln("<info>正在重载插件: {$addon}</info>");
|
||||
try {
|
||||
$reloaded = AddonHotReload::reloadAddon($addon, $force);
|
||||
if ($reloaded) {
|
||||
$output->writeln("<info>插件 {$addon} 重载成功</info>");
|
||||
$status = AddonHotReload::getReloadStatus($addon);
|
||||
if (isset($status['files_changed'])) {
|
||||
$output->writeln('变更文件: ' . implode(', ', $status['files_changed']));
|
||||
}
|
||||
} else {
|
||||
$output->writeln("<comment>插件 {$addon} 无文件变更,无需重载</comment>");
|
||||
}
|
||||
return 0;
|
||||
} catch (\Exception $e) {
|
||||
$output->error('重载失败: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private function reloadAll(Output $output): int
|
||||
{
|
||||
$output->writeln('<info>正在重载所有已启用插件...</info>');
|
||||
try {
|
||||
$results = AddonHotReload::reloadAlladdon();
|
||||
$successCount = 0;
|
||||
$reloadedCount = 0;
|
||||
$failedCount = 0;
|
||||
foreach ($results as $addon => $result) {
|
||||
if ($result['success']) {
|
||||
$successCount++;
|
||||
if ($result['reloaded']) {
|
||||
$reloadedCount++;
|
||||
$output->writeln("<info>✓ {$addon} - 已重载</info>");
|
||||
} else {
|
||||
$output->writeln("<comment>- {$addon} - 无变更</comment>");
|
||||
}
|
||||
} else {
|
||||
$failedCount++;
|
||||
$output->writeln("<error>✗ {$addon} - {$result['error']}</error>");
|
||||
}
|
||||
}
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>重载完成:</info>');
|
||||
$output->writeln(" 成功: {$successCount}");
|
||||
$output->writeln(" 实际重载: {$reloadedCount}");
|
||||
$output->writeln(" 失败: {$failedCount}");
|
||||
return $failedCount > 0 ? 1 : 0;
|
||||
} catch (\Exception $e) {
|
||||
$output->error('重载失败: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private function reloadStatus(string $addon, Output $output): int
|
||||
{
|
||||
if ($addon) {
|
||||
$status = AddonHotReload::getReloadStatus($addon);
|
||||
$output->writeln("<info>插件 {$addon} 重载状态:</info>");
|
||||
$output->writeln(' 状态: ' . ($status['status'] ?? 'unknown'));
|
||||
if (isset($status['reload_time'])) {
|
||||
$output->writeln(' 最后重载时间: ' . date('Y-m-d H:i:s', $status['reload_time']));
|
||||
}
|
||||
if (isset($status['error'])) {
|
||||
$output->writeln(' 错误信息: ' . $status['error']);
|
||||
}
|
||||
if (isset($status['files_changed'])) {
|
||||
$output->writeln(' 变更文件: ' . implode(', ', $status['files_changed']));
|
||||
}
|
||||
} else {
|
||||
$stats = AddonHotReload::getReloadStatistics();
|
||||
$output->writeln('<info>插件热重载统计:</info>');
|
||||
$output->writeln(' 总重载次数: ' . ($stats['total_reloads'] ?? 0));
|
||||
$output->writeln(' 成功次数: ' . ($stats['successful_reloads'] ?? 0));
|
||||
$output->writeln(' 失败次数: ' . ($stats['failed_reloads'] ?? 0));
|
||||
if (!empty($stats['addon'])) {
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>各插件重载状态:</info>');
|
||||
foreach ($stats['addon'] as $name => $status) {
|
||||
$icon = ($status['status'] ?? '') === 'success' ? '✓' : '✗';
|
||||
$color = ($status['status'] ?? '') === 'success' ? 'info' : 'error';
|
||||
$output->writeln("<{$color}>{$icon} {$name}</{$color}>");
|
||||
if (isset($status['reload_time'])) {
|
||||
$output->writeln(' 最后重载: ' . date('Y-m-d H:i:s', $status['reload_time']));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace ywxapp\command;
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
use think\facade\Config;
|
||||
use think\facade\Log;
|
||||
use ywxapp\service\AddonService;
|
||||
use ywxapp\model\BaseModel;
|
||||
|
||||
/**
|
||||
* 插件一键修复命令("插件坏什么修什么")
|
||||
*
|
||||
* 整合以下自愈能力,使一个出问题的插件恢复到「菜单正确 + 表结构完整 + 配置齐全」的状态:
|
||||
* 1. 重载菜单:refreshMenu()(清旧 admin_power/user_rule + 重建,顺带修复菜单路由前缀)
|
||||
* 2. 数据库自愈:解析 install.sql 中该插件全部 CREATE TABLE,缺表建表、缺列补列
|
||||
* 3. 配置行补全:确保 addon_config 表存在该插件配置行(无则按默认 info 写入)
|
||||
*
|
||||
* 用法:
|
||||
* php think addon:repair <插件名> # 修复指定插件(菜单 + 表 + 配置)
|
||||
* php think addon:repair <插件名> --menu # 仅重载菜单
|
||||
* php think addon:repair <插件名> --db # 仅自愈数据库表/列
|
||||
* php think addon:repair <插件名> --config # 仅补全配置行
|
||||
* php think addon:repair all # 逐个修复全部已安装插件
|
||||
*
|
||||
* php think addon:repair appmall --menu
|
||||
*/
|
||||
class AddonRepair extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('addon:repair')
|
||||
->addArgument('addon', Argument::REQUIRED, '插件名(或 all 修复全部)')
|
||||
->addOption('menu', 'm', Option::VALUE_NONE, '仅重载菜单')
|
||||
->addOption('db', 'b', Option::VALUE_NONE, '仅自愈数据库表/列')
|
||||
->addOption('config', 'c', Option::VALUE_NONE, '仅补全配置行')
|
||||
->addOption('force', 'f', Option::VALUE_NONE, '执行破坏性修复(如重命名无前缀孤儿表)')
|
||||
->setDescription('插件一键修复:重载菜单 / 自愈表结构 / 补全配置');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output): int
|
||||
{
|
||||
$name = strtolower(trim((string) $input->getArgument('addon')));
|
||||
|
||||
// 指定了单一 --xxx 则只跑对应步骤;否则全跑
|
||||
$onlyMenu = (bool) $input->getOption('menu');
|
||||
$onlyDb = (bool) $input->getOption('db');
|
||||
$onlyConfig = (bool) $input->getOption('config');
|
||||
$all = !$onlyMenu && !$onlyDb && !$onlyConfig;
|
||||
|
||||
if ($name === 'all') {
|
||||
$names = $this->installedaddon();
|
||||
if (empty($names)) {
|
||||
$output->writeln('<comment>没有已安装的插件。</comment>');
|
||||
return 0;
|
||||
}
|
||||
$output->writeln('<info>开始逐个修复 ' . count($names) . ' 个插件...</info>');
|
||||
$failed = [];
|
||||
foreach ($names as $n) {
|
||||
$output->writeln("\n========== 修复插件: {$n} ==========");
|
||||
$code = $this->repairOne($n, $all, $onlyMenu, $onlyDb, $onlyConfig, $output);
|
||||
if ($code !== 0) {
|
||||
$failed[] = $n;
|
||||
}
|
||||
}
|
||||
$output->writeln("\n=== 修复汇总 ===");
|
||||
$output->writeln('总数: ' . count($names) . ' 失败: ' . count($failed));
|
||||
if ($failed) {
|
||||
$output->writeln('<error>失败: ' . implode(', ', $failed) . '</error>');
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $this->repairOne($name, $all, $onlyMenu, $onlyDb, $onlyConfig, $output);
|
||||
}
|
||||
|
||||
private function repairOne(string $name, bool $all, bool $onlyMenu, bool $onlyDb, bool $onlyConfig, Output $output): int
|
||||
{
|
||||
try {
|
||||
$svc = AddonService::instance($name);
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeln('<error>初始化插件失败: ' . $e->getMessage() . '</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$ok = true;
|
||||
|
||||
if ($all || $onlyMenu) {
|
||||
try {
|
||||
$svc->refreshMenu();
|
||||
$output->writeln('<info>[菜单] 已重载(admin_power/user_rule 已重建)</info>');
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeln('<error>[菜单] 重载失败: ' . $e->getMessage() . '</error>');
|
||||
$ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($all || $onlyDb) {
|
||||
$ok = $this->repairDb($svc, $name, $output) && $ok;
|
||||
}
|
||||
|
||||
if ($all || $onlyConfig) {
|
||||
$ok = $this->repairConfig($svc, $name, $output) && $ok;
|
||||
}
|
||||
|
||||
if ($ok) {
|
||||
$output->writeln("<info>[完成] 插件 {$name} 修复成功</info>");
|
||||
} else {
|
||||
$output->writeln("<error>[完成] 插件 {$name} 修复存在失败项</error>");
|
||||
}
|
||||
return $ok ? 0 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库自愈:解析 install.sql 的 CREATE TABLE,缺表建表,并尽量补列。
|
||||
*/
|
||||
private function repairDb(AddonService $svc, string $name, Output $output): bool
|
||||
{
|
||||
$addonDir = $this->addonDir($name);
|
||||
$installSql = $addonDir . 'install.sql';
|
||||
if (!is_file($installSql)) {
|
||||
$output->writeln('<comment>[数据库] 无 install.sql,跳过</comment>');
|
||||
return true;
|
||||
}
|
||||
|
||||
$content = file_get_contents($installSql);
|
||||
if ($content === false || $content === '') {
|
||||
$output->writeln('<comment>[数据库] install.sql 为空,跳过</comment>');
|
||||
return true;
|
||||
}
|
||||
|
||||
// 必须从 Config 取前缀(与 AddonService::importsql 一致),CLI 下 Db::getConfig 可能为空
|
||||
$default = Config::get('database.default');
|
||||
$prefix = Config::get("database.connections.{$default}.prefix");
|
||||
if ($prefix === '' || $prefix === null) {
|
||||
// 兜底:框架默认前缀,避免建出无前缀的孤儿表
|
||||
$prefix = 'wxapp_';
|
||||
$output->writeln('<comment>[数据库] 未检测到库前缀,按默认 wxapp_ 处理</comment>');
|
||||
}
|
||||
// 还原框架在 importsql 时做的 __PREFIX__ 替换,得到运行时完整建表语句
|
||||
$content = str_ireplace('__PREFIX__', $prefix, $content);
|
||||
// 去掉注释行,避免误执行
|
||||
$content = preg_replace('/--.*$/m', '', $content);
|
||||
|
||||
// 按语句拆分(以分号结尾)
|
||||
$statements = array_filter(array_map('trim', explode(';', $content)));
|
||||
|
||||
$created = 0;
|
||||
$exists = 0;
|
||||
$failed = 0;
|
||||
foreach ($statements as $stmt) {
|
||||
if (!preg_match('/^\s*CREATE\s+TABLE/i', $stmt)) {
|
||||
continue;
|
||||
}
|
||||
// 提取表名(已带前缀)
|
||||
if (!preg_match('/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"]?([a-zA-Z0-9_]+)[`"]?/i', $stmt, $m)) {
|
||||
continue;
|
||||
}
|
||||
$table = $m[1];
|
||||
try {
|
||||
if (BaseModel::tableExists($table)) {
|
||||
$exists++;
|
||||
// 表存在也尝试补列(基于 install.sql 的后续 ALTER/列定义无法可靠解析,
|
||||
// 这里只保证表存在;列级自愈由业务层 ensureColumn 负责)
|
||||
continue;
|
||||
}
|
||||
Db::execute($stmt);
|
||||
$created++;
|
||||
$output->writeln("<info>[数据库] 已建表: {$table}</info>");
|
||||
} catch (\Throwable $e) {
|
||||
$failed++;
|
||||
$output->writeln('<error>[数据库] 建表失败 ' . $table . ': ' . $e->getMessage() . '</error>');
|
||||
Log::error("[AddonRepair] create table {$table} failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln("[数据库] 已建表 {$created} / 已存在 {$exists} / 失败 {$failed}");
|
||||
|
||||
// 孤儿无前缀表检测与修复:
|
||||
// 某些插件经 importsql 在前缀未就绪时被建为无前缀表(如 appmarket_addon_submissions),
|
||||
// 而运行时 Db::name() 拼出的表名带前缀,导致业务读不到数据。这里扫描并提示/修复。
|
||||
$orphans = $this->detectOrphanTables($name, $prefix);
|
||||
if (!empty($orphans)) {
|
||||
$force = (bool) $input->getOption('force');
|
||||
foreach ($orphans as $plain => $prefixed) {
|
||||
if ($force) {
|
||||
try {
|
||||
if (!BaseModel::tableExists($prefixed)) {
|
||||
Db::execute("RENAME TABLE `{$plain}` TO `{$prefixed}`");
|
||||
$output->writeln("<info>[数据库] 已修复孤儿表: {$plain} -> {$prefixed}</info>");
|
||||
} else {
|
||||
$output->writeln("<comment>[数据库] 已存在带前缀表,跳过重命名: {$prefixed}(无前缀表 {$plain} 需手动清理)</comment>");
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeln('<error>[数据库] 重命名失败 ' . $plain . ': ' . $e->getMessage() . '</error>');
|
||||
$failed++;
|
||||
}
|
||||
} else {
|
||||
$output->writeln("<error>[数据库] 发现无前缀孤儿表: {$plain}(应命名为 {$prefixed},加 --force 自动重命名修复)</error>");
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $failed === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描本插件是否存在「无前缀的孤儿表」:install.sql 里 __PREFIX__xxx 对应的运行时表名带前缀,
|
||||
* 但库中却存在无前缀同名表(导入时前缀为空导致)。返回 [无前缀表 => 正确带前缀表] 映射。
|
||||
*/
|
||||
private function detectOrphanTables(string $name, string $prefix): array
|
||||
{
|
||||
$map = [];
|
||||
$base = defined('ADDON_PATH') ? ADDON_PATH : (root_path() . 'addon' . DIRECTORY_SEPARATOR);
|
||||
$installSql = $base . $name . DIRECTORY_SEPARATOR . 'install.sql';
|
||||
if (!is_file($installSql)) {
|
||||
return $map;
|
||||
}
|
||||
$content = preg_replace('/--.*$/m', '', (string) file_get_contents($installSql));
|
||||
if (!preg_match_all('/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"]?__PREFIX__([a-zA-Z0-9_]+)[`"]?/i', $content, $matches)) {
|
||||
return $map;
|
||||
}
|
||||
foreach (array_unique($matches[1]) as $suffix) {
|
||||
$plain = $name . '_' . ltrim($suffix, '_');
|
||||
$prefixed = $prefix . $plain;
|
||||
if (BaseModel::tableExists($plain) && !BaseModel::tableExists($prefixed)) {
|
||||
$map[$plain] = $prefixed;
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置行补全:确保 addon_config 表有该插件配置行。
|
||||
*/
|
||||
private function repairConfig(AddonService $svc, string $name, Output $output): bool
|
||||
{
|
||||
$default = Config::get('database.default');
|
||||
$prefix = Config::get("database.connections.{$default}.prefix") ?: '';
|
||||
$configTable = $prefix . 'addon_config';
|
||||
try {
|
||||
if (!BaseModel::tableExists($configTable)) {
|
||||
$output->writeln('<comment>[配置] addon_config 表不存在,跳过</comment>');
|
||||
return true;
|
||||
}
|
||||
$row = Db::table($configTable)->where('name', $name)->find();
|
||||
if (!empty($row)) {
|
||||
$output->writeln('<comment>[配置] 配置行已存在,跳过</comment>');
|
||||
return true;
|
||||
}
|
||||
// 读取插件默认配置(config.php 返回数组)
|
||||
$configFile = $this->addonDir($name) . 'config.php';
|
||||
$value = [];
|
||||
if (is_file($configFile)) {
|
||||
$cfg = include $configFile;
|
||||
if (is_array($cfg)) {
|
||||
$value = $cfg;
|
||||
}
|
||||
}
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'value' => json_encode($value, JSON_UNESCAPED_UNICODE),
|
||||
];
|
||||
// 兼容不同时间戳字段名(createtime/updatetime 或 create_time/update_time)
|
||||
$cols = Db::query("SHOW COLUMNS FROM `{$configTable}`");
|
||||
$colNames = array_column($cols, 'Field');
|
||||
$now = time();
|
||||
if (in_array('createtime', $colNames, true)) {
|
||||
$data['createtime'] = $now;
|
||||
} elseif (in_array('create_time', $colNames, true)) {
|
||||
$data['create_time'] = $now;
|
||||
}
|
||||
if (in_array('updatetime', $colNames, true)) {
|
||||
$data['updatetime'] = $now;
|
||||
} elseif (in_array('update_time', $colNames, true)) {
|
||||
$data['update_time'] = $now;
|
||||
}
|
||||
Db::table($configTable)->insert($data);
|
||||
$output->writeln("<info>[配置] 已补全配置行: {$name}</info>");
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeln('<error>[配置] 补全失败: ' . $e->getMessage() . '</error>');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function addonDir(string $name): string
|
||||
{
|
||||
$base = defined('ADDON_PATH') ? ADDON_PATH : (root_path() . 'addon' . DIRECTORY_SEPARATOR);
|
||||
return $base . $name . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
private function installedaddon(): array
|
||||
{
|
||||
$base = defined('ADDON_PATH') ? ADDON_PATH : (root_path() . 'addon' . DIRECTORY_SEPARATOR);
|
||||
if (!is_dir($base)) {
|
||||
return [];
|
||||
}
|
||||
$names = [];
|
||||
foreach (array_diff(scandir($base), ['.', '..']) as $cand) {
|
||||
$dir = $base . $cand . DIRECTORY_SEPARATOR;
|
||||
if (is_dir($dir) && is_file($dir . 'info.php')) {
|
||||
$names[] = $cand;
|
||||
}
|
||||
}
|
||||
return $names;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ywxapp\command;
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
|
||||
/**
|
||||
* AppCommand 应用命令统一入口
|
||||
*
|
||||
* 将 ywxapp/command/app/ 目录下的全部命令(app 脚手架 / 生成器)逻辑内联到本类,
|
||||
* 绑定为单一入口 `php think app <子命令> [参数]`,不再保留独立的命令类文件。
|
||||
* 插件等其它命令(addon:* 系列)由各自独立注册,不在此处理。
|
||||
*
|
||||
* 子命令:
|
||||
* build 构建应用目录(原 build:app)
|
||||
* clear 清理运行时文件(原 clear)
|
||||
* controller 生成控制器(原 app:controller,支持 --api/--plain)
|
||||
* model 生成模型(原 app:model)
|
||||
* event 生成事件(原 app:event)
|
||||
* listener 生成监听器(原 make:listener)
|
||||
* middleware 生成中间件(原 make:middleware)
|
||||
* service 生成服务(原 make:service)
|
||||
* subscribe 生成订阅器(原 make:subscribe)
|
||||
* validate 生成验证器(原 make:validate)
|
||||
*
|
||||
* 用法:
|
||||
* php think app # 列出所有可用子命令
|
||||
* php think app controller app\index # 生成控制器
|
||||
* php think app controller app\index --api # 生成 api 风格控制器
|
||||
* php think app model app\demo # 生成模型
|
||||
* php think app clear --cache # 清理缓存
|
||||
* php think app build myapp # 构建应用目录
|
||||
*/
|
||||
class AppCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Make 型子命令配置
|
||||
* type : 类型标签(用于成功/存在提示)
|
||||
* stub : stub 文件名(不含 .stub 扩展)
|
||||
* ns : 命名空间末尾段(如 controller/model/event ...)
|
||||
* suffix : 类名后缀(仅 controller 在开启 controller_suffix 时追加 Controller)
|
||||
*/
|
||||
private const MAKE_TYPES = [
|
||||
'controller' => ['type' => 'Controller', 'stub' => 'controller', 'ns' => 'controller', 'suffix' => 'Controller'],
|
||||
'model' => ['type' => 'Model', 'stub' => 'model', 'ns' => 'model', 'suffix' => ''],
|
||||
'event' => ['type' => 'Event', 'stub' => 'event', 'ns' => 'event', 'suffix' => ''],
|
||||
'listener' => ['type' => 'Listener', 'stub' => 'listener', 'ns' => 'listener', 'suffix' => ''],
|
||||
'middleware' => ['type' => 'Middleware', 'stub' => 'middleware', 'ns' => 'middleware', 'suffix' => ''],
|
||||
'service' => ['type' => 'Service', 'stub' => 'service', 'ns' => 'service', 'suffix' => ''],
|
||||
'subscribe' => ['type' => 'Subscribe', 'stub' => 'subscribe', 'ns' => 'subscribe', 'suffix' => ''],
|
||||
'validate' => ['type' => 'Validate', 'stub' => 'validate', 'ns' => 'validate', 'suffix' => ''],
|
||||
];
|
||||
|
||||
/**
|
||||
* 应用基础目录(Build 使用)
|
||||
* @var string
|
||||
*/
|
||||
private $basePath = '';
|
||||
|
||||
/**
|
||||
* stub 模板目录(已从 ywxapp/command/app/stubs 上移至此)
|
||||
*/
|
||||
private function stubDir(): string
|
||||
{
|
||||
return __DIR__ . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('app')
|
||||
->addOption('list', 'l', Option::VALUE_NONE, '列出所有可用子命令')
|
||||
->addOption('api', null, Option::VALUE_NONE, '生成 api 风格控制器(仅 controller)')
|
||||
->addOption('plain', null, Option::VALUE_NONE, '生成空控制器(仅 controller)')
|
||||
->addOption('cache', 'c', Option::VALUE_NONE, '清理缓存目录(仅 clear)')
|
||||
->addOption('log', null, Option::VALUE_NONE, '清理日志目录(仅 clear)')
|
||||
->addOption('dir', 'r', Option::VALUE_NONE, '清理空目录(仅 clear)')
|
||||
->addArgument('subcommand', Argument::OPTIONAL, '子命令名(build/clear/controller/model/event/listener/middleware/service/subscribe/validate)')
|
||||
->addArgument('name', Argument::OPTIONAL, '类名或 app 名(如 app\\index 或 myapp)')
|
||||
->setDescription('YwxApp 应用命令入口:聚合 ywxapp/command/app 下的命令,php think app <子命令> [参数]');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
protected function execute(Input $input, Output $output): int
|
||||
{
|
||||
if ($input->getOption('list') || empty($input->getArgument('subcommand'))) {
|
||||
$this->showList($output);
|
||||
return 0;
|
||||
}
|
||||
|
||||
$sub = $input->getArgument('subcommand');
|
||||
$name = (string) $input->getArgument('name');
|
||||
|
||||
if (isset(self::MAKE_TYPES[$sub])) {
|
||||
return $this->runMake($sub, $name, $input, $output);
|
||||
}
|
||||
if ($sub === 'build') {
|
||||
return $this->runBuild($name, $output);
|
||||
}
|
||||
if ($sub === 'clear') {
|
||||
return $this->runClear($name, $input, $output);
|
||||
}
|
||||
|
||||
$output->writeln('<error>未知子命令: ' . $sub . '</error>');
|
||||
$this->showList($output);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有可用子命令
|
||||
*/
|
||||
private function showList(Output $output): void
|
||||
{
|
||||
$output->writeln('<info>YwxApp 应用命令(`php think app <子命令> [参数]`):</info>');
|
||||
$output->writeln('');
|
||||
$width = max(array_map('strlen', array_keys(self::MAKE_TYPES + ['build' => 1, 'clear' => 1])));
|
||||
foreach (array_keys(['build' => 1, 'clear' => 1] + self::MAKE_TYPES) as $name) {
|
||||
$desc = [
|
||||
'build' => '构建应用目录',
|
||||
'clear' => '清理运行时文件',
|
||||
'controller' => '生成控制器(--api/--plain)',
|
||||
'model' => '生成模型',
|
||||
'event' => '生成事件',
|
||||
'listener' => '生成监听器',
|
||||
'middleware' => '生成中间件',
|
||||
'service' => '生成服务',
|
||||
'subscribe' => '生成订阅器',
|
||||
'validate' => '生成验证器',
|
||||
][$name];
|
||||
$output->writeln(' ' . str_pad($name, $width) . ' ' . $desc);
|
||||
}
|
||||
$output->writeln('');
|
||||
$output->writeln('示例:php think app controller app\\index');
|
||||
$output->writeln(' php think app clear --cache');
|
||||
}
|
||||
|
||||
/* ===================== Make 型生成器 ===================== */
|
||||
|
||||
/**
|
||||
* 运行 Make 型子命令
|
||||
*/
|
||||
private function runMake(string $type, string $name, Input $input, Output $output): int
|
||||
{
|
||||
if ($name === '') {
|
||||
$output->writeln('<error>缺少类名参数,用法:php think app ' . $type . ' <类名></error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$cfg = self::MAKE_TYPES[$type];
|
||||
$classname = $this->makeClassName($type, $name);
|
||||
$pathname = $this->makePathName($classname);
|
||||
|
||||
if (is_file($pathname)) {
|
||||
$output->writeln('<error>' . $cfg['type'] . ':' . $classname . ' already exists!</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!is_dir(dirname($pathname))) {
|
||||
mkdir(dirname($pathname), 0755, true);
|
||||
}
|
||||
|
||||
file_put_contents($pathname, $this->buildClass($classname, $type, $input));
|
||||
|
||||
$output->writeln('<info>' . $cfg['type'] . ':' . $classname . ' created successfully.</info>');
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 stub 路径(controller 支持 --api/--plain)
|
||||
*/
|
||||
private function makeStub(string $type, Input $input): string
|
||||
{
|
||||
if ($type === 'controller') {
|
||||
if ($input->getOption('api')) {
|
||||
return $this->stubDir() . 'controller.api.stub';
|
||||
}
|
||||
if ($input->getOption('plain')) {
|
||||
return $this->stubDir() . 'controller.plain.stub';
|
||||
}
|
||||
return $this->stubDir() . 'controller.stub';
|
||||
}
|
||||
|
||||
return $this->stubDir() . self::MAKE_TYPES[$type]['stub'] . '.stub';
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析命名空间
|
||||
*/
|
||||
private function makeNamespace(string $type, string $app): string
|
||||
{
|
||||
return 'app' . ($app ? '\\' . $app : '') . '\\' . self::MAKE_TYPES[$type]['ns'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析完整类名(等价于 think\console\command\Make::getClassName + Controller 后缀)
|
||||
*/
|
||||
private function makeClassName(string $type, string $name): string
|
||||
{
|
||||
if (str_contains($name, '\\')) {
|
||||
$class = $name;
|
||||
} else {
|
||||
$app = '';
|
||||
if (str_contains($name, '@')) {
|
||||
[$app, $name] = explode('@', $name);
|
||||
}
|
||||
if (str_contains($name, '/')) {
|
||||
$name = str_replace('/', '\\', $name);
|
||||
}
|
||||
$class = $this->makeNamespace($type, $app) . '\\' . $name;
|
||||
}
|
||||
|
||||
$suffix = self::MAKE_TYPES[$type]['suffix'];
|
||||
if ($suffix !== '' && $this->app->config->get('route.controller_suffix')) {
|
||||
$class .= $suffix;
|
||||
}
|
||||
|
||||
return $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析文件路径(等价于各 Make 命令重写的 getPathName)
|
||||
*/
|
||||
private function makePathName(string $name): string
|
||||
{
|
||||
return $this->app->getRootPath() . ltrim(str_replace('\\', '/', $name), '/') . '.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* 用 stub 渲染类文件(等价于 think\console\command\Make::buildClass)
|
||||
*/
|
||||
private function buildClass(string $name, string $type, Input $input): string
|
||||
{
|
||||
$stub = file_get_contents($this->makeStub($type, $input));
|
||||
|
||||
$namespace = trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\');
|
||||
$class = str_replace($namespace . '\\', '', $name);
|
||||
|
||||
return str_replace(
|
||||
['{%className%}', '{%actionSuffix%}', '{%namespace%}', '{%app_namespace%}'],
|
||||
[
|
||||
$class,
|
||||
$this->app->config->get('route.action_suffix'),
|
||||
$namespace,
|
||||
$this->app->getNamespace(),
|
||||
],
|
||||
$stub
|
||||
);
|
||||
}
|
||||
|
||||
/* ===================== Build 命令 ===================== */
|
||||
|
||||
/**
|
||||
* 运行 build 子命令(原 build:app)
|
||||
*/
|
||||
private function runBuild(string $app, Output $output): int
|
||||
{
|
||||
$this->basePath = $this->app->getRootPath() . 'app' . DIRECTORY_SEPARATOR;
|
||||
$app = $app ?: '';
|
||||
|
||||
if (is_file($this->basePath . 'build.php')) {
|
||||
$list = include $this->basePath . 'build.php';
|
||||
} else {
|
||||
$list = [
|
||||
'__dir__' => ['controller', 'model', 'view'],
|
||||
];
|
||||
}
|
||||
|
||||
$this->buildApp($app, $list);
|
||||
$output->writeln('<info>Successed</info>');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建应用
|
||||
*/
|
||||
private function buildApp(string $app, array $list = []): void
|
||||
{
|
||||
if (!is_dir($this->basePath . $app)) {
|
||||
mkdir($this->basePath . $app);
|
||||
}
|
||||
|
||||
$appPath = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '');
|
||||
$namespace = 'app' . ($app ? '\\' . $app : '');
|
||||
|
||||
$this->buildCommon($app);
|
||||
$this->buildHello($app, $namespace);
|
||||
$this->buildapp1($app, $namespace);
|
||||
$this->buildInfo($app);
|
||||
|
||||
foreach ($list as $path => $file) {
|
||||
if ('__dir__' == $path) {
|
||||
foreach ($file as $dir) {
|
||||
$this->checkDirBuild($appPath . $dir);
|
||||
}
|
||||
} elseif ('__file__' == $path) {
|
||||
foreach ($file as $fname) {
|
||||
if (!is_file($appPath . $fname)) {
|
||||
file_put_contents($appPath . $fname, 'php' == pathinfo($fname, PATHINFO_EXTENSION) ? '<?php' . PHP_EOL : '');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach ($file as $val) {
|
||||
$val = trim($val);
|
||||
$filename = $appPath . $path . DIRECTORY_SEPARATOR . $val . '.php';
|
||||
$space = $namespace . '\\' . $path;
|
||||
$class = $val;
|
||||
switch ($path) {
|
||||
case 'controller':
|
||||
if ($this->app->config->get('route.controller_suffix')) {
|
||||
$filename = $appPath . $path . DIRECTORY_SEPARATOR . $val . 'Controller.php';
|
||||
$class = $val . 'Controller';
|
||||
}
|
||||
$content = '<?php' . PHP_EOL . 'namespace ' . $space . ';' . PHP_EOL . PHP_EOL . 'class ' . $class . PHP_EOL . '{' . PHP_EOL . PHP_EOL . '}';
|
||||
break;
|
||||
case 'model':
|
||||
$content = '<?php' . PHP_EOL . 'namespace ' . $space . ';' . PHP_EOL . PHP_EOL . 'use think\Model;' . PHP_EOL . PHP_EOL . 'class ' . $class . ' extends Model' . PHP_EOL . '{' . PHP_EOL . PHP_EOL . '}';
|
||||
break;
|
||||
case 'view':
|
||||
$filename = $appPath . $path . DIRECTORY_SEPARATOR . $val . '.html';
|
||||
$this->checkDirBuild(dirname($filename));
|
||||
$content = '';
|
||||
break;
|
||||
default:
|
||||
$content = '<?php' . PHP_EOL . 'namespace ' . $space . ';' . PHP_EOL . PHP_EOL . 'class ' . $class . PHP_EOL . '{' . PHP_EOL . PHP_EOL . '}';
|
||||
}
|
||||
|
||||
if (!is_file($filename)) {
|
||||
file_put_contents($filename, $content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建应用的欢迎页面
|
||||
*/
|
||||
private function buildHello(string $app, string $namespace): void
|
||||
{
|
||||
$suffix = $this->app->config->get('route.controller_suffix') ? 'Controller' : '';
|
||||
$filename = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '') . 'controller' . DIRECTORY_SEPARATOR . 'Index' . $suffix . '.php';
|
||||
|
||||
if (!is_file($filename)) {
|
||||
$content = file_get_contents($this->stubDir() . 'controller.stub');
|
||||
$content = str_replace(
|
||||
['{%className%}', '{%actionSuffix%}', '{%namespace%}', '{%app_namespace%}'],
|
||||
['Index', $suffix, $namespace . '\\controller', $this->app->getNamespace()],
|
||||
$content
|
||||
);
|
||||
$this->checkDirBuild(dirname($filename));
|
||||
file_put_contents($filename, $content);
|
||||
}
|
||||
}
|
||||
|
||||
private function buildapp1(string $app, string $namespace): void
|
||||
{
|
||||
$appPath = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '');
|
||||
if (!is_file($appPath . 'app.php')) {
|
||||
$content = file_get_contents($this->stubDir() . 'app.stub');
|
||||
$content = str_replace(['{%namespace%}', '{%app%}', '{%className%}'], [$namespace, $app, 'app'], $content);
|
||||
file_put_contents($appPath . 'app.php', $content);
|
||||
}
|
||||
}
|
||||
|
||||
private function buildInfo(string $app): void
|
||||
{
|
||||
$appPath = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '');
|
||||
if (!is_file($appPath . 'info.php')) {
|
||||
$array = [
|
||||
'name' => '',
|
||||
'title' => '',
|
||||
'intro' => '',
|
||||
'author' => '',
|
||||
'website' => '',
|
||||
'version' => '1.0.0',
|
||||
'state' => 1,
|
||||
'url' => '',
|
||||
'license' => '',
|
||||
'licenseto' => 0,
|
||||
];
|
||||
$array = array_merge($array, ['name' => $app, 'url' => '/' . $app]);
|
||||
$output = "<?php\n return " . var_export($array, true) . ";\n?>";
|
||||
file_put_contents($appPath . 'info.php', $output);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建应用的公共文件
|
||||
*/
|
||||
private function buildCommon(string $app): void
|
||||
{
|
||||
$appPath = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '');
|
||||
foreach (['event', 'middleware', 'common', 'config'] as $nm) {
|
||||
if (!is_file($appPath . $nm . '.php')) {
|
||||
file_put_contents($appPath . $nm . '.php', '<?php' . PHP_EOL . '// 这是系统自动生成的' . $nm . '定义文件' . PHP_EOL . 'return [' . PHP_EOL . PHP_EOL . '];' . PHP_EOL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建目录
|
||||
*/
|
||||
private function checkDirBuild(string $dirname): void
|
||||
{
|
||||
if (!is_dir($dirname)) {
|
||||
mkdir($dirname, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== Clear 命令 ===================== */
|
||||
|
||||
/**
|
||||
* 运行 clear 子命令(原 clear)
|
||||
*/
|
||||
private function runClear(string $app, Input $input, Output $output): int
|
||||
{
|
||||
$runtimePath = $this->app->getRootPath() . 'runtime' . DIRECTORY_SEPARATOR . ($app ? $app . DIRECTORY_SEPARATOR : '');
|
||||
|
||||
if ($input->getOption('cache')) {
|
||||
$path = $runtimePath . 'cache';
|
||||
} elseif ($input->getOption('log')) {
|
||||
$path = $runtimePath . 'log';
|
||||
} else {
|
||||
$path = $runtimePath;
|
||||
}
|
||||
|
||||
$rmdir = $input->getOption('dir') ? true : false;
|
||||
$this->clearRuntime(rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, $rmdir);
|
||||
|
||||
$output->writeln('<info>Clear Successed</info>');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归清理目录
|
||||
*/
|
||||
private function clearRuntime(string $path, bool $rmdir): void
|
||||
{
|
||||
$files = is_dir($path) ? scandir($path) : [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ('.' != $file && '..' != $file && is_dir($path . $file)) {
|
||||
array_map('unlink', glob($path . $file . DIRECTORY_SEPARATOR . '*.*'));
|
||||
if ($rmdir) {
|
||||
rmdir($path . $file);
|
||||
}
|
||||
} elseif ('.gitignore' != $file && is_file($path . $file)) {
|
||||
unlink($path . $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
// app/command/StartWorkerman.php
|
||||
namespace ywxapp\command;
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\console\input\Option;
|
||||
use GatewayWorker\Gateway;
|
||||
use GatewayWorker\BusinessWorker;
|
||||
use GatewayWorker\Register;
|
||||
use think\facade\App;
|
||||
use think\facade\Config;
|
||||
|
||||
/**
|
||||
* GatewayWorker 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class GatewayWorker extends Command
|
||||
{
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('workerman:gateway')
|
||||
->addOption('acthon', 'a', Option::VALUE_OPTIONAL, '操作')
|
||||
->setDescription('Start the Workerman server');
|
||||
}
|
||||
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
// register 必须是text协议,切记不能将register端口开放给外网
|
||||
$register = new Register('text://127.0.0.1:1238');
|
||||
|
||||
// bussinessWorker 进程
|
||||
$worker = new BusinessWorker();
|
||||
$worker->name = 'ChatBusinessWorker';
|
||||
$worker->count = 4;
|
||||
$worker->registerAddress = '127.0.0.1:1238';
|
||||
$worker->eventHandler = '\\ywxapp\handler\BusinessHandler';
|
||||
|
||||
$context = array(
|
||||
'ssl' => array(
|
||||
'local_cert' => __DIR__ . '/fullchain.pem', // 也可以是crt文件
|
||||
'local_pk' => __DIR__ . '/privkey.pem',
|
||||
'verify_peer' => false,
|
||||
'allow_self_signed' => true, //如果是自签名证书需要开启此选项
|
||||
)
|
||||
);
|
||||
$gateway = new Gateway("websocket://0.0.0.0:8282", $context);
|
||||
$gateway->name = 'AppGateway';
|
||||
$gateway->count = 2;
|
||||
$gateway->lanIp = '127.0.0.1';
|
||||
$gateway->startPort = 2900;
|
||||
$gateway->registerAddress = '127.0.0.1:1238';
|
||||
//$gateway->onWorkerStart
|
||||
//$gateway->onWorkerStop
|
||||
//$gateway->onConnect
|
||||
//$gateway->onMessage
|
||||
//$gateway->onClose
|
||||
$gateway->transport = 'ssl';
|
||||
|
||||
// 初始化 ThinkPHP 应用
|
||||
$app = App::getInstance();
|
||||
$gateway->statusCallback = function ($status) use ($app) {
|
||||
// 在这里可以使用 ThinkPHP 容器
|
||||
$app->make(\ywxapp\services\AppService::class);
|
||||
// print_r($app->user);
|
||||
|
||||
// 你可以在这里调用 ThinkPHP 的服务
|
||||
// $service = $app->make(\app\service\MyService::class);
|
||||
// $service->doSomething();
|
||||
|
||||
// 处理工作进程状态变化
|
||||
// 例如:记录日志,发送通知等
|
||||
file_put_contents('status.log', json_encode($status) . PHP_EOL, FILE_APPEND);
|
||||
};
|
||||
|
||||
// $gateway->pingInterval = 10;
|
||||
// $gateway->pingNotResponseLimit = 1;
|
||||
// $gateway->pingData = '{"type":"ping"}';
|
||||
Gateway::runAll();
|
||||
|
||||
// 输出启动信息
|
||||
$output->writeln("Workerman server started on http://0.0.0.0:2345");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace ywxapp\command;
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\App;
|
||||
use ywxapp\service\FrameworkService;
|
||||
use ywxapp\service\AddonService;
|
||||
|
||||
/**
|
||||
* 一键离线升级命令(手动执行升级)
|
||||
*
|
||||
* 用于绕过在线下载,直接对本地打包产物执行升级:
|
||||
* - 框架核心包 / 框架整站包 → 复用 FrameworkService::apply(解压、备份、跑 upgrade.sql、写版本号)
|
||||
* - 插件包 → 复用 AddonService::local(解压覆盖、导入 install.sql、重导菜单)
|
||||
*
|
||||
* 用法:
|
||||
* # 框架核心补丁包(剥离 ywxapp/ 前缀,解压到 ywxapp/)
|
||||
* php think ywxapp:upgrade --file=runtime/framework/ywxapp-1.0.9.zip --ver=1.0.9 --type=framework --patch
|
||||
* # 框架整站包(不解前缀,解压到项目根)
|
||||
* php think ywxapp:upgrade --file=runtime/framework/ywxapp-1.0.9.zip --ver=1.0.9 --type=framework
|
||||
* # 插件升级(zip 根已是插件内容)
|
||||
* php think ywxapp:upgrade --file=runtime/market/forum-1.0.1.zip --type=addon
|
||||
*
|
||||
* 说明:
|
||||
* - 框架 --ver 必填(写入 config/ywxapp.php);插件版本从 zip 内 info.php 自动读取。
|
||||
* - 命令执行前会自动备份(FrameworkService 备份到 runtime/framework/backup;AddonService 备份到 runtime/addon)。
|
||||
*/
|
||||
class Upgrade extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('ywxapp:upgrade')
|
||||
->addOption('file', 'f', Option::VALUE_REQUIRED, '升级包本地路径(zip)')
|
||||
->addOption('type', 't', Option::VALUE_REQUIRED, '类型: framework | addon')
|
||||
->addOption('ver', 'r', Option::VALUE_OPTIONAL, '目标版本号(framework 必填;addon 自动读取)')
|
||||
->addOption('patch', 'p', Option::VALUE_NONE, '框架补丁包(核心包,剥离 ywxapp/ 前缀;整站包勿加)')
|
||||
->setDescription('本地离线升级:框架核心/整站包或插件包一键手动执行');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output): int
|
||||
{
|
||||
$file = trim((string) $input->getOption('file'));
|
||||
$type = strtolower(trim((string) $input->getOption('type')));
|
||||
$version = trim((string) $input->getOption('ver'));
|
||||
$isPatch = (bool) $input->getOption('patch');
|
||||
|
||||
if ($file === '' || !is_file($file)) {
|
||||
$output->writeln('<error>请通过 --file 指定存在的 zip 路径</error>');
|
||||
return 1;
|
||||
}
|
||||
if (!in_array($type, ['framework', 'addon'], true)) {
|
||||
$output->writeln('<error>--type 必须是 framework 或 addon</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($type === 'framework') {
|
||||
if ($version === '') {
|
||||
$output->writeln('<error>framework 升级需通过 --version 指定目标版本号</error>');
|
||||
return 1;
|
||||
}
|
||||
$output->writeln("<info>开始升级框架到 {$version}(" . ($isPatch ? '补丁包' : '整站包') . ")...</info>");
|
||||
$svc = App::getInstance()->make(FrameworkService::class);
|
||||
$res = $svc->apply($file, $version, $isPatch ? 1 : 0);
|
||||
$output->writeln('<info>框架升级完成:' . ($res['msg'] ?? 'ok') . '</info>');
|
||||
} else {
|
||||
$output->writeln("<info>开始升级插件({$file})...</info>");
|
||||
// 从 zip 内 info.php 解析插件名,用于实例化 AddonService(需先确定 addonDir)
|
||||
$addonName = $this->readAddonNameFromZip($file);
|
||||
if ($addonName === '') {
|
||||
$output->writeln('<error>无法从升级包内 info.php 解析插件名</error>');
|
||||
return 1;
|
||||
}
|
||||
// 构造 think\File 对象(real 路径,足够 validate/upload 使用)
|
||||
$fileObj = new \think\File($file);
|
||||
$svc = new AddonService($addonName);
|
||||
$res = $svc->localUpgrade($fileObj);
|
||||
$output->writeln('<info>插件升级完成:' . $addonName
|
||||
. ' ' . ($res['from'] ?? '') . ' -> ' . ($res['to'] ?? '') . '</info>');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeln('<error>升级失败:' . $e->getMessage() . '</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln('<comment>建议随后清理缓存:删除 runtime/cache、runtime/temp</comment>');
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从升级包 zip 内 info.php 解析插件名
|
||||
*
|
||||
* @param string $zipFile
|
||||
* @return string
|
||||
*/
|
||||
private function readAddonNameFromZip(string $zipFile): string
|
||||
{
|
||||
$zip = new \ZipArchive();
|
||||
if ($zip->open($zipFile) !== true) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
// 寻找包内 info.php(可能位于根目录或 <addon>/info.php)
|
||||
$name = '';
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$entry = $zip->getNameIndex($i);
|
||||
if (basename($entry) === 'info.php' && substr($entry, -strlen('info.php')) === 'info.php') {
|
||||
$content = $zip->getFromIndex($i);
|
||||
if ($content !== false && preg_match("/'name'\s*=>\s*'([^']+)'/", $content, $m)) {
|
||||
$name = $m[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $name;
|
||||
} finally {
|
||||
$zip->close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
use ywxapp\library\Menu;
|
||||
use ywxapp\AddonBase;
|
||||
use think\Request;
|
||||
|
||||
class {%className%} extends addon
|
||||
{
|
||||
/**
|
||||
* 插件安装方法
|
||||
* @return bool
|
||||
*/
|
||||
public function install()
|
||||
{
|
||||
$menu = [];
|
||||
Menu::create($menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件卸载方法
|
||||
* @return bool
|
||||
*/
|
||||
public function uninstall()
|
||||
{
|
||||
Menu::delete('{%addon%}');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件启用方法
|
||||
*/
|
||||
public function enable()
|
||||
{
|
||||
Menu::enable('{%addon%}');
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件禁用方法
|
||||
*/
|
||||
public function disable()
|
||||
{
|
||||
Menu::disable('{%addon%}');
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件升级方法(覆盖升级时的数据/配置迁移)
|
||||
* @param string $currentVersion 已安装版本号
|
||||
*/
|
||||
public function upgrade($currentVersion = '')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端菜单方法
|
||||
*/
|
||||
public function frontMenu(){}
|
||||
|
||||
/**
|
||||
* 会员菜单方法
|
||||
*/
|
||||
public function memberMenu(){}
|
||||
|
||||
/**
|
||||
* 后台菜单方法
|
||||
*/
|
||||
public function backMenu(){}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
class {%className%} extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
// 指令配置
|
||||
$this->setName('{%commandName%}')
|
||||
->setDescription('the {%commandName%} command');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
// 指令输出
|
||||
$output->writeln('{%commandName%}');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
use think\Request;
|
||||
|
||||
class {%className%} extends \ywxapp\controller\AddonFrontend
|
||||
{
|
||||
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['*'];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化 _initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 显示资源列表
|
||||
*
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function index{%actionSuffix%}()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新建的资源
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function save{%actionSuffix%}(Request $request)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示指定的资源
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function read{%actionSuffix%}($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存更新的资源
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function update{%actionSuffix%}(Request $request, $id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定资源
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function delete{%actionSuffix%}($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
class {%className%} extends \ywxapp\controller\AddonFrontend
|
||||
{
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['*'];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化 _initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
use think\Request;
|
||||
|
||||
class {%className%} extends \ywxapp\controller\AddonFrontend
|
||||
{
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['*'];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化 _initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 显示资源列表
|
||||
*
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function index{%actionSuffix%}()
|
||||
{
|
||||
return "这是一个{%app_namespace%} 插件应用";
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示创建资源表单页.
|
||||
*
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function create{%actionSuffix%}()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新建的资源
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function save{%actionSuffix%}(Request $request)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示指定的资源
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function read{%actionSuffix%}($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示编辑资源表单页.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function edit{%actionSuffix%}($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存更新的资源
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function update{%actionSuffix%}(Request $request, $id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定资源
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function delete{%actionSuffix%}($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
class {%className%}
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
//这是{%app%}应用的配置文件
|
||||
return [
|
||||
'name' => '{%app%}',
|
||||
'title' => '',
|
||||
'intro' => '',
|
||||
'author' => '',
|
||||
'website' => '',
|
||||
'version' => '1.0.0',
|
||||
'state' => 1,
|
||||
'url' => '/{%app%}',
|
||||
'license' => '',
|
||||
'licenseto' => 0,
|
||||
'config' => [],
|
||||
'events' => [
|
||||
// 事件绑定,
|
||||
'bind' => [],
|
||||
// 事件监听
|
||||
'listen' => [],
|
||||
// 事件订阅
|
||||
'subscribe' => [],
|
||||
],
|
||||
'middleware' => [
|
||||
// 别名 => 中间件类
|
||||
'alias' => [],
|
||||
// 中间件优先级,越靠前优先级越高
|
||||
'priority' => [],
|
||||
],
|
||||
'services' => [],
|
||||
];
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
class {%className%}
|
||||
{
|
||||
/**
|
||||
* 事件监听处理
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($param)
|
||||
{
|
||||
// 这里可以执行一些插件的初始化操作,比如加载配置、注册服务等
|
||||
// 例如:加载插件的语言包
|
||||
// \think\facade\Lang::load(addon_path('demo') . 'lang/zh-cn.php');
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
class {%className%}
|
||||
{
|
||||
/**
|
||||
* 处理请求
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @param \Closure $next
|
||||
* @return Response
|
||||
*/
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
use ywxapp\model\BaseModel;
|
||||
|
||||
/**
|
||||
* @mixin \think\Model
|
||||
*/
|
||||
class {%className%} extends BaseModel
|
||||
{
|
||||
protected function getOptions(): array
|
||||
{
|
||||
// 所有的参数配置统一返回
|
||||
return [
|
||||
'strict' => false,
|
||||
//'name' => '',
|
||||
// 'connection' => '',
|
||||
// 'query' => [],
|
||||
// 'type' => [],
|
||||
// 'hidden' => [],
|
||||
// 'visible' => [],
|
||||
// 'append' => [],
|
||||
'autoRelation' => [],
|
||||
'createTime' => 'create_at',
|
||||
'updateTime' => 'update_at',
|
||||
'dateFormat' => 'Y-m-d H:i:s',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
class {%className%} extends \think\Service
|
||||
{
|
||||
/**
|
||||
* 注册服务
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行服务
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
class {%className%}
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class {%className%} extends Validate
|
||||
{
|
||||
/**
|
||||
* 定义验证规则
|
||||
* 格式:'字段名' => ['规则1','规则2'...]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $rule = [];
|
||||
|
||||
/**
|
||||
* 定义错误信息
|
||||
* 格式:'字段名.规则名' => '错误信息'
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $message = [];
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
use ywxapp\library\Menu;
|
||||
use ywxapp\app\app;
|
||||
use think\Request;
|
||||
|
||||
class {%className%} extends app
|
||||
{
|
||||
/**
|
||||
* 插件安装方法
|
||||
* @return bool
|
||||
*/
|
||||
public function install()
|
||||
{
|
||||
$menu = [];
|
||||
Menu::create($menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件卸载方法
|
||||
* @return bool
|
||||
*/
|
||||
public function uninstall()
|
||||
{
|
||||
Menu::delete('{%app%}');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件启用方法
|
||||
*/
|
||||
public function enable()
|
||||
{
|
||||
Menu::enable('{%app%}');
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件禁用方法
|
||||
*/
|
||||
public function disable()
|
||||
{
|
||||
Menu::disable('{%app%}');
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端菜单方法
|
||||
*/
|
||||
abstract public function frontMenu();
|
||||
|
||||
/**
|
||||
* 会员菜单方法
|
||||
*/
|
||||
abstract public function memberMenu();
|
||||
|
||||
/**
|
||||
* 后台菜单方法
|
||||
*/
|
||||
abstract public function backMenu();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
/**
|
||||
* 应用模块引导类
|
||||
*
|
||||
* 由 `php think app build` 自动生成(应用标识:{%app%})。
|
||||
* 可作为该应用模块的入口 / 服务提供者,在此注册自定义服务、事件、中间件等。
|
||||
*/
|
||||
class {%className%}
|
||||
{
|
||||
/**
|
||||
* 应用模块标识
|
||||
* @var string
|
||||
*/
|
||||
public string $name = '{%app%}';
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
class {%className%} extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
// 指令配置
|
||||
$this->setName('{%commandName%}')
|
||||
->setDescription('the {%commandName%} command');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
// 指令输出
|
||||
$output->writeln('{%commandName%}');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
use think\Request;
|
||||
use ywxapp\app\Controller;
|
||||
|
||||
class {%className%} extends Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['*'];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化 _initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 显示资源列表
|
||||
*
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function index{%actionSuffix%}()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新建的资源
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function save{%actionSuffix%}(Request $request)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示指定的资源
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function read{%actionSuffix%}($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存更新的资源
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function update{%actionSuffix%}(Request $request, $id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定资源
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function delete{%actionSuffix%}($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
use ywxapp\app\Controller;
|
||||
|
||||
class {%className%} extends Controller
|
||||
{
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['*'];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化 _initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
use think\Request;
|
||||
use ywxapp\app\Controller;
|
||||
|
||||
class {%className%} extends Controller
|
||||
{
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['*'];
|
||||
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化 _initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 显示资源列表
|
||||
*
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function index{%actionSuffix%}()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示创建资源表单页.
|
||||
*
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function create{%actionSuffix%}()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新建的资源
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function save{%actionSuffix%}(Request $request)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示指定的资源
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function read{%actionSuffix%}($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示编辑资源表单页.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function edit{%actionSuffix%}($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存更新的资源
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function update{%actionSuffix%}(Request $request, $id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定资源
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function delete{%actionSuffix%}($id)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
class {%className%}
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
class {%className%}
|
||||
{
|
||||
/**
|
||||
* 事件监听处理
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($event)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
class {%className%}
|
||||
{
|
||||
/**
|
||||
* 处理请求
|
||||
*
|
||||
* @param \think\Request $request
|
||||
* @param \Closure $next
|
||||
* @return Response
|
||||
*/
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
use ywxapp\model\BaseModel;
|
||||
// use think\model\concern\SoftDelete;
|
||||
/**
|
||||
* @mixin \think\Model
|
||||
*/
|
||||
class {%className%} extends BaseModel
|
||||
{
|
||||
//use SoftDelete;
|
||||
protected function getOptions(): array
|
||||
{
|
||||
return [
|
||||
'strict' => true,
|
||||
'name' => 'user',
|
||||
'autoWriteTimestamp' => 'int',
|
||||
'readonly' => ['uid'],
|
||||
'createTime' => 'create_at',
|
||||
'updateTime' => 'update_at',
|
||||
//'deleteTime' => 'delete_at',
|
||||
//'hidden' => ['password', 'delete_at'],
|
||||
//'append' => ['status_text', 'last_login'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
class {%className%} extends \think\Service
|
||||
{
|
||||
/**
|
||||
* 注册服务
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行服务
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
class {%className%}
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace {%namespace%};
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class {%className%} extends Validate
|
||||
{
|
||||
/**
|
||||
* 定义验证规则
|
||||
* 格式:'字段名' => ['规则1','规则2'...]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $rule = [];
|
||||
|
||||
/**
|
||||
* 定义错误信息
|
||||
* 格式:'字段名.规则名' => '错误信息'
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $message = [];
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIE8DCCA9igAwIBAgISBIqpUxWdLjxwT3OUCLv/nisJMA0GCSqGSIb3DQEBCwUA
|
||||
MDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD
|
||||
EwNSMTEwHhcNMjQxMDA2MjMyOTAyWhcNMjUwMTA0MjMyOTAxWjAaMRgwFgYDVQQD
|
||||
Ew93d3cueGl4aW5nd2wuY24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
|
||||
AQCYXKbejo2pQKnyNpgPCq2czi7NQkooXDlqj4t7SEk4Tw0AtauGNPIal+pBWU5r
|
||||
HKeUuKEDGz/LqcCyd9YfhBvbCa6jCPGmymgUVXES5cnHJKw5wc1+kjJq1dheQ55T
|
||||
SCeZ1PVdZNyfDuhh4i8p2tREh2mxnRPY24DZrhBydkAwlqABeC7aarCDj9gPb2uh
|
||||
/EcJ51Q+geQhRVGSlLYQQS7+gNVL/WdKuSH0eGS07yw5SnsPTSRrCLTemKOmxZqV
|
||||
zGQKRM3vkz8zRfaHQbW+eL/y3udvbJbKdbuwACzlZbJOXT64DKb5Le8ARWJozEuV
|
||||
+lgf2RZkmWzZgQb1L/INymnLAgMBAAGjggIVMIICETAOBgNVHQ8BAf8EBAMCBaAw
|
||||
HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYD
|
||||
VR0OBBYEFDU0Hjnqlsf+CZ9J0fuHBfwGoNxzMB8GA1UdIwQYMBaAFMXPRqTq9MPA
|
||||
emyVxC2wXpIvJuO5MFcGCCsGAQUFBwEBBEswSTAiBggrBgEFBQcwAYYWaHR0cDov
|
||||
L3IxMS5vLmxlbmNyLm9yZzAjBggrBgEFBQcwAoYXaHR0cDovL3IxMS5pLmxlbmNy
|
||||
Lm9yZy8wGgYDVR0RBBMwEYIPd3d3LnhpeGluZ3dsLmNuMBMGA1UdIAQMMAowCAYG
|
||||
Z4EMAQIBMIIBBgYKKwYBBAHWeQIEAgSB9wSB9ADyAHcAouMK5EXvva2bfjjtR2d3
|
||||
U9eCW4SU1yteGyzEuVCkR+cAAAGSZF/NWwAABAMASDBGAiEA6H2F+bZd9MQsUPMF
|
||||
Xtq1n3i/KDFKIFsxQzEuJ4DCzasCIQDK0VQovaW0NUkQD2Tj+YLIISulAx1zHTkW
|
||||
N1MU8W+FJgB3ABNK3xq1mEIJeAxv70x6kaQWtyNJzlhXat+u2qfCq+AiAAABkmRf
|
||||
zjwAAAQDAEgwRgIhAPOG0h/G9lUBsLmcsutR2yMBET4dP1S7Ii2X/j9ijKfOAiEA
|
||||
22i5IJ6hR9l0XC+SiZ8Q5ZMnzhVJv9NBJsADSlqORz0wDQYJKoZIhvcNAQELBQAD
|
||||
ggEBACeS4gOR8V51NV1HtSfBDf/AF4/G/u4BBbsi3xEW0NW8McD8OkwTI5NRcupN
|
||||
A1jYMMFAqOUFgMjC6hJO/fOO7iFbKpbkBy3y+eI+943uaP8i5+S6ORErshn6A4NS
|
||||
hnGfXxAclS9XZadnayjY3sUCpvqxkGh7Yf1s2Qpk7UIdBH0wT+H61IyGCGMsP72Q
|
||||
J/VgDWi0eDWi8iKXL2vdKbSA2+8VyLcW5NY+hnITzXNCDaHLICxjo4Fsy/YmM4o1
|
||||
GJFuZbN/7Od7bEqoa4PmSyzP/3UwLYUJ+0eQTIqqBEEQJac0TwbCwlMF+i99M1lm
|
||||
7LDYQtTuVk3a4j4bDsEdxKgXqQ0=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFBjCCAu6gAwIBAgIRAIp9PhPWLzDvI4a9KQdrNPgwDQYJKoZIhvcNAQELBQAw
|
||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw
|
||||
WhcNMjcwMzEyMjM1OTU5WjAzMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
|
||||
RW5jcnlwdDEMMAoGA1UEAxMDUjExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
CgKCAQEAuoe8XBsAOcvKCs3UZxD5ATylTqVhyybKUvsVAbe5KPUoHu0nsyQYOWcJ
|
||||
DAjs4DqwO3cOvfPlOVRBDE6uQdaZdN5R2+97/1i9qLcT9t4x1fJyyXJqC4N0lZxG
|
||||
AGQUmfOx2SLZzaiSqhwmej/+71gFewiVgdtxD4774zEJuwm+UE1fj5F2PVqdnoPy
|
||||
6cRms+EGZkNIGIBloDcYmpuEMpexsr3E+BUAnSeI++JjF5ZsmydnS8TbKF5pwnnw
|
||||
SVzgJFDhxLyhBax7QG0AtMJBP6dYuC/FXJuluwme8f7rsIU5/agK70XEeOtlKsLP
|
||||
Xzze41xNG/cLJyuqC0J3U095ah2H2QIDAQABo4H4MIH1MA4GA1UdDwEB/wQEAwIB
|
||||
hjAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwEgYDVR0TAQH/BAgwBgEB
|
||||
/wIBADAdBgNVHQ4EFgQUxc9GpOr0w8B6bJXELbBeki8m47kwHwYDVR0jBBgwFoAU
|
||||
ebRZ5nu25eQBc4AIiMgaWPbpm24wMgYIKwYBBQUHAQEEJjAkMCIGCCsGAQUFBzAC
|
||||
hhZodHRwOi8veDEuaS5sZW5jci5vcmcvMBMGA1UdIAQMMAowCAYGZ4EMAQIBMCcG
|
||||
A1UdHwQgMB4wHKAaoBiGFmh0dHA6Ly94MS5jLmxlbmNyLm9yZy8wDQYJKoZIhvcN
|
||||
AQELBQADggIBAE7iiV0KAxyQOND1H/lxXPjDj7I3iHpvsCUf7b632IYGjukJhM1y
|
||||
v4Hz/MrPU0jtvfZpQtSlET41yBOykh0FX+ou1Nj4ScOt9ZmWnO8m2OG0JAtIIE38
|
||||
01S0qcYhyOE2G/93ZCkXufBL713qzXnQv5C/viOykNpKqUgxdKlEC+Hi9i2DcaR1
|
||||
e9KUwQUZRhy5j/PEdEglKg3l9dtD4tuTm7kZtB8v32oOjzHTYw+7KdzdZiw/sBtn
|
||||
UfhBPORNuay4pJxmY/WrhSMdzFO2q3Gu3MUBcdo27goYKjL9CTF8j/Zz55yctUoV
|
||||
aneCWs/ajUX+HypkBTA+c8LGDLnWO2NKq0YD/pnARkAnYGPfUDoHR9gVSp/qRx+Z
|
||||
WghiDLZsMwhN1zjtSC0uBWiugF3vTNzYIEFfaPG7Ws3jDrAMMYebQ95JQ+HIBD/R
|
||||
PBuHRTBpqKlyDnkSHDHYPiNX3adPoPAcgdF3H2/W0rmoswMWgTlLn1Wu0mrks7/q
|
||||
pdWfS6PJ1jty80r2VKsM/Dj3YIDfbjXKdaFU5C+8bhfJGqU3taKauuz0wHVGT3eo
|
||||
6FlWkWYtbt4pgdamlwVeZEW+LM7qZEJEsMNPrfC03APKmZsJgpWCDWOKZvkZcvjV
|
||||
uYkQ4omYCTX5ohy+knMjdOmdH9c7SpqEWBDC86fiNex+O0XOMEZSa8DA
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCYXKbejo2pQKny
|
||||
NpgPCq2czi7NQkooXDlqj4t7SEk4Tw0AtauGNPIal+pBWU5rHKeUuKEDGz/LqcCy
|
||||
d9YfhBvbCa6jCPGmymgUVXES5cnHJKw5wc1+kjJq1dheQ55TSCeZ1PVdZNyfDuhh
|
||||
4i8p2tREh2mxnRPY24DZrhBydkAwlqABeC7aarCDj9gPb2uh/EcJ51Q+geQhRVGS
|
||||
lLYQQS7+gNVL/WdKuSH0eGS07yw5SnsPTSRrCLTemKOmxZqVzGQKRM3vkz8zRfaH
|
||||
QbW+eL/y3udvbJbKdbuwACzlZbJOXT64DKb5Le8ARWJozEuV+lgf2RZkmWzZgQb1
|
||||
L/INymnLAgMBAAECggEABNxFISnAWyXfm1p31K2tMX4NACnyiQG9fdNrylhZUvKH
|
||||
96h+pZ61b85o1VeD/jwWcdLHUCdjtvDeCijIgTjgVUckdbmopn7OfdeRQ95mEXRE
|
||||
tOhPJeUuto8b1+X6/FfQ65dfkcXQmUJgfL7lF5tjf3bSaFgadKXhS7sXII+lJzzv
|
||||
ljYTy2b2PzycXaqNTMovamiAtAukt6lo2NjN2NC5GynJJ8LWEOoFKHbOPEh1gUnK
|
||||
7Zvwgg1bm+FRNGiHiKJ7QAsgW3Ua90vqLt1qYZbPH01l9NlrFK/H8QE/GDiLL5Tu
|
||||
anWqlq1PGJHdnBCX+A+Txz0//SVk8lCKP2fgj1HQAQKBgQDOZL4EQkXWEeDDQPNW
|
||||
Zvo2D3K+SvqmJTXh01XwoexyOUgAxE1MgzRLQmAUoQo9wH6PUZlLS1YKOsBkeHov
|
||||
PyHVxN2oJNgSjLpOGjUm0juBhfYr3LL0TeRF9YurO7IUecyw+crzNGwnqje/f7n9
|
||||
SjROsdNeha0sLZ4dNJhNgCPYuwKBgQC8+2FuVN4e6NgcCbggvVtY59poqN+jdRsh
|
||||
Gni6IXHjzgLAGIFZqsYN5tWIhA3a/htnkcJyL6H5LquK406HRxKR4VtX7KyscCbn
|
||||
wlKcGx0oIEYhLOyVAR2gu/E6oEbKjLatKio6GAjok1LB25PFZkczC0jAZMtX5Afn
|
||||
zL2GSMnqMQKBgFg8dYf4lUapqf+PmviLWdkWzaVRBMtXCSwcX5dagm1q07+QLMPT
|
||||
K94o6E6pcmloDDNVXUX1VTlWWL4bS5E7Wkm7uk+SQNXdWCDfz21jX5FGJjImTlNn
|
||||
oXnPOgDgqodacwoOIJfNB5gFi4PRJUCGIsqp94VnfNtwPTKbM6meaLTVAoGAVObO
|
||||
z+Wa4OIU7QvEyBiqKFgJfImZ53KeHJIq+Nw7sW+FNs4LlsAtOGOjPTCulNsibrZC
|
||||
WFBkAXHhKYWTax0YD6fiBK9UqCe+otJfkLhxsexF9XOcWhjlOagV6RPGmgr7qvJN
|
||||
hEn1/p7pSCSgz8dyZ1FDfwQJgtP0ZURLRUAATpECgYBX8hdnTVQ7rr+6vfpQ+yGg
|
||||
QGGE3XySTe/LF8RIZju4aqyy9ejOnT/B0V8PcfMOXCcGJEvOtLwhWVmfx302xGdF
|
||||
HOt/GnIXzjnH+V+DjpgXNmZdl6x9iblc08kAULFWXMxvzqyEIaYvJ8suVPQnGI9Q
|
||||
Ls1fbWv3O/tHE3r1G8APXw==
|
||||
-----END PRIVATE KEY-----
|
||||
Reference in New Issue
Block a user