mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-07-07 10:32:48 +08:00
feat: 发布智能体版
This commit is contained in:
@@ -7,29 +7,34 @@
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
extend/base/common/
|
||||
├── service/
|
||||
│ ├── ToolsDbServiceBase.php # 数据库工具服务
|
||||
│ ├── ToolsLogServiceBase.php # 日志工具服务
|
||||
│ └── ToolsBackupServiceBase.php # 备份工具服务
|
||||
└── command/
|
||||
└── tools/
|
||||
├── README.md # 本文件(命名规范说明)
|
||||
└── db/ # 数据库工具
|
||||
├── ToolsDbQueryBase.php # 基础命令类
|
||||
├── ToolsDbExecuteBase.php
|
||||
└── ...
|
||||
extend/base/common/
|
||||
├── service/
|
||||
│ ├── ToolsDbServiceBase.php # 数据库工具服务
|
||||
│ ├── ToolsLogServiceBase.php # 日志工具服务
|
||||
│ └── ToolsBackupServiceBase.php # 备份工具服务
|
||||
└── command/
|
||||
└── tools/
|
||||
├── README.md # 本文件(命名规范说明)
|
||||
├── db/ # 数据库工具
|
||||
│ ├── ToolsDbQueryBase.php # 基础命令类
|
||||
│ ├── ToolsDbExecuteBase.php
|
||||
│ └── ...
|
||||
├── http/ # HTTP 工具
|
||||
│ └── ToolsHttpCallBase.php
|
||||
└── ...
|
||||
|
||||
app/common/command/
|
||||
├── tools/
|
||||
│ └── db/ # 数据库工具
|
||||
│ ├── ToolsDbQuery.php # 业务命令类
|
||||
│ ├── ToolsDbExecute.php
|
||||
│ └── ...
|
||||
├── admin/ # 普通命令(无前缀)
|
||||
├── scheme/
|
||||
├── curd/
|
||||
└── ...
|
||||
app/common/command/
|
||||
├── tools/
|
||||
│ ├── db/ # 数据库工具
|
||||
│ │ ├── ToolsDbQuery.php # 业务命令类
|
||||
│ │ ├── ToolsDbExecute.php
|
||||
│ │ └── ...
|
||||
│ └── http/ # HTTP 工具
|
||||
│ └── ToolsHttpCall.php
|
||||
├── admin/ # 普通命令(无前缀)
|
||||
├── scheme/
|
||||
├── curd/
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 命名规则对比
|
||||
@@ -77,22 +82,24 @@ app/common/command/
|
||||
|
||||
### 2. 命令名称规则
|
||||
|
||||
- 格式:`tools:[类型]:[功能]`
|
||||
- 示例:
|
||||
- `tools:db:query` - 数据库查询
|
||||
- `tools:db:execute` - 数据库执行
|
||||
- `tools:log:clear` - 日志清理
|
||||
- `tools:backup:create` - 创建备份
|
||||
- 格式:`tools:[类型]:[功能]`
|
||||
- 示例:
|
||||
- `tools:db:query` - 数据库查询
|
||||
- `tools:db:execute` - 数据库执行
|
||||
- `tools:http:call` - HTTP 调用(类似 curl)
|
||||
- `tools:log:clear` - 日志清理
|
||||
- `tools:backup:create` - 创建备份
|
||||
|
||||
### 3. 目录结构规则
|
||||
|
||||
```
|
||||
tools/
|
||||
├── db/ # 数据库工具(db)
|
||||
├── log/ # 日志工具(log)
|
||||
├── backup/ # 备份工具(backup)
|
||||
├── cache/ # 缓存工具(cache)
|
||||
└── ...
|
||||
tools/
|
||||
├── db/ # 数据库工具(db)
|
||||
├── http/ # HTTP 工具(http)
|
||||
├── log/ # 日志工具(log)
|
||||
├── backup/ # 备份工具(backup)
|
||||
├── cache/ # 缓存工具(cache)
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 创建新 Tools 命令的步骤
|
||||
@@ -185,10 +192,17 @@ $this->commands([
|
||||
|
||||
## 已实现的工具
|
||||
|
||||
- **数据库工具 (db)**
|
||||
- `tools:db:query` - 执行 SQL 查询
|
||||
- `tools:db:execute` - 执行 SQL 非查询语句
|
||||
- `tools:db:table` - 使用查询构建器操作表
|
||||
- `tools:db:info` - 显示数据库信息
|
||||
- `tools:db:desc` - 显示表结构
|
||||
- `tools:db:count` - 统计表记录数
|
||||
- **数据库工具 (db)**
|
||||
- `tools:db:query` - 执行 SQL 查询
|
||||
- `tools:db:execute` - 执行 SQL 非查询语句
|
||||
- `tools:db:table` - 使用查询构建器操作表
|
||||
- `tools:db:info` - 显示数据库信息
|
||||
- `tools:db:desc` - 显示表结构
|
||||
- `tools:db:count` - 统计表记录数
|
||||
|
||||
- **HTTP 工具 (http)**
|
||||
- `tools:http:call` - HTTP 调用工具,类似 curl
|
||||
- 支持标准 curl 参数:`--url`, `--method`, `--data`, `--body`, `--headers`
|
||||
- 支持框架特性参数:`--app`, `--controller`, `--action`, `--super-token`, `--user-id`
|
||||
- JSON 输出格式:`{ success, response: { status, data, headers }, execution_time, exception }`
|
||||
|
||||
|
||||
277
extend/base/common/command/tools/agent/ToolsAgentPublishBase.php
Normal file
277
extend/base/common/command/tools/agent/ToolsAgentPublishBase.php
Normal file
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\command\tools\agent;
|
||||
|
||||
use app\common\console\Command;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\App;
|
||||
|
||||
class ToolsAgentPublishBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:agent:publish')
|
||||
->setDescription('发布 AGENTS.md 与 .agent 到兼容目标(复制 rules/skills;可选生成 CLAUDE.md)')
|
||||
->addOption('target', 't', Option::VALUE_OPTIONAL, '发布目标:trae|opencode|roocode|cursor|claude|all', 'all')
|
||||
->addOption('dry-run', null, Option::VALUE_NONE, '只输出将要写入/覆盖的清单,不落盘')
|
||||
->addOption('clean', null, Option::VALUE_NONE, '清理目标中由发布器生成的内容(仅影响 rules/skills 与发布文件)')
|
||||
->addOption('force', 'f', Option::VALUE_NONE, '跳过覆盖确认');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$rootPath = rtrim(App::getRootPath(), "\\/ \t\n\r\0\x0B");
|
||||
|
||||
$sourceAgentsPath = $rootPath . DIRECTORY_SEPARATOR . 'AGENTS.md';
|
||||
$sourceRulesDir = $rootPath . DIRECTORY_SEPARATOR . '.agent' . DIRECTORY_SEPARATOR . 'rules';
|
||||
$sourceSkillsDir = $rootPath . DIRECTORY_SEPARATOR . '.agent' . DIRECTORY_SEPARATOR . 'skills';
|
||||
|
||||
if (!is_file($sourceAgentsPath)) {
|
||||
$output->error('缺少单一事实来源文件:' . $sourceAgentsPath);
|
||||
return;
|
||||
}
|
||||
if (!is_dir($sourceRulesDir)) {
|
||||
$output->error('缺少单一事实来源目录:' . $sourceRulesDir);
|
||||
return;
|
||||
}
|
||||
if (!is_dir($sourceSkillsDir)) {
|
||||
$output->error('缺少单一事实来源目录:' . $sourceSkillsDir);
|
||||
return;
|
||||
}
|
||||
|
||||
$target = strtolower((string)$input->getOption('target'));
|
||||
if ($target === '') {
|
||||
$target = 'all';
|
||||
}
|
||||
|
||||
$supportedTargets = ['trae', 'opencode', 'roocode', 'cursor', 'claude', 'all'];
|
||||
if (!in_array($target, $supportedTargets, true)) {
|
||||
$output->error('不支持的 target:' . $target . '(可选:' . implode('|', $supportedTargets) . ')');
|
||||
return;
|
||||
}
|
||||
|
||||
$dryRun = (bool)$input->getOption('dry-run');
|
||||
$clean = (bool)$input->getOption('clean');
|
||||
|
||||
$targets = $target === 'all' ? ['trae', 'opencode', 'roocode', 'cursor', 'claude'] : [$target];
|
||||
|
||||
$writeActions = [];
|
||||
$deleteActions = [];
|
||||
|
||||
$agentsContent = file_get_contents($sourceAgentsPath);
|
||||
if ($agentsContent === false) {
|
||||
$output->error('读取失败:' . $sourceAgentsPath);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($targets as $t) {
|
||||
if (in_array($t, ['trae', 'opencode', 'roocode', 'cursor'], true)) {
|
||||
$distRoot = $rootPath . DIRECTORY_SEPARATOR . '.' . $t;
|
||||
$distRules = $distRoot . DIRECTORY_SEPARATOR . 'rules';
|
||||
$distSkills = $distRoot . DIRECTORY_SEPARATOR . 'skills';
|
||||
|
||||
if ($t === 'cursor') {
|
||||
$deleteActions[] = $distRules . DIRECTORY_SEPARATOR . 'ulthon-framework.mdc';
|
||||
}
|
||||
|
||||
if ($clean) {
|
||||
$deleteActions[] = $distRules;
|
||||
$deleteActions[] = $distSkills;
|
||||
}
|
||||
|
||||
$writeActions = array_merge($writeActions, $this->planCopyDir($sourceRulesDir, $distRules));
|
||||
$writeActions = array_merge($writeActions, $this->planCopyDir($sourceSkillsDir, $distSkills));
|
||||
}
|
||||
|
||||
if ($t === 'claude') {
|
||||
$distClaude = $rootPath . DIRECTORY_SEPARATOR . 'CLAUDE.md';
|
||||
if ($clean) {
|
||||
$deleteActions[] = $distClaude;
|
||||
}
|
||||
$writeActions = array_merge($writeActions, $this->planWriteFile($distClaude, $agentsContent));
|
||||
}
|
||||
}
|
||||
|
||||
$deleteActions = array_values(array_unique(array_filter($deleteActions, 'strlen')));
|
||||
|
||||
$plannedDeletes = $this->filterExistingDeletes($deleteActions);
|
||||
$plannedWrites = $this->filterEffectiveWrites($writeActions, $clean);
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>=== tools:agent:publish 计划 ===</info>');
|
||||
$output->writeln('target:' . $target);
|
||||
$output->writeln('dry-run:' . ($dryRun ? 'yes' : 'no'));
|
||||
$output->writeln('clean:' . ($clean ? 'yes' : 'no'));
|
||||
$output->writeln('');
|
||||
|
||||
if (empty($plannedDeletes) && empty($plannedWrites)) {
|
||||
$output->writeln('<comment>无变更:目标已是最新状态。</comment>');
|
||||
$output->writeln('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($plannedDeletes)) {
|
||||
$output->writeln('<comment>将删除:</comment>');
|
||||
foreach ($plannedDeletes as $path) {
|
||||
$output->writeln(' - ' . $path);
|
||||
}
|
||||
$output->writeln('');
|
||||
}
|
||||
|
||||
if (!empty($plannedWrites)) {
|
||||
$output->writeln('<comment>将写入:</comment>');
|
||||
foreach ($plannedWrites as $item) {
|
||||
$output->writeln(' - [' . $item['mode'] . '] ' . $item['dist']);
|
||||
}
|
||||
$output->writeln('');
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$output->confirm($input, '将执行上述写入/覆盖/删除操作,是否继续?', true)) {
|
||||
$output->comment('已取消。');
|
||||
$output->newLine();
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($plannedDeletes as $path) {
|
||||
$this->deletePath($path);
|
||||
}
|
||||
|
||||
foreach ($plannedWrites as $item) {
|
||||
$this->ensureDir(dirname($item['dist']));
|
||||
file_put_contents($item['dist'], $item['content']);
|
||||
}
|
||||
|
||||
$output->info('发布完成。');
|
||||
$output->newLine();
|
||||
}
|
||||
|
||||
protected function planCopyDir(string $sourceDir, string $distDir): array
|
||||
{
|
||||
$actions = [];
|
||||
if (!is_dir($sourceDir)) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($sourceDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
$sourceDirNormalized = rtrim($sourceDir, "\\/") . DIRECTORY_SEPARATOR;
|
||||
foreach ($iterator as $fileInfo) {
|
||||
if (!$fileInfo->isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$src = $fileInfo->getPathname();
|
||||
$relative = substr($src, strlen($sourceDirNormalized));
|
||||
$dist = $distDir . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $relative);
|
||||
|
||||
$content = file_get_contents($src);
|
||||
if ($content === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$actions[] = [
|
||||
'dist' => $dist,
|
||||
'content' => $content,
|
||||
];
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
protected function planWriteFile(string $dist, string $content): array
|
||||
{
|
||||
return [[
|
||||
'dist' => $dist,
|
||||
'content' => $content,
|
||||
]];
|
||||
}
|
||||
|
||||
protected function filterEffectiveWrites(array $actions, bool $forceWrite = false): array
|
||||
{
|
||||
$result = [];
|
||||
$resultMap = [];
|
||||
foreach ($actions as $item) {
|
||||
$dist = (string)($item['dist'] ?? '');
|
||||
$content = (string)($item['content'] ?? '');
|
||||
if ($dist === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mode = 'create';
|
||||
if (is_file($dist)) {
|
||||
$existing = file_get_contents($dist);
|
||||
if (!$forceWrite && $existing !== false && $existing === $content) {
|
||||
continue;
|
||||
}
|
||||
$mode = 'update';
|
||||
}
|
||||
|
||||
$resultMap[$dist] = [
|
||||
'dist' => $dist,
|
||||
'content' => $content,
|
||||
'mode' => $mode,
|
||||
];
|
||||
}
|
||||
$result = array_values($resultMap);
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function filterExistingDeletes(array $deleteActions): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($deleteActions as $path) {
|
||||
if (is_file($path) || is_dir($path)) {
|
||||
$result[] = $path;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function deletePath(string $path): void
|
||||
{
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
return;
|
||||
}
|
||||
if (!is_dir($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $fileInfo) {
|
||||
if ($fileInfo->isDir()) {
|
||||
@rmdir($fileInfo->getPathname());
|
||||
continue;
|
||||
}
|
||||
@unlink($fileInfo->getPathname());
|
||||
}
|
||||
|
||||
@rmdir($path);
|
||||
}
|
||||
|
||||
protected function ensureDir(string $dir): void
|
||||
{
|
||||
if ($dir === '' || is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
@mkdir($dir, 0777, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace base\common\command\tools\db;
|
||||
|
||||
use base\common\service\ToolsDbServiceBase;
|
||||
use think\console\Command;
|
||||
use app\common\service\tools\DbService;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
@@ -13,6 +13,8 @@ class ToolsDbCountBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:db:count')
|
||||
->setDescription('统计表记录数')
|
||||
->addArgument('table', null, '表名')
|
||||
@@ -24,12 +26,12 @@ class ToolsDbCountBase extends Command
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
$service->showHelp('tools:db:count', $output);
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
|
||||
if (!$service->checkDebugMode($output)) {
|
||||
return;
|
||||
@@ -61,19 +63,19 @@ class ToolsDbCountBase extends Command
|
||||
$endTime = microtime(true);
|
||||
$executionTime = round(($endTime - $startTime) * 1000, 2);
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>' . str_repeat('=', 60) . '</comment>');
|
||||
$output->writeln('<info>表名:' . $fullTableName . '</info>');
|
||||
$output->writeln('<comment>' . str_repeat('=', 60) . '</comment>');
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>记录数:</info>' . $count);
|
||||
$output->writeln('<info>执行时间:</info>' . $executionTime . 'ms');
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>' . str_repeat('=', 60) . '</comment>');
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->info('表名:' . $fullTableName);
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->newLine();
|
||||
$output->info('记录数:' . $count);
|
||||
$output->info('执行时间:' . $executionTime . 'ms');
|
||||
$output->newLine();
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->newLine();
|
||||
} catch (\Exception $e) {
|
||||
$output->error('统计失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace base\common\command\tools\db;
|
||||
|
||||
use base\common\service\ToolsDbServiceBase;
|
||||
use think\console\Command;
|
||||
use app\common\service\tools\DbService;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
@@ -13,6 +13,8 @@ class ToolsDbDescBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:db:desc')
|
||||
->setDescription('显示表结构信息')
|
||||
->addArgument('table', null, '表名')
|
||||
@@ -24,12 +26,12 @@ class ToolsDbDescBase extends Command
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
$service->showHelp('tools:db:desc', $output);
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
|
||||
if (!$service->checkDebugMode($output)) {
|
||||
return;
|
||||
@@ -48,12 +50,6 @@ class ToolsDbDescBase extends Command
|
||||
$showIndex = $input->getOption('show-index');
|
||||
|
||||
try {
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>' . str_repeat('=', 60) . '</comment>');
|
||||
$output->writeln('<info>表结构:' . $fullTableName . '</info>');
|
||||
$output->writeln('<comment>' . str_repeat('=', 60) . '</comment>');
|
||||
$output->writeln('');
|
||||
|
||||
$tableComment = '';
|
||||
try {
|
||||
$escaped = addcslashes($fullTableName, "\\_%");
|
||||
@@ -77,9 +73,6 @@ class ToolsDbDescBase extends Command
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln('<info>表注释:</info>' . $tableComment);
|
||||
$output->writeln('');
|
||||
|
||||
$columns = Db::connect($connection)->query("SHOW FULL COLUMNS FROM `$fullTableName`");
|
||||
|
||||
if (empty($columns)) {
|
||||
@@ -88,6 +81,45 @@ class ToolsDbDescBase extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$columnData = [];
|
||||
foreach ($columns as $column) {
|
||||
$columnData[] = [
|
||||
'name' => $column['Field'],
|
||||
'type' => $column['Type'],
|
||||
'null' => $column['Null'] === 'YES',
|
||||
'key' => $column['Key'],
|
||||
'default' => $column['Default'] ?? null,
|
||||
'extra' => $column['Extra'] ?? '',
|
||||
'comment' => $column['Comment'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
$indexData = [];
|
||||
if ($showIndex) {
|
||||
$indexes = Db::connect($connection)->query("SHOW INDEX FROM `$fullTableName`");
|
||||
|
||||
if (!empty($indexes)) {
|
||||
foreach ($indexes as $index) {
|
||||
$indexData[] = [
|
||||
'name' => $index['Key_name'],
|
||||
'column' => $index['Column_name'],
|
||||
'unique' => $index['Non_unique'] == 0,
|
||||
'type' => $index['Index_type'],
|
||||
'sequence' => $index['Seq_in_index'],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$output->newLine();
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->info('表结构:' . $fullTableName);
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->newLine();
|
||||
|
||||
$output->info('表注释:' . $tableComment);
|
||||
$output->newLine();
|
||||
|
||||
$tableData = [];
|
||||
foreach ($columns as $column) {
|
||||
$tableData[] = [
|
||||
@@ -104,18 +136,16 @@ class ToolsDbDescBase extends Command
|
||||
$service->formatTableOutput($tableData, $output);
|
||||
|
||||
if ($showIndex) {
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>' . str_repeat('-', 60) . '</comment>');
|
||||
$output->writeln('<info>索引信息</info>');
|
||||
$output->writeln('<comment>' . str_repeat('-', 60) . '</comment>');
|
||||
$output->writeln('');
|
||||
|
||||
$indexes = Db::connect($connection)->query("SHOW INDEX FROM `$fullTableName`");
|
||||
$output->newLine();
|
||||
$output->comment(str_repeat('-', 60));
|
||||
$output->info('索引信息');
|
||||
$output->comment(str_repeat('-', 60));
|
||||
$output->newLine();
|
||||
|
||||
if (!empty($indexes)) {
|
||||
$indexData = [];
|
||||
$indexDataDisplay = [];
|
||||
foreach ($indexes as $index) {
|
||||
$indexData[] = [
|
||||
$indexDataDisplay[] = [
|
||||
'索引名' => $index['Key_name'],
|
||||
'列名' => $index['Column_name'],
|
||||
'唯一' => $index['Non_unique'] == 0 ? '是' : '否',
|
||||
@@ -123,18 +153,18 @@ class ToolsDbDescBase extends Command
|
||||
'顺序' => $index['Seq_in_index'],
|
||||
];
|
||||
}
|
||||
$service->formatTableOutput($indexData, $output);
|
||||
$service->formatTableOutput($indexDataDisplay, $output);
|
||||
} else {
|
||||
$output->writeln('无索引');
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>' . str_repeat('=', 60) . '</comment>');
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->newLine();
|
||||
} catch (\Exception $e) {
|
||||
$output->error('获取表结构失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace base\common\command\tools\db;
|
||||
|
||||
use base\common\service\ToolsDbServiceBase;
|
||||
use think\console\Command;
|
||||
use app\common\service\tools\DbService;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
@@ -13,10 +13,11 @@ class ToolsDbExecuteBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:db:execute')
|
||||
->setDescription('执行 SQL 非查询语句(INSERT/UPDATE/DELETE)')
|
||||
->addArgument('sql', null, 'SQL 执行语句')
|
||||
->addOption('force', null, Option::VALUE_NONE, '跳过确认直接执行')
|
||||
->addOption('transaction', null, Option::VALUE_NONE, '在事务中执行(失败自动回滚)')
|
||||
->addOption('connection', null, Option::VALUE_OPTIONAL, '指定数据库连接配置')
|
||||
->addOption('help', 'h', Option::VALUE_NONE, '显示帮助信息');
|
||||
@@ -25,12 +26,12 @@ class ToolsDbExecuteBase extends Command
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
$service->showHelp('tools:db:execute', $output);
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
|
||||
if (!$service->checkDebugMode($output)) {
|
||||
return;
|
||||
@@ -45,7 +46,6 @@ class ToolsDbExecuteBase extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$force = $input->getOption('force');
|
||||
$useTransaction = $input->getOption('transaction');
|
||||
|
||||
if (preg_match('/^\s*SELECT\s+/i', $sql)) {
|
||||
@@ -53,17 +53,14 @@ class ToolsDbExecuteBase extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>准备执行的 SQL:</comment>');
|
||||
$output->newLine();
|
||||
$output->comment('准备执行的 SQL:');
|
||||
$output->writeln($sql);
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
|
||||
if (!$force) {
|
||||
$confirm = $output->confirm($input, '<question>确定要执行此 SQL 语句吗?</question> ');
|
||||
if (!$confirm) {
|
||||
$output->writeln('<comment>操作已取消</comment>');
|
||||
return;
|
||||
}
|
||||
if (!$output->confirm($input, '确定要执行此 SQL 语句吗?', true)) {
|
||||
$output->comment('已取消。');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -85,19 +82,19 @@ class ToolsDbExecuteBase extends Command
|
||||
$endTime = microtime(true);
|
||||
$executionTime = round(($endTime - $startTime) * 1000, 2);
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>执行成功</info>');
|
||||
$output->newLine();
|
||||
$output->info('执行成功');
|
||||
$output->writeln('影响行数:' . $affectedRows);
|
||||
$output->writeln('执行时间:' . $executionTime . 'ms');
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
$output->error('执行失败:' . $e->getMessage());
|
||||
if ($useTransaction) {
|
||||
$output->writeln('<comment>事务已回滚</comment>');
|
||||
$output->comment('事务已回滚');
|
||||
}
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace base\common\command\tools\db;
|
||||
|
||||
use base\common\service\ToolsDbServiceBase;
|
||||
use think\console\Command;
|
||||
use app\common\service\tools\DbService;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
@@ -14,6 +14,8 @@ class ToolsDbInfoBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:db:info')
|
||||
->setDescription('显示数据库连接信息和表列表')
|
||||
->addOption('with-count', null, Option::VALUE_NONE, '显示每个表的记录数')
|
||||
@@ -24,12 +26,12 @@ class ToolsDbInfoBase extends Command
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
$service->showHelp('tools:db:info', $output);
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
|
||||
if (!$service->checkDebugMode($output)) {
|
||||
return;
|
||||
@@ -47,11 +49,69 @@ class ToolsDbInfoBase extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>' . str_repeat('=', 60) . '</comment>');
|
||||
$output->writeln('<info>数据库连接信息</info>');
|
||||
$output->writeln('<comment>' . str_repeat('=', 60) . '</comment>');
|
||||
$output->writeln('');
|
||||
$tables = Db::connect($connection)->getTables();
|
||||
|
||||
$tableList = [];
|
||||
if (!empty($tables)) {
|
||||
$prefix = $config['prefix'] ?? '';
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$shortName = $table;
|
||||
if ($prefix && str_starts_with($table, $prefix)) {
|
||||
$shortName = substr($table, strlen($prefix));
|
||||
}
|
||||
|
||||
$tableItem = [
|
||||
'name' => $shortName,
|
||||
'full_name' => $table
|
||||
];
|
||||
|
||||
if ($withCount) {
|
||||
$count = Db::connect($connection)->table($table)->count();
|
||||
$tableItem['count'] = $count;
|
||||
}
|
||||
|
||||
$tableList[] = $tableItem;
|
||||
}
|
||||
}
|
||||
|
||||
$output->newLine();
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->info('数据库连接信息');
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->newLine();
|
||||
|
||||
$output->info('连接名称:' . $connection);
|
||||
$output->info('数据库类型:' . ($config['type'] ?? 'unknown'));
|
||||
$output->info('主机地址:' . ($config['hostname'] ?? 'unknown'));
|
||||
$output->info('数据库名:' . ($config['database'] ?? 'unknown'));
|
||||
$output->info('端口:' . ($config['hostport'] ?? 'unknown'));
|
||||
$output->info('字符集:' . ($config['charset'] ?? 'unknown'));
|
||||
$output->info('表前缀:' . ($config['prefix'] ?? ''));
|
||||
$output->newLine();
|
||||
|
||||
$output->comment(str_repeat('-', 60));
|
||||
$output->info('表列表');
|
||||
$output->comment(str_repeat('-', 60));
|
||||
$output->newLine();
|
||||
|
||||
if (empty($tables)) {
|
||||
$output->writeln('无表');
|
||||
$output->newLine();
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($tableList as $tableItem) {
|
||||
if ($withCount) {
|
||||
$output->info(str_pad($tableItem['name'], 40) . ($tableItem['count'] ?? 0) . ' 条记录');
|
||||
} else {
|
||||
$output->info($tableItem['name']);
|
||||
}
|
||||
}
|
||||
|
||||
$output->newLine();
|
||||
$output->comment(str_repeat('=', 60));
|
||||
$output->newLine();
|
||||
$output->writeln('<info>连接名称:</info>' . $connection);
|
||||
$output->writeln('<info>数据库类型:</info>' . ($config['type'] ?? 'unknown'));
|
||||
$output->writeln('<info>主机地址:</info>' . ($config['hostname'] ?? 'unknown'));
|
||||
@@ -66,27 +126,17 @@ class ToolsDbInfoBase extends Command
|
||||
$output->writeln('<comment>' . str_repeat('-', 60) . '</comment>');
|
||||
$output->writeln('');
|
||||
|
||||
$tables = Db::connect($connection)->getTables();
|
||||
|
||||
if (empty($tables)) {
|
||||
$output->writeln('无表');
|
||||
$output->writeln('');
|
||||
return;
|
||||
}
|
||||
|
||||
$prefix = $config['prefix'] ?? '';
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$shortName = $table;
|
||||
if ($prefix && str_starts_with($table, $prefix)) {
|
||||
$shortName = substr($table, strlen($prefix));
|
||||
}
|
||||
|
||||
foreach ($tableList as $tableItem) {
|
||||
if ($withCount) {
|
||||
$count = Db::connect($connection)->table($table)->count();
|
||||
$output->writeln('<info>' . str_pad($shortName, 40) . '</info>' . $count . ' 条记录');
|
||||
$output->writeln('<info>' . str_pad($tableItem['name'], 40) . '</info>' . ($tableItem['count'] ?? 0) . ' 条记录');
|
||||
} else {
|
||||
$output->writeln('<info>' . $shortName . '</info>');
|
||||
$output->writeln('<info>' . $tableItem['name'] . '</info>');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,4 +148,4 @@ class ToolsDbInfoBase extends Command
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace base\common\command\tools\db;
|
||||
|
||||
use base\common\service\ToolsDbServiceBase;
|
||||
use think\console\Command;
|
||||
use app\common\service\tools\DbService;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
@@ -13,10 +13,11 @@ class ToolsDbQueryBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:db:query')
|
||||
->setDescription('执行 SQL 查询语句(SELECT)并显示结果')
|
||||
->addArgument('sql', null, 'SQL 查询语句')
|
||||
->addOption('format', null, Option::VALUE_OPTIONAL, '输出格式,可选值:table(默认)、json', 'table')
|
||||
->addOption('limit', null, Option::VALUE_OPTIONAL, '限制显示行数')
|
||||
->addOption('connection', null, Option::VALUE_OPTIONAL, '指定数据库连接配置')
|
||||
->addOption('help', 'h', Option::VALUE_NONE, '显示帮助信息');
|
||||
@@ -25,12 +26,12 @@ class ToolsDbQueryBase extends Command
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
$service->showHelp('tools:db:query', $output);
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
|
||||
if (!$service->checkDebugMode($output)) {
|
||||
return;
|
||||
@@ -45,7 +46,6 @@ class ToolsDbQueryBase extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$format = $input->getOption('format') ?: 'table';
|
||||
$limit = $input->getOption('limit');
|
||||
|
||||
if (!preg_match('/^\s*SELECT\s+/i', $sql)) {
|
||||
@@ -69,22 +69,16 @@ class ToolsDbQueryBase extends Command
|
||||
$query = array_slice($query, 0, (int)$limit);
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
|
||||
if ($format === 'json') {
|
||||
$service->formatJsonOutput($query, $output);
|
||||
} else {
|
||||
$service->formatTableOutput($query, $output);
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>查询成功</info>');
|
||||
$output->newLine();
|
||||
$service->formatTableOutput($query, $output);
|
||||
$output->newLine();
|
||||
$output->info('查询成功');
|
||||
$output->writeln('影响行数:' . count($query));
|
||||
$output->writeln('执行时间:' . $executionTime . 'ms');
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
} catch (\Exception $e) {
|
||||
$output->error('查询失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace base\common\command\tools\db;
|
||||
|
||||
use base\common\service\ToolsDbServiceBase;
|
||||
use think\console\Command;
|
||||
use app\common\service\tools\DbService;
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
@@ -13,6 +13,8 @@ class ToolsDbTableBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:db:table')
|
||||
->setDescription('使用查询构建器操作表')
|
||||
->addArgument('table', null, '表名')
|
||||
@@ -28,12 +30,12 @@ class ToolsDbTableBase extends Command
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
$service->showHelp('tools:db:table', $output);
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new ToolsDbServiceBase();
|
||||
$service = new DbService();
|
||||
|
||||
if (!$service->checkDebugMode($output)) {
|
||||
return;
|
||||
@@ -75,10 +77,11 @@ class ToolsDbTableBase extends Command
|
||||
|
||||
if ($count) {
|
||||
$result = $query->count();
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>表名:' . $fullTableName . '</info>');
|
||||
|
||||
$output->newLine();
|
||||
$output->info('表名:' . $fullTableName);
|
||||
$output->writeln('记录数:' . $result);
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
} else {
|
||||
if ($limit && is_numeric($limit)) {
|
||||
$query->limit((int)$limit);
|
||||
@@ -89,17 +92,17 @@ class ToolsDbTableBase extends Command
|
||||
$endTime = microtime(true);
|
||||
$executionTime = round(($endTime - $startTime) * 1000, 2);
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>表名:' . $fullTableName . '</info>');
|
||||
$output->newLine();
|
||||
$output->info('表名:' . $fullTableName);
|
||||
$service->formatTableOutput($result, $output);
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
$output->writeln('影响行数:' . count($result));
|
||||
$output->writeln('执行时间:' . $executionTime . 'ms');
|
||||
$output->writeln('');
|
||||
$output->newLine();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$output->error('查询失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
445
extend/base/common/command/tools/http/ToolsHttpCallBase.php
Normal file
445
extend/base/common/command/tools/http/ToolsHttpCallBase.php
Normal file
@@ -0,0 +1,445 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\command\tools\http;
|
||||
|
||||
use app\admin\model\SystemAdmin;
|
||||
use app\common\console\Command;
|
||||
use app\common\constants\AdminConstant;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\Cache;
|
||||
|
||||
/**
|
||||
* tools:http:call 命令基类
|
||||
* 类似 curl 的 HTTP 调用功能,支持框架特性参数
|
||||
*/
|
||||
class ToolsHttpCallBase extends Command
|
||||
{
|
||||
/**
|
||||
* Guzzle HTTP 客户端
|
||||
* @var Client
|
||||
*/
|
||||
protected $httpClient;
|
||||
|
||||
/**
|
||||
* 执行开始时间(毫秒)
|
||||
* @var float
|
||||
*/
|
||||
protected $startTime;
|
||||
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:http:call')
|
||||
->setDescription('HTTP 调用工具,类似 curl,支持框架特性参数')
|
||||
->addArgument('url', Argument::OPTIONAL, '请求 URL 或控制器路径(如 /admin/user/index)')
|
||||
->addOption('url', null, Option::VALUE_OPTIONAL, '请求 URL')
|
||||
->addOption('method', 'm', Option::VALUE_OPTIONAL, 'HTTP 方法 (GET, POST, PUT, DELETE)', 'GET')
|
||||
->addOption('data', 'd', Option::VALUE_OPTIONAL, '请求数据(JSON 或 key=value)')
|
||||
->addOption('body', null, Option::VALUE_OPTIONAL, '请求体(原始数据)')
|
||||
->addOption('headers', 'H', Option::VALUE_OPTIONAL, '请求头(JSON 格式,如 {"Content-Type":"application/json"})')
|
||||
->addOption('app', null, Option::VALUE_OPTIONAL, '应用名称(框架特性)')
|
||||
->addOption('controller', null, Option::VALUE_OPTIONAL, '控制器名称(框架特性)')
|
||||
->addOption('action', null, Option::VALUE_OPTIONAL, '动作名称(框架特性)')
|
||||
->addOption('page-data', null, Option::VALUE_NONE, '返回页面 assign 数据(追加 get_page_data=1)')
|
||||
->addOption('super-token', null, Option::VALUE_OPTIONAL, '超级 Token(框架特性)', 'true')
|
||||
->addOption('user-id', null, Option::VALUE_OPTIONAL, '用户 ID(框架特性)');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$this->startTime = microtime(true);
|
||||
|
||||
// 检查环境
|
||||
$this->checkEnvironment($input);
|
||||
|
||||
// 初始化 HTTP 客户端
|
||||
$this->httpClient = new Client([
|
||||
'base_uri' => $this->getBaseUrl(),
|
||||
'timeout' => 30,
|
||||
'verify' => false, // 开发环境跳过 SSL 验证
|
||||
]);
|
||||
|
||||
try {
|
||||
// 构建请求
|
||||
$request = $this->buildRequest($input);
|
||||
|
||||
// 执行请求
|
||||
$response = $this->executeRequest($request, $output);
|
||||
|
||||
// 输出结果
|
||||
$this->outputResult($response, $output);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$executionTime = round((microtime(true) - $this->startTime) * 1000, 2);
|
||||
$this->outputError($e, $executionTime, $output);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查运行环境
|
||||
*/
|
||||
protected function checkEnvironment(Input $input): void
|
||||
{
|
||||
// 检查是否使用框架特性参数
|
||||
$usingFrameworkParams = !empty($input->getOption('app')) ||
|
||||
!empty($input->getOption('controller')) ||
|
||||
!empty($input->getOption('action'));
|
||||
|
||||
if ($usingFrameworkParams) {
|
||||
// 检查应用是否运行
|
||||
$baseUrl = $this->getBaseUrl();
|
||||
$isLocalhost = (strpos($baseUrl, 'localhost') !== false || strpos($baseUrl, '127.0.0.1') !== false);
|
||||
|
||||
if ($isLocalhost) {
|
||||
// 尝试检测应用是否运行
|
||||
$client = new Client([
|
||||
'timeout' => 20,
|
||||
'verify' => false,
|
||||
'http_errors' => false,
|
||||
]);
|
||||
try {
|
||||
$client->get($baseUrl);
|
||||
} catch (\GuzzleHttp\Exception\ConnectException $e) {
|
||||
throw new \Exception(
|
||||
"无法连接到应用服务器 ($baseUrl)。请确保应用正在运行(执行 'php think run')" .
|
||||
" 或者设置 'app.app_host' 环境变量。"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取基础 URL
|
||||
*/
|
||||
protected function getBaseUrl(): string
|
||||
{
|
||||
$appUrl = env('app.app_host', '');
|
||||
if (empty($appUrl)) {
|
||||
$appUrl = 'http://127.0.0.1:8000';
|
||||
}
|
||||
return rtrim($appUrl, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建请求
|
||||
*/
|
||||
protected function buildRequest(Input $input): array
|
||||
{
|
||||
// 获取 URL(优先使用 --url 参数,其次使用位置参数)
|
||||
$url = $input->getOption('url') ?: $input->getArgument('url');
|
||||
|
||||
// 如果使用框架特性参数,构建 URL
|
||||
if (empty($url)) {
|
||||
$url = $this->buildUrlFromFrameworkParams($input);
|
||||
}
|
||||
|
||||
if (empty($url)) {
|
||||
throw new \Exception('请提供 URL 或使用框架特性参数(--app, --controller, --action)');
|
||||
}
|
||||
|
||||
if ($input->getOption('page-data') && strpos($url, 'get_page_data=') === false) {
|
||||
$url .= (strpos($url, '?') === false ? '?' : '&') . 'get_page_data=1';
|
||||
}
|
||||
|
||||
$request = [
|
||||
'url' => $url,
|
||||
'method' => strtoupper($input->getOption('method')),
|
||||
'headers' => [],
|
||||
'body' => null,
|
||||
];
|
||||
|
||||
// 解析请求头
|
||||
$headers = $input->getOption('headers');
|
||||
if (!empty($headers)) {
|
||||
$request['headers'] = json_decode($headers, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new \Exception('请求头格式错误:' . json_last_error_msg());
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 Accept 头为 JSON
|
||||
$request['headers']['Accept'] = 'application/json';
|
||||
|
||||
// 解析请求数据
|
||||
$data = $input->getOption('data');
|
||||
$body = $input->getOption('body');
|
||||
|
||||
if (!empty($body)) {
|
||||
$request['body'] = $body;
|
||||
} elseif (!empty($data)) {
|
||||
$request['headers']['Content-Type'] = 'application/json';
|
||||
$request['body'] = $this->normalizeDataToJson($data);
|
||||
}
|
||||
|
||||
$superToken = $this->resolveSuperToken($input);
|
||||
if (!is_null($superToken)) {
|
||||
$userId = $input->getOption('user-id') ?: AdminConstant::SUPER_ADMIN_ID;
|
||||
$admin = SystemAdmin::find($userId);
|
||||
if (empty($admin)) {
|
||||
throw new \Exception('管理员不存在:' . $userId);
|
||||
}
|
||||
|
||||
$adminData = $admin->toArray();
|
||||
unset($adminData['password']);
|
||||
$adminData['expire_time'] = time() + 7200;
|
||||
|
||||
Cache::store('login')->set($superToken, $adminData, 7200);
|
||||
$request['headers']['Authorization'] = 'Bearer ' . $superToken;
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
protected function normalizeDataToJson(string $raw): string
|
||||
{
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') {
|
||||
return $raw;
|
||||
}
|
||||
|
||||
while (strlen($raw) >= 2) {
|
||||
$first = $raw[0];
|
||||
$last = $raw[strlen($raw) - 1];
|
||||
if (!($first === $last && ($first === '"' || $first === "'"))) {
|
||||
break;
|
||||
}
|
||||
$raw = substr($raw, 1, -1);
|
||||
$raw = trim($raw);
|
||||
}
|
||||
|
||||
json_decode($raw, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
return $raw;
|
||||
}
|
||||
|
||||
if (strpos($raw, '=') === false) {
|
||||
throw new \Exception('数据格式错误:必须是 JSON 字符串,或 key=value 格式');
|
||||
}
|
||||
|
||||
$data = $this->parseKeyValueData($raw);
|
||||
|
||||
return json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
protected function parseKeyValueData(string $raw): array
|
||||
{
|
||||
$parts = preg_split('/[&,\s]+/', trim($raw)) ?: [];
|
||||
$result = [];
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$part = trim((string) $part);
|
||||
if ($part === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pos = strpos($part, '=');
|
||||
if ($pos === false) {
|
||||
throw new \Exception('数据格式错误:key=value 格式中存在无效片段:' . $part);
|
||||
}
|
||||
|
||||
$key = urldecode(trim(substr($part, 0, $pos)));
|
||||
$value = urldecode(substr($part, $pos + 1));
|
||||
|
||||
if ($key === '') {
|
||||
throw new \Exception('数据格式错误:key 不能为空');
|
||||
}
|
||||
|
||||
$result[$key] = $this->castStringValue($value);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function castStringValue(string $value)
|
||||
{
|
||||
$value = trim($value);
|
||||
|
||||
$lower = strtolower($value);
|
||||
if ($lower === 'true') {
|
||||
return true;
|
||||
}
|
||||
if ($lower === 'false') {
|
||||
return false;
|
||||
}
|
||||
if ($lower === 'null') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value !== '' && ($value[0] === '{' || $value[0] === '[')) {
|
||||
$decoded = json_decode($value, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('/^-?\d+$/', $value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从框架特性参数构建 URL
|
||||
*/
|
||||
protected function buildUrlFromFrameworkParams(Input $input): string
|
||||
{
|
||||
$app = $input->getOption('app');
|
||||
$controller = $input->getOption('controller');
|
||||
$action = $input->getOption('action');
|
||||
|
||||
if (empty($app) || empty($controller) || empty($action)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 构建路由路径
|
||||
$path = '/' . $app . '/' . $controller . '/' . $action;
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
protected function resolveSuperToken(Input $input): ?string
|
||||
{
|
||||
$raw = $input->getOption('super-token');
|
||||
$value = is_bool($raw) ? ($raw ? 'true' : 'false') : trim((string) $raw);
|
||||
$lower = strtolower($value);
|
||||
|
||||
if (in_array($lower, ['false', '0', 'off', 'no'], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value === '' || in_array($lower, ['true', '1', 'on', 'yes'], true)) {
|
||||
return $this->generateToken();
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
protected function generateToken(): string
|
||||
{
|
||||
return bin2hex(random_bytes(16));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 HTTP 请求
|
||||
*/
|
||||
protected function executeRequest(array $request, Output $output): array
|
||||
{
|
||||
$guzzleOptions = [
|
||||
'headers' => $request['headers'],
|
||||
];
|
||||
|
||||
// 如果有请求体,添加到选项中
|
||||
if ($request['body'] !== null) {
|
||||
$guzzleOptions['body'] = $request['body'];
|
||||
}
|
||||
|
||||
// 执行请求
|
||||
$guzzleResponse = $this->httpClient->request($request['method'], $request['url'], $guzzleOptions);
|
||||
|
||||
// 解析响应
|
||||
$response = [
|
||||
'status' => $guzzleResponse->getStatusCode(),
|
||||
'headers' => $guzzleResponse->getHeaders(),
|
||||
'data' => null,
|
||||
];
|
||||
|
||||
// 解析响应体
|
||||
$body = $guzzleResponse->getBody()->getContents();
|
||||
$jsonData = json_decode($body, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$response['data'] = $jsonData;
|
||||
} else {
|
||||
$response['data'] = $body;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出结果
|
||||
*/
|
||||
protected function outputResult(array $response, Output $output): void
|
||||
{
|
||||
$executionTime = round((microtime(true) - $this->startTime) * 1000, 2);
|
||||
|
||||
// 构建输出数据
|
||||
$outputData = [
|
||||
'success' => $response['status'] >= 200 && $response['status'] < 300,
|
||||
'response' => [
|
||||
'status' => $response['status'],
|
||||
'data' => $response['data'],
|
||||
'headers' => $response['headers'],
|
||||
],
|
||||
'execution_time' => $executionTime,
|
||||
'exception' => null,
|
||||
];
|
||||
|
||||
// 文本模式:输出可读格式
|
||||
$this->outputTextResult($outputData, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出文本格式结果
|
||||
*/
|
||||
protected function outputTextResult(array $data, Output $output): void
|
||||
{
|
||||
$response = $data['response'];
|
||||
|
||||
$output->newLine();
|
||||
$output->info('HTTP 状态: ' . $response['status']);
|
||||
$output->info('执行时间: ' . $data['execution_time'] . 'ms');
|
||||
$output->newLine();
|
||||
|
||||
// 输出响应头
|
||||
$output->comment('响应头:');
|
||||
foreach ($response['headers'] as $key => $values) {
|
||||
$output->writeln(' ' . $key . ': ' . implode(', ', $values));
|
||||
}
|
||||
$output->newLine();
|
||||
|
||||
// 输出响应数据
|
||||
$output->comment('响应数据:');
|
||||
if (is_string($response['data'])) {
|
||||
$output->writeln(' ' . $response['data']);
|
||||
} else {
|
||||
$output->writeln(' ' . json_encode($response['data'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
|
||||
}
|
||||
$output->newLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出错误信息
|
||||
*/
|
||||
protected function outputError(\Exception $e, float $executionTime, Output $output): void
|
||||
{
|
||||
$output->error('错误: ' . $e->getMessage());
|
||||
$output->writeln('执行时间: ' . $executionTime . 'ms');
|
||||
if (env('app_debug', false)) {
|
||||
$output->writeln('文件: ' . $e->getFile() . ':' . $e->getLine());
|
||||
|
||||
// 如果是 Guzzle 连接错误,提供提示
|
||||
if (strpos($e->getMessage(), 'Failed to connect') !== false) {
|
||||
$baseUrl = $this->getBaseUrl();
|
||||
$output->comment('提示: 请确保应用正在运行(执行 "php think run")或设置 "app.app_host" 环境变量');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
199
extend/base/common/command/tools/log/ToolsLogSearchBase.php
Normal file
199
extend/base/common/command/tools/log/ToolsLogSearchBase.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\command\tools\log;
|
||||
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* tools:log:search 命令基类
|
||||
*/
|
||||
class ToolsLogSearchBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:log:search')
|
||||
->setDescription('搜索数据库日志')
|
||||
->addArgument('keywords', Argument::REQUIRED, '搜索关键词')
|
||||
->addOption('level', 'l', Option::VALUE_OPTIONAL, '过滤日志级别 (info, warning, error)')
|
||||
->addOption('limit', null, Option::VALUE_OPTIONAL, '限制显示条数', 50)
|
||||
->addOption('controller', 'c', Option::VALUE_OPTIONAL, '过滤控制器名称')
|
||||
->addOption('help', 'h', Option::VALUE_NONE, '显示帮助信息');
|
||||
}
|
||||
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$this->showHelp($output);
|
||||
return;
|
||||
}
|
||||
|
||||
$keywords = $input->getArgument('keywords');
|
||||
|
||||
if (empty($keywords)) {
|
||||
$output->error('请提供搜索关键词');
|
||||
return;
|
||||
}
|
||||
|
||||
$filters = $this->buildFilters($input);
|
||||
$limit = (int)$input->getOption('limit') ?? 50;
|
||||
|
||||
try {
|
||||
$query = Db::name('debug_log');
|
||||
|
||||
// 搜索关键词(在 content 字段中)
|
||||
$query->whereLike('content', '%' . $keywords . '%');
|
||||
|
||||
// 应用过滤条件
|
||||
if (!empty($filters['level'])) {
|
||||
$query->where('level', '=', $filters['level']);
|
||||
}
|
||||
if (!empty($filters['controller'])) {
|
||||
$query->whereLike('controller_name', '%' . $filters['controller'] . '%');
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
$count = $query->count();
|
||||
|
||||
// 获取日志记录
|
||||
$logs = $query->order('create_time', 'desc')
|
||||
->limit($limit)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 格式化时间戳并高亮关键词
|
||||
foreach ($logs as &$log) {
|
||||
if (!empty($log['create_time'])) {
|
||||
$log['create_time_formatted'] = date('Y-m-d H:i:s', $log['create_time']);
|
||||
}
|
||||
// 高亮关键词
|
||||
$log['content_highlighted'] = $this->highlightKeywords($log['content'] ?? '', $keywords);
|
||||
}
|
||||
unset($log);
|
||||
|
||||
$this->outputText($logs, $count, $keywords, $filters, $output);
|
||||
} catch (\Exception $e) {
|
||||
$output->error('搜索失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建过滤条件
|
||||
*/
|
||||
protected function buildFilters(Input $input): array
|
||||
{
|
||||
return [
|
||||
'level' => $input->getOption('level'),
|
||||
'controller' => $input->getOption('controller')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 高亮关键词
|
||||
*/
|
||||
protected function highlightKeywords(string $content, string $keywords): string
|
||||
{
|
||||
// 使用 ANSI 颜色代码高亮关键词(仅用于文本模式)
|
||||
return str_replace($keywords, '<comment>' . $keywords . '</comment>', $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本格式输出
|
||||
*/
|
||||
protected function outputText(array $logs, int $count, string $keywords, array $filters, Output $output): void
|
||||
{
|
||||
$output->newLine();
|
||||
$output->info('=== 数据库日志搜索结果 ===');
|
||||
$output->newLine();
|
||||
|
||||
$output->comment('搜索关键词:' . $keywords);
|
||||
$output->newLine();
|
||||
|
||||
// 显示过滤条件
|
||||
$activeFilters = array_filter($filters, function($value) {
|
||||
return !empty($value);
|
||||
});
|
||||
if (!empty($activeFilters)) {
|
||||
$output->comment('过滤条件:');
|
||||
foreach ($activeFilters as $key => $value) {
|
||||
$output->writeln(' - ' . $key . ': ' . $value);
|
||||
}
|
||||
$output->newLine();
|
||||
}
|
||||
|
||||
if (empty($logs)) {
|
||||
$output->comment('没有找到包含 "' . $keywords . '" 的日志');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示搜索结果
|
||||
$output->info('搜索结果(共 ' . $count . ' 条,显示 ' . count($logs) . ' 条):');
|
||||
$output->newLine();
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$levelMethod = $this->getLevelMethod($log['level'] ?? '');
|
||||
$output->$levelMethod('[' . $log['level'] . '] ' .
|
||||
'[' . ($log['create_time_formatted'] ?? 'N/A') . '] ' .
|
||||
'[' . ($log['controller_name'] ?? '') . '::' . ($log['action_name'] ?? '') . ']');
|
||||
|
||||
// 显示内容并高亮关键词
|
||||
$content = $log['content'] ?? '';
|
||||
if (strlen($content) > 200) {
|
||||
$content = substr($content, 0, 200) . '...';
|
||||
}
|
||||
$output->writeln(' ' . $this->highlightKeywords($content, $keywords));
|
||||
$output->newLine();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志级别对应的方法名
|
||||
*/
|
||||
protected function getLevelMethod(string $level): string
|
||||
{
|
||||
$level = strtolower($level);
|
||||
$methods = [
|
||||
'error' => 'error',
|
||||
'warning' => 'comment',
|
||||
'info' => 'info',
|
||||
'debug' => 'info'
|
||||
];
|
||||
|
||||
return $methods[$level] ?? 'info';
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示帮助信息
|
||||
*/
|
||||
protected function showHelp(Output $output): void
|
||||
{
|
||||
$output->newLine();
|
||||
$output->info('命令名称:');
|
||||
$output->writeln(' tools:log:search - 搜索数据库日志');
|
||||
$output->newLine();
|
||||
$output->info('用法:');
|
||||
$output->writeln(' php think tools:log:search 关键词 [选项]');
|
||||
$output->newLine();
|
||||
$output->info('参数:');
|
||||
$output->writeln(' keywords 搜索关键词(必填)');
|
||||
$output->newLine();
|
||||
$output->info('选项:');
|
||||
$output->writeln(' -l, --level 过滤日志级别 (info, warning, error)');
|
||||
$output->writeln(' --limit 限制显示条数 (默认: 50)');
|
||||
$output->writeln(' -c, --controller 过滤控制器名称');
|
||||
$output->writeln(' -h, --help 显示帮助信息');
|
||||
$output->newLine();
|
||||
$output->info('示例:');
|
||||
$output->writeln(' php think tools:log:search "用户登录失败" --level=error');
|
||||
$output->writeln(' php think tools:log:search "数据库连接" --controller=Admin --limit=20');
|
||||
$output->writeln(' php think tools:log:search "Exception" --level=error');
|
||||
$output->newLine();
|
||||
}
|
||||
}
|
||||
195
extend/base/common/command/tools/log/ToolsLogShowBase.php
Normal file
195
extend/base/common/command/tools/log/ToolsLogShowBase.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\command\tools\log;
|
||||
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* tools:log:show 命令基类
|
||||
*/
|
||||
class ToolsLogShowBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:log:show')
|
||||
->setDescription('查看数据库日志')
|
||||
->addOption('level', 'l', Option::VALUE_OPTIONAL, '过滤日志级别 (info, warning, error)')
|
||||
->addOption('limit', null, Option::VALUE_OPTIONAL, '限制显示条数', 50)
|
||||
->addOption('controller', 'c', Option::VALUE_OPTIONAL, '过滤控制器名称')
|
||||
->addOption('action', 'a', Option::VALUE_OPTIONAL, '过滤操作名称')
|
||||
->addOption('start', 's', Option::VALUE_OPTIONAL, '开始时间 (Y-m-d H:i:s)')
|
||||
->addOption('end', 'e', Option::VALUE_OPTIONAL, '结束时间 (Y-m-d H:i:s)')
|
||||
->addOption('help', 'h', Option::VALUE_NONE, '显示帮助信息');
|
||||
}
|
||||
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$this->showHelp($output);
|
||||
return;
|
||||
}
|
||||
|
||||
$filters = $this->buildFilters($input);
|
||||
$limit = (int)$input->getOption('limit') ?? 50;
|
||||
|
||||
try {
|
||||
$query = Db::name('debug_log');
|
||||
|
||||
// 应用过滤条件
|
||||
if (!empty($filters['level'])) {
|
||||
$query->where('level', '=', $filters['level']);
|
||||
}
|
||||
if (!empty($filters['controller'])) {
|
||||
$query->whereLike('controller_name', '%' . $filters['controller'] . '%');
|
||||
}
|
||||
if (!empty($filters['action'])) {
|
||||
$query->whereLike('action_name', '%' . $filters['action'] . '%');
|
||||
}
|
||||
if (!empty($filters['start'])) {
|
||||
$startTime = strtotime($filters['start']);
|
||||
if ($startTime !== false) {
|
||||
$query->where('create_time', '>=', $startTime);
|
||||
}
|
||||
}
|
||||
if (!empty($filters['end'])) {
|
||||
$endTime = strtotime($filters['end']);
|
||||
if ($endTime !== false) {
|
||||
$query->where('create_time', '<=', $endTime);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
$count = $query->count();
|
||||
|
||||
// 获取日志记录
|
||||
$logs = $query->order('create_time', 'desc')
|
||||
->limit($limit)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 格式化时间戳
|
||||
foreach ($logs as &$log) {
|
||||
if (!empty($log['create_time'])) {
|
||||
$log['create_time_formatted'] = date('Y-m-d H:i:s', $log['create_time']);
|
||||
}
|
||||
}
|
||||
unset($log);
|
||||
|
||||
$this->outputText($logs, $count, $filters, $output);
|
||||
} catch (\Exception $e) {
|
||||
$output->error('查询失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建过滤条件
|
||||
*/
|
||||
protected function buildFilters(Input $input): array
|
||||
{
|
||||
return [
|
||||
'level' => $input->getOption('level'),
|
||||
'controller' => $input->getOption('controller'),
|
||||
'action' => $input->getOption('action'),
|
||||
'start' => $input->getOption('start'),
|
||||
'end' => $input->getOption('end')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本格式输出
|
||||
*/
|
||||
protected function outputText(array $logs, int $count, array $filters, Output $output): void
|
||||
{
|
||||
$output->newLine();
|
||||
$output->info('=== 数据库日志查询结果 ===');
|
||||
$output->newLine();
|
||||
|
||||
// 显示过滤条件
|
||||
$activeFilters = array_filter($filters, function($value) {
|
||||
return !empty($value);
|
||||
});
|
||||
if (!empty($activeFilters)) {
|
||||
$output->comment('过滤条件:');
|
||||
foreach ($activeFilters as $key => $value) {
|
||||
$output->writeln(' - ' . $key . ': ' . $value);
|
||||
}
|
||||
$output->newLine();
|
||||
}
|
||||
|
||||
if (empty($logs)) {
|
||||
$output->comment('没有找到符合条件的日志');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示日志列表
|
||||
$output->info('日志列表(共 ' . $count . ' 条,显示 ' . count($logs) . ' 条):');
|
||||
$output->newLine();
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$levelMethod = $this->getLevelMethod($log['level'] ?? '');
|
||||
$output->$levelMethod('[' . $log['level'] . '] ' .
|
||||
'[' . ($log['create_time_formatted'] ?? 'N/A') . '] ' .
|
||||
'[' . ($log['controller_name'] ?? '') . '::' . ($log['action_name'] ?? '') . ']');
|
||||
|
||||
// 显示内容摘要
|
||||
$content = $log['content'] ?? '';
|
||||
if (strlen($content) > 200) {
|
||||
$content = substr($content, 0, 200) . '...';
|
||||
}
|
||||
$output->writeln(' ' . $content);
|
||||
$output->newLine();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志级别对应的方法名
|
||||
*/
|
||||
protected function getLevelMethod(string $level): string
|
||||
{
|
||||
$level = strtolower($level);
|
||||
$methods = [
|
||||
'error' => 'error',
|
||||
'warning' => 'comment',
|
||||
'info' => 'info',
|
||||
'debug' => 'info'
|
||||
];
|
||||
|
||||
return $methods[$level] ?? 'info';
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示帮助信息
|
||||
*/
|
||||
protected function showHelp(Output $output): void
|
||||
{
|
||||
$output->newLine();
|
||||
$output->info('命令名称:');
|
||||
$output->writeln(' tools:log:show - 查看数据库日志');
|
||||
$output->newLine();
|
||||
$output->info('用法:');
|
||||
$output->writeln(' php think tools:log:show [选项]');
|
||||
$output->newLine();
|
||||
$output->info('选项:');
|
||||
$output->writeln(' -l, --level 过滤日志级别 (info, warning, error)');
|
||||
$output->writeln(' --limit 限制显示条数 (默认: 50)');
|
||||
$output->writeln(' -c, --controller 过滤控制器名称');
|
||||
$output->writeln(' -a, --action 过滤操作名称');
|
||||
$output->writeln(' -s, --start 开始时间 (Y-m-d H:i:s)');
|
||||
$output->writeln(' -e, --end 结束时间 (Y-m-d H:i:s)');
|
||||
$output->writeln(' -h, --help 显示帮助信息');
|
||||
$output->newLine();
|
||||
$output->info('示例:');
|
||||
$output->writeln(' php think tools:log:show --level=error --limit=10');
|
||||
$output->writeln(' php think tools:log:show --controller=Admin --limit=20');
|
||||
$output->writeln(' php think tools:log:show --start="2024-01-01 00:00:00" --end="2024-12-31 23:59:59"');
|
||||
$output->newLine();
|
||||
}
|
||||
}
|
||||
259
extend/base/common/command/tools/log/ToolsLogStatsBase.php
Normal file
259
extend/base/common/command/tools/log/ToolsLogStatsBase.php
Normal file
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\command\tools\log;
|
||||
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* tools:log:stats 命令基类
|
||||
*/
|
||||
class ToolsLogStatsBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('tools:log:stats')
|
||||
->setDescription('统计数据库日志')
|
||||
->addOption('by-level', null, Option::VALUE_NONE, '按日志级别统计')
|
||||
->addOption('by-controller', null, Option::VALUE_NONE, '按控制器统计')
|
||||
->addOption('by-time', null, Option::VALUE_NONE, '按时间统计')
|
||||
->addOption('time-group', null, Option::VALUE_OPTIONAL, '时间分组 (hour|day|week|month)', 'day')
|
||||
->addOption('help', 'h', Option::VALUE_NONE, '显示帮助信息');
|
||||
}
|
||||
|
||||
protected function execute($input, $output)
|
||||
{
|
||||
if ($input->getOption('help')) {
|
||||
$this->showHelp($output);
|
||||
return;
|
||||
}
|
||||
|
||||
$byLevel = $input->getOption('by-level');
|
||||
$byController = $input->getOption('by-controller');
|
||||
$byTime = $input->getOption('by-time');
|
||||
$timeGroup = $input->getOption('time-group') ?? 'day';
|
||||
|
||||
// 如果没有指定任何统计类型,默认显示所有统计
|
||||
if (!$byLevel && !$byController && !$byTime) {
|
||||
$byLevel = $byController = $byTime = true;
|
||||
}
|
||||
|
||||
try {
|
||||
$stats = [
|
||||
'by_level' => [],
|
||||
'by_controller' => [],
|
||||
'by_time' => []
|
||||
];
|
||||
|
||||
// 按日志级别统计
|
||||
if ($byLevel) {
|
||||
$stats['by_level'] = $this->statsByLevel();
|
||||
}
|
||||
|
||||
// 按控制器统计
|
||||
if ($byController) {
|
||||
$stats['by_controller'] = $this->statsByController();
|
||||
}
|
||||
|
||||
// 按时间统计
|
||||
if ($byTime) {
|
||||
$stats['by_time'] = $this->statsByTime($timeGroup);
|
||||
}
|
||||
|
||||
// 总数
|
||||
$stats['total'] = Db::name('debug_log')->count();
|
||||
|
||||
$this->outputText($stats, $byLevel, $byController, $byTime, $output);
|
||||
} catch (\Exception $e) {
|
||||
$output->error('统计失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按日志级别统计
|
||||
*/
|
||||
protected function statsByLevel(): array
|
||||
{
|
||||
$results = Db::name('debug_log')
|
||||
->field('level, COUNT(*) as count')
|
||||
->group('level')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$stats = [];
|
||||
foreach ($results as $row) {
|
||||
$stats[$row['level']] = (int)$row['count'];
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按控制器统计
|
||||
*/
|
||||
protected function statsByController(): array
|
||||
{
|
||||
$results = Db::name('debug_log')
|
||||
->field('controller_name, COUNT(*) as count')
|
||||
->where('controller_name', '<>', '')
|
||||
->group('controller_name')
|
||||
->order('count', 'desc')
|
||||
->limit(20)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$stats = [];
|
||||
foreach ($results as $row) {
|
||||
$stats[$row['controller_name']] = (int)$row['count'];
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按时间统计
|
||||
*/
|
||||
protected function statsByTime(string $group): array
|
||||
{
|
||||
$query = Db::name('debug_log');
|
||||
|
||||
// 根据分组类型格式化时间
|
||||
switch ($group) {
|
||||
case 'hour':
|
||||
$dateFormat = '%Y-%m-%d %H:00';
|
||||
$phpFormat = 'Y-m-d H:00';
|
||||
break;
|
||||
case 'week':
|
||||
$dateFormat = '%Y-%u'; // 年-周数
|
||||
$phpFormat = 'Y-W'; // 年-周数
|
||||
break;
|
||||
case 'month':
|
||||
$dateFormat = '%Y-%m';
|
||||
$phpFormat = 'Y-m';
|
||||
break;
|
||||
case 'day':
|
||||
default:
|
||||
$dateFormat = '%Y-%m-%d';
|
||||
$phpFormat = 'Y-m-d';
|
||||
break;
|
||||
}
|
||||
|
||||
// 使用 MySQL DATE_FORMAT 函数
|
||||
$results = $query->field('FROM_UNIXTIME(create_time, \'' . $dateFormat . '\') as time_period, COUNT(*) as count')
|
||||
->where('create_time', '>', 0)
|
||||
->group('time_period')
|
||||
->order('time_period', 'desc')
|
||||
->limit(30)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$stats = [];
|
||||
foreach ($results as $row) {
|
||||
$stats[$row['time_period']] = (int)$row['count'];
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本格式输出
|
||||
*/
|
||||
protected function outputText(array $stats, bool $byLevel, bool $byController, bool $byTime, Output $output): void
|
||||
{
|
||||
$output->newLine();
|
||||
$output->info('=== 数据库日志统计 ===');
|
||||
$output->newLine();
|
||||
|
||||
// 总数
|
||||
$output->info('总日志数:' . ($stats['total'] ?? 0));
|
||||
$output->newLine();
|
||||
|
||||
// 按级别统计
|
||||
if ($byLevel) {
|
||||
$output->comment('按日志级别统计:');
|
||||
if (empty($stats['by_level'])) {
|
||||
$output->writeln(' 无数据');
|
||||
} else {
|
||||
foreach ($stats['by_level'] as $level => $count) {
|
||||
$output->writeln(' ' . $level . ':' . $count . ' 条');
|
||||
}
|
||||
}
|
||||
$output->newLine();
|
||||
}
|
||||
|
||||
// 按控制器统计
|
||||
if ($byController) {
|
||||
$output->comment('按控制器统计(Top 20):');
|
||||
if (empty($stats['by_controller'])) {
|
||||
$output->writeln(' 无数据');
|
||||
} else {
|
||||
foreach ($stats['by_controller'] as $controller => $count) {
|
||||
$output->writeln(' ' . $controller . ':' . $count . ' 条');
|
||||
}
|
||||
}
|
||||
$output->newLine();
|
||||
}
|
||||
|
||||
// 按时间统计
|
||||
if ($byTime) {
|
||||
$output->comment('按时间统计(最近 30 个时段):');
|
||||
if (empty($stats['by_time'])) {
|
||||
$output->writeln(' 无数据');
|
||||
} else {
|
||||
foreach ($stats['by_time'] as $timePeriod => $count) {
|
||||
$output->writeln(' ' . $timePeriod . ':' . $count . ' 条');
|
||||
}
|
||||
}
|
||||
$output->newLine();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志级别对应的颜色
|
||||
*/
|
||||
protected function getLevelColor(string $level): string
|
||||
{
|
||||
$level = strtolower($level);
|
||||
$colors = [
|
||||
'error' => 'error',
|
||||
'warning' => 'comment',
|
||||
'info' => 'info',
|
||||
'debug' => 'info'
|
||||
];
|
||||
|
||||
return $colors[$level] ?? 'info';
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示帮助信息
|
||||
*/
|
||||
protected function showHelp(Output $output): void
|
||||
{
|
||||
$output->newLine();
|
||||
$output->info('命令名称:');
|
||||
$output->writeln(' tools:log:stats - 统计数据库日志');
|
||||
$output->newLine();
|
||||
$output->info('用法:');
|
||||
$output->writeln(' php think tools:log:stats [选项]');
|
||||
$output->newLine();
|
||||
$output->info('选项:');
|
||||
$output->writeln(' --by-level 按日志级别统计');
|
||||
$output->writeln(' --by-controller 按控制器统计');
|
||||
$output->writeln(' --by-time 按时间统计');
|
||||
$output->writeln(' --time-group 时间分组 (hour|day|week|month,默认: day)');
|
||||
$output->writeln(' -h, --help 显示帮助信息');
|
||||
$output->newLine();
|
||||
$output->info('示例:');
|
||||
$output->writeln(' php think tools:log:stats');
|
||||
$output->writeln(' php think tools:log:stats --by-level');
|
||||
$output->writeln(' php think tools:log:stats --by-controller --by-time');
|
||||
$output->writeln(' php think tools:log:stats --by-time --time-group=hour');
|
||||
$output->newLine();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user