chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+972
View File
@@ -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;
}
}
+44
View File
@@ -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;
}
}
+435
View File
@@ -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);
}
}
}
}
+265
View 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;
}
}
+322
View File
@@ -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;
}
}
+471
View File
@@ -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 . ' &lt;类名&gt;</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);
}
}
}
}
+95
View 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");
}
}
+131
View File
@@ -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/backupAddonService 备份到 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();
}
}
}
+73
View File
@@ -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(){}
}
+26
View File
@@ -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%}');
}
}
+84
View File
@@ -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()
{}
//
}
+104
View File
@@ -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)
{
//
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
declare (strict_types = 1);
namespace {%namespace%};
class {%className%}
{
public function __construct()
{
}
}
+30
View File
@@ -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' => [],
];
+20
View File
@@ -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');
}
}
+19
View File
@@ -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)
{
//
}
}
+31
View File
@@ -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',
];
}
}
+27
View File
@@ -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()
{
//
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
declare (strict_types = 1);
namespace {%namespace%};
class {%className%}
{
}
+25
View File
@@ -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 = [];
}
+64
View File
@@ -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();
}
+27
View File
@@ -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%}';
}
+26
View File
@@ -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%}');
}
}
+85
View File
@@ -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)
{
//
}
}
+29
View File
@@ -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()
{}
//
}
+105
View File
@@ -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)
{
//
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
declare (strict_types = 1);
namespace {%namespace%};
class {%className%}
{
}
+17
View File
@@ -0,0 +1,17 @@
<?php
declare (strict_types = 1);
namespace {%namespace%};
class {%className%}
{
/**
* 事件监听处理
*
* @return mixed
*/
public function handle($event)
{
//
}
}
+19
View File
@@ -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)
{
//
}
}
+28
View File
@@ -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'],
];
}
}
+27
View File
@@ -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()
{
//
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
declare (strict_types = 1);
namespace {%namespace%};
class {%className%}
{
}
+25
View File
@@ -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 = [];
}
+59
View File
@@ -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-----
+28
View File
@@ -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-----