初始化

缺陷:主题配色需要优化整体的同风格
This commit is contained in:
2026-08-14 23:21:21 +08:00
parent 6e2769c528
commit bcf54c2727
152 changed files with 13521 additions and 141 deletions

View File

@@ -0,0 +1,238 @@
<?php
namespace App\Console\Commands;
use App\Models\ApiEndpointModel;
use App\Service\PermissionService;
use Illuminate\Console\Command;
use Illuminate\Routing\Route as RoutingRoute;
use Illuminate\Support\Facades\Route;
use ReflectionMethod;
/**
* 把已注册的路由同步进 nl_api_endpoint
*
* 这张表同时是操作日志的匹配依据和接口级权限的授权清单,手工维护必然漏。
* 路由本身由 autoRouteRegister 反射生成,这里再反射一次路由表,
* 顺便把控制器方法的 PHPDoc 首行当作操作名,新增模块不用再补 SQL。
*/
class EndpointSyncCommand extends Command
{
protected $signature = 'lgp:endpoint-sync
{--dry-run : 只预演不写库}
{--prune : 把路由表里已不存在的接口标记为停用}';
protected $description = '同步路由到接口注册表 nl_api_endpoint幂等';
private bool $dryRun = false;
public function handle(): int
{
$this->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<int, string> 去掉 @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<int, string> $seenKeys url#method 组合
* @return array<int, object>
*/
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;
}
}