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

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