mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-07-08 02:52:49 +08:00
feat: 发布智能体版
This commit is contained in:
@@ -21,11 +21,12 @@ class AjaxBase extends AdminController
|
||||
*/
|
||||
public function initAdmin()
|
||||
{
|
||||
$cacheData = Cache::get('initAdmin_' . $this->sessionAdmin->id);
|
||||
$adminId = $this->getAdminId();
|
||||
$cacheData = Cache::get('initAdmin_' . $adminId);
|
||||
if (!empty($cacheData)) {
|
||||
return json($cacheData);
|
||||
}
|
||||
$menuService = new MenuService($this->sessionAdmin->id);
|
||||
$menuService = new MenuService($adminId);
|
||||
$data = [
|
||||
'logoInfo' => [
|
||||
'title' => sysconfig('site', 'logo_title'),
|
||||
@@ -35,7 +36,7 @@ class AjaxBase extends AdminController
|
||||
'homeInfo' => $menuService->getHomeInfo(),
|
||||
'menuInfo' => $menuService->getMenuTree(),
|
||||
];
|
||||
Cache::tag('initAdmin')->set('initAdmin_' . $this->sessionAdmin->id, $data);
|
||||
Cache::tag('initAdmin')->set('initAdmin_' . $adminId, $data);
|
||||
|
||||
return json($data);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ class IndexBase extends AdminController
|
||||
*/
|
||||
public function editAdmin()
|
||||
{
|
||||
$id = $this->sessionAdmin->id;
|
||||
$id = $this->getAdminId();
|
||||
$row = (new SystemAdmin())
|
||||
->withoutField('password')
|
||||
->find($id);
|
||||
@@ -96,7 +96,7 @@ class IndexBase extends AdminController
|
||||
*/
|
||||
public function editPassword()
|
||||
{
|
||||
$id = $this->sessionAdmin->id;
|
||||
$id = $this->getAdminId();
|
||||
$row = (new SystemAdmin())
|
||||
->withoutField('password')
|
||||
->find($id);
|
||||
@@ -140,7 +140,7 @@ class IndexBase extends AdminController
|
||||
{
|
||||
$pid = $this->request->param('pid', 0);
|
||||
|
||||
$menuService = new MenuService($this->sessionAdmin->id);
|
||||
$menuService = new MenuService($this->getAdminId());
|
||||
|
||||
$home_info = $menuService->getHomeInfo();
|
||||
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* @internal-framework
|
||||
*
|
||||
* 此文件为框架核心初始化数据
|
||||
*
|
||||
* 用途:商城分类演示数据初始化
|
||||
* 维护者:框架维护者
|
||||
*
|
||||
* 注意:此文件属于框架内核,业务开发者不应修改
|
||||
*/
|
||||
|
||||
$ul_mall_cate = array(
|
||||
array(
|
||||
"title" => "手机",
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* @internal-framework
|
||||
*
|
||||
* 此文件为框架核心初始化数据
|
||||
*
|
||||
* 用途:商城商品演示数据初始化
|
||||
* 维护者:框架维护者
|
||||
*
|
||||
* 注意:此文件属于框架内核,业务开发者不应修改
|
||||
*/
|
||||
|
||||
$ul_mall_goods = array(
|
||||
array(
|
||||
"title" => "落地-风扇",
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* @internal-framework
|
||||
*
|
||||
* 此文件为框架核心初始化数据
|
||||
*
|
||||
* 用途:商城标签演示数据初始化
|
||||
* 维护者:框架维护者
|
||||
*
|
||||
* 注意:此文件属于框架内核,业务开发者不应修改
|
||||
*/
|
||||
|
||||
$ul_mall_tag = array(
|
||||
array(
|
||||
"id" => 1,
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* @internal-framework
|
||||
*
|
||||
* 此文件为框架核心初始化数据
|
||||
*
|
||||
* 用途:系统角色初始化
|
||||
* 维护者:框架维护者
|
||||
*
|
||||
* 注意:此文件属于框架内核,业务开发者不应修改
|
||||
*/
|
||||
|
||||
$ul_system_auth = array(
|
||||
array(
|
||||
"id" => 1,
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* @internal-framework
|
||||
*
|
||||
* 此文件为框架核心初始化数据
|
||||
*
|
||||
* 用途:系统权限节点初始化
|
||||
* 维护者:框架维护者
|
||||
*
|
||||
* 注意:此文件属于框架内核,业务开发者不应修改
|
||||
*/
|
||||
|
||||
$ul_system_auth_node = array(
|
||||
|
||||
);
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* @internal-framework
|
||||
*
|
||||
* 此文件为框架核心初始化数据
|
||||
*
|
||||
* 用途:系统配置初始化(包括上传、短信、站点、微信、OSS等配置项)
|
||||
* 维护者:框架维护者
|
||||
*
|
||||
* 注意:此文件属于框架内核,业务开发者不应修改
|
||||
*/
|
||||
|
||||
$ul_system_config = array(
|
||||
array(
|
||||
"name" => "alisms_access_key_id",
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* @internal-framework
|
||||
*
|
||||
* 此文件为框架核心初始化数据
|
||||
*
|
||||
* 用途:系统菜单初始化(系统管理、商城管理等菜单结构)
|
||||
* 维护者:框架维护者
|
||||
*
|
||||
* 注意:此文件属于框架内核,业务开发者不应修改
|
||||
*/
|
||||
|
||||
$ul_system_menu = array(
|
||||
array(
|
||||
"id" => 227,
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* @internal-framework
|
||||
*
|
||||
* 此文件为框架核心初始化数据
|
||||
*
|
||||
* 用途:系统快捷入口初始化
|
||||
* 维护者:框架维护者
|
||||
*
|
||||
* 注意:此文件属于框架内核,业务开发者不应修改
|
||||
*/
|
||||
|
||||
$ul_system_quick = array(
|
||||
array(
|
||||
"title" => "管理员管理",
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* @internal-framework
|
||||
*
|
||||
* 此文件为框架内置功能
|
||||
*
|
||||
* 用途:框架版本更新代码(v2.0.74)- JS代码属性名称替换
|
||||
* 维护者:框架维护者
|
||||
*
|
||||
* 注意:此文件属于框架内核,业务开发者不应修改
|
||||
*/
|
||||
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
|
||||
@@ -263,6 +263,11 @@ class BuildCurdServiceBase
|
||||
{
|
||||
$this->table = $table;
|
||||
|
||||
// 自动检测并处理表前缀
|
||||
// 支持用户输入带前缀(ul_daka_record)或不带前缀(daka_record)两种方式
|
||||
// 注意:tablePrefix 可能已包含下划线(如 'ul_'),需要先检测
|
||||
$this->table = $this->stripTablePrefix($this->table);
|
||||
|
||||
$schemeClass = 'app\\admin\\scheme\\' . Str::studly($this->table);
|
||||
if (!class_exists($schemeClass)) {
|
||||
throw new TableException("未找到 {$schemeClass},请先执行:php think scheme:make -t {$this->table} 或手动创建 Scheme");
|
||||
@@ -352,6 +357,28 @@ class BuildCurdServiceBase
|
||||
return $controllerFilename;
|
||||
}
|
||||
|
||||
/**
|
||||
* 去除表前缀.
|
||||
* @param string $tableName
|
||||
* @return string
|
||||
*/
|
||||
protected function stripTablePrefix($tableName)
|
||||
{
|
||||
$prefixToStrip = $this->tablePrefix;
|
||||
|
||||
// 如果 tablePrefix 不以 _ 结尾,加上下划线
|
||||
if (!str_ends_with($prefixToStrip, '_')) {
|
||||
$prefixToStrip .= '_';
|
||||
}
|
||||
|
||||
if (str_starts_with($tableName, $prefixToStrip)) {
|
||||
// 用户输入了带前缀的表名,自动去除前缀
|
||||
return substr($tableName, strlen($prefixToStrip));
|
||||
}
|
||||
|
||||
return $tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置关联表.
|
||||
* @param $relationTable
|
||||
@@ -365,6 +392,9 @@ class BuildCurdServiceBase
|
||||
*/
|
||||
public function setRelation($relationTable, $foreignKey, $primaryKey = null, $modelFilename = null, $onlyShowFileds = [], $bindSelectField = null)
|
||||
{
|
||||
// 自动检测并处理关联表前缀(与 setTable 保持一致)
|
||||
$relationTable = $this->stripTablePrefix($relationTable);
|
||||
|
||||
if (!isset($this->tableColumns[$foreignKey])) {
|
||||
throw new TableException("主表不存在外键字段:{$foreignKey}");
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ class OutputBase extends Output
|
||||
$question = $this->appendForceForceTip($question);
|
||||
|
||||
if ($this->isForceForceEnabled($input)) {
|
||||
return true;
|
||||
return $default;
|
||||
}
|
||||
|
||||
return parent::confirm($input, $question, $default);
|
||||
|
||||
@@ -134,6 +134,40 @@ class AdminControllerBase extends BaseController
|
||||
$this->assign('Controller', $this, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录管理员ID(控制器上下文)。
|
||||
*
|
||||
* 优先使用控制器已完成鉴权后的上下文($this->sessionAdmin),
|
||||
* 在非标准场景下兼容从 Request 挂载的 adminInfo 读取,
|
||||
* 最后兜底到全局函数 get_session_admin('id')(适用于无控制器上下文的通用读取方式)。
|
||||
*
|
||||
* @param mixed $default 未登录或无法获取时返回的默认值
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAdminId($default = null)
|
||||
{
|
||||
if (!empty($this->sessionAdmin) && isset($this->sessionAdmin->id)) {
|
||||
return $this->sessionAdmin->id;
|
||||
}
|
||||
|
||||
if (isset($this->request->adminInfo)) {
|
||||
$adminInfo = $this->request->adminInfo;
|
||||
if (is_array($adminInfo) && isset($adminInfo['id'])) {
|
||||
return $adminInfo['id'];
|
||||
}
|
||||
if (is_object($adminInfo) && isset($adminInfo->id)) {
|
||||
return $adminInfo->id;
|
||||
}
|
||||
}
|
||||
|
||||
$adminId = get_session_admin('id');
|
||||
if ($adminId !== null && $adminId !== '') {
|
||||
return $adminId;
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function initSort()
|
||||
{
|
||||
$sort = $this->request->param('sort');
|
||||
@@ -232,6 +266,10 @@ class AdminControllerBase extends BaseController
|
||||
*/
|
||||
public function fetch($template = '', $vars = [])
|
||||
{
|
||||
if (Config::get('app.auto_parse_api') && $this->request->param('get_page_data') && !$this->request->isJson()) {
|
||||
return json_message([], 400, '使用 get_page_data 获取页面数据时,请设置请求头 Accept: application/json');
|
||||
}
|
||||
|
||||
$this->assign('data_brage', json_encode($this->dataBrage));
|
||||
$this->assign('content_js', $this->fetchJS($template), -1);
|
||||
|
||||
|
||||
@@ -19,5 +19,14 @@ class Field
|
||||
public bool $autoIncrement = false,
|
||||
public bool $primary = false
|
||||
) {
|
||||
$type = strtolower(trim($this->type));
|
||||
|
||||
if ($this->length !== null && $this->length < 1) {
|
||||
throw new \InvalidArgumentException("Scheme 字段 length 必须 >= 1,当前={$this->length}");
|
||||
}
|
||||
|
||||
if ($type === 'char' && $this->length !== null && $this->length > 255) {
|
||||
throw new \InvalidArgumentException("Scheme 字段类型 char 的 length 超出范围,允许 1-255,当前={$this->length}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
326
extend/base/common/service/ErrorHandlerBase.php
Normal file
326
extend/base/common/service/ErrorHandlerBase.php
Normal file
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\service;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
abstract class ErrorHandlerBase
|
||||
{
|
||||
/**
|
||||
* 错误码注册表
|
||||
*/
|
||||
protected array $errorCodes = [
|
||||
// 通用错误 (ERR_GEN_XXX)
|
||||
'ERR_GEN_UNKNOWN' => '未知错误',
|
||||
'ERR_GEN_INVALID_PARAM' => '参数错误',
|
||||
'ERR_GEN_MISSING_PARAM' => '缺少必需参数',
|
||||
'ERR_GEN_OPERATION_FAILED' => '操作失败',
|
||||
'ERR_GEN_PERMISSION_DENIED' => '权限不足',
|
||||
'ERR_GEN_NOT_FOUND' => '资源不存在',
|
||||
'ERR_GEN_TIMEOUT' => '操作超时',
|
||||
|
||||
// 数据库错误 (ERR_DB_XXX)
|
||||
'ERR_DB_CONNECTION' => '数据库连接失败',
|
||||
'ERR_DB_QUERY' => '数据库查询失败',
|
||||
'ERR_DB_INSERT' => '数据插入失败',
|
||||
'ERR_DB_UPDATE' => '数据更新失败',
|
||||
'ERR_DB_DELETE' => '数据删除失败',
|
||||
'ERR_DB_TRANSACTION' => '数据库事务执行失败',
|
||||
'ERR_DB_SCHEME_MISMATCH' => '数据库结构与期望不符',
|
||||
|
||||
// 文件系统错误 (ERR_FS_XXX)
|
||||
'ERR_FS_NOT_FOUND' => '文件不存在',
|
||||
'ERR_FS_READ_FAILED' => '文件读取失败',
|
||||
'ERR_FS_WRITE_FAILED' => '文件写入失败',
|
||||
'ERR_FS_DELETE_FAILED' => '文件删除失败',
|
||||
'ERR_FS_PERMISSION' => '文件权限不足',
|
||||
|
||||
// 网络错误 (ERR_NET_XXX)
|
||||
'ERR_NET_CONNECTION' => '网络连接失败',
|
||||
'ERR_NET_TIMEOUT' => '网络请求超时',
|
||||
'ERR_NET_RESPONSE' => '网络响应错误',
|
||||
|
||||
// 配置错误 (ERR_CFG_XXX)
|
||||
'ERR_CFG_MISSING' => '配置缺失',
|
||||
'ERR_CFG_INVALID' => '配置无效',
|
||||
];
|
||||
|
||||
/**
|
||||
* 修复建议注册表
|
||||
*/
|
||||
protected array $suggestions = [
|
||||
// 通用错误修复建议
|
||||
'ERR_GEN_UNKNOWN' => '未知错误,请稍后重试或联系管理员',
|
||||
'ERR_GEN_INVALID_PARAM' => '请检查传入的参数是否符合要求',
|
||||
'ERR_GEN_MISSING_PARAM' => '请确保所有必需参数都已提供',
|
||||
'ERR_GEN_OPERATION_FAILED' => '请稍后重试,如果问题持续存在请联系管理员',
|
||||
'ERR_GEN_PERMISSION_DENIED' => '请检查您是否有执行此操作的权限',
|
||||
'ERR_GEN_NOT_FOUND' => '请确认请求的资源ID是否正确',
|
||||
'ERR_GEN_TIMEOUT' => '请检查网络连接或稍后重试',
|
||||
|
||||
// 数据库错误修复建议
|
||||
'ERR_DB_CONNECTION' => '请检查数据库连接配置和网络连接',
|
||||
'ERR_DB_QUERY' => '请检查SQL语句语法和数据库表结构',
|
||||
'ERR_DB_INSERT' => '请检查数据是否符合表结构和约束条件',
|
||||
'ERR_DB_UPDATE' => '请确保要更新的数据存在且符合约束条件',
|
||||
'ERR_DB_DELETE' => '请确保要删除的数据存在',
|
||||
'ERR_DB_TRANSACTION' => '请检查事务中的操作是否都执行成功',
|
||||
'ERR_DB_SCHEME_MISMATCH' => '请运行数据库迁移命令: php think migrate:run',
|
||||
|
||||
// 文件系统错误修复建议
|
||||
'ERR_FS_NOT_FOUND' => '请检查文件路径是否正确',
|
||||
'ERR_FS_READ_FAILED' => '请检查文件是否存在且有读取权限',
|
||||
'ERR_FS_WRITE_FAILED' => '请检查目录是否存在且有写入权限',
|
||||
'ERR_FS_DELETE_FAILED' => '请检查文件是否存在且有删除权限',
|
||||
'ERR_FS_PERMISSION' => '请检查文件或目录的权限设置',
|
||||
|
||||
// 网络错误修复建议
|
||||
'ERR_NET_CONNECTION' => '请检查网络连接和目标服务器状态',
|
||||
'ERR_NET_TIMEOUT' => '请检查网络连接或增加超时时间',
|
||||
'ERR_NET_RESPONSE' => '请检查请求参数和服务器响应',
|
||||
|
||||
// 配置错误修复建议
|
||||
'ERR_CFG_MISSING' => '请在配置文件中添加缺失的配置项',
|
||||
'ERR_CFG_INVALID' => '请检查配置项的值是否符合要求',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取结构化错误信息
|
||||
*
|
||||
* @param string $errorCode 错误码
|
||||
* @param string|null $errorMessage 错误消息(覆盖默认错误消息)
|
||||
* @param array $context 上下文信息
|
||||
* @return array 结构化错误信息
|
||||
*/
|
||||
public function getError(string $errorCode, ?string $errorMessage = null, array $context = []): array
|
||||
{
|
||||
$defaultMessage = $this->errorCodes[$errorCode] ?? $this->errorCodes['ERR_GEN_UNKNOWN'];
|
||||
$actualMessage = $errorMessage ?? $defaultMessage;
|
||||
|
||||
$error = [
|
||||
'success' => false,
|
||||
'error_code' => $errorCode,
|
||||
'error_message' => $actualMessage,
|
||||
];
|
||||
|
||||
// 添加文件和行信息(如果存在)
|
||||
if (isset($context['file'])) {
|
||||
$error['file'] = $context['file'];
|
||||
}
|
||||
if (isset($context['line'])) {
|
||||
$error['line'] = $context['line'];
|
||||
}
|
||||
|
||||
// 添加修复建议
|
||||
$suggestion = $this->suggestions[$errorCode] ?? $this->suggestions['ERR_GEN_UNKNOWN'];
|
||||
$error['suggestion'] = $suggestion;
|
||||
|
||||
// 添加额外的上下文信息
|
||||
if (!empty($context)) {
|
||||
$error['context'] = $context;
|
||||
}
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从异常生成结构化错误信息
|
||||
*
|
||||
* @param Throwable $exception 异常对象
|
||||
* @param string|null $customErrorCode 自定义错误码
|
||||
* @return array 结构化错误信息
|
||||
*/
|
||||
public function getErrorForException(Throwable $exception, ?string $customErrorCode = null): array
|
||||
{
|
||||
// 尝试从异常消息推断错误码
|
||||
$errorCode = $customErrorCode ?? $this->inferErrorCodeFromException($exception);
|
||||
|
||||
$error = [
|
||||
'success' => false,
|
||||
'error_code' => $errorCode,
|
||||
'error_message' => $exception->getMessage(),
|
||||
];
|
||||
|
||||
// 添加文件和行信息
|
||||
if ($exception->getFile()) {
|
||||
$error['file'] = $exception->getFile();
|
||||
}
|
||||
if ($exception->getLine()) {
|
||||
$error['line'] = $exception->getLine();
|
||||
}
|
||||
|
||||
// 添加修复建议
|
||||
$suggestion = $this->suggestions[$errorCode] ?? $this->suggestions['ERR_GEN_UNKNOWN'];
|
||||
$error['suggestion'] = $suggestion;
|
||||
|
||||
// 添加异常类型
|
||||
$error['exception_type'] = get_class($exception);
|
||||
|
||||
// 添加堆栈跟踪(在开发模式下)
|
||||
if ($this->isDebugMode()) {
|
||||
$error['trace'] = $this->formatTrace($exception);
|
||||
}
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从异常推断错误码
|
||||
*
|
||||
* @param Throwable $exception 异常对象
|
||||
* @return string 错误码
|
||||
*/
|
||||
protected function inferErrorCodeFromException(Throwable $exception): string
|
||||
{
|
||||
$message = $exception->getMessage();
|
||||
$class = get_class($exception);
|
||||
|
||||
// 根据异常类型推断
|
||||
if (strpos($class, 'Db') !== false || strpos($class, 'Query') !== false) {
|
||||
if (strpos($message, 'connection') !== false || strpos($message, 'connect') !== false) {
|
||||
return 'ERR_DB_CONNECTION';
|
||||
}
|
||||
if (strpos($message, 'SQLSTATE') !== false) {
|
||||
return 'ERR_DB_QUERY';
|
||||
}
|
||||
return 'ERR_DB_OPERATION_FAILED';
|
||||
}
|
||||
|
||||
if (strpos($class, 'File') !== false || strpos($class, 'Stream') !== false) {
|
||||
if (strpos($message, 'No such file') !== false || strpos($message, 'not found') !== false) {
|
||||
return 'ERR_FS_NOT_FOUND';
|
||||
}
|
||||
if (strpos($message, 'permission') !== false) {
|
||||
return 'ERR_FS_PERMISSION';
|
||||
}
|
||||
return 'ERR_FS_READ_FAILED';
|
||||
}
|
||||
|
||||
if (strpos($class, 'Network') !== false || strpos($class, 'Curl') !== false) {
|
||||
if (strpos($message, 'timeout') !== false) {
|
||||
return 'ERR_NET_TIMEOUT';
|
||||
}
|
||||
return 'ERR_NET_CONNECTION';
|
||||
}
|
||||
|
||||
// 根据消息内容推断
|
||||
if (strpos($message, 'permission') !== false || strpos($message, 'denied') !== false) {
|
||||
return 'ERR_GEN_PERMISSION_DENIED';
|
||||
}
|
||||
|
||||
if (strpos($message, 'not found') !== false || strpos($message, '不存在') !== false) {
|
||||
return 'ERR_GEN_NOT_FOUND';
|
||||
}
|
||||
|
||||
if (strpos($message, 'timeout') !== false) {
|
||||
return 'ERR_GEN_TIMEOUT';
|
||||
}
|
||||
|
||||
return 'ERR_GEN_UNKNOWN';
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化堆栈跟踪
|
||||
*
|
||||
* @param Throwable $exception 异常对象
|
||||
* @return array 格式化的堆栈跟踪
|
||||
*/
|
||||
protected function formatTrace(Throwable $exception): array
|
||||
{
|
||||
$trace = [];
|
||||
foreach ($exception->getTrace() as $index => $frame) {
|
||||
$traceItem = [
|
||||
'index' => $index,
|
||||
];
|
||||
|
||||
if (isset($frame['file'])) {
|
||||
$traceItem['file'] = $frame['file'];
|
||||
}
|
||||
if (isset($frame['line'])) {
|
||||
$traceItem['line'] = $frame['line'];
|
||||
}
|
||||
if (isset($frame['function'])) {
|
||||
$traceItem['function'] = $frame['function'];
|
||||
}
|
||||
if (isset($frame['class'])) {
|
||||
$traceItem['class'] = $frame['class'];
|
||||
}
|
||||
if (isset($frame['type'])) {
|
||||
$traceItem['type'] = $frame['type'];
|
||||
}
|
||||
|
||||
$trace[] = $traceItem;
|
||||
}
|
||||
return $trace;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为调试模式
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isDebugMode(): bool
|
||||
{
|
||||
try {
|
||||
$app = \think\facade\App::instance();
|
||||
return (bool)($app->config->get('app.app_debug') ?? false);
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出错误(作为JSON)
|
||||
*
|
||||
* @param array $error 错误信息
|
||||
* @return string JSON格式的错误信息
|
||||
*/
|
||||
public function outputError(array $error): string
|
||||
{
|
||||
$options = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
|
||||
return json_encode($error, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册自定义错误码
|
||||
*
|
||||
* @param string $errorCode 错误码
|
||||
* @param string $errorMessage 错误消息
|
||||
* @param string|null $suggestion 修复建议
|
||||
* @return void
|
||||
*/
|
||||
public function registerErrorCode(string $errorCode, string $errorMessage, ?string $suggestion = null): void
|
||||
{
|
||||
$this->errorCodes[$errorCode] = $errorMessage;
|
||||
if ($suggestion !== null) {
|
||||
$this->suggestions[$errorCode] = $suggestion;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量注册错误码
|
||||
*
|
||||
* @param array $errors 错误码数组,格式:['ERR_XXX' => ['message' => '...', 'suggestion' => '...']]
|
||||
* @return void
|
||||
*/
|
||||
public function registerErrorCodes(array $errors): void
|
||||
{
|
||||
foreach ($errors as $errorCode => $config) {
|
||||
$this->errorCodes[$errorCode] = $config['message'] ?? '未知错误';
|
||||
if (isset($config['suggestion'])) {
|
||||
$this->suggestions[$errorCode] = $config['suggestion'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的错误码
|
||||
*
|
||||
* @return array 错误码数组
|
||||
*/
|
||||
public function getRegisteredErrorCodes(): array
|
||||
{
|
||||
return $this->errorCodes;
|
||||
}
|
||||
}
|
||||
@@ -111,4 +111,198 @@ class MenuServiceBase
|
||||
|
||||
return $menuData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建菜单
|
||||
*
|
||||
* @param array $data 菜单数据
|
||||
* @return int 返回新创建的菜单ID
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function create(array $data): int
|
||||
{
|
||||
$menu = Db::name('system_menu');
|
||||
|
||||
// 准备数据
|
||||
$insertData = [
|
||||
'pid' => $data['parent_id'] ?? 0,
|
||||
'title' => $data['title'] ?? '',
|
||||
'icon' => $data['icon'] ?? '',
|
||||
'href' => $data['path'] ?? '',
|
||||
'auth_node' => $data['node'] ?? '',
|
||||
'sort' => $data['sort'] ?? 100,
|
||||
'status' => 1,
|
||||
'target' => '_self',
|
||||
'remark' => $data['remark'] ?? '',
|
||||
'create_time' => time(),
|
||||
'update_time' => time(),
|
||||
'delete_time' => 0,
|
||||
];
|
||||
|
||||
// 验证必填字段
|
||||
if (empty($insertData['title'])) {
|
||||
throw new \Exception('菜单标题不能为空');
|
||||
}
|
||||
|
||||
// 验证父菜单是否存在
|
||||
if ($insertData['pid'] > 0) {
|
||||
$parentMenu = Db::name('system_menu')
|
||||
->where('id', $insertData['pid'])
|
||||
->where('delete_time', 0)
|
||||
->find();
|
||||
if (empty($parentMenu)) {
|
||||
throw new \Exception("父菜单ID {$insertData['pid']} 不存在");
|
||||
}
|
||||
}
|
||||
|
||||
// 插入数据
|
||||
$menuId = $menu->insertGetId($insertData);
|
||||
|
||||
if (!$menuId) {
|
||||
throw new \Exception('菜单创建失败');
|
||||
}
|
||||
|
||||
return (int)$menuId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新菜单
|
||||
*
|
||||
* @param int $menuId 菜单ID
|
||||
* @param array $data 更新数据
|
||||
* @return bool 是否更新成功
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function update(int $menuId, array $data): bool
|
||||
{
|
||||
// 验证菜单是否存在
|
||||
$menu = Db::name('system_menu')
|
||||
->where('id', $menuId)
|
||||
->where('delete_time', 0)
|
||||
->find();
|
||||
|
||||
if (empty($menu)) {
|
||||
throw new \Exception("菜单ID {$menuId} 不存在");
|
||||
}
|
||||
|
||||
// 准备更新数据
|
||||
$updateData = [
|
||||
'update_time' => time(),
|
||||
];
|
||||
|
||||
if (isset($data['title'])) {
|
||||
$updateData['title'] = $data['title'];
|
||||
}
|
||||
if (isset($data['parent_id'])) {
|
||||
$updateData['pid'] = $data['parent_id'];
|
||||
}
|
||||
if (isset($data['icon'])) {
|
||||
$updateData['icon'] = $data['icon'];
|
||||
}
|
||||
if (isset($data['path'])) {
|
||||
$updateData['href'] = $data['path'];
|
||||
}
|
||||
if (isset($data['node'])) {
|
||||
$updateData['auth_node'] = $data['node'];
|
||||
}
|
||||
if (isset($data['sort'])) {
|
||||
$updateData['sort'] = $data['sort'];
|
||||
}
|
||||
if (isset($data['status'])) {
|
||||
$updateData['status'] = $data['status'];
|
||||
}
|
||||
if (isset($data['remark'])) {
|
||||
$updateData['remark'] = $data['remark'];
|
||||
}
|
||||
if (isset($data['target'])) {
|
||||
$updateData['target'] = $data['target'];
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if (empty($menu['title']) && empty($updateData['title'])) {
|
||||
throw new \Exception('菜单标题不能为空');
|
||||
}
|
||||
|
||||
// 验证父菜单是否存在(如果修改了父菜单)
|
||||
if (isset($updateData['pid']) && $updateData['pid'] > 0) {
|
||||
// 不能将自己设置为父菜单
|
||||
if ($updateData['pid'] == $menuId) {
|
||||
throw new \Exception('不能将自己设置为父菜单');
|
||||
}
|
||||
|
||||
$parentMenu = Db::name('system_menu')
|
||||
->where('id', $updateData['pid'])
|
||||
->where('delete_time', 0)
|
||||
->find();
|
||||
if (empty($parentMenu)) {
|
||||
throw new \Exception("父菜单ID {$updateData['pid']} 不存在");
|
||||
}
|
||||
}
|
||||
|
||||
// 更新数据
|
||||
$result = Db::name('system_menu')
|
||||
->where('id', $menuId)
|
||||
->update($updateData);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除菜单
|
||||
*
|
||||
* @param int $menuId 菜单ID
|
||||
* @return bool 是否删除成功
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function delete(int $menuId): bool
|
||||
{
|
||||
// 验证菜单是否存在
|
||||
$menu = Db::name('system_menu')
|
||||
->where('id', $menuId)
|
||||
->where('delete_time', 0)
|
||||
->find();
|
||||
|
||||
if (empty($menu)) {
|
||||
throw new \Exception("菜单ID {$menuId} 不存在");
|
||||
}
|
||||
|
||||
// 检查是否有子菜单
|
||||
$hasChildren = Db::name('system_menu')
|
||||
->where('pid', $menuId)
|
||||
->where('delete_time', 0)
|
||||
->count();
|
||||
|
||||
if ($hasChildren > 0) {
|
||||
throw new \Exception('该菜单下存在子菜单,请先删除子菜单');
|
||||
}
|
||||
|
||||
// 软删除
|
||||
$result = Db::name('system_menu')
|
||||
->where('id', $menuId)
|
||||
->update([
|
||||
'delete_time' => time(),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单详情
|
||||
*
|
||||
* @param int $menuId 菜单ID
|
||||
* @return array|null 菜单数据
|
||||
* @throws \think\db\exception\DataNotFoundException
|
||||
* @throws \think\db\exception\DbException
|
||||
* @throws \think\db\exception\ModelNotFoundException
|
||||
*/
|
||||
public function getMenu(int $menuId): ?array
|
||||
{
|
||||
$menu = Db::name('system_menu')
|
||||
->where('id', $menuId)
|
||||
->where('delete_time', 0)
|
||||
->find();
|
||||
|
||||
return $menu ? $menu : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ class SchemeToDbService
|
||||
$fullTableName = $prefix . $tableName;
|
||||
}
|
||||
|
||||
$sql = $this->buildCreateTableSql($fullTableName, $tableAttr, $ref);
|
||||
|
||||
// 检查表是否存在
|
||||
$tableExists = $this->checkTableExists($connection, $fullTableName);
|
||||
$backupTableName = null;
|
||||
@@ -66,7 +68,6 @@ class SchemeToDbService
|
||||
}
|
||||
|
||||
// 2. 建表
|
||||
$sql = $this->buildCreateTableSql($fullTableName, $tableAttr, $ref);
|
||||
Db::connect($connection)->execute($sql);
|
||||
|
||||
// 3. 恢复数据
|
||||
@@ -121,88 +122,159 @@ class SchemeToDbService
|
||||
$schemeIndexes = $this->buildSchemeIndexSignature($ref);
|
||||
$dbIndexes = $this->buildDbIndexSignature($dbKeysRows);
|
||||
|
||||
$diffs = [];
|
||||
$missingFields = [];
|
||||
$extraFields = [];
|
||||
$changedFields = [];
|
||||
|
||||
foreach ($schemeColumns as $field => $sig) {
|
||||
if (!isset($dbColumns[$field])) {
|
||||
$diffs[] = "缺少字段:{$field}";
|
||||
$missingFields[] = $field;
|
||||
continue;
|
||||
}
|
||||
|
||||
$row = $dbColumns[$field];
|
||||
$fieldChanges = [];
|
||||
|
||||
$dbType = $this->normalizeDbType((string)$row['Type']);
|
||||
$schemeType = $this->normalizeDbType($sig['type']);
|
||||
if ($dbType !== $schemeType) {
|
||||
$diffs[] = "字段类型不一致:{$field} DB={$dbType} Scheme={$schemeType}";
|
||||
$fieldChanges['type'] = ['db' => $dbType, 'scheme' => $schemeType];
|
||||
}
|
||||
|
||||
$dbNull = (string)$row['Null'];
|
||||
$schemeNull = $sig['null'];
|
||||
if ($dbNull !== $schemeNull) {
|
||||
$diffs[] = "字段可空不一致:{$field} DB={$dbNull} Scheme={$schemeNull}";
|
||||
$fieldChanges['null'] = ['db' => $dbNull, 'scheme' => $schemeNull];
|
||||
}
|
||||
|
||||
$dbDefault = $row['Default'];
|
||||
$schemeDefault = $sig['default'];
|
||||
if (!$this->defaultEquals($dbDefault, $schemeDefault)) {
|
||||
$dbStr = is_null($dbDefault) ? 'NULL' : (string)$dbDefault;
|
||||
$schemeStr = is_null($schemeDefault) ? 'NULL' : (string)$schemeDefault;
|
||||
$diffs[] = "字段默认值不一致:{$field} DB={$dbStr} Scheme={$schemeStr}";
|
||||
$fieldChanges['default'] = [
|
||||
'db' => $this->stringifyDefault($dbDefault),
|
||||
'scheme' => $this->stringifyDefault($schemeDefault),
|
||||
];
|
||||
}
|
||||
|
||||
$dbExtra = (string)$row['Extra'];
|
||||
$schemeExtra = $sig['extra'];
|
||||
if ($dbExtra !== $schemeExtra) {
|
||||
$diffs[] = "字段 Extra 不一致:{$field} DB={$dbExtra} Scheme={$schemeExtra}";
|
||||
$fieldChanges['extra'] = ['db' => $dbExtra, 'scheme' => $schemeExtra];
|
||||
}
|
||||
|
||||
$dbPrimary = (string)$row['Key'] === 'PRI';
|
||||
if ($dbPrimary !== $sig['primary']) {
|
||||
$diffs[] = "字段主键不一致:{$field} DB=" . ($dbPrimary ? 'PRI' : '') . " Scheme=" . ($sig['primary'] ? 'PRI' : '');
|
||||
$fieldChanges['primary'] = [
|
||||
'db' => $dbPrimary ? 'PRI' : '',
|
||||
'scheme' => $sig['primary'] ? 'PRI' : '',
|
||||
];
|
||||
}
|
||||
|
||||
$dbComment = (string)$row['Comment'];
|
||||
$schemeComment = $sig['comment'];
|
||||
if ($this->parseComment($dbComment) !== $this->parseComment($schemeComment)) {
|
||||
$diffs[] = "字段注释不一致:{$field} DB={$dbComment} Scheme={$schemeComment}";
|
||||
$fieldChanges['comment'] = ['db' => $dbComment, 'scheme' => $schemeComment];
|
||||
}
|
||||
|
||||
if (!empty($fieldChanges)) {
|
||||
$changedFields[$field] = $fieldChanges;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($dbColumns as $field => $row) {
|
||||
if (!isset($schemeColumns[$field])) {
|
||||
$diffs[] = "多余字段:{$field}";
|
||||
$extraFields[] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
$missingIndexes = [];
|
||||
$extraIndexes = [];
|
||||
$changedIndexes = [];
|
||||
|
||||
foreach ($schemeIndexes as $name => $idx) {
|
||||
if (!isset($dbIndexes[$name])) {
|
||||
$diffs[] = "缺少索引:{$name}";
|
||||
$missingIndexes[] = $name;
|
||||
continue;
|
||||
}
|
||||
|
||||
$dbIdx = $dbIndexes[$name];
|
||||
$idxChanges = [];
|
||||
|
||||
if ($dbIdx['type'] !== $idx['type']) {
|
||||
$diffs[] = "索引类型不一致:{$name} DB={$dbIdx['type']} Scheme={$idx['type']}";
|
||||
$idxChanges['type'] = ['db' => $dbIdx['type'], 'scheme' => $idx['type']];
|
||||
}
|
||||
|
||||
if ($dbIdx['columns'] !== $idx['columns']) {
|
||||
$diffs[] = "索引字段不一致:{$name} DB=" . implode(',', $dbIdx['columns']) . " Scheme=" . implode(',', $idx['columns']);
|
||||
$idxChanges['columns'] = [
|
||||
'db' => implode(',', $dbIdx['columns']),
|
||||
'scheme' => implode(',', $idx['columns']),
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($idxChanges)) {
|
||||
$changedIndexes[$name] = $idxChanges;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($dbIndexes as $name => $idx) {
|
||||
if (!isset($schemeIndexes[$name])) {
|
||||
$diffs[] = "多余索引:{$name}";
|
||||
$extraIndexes[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$tableCommentDiff = $this->diffTableComment($connection, $fullTableName, $tableAttr->comment);
|
||||
if ($tableCommentDiff !== null) {
|
||||
$diffs[] = $tableCommentDiff;
|
||||
$hasDiff = !empty($missingFields) || !empty($extraFields) || !empty($changedFields) || !empty($missingIndexes) || !empty($extraIndexes) || !empty($changedIndexes) || $tableCommentDiff !== null;
|
||||
if (!$hasDiff) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $diffs;
|
||||
$lines = [];
|
||||
$lines[] = '差异汇总:'
|
||||
. '字段缺失=' . count($missingFields)
|
||||
. ',字段多余=' . count($extraFields)
|
||||
. ',字段修改=' . count($changedFields)
|
||||
. ';索引缺失=' . count($missingIndexes)
|
||||
. ',索引多余=' . count($extraIndexes)
|
||||
. ',索引修改=' . count($changedIndexes)
|
||||
. ';表注释=' . ($tableCommentDiff !== null ? '不一致' : '一致');
|
||||
|
||||
if (!empty($missingFields)) {
|
||||
$lines[] = '字段缺失(' . count($missingFields) . '):' . implode(',', $missingFields);
|
||||
}
|
||||
if (!empty($extraFields)) {
|
||||
$lines[] = '字段多余(' . count($extraFields) . '):' . implode(',', $extraFields);
|
||||
}
|
||||
if (!empty($changedFields)) {
|
||||
$lines[] = '字段修改(' . count($changedFields) . '):' . implode(',', array_keys($changedFields));
|
||||
foreach ($changedFields as $field => $changes) {
|
||||
$lines[] = '字段 ' . $field . ':';
|
||||
foreach ($changes as $k => $v) {
|
||||
$lines[] = ' - ' . $k . ':DB=' . $v['db'] . ' Scheme=' . $v['scheme'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($missingIndexes)) {
|
||||
$lines[] = '索引缺失(' . count($missingIndexes) . '):' . implode(',', $missingIndexes);
|
||||
}
|
||||
if (!empty($extraIndexes)) {
|
||||
$lines[] = '索引多余(' . count($extraIndexes) . '):' . implode(',', $extraIndexes);
|
||||
}
|
||||
if (!empty($changedIndexes)) {
|
||||
$lines[] = '索引修改(' . count($changedIndexes) . '):' . implode(',', array_keys($changedIndexes));
|
||||
foreach ($changedIndexes as $name => $changes) {
|
||||
$lines[] = '索引 ' . $name . ':';
|
||||
foreach ($changes as $k => $v) {
|
||||
$lines[] = ' - ' . $k . ':DB=' . $v['db'] . ' Scheme=' . $v['scheme'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($tableCommentDiff !== null) {
|
||||
$lines[] = $tableCommentDiff;
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
public function getColumnsForCurd(string $className, array $onlyFields = []): array
|
||||
@@ -227,6 +299,7 @@ class SchemeToDbService
|
||||
|
||||
/** @var Field $field */
|
||||
$field = $fieldAttrs[0]->newInstance();
|
||||
$this->validateSchemeField($field, $ref->getName(), $fieldName);
|
||||
|
||||
$columns[$fieldName] = [
|
||||
'type' => $this->buildMysqlTypeFromSchemeField($field),
|
||||
@@ -283,6 +356,7 @@ class SchemeToDbService
|
||||
/** @var Field $field */
|
||||
$field = $fieldAttrs[0]->newInstance();
|
||||
$fieldName = $prop->getName();
|
||||
$this->validateSchemeField($field, $ref->getName(), $fieldName);
|
||||
|
||||
$line = "`$fieldName` {$field->type}";
|
||||
|
||||
@@ -364,6 +438,20 @@ class SchemeToDbService
|
||||
return "CREATE TABLE `$tableName` (\n $body\n) ENGINE={$tableAttr->engine} DEFAULT CHARSET={$tableAttr->charset}$comment";
|
||||
}
|
||||
|
||||
protected function validateSchemeField(Field $field, string $className, string $fieldName): void
|
||||
{
|
||||
$type = strtolower(trim($field->type));
|
||||
$length = $field->length;
|
||||
|
||||
if ($length !== null && $length < 1) {
|
||||
throw new \InvalidArgumentException("Scheme 字段定义非法:{$className}::\${$fieldName} length 必须 >= 1,当前={$length}");
|
||||
}
|
||||
|
||||
if ($type === 'char' && $length !== null && $length > 255) {
|
||||
throw new \InvalidArgumentException("Scheme 字段定义非法:{$className}::\${$fieldName} type=char 的 length 超出范围,允许 1-255,当前={$length}");
|
||||
}
|
||||
}
|
||||
|
||||
protected function restoreData(string $connection, $newTable, $oldTable, array $properties)
|
||||
{
|
||||
$fields = [];
|
||||
@@ -431,6 +519,7 @@ class SchemeToDbService
|
||||
$fieldName = $prop->getName();
|
||||
/** @var Field $field */
|
||||
$field = $fieldAttrs[0]->newInstance();
|
||||
$this->validateSchemeField($field, $ref->getName(), $fieldName);
|
||||
|
||||
$sig[$fieldName] = [
|
||||
'type' => $this->buildMysqlTypeFromSchemeField($field),
|
||||
@@ -614,6 +703,23 @@ class SchemeToDbService
|
||||
return (string)$dbDefault === (string)$schemeDefault;
|
||||
}
|
||||
|
||||
protected function stringifyDefault($value): string
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return 'NULL';
|
||||
}
|
||||
|
||||
if (is_bool($value)) {
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
if (is_int($value) || is_float($value)) {
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
protected function diffTableComment(string $connection, string $tableName, string $schemeComment): ?string
|
||||
{
|
||||
$type = strtolower((string)Config::get('database.connections.' . $connection . '.type', ''));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\service;
|
||||
namespace base\common\service\tools;
|
||||
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
@@ -9,7 +9,7 @@ use think\facade\Env;
|
||||
use think\facade\Config;
|
||||
use think\Exception;
|
||||
|
||||
class ToolsDbServiceBase
|
||||
class DbServiceBase
|
||||
{
|
||||
protected $connection = 'main';
|
||||
|
||||
@@ -202,16 +202,27 @@ if (!function_exists('auth')) {
|
||||
}
|
||||
}
|
||||
|
||||
function get_session_admin($key = null)
|
||||
{
|
||||
$token = read_header_token();
|
||||
if (!empty($token)) {
|
||||
$admin = Cache::store('login')->get($token);
|
||||
} else {
|
||||
$admin = session('admin');
|
||||
}
|
||||
if (!function_exists('get_session_admin')) {
|
||||
/**
|
||||
* 获取当前登录管理员信息(全局函数)。
|
||||
*
|
||||
* 该函数用于无控制器上下文的场景(如 Service/Helper/Command 等)获取登录态数据。
|
||||
* 在控制器内建议优先使用 $this->getAdminId() / $this->sessionAdmin 获取已鉴权后的上下文。
|
||||
*
|
||||
* @param string|null $key 取值路径(支持 Arr::get 语法),为空返回整个管理员数据
|
||||
* @return mixed
|
||||
*/
|
||||
function get_session_admin($key = null)
|
||||
{
|
||||
$token = read_header_token();
|
||||
if (!empty($token)) {
|
||||
$admin = Cache::store('login')->get($token);
|
||||
} else {
|
||||
$admin = session('admin');
|
||||
}
|
||||
|
||||
return Arr::get($admin, $key);
|
||||
return Arr::get($admin, $key);
|
||||
}
|
||||
}
|
||||
|
||||
function read_header_token()
|
||||
|
||||
@@ -2,13 +2,47 @@
|
||||
|
||||
namespace think;
|
||||
|
||||
use app\common\command\Curd;
|
||||
use app\common\command\OssStatic;
|
||||
use app\common\command\Timer;
|
||||
use app\common\command\admin\Clear;
|
||||
use app\common\command\admin\MigrateFileData;
|
||||
use app\common\command\admin\ResetPassword;
|
||||
use app\common\command\admin\Update;
|
||||
use app\common\command\admin\UpdateCode;
|
||||
use app\common\command\admin\Version;
|
||||
use app\common\command\admin\menu\AdminMenuCreate;
|
||||
use app\common\command\admin\menu\AdminMenuDelete;
|
||||
use app\common\command\admin\menu\AdminMenuExport;
|
||||
use app\common\command\admin\menu\AdminMenuList;
|
||||
use app\common\command\admin\menu\AdminMenuUpdate;
|
||||
use app\common\command\admin\permission\AdminPermissionNodes;
|
||||
use app\common\command\admin\permission\PermissionUser;
|
||||
use app\common\command\admin\role\AdminRoleCreate;
|
||||
use app\common\command\admin\role\AdminRoleDelete;
|
||||
use app\common\command\admin\role\AdminRoleInfo;
|
||||
use app\common\command\admin\role\AdminRoleList;
|
||||
use app\common\command\admin\role\AdminRolePermissionAssign;
|
||||
use app\common\command\admin\role\AdminRolePermissionList;
|
||||
use app\common\command\admin\role\AdminRolePermissionRevoke;
|
||||
use app\common\command\admin\user\AdminUserRoleAssign;
|
||||
use app\common\command\admin\user\AdminUserRoleList;
|
||||
use app\common\command\admin\user\AdminUserRoleRevoke;
|
||||
use app\common\command\curd\Migrate;
|
||||
use app\common\command\scheme\Backup;
|
||||
use app\common\command\scheme\Make;
|
||||
use app\common\command\scheme\Sync;
|
||||
use app\common\command\tools\http\ToolsHttpCall;
|
||||
use app\common\command\tools\db\ToolsDbCount;
|
||||
use app\common\command\tools\db\ToolsDbDesc;
|
||||
use app\common\command\tools\db\ToolsDbExecute;
|
||||
use app\common\command\tools\db\ToolsDbInfo;
|
||||
use app\common\command\tools\db\ToolsDbQuery;
|
||||
use app\common\command\tools\db\ToolsDbTable;
|
||||
use app\common\command\tools\agent\ToolsAgentPublish;
|
||||
use app\common\command\tools\log\ToolsLogSearch;
|
||||
use app\common\command\tools\log\ToolsLogShow;
|
||||
use app\common\command\tools\log\ToolsLogStats;
|
||||
use app\common\command\Test;
|
||||
use app\common\event\AdminLoginSuccess\LogEvent;
|
||||
use app\common\event\AdminLoginType\DemoEvent;
|
||||
@@ -57,13 +91,47 @@ class UlthonAdminService extends Service
|
||||
// 绑定命令行
|
||||
$this->commands([
|
||||
Test::class,
|
||||
Curd::class,
|
||||
OssStatic::class,
|
||||
ResetPassword::class,
|
||||
Timer::class,
|
||||
Version::class,
|
||||
Migrate::class,
|
||||
Clear::class,
|
||||
Update::class,
|
||||
UpdateCode::class,
|
||||
AdminPermissionNodes::class,
|
||||
PermissionUser::class,
|
||||
AdminMenuList::class,
|
||||
AdminMenuDelete::class,
|
||||
AdminMenuCreate::class,
|
||||
AdminMenuUpdate::class,
|
||||
AdminMenuExport::class,
|
||||
AdminRoleCreate::class,
|
||||
AdminRoleDelete::class,
|
||||
AdminRoleList::class,
|
||||
AdminRoleInfo::class,
|
||||
AdminRolePermissionAssign::class,
|
||||
AdminRolePermissionRevoke::class,
|
||||
AdminRolePermissionList::class,
|
||||
AdminUserRoleAssign::class,
|
||||
AdminUserRoleRevoke::class,
|
||||
AdminUserRoleList::class,
|
||||
Make::class,
|
||||
Sync::class,
|
||||
Backup::class,
|
||||
ToolsHttpCall::class,
|
||||
MigrateFileData::class,
|
||||
ToolsAgentPublish::class,
|
||||
ToolsDbQuery::class,
|
||||
ToolsDbExecute::class,
|
||||
ToolsDbTable::class,
|
||||
ToolsDbInfo::class,
|
||||
ToolsDbDesc::class,
|
||||
ToolsDbCount::class,
|
||||
ToolsLogShow::class,
|
||||
ToolsLogSearch::class,
|
||||
ToolsLogStats::class,
|
||||
]);
|
||||
|
||||
// 绑定标识容器
|
||||
|
||||
@@ -110,32 +110,39 @@ class DebugMysql implements LogHandlerInterface
|
||||
$log_key = uniqid();
|
||||
}
|
||||
|
||||
foreach ($log as $log_level => $log_list) {
|
||||
foreach ($log_list as $key => $log_item) {
|
||||
if (!is_string($log_item)) {
|
||||
$log_item = print_r($log_item, true);
|
||||
}
|
||||
foreach ($log as $log_item) {
|
||||
// 适配 ThinkPHP 8.x 的 LogRecord 对象格式
|
||||
// 兼容旧格式:包含 type 和 message 属性的对象
|
||||
if (is_object($log_item) && isset($log_item->type) && isset($log_item->message)) {
|
||||
$log_level = $log_item->type;
|
||||
$log_content = $log_item->message;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
$log_data = [
|
||||
'level' => $log_level,
|
||||
'content' => $log_item,
|
||||
'create_time' => $create_time,
|
||||
'create_time_title' => $create_time_title,
|
||||
'uid' => $log_key,
|
||||
'app_name' => $app_name,
|
||||
'controller_name' => $controller_name,
|
||||
'action_name' => $action_name,
|
||||
];
|
||||
if (!is_string($log_content)) {
|
||||
$log_content = print_r($log_content, true);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!is_null($this->pdo)) {
|
||||
$this->saveByConnect($log_data);
|
||||
} else {
|
||||
$this->saveByFile($log_data);
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
$log_data = [
|
||||
'level' => $log_level,
|
||||
'content' => $log_content,
|
||||
'create_time' => $create_time,
|
||||
'create_time_title' => $create_time_title,
|
||||
'uid' => $log_key,
|
||||
'app_name' => $app_name,
|
||||
'controller_name' => $controller_name,
|
||||
'action_name' => $action_name,
|
||||
];
|
||||
|
||||
try {
|
||||
if (!is_null($this->pdo)) {
|
||||
$this->saveByConnect($log_data);
|
||||
} else {
|
||||
$this->saveByFile($log_data);
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
$this->saveByFile($log_data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +249,7 @@ class DebugMysql implements LogHandlerInterface
|
||||
$dirname = dirname($log_path);
|
||||
|
||||
if (!is_dir($dirname)) {
|
||||
mkdir($log_path, 0777, true);
|
||||
mkdir($dirname, 0777, true);
|
||||
}
|
||||
|
||||
$first_line = false;
|
||||
|
||||
Reference in New Issue
Block a user