mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-08-30 12:45:32 +08:00
feat: 发布智能体版
This commit is contained in:
23
extend/base/common/command/CommandBase.php
Normal file
23
extend/base/common/command/CommandBase.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\command;
|
||||
|
||||
use think\console\Command;
|
||||
|
||||
/**
|
||||
* 命令基类(extend/base 层)
|
||||
*
|
||||
* 提供所有命令的基础功能
|
||||
* 命令行(CLI)只使用文本输出
|
||||
* JSON 输出由 Web API 统一处理
|
||||
*/
|
||||
abstract class CommandBase extends Command
|
||||
{
|
||||
/**
|
||||
* 配置命令参数
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace base\common\command;
|
||||
|
||||
use app\admin\service\curd\BuildCurdService;
|
||||
use app\common\console\Command;
|
||||
use app\common\tools\PathTools;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
@@ -15,6 +15,8 @@ class CurdBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('curd')
|
||||
->addOption('table', 't', Option::VALUE_REQUIRED, '主表名', null)
|
||||
->addOption('controllerFilename', 'c', Option::VALUE_REQUIRED, '控制器文件名', null)
|
||||
@@ -22,12 +24,55 @@ class CurdBase extends Command
|
||||
//
|
||||
->addOption('force', 'f', Option::VALUE_NONE, '强制覆盖模式')
|
||||
->addOption('delete', 'd', Option::VALUE_NONE, '删除模式')
|
||||
->addOption('runtime', 'r', Option::VALUE_NONE, '临时生成')
|
||||
->setDescription('一键curd命令服务');
|
||||
->addOption('runtime', 'r', Option::VALUE_NONE, '临时生成(生成到 runtime 目录)')
|
||||
->addOption('examples', null, Option::VALUE_NONE, '显示使用示例')
|
||||
->setDescription('一键生成 CURD 代码(控制器、模型、视图、JS)');
|
||||
}
|
||||
|
||||
protected function getExamplesText(): string
|
||||
{
|
||||
return <<<'EXAMPLES'
|
||||
<info>CURD 命令使用示例</info>
|
||||
|
||||
<comment>示例 1:首次生成(基本用法)</comment>
|
||||
<info>php think curd -t daka_record</info>
|
||||
说明:直接生成到项目目录,如文件已存在会跳过
|
||||
|
||||
<comment>示例 2:增量更新(已有业务代码)</comment>
|
||||
<info>php think curd -t daka_record -r</info>
|
||||
说明:生成到 runtime 临时目录,用于手动对比并合并新增字段代码(避免覆盖业务逻辑)
|
||||
|
||||
<comment>示例 3:带前缀的表名</comment>
|
||||
<info>php think curd -t ul_daka_record</info>
|
||||
说明:自动识别并去除前缀
|
||||
|
||||
<comment>示例 4:强制覆盖(谨慎使用)</comment>
|
||||
<info>php think curd -t daka_record -f</info>
|
||||
说明:覆盖已存在的文件,<error>会丢失手动修改的内容</error>
|
||||
|
||||
<comment>示例 5:删除已生成的文件</comment>
|
||||
<info>php think curd -t daka_record -d</info>
|
||||
说明:删除之前生成的所有文件(会要求确认)
|
||||
注意:<error>这是删除文件,不是删除数据库记录</error>
|
||||
|
||||
<comment>常见错误及解决:</comment>
|
||||
错误:表不存在
|
||||
→ 先创建表:php think scheme:sync
|
||||
→ 或使用迁移:php think migrate:run
|
||||
|
||||
错误:Scheme 与数据库不一致
|
||||
→ 同步 Scheme:php think scheme:sync
|
||||
EXAMPLES;
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
// 显示使用示例
|
||||
if ($input->hasOption('examples')) {
|
||||
$output->writeln($this->getExamplesText());
|
||||
return true;
|
||||
}
|
||||
|
||||
$table = $input->getOption('table');
|
||||
$controllerFilename = $input->getOption('controllerFilename');
|
||||
$modelFilename = $input->getOption('modelFilename');
|
||||
@@ -49,11 +94,15 @@ class CurdBase extends Command
|
||||
return false;
|
||||
}
|
||||
|
||||
// 收集警告信息
|
||||
$warnings = [];
|
||||
|
||||
try {
|
||||
$build = (new BuildCurdService())
|
||||
->setTable($table)
|
||||
->setForce($force);
|
||||
|
||||
$runtime_path = '';
|
||||
if ($input->hasOption('runtime')) {
|
||||
$runtime_path = App::getRuntimePath() . 'source' . DS . 'build' . DS . date('YmdHis') . DS;
|
||||
PathTools::intiDir($runtime_path . 'a.temp');
|
||||
@@ -70,8 +119,9 @@ class CurdBase extends Command
|
||||
$define = $column['define'];
|
||||
|
||||
if (!isset($define['table'])) {
|
||||
$output->error("关联字段{$field}没有设置关联表名称");
|
||||
|
||||
$error = "关联字段{$field}没有设置关联表名称";
|
||||
$output->error($error);
|
||||
$warnings[] = ['message' => $error];
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -96,6 +146,7 @@ class CurdBase extends Command
|
||||
$build = $build->render();
|
||||
$fileList = $build->getFileList();
|
||||
|
||||
$result = [];
|
||||
if (!$delete) {
|
||||
if ($force) {
|
||||
$output->writeln('>>>>>>>>>>>>>>>');
|
||||
@@ -104,10 +155,9 @@ class CurdBase extends Command
|
||||
}
|
||||
$output->writeln('>>>>>>>>>>>>>>>');
|
||||
|
||||
$ask_force_delete_result = $output->confirm($input, '确定强制生成上方所有文件? 如果文件存在会直接覆盖。');
|
||||
|
||||
if (!$ask_force_delete_result) {
|
||||
throw new Exception('取消文件CURD生成操作');
|
||||
if (!$output->confirm($input, '确定强制生成上方所有文件? 如果文件存在会直接覆盖。', true)) {
|
||||
$output->comment('已取消。');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$result = $build->create();
|
||||
@@ -119,10 +169,9 @@ class CurdBase extends Command
|
||||
}
|
||||
$output->writeln('>>>>>>>>>>>>>>>');
|
||||
|
||||
$ask_force_delete_result = $output->confirm($input, '确定删除上方所有文件? ');
|
||||
|
||||
if (!$ask_force_delete_result) {
|
||||
throw new Exception('取消删除文件操作');
|
||||
if (!$output->confirm($input, '确定删除上方所有文件? ', true)) {
|
||||
$output->comment('已取消。');
|
||||
return false;
|
||||
}
|
||||
$result = $build->delete();
|
||||
$output->info('>>>>>>>>>>>>>>>');
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace base\common\command;
|
||||
|
||||
use app\common\console\Command;
|
||||
use app\common\service\UploadService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\facade\Filesystem;
|
||||
@@ -13,37 +13,53 @@ class OssStaticBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('OssStatic')
|
||||
->setDescription('将静态资源上传到oss上');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$output->writeln('========正在上传静态资源到OSS上:========' . date('Y-m-d H:i:s'));
|
||||
try {
|
||||
$start_time = date('Y-m-d H:i:s');
|
||||
$list = Filesystem::disk('local_static')->listContents('/', true);
|
||||
$upload_service = new UploadService();
|
||||
$uploadPrefix = config('app.oss_static_prefix', 'oss_static_prefix');
|
||||
|
||||
$list = Filesystem::disk('local_static')->listContents('/', true);
|
||||
$upload_service = new UploadService();
|
||||
$uploadPrefix = config('app.oss_static_prefix', 'oss_static_prefix');
|
||||
$success_count = 0;
|
||||
$failed_count = 0;
|
||||
|
||||
foreach ($list as $file_item) {
|
||||
if ($file_item['type'] != 'file') {
|
||||
continue;
|
||||
foreach ($list as $file_item) {
|
||||
if ($file_item['type'] != 'file') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$file_path = $file_item['path'];
|
||||
|
||||
$file_path = Filesystem::disk('local_static')->path($file_path);
|
||||
|
||||
$file = new File($file_path, false);
|
||||
|
||||
$save_name = $file_item['path'];
|
||||
try {
|
||||
$model_file = $upload_service->save($file, $save_name, true, $uploadPrefix, true);
|
||||
$success_count++;
|
||||
$output->info('文件上传成功:' . $save_name . '。上传地址:' . $model_file['url']);
|
||||
} catch (\Throwable $th) {
|
||||
$failed_count++;
|
||||
$output->error('文件上传失败:' . $save_name . '。错误信息:' . $th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$file_path = $file_item['path'];
|
||||
// 文本模式输出
|
||||
$output->writeln('========正在上传静态资源到OSS上:========' . $start_time);
|
||||
$output->writeln('========已完成静态资源上传到OSS上:========' . date('Y-m-d H:i:s'));
|
||||
$output->writeln('总计: ' . ($success_count + $failed_count) . ' 个文件,成功: ' . $success_count . ' 个,失败: ' . $failed_count . ' 个');
|
||||
|
||||
$file_path = Filesystem::disk('local_static')->path($file_path);
|
||||
|
||||
$file = new File($file_path, false);
|
||||
|
||||
$save_name = $file_item['path'];
|
||||
try {
|
||||
$model_file = $upload_service->save($file, $save_name, true, $uploadPrefix, true);
|
||||
$output->info('文件上传成功:' . $save_name . '。上传地址:' . $model_file['url']);
|
||||
} catch (\Throwable $th) {
|
||||
$output->error('文件上传失败:' . $save_name . '。错误信息:' . $th->getMessage());
|
||||
}
|
||||
return $failed_count > 0 ? 1 : 0;
|
||||
} catch (\Throwable $e) {
|
||||
throw $e;
|
||||
}
|
||||
$output->writeln('========已完成静态资源上传到OSS上:========' . date('Y-m-d H:i:s'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace base\common\command;
|
||||
|
||||
use app\common\console\Command;
|
||||
use app\common\interface\test\CommandTestInterface;
|
||||
use app\common\service\test\LogTestService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
@@ -4,11 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace base\common\command;
|
||||
|
||||
use app\common\console\Command;
|
||||
use app\common\service\HostService;
|
||||
use app\common\service\TimerService;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Promise\Utils;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
@@ -29,6 +29,8 @@ class TimerBase extends Command
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
// 指令配置
|
||||
$this->setName('timer')
|
||||
->addOption('temp', null, Option::VALUE_NONE)
|
||||
@@ -41,52 +43,57 @@ class TimerBase extends Command
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
// 指令输出
|
||||
$output->writeln('start timer');
|
||||
try {
|
||||
// 指令输出
|
||||
$output->writeln('start timer');
|
||||
|
||||
$site_domain = sysconfig('site', 'site_domain');
|
||||
if (empty($site_domain)) {
|
||||
$output->writeln('请前往后台设置站点域名(site_domain)配置项');
|
||||
$site_domain = sysconfig('site', 'site_domain');
|
||||
if (empty($site_domain)) {
|
||||
$output->writeln('请前往后台设置站点域名(site_domain)配置项');
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$host = $site_domain;
|
||||
$host = $site_domain;
|
||||
|
||||
if ($input->hasOption('local')) {
|
||||
$host = $input->getOption('local-host') . ':' . $input->getOption('local-port');
|
||||
}
|
||||
if ($input->hasOption('local')) {
|
||||
$host = $input->getOption('local-host') . ':' . $input->getOption('local-port');
|
||||
}
|
||||
|
||||
$output->writeln('站点域名:' . $host);
|
||||
$site_host = parse_url($host, PHP_URL_HOST);
|
||||
$output->writeln('站点域名:' . $host);
|
||||
$site_host = parse_url($host, PHP_URL_HOST);
|
||||
|
||||
// 设置配置的任务
|
||||
$timer_service = new TimerService();
|
||||
$request_list = $timer_service->generateAllRequestList();
|
||||
$call_list = $timer_service->generateAllCallList();
|
||||
// 设置配置的任务
|
||||
$timer_service = new TimerService();
|
||||
$request_list = $timer_service->generateAllRequestList();
|
||||
$call_list = $timer_service->generateAllCallList();
|
||||
|
||||
// 内置的节点注册任务
|
||||
$system_host_register =
|
||||
[
|
||||
'name' => 'system_host_register', // 定时任务的名称,不能重复
|
||||
'type' => 'call', // 定时任务的类型,默认只支持site,你也可以重写定时器命令行以支持其他命令
|
||||
'target' => [HostService::class, 'heartbeat'], // 要访问的地址,如果不是以https开头,那么以后台的系统配置中读取相关配置,如果没有配置则不执行
|
||||
'frequency' => 30, // 执行频率,单位:秒,填写10,则每10秒过后执行一次
|
||||
];
|
||||
$system_host_call_list = TimerService::generateTaskInstanceFromConfig($system_host_register);
|
||||
$call_list = array_merge($call_list, $system_host_call_list);
|
||||
// 内置的节点注册任务
|
||||
$system_host_register =
|
||||
[
|
||||
'name' => 'system_host_register', // 定时任务的名称,不能重复
|
||||
'type' => 'call', // 定时任务的类型,默认只支持site,你也可以重写定时器命令行以支持其他命令
|
||||
'target' => [HostService::class, 'heartbeat'], // 要访问的地址,如果不是以https开头,那么以后台的系统配置中读取相关配置,如果没有配置则不执行
|
||||
'frequency' => 30, // 执行频率,单位:秒,填写10,则每10秒过后执行一次
|
||||
];
|
||||
$system_host_call_list = TimerService::generateTaskInstanceFromConfig($system_host_register);
|
||||
$call_list = array_merge($call_list, $system_host_call_list);
|
||||
|
||||
$this->host = $host;
|
||||
$this->siteDomain = $site_domain;
|
||||
$this->siteHost = $site_host;
|
||||
$this->requestList = $request_list;
|
||||
$this->callList = $call_list;
|
||||
$this->host = $host;
|
||||
$this->siteDomain = $site_domain;
|
||||
$this->siteHost = $site_host;
|
||||
$this->requestList = $request_list;
|
||||
$this->callList = $call_list;
|
||||
|
||||
$timer_mode = Config::get('timer.mode', 'normal');
|
||||
if ($timer_mode == 'normal') {
|
||||
$this->runNormal();
|
||||
} else {
|
||||
$this->runParallel();
|
||||
// 文本模式:正常运行定时器
|
||||
$timer_mode = Config::get('timer.mode', 'normal');
|
||||
if ($timer_mode == 'normal') {
|
||||
$this->runNormal();
|
||||
} else {
|
||||
$this->runParallel();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin;
|
||||
|
||||
use think\console\Command;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\facade\App;
|
||||
@@ -13,6 +13,8 @@ class ClearBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
// 指令配置
|
||||
$this->setName('admin:clear')
|
||||
->setDescription('删除开发临时生成目录');
|
||||
@@ -20,31 +22,34 @@ class ClearBase extends Command
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
// 指令输出
|
||||
$output->writeln('删除测试目录');
|
||||
|
||||
$dir = App::getRootPath() . '/runtime/source/';
|
||||
$deleted = false;
|
||||
$message = '';
|
||||
$command_line = '';
|
||||
|
||||
$dir = App::getRootPath() . '/runtime/source/';
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
$output->writeln('删除成功');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (strpos(strtolower(PHP_OS), 'win') === 0) {
|
||||
$command_line = implode(' ', ['rd', '/s', '/q', str_replace('/', '\\', $dir)]);
|
||||
$deleted = true;
|
||||
$message = '目录不存在,无需删除';
|
||||
} else {
|
||||
$command_line = implode(' ', ['rm', '-rf', $dir]);
|
||||
if (strpos(strtolower(PHP_OS), 'win') === 0) {
|
||||
$command_line = implode(' ', ['rd', '/s', '/q', str_replace('/', '\\', $dir)]);
|
||||
} else {
|
||||
$command_line = implode(' ', ['rm', '-rf', $dir]);
|
||||
}
|
||||
|
||||
exec($command_line);
|
||||
$deleted = !is_dir($dir);
|
||||
$message = $deleted ? '删除成功' : '删除失败';
|
||||
}
|
||||
|
||||
$output->info('删除目录:' . $command_line);
|
||||
// 文本模式输出
|
||||
$output->writeln('删除测试目录');
|
||||
if ($command_line) {
|
||||
$output->info('删除目录:' . $command_line);
|
||||
$output->info('run command: ' . $command_line);
|
||||
}
|
||||
$output->info($message);
|
||||
|
||||
$output->info('run command: ' . $command_line);
|
||||
|
||||
exec($command_line);
|
||||
|
||||
$output->info('删除成功');
|
||||
return $deleted ? 0 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace base\common\command\admin;
|
||||
|
||||
use app\admin\service\AdminUpdateService;
|
||||
use think\console\Command;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
@@ -17,11 +17,13 @@ class UpdateBase extends Command
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
// 指令配置
|
||||
$this->setName('admin:update')
|
||||
->addOption('reinstall', null, Option::VALUE_NONE, '重装版本')
|
||||
->addOption('update-ulthon', null, Option::VALUE_NONE, '重装版本')
|
||||
->setDescription('the admin:update command');
|
||||
->addOption('update-ulthon', null, Option::VALUE_NONE, '更新 ulthon_admin')
|
||||
->setDescription('更新系统代码');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
@@ -38,6 +40,7 @@ class UpdateBase extends Command
|
||||
$update_service = new AdminUpdateService($repo);
|
||||
$update_service->input = $input;
|
||||
$update_service->output = $output;
|
||||
|
||||
$update_service->update();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin;
|
||||
|
||||
use app\common\console\Command;
|
||||
use app\common\tools\PathTools;
|
||||
use think\App as ThinkApp;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
@@ -85,6 +85,8 @@ class VersionBase extends Command
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
// 指令配置
|
||||
$this->setName('admin:version')
|
||||
->addOption('generate-comment', null, Option::VALUE_NONE, '使用git命令生成说明文件')
|
||||
@@ -96,6 +98,7 @@ class VersionBase extends Command
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
// 文本模式输出
|
||||
// 指令输出
|
||||
if (!empty(static::PRODUCT_VERSION)) {
|
||||
$output->info('当前版本号为:' . static::PRODUCT_VERSION);
|
||||
|
||||
106
extend/base/common/command/admin/menu/AdminMenuCreateBase.php
Normal file
106
extend/base/common/command/admin/menu/AdminMenuCreateBase.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\menu;
|
||||
|
||||
use app\admin\model\SystemMenu;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
/**
|
||||
* admin:menu:create command - 创建菜单
|
||||
*/
|
||||
class AdminMenuCreateBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
// 命令配置
|
||||
$this->setName('admin:menu:create')
|
||||
->addOption('title', null, Option::VALUE_REQUIRED, '菜单标题(必填)')
|
||||
->addOption('path', null, Option::VALUE_OPTIONAL, '菜单路径', '')
|
||||
->addOption('icon', null, Option::VALUE_OPTIONAL, '菜单图标', '')
|
||||
->addOption('parent-id', null, Option::VALUE_OPTIONAL, '父菜单ID(默认为0)', 0)
|
||||
->addOption('sort', null, Option::VALUE_OPTIONAL, '排序(默认为100)', 100)
|
||||
->addOption('node', null, Option::VALUE_OPTIONAL, '权限节点', '')
|
||||
->addOption('remark', null, Option::VALUE_OPTIONAL, '备注说明', '')
|
||||
->setDescription('创建菜单');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
// 获取参数
|
||||
$title = $input->getOption('title');
|
||||
$path = $input->getOption('path');
|
||||
$icon = $input->getOption('icon');
|
||||
$parentId = $input->getOption('parent-id');
|
||||
$sort = $input->getOption('sort');
|
||||
$node = $input->getOption('node');
|
||||
$remark = $input->getOption('remark');
|
||||
|
||||
// 验证必填参数
|
||||
if (empty($title)) {
|
||||
$output->error('菜单标题不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 准备菜单数据
|
||||
$menuData = [
|
||||
'title' => $title,
|
||||
'path' => $path,
|
||||
'icon' => $icon,
|
||||
'parent_id' => (int)$parentId,
|
||||
'sort' => (int)$sort,
|
||||
'node' => $node,
|
||||
'remark' => $remark,
|
||||
];
|
||||
|
||||
// 实例化菜单服务(使用 app\admin\service\MenuService,它继承自 base\common\service\MenuServiceBase)
|
||||
// 由于 MenuServiceBase 需要 adminId 参数,我们使用 0 表示命令行操作
|
||||
$menuService = new \app\common\service\MenuService(0);
|
||||
|
||||
// 创建菜单
|
||||
$menuId = $menuService->create($menuData);
|
||||
|
||||
// 获取创建的菜单详情
|
||||
$menuModel = SystemMenu::find($menuId);
|
||||
$menu = $menuModel ? $menuModel->toArray() : null;
|
||||
|
||||
if (empty($menu)) {
|
||||
throw new \Exception('菜单创建成功,但无法获取菜单详情');
|
||||
}
|
||||
|
||||
$outputData = [
|
||||
'id' => (int)$menu['id'],
|
||||
'title' => $menu['title'] ?? '',
|
||||
'path' => $menu['href'] ?? '',
|
||||
'icon' => $menu['icon'] ?? '',
|
||||
'parent_id' => (int)($menu['pid'] ?? 0),
|
||||
'sort' => (int)($menu['sort'] ?? 0),
|
||||
'node' => $menu['auth_node'] ?? '',
|
||||
];
|
||||
|
||||
// 输出结果
|
||||
$output->info('菜单创建成功');
|
||||
$output->info('菜单ID: ' . $outputData['id']);
|
||||
$output->info('菜单标题: ' . $outputData['title']);
|
||||
$output->info('菜单路径: ' . $outputData['path']);
|
||||
$output->info('菜单图标: ' . $outputData['icon']);
|
||||
$output->info('父菜单ID: ' . $outputData['parent_id']);
|
||||
$output->info('排序: ' . $outputData['sort']);
|
||||
if (!empty($outputData['node'])) {
|
||||
$output->info('权限节点: ' . $outputData['node']);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$output->error('创建菜单失败: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\menu;
|
||||
|
||||
use app\admin\model\SystemMenu;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
/**
|
||||
* admin:menu:delete command - 删除菜单
|
||||
*/
|
||||
class AdminMenuDeleteBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
// 指令配置
|
||||
$this->setName('admin:menu:delete')
|
||||
->addOption('id', null, Option::VALUE_REQUIRED, '菜单ID')
|
||||
->setDescription('删除菜单');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
// 获取参数
|
||||
$id = $input->getOption('id');
|
||||
|
||||
// 验证参数
|
||||
if (empty($id)) {
|
||||
$output->error('菜单ID不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 验证菜单是否存在
|
||||
$menu = SystemMenu::find($id);
|
||||
if (empty($menu)) {
|
||||
$output->error('菜单ID ' . $id . ' 不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 检查是否有子菜单
|
||||
$childCount = SystemMenu::where('pid', $id)
|
||||
->where('delete_time', 0)
|
||||
->count();
|
||||
|
||||
if ($childCount > 0) {
|
||||
$output->error('菜单ID ' . $id . ' 存在 ' . $childCount . ' 个子菜单,请先删除子菜单');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 保存菜单信息用于输出
|
||||
$menuInfo = [
|
||||
'id' => (int)$menu->id,
|
||||
'title' => $menu->title,
|
||||
'path' => $menu->href ?? '',
|
||||
'icon' => $menu->icon ?? '',
|
||||
'parent_id' => (int)($menu->pid ?? 0),
|
||||
'sort' => (int)($menu->sort ?? 0),
|
||||
'node' => $menu->auth_node ?? '',
|
||||
];
|
||||
|
||||
// 4. 执行删除(软删除)
|
||||
$menu->delete();
|
||||
|
||||
// 5. 输出结果
|
||||
$output->info('菜单删除成功');
|
||||
$output->info('菜单ID: ' . $menuInfo['id']);
|
||||
$output->info('菜单名称: ' . $menuInfo['title']);
|
||||
$output->info('菜单路径: ' . $menuInfo['path']);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$output->error('删除菜单失败: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
127
extend/base/common/command/admin/menu/AdminMenuExportBase.php
Normal file
127
extend/base/common/command/admin/menu/AdminMenuExportBase.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\menu;
|
||||
|
||||
use app\admin\model\SystemMenu;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
/**
|
||||
* admin:menu:export command - 导出菜单
|
||||
*/
|
||||
class AdminMenuExportBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
// 指令配置
|
||||
$this->setName('admin:menu:export')
|
||||
->setDescription('导出菜单数据')
|
||||
->addOption('format', null, Option::VALUE_OPTIONAL, '输出格式(text/json)', 'text')
|
||||
->addOption('output', null, Option::VALUE_OPTIONAL, '输出文件路径(可选)');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
// 获取参数
|
||||
$outputPath = $input->getOption('output');
|
||||
|
||||
try {
|
||||
// 1. 查询所有菜单
|
||||
$menus = SystemMenu::where('delete_time', 0)
|
||||
->order(['sort' => 'desc', 'id' => 'asc'])
|
||||
->select()
|
||||
->hidden(['create_time', 'update_time', 'delete_time']);
|
||||
|
||||
// 2. 格式化菜单数据
|
||||
$menuData = [];
|
||||
foreach ($menus as $menu) {
|
||||
$menuData[] = [
|
||||
'id' => (int)$menu->id,
|
||||
'pid' => (int)($menu->pid ?? 0),
|
||||
'title' => $menu->title ?? '',
|
||||
'href' => $menu->href ?? '',
|
||||
'icon' => $menu->icon ?? '',
|
||||
'sort' => (int)($menu->sort ?? 0),
|
||||
'auth_node' => $menu->auth_node ?? '',
|
||||
'status' => (int)($menu->status ?? 0),
|
||||
'type' => (int)($menu->type ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
// JSON 模式输出
|
||||
if ($input->getOption('format') === 'json') {
|
||||
if (!empty($outputPath)) {
|
||||
$jsonData = json_encode($menuData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
if ($jsonData === false) {
|
||||
throw new \RuntimeException('JSON 编码失败');
|
||||
}
|
||||
|
||||
$result = file_put_contents($outputPath, $jsonData);
|
||||
if ($result === false) {
|
||||
throw new \RuntimeException('无法写入文件: ' . $outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
$json = json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'menus' => $menuData,
|
||||
'output' => !empty($outputPath) ? $outputPath : null,
|
||||
],
|
||||
'warnings' => [],
|
||||
'metadata' => [
|
||||
'count' => count($menuData),
|
||||
'exported_at' => date('c'),
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$output->writeln($json);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. 如果指定了输出路径,写入文件
|
||||
if (!empty($outputPath)) {
|
||||
$jsonData = json_encode($menuData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
$result = file_put_contents($outputPath, $jsonData);
|
||||
|
||||
if ($result === false) {
|
||||
$output->error('无法写入文件: ' . $outputPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
$output->info('成功导出 ' . count($menuData) . ' 个菜单到文件: ' . $outputPath);
|
||||
} else {
|
||||
// 4. 输出结果到控制台
|
||||
$output->info('成功导出 ' . count($menuData) . ' 个菜单');
|
||||
|
||||
// 以表格形式输出菜单数据
|
||||
$output->writeln('菜单ID | 父菜单ID | 菜单标题 | 菜单路径 | 图标 | 排序 | 权限节点 | 状态 | 类型');
|
||||
$output->writeln('--------|----------|----------|----------|------|------|----------|------|------');
|
||||
|
||||
foreach ($menuData as $menu) {
|
||||
$output->writeln($menu['id'] . ' | ' .
|
||||
$menu['pid'] . ' | ' .
|
||||
$menu['title'] . ' | ' .
|
||||
$menu['href'] . ' | ' .
|
||||
$menu['icon'] . ' | ' .
|
||||
$menu['sort'] . ' | ' .
|
||||
$menu['auth_node'] . ' | ' .
|
||||
$menu['status'] . ' | ' .
|
||||
$menu['type']);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$output->error('导出菜单失败: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
132
extend/base/common/command/admin/menu/AdminMenuListBase.php
Normal file
132
extend/base/common/command/admin/menu/AdminMenuListBase.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\menu;
|
||||
|
||||
use app\admin\model\SystemMenu;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
class AdminMenuListBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('admin:menu:list')
|
||||
->setDescription('列出所有菜单(树形结构)')
|
||||
->addOption('format', null, Option::VALUE_OPTIONAL, '输出格式(tree/table/json)', 'tree')
|
||||
->addOption('status', null, Option::VALUE_OPTIONAL, '筛选状态(0=禁用,1=启用,all=全部)', 'all')
|
||||
->addOption('pid', null, Option::VALUE_OPTIONAL, '指定父菜单ID(默认从根菜单开始)', null);
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$format = $input->getOption('format');
|
||||
$statusFilter = $input->getOption('status');
|
||||
$pidFilter = $input->getOption('pid');
|
||||
|
||||
try {
|
||||
$query = SystemMenu::where('delete_time', 0);
|
||||
|
||||
if ($statusFilter !== 'all') {
|
||||
$query->where('status', (int)$statusFilter);
|
||||
}
|
||||
|
||||
$menus = $query->order(['sort' => 'desc', 'id' => 'asc'])
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
if (empty($menus)) {
|
||||
$output->comment('没有找到菜单数据');
|
||||
return true;
|
||||
}
|
||||
|
||||
$menuTree = $this->buildTree($menus, $pidFilter ? (int)$pidFilter : 0);
|
||||
|
||||
switch ($format) {
|
||||
case 'json':
|
||||
$this->outputJson($output, $menuTree, count($menus));
|
||||
break;
|
||||
case 'table':
|
||||
$this->outputTable($output, $menus);
|
||||
break;
|
||||
case 'tree':
|
||||
default:
|
||||
$this->outputTree($output, $menuTree);
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$output->error('获取菜单列表失败: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function buildTree(array $menus, int $pid = 0): array
|
||||
{
|
||||
$tree = [];
|
||||
foreach ($menus as $menu) {
|
||||
if ((int)$menu['pid'] === $pid) {
|
||||
$children = $this->buildTree($menus, (int)$menu['id']);
|
||||
if (!empty($children)) {
|
||||
$menu['children'] = $children;
|
||||
}
|
||||
$tree[] = $menu;
|
||||
}
|
||||
}
|
||||
return $tree;
|
||||
}
|
||||
|
||||
protected function outputTree(Output $output, array $tree, int $level = 0): void
|
||||
{
|
||||
$prefix = str_repeat(' ', $level);
|
||||
$branch = $level > 0 ? '├─ ' : '';
|
||||
|
||||
foreach ($tree as $node) {
|
||||
$status = $node['status'] ? '<info>●</info>' : '<comment>○</comment>';
|
||||
$href = $node['href'] ?? '-';
|
||||
$output->writeln("{$prefix}{$branch}{$status} [{$node['id']}] {$node['title']} <comment>({$href})</comment>");
|
||||
|
||||
if (!empty($node['children'])) {
|
||||
$this->outputTree($output, $node['children'], $level + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function outputTable(Output $output, array $menus): void
|
||||
{
|
||||
$output->writeln('ID | PID | 标题 | 路径 | 状态');
|
||||
$output->writeln('---|-----|------|------|------');
|
||||
|
||||
foreach ($menus as $menu) {
|
||||
$status = $menu['status'] ? '启用' : '禁用';
|
||||
$output->writeln(sprintf(
|
||||
'%d | %d | %s | %s | %s',
|
||||
$menu['id'],
|
||||
$menu['pid'],
|
||||
$menu['title'],
|
||||
$menu['href'] ?? '-',
|
||||
$status
|
||||
));
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
$output->info('共 ' . count($menus) . ' 个菜单');
|
||||
}
|
||||
|
||||
protected function outputJson(Output $output, array $tree, int $total): void
|
||||
{
|
||||
$data = [
|
||||
'success' => true,
|
||||
'data' => ['menus' => $tree],
|
||||
'metadata' => ['total' => $total],
|
||||
];
|
||||
$output->writeln(json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||
}
|
||||
}
|
||||
129
extend/base/common/command/admin/menu/AdminMenuUpdateBase.php
Normal file
129
extend/base/common/command/admin/menu/AdminMenuUpdateBase.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\menu;
|
||||
|
||||
use app\admin\model\SystemMenu;
|
||||
use app\common\console\Command;
|
||||
use app\common\service\MenuService;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
/**
|
||||
* admin:menu:update command - 更新菜单
|
||||
*/
|
||||
class AdminMenuUpdateBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
// 指令配置
|
||||
$this->setName('admin:menu:update')
|
||||
->addOption('id', null, Option::VALUE_REQUIRED, '菜单ID(必填)')
|
||||
->addOption('title', null, Option::VALUE_OPTIONAL, '菜单标题')
|
||||
->addOption('path', null, Option::VALUE_OPTIONAL, '菜单路径')
|
||||
->addOption('icon', null, Option::VALUE_OPTIONAL, '菜单图标')
|
||||
->addOption('parent-id', null, Option::VALUE_OPTIONAL, '父级菜单ID')
|
||||
->addOption('sort', null, Option::VALUE_OPTIONAL, '排序')
|
||||
->setDescription('编辑菜单');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
// 获取参数
|
||||
$id = (int)($input->getOption('id') ?? 0);
|
||||
$title = $input->getOption('title');
|
||||
$path = $input->getOption('path');
|
||||
$icon = $input->getOption('icon');
|
||||
$parentId = $input->getOption('parent-id');
|
||||
$sort = $input->getOption('sort');
|
||||
|
||||
// 验证参数:id 是必填的
|
||||
if (empty($id)) {
|
||||
$output->error('菜单ID不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 验证菜单是否存在
|
||||
$menu = SystemMenu::find($id);
|
||||
if (empty($menu)) {
|
||||
$output->writeln('<error>菜单ID ' . $id . ' 不存在</error>');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 准备更新数据
|
||||
$updateData = [];
|
||||
|
||||
if ($title !== null) {
|
||||
$updateData['title'] = $title;
|
||||
}
|
||||
|
||||
if ($path !== null) {
|
||||
$updateData['path'] = $path;
|
||||
}
|
||||
|
||||
if ($icon !== null) {
|
||||
$updateData['icon'] = $icon;
|
||||
}
|
||||
|
||||
if ($parentId !== null) {
|
||||
$updateData['parent_id'] = (int)$parentId;
|
||||
}
|
||||
|
||||
if ($sort !== null) {
|
||||
$updateData['sort'] = (int)$sort;
|
||||
}
|
||||
|
||||
// 检查是否有需要更新的字段
|
||||
if (empty($updateData)) {
|
||||
$output->error('没有提供需要更新的字段');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 调用 MenuService 更新菜单
|
||||
$menuService = new MenuService(0);
|
||||
$success = $menuService->update($id, $updateData);
|
||||
|
||||
if (!$success) {
|
||||
$output->writeln('<error>菜单更新失败</error>');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 重新查询菜单数据
|
||||
$updatedMenu = SystemMenu::find($id);
|
||||
if (empty($updatedMenu)) {
|
||||
$output->writeln('<error>菜单更新后查询失败</error>');
|
||||
return false;
|
||||
}
|
||||
|
||||
$outputData = [
|
||||
'id' => (int)$updatedMenu->id,
|
||||
'title' => $updatedMenu->title ?? '',
|
||||
'path' => $updatedMenu->href ?? '',
|
||||
'icon' => $updatedMenu->icon ?? '',
|
||||
'parent_id' => (int)($updatedMenu->pid ?? 0),
|
||||
'sort' => (int)($updatedMenu->sort ?? 0),
|
||||
'node' => $updatedMenu->auth_node ?? '',
|
||||
];
|
||||
|
||||
// 5. 输出结果
|
||||
$output->writeln('<info>菜单编辑成功</info>');
|
||||
$output->writeln('<info>菜单ID: ' . $outputData['id'] . '</info>');
|
||||
$output->writeln('<info>菜单标题: ' . $outputData['title'] . '</info>');
|
||||
$output->writeln('<info>菜单路径: ' . $outputData['path'] . '</info>');
|
||||
$output->writeln('<info>菜单图标: ' . $outputData['icon'] . '</info>');
|
||||
$output->writeln('<info>父级菜单ID: ' . $outputData['parent_id'] . '</info>');
|
||||
$output->writeln('<info>排序: ' . $outputData['sort'] . '</info>');
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$output->error('编辑菜单失败: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\permission;
|
||||
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Input\Option;
|
||||
use think\console\Output;
|
||||
use app\admin\service\NodeService;
|
||||
|
||||
/**
|
||||
* admin:permission:nodes command - 查看权限节点
|
||||
*/
|
||||
class AdminPermissionNodesBase extends Command
|
||||
{
|
||||
/**
|
||||
* 配置命令参数
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
// 指令配置
|
||||
$this->setName('admin:permission:nodes')
|
||||
->setDescription('查看权限节点')
|
||||
->addOption('level', null, Option::VALUE_OPTIONAL, '按级别过滤 (1=控制器, 2=方法)')
|
||||
->addOption('module', null, Option::VALUE_OPTIONAL, '按模块过滤');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*
|
||||
* @param Input $input
|
||||
* @param Output $output
|
||||
* @return int
|
||||
* @throws \Doctrine\Common\Annotations\AnnotationException
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
try {
|
||||
// 获取所有权限节点
|
||||
$nodeService = new NodeService();
|
||||
$nodeList = $nodeService->getNodelist();
|
||||
|
||||
// 过滤条件
|
||||
$filterLevel = $input->getOption('level');
|
||||
$filterModule = $input->getOption('module');
|
||||
|
||||
// 应用过滤
|
||||
$filteredNodes = $nodeList;
|
||||
if ($filterLevel !== null) {
|
||||
$level = (int)$filterLevel;
|
||||
$filteredNodes = array_filter($filteredNodes, function ($node) use ($level) {
|
||||
return isset($node['type']) && $node['type'] === $level;
|
||||
});
|
||||
}
|
||||
|
||||
if ($filterModule !== null) {
|
||||
$filteredNodes = array_filter($filteredNodes, function ($node) use ($filterModule) {
|
||||
return isset($node['module']) && $node['module'] === $filterModule;
|
||||
});
|
||||
}
|
||||
|
||||
// 重新索引数组
|
||||
$filteredNodes = array_values($filteredNodes);
|
||||
|
||||
// 输出表头
|
||||
$output->info('权限节点列表');
|
||||
$output->writeln('================================');
|
||||
|
||||
if (empty($filteredNodes)) {
|
||||
$output->comment('没有找到符合条件的权限节点');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 输出权限节点信息
|
||||
foreach ($filteredNodes as $node) {
|
||||
$levelText = $node['type'] == 1 ? '控制器' : '方法';
|
||||
$authText = isset($node['auth']) && $node['auth'] ? '是' : '否';
|
||||
|
||||
$output->info('节点: ' . ($node['node'] ?? ''));
|
||||
$output->comment(' 标题: ' . ($node['title'] ?? ''));
|
||||
$output->comment(' 级别: ' . $levelText . ' (' . ($node['type'] ?? 0) . ')');
|
||||
$output->comment(' 模块: ' . ($node['module'] ?? ''));
|
||||
$output->comment(' 需要授权: ' . $authText);
|
||||
$output->writeln('--------------------------------');
|
||||
}
|
||||
|
||||
$output->info('总计: ' . count($filteredNodes) . ' 个权限节点');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$output->error('获取权限节点失败: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\permission;
|
||||
|
||||
use app\admin\model\SystemAdmin;
|
||||
use app\admin\model\SystemAuth;
|
||||
use app\admin\model\SystemAuthNode;
|
||||
use app\admin\service\NodeService;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
class AdminPermissionUserBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('admin:permission:user')
|
||||
->addOption('user-id', 'u', Option::VALUE_OPTIONAL, '用户ID', null)
|
||||
->setDescription('查看用户权限');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$userId = $input->getOption('user-id');
|
||||
|
||||
// 验证用户ID参数
|
||||
if (empty($userId)) {
|
||||
$output->error('缺少必要参数 --user-id');
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询用户
|
||||
$user = SystemAdmin::find($userId);
|
||||
|
||||
if (empty($user)) {
|
||||
$output->error('用户不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询用户角色
|
||||
$roles = [];
|
||||
$authIds = $user->auth_ids;
|
||||
$authIdArray = [];
|
||||
|
||||
if (!empty($authIds)) {
|
||||
$authIdArray = array_filter(explode(',', $authIds));
|
||||
|
||||
if (!empty($authIdArray)) {
|
||||
$roles = SystemAuth::whereIn('id', $authIdArray)
|
||||
->where('delete_time', 0)
|
||||
->field('id,title')
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
// 查询用户权限节点
|
||||
$nodes = [];
|
||||
if (!empty($authIdArray)) {
|
||||
$nodes = SystemAuthNode::whereIn('auth_id', $authIdArray)
|
||||
->column('node');
|
||||
}
|
||||
|
||||
// 输出结果
|
||||
$output->info('用户权限信息');
|
||||
$output->info('用户ID: ' . $user->id);
|
||||
$output->info('用户名: ' . $user->username);
|
||||
|
||||
if (!empty($roles)) {
|
||||
$output->info('角色列表:');
|
||||
foreach ($roles as $role) {
|
||||
$output->comment(' - ID: ' . $role['id'] . ', 标题: ' . $role['title']);
|
||||
}
|
||||
} else {
|
||||
$output->comment('用户没有分配任何角色');
|
||||
}
|
||||
|
||||
if (!empty($nodes)) {
|
||||
$output->info('权限节点列表:');
|
||||
foreach ($nodes as $node) {
|
||||
$output->comment(' - ' . $node);
|
||||
}
|
||||
} else {
|
||||
$output->comment('用户没有分配任何权限节点');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\role;
|
||||
|
||||
use app\admin\model\SystemAuth;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
class AdminRoleCreateBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('admin:role:create')
|
||||
->addOption('title', null, Option::VALUE_REQUIRED, '角色名称')
|
||||
->addOption('remark', null, Option::VALUE_OPTIONAL, '角色备注')
|
||||
->setDescription('创建角色');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$title = $input->getOption('title');
|
||||
$remark = $input->getOption('remark');
|
||||
|
||||
if (empty($title)) {
|
||||
$output->error('角色名称不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$auth = new SystemAuth();
|
||||
$auth->title = $title;
|
||||
$auth->remark = $remark ?? '';
|
||||
$auth->sort = 0;
|
||||
$auth->status = 1;
|
||||
$auth->save();
|
||||
|
||||
$output->info('角色创建成功');
|
||||
$output->info('角色ID: ' . $auth->id);
|
||||
$output->info('角色名称: ' . $auth->title);
|
||||
} catch (\Throwable $e) {
|
||||
$output->error('创建角色失败: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\role;
|
||||
|
||||
use app\admin\model\SystemAuth;
|
||||
use app\admin\model\SystemAuthNode;
|
||||
use app\admin\model\SystemAdmin;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
class AdminRoleDeleteBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('admin:role:delete')
|
||||
->addOption('role-id', null, Option::VALUE_REQUIRED, '角色ID')
|
||||
->setDescription('删除角色');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$roleId = $input->getOption('role-id');
|
||||
|
||||
if (empty($roleId)) {
|
||||
$output->error('角色ID不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$auth = SystemAuth::find($roleId);
|
||||
if (empty($auth)) {
|
||||
$output->error('角色ID ' . $roleId . ' 不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
$checkUsers = SystemAdmin::where('auth_ids', 'like', '%' . $roleId . '%')
|
||||
->where('delete_time', 0)
|
||||
->count();
|
||||
|
||||
if ($checkUsers > 0) {
|
||||
$output->error('角色ID ' . $roleId . ' 已分配给 ' . $checkUsers . ' 个用户,无法删除');
|
||||
return false;
|
||||
}
|
||||
|
||||
SystemAuthNode::where('auth_id', $roleId)->delete();
|
||||
$auth->delete();
|
||||
|
||||
$output->info('角色删除成功');
|
||||
$output->info('角色ID: ' . $roleId);
|
||||
} catch (\Throwable $e) {
|
||||
$output->error('删除角色失败: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
65
extend/base/common/command/admin/role/AdminRoleInfoBase.php
Normal file
65
extend/base/common/command/admin/role/AdminRoleInfoBase.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\role;
|
||||
|
||||
use app\admin\model\SystemAuth;
|
||||
use app\admin\model\SystemAuthNode;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
class AdminRoleInfoBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('admin:role:info')
|
||||
->addOption('role-id', null, Option::VALUE_REQUIRED, '角色ID')
|
||||
->setDescription('查看角色详情');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$roleId = $input->getOption('role-id');
|
||||
|
||||
if (empty($roleId)) {
|
||||
$output->error('角色ID不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$role = SystemAuth::find($roleId);
|
||||
if (empty($role)) {
|
||||
$output->error('角色ID ' . $roleId . ' 不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
$nodes = SystemAuthNode::where('auth_id', $roleId)
|
||||
->column('node');
|
||||
|
||||
$output->info('角色详情');
|
||||
$output->writeln('================================');
|
||||
$output->info('角色ID: ' . $role->id);
|
||||
$output->comment('角色名称: ' . $role->title);
|
||||
$output->comment('状态: ' . ($role->status == 1 ? '启用' : '禁用'));
|
||||
$output->comment('排序: ' . $role->sort);
|
||||
if (!empty($role->remark)) {
|
||||
$output->comment('备注: ' . $role->remark);
|
||||
}
|
||||
$output->writeln('--------------------------------');
|
||||
$output->info('权限节点列表 (' . count($nodes) . ' 个):');
|
||||
foreach ($nodes as $node) {
|
||||
$output->comment(' - ' . $node);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$output->error('获取角色详情失败: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
64
extend/base/common/command/admin/role/AdminRoleListBase.php
Normal file
64
extend/base/common/command/admin/role/AdminRoleListBase.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\role;
|
||||
|
||||
use app\admin\model\SystemAuth;
|
||||
use app\admin\model\SystemAuthNode;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
|
||||
class AdminRoleListBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('admin:role:list')
|
||||
->setDescription('查看角色列表');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
try {
|
||||
$roles = SystemAuth::where('delete_time', 0)
|
||||
->field('id,title,sort,status,remark')
|
||||
->order('sort', 'asc')
|
||||
->order('id', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
if (empty($roles)) {
|
||||
$output->comment('没有找到任何角色');
|
||||
return true;
|
||||
}
|
||||
|
||||
$output->info('角色列表');
|
||||
$output->writeln('================================');
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$nodeCount = SystemAuthNode::where('auth_id', $role['id'])->count();
|
||||
$statusText = $role['status'] == 1 ? '启用' : '禁用';
|
||||
|
||||
$output->info('角色ID: ' . $role['id']);
|
||||
$output->comment(' 角色名称: ' . $role['title']);
|
||||
$output->comment(' 权限节点数: ' . $nodeCount);
|
||||
$output->comment(' 状态: ' . $statusText);
|
||||
$output->comment(' 排序: ' . $role['sort']);
|
||||
if (!empty($role['remark'])) {
|
||||
$output->comment(' 备注: ' . $role['remark']);
|
||||
}
|
||||
$output->writeln('--------------------------------');
|
||||
}
|
||||
|
||||
$output->info('总计: ' . count($roles) . ' 个角色');
|
||||
} catch (\Throwable $e) {
|
||||
$output->error('获取角色列表失败: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\role;
|
||||
|
||||
use app\admin\model\SystemAuth;
|
||||
use app\admin\model\SystemAuthNode;
|
||||
use app\admin\service\NodeService;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
class AdminRolePermissionAssignBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('admin:role:permission:assign')
|
||||
->addOption('role-id', null, Option::VALUE_REQUIRED, '角色ID')
|
||||
->addOption('nodes', null, Option::VALUE_REQUIRED, '权限节点列表(逗号分隔)')
|
||||
->setDescription('给角色分配权限');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$roleId = $input->getOption('role-id');
|
||||
$nodesParam = $input->getOption('nodes');
|
||||
|
||||
if (empty($roleId)) {
|
||||
$output->error('角色ID不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($nodesParam)) {
|
||||
$output->error('权限节点不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$auth = SystemAuth::find($roleId);
|
||||
if (empty($auth)) {
|
||||
$output->error('角色ID ' . $roleId . ' 不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
$nodes = $this->parseNodes($nodesParam);
|
||||
if (empty($nodes)) {
|
||||
$output->error('权限节点列表为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
$validNodes = $this->validateNodes($nodes);
|
||||
$invalidNodes = array_diff($nodes, $validNodes);
|
||||
|
||||
if (!empty($invalidNodes)) {
|
||||
$invalidCount = count($invalidNodes);
|
||||
$output->error("[错误] 发现 {$invalidCount} 个无效的权限节点:");
|
||||
foreach (array_values($invalidNodes) as $node) {
|
||||
$output->error(" - {$node}");
|
||||
}
|
||||
$output->writeln('');
|
||||
}
|
||||
|
||||
if (empty($validNodes)) {
|
||||
$output->error("有效权限节点数: 0");
|
||||
$output->error("分配失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
$existingNodes = SystemAuthNode::where('auth_id', $roleId)
|
||||
->column('node');
|
||||
$newNodes = array_diff($validNodes, $existingNodes);
|
||||
|
||||
if (empty($newNodes)) {
|
||||
$output->info('所有权限节点已存在,无需重复分配');
|
||||
return true;
|
||||
}
|
||||
|
||||
$authNodes = [];
|
||||
foreach ($newNodes as $node) {
|
||||
$authNodes[] = [
|
||||
'auth_id' => $roleId,
|
||||
'node' => $node,
|
||||
];
|
||||
}
|
||||
|
||||
(new SystemAuthNode())->saveAll($authNodes);
|
||||
|
||||
$output->info('权限分配成功');
|
||||
$output->info('角色ID: ' . $roleId);
|
||||
$output->info('新增的权限节点: ' . implode(', ', $newNodes));
|
||||
$output->info('该角色总权限节点数: ' . (count($existingNodes) + count($newNodes)));
|
||||
} catch (\Throwable $e) {
|
||||
$output->error('分配权限失败: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function parseNodes(string $nodesParam): array
|
||||
{
|
||||
$nodes = explode(',', $nodesParam);
|
||||
$nodes = array_map('trim', $nodes);
|
||||
$nodes = array_filter($nodes);
|
||||
|
||||
return array_values($nodes);
|
||||
}
|
||||
|
||||
protected function validateNodes(array $nodes): array
|
||||
{
|
||||
$nodeService = new NodeService();
|
||||
$allNodes = $nodeService->getNodeParis();
|
||||
|
||||
$validNodes = [];
|
||||
foreach ($nodes as $node) {
|
||||
if (isset($allNodes[$node])) {
|
||||
$validNodes[] = $node;
|
||||
}
|
||||
}
|
||||
|
||||
return $validNodes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\role;
|
||||
|
||||
use app\admin\model\SystemAuth;
|
||||
use app\admin\model\SystemAuthNode;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
class AdminRolePermissionListBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('admin:role:permission:list')
|
||||
->addOption('role-id', null, Option::VALUE_REQUIRED, '角色ID')
|
||||
->setDescription('查看角色权限');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$roleId = $input->getOption('role-id');
|
||||
|
||||
if (empty($roleId)) {
|
||||
$output->error('角色ID不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$role = SystemAuth::find($roleId);
|
||||
if (empty($role)) {
|
||||
$output->error('角色ID ' . $roleId . ' 不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
$nodes = SystemAuthNode::where('auth_id', $roleId)
|
||||
->column('node');
|
||||
|
||||
$output->info('角色权限列表');
|
||||
$output->writeln('================================');
|
||||
$output->info('角色ID: ' . $role->id);
|
||||
$output->comment('角色名称: ' . $role->title);
|
||||
$output->writeln('--------------------------------');
|
||||
|
||||
if (empty($nodes)) {
|
||||
$output->comment('该角色没有分配任何权限节点');
|
||||
} else {
|
||||
$output->info('权限节点 (' . count($nodes) . ' 个):');
|
||||
foreach ($nodes as $node) {
|
||||
$output->comment(' - ' . $node);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$output->error('获取角色权限失败: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\role;
|
||||
|
||||
use app\admin\model\SystemAuth;
|
||||
use app\admin\model\SystemAuthNode;
|
||||
use app\admin\service\NodeService;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
class AdminRolePermissionRevokeBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('admin:role:permission:revoke')
|
||||
->addOption('role-id', null, Option::VALUE_REQUIRED, '角色ID')
|
||||
->addOption('nodes', null, Option::VALUE_REQUIRED, '权限节点列表(逗号分隔)')
|
||||
->setDescription('撤回角色权限');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$roleId = $input->getOption('role-id');
|
||||
$nodesParam = $input->getOption('nodes');
|
||||
|
||||
if (empty($roleId)) {
|
||||
$output->error('角色ID不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($nodesParam)) {
|
||||
$output->error('权限节点不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$auth = SystemAuth::find($roleId);
|
||||
if (empty($auth)) {
|
||||
$output->error('角色ID ' . $roleId . ' 不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
$nodes = $this->parseNodes($nodesParam);
|
||||
if (empty($nodes)) {
|
||||
$output->error('权限节点列表为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
$validNodes = $this->validateNodes($nodes);
|
||||
$invalidNodes = array_diff($nodes, $validNodes);
|
||||
|
||||
if (!empty($invalidNodes)) {
|
||||
$output->comment('警告: 以下权限节点无效: ' . implode(', ', array_values($invalidNodes)));
|
||||
}
|
||||
|
||||
if (empty($validNodes)) {
|
||||
$output->error('没有有效的权限节点');
|
||||
return false;
|
||||
}
|
||||
|
||||
$deletedCount = SystemAuthNode::where('auth_id', $roleId)
|
||||
->where('node', 'in', $validNodes)
|
||||
->delete();
|
||||
|
||||
if ($deletedCount == 0) {
|
||||
$output->error('没有权限可以撤回(角色不包含指定的权限节点)');
|
||||
return false;
|
||||
}
|
||||
|
||||
$remainingCount = SystemAuthNode::where('auth_id', $roleId)->count();
|
||||
|
||||
$output->info('权限撤回成功');
|
||||
$output->info('角色ID: ' . $roleId);
|
||||
$output->info('撤回的权限节点: ' . implode(', ', $validNodes));
|
||||
$output->info('该角色剩余权限节点数: ' . $remainingCount);
|
||||
} catch (\Throwable $e) {
|
||||
$output->error('撤回权限失败: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function parseNodes(string $nodesParam): array
|
||||
{
|
||||
$nodes = explode(',', $nodesParam);
|
||||
$nodes = array_map('trim', $nodes);
|
||||
$nodes = array_filter($nodes);
|
||||
|
||||
return array_values($nodes);
|
||||
}
|
||||
|
||||
protected function validateNodes(array $nodes): array
|
||||
{
|
||||
$nodeService = new NodeService();
|
||||
$allNodes = $nodeService->getNodeParis();
|
||||
|
||||
$validNodes = [];
|
||||
foreach ($nodes as $node) {
|
||||
if (isset($allNodes[$node])) {
|
||||
$validNodes[] = $node;
|
||||
}
|
||||
}
|
||||
|
||||
return $validNodes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\user;
|
||||
|
||||
use app\admin\model\SystemAdmin;
|
||||
use app\admin\model\SystemAuth;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
class AdminUserRoleAssignBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('admin:user:role:assign')
|
||||
->addOption('user-id', null, Option::VALUE_REQUIRED, '用户ID')
|
||||
->addOption('role-ids', null, Option::VALUE_REQUIRED, '角色ID列表(逗号分隔)')
|
||||
->setDescription('给用户分配角色');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$userId = $input->getOption('user-id');
|
||||
$roleIdsParam = $input->getOption('role-ids');
|
||||
|
||||
if (empty($userId)) {
|
||||
$output->error('用户ID不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($roleIdsParam)) {
|
||||
$output->error('角色ID不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$admin = SystemAdmin::find($userId);
|
||||
if (empty($admin)) {
|
||||
$output->error('用户ID ' . $userId . ' 不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
$roleIds = $this->parseRoleIds($roleIdsParam);
|
||||
if (empty($roleIds)) {
|
||||
$output->error('角色ID列表为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
$validRoleIds = $this->validateRoleIds($roleIds);
|
||||
$invalidRoleIds = array_diff($roleIds, $validRoleIds);
|
||||
|
||||
if (!empty($invalidRoleIds)) {
|
||||
$invalidCount = count($invalidRoleIds);
|
||||
$output->error("[错误] 发现 {$invalidCount} 个无效的角色ID:");
|
||||
foreach (array_values($invalidRoleIds) as $roleId) {
|
||||
$output->error(" - {$roleId}");
|
||||
}
|
||||
$output->writeln('');
|
||||
}
|
||||
|
||||
if (empty($validRoleIds)) {
|
||||
$output->error("有效角色ID数: 0");
|
||||
$output->error("分配失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
$existingAuthIds = $admin->auth_ids;
|
||||
$existingAuthIdArray = [];
|
||||
if (!empty($existingAuthIds)) {
|
||||
$existingAuthIdArray = array_filter(explode(',', $existingAuthIds));
|
||||
}
|
||||
|
||||
$newRoleIds = array_diff($validRoleIds, $existingAuthIdArray);
|
||||
|
||||
if (empty($newRoleIds)) {
|
||||
$output->info('所有角色已存在,无需重复分配');
|
||||
return true;
|
||||
}
|
||||
|
||||
$mergedRoleIds = array_merge($existingAuthIdArray, $newRoleIds);
|
||||
$mergedRoleIds = array_values(array_unique($mergedRoleIds));
|
||||
|
||||
$admin->auth_ids = implode(',', $mergedRoleIds);
|
||||
$admin->save();
|
||||
|
||||
$output->info('角色分配成功');
|
||||
$output->info('用户ID: ' . $userId);
|
||||
$output->info('新增的角色ID: ' . implode(', ', $newRoleIds));
|
||||
$output->info('该用户总角色数: ' . count($mergedRoleIds));
|
||||
$output->info('所有角色ID: ' . implode(', ', $mergedRoleIds));
|
||||
} catch (\Throwable $e) {
|
||||
$output->error('分配角色失败: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function parseRoleIds(string $roleIdsParam): array
|
||||
{
|
||||
$roleIds = explode(',', $roleIdsParam);
|
||||
$roleIds = array_map('trim', $roleIds);
|
||||
$roleIds = array_filter($roleIds);
|
||||
|
||||
return array_values($roleIds);
|
||||
}
|
||||
|
||||
protected function validateRoleIds(array $roleIds): array
|
||||
{
|
||||
$validRoleIds = SystemAuth::whereIn('id', $roleIds)
|
||||
->where('delete_time', 0)
|
||||
->column('id');
|
||||
|
||||
return $validRoleIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\user;
|
||||
|
||||
use app\admin\model\SystemAdmin;
|
||||
use app\admin\model\SystemAuth;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
class AdminUserRoleListBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('admin:user:role:list')
|
||||
->addOption('user-id', null, Option::VALUE_REQUIRED, '用户ID')
|
||||
->setDescription('查看用户角色');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$userId = $input->getOption('user-id');
|
||||
|
||||
if (empty($userId)) {
|
||||
$output->error('用户ID不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$user = SystemAdmin::find($userId);
|
||||
if (empty($user)) {
|
||||
$output->error('用户ID ' . $userId . ' 不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
$authIds = $user->auth_ids;
|
||||
$authIdArray = [];
|
||||
|
||||
if (!empty($authIds)) {
|
||||
$authIdArray = array_filter(explode(',', $authIds));
|
||||
}
|
||||
|
||||
if (empty($authIdArray)) {
|
||||
$output->info('用户角色列表');
|
||||
$output->writeln('================================');
|
||||
$output->info('用户ID: ' . $user->id);
|
||||
$output->info('用户名: ' . $user->username);
|
||||
$output->writeln('--------------------------------');
|
||||
$output->comment('该用户没有分配任何角色');
|
||||
return true;
|
||||
}
|
||||
|
||||
$roles = SystemAuth::whereIn('id', $authIdArray)
|
||||
->where('delete_time', 0)
|
||||
->field('id,title,status,remark')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$output->info('用户角色列表');
|
||||
$output->writeln('================================');
|
||||
$output->info('用户ID: ' . $user->id);
|
||||
$output->info('用户名: ' . $user->username);
|
||||
$output->writeln('--------------------------------');
|
||||
|
||||
if (empty($roles)) {
|
||||
$output->comment('没有找到有效的角色信息');
|
||||
} else {
|
||||
foreach ($roles as $role) {
|
||||
$statusText = $role['status'] == 1 ? '启用' : '禁用';
|
||||
$output->info('角色ID: ' . $role['id']);
|
||||
$output->comment(' 角色名称: ' . $role['title']);
|
||||
$output->comment(' 状态: ' . $statusText);
|
||||
if (!empty($role['remark'])) {
|
||||
$output->comment(' 备注: ' . $role['remark']);
|
||||
}
|
||||
$output->writeln('--------------------------------');
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$output->error('获取用户角色失败: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\user;
|
||||
|
||||
use app\admin\model\SystemAdmin;
|
||||
use app\admin\model\SystemAuth;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
|
||||
class AdminUserRoleRevokeBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('admin:user:role:revoke')
|
||||
->addOption('user-id', null, Option::VALUE_REQUIRED, '用户ID')
|
||||
->addOption('role-ids', null, Option::VALUE_REQUIRED, '角色ID列表(逗号分隔)')
|
||||
->setDescription('撤回用户角色');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$userId = $input->getOption('user-id');
|
||||
$roleIdsParam = $input->getOption('role-ids');
|
||||
|
||||
if (empty($userId)) {
|
||||
$output->error('用户ID不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($roleIdsParam)) {
|
||||
$output->error('角色ID不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$admin = SystemAdmin::find($userId);
|
||||
if (empty($admin)) {
|
||||
$output->error('用户ID ' . $userId . ' 不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
$roleIds = $this->parseRoleIds($roleIdsParam);
|
||||
if (empty($roleIds)) {
|
||||
$output->error('角色ID列表为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
$validRoleIds = $this->validateRoleIds($roleIds);
|
||||
$invalidRoleIds = array_diff($roleIds, $validRoleIds);
|
||||
|
||||
if (!empty($invalidRoleIds)) {
|
||||
$output->comment('警告: 以下角色ID无效: ' . implode(', ', array_values($invalidRoleIds)));
|
||||
}
|
||||
|
||||
if (empty($validRoleIds)) {
|
||||
$output->error('没有有效的角色ID');
|
||||
return false;
|
||||
}
|
||||
|
||||
$existingAuthIds = $admin->auth_ids;
|
||||
if (empty($existingAuthIds)) {
|
||||
$output->error('用户未分配任何角色,无法撤回');
|
||||
return false;
|
||||
}
|
||||
|
||||
$existingAuthIdArray = array_filter(explode(',', $existingAuthIds));
|
||||
|
||||
$revokedRoleIds = array_intersect($validRoleIds, $existingAuthIdArray);
|
||||
|
||||
if (empty($revokedRoleIds)) {
|
||||
$output->error('没有角色可以撤回(用户不拥有指定的角色)');
|
||||
return false;
|
||||
}
|
||||
|
||||
$remainingRoleIds = array_diff($existingAuthIdArray, $revokedRoleIds);
|
||||
$admin->auth_ids = empty($remainingRoleIds) ? '' : implode(',', $remainingRoleIds);
|
||||
$admin->save();
|
||||
|
||||
$output->info('角色撤回成功');
|
||||
$output->info('用户ID: ' . $userId);
|
||||
$output->info('撤回的角色ID: ' . implode(', ', $revokedRoleIds));
|
||||
$output->info('该用户剩余角色数: ' . count($remainingRoleIds));
|
||||
if (!empty($remainingRoleIds)) {
|
||||
$output->info('剩余角色ID: ' . implode(', ', $remainingRoleIds));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$output->error('撤回角色失败: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function parseRoleIds(string $roleIdsParam): array
|
||||
{
|
||||
$roleIds = explode(',', $roleIdsParam);
|
||||
$roleIds = array_map('trim', $roleIds);
|
||||
$roleIds = array_filter($roleIds);
|
||||
|
||||
return array_values($roleIds);
|
||||
}
|
||||
|
||||
protected function validateRoleIds(array $roleIds): array
|
||||
{
|
||||
$validRoleIds = SystemAuth::whereIn('id', $roleIds)
|
||||
->where('delete_time', 0)
|
||||
->column('id');
|
||||
|
||||
return $validRoleIds;
|
||||
}
|
||||
}
|
||||
@@ -86,15 +86,14 @@ class MigrateBase extends Command
|
||||
|
||||
if ($is_extis) {
|
||||
$output->error('文件已存在:' . $patt_files[0]);
|
||||
if (!$force) {
|
||||
$confirm_force = $output->confirm($input, '确定要覆盖文件吗?如果您想生成更新文件请添加-u参数', false);
|
||||
|
||||
if (!$confirm_force) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$output->confirm($input, '确定要覆盖文件吗?如果您想生成更新文件请添加-u参数', true)) {
|
||||
$output->comment('已取消。');
|
||||
return false;
|
||||
}
|
||||
|
||||
$output->highlight('执行覆盖操作');
|
||||
|
||||
|
||||
$dist_file_path = $patt_files[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,24 @@
|
||||
|
||||
namespace base\common\command\scheme;
|
||||
|
||||
use think\console\Command;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
use think\console\Input\Option;
|
||||
use think\console\Input\Argument;
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
use think\facade\Config;
|
||||
use app\common\service\scheme\DbToSchemeService;
|
||||
use app\common\service\scheme\SchemeToDbService;
|
||||
use app\common\service\scheme\attribute\Table;
|
||||
use ReflectionClass;
|
||||
|
||||
class Make extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('scheme:make')
|
||||
->addOption('table', 't', Option::VALUE_REQUIRED, "The table name (without prefix)")
|
||||
->addArgument('table', Argument::OPTIONAL, "The table name (without prefix)")
|
||||
@@ -27,7 +31,7 @@ class Make extends Command
|
||||
$table = $input->getOption('table') ?: $input->getArgument('table');
|
||||
$service = new DbToSchemeService();
|
||||
$compare = new SchemeToDbService();
|
||||
|
||||
|
||||
$tables = [];
|
||||
if ($table) {
|
||||
$tables[] = $table;
|
||||
@@ -39,7 +43,7 @@ class Make extends Command
|
||||
$connection = Config::get('database.default', 'mysql');
|
||||
$prefix = Config::get('database.connections.' . $connection . '.prefix', '');
|
||||
$backupPrefix = Config::get('scheme.backup_prefix', 'backup');
|
||||
|
||||
|
||||
foreach ($allTables as $t) {
|
||||
if ($this->isBackupTable($t, $prefix, $backupPrefix)) {
|
||||
continue;
|
||||
@@ -50,7 +54,7 @@ class Make extends Command
|
||||
if ($prefix && str_starts_with($t, $prefix)) {
|
||||
$shortName = substr($t, strlen($prefix));
|
||||
}
|
||||
|
||||
|
||||
if (!in_array($shortName, $config) && !in_array($t, $config)) {
|
||||
$tables[] = $shortName;
|
||||
}
|
||||
@@ -61,7 +65,7 @@ class Make extends Command
|
||||
$output->writeln("Processing table: $t");
|
||||
try {
|
||||
$code = $service->generate($t);
|
||||
|
||||
|
||||
// 提取类名以确定文件名
|
||||
if (preg_match('/class\s+(\w+)/', $code, $matches)) {
|
||||
$className = $matches[1];
|
||||
@@ -78,17 +82,17 @@ class Make extends Command
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 确保目录存在
|
||||
if (!is_dir(dirname($path))) {
|
||||
mkdir(dirname($path), 0755, true);
|
||||
}
|
||||
|
||||
|
||||
file_put_contents($path, $code);
|
||||
$output->writeln("<info>Generated: $path</info>");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln("<error>Error processing $t: " . $e->getMessage() . "</error>");
|
||||
$output->error("Error processing $t: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,27 @@
|
||||
|
||||
namespace base\common\command\scheme;
|
||||
|
||||
use think\console\Command;
|
||||
use app\common\console\Command;
|
||||
use app\common\service\scheme\SchemeToDbService;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Input\Option;
|
||||
use think\console\Output;
|
||||
use ReflectionClass;
|
||||
use app\common\scheme\attribute\Table;
|
||||
use think\facade\Config;
|
||||
use think\facade\Db;
|
||||
use app\common\service\scheme\SchemeToDbService;
|
||||
use app\common\scheme\attribute\Table;
|
||||
use ReflectionClass;
|
||||
|
||||
/**
|
||||
* scheme:sync 命令基类
|
||||
*/
|
||||
class Sync extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('scheme:sync')
|
||||
->addOption('skip-data', null, Option::VALUE_NONE, 'Skip data migration')
|
||||
->addOption('force', null, Option::VALUE_NONE, 'Force execution without confirmation')
|
||||
->setDescription('Synchronize Scheme classes to Database');
|
||||
}
|
||||
|
||||
@@ -32,17 +36,20 @@ class Sync extends Command
|
||||
$connection = Config::get('database.default', 'mysql');
|
||||
$prefix = Config::get('database.connections.' . $connection . '.prefix', '');
|
||||
$backupPrefix = Config::get('scheme.backup_prefix', 'backup');
|
||||
|
||||
|
||||
if (!is_dir($schemeDir)) {
|
||||
$output->writeln("<error>Scheme directory not found: $schemeDir</error>");
|
||||
$error = "Scheme directory not found: $schemeDir";
|
||||
$output->writeln("<error>$error</error>");
|
||||
return;
|
||||
}
|
||||
|
||||
$pendingSync = [];
|
||||
$files = glob($schemeDir . '*.php');
|
||||
|
||||
foreach ($files as $file) {
|
||||
require_once $file;
|
||||
$className = 'app\\admin\\scheme\\' . basename($file, '.php');
|
||||
|
||||
|
||||
if (class_exists($className)) {
|
||||
$tableName = $this->getTableNameFromScheme($className);
|
||||
if (empty($tableName)) {
|
||||
@@ -64,30 +71,62 @@ class Sync extends Command
|
||||
try {
|
||||
$diffs = $service->diff($className);
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeln("<error>Check failed for $className: " . $e->getMessage() . "</error>");
|
||||
$output->error("Check failed for $className: " . $e->getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($diffs) === 1 && str_starts_with($diffs[0], '无法读取数据库表结构')) {
|
||||
$output->writeln("<error>Check failed for $className: {$diffs[0]}</error>");
|
||||
$output->error("Check failed for $className: {$diffs[0]}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($diffs)) {
|
||||
$output->writeln("Skipping $className (no schema changes)");
|
||||
continue;
|
||||
}
|
||||
|
||||
$output->writeln("Syncing $className...");
|
||||
try {
|
||||
$backup = $service->sync($className, $skipData);
|
||||
$output->writeln("<info>Success!</info>");
|
||||
if ($backup) {
|
||||
$output->writeln("Backup created: $backup");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln("<error>Failed: " . $e->getMessage() . "</error>");
|
||||
$pendingSync[] = [
|
||||
'className' => $className,
|
||||
'tableName' => $fullTableName,
|
||||
'diffs' => $diffs
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($pendingSync)) {
|
||||
$output->writeln('<info>未检测到 Scheme 变更。</info>');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示所有待同步的变更
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>========================================</comment>');
|
||||
$output->writeln('<comment>即将执行以下 Scheme 变更:</comment>');
|
||||
$output->writeln('<comment>========================================</comment>');
|
||||
foreach ($pendingSync as $item) {
|
||||
$output->writeln("<comment>表名: {$item['tableName']} ({$item['className']})</comment>");
|
||||
foreach ($item['diffs'] as $line) {
|
||||
$output->writeln(" $line");
|
||||
}
|
||||
$output->writeln('');
|
||||
}
|
||||
|
||||
// 确认(全局 -ff 可跳过)
|
||||
if (!$output->confirm($input, '确认要将这些变更应用到数据库吗?', true)) {
|
||||
$output->comment('操作已取消。');
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行同步
|
||||
foreach ($pendingSync as $item) {
|
||||
$output->writeln("正在同步 {$item['className']}...");
|
||||
try {
|
||||
$backup = $service->sync($item['className'], $skipData);
|
||||
$output->writeln("<info>成功!</info>");
|
||||
if ($backup) {
|
||||
$output->writeln("已创建备份: $backup");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln("<error>失败: " . $e->getMessage() . "</error>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,29 +7,34 @@
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
extend/base/common/
|
||||
├── service/
|
||||
│ ├── ToolsDbServiceBase.php # 数据库工具服务
|
||||
│ ├── ToolsLogServiceBase.php # 日志工具服务
|
||||
│ └── ToolsBackupServiceBase.php # 备份工具服务
|
||||
└── command/
|
||||
└── tools/
|
||||
├── README.md # 本文件(命名规范说明)
|
||||
└── db/ # 数据库工具
|
||||
├── ToolsDbQueryBase.php # 基础命令类
|
||||
├── ToolsDbExecuteBase.php
|
||||
└── ...
|
||||
extend/base/common/
|
||||
├── service/
|
||||
│ ├── ToolsDbServiceBase.php # 数据库工具服务
|
||||
│ ├── ToolsLogServiceBase.php # 日志工具服务
|
||||
│ └── ToolsBackupServiceBase.php # 备份工具服务
|
||||
└── command/
|
||||
└── tools/
|
||||
├── README.md # 本文件(命名规范说明)
|
||||
├── db/ # 数据库工具
|
||||
│ ├── ToolsDbQueryBase.php # 基础命令类
|
||||
│ ├── ToolsDbExecuteBase.php
|
||||
│ └── ...
|
||||
├── http/ # HTTP 工具
|
||||
│ └── ToolsHttpCallBase.php
|
||||
└── ...
|
||||
|
||||
app/common/command/
|
||||
├── tools/
|
||||
│ └── db/ # 数据库工具
|
||||
│ ├── ToolsDbQuery.php # 业务命令类
|
||||
│ ├── ToolsDbExecute.php
|
||||
│ └── ...
|
||||
├── admin/ # 普通命令(无前缀)
|
||||
├── scheme/
|
||||
├── curd/
|
||||
└── ...
|
||||
app/common/command/
|
||||
├── tools/
|
||||
│ ├── db/ # 数据库工具
|
||||
│ │ ├── ToolsDbQuery.php # 业务命令类
|
||||
│ │ ├── ToolsDbExecute.php
|
||||
│ │ └── ...
|
||||
│ └── http/ # HTTP 工具
|
||||
│ └── ToolsHttpCall.php
|
||||
├── admin/ # 普通命令(无前缀)
|
||||
├── scheme/
|
||||
├── curd/
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 命名规则对比
|
||||
@@ -77,22 +82,24 @@ app/common/command/
|
||||
|
||||
### 2. 命令名称规则
|
||||
|
||||
- 格式:`tools:[类型]:[功能]`
|
||||
- 示例:
|
||||
- `tools:db:query` - 数据库查询
|
||||
- `tools:db:execute` - 数据库执行
|
||||
- `tools:log:clear` - 日志清理
|
||||
- `tools:backup:create` - 创建备份
|
||||
- 格式:`tools:[类型]:[功能]`
|
||||
- 示例:
|
||||
- `tools:db:query` - 数据库查询
|
||||
- `tools:db:execute` - 数据库执行
|
||||
- `tools:http:call` - HTTP 调用(类似 curl)
|
||||
- `tools:log:clear` - 日志清理
|
||||
- `tools:backup:create` - 创建备份
|
||||
|
||||
### 3. 目录结构规则
|
||||
|
||||
```
|
||||
tools/
|
||||
├── db/ # 数据库工具(db)
|
||||
├── log/ # 日志工具(log)
|
||||
├── backup/ # 备份工具(backup)
|
||||
├── cache/ # 缓存工具(cache)
|
||||
└── ...
|
||||
tools/
|
||||
├── db/ # 数据库工具(db)
|
||||
├── http/ # HTTP 工具(http)
|
||||
├── log/ # 日志工具(log)
|
||||
├── backup/ # 备份工具(backup)
|
||||
├── cache/ # 缓存工具(cache)
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 创建新 Tools 命令的步骤
|
||||
@@ -185,10 +192,17 @@ $this->commands([
|
||||
|
||||
## 已实现的工具
|
||||
|
||||
- **数据库工具 (db)**
|
||||
- `tools:db:query` - 执行 SQL 查询
|
||||
- `tools:db:execute` - 执行 SQL 非查询语句
|
||||
- `tools:db:table` - 使用查询构建器操作表
|
||||
- `tools:db:info` - 显示数据库信息
|
||||
- `tools:db:desc` - 显示表结构
|
||||
- `tools:db:count` - 统计表记录数
|
||||
- **数据库工具 (db)**
|
||||
- `tools:db:query` - 执行 SQL 查询
|
||||
- `tools:db:execute` - 执行 SQL 非查询语句
|
||||
- `tools:db:table` - 使用查询构建器操作表
|
||||
- `tools:db:info` - 显示数据库信息
|
||||
- `tools:db:desc` - 显示表结构
|
||||
- `tools:db:count` - 统计表记录数
|
||||
|
||||
- **HTTP 工具 (http)**
|
||||
- `tools:http:call` - HTTP 调用工具,类似 curl
|
||||
- 支持标准 curl 参数:`--url`, `--method`, `--data`, `--body`, `--headers`
|
||||
- 支持框架特性参数:`--app`, `--controller`, `--action`, `--super-token`, `--user-id`
|
||||
- JSON 输出格式:`{ success, response: { status, data, headers }, execution_time, exception }`
|
||||
|
||||
|
||||
277
extend/base/common/command/tools/agent/ToolsAgentPublishBase.php
Normal file
277
extend/base/common/command/tools/agent/ToolsAgentPublishBase.php
Normal file
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\command\tools\agent;
|
||||
|
||||
use app\common\console\Command;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\App;
|
||||
|
||||
class ToolsAgentPublishBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:agent:publish')
|
||||
->setDescription('发布 AGENTS.md 与 .agent 到兼容目标(复制 rules/skills;可选生成 CLAUDE.md)')
|
||||
->addOption('target', 't', Option::VALUE_OPTIONAL, '发布目标:trae|opencode|roocode|cursor|claude|all', 'all')
|
||||
->addOption('dry-run', null, Option::VALUE_NONE, '只输出将要写入/覆盖的清单,不落盘')
|
||||
->addOption('clean', null, Option::VALUE_NONE, '清理目标中由发布器生成的内容(仅影响 rules/skills 与发布文件)')
|
||||
->addOption('force', 'f', Option::VALUE_NONE, '跳过覆盖确认');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$rootPath = rtrim(App::getRootPath(), "\\/ \t\n\r\0\x0B");
|
||||
|
||||
$sourceAgentsPath = $rootPath . DIRECTORY_SEPARATOR . 'AGENTS.md';
|
||||
$sourceRulesDir = $rootPath . DIRECTORY_SEPARATOR . '.agent' . DIRECTORY_SEPARATOR . 'rules';
|
||||
$sourceSkillsDir = $rootPath . DIRECTORY_SEPARATOR . '.agent' . DIRECTORY_SEPARATOR . 'skills';
|
||||
|
||||
if (!is_file($sourceAgentsPath)) {
|
||||
$output->error('缺少单一事实来源文件:' . $sourceAgentsPath);
|
||||
return;
|
||||
}
|
||||
if (!is_dir($sourceRulesDir)) {
|
||||
$output->error('缺少单一事实来源目录:' . $sourceRulesDir);
|
||||
return;
|
||||
}
|
||||
if (!is_dir($sourceSkillsDir)) {
|
||||
$output->error('缺少单一事实来源目录:' . $sourceSkillsDir);
|
||||
return;
|
||||
}
|
||||
|
||||
$target = strtolower((string)$input->getOption('target'));
|
||||
if ($target === '') {
|
||||
$target = 'all';
|
||||
}
|
||||
|
||||
$supportedTargets = ['trae', 'opencode', 'roocode', 'cursor', 'claude', 'all'];
|
||||
if (!in_array($target, $supportedTargets, true)) {
|
||||
$output->error('不支持的 target:' . $target . '(可选:' . implode('|', $supportedTargets) . ')');
|
||||
return;
|
||||
}
|
||||
|
||||
$dryRun = (bool)$input->getOption('dry-run');
|
||||
$clean = (bool)$input->getOption('clean');
|
||||
|
||||
$targets = $target === 'all' ? ['trae', 'opencode', 'roocode', 'cursor', 'claude'] : [$target];
|
||||
|
||||
$writeActions = [];
|
||||
$deleteActions = [];
|
||||
|
||||
$agentsContent = file_get_contents($sourceAgentsPath);
|
||||
if ($agentsContent === false) {
|
||||
$output->error('读取失败:' . $sourceAgentsPath);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($targets as $t) {
|
||||
if (in_array($t, ['trae', 'opencode', 'roocode', 'cursor'], true)) {
|
||||
$distRoot = $rootPath . DIRECTORY_SEPARATOR . '.' . $t;
|
||||
$distRules = $distRoot . DIRECTORY_SEPARATOR . 'rules';
|
||||
$distSkills = $distRoot . DIRECTORY_SEPARATOR . 'skills';
|
||||
|
||||
if ($t === 'cursor') {
|
||||
$deleteActions[] = $distRules . DIRECTORY_SEPARATOR . 'ulthon-framework.mdc';
|
||||
}
|
||||
|
||||
if ($clean) {
|
||||
$deleteActions[] = $distRules;
|
||||
$deleteActions[] = $distSkills;
|
||||
}
|
||||
|
||||
$writeActions = array_merge($writeActions, $this->planCopyDir($sourceRulesDir, $distRules));
|
||||
$writeActions = array_merge($writeActions, $this->planCopyDir($sourceSkillsDir, $distSkills));
|
||||
}
|
||||
|
||||
if ($t === 'claude') {
|
||||
$distClaude = $rootPath . DIRECTORY_SEPARATOR . 'CLAUDE.md';
|
||||
if ($clean) {
|
||||
$deleteActions[] = $distClaude;
|
||||
}
|
||||
$writeActions = array_merge($writeActions, $this->planWriteFile($distClaude, $agentsContent));
|
||||
}
|
||||
}
|
||||
|
||||
$deleteActions = array_values(array_unique(array_filter($deleteActions, 'strlen')));
|
||||
|
||||
$plannedDeletes = $this->filterExistingDeletes($deleteActions);
|
||||
$plannedWrites = $this->filterEffectiveWrites($writeActions, $clean);
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>=== tools:agent:publish 计划 ===</info>');
|
||||
$output->writeln('target:' . $target);
|
||||
$output->writeln('dry-run:' . ($dryRun ? 'yes' : 'no'));
|
||||
$output->writeln('clean:' . ($clean ? 'yes' : 'no'));
|
||||
$output->writeln('');
|
||||
|
||||
if (empty($plannedDeletes) && empty($plannedWrites)) {
|
||||
$output->writeln('<comment>无变更:目标已是最新状态。</comment>');
|
||||
$output->writeln('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($plannedDeletes)) {
|
||||
$output->writeln('<comment>将删除:</comment>');
|
||||
foreach ($plannedDeletes as $path) {
|
||||
$output->writeln(' - ' . $path);
|
||||
}
|
||||
$output->writeln('');
|
||||
}
|
||||
|
||||
if (!empty($plannedWrites)) {
|
||||
$output->writeln('<comment>将写入:</comment>');
|
||||
foreach ($plannedWrites as $item) {
|
||||
$output->writeln(' - [' . $item['mode'] . '] ' . $item['dist']);
|
||||
}
|
||||
$output->writeln('');
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$output->confirm($input, '将执行上述写入/覆盖/删除操作,是否继续?', true)) {
|
||||
$output->comment('已取消。');
|
||||
$output->newLine();
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($plannedDeletes as $path) {
|
||||
$this->deletePath($path);
|
||||
}
|
||||
|
||||
foreach ($plannedWrites as $item) {
|
||||
$this->ensureDir(dirname($item['dist']));
|
||||
file_put_contents($item['dist'], $item['content']);
|
||||
}
|
||||
|
||||
$output->info('发布完成。');
|
||||
$output->newLine();
|
||||
}
|
||||
|
||||
protected function planCopyDir(string $sourceDir, string $distDir): array
|
||||
{
|
||||
$actions = [];
|
||||
if (!is_dir($sourceDir)) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($sourceDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
$sourceDirNormalized = rtrim($sourceDir, "\\/") . DIRECTORY_SEPARATOR;
|
||||
foreach ($iterator as $fileInfo) {
|
||||
if (!$fileInfo->isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$src = $fileInfo->getPathname();
|
||||
$relative = substr($src, strlen($sourceDirNormalized));
|
||||
$dist = $distDir . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $relative);
|
||||
|
||||
$content = file_get_contents($src);
|
||||
if ($content === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$actions[] = [
|
||||
'dist' => $dist,
|
||||
'content' => $content,
|
||||
];
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
protected function planWriteFile(string $dist, string $content): array
|
||||
{
|
||||
return [[
|
||||
'dist' => $dist,
|
||||
'content' => $content,
|
||||
]];
|
||||
}
|
||||
|
||||
protected function filterEffectiveWrites(array $actions, bool $forceWrite = false): array
|
||||
{
|
||||
$result = [];
|
||||
$resultMap = [];
|
||||
foreach ($actions as $item) {
|
||||
$dist = (string)($item['dist'] ?? '');
|
||||
$content = (string)($item['content'] ?? '');
|
||||
if ($dist === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mode = 'create';
|
||||
if (is_file($dist)) {
|
||||
$existing = file_get_contents($dist);
|
||||
if (!$forceWrite && $existing !== false && $existing === $content) {
|
||||
continue;
|
||||
}
|
||||
$mode = 'update';
|
||||
}
|
||||
|
||||
$resultMap[$dist] = [
|
||||
'dist' => $dist,
|
||||
'content' => $content,
|
||||
'mode' => $mode,
|
||||
];
|
||||
}
|
||||
$result = array_values($resultMap);
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function filterExistingDeletes(array $deleteActions): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($deleteActions as $path) {
|
||||
if (is_file($path) || is_dir($path)) {
|
||||
$result[] = $path;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function deletePath(string $path): void
|
||||
{
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
return;
|
||||
}
|
||||
if (!is_dir($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $fileInfo) {
|
||||
if ($fileInfo->isDir()) {
|
||||
@rmdir($fileInfo->getPathname());
|
||||
continue;
|
||||
}
|
||||
@unlink($fileInfo->getPathname());
|
||||
}
|
||||
|
||||
@rmdir($path);
|
||||
}
|
||||
|
||||
protected function ensureDir(string $dir): void
|
||||
{
|
||||
if ($dir === '' || is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
@mkdir($dir, 0777, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace base\common\command\tools\db;
|
||||
|
||||
use base\common\service\ToolsDbServiceBase;
|
||||
use think\console\Command;
|
||||
use app\common\service\tools\DbService;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
@@ -13,6 +13,8 @@ class ToolsDbCountBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:db:count')
|
||||
->setDescription('统计表记录数')
|
||||
->addArgument('table', null, '表名')
|
||||
@@ -24,12 +26,12 @@ class ToolsDbCountBase extends Command
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
$service->showHelp('tools:db:count', $output);
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
|
||||
if (!$service->checkDebugMode($output)) {
|
||||
return;
|
||||
@@ -61,19 +63,19 @@ class ToolsDbCountBase extends Command
|
||||
$endTime = microtime(true);
|
||||
$executionTime = round(($endTime - $startTime) * 1000, 2);
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>' . str_repeat('=', 60) . '</comment>');
|
||||
$output->writeln('<info>表名:' . $fullTableName . '</info>');
|
||||
$output->writeln('<comment>' . str_repeat('=', 60) . '</comment>');
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>记录数:</info>' . $count);
|
||||
$output->writeln('<info>执行时间:</info>' . $executionTime . 'ms');
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>' . str_repeat('=', 60) . '</comment>');
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->info('表名:' . $fullTableName);
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->newLine();
|
||||
$output->info('记录数:' . $count);
|
||||
$output->info('执行时间:' . $executionTime . 'ms');
|
||||
$output->newLine();
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->newLine();
|
||||
} catch (\Exception $e) {
|
||||
$output->error('统计失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace base\common\command\tools\db;
|
||||
|
||||
use base\common\service\ToolsDbServiceBase;
|
||||
use think\console\Command;
|
||||
use app\common\service\tools\DbService;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
@@ -13,6 +13,8 @@ class ToolsDbDescBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:db:desc')
|
||||
->setDescription('显示表结构信息')
|
||||
->addArgument('table', null, '表名')
|
||||
@@ -24,12 +26,12 @@ class ToolsDbDescBase extends Command
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
$service->showHelp('tools:db:desc', $output);
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
|
||||
if (!$service->checkDebugMode($output)) {
|
||||
return;
|
||||
@@ -48,12 +50,6 @@ class ToolsDbDescBase extends Command
|
||||
$showIndex = $input->getOption('show-index');
|
||||
|
||||
try {
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>' . str_repeat('=', 60) . '</comment>');
|
||||
$output->writeln('<info>表结构:' . $fullTableName . '</info>');
|
||||
$output->writeln('<comment>' . str_repeat('=', 60) . '</comment>');
|
||||
$output->writeln('');
|
||||
|
||||
$tableComment = '';
|
||||
try {
|
||||
$escaped = addcslashes($fullTableName, "\\_%");
|
||||
@@ -77,9 +73,6 @@ class ToolsDbDescBase extends Command
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln('<info>表注释:</info>' . $tableComment);
|
||||
$output->writeln('');
|
||||
|
||||
$columns = Db::connect($connection)->query("SHOW FULL COLUMNS FROM `$fullTableName`");
|
||||
|
||||
if (empty($columns)) {
|
||||
@@ -88,6 +81,45 @@ class ToolsDbDescBase extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$columnData = [];
|
||||
foreach ($columns as $column) {
|
||||
$columnData[] = [
|
||||
'name' => $column['Field'],
|
||||
'type' => $column['Type'],
|
||||
'null' => $column['Null'] === 'YES',
|
||||
'key' => $column['Key'],
|
||||
'default' => $column['Default'] ?? null,
|
||||
'extra' => $column['Extra'] ?? '',
|
||||
'comment' => $column['Comment'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
$indexData = [];
|
||||
if ($showIndex) {
|
||||
$indexes = Db::connect($connection)->query("SHOW INDEX FROM `$fullTableName`");
|
||||
|
||||
if (!empty($indexes)) {
|
||||
foreach ($indexes as $index) {
|
||||
$indexData[] = [
|
||||
'name' => $index['Key_name'],
|
||||
'column' => $index['Column_name'],
|
||||
'unique' => $index['Non_unique'] == 0,
|
||||
'type' => $index['Index_type'],
|
||||
'sequence' => $index['Seq_in_index'],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$output->newLine();
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->info('表结构:' . $fullTableName);
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->newLine();
|
||||
|
||||
$output->info('表注释:' . $tableComment);
|
||||
$output->newLine();
|
||||
|
||||
$tableData = [];
|
||||
foreach ($columns as $column) {
|
||||
$tableData[] = [
|
||||
@@ -104,18 +136,16 @@ class ToolsDbDescBase extends Command
|
||||
$service->formatTableOutput($tableData, $output);
|
||||
|
||||
if ($showIndex) {
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>' . str_repeat('-', 60) . '</comment>');
|
||||
$output->writeln('<info>索引信息</info>');
|
||||
$output->writeln('<comment>' . str_repeat('-', 60) . '</comment>');
|
||||
$output->writeln('');
|
||||
|
||||
$indexes = Db::connect($connection)->query("SHOW INDEX FROM `$fullTableName`");
|
||||
$output->newLine();
|
||||
$output->comment(str_repeat('-', 60));
|
||||
$output->info('索引信息');
|
||||
$output->comment(str_repeat('-', 60));
|
||||
$output->newLine();
|
||||
|
||||
if (!empty($indexes)) {
|
||||
$indexData = [];
|
||||
$indexDataDisplay = [];
|
||||
foreach ($indexes as $index) {
|
||||
$indexData[] = [
|
||||
$indexDataDisplay[] = [
|
||||
'索引名' => $index['Key_name'],
|
||||
'列名' => $index['Column_name'],
|
||||
'唯一' => $index['Non_unique'] == 0 ? '是' : '否',
|
||||
@@ -123,18 +153,18 @@ class ToolsDbDescBase extends Command
|
||||
'顺序' => $index['Seq_in_index'],
|
||||
];
|
||||
}
|
||||
$service->formatTableOutput($indexData, $output);
|
||||
$service->formatTableOutput($indexDataDisplay, $output);
|
||||
} else {
|
||||
$output->writeln('无索引');
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>' . str_repeat('=', 60) . '</comment>');
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->newLine();
|
||||
} catch (\Exception $e) {
|
||||
$output->error('获取表结构失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace base\common\command\tools\db;
|
||||
|
||||
use base\common\service\ToolsDbServiceBase;
|
||||
use think\console\Command;
|
||||
use app\common\service\tools\DbService;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
@@ -13,10 +13,11 @@ class ToolsDbExecuteBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:db:execute')
|
||||
->setDescription('执行 SQL 非查询语句(INSERT/UPDATE/DELETE)')
|
||||
->addArgument('sql', null, 'SQL 执行语句')
|
||||
->addOption('force', null, Option::VALUE_NONE, '跳过确认直接执行')
|
||||
->addOption('transaction', null, Option::VALUE_NONE, '在事务中执行(失败自动回滚)')
|
||||
->addOption('connection', null, Option::VALUE_OPTIONAL, '指定数据库连接配置')
|
||||
->addOption('help', 'h', Option::VALUE_NONE, '显示帮助信息');
|
||||
@@ -25,12 +26,12 @@ class ToolsDbExecuteBase extends Command
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
$service->showHelp('tools:db:execute', $output);
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
|
||||
if (!$service->checkDebugMode($output)) {
|
||||
return;
|
||||
@@ -45,7 +46,6 @@ class ToolsDbExecuteBase extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$force = $input->getOption('force');
|
||||
$useTransaction = $input->getOption('transaction');
|
||||
|
||||
if (preg_match('/^\s*SELECT\s+/i', $sql)) {
|
||||
@@ -53,17 +53,14 @@ class ToolsDbExecuteBase extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>准备执行的 SQL:</comment>');
|
||||
$output->newLine();
|
||||
$output->comment('准备执行的 SQL:');
|
||||
$output->writeln($sql);
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
|
||||
if (!$force) {
|
||||
$confirm = $output->confirm($input, '<question>确定要执行此 SQL 语句吗?</question> ');
|
||||
if (!$confirm) {
|
||||
$output->writeln('<comment>操作已取消</comment>');
|
||||
return;
|
||||
}
|
||||
if (!$output->confirm($input, '确定要执行此 SQL 语句吗?', true)) {
|
||||
$output->comment('已取消。');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -85,19 +82,19 @@ class ToolsDbExecuteBase extends Command
|
||||
$endTime = microtime(true);
|
||||
$executionTime = round(($endTime - $startTime) * 1000, 2);
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>执行成功</info>');
|
||||
$output->newLine();
|
||||
$output->info('执行成功');
|
||||
$output->writeln('影响行数:' . $affectedRows);
|
||||
$output->writeln('执行时间:' . $executionTime . 'ms');
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
$output->error('执行失败:' . $e->getMessage());
|
||||
if ($useTransaction) {
|
||||
$output->writeln('<comment>事务已回滚</comment>');
|
||||
$output->comment('事务已回滚');
|
||||
}
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace base\common\command\tools\db;
|
||||
|
||||
use base\common\service\ToolsDbServiceBase;
|
||||
use think\console\Command;
|
||||
use app\common\service\tools\DbService;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
@@ -14,6 +14,8 @@ class ToolsDbInfoBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:db:info')
|
||||
->setDescription('显示数据库连接信息和表列表')
|
||||
->addOption('with-count', null, Option::VALUE_NONE, '显示每个表的记录数')
|
||||
@@ -24,12 +26,12 @@ class ToolsDbInfoBase extends Command
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
$service->showHelp('tools:db:info', $output);
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
|
||||
if (!$service->checkDebugMode($output)) {
|
||||
return;
|
||||
@@ -47,11 +49,69 @@ class ToolsDbInfoBase extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>' . str_repeat('=', 60) . '</comment>');
|
||||
$output->writeln('<info>数据库连接信息</info>');
|
||||
$output->writeln('<comment>' . str_repeat('=', 60) . '</comment>');
|
||||
$output->writeln('');
|
||||
$tables = Db::connect($connection)->getTables();
|
||||
|
||||
$tableList = [];
|
||||
if (!empty($tables)) {
|
||||
$prefix = $config['prefix'] ?? '';
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$shortName = $table;
|
||||
if ($prefix && str_starts_with($table, $prefix)) {
|
||||
$shortName = substr($table, strlen($prefix));
|
||||
}
|
||||
|
||||
$tableItem = [
|
||||
'name' => $shortName,
|
||||
'full_name' => $table
|
||||
];
|
||||
|
||||
if ($withCount) {
|
||||
$count = Db::connect($connection)->table($table)->count();
|
||||
$tableItem['count'] = $count;
|
||||
}
|
||||
|
||||
$tableList[] = $tableItem;
|
||||
}
|
||||
}
|
||||
|
||||
$output->newLine();
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->info('数据库连接信息');
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->newLine();
|
||||
|
||||
$output->info('连接名称:' . $connection);
|
||||
$output->info('数据库类型:' . ($config['type'] ?? 'unknown'));
|
||||
$output->info('主机地址:' . ($config['hostname'] ?? 'unknown'));
|
||||
$output->info('数据库名:' . ($config['database'] ?? 'unknown'));
|
||||
$output->info('端口:' . ($config['hostport'] ?? 'unknown'));
|
||||
$output->info('字符集:' . ($config['charset'] ?? 'unknown'));
|
||||
$output->info('表前缀:' . ($config['prefix'] ?? ''));
|
||||
$output->newLine();
|
||||
|
||||
$output->comment(str_repeat('-', 60));
|
||||
$output->info('表列表');
|
||||
$output->comment(str_repeat('-', 60));
|
||||
$output->newLine();
|
||||
|
||||
if (empty($tables)) {
|
||||
$output->writeln('无表');
|
||||
$output->newLine();
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($tableList as $tableItem) {
|
||||
if ($withCount) {
|
||||
$output->info(str_pad($tableItem['name'], 40) . ($tableItem['count'] ?? 0) . ' 条记录');
|
||||
} else {
|
||||
$output->info($tableItem['name']);
|
||||
}
|
||||
}
|
||||
|
||||
$output->newLine();
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->newLine();
|
||||
$output->writeln('<info>连接名称:</info>' . $connection);
|
||||
$output->writeln('<info>数据库类型:</info>' . ($config['type'] ?? 'unknown'));
|
||||
$output->writeln('<info>主机地址:</info>' . ($config['hostname'] ?? 'unknown'));
|
||||
@@ -66,27 +126,17 @@ class ToolsDbInfoBase extends Command
|
||||
$output->writeln('<comment>' . str_repeat('-', 60) . '</comment>');
|
||||
$output->writeln('');
|
||||
|
||||
$tables = Db::connect($connection)->getTables();
|
||||
|
||||
if (empty($tables)) {
|
||||
$output->writeln('无表');
|
||||
$output->writeln('');
|
||||
return;
|
||||
}
|
||||
|
||||
$prefix = $config['prefix'] ?? '';
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$shortName = $table;
|
||||
if ($prefix && str_starts_with($table, $prefix)) {
|
||||
$shortName = substr($table, strlen($prefix));
|
||||
}
|
||||
|
||||
foreach ($tableList as $tableItem) {
|
||||
if ($withCount) {
|
||||
$count = Db::connect($connection)->table($table)->count();
|
||||
$output->writeln('<info>' . str_pad($shortName, 40) . '</info>' . $count . ' 条记录');
|
||||
$output->writeln('<info>' . str_pad($tableItem['name'], 40) . '</info>' . ($tableItem['count'] ?? 0) . ' 条记录');
|
||||
} else {
|
||||
$output->writeln('<info>' . $shortName . '</info>');
|
||||
$output->writeln('<info>' . $tableItem['name'] . '</info>');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,4 +148,4 @@ class ToolsDbInfoBase extends Command
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace base\common\command\tools\db;
|
||||
|
||||
use base\common\service\ToolsDbServiceBase;
|
||||
use think\console\Command;
|
||||
use app\common\service\tools\DbService;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
@@ -13,10 +13,11 @@ class ToolsDbQueryBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:db:query')
|
||||
->setDescription('执行 SQL 查询语句(SELECT)并显示结果')
|
||||
->addArgument('sql', null, 'SQL 查询语句')
|
||||
->addOption('format', null, Option::VALUE_OPTIONAL, '输出格式,可选值:table(默认)、json', 'table')
|
||||
->addOption('limit', null, Option::VALUE_OPTIONAL, '限制显示行数')
|
||||
->addOption('connection', null, Option::VALUE_OPTIONAL, '指定数据库连接配置')
|
||||
->addOption('help', 'h', Option::VALUE_NONE, '显示帮助信息');
|
||||
@@ -25,12 +26,12 @@ class ToolsDbQueryBase extends Command
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
$service->showHelp('tools:db:query', $output);
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
|
||||
if (!$service->checkDebugMode($output)) {
|
||||
return;
|
||||
@@ -45,7 +46,6 @@ class ToolsDbQueryBase extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$format = $input->getOption('format') ?: 'table';
|
||||
$limit = $input->getOption('limit');
|
||||
|
||||
if (!preg_match('/^\s*SELECT\s+/i', $sql)) {
|
||||
@@ -69,22 +69,16 @@ class ToolsDbQueryBase extends Command
|
||||
$query = array_slice($query, 0, (int)$limit);
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
|
||||
if ($format === 'json') {
|
||||
$service->formatJsonOutput($query, $output);
|
||||
} else {
|
||||
$service->formatTableOutput($query, $output);
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>查询成功</info>');
|
||||
$output->newLine();
|
||||
$service->formatTableOutput($query, $output);
|
||||
$output->newLine();
|
||||
$output->info('查询成功');
|
||||
$output->writeln('影响行数:' . count($query));
|
||||
$output->writeln('执行时间:' . $executionTime . 'ms');
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
} catch (\Exception $e) {
|
||||
$output->error('查询失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace base\common\command\tools\db;
|
||||
|
||||
use base\common\service\ToolsDbServiceBase;
|
||||
use think\console\Command;
|
||||
use app\common\service\tools\DbService;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
@@ -13,6 +13,8 @@ class ToolsDbTableBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:db:table')
|
||||
->setDescription('使用查询构建器操作表')
|
||||
->addArgument('table', null, '表名')
|
||||
@@ -28,12 +30,12 @@ class ToolsDbTableBase extends Command
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
$service->showHelp('tools:db:table', $output);
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
|
||||
if (!$service->checkDebugMode($output)) {
|
||||
return;
|
||||
@@ -75,10 +77,11 @@ class ToolsDbTableBase extends Command
|
||||
|
||||
if ($count) {
|
||||
$result = $query->count();
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>表名:' . $fullTableName . '</info>');
|
||||
|
||||
$output->newLine();
|
||||
$output->info('表名:' . $fullTableName);
|
||||
$output->writeln('记录数:' . $result);
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
} else {
|
||||
if ($limit && is_numeric($limit)) {
|
||||
$query->limit((int)$limit);
|
||||
@@ -89,17 +92,17 @@ class ToolsDbTableBase extends Command
|
||||
$endTime = microtime(true);
|
||||
$executionTime = round(($endTime - $startTime) * 1000, 2);
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>表名:' . $fullTableName . '</info>');
|
||||
$output->newLine();
|
||||
$output->info('表名:' . $fullTableName);
|
||||
$service->formatTableOutput($result, $output);
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
$output->writeln('影响行数:' . count($result));
|
||||
$output->writeln('执行时间:' . $executionTime . 'ms');
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$output->error('查询失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
445
extend/base/common/command/tools/http/ToolsHttpCallBase.php
Normal file
445
extend/base/common/command/tools/http/ToolsHttpCallBase.php
Normal file
@@ -0,0 +1,445 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\command\tools\http;
|
||||
|
||||
use app\admin\model\SystemAdmin;
|
||||
use app\common\console\Command;
|
||||
use app\common\constants\AdminConstant;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\Cache;
|
||||
|
||||
/**
|
||||
* tools:http:call 命令基类
|
||||
* 类似 curl 的 HTTP 调用功能,支持框架特性参数
|
||||
*/
|
||||
class ToolsHttpCallBase extends Command
|
||||
{
|
||||
/**
|
||||
* Guzzle HTTP 客户端
|
||||
* @var Client
|
||||
*/
|
||||
protected $httpClient;
|
||||
|
||||
/**
|
||||
* 执行开始时间(毫秒)
|
||||
* @var float
|
||||
*/
|
||||
protected $startTime;
|
||||
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:http:call')
|
||||
->setDescription('HTTP 调用工具,类似 curl,支持框架特性参数')
|
||||
->addArgument('url', Argument::OPTIONAL, '请求 URL 或控制器路径(如 /admin/user/index)')
|
||||
->addOption('url', null, Option::VALUE_OPTIONAL, '请求 URL')
|
||||
->addOption('method', 'm', Option::VALUE_OPTIONAL, 'HTTP 方法 (GET, POST, PUT, DELETE)', 'GET')
|
||||
->addOption('data', 'd', Option::VALUE_OPTIONAL, '请求数据(JSON 或 key=value)')
|
||||
->addOption('body', null, Option::VALUE_OPTIONAL, '请求体(原始数据)')
|
||||
->addOption('headers', 'H', Option::VALUE_OPTIONAL, '请求头(JSON 格式,如 {"Content-Type":"application/json"})')
|
||||
->addOption('app', null, Option::VALUE_OPTIONAL, '应用名称(框架特性)')
|
||||
->addOption('controller', null, Option::VALUE_OPTIONAL, '控制器名称(框架特性)')
|
||||
->addOption('action', null, Option::VALUE_OPTIONAL, '动作名称(框架特性)')
|
||||
->addOption('page-data', null, Option::VALUE_NONE, '返回页面 assign 数据(追加 get_page_data=1)')
|
||||
->addOption('super-token', null, Option::VALUE_OPTIONAL, '超级 Token(框架特性)', 'true')
|
||||
->addOption('user-id', null, Option::VALUE_OPTIONAL, '用户 ID(框架特性)');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$this->startTime = microtime(true);
|
||||
|
||||
// 检查环境
|
||||
$this->checkEnvironment($input);
|
||||
|
||||
// 初始化 HTTP 客户端
|
||||
$this->httpClient = new Client([
|
||||
'base_uri' => $this->getBaseUrl(),
|
||||
'timeout' => 30,
|
||||
'verify' => false, // 开发环境跳过 SSL 验证
|
||||
]);
|
||||
|
||||
try {
|
||||
// 构建请求
|
||||
$request = $this->buildRequest($input);
|
||||
|
||||
// 执行请求
|
||||
$response = $this->executeRequest($request, $output);
|
||||
|
||||
// 输出结果
|
||||
$this->outputResult($response, $output);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$executionTime = round((microtime(true) - $this->startTime) * 1000, 2);
|
||||
$this->outputError($e, $executionTime, $output);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查运行环境
|
||||
*/
|
||||
protected function checkEnvironment(Input $input): void
|
||||
{
|
||||
// 检查是否使用框架特性参数
|
||||
$usingFrameworkParams = !empty($input->getOption('app')) ||
|
||||
!empty($input->getOption('controller')) ||
|
||||
!empty($input->getOption('action'));
|
||||
|
||||
if ($usingFrameworkParams) {
|
||||
// 检查应用是否运行
|
||||
$baseUrl = $this->getBaseUrl();
|
||||
$isLocalhost = (strpos($baseUrl, 'localhost') !== false || strpos($baseUrl, '127.0.0.1') !== false);
|
||||
|
||||
if ($isLocalhost) {
|
||||
// 尝试检测应用是否运行
|
||||
$client = new Client([
|
||||
'timeout' => 20,
|
||||
'verify' => false,
|
||||
'http_errors' => false,
|
||||
]);
|
||||
try {
|
||||
$client->get($baseUrl);
|
||||
} catch (\GuzzleHttp\Exception\ConnectException $e) {
|
||||
throw new \Exception(
|
||||
"无法连接到应用服务器 ($baseUrl)。请确保应用正在运行(执行 'php think run')" .
|
||||
" 或者设置 'app.app_host' 环境变量。"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取基础 URL
|
||||
*/
|
||||
protected function getBaseUrl(): string
|
||||
{
|
||||
$appUrl = env('app.app_host', '');
|
||||
if (empty($appUrl)) {
|
||||
$appUrl = 'http://127.0.0.1:8000';
|
||||
}
|
||||
return rtrim($appUrl, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建请求
|
||||
*/
|
||||
protected function buildRequest(Input $input): array
|
||||
{
|
||||
// 获取 URL(优先使用 --url 参数,其次使用位置参数)
|
||||
$url = $input->getOption('url') ?: $input->getArgument('url');
|
||||
|
||||
// 如果使用框架特性参数,构建 URL
|
||||
if (empty($url)) {
|
||||
$url = $this->buildUrlFromFrameworkParams($input);
|
||||
}
|
||||
|
||||
if (empty($url)) {
|
||||
throw new \Exception('请提供 URL 或使用框架特性参数(--app, --controller, --action)');
|
||||
}
|
||||
|
||||
if ($input->getOption('page-data') && strpos($url, 'get_page_data=') === false) {
|
||||
$url .= (strpos($url, '?') === false ? '?' : '&') . 'get_page_data=1';
|
||||
}
|
||||
|
||||
$request = [
|
||||
'url' => $url,
|
||||
'method' => strtoupper($input->getOption('method')),
|
||||
'headers' => [],
|
||||
'body' => null,
|
||||
];
|
||||
|
||||
// 解析请求头
|
||||
$headers = $input->getOption('headers');
|
||||
if (!empty($headers)) {
|
||||
$request['headers'] = json_decode($headers, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new \Exception('请求头格式错误:' . json_last_error_msg());
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 Accept 头为 JSON
|
||||
$request['headers']['Accept'] = 'application/json';
|
||||
|
||||
// 解析请求数据
|
||||
$data = $input->getOption('data');
|
||||
$body = $input->getOption('body');
|
||||
|
||||
if (!empty($body)) {
|
||||
$request['body'] = $body;
|
||||
} elseif (!empty($data)) {
|
||||
$request['headers']['Content-Type'] = 'application/json';
|
||||
$request['body'] = $this->normalizeDataToJson($data);
|
||||
}
|
||||
|
||||
$superToken = $this->resolveSuperToken($input);
|
||||
if (!is_null($superToken)) {
|
||||
$userId = $input->getOption('user-id') ?: AdminConstant::SUPER_ADMIN_ID;
|
||||
$admin = SystemAdmin::find($userId);
|
||||
if (empty($admin)) {
|
||||
throw new \Exception('管理员不存在:' . $userId);
|
||||
}
|
||||
|
||||
$adminData = $admin->toArray();
|
||||
unset($adminData['password']);
|
||||
$adminData['expire_time'] = time() + 7200;
|
||||
|
||||
Cache::store('login')->set($superToken, $adminData, 7200);
|
||||
$request['headers']['Authorization'] = 'Bearer ' . $superToken;
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
protected function normalizeDataToJson(string $raw): string
|
||||
{
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') {
|
||||
return $raw;
|
||||
}
|
||||
|
||||
while (strlen($raw) >= 2) {
|
||||
$first = $raw[0];
|
||||
$last = $raw[strlen($raw) - 1];
|
||||
if (!($first === $last && ($first === '"' || $first === "'"))) {
|
||||
break;
|
||||
}
|
||||
$raw = substr($raw, 1, -1);
|
||||
$raw = trim($raw);
|
||||
}
|
||||
|
||||
json_decode($raw, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
return $raw;
|
||||
}
|
||||
|
||||
if (strpos($raw, '=') === false) {
|
||||
throw new \Exception('数据格式错误:必须是 JSON 字符串,或 key=value 格式');
|
||||
}
|
||||
|
||||
$data = $this->parseKeyValueData($raw);
|
||||
|
||||
return json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
protected function parseKeyValueData(string $raw): array
|
||||
{
|
||||
$parts = preg_split('/[&,\s]+/', trim($raw)) ?: [];
|
||||
$result = [];
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$part = trim((string) $part);
|
||||
if ($part === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pos = strpos($part, '=');
|
||||
if ($pos === false) {
|
||||
throw new \Exception('数据格式错误:key=value 格式中存在无效片段:' . $part);
|
||||
}
|
||||
|
||||
$key = urldecode(trim(substr($part, 0, $pos)));
|
||||
$value = urldecode(substr($part, $pos + 1));
|
||||
|
||||
if ($key === '') {
|
||||
throw new \Exception('数据格式错误:key 不能为空');
|
||||
}
|
||||
|
||||
$result[$key] = $this->castStringValue($value);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function castStringValue(string $value)
|
||||
{
|
||||
$value = trim($value);
|
||||
|
||||
$lower = strtolower($value);
|
||||
if ($lower === 'true') {
|
||||
return true;
|
||||
}
|
||||
if ($lower === 'false') {
|
||||
return false;
|
||||
}
|
||||
if ($lower === 'null') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value !== '' && ($value[0] === '{' || $value[0] === '[')) {
|
||||
$decoded = json_decode($value, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('/^-?\d+$/', $value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从框架特性参数构建 URL
|
||||
*/
|
||||
protected function buildUrlFromFrameworkParams(Input $input): string
|
||||
{
|
||||
$app = $input->getOption('app');
|
||||
$controller = $input->getOption('controller');
|
||||
$action = $input->getOption('action');
|
||||
|
||||
if (empty($app) || empty($controller) || empty($action)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 构建路由路径
|
||||
$path = '/' . $app . '/' . $controller . '/' . $action;
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
protected function resolveSuperToken(Input $input): ?string
|
||||
{
|
||||
$raw = $input->getOption('super-token');
|
||||
$value = is_bool($raw) ? ($raw ? 'true' : 'false') : trim((string) $raw);
|
||||
$lower = strtolower($value);
|
||||
|
||||
if (in_array($lower, ['false', '0', 'off', 'no'], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value === '' || in_array($lower, ['true', '1', 'on', 'yes'], true)) {
|
||||
return $this->generateToken();
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
protected function generateToken(): string
|
||||
{
|
||||
return bin2hex(random_bytes(16));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 HTTP 请求
|
||||
*/
|
||||
protected function executeRequest(array $request, Output $output): array
|
||||
{
|
||||
$guzzleOptions = [
|
||||
'headers' => $request['headers'],
|
||||
];
|
||||
|
||||
// 如果有请求体,添加到选项中
|
||||
if ($request['body'] !== null) {
|
||||
$guzzleOptions['body'] = $request['body'];
|
||||
}
|
||||
|
||||
// 执行请求
|
||||
$guzzleResponse = $this->httpClient->request($request['method'], $request['url'], $guzzleOptions);
|
||||
|
||||
// 解析响应
|
||||
$response = [
|
||||
'status' => $guzzleResponse->getStatusCode(),
|
||||
'headers' => $guzzleResponse->getHeaders(),
|
||||
'data' => null,
|
||||
];
|
||||
|
||||
// 解析响应体
|
||||
$body = $guzzleResponse->getBody()->getContents();
|
||||
$jsonData = json_decode($body, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$response['data'] = $jsonData;
|
||||
} else {
|
||||
$response['data'] = $body;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出结果
|
||||
*/
|
||||
protected function outputResult(array $response, Output $output): void
|
||||
{
|
||||
$executionTime = round((microtime(true) - $this->startTime) * 1000, 2);
|
||||
|
||||
// 构建输出数据
|
||||
$outputData = [
|
||||
'success' => $response['status'] >= 200 && $response['status'] < 300,
|
||||
'response' => [
|
||||
'status' => $response['status'],
|
||||
'data' => $response['data'],
|
||||
'headers' => $response['headers'],
|
||||
],
|
||||
'execution_time' => $executionTime,
|
||||
'exception' => null,
|
||||
];
|
||||
|
||||
// 文本模式:输出可读格式
|
||||
$this->outputTextResult($outputData, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出文本格式结果
|
||||
*/
|
||||
protected function outputTextResult(array $data, Output $output): void
|
||||
{
|
||||
$response = $data['response'];
|
||||
|
||||
$output->newLine();
|
||||
$output->info('HTTP 状态: ' . $response['status']);
|
||||
$output->info('执行时间: ' . $data['execution_time'] . 'ms');
|
||||
$output->newLine();
|
||||
|
||||
// 输出响应头
|
||||
$output->comment('响应头:');
|
||||
foreach ($response['headers'] as $key => $values) {
|
||||
$output->writeln(' ' . $key . ': ' . implode(', ', $values));
|
||||
}
|
||||
$output->newLine();
|
||||
|
||||
// 输出响应数据
|
||||
$output->comment('响应数据:');
|
||||
if (is_string($response['data'])) {
|
||||
$output->writeln(' ' . $response['data']);
|
||||
} else {
|
||||
$output->writeln(' ' . json_encode($response['data'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
|
||||
}
|
||||
$output->newLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出错误信息
|
||||
*/
|
||||
protected function outputError(\Exception $e, float $executionTime, Output $output): void
|
||||
{
|
||||
$output->error('错误: ' . $e->getMessage());
|
||||
$output->writeln('执行时间: ' . $executionTime . 'ms');
|
||||
if (env('app_debug', false)) {
|
||||
$output->writeln('文件: ' . $e->getFile() . ':' . $e->getLine());
|
||||
|
||||
// 如果是 Guzzle 连接错误,提供提示
|
||||
if (strpos($e->getMessage(), 'Failed to connect') !== false) {
|
||||
$baseUrl = $this->getBaseUrl();
|
||||
$output->comment('提示: 请确保应用正在运行(执行 "php think run")或设置 "app.app_host" 环境变量');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
199
extend/base/common/command/tools/log/ToolsLogSearchBase.php
Normal file
199
extend/base/common/command/tools/log/ToolsLogSearchBase.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\command\tools\log;
|
||||
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* tools:log:search 命令基类
|
||||
*/
|
||||
class ToolsLogSearchBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:log:search')
|
||||
->setDescription('搜索数据库日志')
|
||||
->addArgument('keywords', Argument::REQUIRED, '搜索关键词')
|
||||
->addOption('level', 'l', Option::VALUE_OPTIONAL, '过滤日志级别 (info, warning, error)')
|
||||
->addOption('limit', null, Option::VALUE_OPTIONAL, '限制显示条数', 50)
|
||||
->addOption('controller', 'c', Option::VALUE_OPTIONAL, '过滤控制器名称')
|
||||
->addOption('help', 'h', Option::VALUE_NONE, '显示帮助信息');
|
||||
}
|
||||
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$this->showHelp($output);
|
||||
return;
|
||||
}
|
||||
|
||||
$keywords = $input->getArgument('keywords');
|
||||
|
||||
if (empty($keywords)) {
|
||||
$output->error('请提供搜索关键词');
|
||||
return;
|
||||
}
|
||||
|
||||
$filters = $this->buildFilters($input);
|
||||
$limit = (int)$input->getOption('limit') ?? 50;
|
||||
|
||||
try {
|
||||
$query = Db::name('debug_log');
|
||||
|
||||
// 搜索关键词(在 content 字段中)
|
||||
$query->whereLike('content', '%' . $keywords . '%');
|
||||
|
||||
// 应用过滤条件
|
||||
if (!empty($filters['level'])) {
|
||||
$query->where('level', '=', $filters['level']);
|
||||
}
|
||||
if (!empty($filters['controller'])) {
|
||||
$query->whereLike('controller_name', '%' . $filters['controller'] . '%');
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
$count = $query->count();
|
||||
|
||||
// 获取日志记录
|
||||
$logs = $query->order('create_time', 'desc')
|
||||
->limit($limit)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 格式化时间戳并高亮关键词
|
||||
foreach ($logs as &$log) {
|
||||
if (!empty($log['create_time'])) {
|
||||
$log['create_time_formatted'] = date('Y-m-d H:i:s', $log['create_time']);
|
||||
}
|
||||
// 高亮关键词
|
||||
$log['content_highlighted'] = $this->highlightKeywords($log['content'] ?? '', $keywords);
|
||||
}
|
||||
unset($log);
|
||||
|
||||
$this->outputText($logs, $count, $keywords, $filters, $output);
|
||||
} catch (\Exception $e) {
|
||||
$output->error('搜索失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建过滤条件
|
||||
*/
|
||||
protected function buildFilters(Input $input): array
|
||||
{
|
||||
return [
|
||||
'level' => $input->getOption('level'),
|
||||
'controller' => $input->getOption('controller')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 高亮关键词
|
||||
*/
|
||||
protected function highlightKeywords(string $content, string $keywords): string
|
||||
{
|
||||
// 使用 ANSI 颜色代码高亮关键词(仅用于文本模式)
|
||||
return str_replace($keywords, '<comment>' . $keywords . '</comment>', $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本格式输出
|
||||
*/
|
||||
protected function outputText(array $logs, int $count, string $keywords, array $filters, Output $output): void
|
||||
{
|
||||
$output->newLine();
|
||||
$output->info('=== 数据库日志搜索结果 ===');
|
||||
$output->newLine();
|
||||
|
||||
$output->comment('搜索关键词:' . $keywords);
|
||||
$output->newLine();
|
||||
|
||||
// 显示过滤条件
|
||||
$activeFilters = array_filter($filters, function($value) {
|
||||
return !empty($value);
|
||||
});
|
||||
if (!empty($activeFilters)) {
|
||||
$output->comment('过滤条件:');
|
||||
foreach ($activeFilters as $key => $value) {
|
||||
$output->writeln(' - ' . $key . ': ' . $value);
|
||||
}
|
||||
$output->newLine();
|
||||
}
|
||||
|
||||
if (empty($logs)) {
|
||||
$output->comment('没有找到包含 "' . $keywords . '" 的日志');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示搜索结果
|
||||
$output->info('搜索结果(共 ' . $count . ' 条,显示 ' . count($logs) . ' 条):');
|
||||
$output->newLine();
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$levelMethod = $this->getLevelMethod($log['level'] ?? '');
|
||||
$output->$levelMethod('[' . $log['level'] . '] ' .
|
||||
'[' . ($log['create_time_formatted'] ?? 'N/A') . '] ' .
|
||||
'[' . ($log['controller_name'] ?? '') . '::' . ($log['action_name'] ?? '') . ']');
|
||||
|
||||
// 显示内容并高亮关键词
|
||||
$content = $log['content'] ?? '';
|
||||
if (strlen($content) > 200) {
|
||||
$content = substr($content, 0, 200) . '...';
|
||||
}
|
||||
$output->writeln(' ' . $this->highlightKeywords($content, $keywords));
|
||||
$output->newLine();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志级别对应的方法名
|
||||
*/
|
||||
protected function getLevelMethod(string $level): string
|
||||
{
|
||||
$level = strtolower($level);
|
||||
$methods = [
|
||||
'error' => 'error',
|
||||
'warning' => 'comment',
|
||||
'info' => 'info',
|
||||
'debug' => 'info'
|
||||
];
|
||||
|
||||
return $methods[$level] ?? 'info';
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示帮助信息
|
||||
*/
|
||||
protected function showHelp(Output $output): void
|
||||
{
|
||||
$output->newLine();
|
||||
$output->info('命令名称:');
|
||||
$output->writeln(' tools:log:search - 搜索数据库日志');
|
||||
$output->newLine();
|
||||
$output->info('用法:');
|
||||
$output->writeln(' php think tools:log:search 关键词 [选项]');
|
||||
$output->newLine();
|
||||
$output->info('参数:');
|
||||
$output->writeln(' keywords 搜索关键词(必填)');
|
||||
$output->newLine();
|
||||
$output->info('选项:');
|
||||
$output->writeln(' -l, --level 过滤日志级别 (info, warning, error)');
|
||||
$output->writeln(' --limit 限制显示条数 (默认: 50)');
|
||||
$output->writeln(' -c, --controller 过滤控制器名称');
|
||||
$output->writeln(' -h, --help 显示帮助信息');
|
||||
$output->newLine();
|
||||
$output->info('示例:');
|
||||
$output->writeln(' php think tools:log:search "用户登录失败" --level=error');
|
||||
$output->writeln(' php think tools:log:search "数据库连接" --controller=Admin --limit=20');
|
||||
$output->writeln(' php think tools:log:search "Exception" --level=error');
|
||||
$output->newLine();
|
||||
}
|
||||
}
|
||||
195
extend/base/common/command/tools/log/ToolsLogShowBase.php
Normal file
195
extend/base/common/command/tools/log/ToolsLogShowBase.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\command\tools\log;
|
||||
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* tools:log:show 命令基类
|
||||
*/
|
||||
class ToolsLogShowBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:log:show')
|
||||
->setDescription('查看数据库日志')
|
||||
->addOption('level', 'l', Option::VALUE_OPTIONAL, '过滤日志级别 (info, warning, error)')
|
||||
->addOption('limit', null, Option::VALUE_OPTIONAL, '限制显示条数', 50)
|
||||
->addOption('controller', 'c', Option::VALUE_OPTIONAL, '过滤控制器名称')
|
||||
->addOption('action', 'a', Option::VALUE_OPTIONAL, '过滤操作名称')
|
||||
->addOption('start', 's', Option::VALUE_OPTIONAL, '开始时间 (Y-m-d H:i:s)')
|
||||
->addOption('end', 'e', Option::VALUE_OPTIONAL, '结束时间 (Y-m-d H:i:s)')
|
||||
->addOption('help', 'h', Option::VALUE_NONE, '显示帮助信息');
|
||||
}
|
||||
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$this->showHelp($output);
|
||||
return;
|
||||
}
|
||||
|
||||
$filters = $this->buildFilters($input);
|
||||
$limit = (int)$input->getOption('limit') ?? 50;
|
||||
|
||||
try {
|
||||
$query = Db::name('debug_log');
|
||||
|
||||
// 应用过滤条件
|
||||
if (!empty($filters['level'])) {
|
||||
$query->where('level', '=', $filters['level']);
|
||||
}
|
||||
if (!empty($filters['controller'])) {
|
||||
$query->whereLike('controller_name', '%' . $filters['controller'] . '%');
|
||||
}
|
||||
if (!empty($filters['action'])) {
|
||||
$query->whereLike('action_name', '%' . $filters['action'] . '%');
|
||||
}
|
||||
if (!empty($filters['start'])) {
|
||||
$startTime = strtotime($filters['start']);
|
||||
if ($startTime !== false) {
|
||||
$query->where('create_time', '>=', $startTime);
|
||||
}
|
||||
}
|
||||
if (!empty($filters['end'])) {
|
||||
$endTime = strtotime($filters['end']);
|
||||
if ($endTime !== false) {
|
||||
$query->where('create_time', '<=', $endTime);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
$count = $query->count();
|
||||
|
||||
// 获取日志记录
|
||||
$logs = $query->order('create_time', 'desc')
|
||||
->limit($limit)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 格式化时间戳
|
||||
foreach ($logs as &$log) {
|
||||
if (!empty($log['create_time'])) {
|
||||
$log['create_time_formatted'] = date('Y-m-d H:i:s', $log['create_time']);
|
||||
}
|
||||
}
|
||||
unset($log);
|
||||
|
||||
$this->outputText($logs, $count, $filters, $output);
|
||||
} catch (\Exception $e) {
|
||||
$output->error('查询失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建过滤条件
|
||||
*/
|
||||
protected function buildFilters(Input $input): array
|
||||
{
|
||||
return [
|
||||
'level' => $input->getOption('level'),
|
||||
'controller' => $input->getOption('controller'),
|
||||
'action' => $input->getOption('action'),
|
||||
'start' => $input->getOption('start'),
|
||||
'end' => $input->getOption('end')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本格式输出
|
||||
*/
|
||||
protected function outputText(array $logs, int $count, array $filters, Output $output): void
|
||||
{
|
||||
$output->newLine();
|
||||
$output->info('=== 数据库日志查询结果 ===');
|
||||
$output->newLine();
|
||||
|
||||
// 显示过滤条件
|
||||
$activeFilters = array_filter($filters, function($value) {
|
||||
return !empty($value);
|
||||
});
|
||||
if (!empty($activeFilters)) {
|
||||
$output->comment('过滤条件:');
|
||||
foreach ($activeFilters as $key => $value) {
|
||||
$output->writeln(' - ' . $key . ': ' . $value);
|
||||
}
|
||||
$output->newLine();
|
||||
}
|
||||
|
||||
if (empty($logs)) {
|
||||
$output->comment('没有找到符合条件的日志');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示日志列表
|
||||
$output->info('日志列表(共 ' . $count . ' 条,显示 ' . count($logs) . ' 条):');
|
||||
$output->newLine();
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$levelMethod = $this->getLevelMethod($log['level'] ?? '');
|
||||
$output->$levelMethod('[' . $log['level'] . '] ' .
|
||||
'[' . ($log['create_time_formatted'] ?? 'N/A') . '] ' .
|
||||
'[' . ($log['controller_name'] ?? '') . '::' . ($log['action_name'] ?? '') . ']');
|
||||
|
||||
// 显示内容摘要
|
||||
$content = $log['content'] ?? '';
|
||||
if (strlen($content) > 200) {
|
||||
$content = substr($content, 0, 200) . '...';
|
||||
}
|
||||
$output->writeln(' ' . $content);
|
||||
$output->newLine();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志级别对应的方法名
|
||||
*/
|
||||
protected function getLevelMethod(string $level): string
|
||||
{
|
||||
$level = strtolower($level);
|
||||
$methods = [
|
||||
'error' => 'error',
|
||||
'warning' => 'comment',
|
||||
'info' => 'info',
|
||||
'debug' => 'info'
|
||||
];
|
||||
|
||||
return $methods[$level] ?? 'info';
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示帮助信息
|
||||
*/
|
||||
protected function showHelp(Output $output): void
|
||||
{
|
||||
$output->newLine();
|
||||
$output->info('命令名称:');
|
||||
$output->writeln(' tools:log:show - 查看数据库日志');
|
||||
$output->newLine();
|
||||
$output->info('用法:');
|
||||
$output->writeln(' php think tools:log:show [选项]');
|
||||
$output->newLine();
|
||||
$output->info('选项:');
|
||||
$output->writeln(' -l, --level 过滤日志级别 (info, warning, error)');
|
||||
$output->writeln(' --limit 限制显示条数 (默认: 50)');
|
||||
$output->writeln(' -c, --controller 过滤控制器名称');
|
||||
$output->writeln(' -a, --action 过滤操作名称');
|
||||
$output->writeln(' -s, --start 开始时间 (Y-m-d H:i:s)');
|
||||
$output->writeln(' -e, --end 结束时间 (Y-m-d H:i:s)');
|
||||
$output->writeln(' -h, --help 显示帮助信息');
|
||||
$output->newLine();
|
||||
$output->info('示例:');
|
||||
$output->writeln(' php think tools:log:show --level=error --limit=10');
|
||||
$output->writeln(' php think tools:log:show --controller=Admin --limit=20');
|
||||
$output->writeln(' php think tools:log:show --start="2024-01-01 00:00:00" --end="2024-12-31 23:59:59"');
|
||||
$output->newLine();
|
||||
}
|
||||
}
|
||||
259
extend/base/common/command/tools/log/ToolsLogStatsBase.php
Normal file
259
extend/base/common/command/tools/log/ToolsLogStatsBase.php
Normal file
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\command\tools\log;
|
||||
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* tools:log:stats 命令基类
|
||||
*/
|
||||
class ToolsLogStatsBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:log:stats')
|
||||
->setDescription('统计数据库日志')
|
||||
->addOption('by-level', null, Option::VALUE_NONE, '按日志级别统计')
|
||||
->addOption('by-controller', null, Option::VALUE_NONE, '按控制器统计')
|
||||
->addOption('by-time', null, Option::VALUE_NONE, '按时间统计')
|
||||
->addOption('time-group', null, Option::VALUE_OPTIONAL, '时间分组 (hour|day|week|month)', 'day')
|
||||
->addOption('help', 'h', Option::VALUE_NONE, '显示帮助信息');
|
||||
}
|
||||
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$this->showHelp($output);
|
||||
return;
|
||||
}
|
||||
|
||||
$byLevel = $input->getOption('by-level');
|
||||
$byController = $input->getOption('by-controller');
|
||||
$byTime = $input->getOption('by-time');
|
||||
$timeGroup = $input->getOption('time-group') ?? 'day';
|
||||
|
||||
// 如果没有指定任何统计类型,默认显示所有统计
|
||||
if (!$byLevel && !$byController && !$byTime) {
|
||||
$byLevel = $byController = $byTime = true;
|
||||
}
|
||||
|
||||
try {
|
||||
$stats = [
|
||||
'by_level' => [],
|
||||
'by_controller' => [],
|
||||
'by_time' => []
|
||||
];
|
||||
|
||||
// 按日志级别统计
|
||||
if ($byLevel) {
|
||||
$stats['by_level'] = $this->statsByLevel();
|
||||
}
|
||||
|
||||
// 按控制器统计
|
||||
if ($byController) {
|
||||
$stats['by_controller'] = $this->statsByController();
|
||||
}
|
||||
|
||||
// 按时间统计
|
||||
if ($byTime) {
|
||||
$stats['by_time'] = $this->statsByTime($timeGroup);
|
||||
}
|
||||
|
||||
// 总数
|
||||
$stats['total'] = Db::name('debug_log')->count();
|
||||
|
||||
$this->outputText($stats, $byLevel, $byController, $byTime, $output);
|
||||
} catch (\Exception $e) {
|
||||
$output->error('统计失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按日志级别统计
|
||||
*/
|
||||
protected function statsByLevel(): array
|
||||
{
|
||||
$results = Db::name('debug_log')
|
||||
->field('level, COUNT(*) as count')
|
||||
->group('level')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$stats = [];
|
||||
foreach ($results as $row) {
|
||||
$stats[$row['level']] = (int)$row['count'];
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按控制器统计
|
||||
*/
|
||||
protected function statsByController(): array
|
||||
{
|
||||
$results = Db::name('debug_log')
|
||||
->field('controller_name, COUNT(*) as count')
|
||||
->where('controller_name', '<>', '')
|
||||
->group('controller_name')
|
||||
->order('count', 'desc')
|
||||
->limit(20)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$stats = [];
|
||||
foreach ($results as $row) {
|
||||
$stats[$row['controller_name']] = (int)$row['count'];
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按时间统计
|
||||
*/
|
||||
protected function statsByTime(string $group): array
|
||||
{
|
||||
$query = Db::name('debug_log');
|
||||
|
||||
// 根据分组类型格式化时间
|
||||
switch ($group) {
|
||||
case 'hour':
|
||||
$dateFormat = '%Y-%m-%d %H:00';
|
||||
$phpFormat = 'Y-m-d H:00';
|
||||
break;
|
||||
case 'week':
|
||||
$dateFormat = '%Y-%u'; // 年-周数
|
||||
$phpFormat = 'Y-W'; // 年-周数
|
||||
break;
|
||||
case 'month':
|
||||
$dateFormat = '%Y-%m';
|
||||
$phpFormat = 'Y-m';
|
||||
break;
|
||||
case 'day':
|
||||
default:
|
||||
$dateFormat = '%Y-%m-%d';
|
||||
$phpFormat = 'Y-m-d';
|
||||
break;
|
||||
}
|
||||
|
||||
// 使用 MySQL DATE_FORMAT 函数
|
||||
$results = $query->field('FROM_UNIXTIME(create_time, \'' . $dateFormat . '\') as time_period, COUNT(*) as count')
|
||||
->where('create_time', '>', 0)
|
||||
->group('time_period')
|
||||
->order('time_period', 'desc')
|
||||
->limit(30)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$stats = [];
|
||||
foreach ($results as $row) {
|
||||
$stats[$row['time_period']] = (int)$row['count'];
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本格式输出
|
||||
*/
|
||||
protected function outputText(array $stats, bool $byLevel, bool $byController, bool $byTime, Output $output): void
|
||||
{
|
||||
$output->newLine();
|
||||
$output->info('=== 数据库日志统计 ===');
|
||||
$output->newLine();
|
||||
|
||||
// 总数
|
||||
$output->info('总日志数:' . ($stats['total'] ?? 0));
|
||||
$output->newLine();
|
||||
|
||||
// 按级别统计
|
||||
if ($byLevel) {
|
||||
$output->comment('按日志级别统计:');
|
||||
if (empty($stats['by_level'])) {
|
||||
$output->writeln(' 无数据');
|
||||
} else {
|
||||
foreach ($stats['by_level'] as $level => $count) {
|
||||
$output->writeln(' ' . $level . ':' . $count . ' 条');
|
||||
}
|
||||
}
|
||||
$output->newLine();
|
||||
}
|
||||
|
||||
// 按控制器统计
|
||||
if ($byController) {
|
||||
$output->comment('按控制器统计(Top 20):');
|
||||
if (empty($stats['by_controller'])) {
|
||||
$output->writeln(' 无数据');
|
||||
} else {
|
||||
foreach ($stats['by_controller'] as $controller => $count) {
|
||||
$output->writeln(' ' . $controller . ':' . $count . ' 条');
|
||||
}
|
||||
}
|
||||
$output->newLine();
|
||||
}
|
||||
|
||||
// 按时间统计
|
||||
if ($byTime) {
|
||||
$output->comment('按时间统计(最近 30 个时段):');
|
||||
if (empty($stats['by_time'])) {
|
||||
$output->writeln(' 无数据');
|
||||
} else {
|
||||
foreach ($stats['by_time'] as $timePeriod => $count) {
|
||||
$output->writeln(' ' . $timePeriod . ':' . $count . ' 条');
|
||||
}
|
||||
}
|
||||
$output->newLine();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志级别对应的颜色
|
||||
*/
|
||||
protected function getLevelColor(string $level): string
|
||||
{
|
||||
$level = strtolower($level);
|
||||
$colors = [
|
||||
'error' => 'error',
|
||||
'warning' => 'comment',
|
||||
'info' => 'info',
|
||||
'debug' => 'info'
|
||||
];
|
||||
|
||||
return $colors[$level] ?? 'info';
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示帮助信息
|
||||
*/
|
||||
protected function showHelp(Output $output): void
|
||||
{
|
||||
$output->newLine();
|
||||
$output->info('命令名称:');
|
||||
$output->writeln(' tools:log:stats - 统计数据库日志');
|
||||
$output->newLine();
|
||||
$output->info('用法:');
|
||||
$output->writeln(' php think tools:log:stats [选项]');
|
||||
$output->newLine();
|
||||
$output->info('选项:');
|
||||
$output->writeln(' --by-level 按日志级别统计');
|
||||
$output->writeln(' --by-controller 按控制器统计');
|
||||
$output->writeln(' --by-time 按时间统计');
|
||||
$output->writeln(' --time-group 时间分组 (hour|day|week|month,默认: day)');
|
||||
$output->writeln(' -h, --help 显示帮助信息');
|
||||
$output->newLine();
|
||||
$output->info('示例:');
|
||||
$output->writeln(' php think tools:log:stats');
|
||||
$output->writeln(' php think tools:log:stats --by-level');
|
||||
$output->writeln(' php think tools:log:stats --by-controller --by-time');
|
||||
$output->writeln(' php think tools:log:stats --by-time --time-group=hour');
|
||||
$output->newLine();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user