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 = "