feat: 发布智能体版

This commit is contained in:
augushong
2026-03-26 20:22:34 +08:00
parent 7ee9e102a5
commit 8cc08bcb8c
138 changed files with 7964 additions and 660 deletions

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -1,4 +1,15 @@
<?php
/**
* @internal-framework
*
* 此文件为框架核心初始化数据
*
* 用途:商城分类演示数据初始化
* 维护者:框架维护者
*
* 注意:此文件属于框架内核,业务开发者不应修改
*/
$ul_mall_cate = array(
array(
"title" => "手机",

View File

@@ -1,4 +1,15 @@
<?php
/**
* @internal-framework
*
* 此文件为框架核心初始化数据
*
* 用途:商城商品演示数据初始化
* 维护者:框架维护者
*
* 注意:此文件属于框架内核,业务开发者不应修改
*/
$ul_mall_goods = array(
array(
"title" => "落地-风扇",

View File

@@ -1,4 +1,15 @@
<?php
/**
* @internal-framework
*
* 此文件为框架核心初始化数据
*
* 用途:商城标签演示数据初始化
* 维护者:框架维护者
*
* 注意:此文件属于框架内核,业务开发者不应修改
*/
$ul_mall_tag = array(
array(
"id" => 1,

View File

@@ -1,4 +1,15 @@
<?php
/**
* @internal-framework
*
* 此文件为框架核心初始化数据
*
* 用途:系统角色初始化
* 维护者:框架维护者
*
* 注意:此文件属于框架内核,业务开发者不应修改
*/
$ul_system_auth = array(
array(
"id" => 1,

View File

@@ -1,4 +1,15 @@
<?php
/**
* @internal-framework
*
* 此文件为框架核心初始化数据
*
* 用途:系统权限节点初始化
* 维护者:框架维护者
*
* 注意:此文件属于框架内核,业务开发者不应修改
*/
$ul_system_auth_node = array(
);

View File

@@ -1,4 +1,15 @@
<?php
/**
* @internal-framework
*
* 此文件为框架核心初始化数据
*
* 用途系统配置初始化包括上传、短信、站点、微信、OSS等配置项
* 维护者:框架维护者
*
* 注意:此文件属于框架内核,业务开发者不应修改
*/
$ul_system_config = array(
array(
"name" => "alisms_access_key_id",

View File

@@ -1,4 +1,15 @@
<?php
/**
* @internal-framework
*
* 此文件为框架核心初始化数据
*
* 用途:系统菜单初始化(系统管理、商城管理等菜单结构)
* 维护者:框架维护者
*
* 注意:此文件属于框架内核,业务开发者不应修改
*/
$ul_system_menu = array(
array(
"id" => 227,

View File

@@ -1,4 +1,15 @@
<?php
/**
* @internal-framework
*
* 此文件为框架核心初始化数据
*
* 用途:系统快捷入口初始化
* 维护者:框架维护者
*
* 注意:此文件属于框架内核,业务开发者不应修改
*/
$ul_system_quick = array(
array(
"title" => "管理员管理",

View File

@@ -1,4 +1,14 @@
<?php
/**
* @internal-framework
*
* 此文件为框架内置功能
*
* 用途框架版本更新代码v2.0.74- JS代码属性名称替换
* 维护者:框架维护者
*
* 注意:此文件属于框架内核,业务开发者不应修改
*/
use think\console\Input;
use think\console\Output;

View File

@@ -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}");
}

View 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()
{
}
}

View File

@@ -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 与数据库不一致
→ 同步 Schemephp 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('>>>>>>>>>>>>>>>');

View File

@@ -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'));
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);

View 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;
}
}

View File

@@ -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;
}
}

View 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;
}
}

View 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));
}
}

View 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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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;
}
}

View 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 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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];
}
}

View File

@@ -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());
}
}
}

View File

@@ -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>");
}
}
}

View File

@@ -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 }`

View 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);
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}
}
}

View 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" 环境变量');
}
}
}
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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}");
}
}
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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', ''));

View File

@@ -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';

View File

@@ -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()

View File

@@ -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,
]);
// 绑定标识容器

View File

@@ -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;