dryRun = (bool) $this->option('dry-run'); if ($this->dryRun) { $this->warn('预演模式:不会写入任何数据'); } $noLog = (array) config('nl.log.no_insert', []); $seen = []; $created = 0; $updated = 0; foreach (Route::getRoutes() as $route) { $url = $this->normalizeUri($route->uri()); if ($url === null) { continue; } $method = $this->mapMethod($route); $key = $url . '#' . $method; if (isset($seen[$key])) { continue; } $seen[$key] = true; $attributes = [ 'name' => $this->resolveName($route, $url), 'description' => $this->resolveDescription($route), 'controller' => $this->resolveController($route), 'is_log' => in_array($url, $noLog, true) ? 0 : 1, 'status' => 1, ]; $exists = ApiEndpointModel::where('url', $url)->where('method', $method)->first(); if ($exists) { // 名称与说明可能被人在后台改过,不覆盖;只补空值与纠正控制器归属 $diff = []; foreach (['controller', 'status'] as $field) { if ((string) $exists->{$field} !== (string) $attributes[$field]) { $diff[$field] = $attributes[$field]; } } foreach (['name', 'description'] as $field) { if ((string) $exists->{$field} === '' && $attributes[$field] !== '') { $diff[$field] = $attributes[$field]; } } if ((int) $exists->deleted_at !== 0) { $diff['deleted_at'] = 0; } if (!empty($diff)) { $updated++; $this->line(" ~ {$url} [{$method}] " . implode(', ', array_keys($diff))); if (!$this->dryRun) { $diff['updated_at'] = time(); ApiEndpointModel::where('id', $exists->id)->update($diff); } } continue; } $created++; $this->line(" + {$url} [{$method}] {$attributes['name']}"); if (!$this->dryRun) { ApiEndpointModel::insert($attributes + [ 'url' => $url, 'method' => $method, 'created_at' => time(), 'deleted_at' => 0, ]); } } $stale = $this->findStale(array_keys($seen)); $this->newLine(); $this->info("新增 {$created} 条,更新 {$updated} 条,路由已不存在 " . count($stale) . ' 条'); if (!empty($stale)) { foreach ($stale as $row) { $this->line(" ? {$row->url} [{$row->method}] {$row->name}"); } if ($this->option('prune') && !$this->dryRun) { ApiEndpointModel::whereIn('id', array_column($stale, 'id'))->update([ 'status' => 0, 'updated_at' => time(), ]); $this->warn('以上接口已停用(未删除,避免历史操作日志失去关联)'); } else { $this->warn('加 --prune 可把它们停用'); } } if (!$this->dryRun) { PermissionService::getInstance()->clear(); $this->info('已清空角色权限缓存'); } return self::SUCCESS; } /** * 只收 /api/ 下的业务接口;带路径参数的路由不进注册表(授权与日志都按固定路径匹配) */ private function normalizeUri(string $uri): ?string { $uri = trim($uri, '/'); if (!str_starts_with($uri, 'api/')) { return null; } $uri = trim(substr($uri, 4), '/'); if ($uri === '' || str_contains($uri, '{')) { return null; } return $uri; } /** * GET=1 POST=2 其他=0,与 ApiOpLogMiddleware 的映射保持一致 */ private function mapMethod(RoutingRoute $route): int { $methods = array_diff($route->methods(), ['HEAD']); if ($methods === ['GET']) { return 1; } if ($methods === ['POST']) { return 2; } return 0; } private function resolveController(RoutingRoute $route): string { $action = $route->getAction('controller'); if (!is_string($action) || !str_contains($action, '@')) { return ''; } return class_basename(explode('@', $action)[0]); } /** * 取控制器方法 PHPDoc 的首行作为操作名,没有注释时回落成路径 */ private function resolveName(RoutingRoute $route, string $url): string { $summary = $this->docSummary($route); return $summary !== '' ? mb_substr($summary, 0, 64) : $url; } private function resolveDescription(RoutingRoute $route): string { $lines = $this->docLines($route); array_shift($lines); return mb_substr(trim(implode(' ', $lines)), 0, 255); } private function docSummary(RoutingRoute $route): string { return $this->docLines($route)[0] ?? ''; } /** * @return array 去掉 @tag 行后的注释正文 */ private function docLines(RoutingRoute $route): array { $action = $route->getAction('controller'); if (!is_string($action) || !str_contains($action, '@')) { return []; } [$class, $method] = explode('@', $action); try { $doc = (new ReflectionMethod($class, $method))->getDocComment(); } catch (\Throwable $e) { return []; } if (!is_string($doc)) { return []; } $lines = []; foreach (explode("\n", $doc) as $line) { $line = trim(ltrim(trim($line), '/*')); if ($line === '' || str_starts_with($line, '@')) { continue; } $lines[] = $line; } return $lines; } /** * @param array $seenKeys url#method 组合 * @return array */ private function findStale(array $seenKeys): array { $stale = []; $rows = ApiEndpointModel::where('deleted_at', 0)->where('status', 1)->get(['id', 'url', 'method', 'name']); foreach ($rows as $row) { if (!in_array($row->url . '#' . (int) $row->method, $seenKeys, true)) { $stale[] = (object) [ 'id' => (int) $row->id, 'url' => (string) $row->url, 'method' => (int) $row->method, 'name' => (string) $row->name, ]; } } return $stale; } }