更新
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* AI 数据目录生成器:静态扫描 app/adminapi/controller 下的全部接口,写出 app/mcp/catalog/generated.php。
|
||||
*
|
||||
* 用法(在 server 目录下):
|
||||
* php app/mcp/cli/catalog.php 只打印统计,不写文件
|
||||
* php app/mcp/cli/catalog.php --write 重新生成 generated.php
|
||||
*
|
||||
* 生成结果只是“盘点”:接口种类、列表类、HTTP 方式、疑似写操作、外部调用、可用参数。
|
||||
* 是否对 AI 开放由运行时规则 + 人工审核文件 resources.php 共同决定(见 app/mcp/service/Catalog.php)。
|
||||
*/
|
||||
|
||||
use think\App;
|
||||
|
||||
$root = dirname(__DIR__, 3) . DIRECTORY_SEPARATOR;
|
||||
require $root . 'vendor/autoload.php';
|
||||
(new App($root))->initialize();
|
||||
|
||||
$controllerRoot = $root . 'app' . DIRECTORY_SEPARATOR . 'adminapi' . DIRECTORY_SEPARATOR . 'controller';
|
||||
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($controllerRoot, FilesystemIterator::SKIP_DOTS));
|
||||
|
||||
const WRITE_NAME = '/^(add|edit|del|delete|update|save|create|set|bind|unbind|sync|send|import|upload|assign|confirm|cancel|audit|refund|pay|void|copy|sort|change|reset|clear|mark|remove|retry|regenerate|review|generate|export|notify|callback|close|open|start|stop|withdraw|submit|apply|approve|reject|handle|push|rollback|restore|transfer|merge|split|adjust|lock|unlock|enable|disable|publish|login|logout|register|recall|resend|rebind|toggle|batch|move|release|finish|complete|init|install|upgrade|clean|purge|refresh|dispatch|run|execute|trigger|call|hangup|accept|invite|join|leave|kick|share|like|unlike|follow|unfollow|read|reply|forward|archive|unarchive|pin|unpin|star|unstar|download)/i';
|
||||
const READ_NAME = '/^(lists?|detail|info|overview|stats?|statistics|summary|index|all|options?|trend|leaderboard|multi|reports?|statuses|progress|orders|records?|logs?|dict|count|search|query|check|preview|show|view|tree|config|get[A-Z]|[a-z]+(Lists?|Stats?|Statistics|Options|Trend|Lines|Breakdown|Detail|Info|Summary|Overview|Records?|Logs?|Count|Tree|Matrix|Board|Report|Reports|Data|History|Board)$)/';
|
||||
const EXTERNAL = '/(Http::|curl_init|curl_exec|GuzzleHttp|new\s+Client\s*\(|easywechat|EasyWeChat|Qywx\w*(Api|Client)|qyapi\.weixin|api\.weixin|TencentCloud|file_get_contents\(\s*[\'"]https?:|HttpClient|Gancao\w*Service|EjPharmacy\w*Service|SmsDriver|sendSms|Tencent\w*Im\w*Service|\bTimService::|\bImService::|Kuaidi|express\w*Service|logisticsTrace)/i';
|
||||
const WRITES = '/(->save\(|::create\(|->insert(All|GetId)?\(|->update\(\s*\[|::update\(\s*\[|->delete\(|::destroy\(|->inc\(|->dec\(|->setInc\(|->setDec\(|Db::execute|->saveAll\(|markAssignRead|->startTrans\(|Db::startTrans|::transaction\(|->exp\()/';
|
||||
|
||||
function useMap(string $source, string $namespace): array
|
||||
{
|
||||
$map = [];
|
||||
if (preg_match_all('/^use\s+([^;\s]+)(?:\s+as\s+(\w+))?;/m', $source, $m, PREG_SET_ORDER)) {
|
||||
foreach ($m as $u) {
|
||||
$alias = $u[2] ?? '' ?: substr(strrchr('\\' . $u[1], '\\'), 1);
|
||||
$map[$alias] = ltrim($u[1], '\\');
|
||||
}
|
||||
}
|
||||
$map['__ns'] = $namespace;
|
||||
return $map;
|
||||
}
|
||||
|
||||
function resolveClass(string $short, array $uses): ?string
|
||||
{
|
||||
if (str_contains($short, '\\')) {
|
||||
return ltrim($short, '\\');
|
||||
}
|
||||
if (isset($uses[$short])) {
|
||||
return $uses[$short];
|
||||
}
|
||||
$guess = $uses['__ns'] . '\\' . $short;
|
||||
return class_exists($guess) ? $guess : null;
|
||||
}
|
||||
|
||||
function methodSource(ReflectionMethod $method): string
|
||||
{
|
||||
$file = $method->getFileName();
|
||||
if (!$file || !is_file($file)) {
|
||||
return '';
|
||||
}
|
||||
$lines = file($file);
|
||||
return implode('', array_slice($lines, $method->getStartLine() - 1, $method->getEndLine() - $method->getStartLine() + 1));
|
||||
}
|
||||
|
||||
function classMethodSource(string $class, string $method): string
|
||||
{
|
||||
try {
|
||||
return methodSource(new ReflectionMethod($class, $method));
|
||||
} catch (Throwable $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** 参数名:列表类 setSearch 的字段、$this->params['x']、request->get('x') 以及 Logic 里的 $params['x'] */
|
||||
function scanParams(string $source): array
|
||||
{
|
||||
$params = [];
|
||||
$patterns = [
|
||||
'/\$this->params\[\s*[\'"](\w+)[\'"]\s*\]/',
|
||||
'/\$params\[\s*[\'"](\w+)[\'"]\s*\]/',
|
||||
'/->(?:get|param|post)\(\s*[\'"](\w+)(?:\/\w)?[\'"]/',
|
||||
'/request\(\)->(?:get|param|post)\(\s*[\'"](\w+)(?:\/\w)?[\'"]/',
|
||||
];
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match_all($pattern, $source, $m)) {
|
||||
array_push($params, ...$m[1]);
|
||||
}
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
function scanSearch(string $listsClass): array
|
||||
{
|
||||
$source = classMethodSource($listsClass, 'setSearch');
|
||||
if ($source === '') {
|
||||
return [];
|
||||
}
|
||||
$fields = [];
|
||||
if (preg_match_all('/[\'"]([a-z_]+\.)?([a-z_]\w*)[\'"]/i', $source, $m, PREG_SET_ORDER)) {
|
||||
foreach ($m as $f) {
|
||||
$name = $f[2];
|
||||
if (in_array($name, ['in', 'like', 'between', 'between_time', 'find_in_set'], true)) {
|
||||
continue;
|
||||
}
|
||||
$fields[] = $name;
|
||||
}
|
||||
}
|
||||
if (str_contains($source, 'between_time')) {
|
||||
array_push($fields, 'start_time', 'end_time');
|
||||
}
|
||||
if (preg_match("/['\"]between['\"]/", $source)) {
|
||||
array_push($fields, 'start', 'end');
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$inventory = [];
|
||||
foreach ($files as $file) {
|
||||
if (!str_ends_with($file->getFilename(), 'Controller.php')) {
|
||||
continue;
|
||||
}
|
||||
$source = file_get_contents($file->getPathname());
|
||||
if (!preg_match('/^namespace\s+([^;]+);/m', $source, $ns) || !preg_match('/^\s*(?:final\s+|abstract\s+)?class\s+(\w+)/m', $source, $cls)) {
|
||||
continue;
|
||||
}
|
||||
$class = $ns[1] . '\\' . $cls[1];
|
||||
if (!class_exists($class)) {
|
||||
continue;
|
||||
}
|
||||
$ref = new ReflectionClass($class);
|
||||
if ($ref->isAbstract()) {
|
||||
continue;
|
||||
}
|
||||
$uses = useMap($source, $ns[1]);
|
||||
$sub = trim(substr($ns[1], strlen('app\\adminapi\\controller')), '\\');
|
||||
$dotted = ($sub === '' ? '' : str_replace('\\', '.', $sub) . '.') . lcfirst(substr($cls[1], 0, -strlen('Controller')));
|
||||
$notNeedLogin = $ref->getDefaultProperties()['notNeedLogin'] ?? [];
|
||||
foreach ($ref->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
|
||||
if ($method->isStatic() || $method->getDeclaringClass()->getName() !== $class || str_starts_with($method->getName(), '__') || in_array($method->getName(), ['initialize', 'isNotNeedLogin'], true)) {
|
||||
continue;
|
||||
}
|
||||
$action = $method->getName();
|
||||
$body = methodSource($method);
|
||||
$lists = null;
|
||||
if (preg_match('/dataLists\(\s*new\s+\\\\?([\w\\\\]+)\s*\(/', $body, $lm)) {
|
||||
$lists = resolveClass($lm[1], $uses);
|
||||
}
|
||||
$logicCalls = [];
|
||||
$logicSource = '';
|
||||
if (preg_match_all('/\b(\w+Logic|\w+Service)::(\w+)\(/', $body, $calls, PREG_SET_ORDER)) {
|
||||
foreach ($calls as $call) {
|
||||
$logicClass = resolveClass($call[1], $uses);
|
||||
if ($logicClass) {
|
||||
$logicCalls[] = $call[1] . '::' . $call[2];
|
||||
$logicSource .= classMethodSource($logicClass, $call[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
$listsSource = '';
|
||||
if ($lists && class_exists($lists)) {
|
||||
$listsRef = new ReflectionClass($lists);
|
||||
$listsSource = (string) file_get_contents($listsRef->getFileName());
|
||||
}
|
||||
$post = (bool) preg_match('/->post\(\)|->isPost\(\)|\$this->request->post\(|request\(\)->post\(/', $body);
|
||||
if ($lists) {
|
||||
$kind = 'list';
|
||||
} elseif (preg_match(WRITE_NAME, $action) && !preg_match(READ_NAME, $action)) {
|
||||
$kind = 'write';
|
||||
} elseif (preg_match('/detail|Detail/', $action) || preg_match("/goCheck\(\s*['\"](detail|id)['\"]/", $body)) {
|
||||
$kind = 'detail';
|
||||
} elseif (preg_match(READ_NAME, $action)) {
|
||||
$kind = 'report';
|
||||
} else {
|
||||
$kind = 'other';
|
||||
}
|
||||
$scanSource = $body . $logicSource;
|
||||
$writes = [];
|
||||
if (preg_match_all(WRITES, $body . ($kind === 'list' ? '' : $logicSource), $wm)) {
|
||||
$writes = array_values(array_unique($wm[1]));
|
||||
}
|
||||
$external = [];
|
||||
if (preg_match_all(EXTERNAL, $scanSource . $listsSource, $em)) {
|
||||
$external = array_values(array_unique($em[1]));
|
||||
}
|
||||
$params = scanParams($body . $logicSource . $listsSource);
|
||||
if ($lists) {
|
||||
$params = array_merge(scanSearch($lists), $params);
|
||||
}
|
||||
$params = array_values(array_unique(array_filter($params, static fn ($p) => !in_array($p, ['page_no', 'page_size', 'page_type', 'export', 'page_start', 'page_end'], true))));
|
||||
$inventory[$dotted . '/' . $action] = [
|
||||
'controller' => $class,
|
||||
'action' => $action,
|
||||
'kind' => $kind,
|
||||
'lists' => $lists,
|
||||
'http' => $post ? 'POST' : 'GET',
|
||||
'writes' => $writes,
|
||||
'external' => $external,
|
||||
'logic' => array_values(array_unique($logicCalls)),
|
||||
'params' => $params,
|
||||
'no_login' => in_array($action, (array) $notNeedLogin, true),
|
||||
];
|
||||
}
|
||||
}
|
||||
ksort($inventory);
|
||||
|
||||
$counts = array_count_values(array_column($inventory, 'kind'));
|
||||
ksort($counts);
|
||||
echo 'controllers scanned, actions: ' . count($inventory) . PHP_EOL;
|
||||
foreach ($counts as $kind => $n) {
|
||||
echo str_pad($kind, 8) . $n . PHP_EOL;
|
||||
}
|
||||
echo 'with external calls: ' . count(array_filter($inventory, static fn ($r) => $r['external'])) . PHP_EOL;
|
||||
echo 'read-kind with write markers: ' . count(array_filter($inventory, static fn ($r) => $r['writes'] && in_array($r['kind'], ['list', 'report', 'detail'], true))) . PHP_EOL;
|
||||
|
||||
if (in_array('--write', $argv, true)) {
|
||||
$target = $root . 'app' . DIRECTORY_SEPARATOR . 'mcp' . DIRECTORY_SEPARATOR . 'catalog' . DIRECTORY_SEPARATOR . 'generated.php';
|
||||
$header = "<?php\n// 由 php app/mcp/cli/catalog.php --write 生成,请勿手工修改;人工审核结论写在 resources.php。\n// 生成时间:" . date('Y-m-d H:i:s') . "\nreturn ";
|
||||
file_put_contents($target, $header . var_export($inventory, true) . ";\n");
|
||||
echo 'written: ' . $target . PHP_EOL;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* AI 数据目录:数据表覆盖检查。逐张表判断它能否通过 AI 查到:
|
||||
* endpoint 后台接口(adminapi)用到这张表,由目录里的接口资源覆盖
|
||||
* table 目录里有针对这张表的“数据表资源”(后台没有页面的表)
|
||||
* system 凭据、会话、配置、日志、队列等系统表,不对 AI 开放
|
||||
* uncovered 以上都不是:需要补一个数据表资源或写明不开放
|
||||
*
|
||||
* 用法(在 server 目录下,连接预发/测试库):
|
||||
* php app/mcp/cli/coverage.php 打印覆盖报告
|
||||
* php app/mcp/cli/coverage.php --write-tables 为 uncovered 的表生成 review/tables.php(仅超级管理员可查,去掉凭据列)
|
||||
*/
|
||||
|
||||
use app\mcp\service\Catalog;
|
||||
use think\App;
|
||||
use think\facade\Db;
|
||||
use think\helper\Str;
|
||||
|
||||
$root = dirname(__DIR__, 3) . DIRECTORY_SEPARATOR;
|
||||
require $root . 'vendor/autoload.php';
|
||||
(new App($root))->initialize();
|
||||
|
||||
const SYSTEM_TABLE = '/(session|token|^config$|_config$|^dev_|generate|crontab|^jobs|migration|install|^ai_grant$|^ai_access_log$|operation_log|^system_|^decorate|^notice_setting|^sms_log|file_cate|^file$|^article|^hot_search|^dict_|^iam_|^admin_role$|^admin_dept$|^admin_jobs$|^jobs$|pay_config|pay_way|^refund_log$|^recharge_order$|^user_auth$|^asset_|_cursor$|provider_state|_inbox$|^prescription_ai_(attempt|limit|request)$|query_log$|patient_trtc|click_log$|allocator$)/';
|
||||
const CREDENTIAL_COLUMN = '/(password|salt|secret|token|cipher|session_key|private_key|api_key|app_key|access_key|aes_key|signature|sign_key|user_?sig|cookie|credential|ticket)/i';
|
||||
|
||||
$prefix = (string) config('database.connections.' . config('database.default') . '.prefix');
|
||||
$tables = [];
|
||||
foreach (Db::query('SHOW TABLES') as $row) {
|
||||
$name = (string) array_values($row)[0];
|
||||
if ($prefix === '' || str_starts_with($name, $prefix)) {
|
||||
$tables[] = substr($name, strlen($prefix));
|
||||
}
|
||||
}
|
||||
sort($tables);
|
||||
|
||||
// 后台接口涉及的表:adminapi 代码里 Db::name/table 直接写的表名,以及 use 的模型类对应的表
|
||||
$modelTable = static function (string $class) use ($prefix): ?string {
|
||||
if (!class_exists($class)) {
|
||||
return null;
|
||||
}
|
||||
$ref = new ReflectionClass($class);
|
||||
if ($ref->isAbstract() || !$ref->isSubclassOf(\think\Model::class)) {
|
||||
return null;
|
||||
}
|
||||
$defaults = $ref->getDefaultProperties();
|
||||
if (!empty($defaults['table'])) {
|
||||
return preg_replace('/^' . preg_quote($prefix, '/') . '/', '', (string) $defaults['table']);
|
||||
}
|
||||
return !empty($defaults['name']) ? (string) $defaults['name'] : Str::snake($ref->getShortName());
|
||||
};
|
||||
$reachable = [];
|
||||
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root . 'app' . DIRECTORY_SEPARATOR . 'adminapi', FilesystemIterator::SKIP_DOTS));
|
||||
foreach ($files as $file) {
|
||||
if ($file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
$source = (string) file_get_contents($file->getPathname());
|
||||
if (preg_match_all('/(?:Db::|->)(?:name|table)\(\s*[\'"](\w+)/', $source, $m)) {
|
||||
foreach ($m[1] as $table) {
|
||||
$reachable[preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table)] = true;
|
||||
}
|
||||
}
|
||||
if (preg_match_all('/^use\s+(app\\\\common\\\\model\\\\[\w\\\\]+);/m', $source, $m)) {
|
||||
foreach ($m[1] as $class) {
|
||||
if ($table = $modelTable($class)) {
|
||||
$reachable[$table] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$tableResources = [];
|
||||
foreach (Catalog::all() as $key => $resource) {
|
||||
if (!empty($resource['handler']['table'])) {
|
||||
$tableResources[$resource['handler']['table']] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
$report = [];
|
||||
foreach ($tables as $table) {
|
||||
$report[$table] = isset($tableResources[$table]) ? 'table' : (preg_match(SYSTEM_TABLE, $table) ? 'system' : (isset($reachable[$table]) ? 'endpoint' : 'uncovered'));
|
||||
}
|
||||
$counts = array_count_values($report);
|
||||
ksort($counts);
|
||||
echo 'tables: ' . count($tables) . ' ' . json_encode($counts) . PHP_EOL;
|
||||
foreach ($report as $table => $status) {
|
||||
if ($status === 'uncovered' || in_array('--verbose', $argv, true)) {
|
||||
echo str_pad($status, 10) . $table . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array('--write-tables', $argv, true)) {
|
||||
$entries = [];
|
||||
foreach ($report as $table => $status) {
|
||||
if ($status !== 'uncovered' && $status !== 'table') {
|
||||
continue;
|
||||
}
|
||||
$columns = [];
|
||||
$types = [];
|
||||
foreach (Db::query('SHOW COLUMNS FROM `' . $prefix . $table . '`') as $column) {
|
||||
$types[$column['Field']] = strtolower((string) $column['Type']);
|
||||
if (!preg_match(CREDENTIAL_COLUMN, (string) $column['Field'])) {
|
||||
$columns[] = $column['Field'];
|
||||
}
|
||||
}
|
||||
$filters = [];
|
||||
foreach ($columns as $column) {
|
||||
if ($column === 'id' || str_ends_with($column, '_id') || in_array($column, ['status', 'type'], true)) {
|
||||
$filters[$column] = '=';
|
||||
}
|
||||
}
|
||||
$date = isset($types['create_time']) ? 'create_time' : null;
|
||||
$entries['table.' . $table . '/lists'] = [
|
||||
'status' => 'open',
|
||||
'kind' => 'table',
|
||||
'perm' => 'ai.mcp/tables',
|
||||
'name' => '数据表 ' . $table,
|
||||
'domain' => '其他数据表',
|
||||
'note' => '后台没有页面的数据表:目前仅超级管理员可查;如需给其他角色开放,把 scope 改为属主列(如 [\'owner\' => [\'doctor_id\']])并审核',
|
||||
'handler' => array_filter([
|
||||
'table' => $table,
|
||||
'columns' => $columns,
|
||||
'filters' => $filters,
|
||||
'date' => $date,
|
||||
'date_type' => $date && str_contains($types[$date], 'int') ? 'int' : ($date ? 'datetime' : null),
|
||||
'soft_delete' => isset($types['delete_time']) ? 'delete_time' : null,
|
||||
'order' => in_array('id', $columns, true) ? 'id desc' : null,
|
||||
'scope' => 'root',
|
||||
], static fn ($v) => $v !== null),
|
||||
];
|
||||
}
|
||||
$target = $root . 'app' . DIRECTORY_SEPARATOR . 'mcp' . DIRECTORY_SEPARATOR . 'catalog' . DIRECTORY_SEPARATOR . 'review' . DIRECTORY_SEPARATOR . 'tables.php';
|
||||
$header = "<?php\n// 后台没有页面的数据表(php app/mcp/cli/coverage.php --write-tables 生成,可手工调整 scope/columns)。\n// 生成时间:" . date('Y-m-d H:i:s') . "\nreturn ";
|
||||
file_put_contents($target, $header . var_export($entries, true) . ";\n");
|
||||
echo 'written ' . count($entries) . ' table resources: ' . $target . PHP_EOL;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* AI 数据目录探测:以指定后台账号的身份,在只读事务里逐个执行候选资源,报告哪些正常、哪些会写库、哪些报错。
|
||||
* 用于人工审核(在测试/预发环境的数据库上运行,不要在生产库上跑)。
|
||||
*
|
||||
* 用法(在 server 目录下):
|
||||
* php app/mcp/cli/probe.php --admin=1 [--only=tcm.] [--external] [--json=runtime/probe.json]
|
||||
* --admin 用哪个后台账号(ID)的权限执行,建议用 root 账号看全貌
|
||||
* --only 只探测以此开头的资源
|
||||
* --external 也探测会调用外部接口的资源(默认跳过)
|
||||
* 注意:只读事务只能挡住写库;起进程、写缓存、调外部接口挡不住。所以写操作类、已标记不开放的、
|
||||
* 以及扫描出外部调用的资源默认一律不执行。
|
||||
*/
|
||||
|
||||
use app\common\model\auth\Admin;
|
||||
use app\mcp\service\Catalog;
|
||||
use app\mcp\service\Dispatcher;
|
||||
use app\mcp\service\Identity;
|
||||
use think\App;
|
||||
|
||||
$root = dirname(__DIR__, 3) . DIRECTORY_SEPARATOR;
|
||||
require $root . 'vendor/autoload.php';
|
||||
$app = new App($root);
|
||||
$app->initialize();
|
||||
|
||||
$options = getopt('', ['admin:', 'only:', 'external', 'json:']);
|
||||
$admin = Admin::where('id', (int) ($options['admin'] ?? 0))->findOrEmpty();
|
||||
if ($admin->isEmpty()) {
|
||||
fwrite(STDERR, "请用 --admin=<后台账号ID> 指定执行身份\n");
|
||||
exit(1);
|
||||
}
|
||||
$identity = new Identity(['id' => 0, 'expire_time' => time() + 3600], $admin);
|
||||
$only = (string) ($options['only'] ?? '');
|
||||
$results = [];
|
||||
foreach (Catalog::all() as $key => $resource) {
|
||||
if ($only !== '' && !str_starts_with($key, $only)) {
|
||||
continue;
|
||||
}
|
||||
if ($resource['kind'] === 'write' || $resource['http'] === 'POST' || !empty($resource['no_login']) || $resource['status'] === Catalog::EXCLUDED) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($options['external']) && !empty($resource['external'])) {
|
||||
continue;
|
||||
}
|
||||
$params = match ($resource['kind']) {
|
||||
'list' => ['page_no' => 1, 'page_size' => 3, 'page_type' => 1],
|
||||
'detail' => [(string) ($resource['guard']['param'] ?? $resource['id_param'] ?? 'id') => 1],
|
||||
default => [],
|
||||
};
|
||||
$started = microtime(true);
|
||||
$envelope = Dispatcher::call($identity, $resource, array_merge($params, (array) ($resource['force'] ?? [])));
|
||||
$ms = (int) round((microtime(true) - $started) * 1000);
|
||||
$msg = $envelope['msg'];
|
||||
$outcome = $envelope['code'] === 1 ? 'ok' : (str_contains($msg, '只读保护') ? 'writes' : (str_contains($msg, '查询失败') ? 'error' : 'fail'));
|
||||
$rows = is_array($envelope['data']['lists'] ?? null) ? count($envelope['data']['lists']) : null;
|
||||
$results[$key] = ['status' => $resource['status'], 'kind' => $resource['kind'], 'outcome' => $outcome, 'msg' => mb_substr($msg, 0, 120), 'rows' => $rows, 'ms' => $ms];
|
||||
printf("%-8s %-8s %-7s %5dms %s %s\n", $outcome, $resource['status'], $resource['kind'], $ms, $key, $outcome === 'ok' ? '' : mb_substr($msg, 0, 80));
|
||||
}
|
||||
$summary = array_count_values(array_column($results, 'outcome'));
|
||||
ksort($summary);
|
||||
echo PHP_EOL . json_encode($summary, JSON_UNESCAPED_UNICODE) . PHP_EOL;
|
||||
if (!empty($options['json'])) {
|
||||
file_put_contents($root . $options['json'], json_encode($results, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||
}
|
||||
Reference in New Issue
Block a user