Files
datahub/backend/app/Controller/api/v1/RouteGroupController.php
T
2026-03-18 09:13:53 +08:00

556 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Controller\Api\V1;
use App\Controller\AbstractController;
use App\Middleware\AuthMiddleware;
use App\Middleware\PermissionMiddleware;
use App\Model\Route;
use App\Model\RouteGroup;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Annotation\RequestMapping;
use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface;
#[OA\Tag(name: 'Route Groups', description: '路由组管理')]
#[Controller(prefix: "/api/v1/route-groups")]
class RouteGroupController extends AbstractController
{
/**
* 路由组列表
*
* 返回所有路由组,包含每组的路由数量
*/
#[OA\Get(
path: '/route-groups',
summary: '路由组列表',
description: '返回所有路由组,包含每组的路由数量',
security: [['bearerAuth' => []]],
tags: ['Route Groups'],
responses: [
new OA\Response(
response: 200,
description: '获取成功',
content: new OA\JsonContent(properties: [
new OA\Property(property: 'code', type: 'integer', example: 0),
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
new OA\Property(property: 'data', type: 'array', items: new OA\Items(properties: [
new OA\Property(property: 'id', type: 'integer', example: 1),
new OA\Property(property: 'name', type: 'string', example: 'user-management'),
new OA\Property(property: 'label', type: 'string', nullable: true, example: '用户管理'),
new OA\Property(property: 'description', type: 'string', nullable: true, example: '用户相关路由'),
new OA\Property(property: 'sort_order', type: 'integer', example: 0),
new OA\Property(property: 'routes_count', type: 'integer', example: 5),
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
])),
])
),
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
]
)]
#[RequestMapping(path: "", methods: "GET")]
#[Middleware(AuthMiddleware::class)]
#[Middleware(PermissionMiddleware::class)]
public function index(): array
{
$groups = RouteGroup::query()
->withCount('routes')
->orderBy('sort_order')
->orderBy('id')
->get();
return [
'code' => 0,
'message' => '获取成功',
'data' => $groups,
];
}
/**
* 创建路由组
*/
#[OA\Post(
path: '/route-groups',
summary: '创建路由组',
security: [['bearerAuth' => []]],
tags: ['Route Groups'],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['name'],
properties: [
new OA\Property(property: 'name', type: 'string', maxLength: 100, example: 'user-management'),
new OA\Property(property: 'label', type: 'string', maxLength: 200, nullable: true, example: '用户管理'),
new OA\Property(property: 'description', type: 'string', nullable: true, example: '用户相关路由'),
new OA\Property(property: 'sort_order', type: 'integer', default: 0, example: 0),
]
)
),
responses: [
new OA\Response(
response: 200,
description: '创建成功',
content: new OA\JsonContent(properties: [
new OA\Property(property: 'code', type: 'integer', example: 0),
new OA\Property(property: 'message', type: 'string', example: '创建成功'),
new OA\Property(property: 'data', properties: [
new OA\Property(property: 'id', type: 'integer', example: 1),
new OA\Property(property: 'name', type: 'string', example: 'user-management'),
new OA\Property(property: 'label', type: 'string', nullable: true, example: '用户管理'),
new OA\Property(property: 'description', type: 'string', nullable: true),
new OA\Property(property: 'sort_order', type: 'integer', example: 0),
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
], type: 'object'),
])
),
new OA\Response(response: 400, description: '参数校验失败或名称重复', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
]
)]
#[RequestMapping(path: "", methods: "POST")]
#[Middleware(AuthMiddleware::class)]
#[Middleware(PermissionMiddleware::class)]
public function store(): ResponseInterface|array
{
$name = $this->request->input('name');
$label = $this->request->input('label');
$description = $this->request->input('description');
$sort_order = $this->request->input('sort_order');
// 校验 name
if (!is_string($name) || trim($name) === '') {
return $this->response->json([
'code' => 400,
'message' => 'name 不能为空',
])->withStatus(400);
}
$name = trim($name);
if (strlen($name) > 100) {
return $this->response->json([
'code' => 400,
'message' => 'name 长度不能超过 100 个字符',
])->withStatus(400);
}
if (RouteGroup::query()->where('name', $name)->exists()) {
return $this->response->json([
'code' => 400,
'message' => '路由组名称已存在',
])->withStatus(400);
}
// 校验 label
if ($label !== null && $label !== '') {
if (!is_string($label)) {
return $this->response->json([
'code' => 400,
'message' => 'label 必须为字符串',
])->withStatus(400);
}
$label = trim($label);
if (strlen($label) > 200) {
return $this->response->json([
'code' => 400,
'message' => 'label 长度不能超过 200 个字符',
])->withStatus(400);
}
} else {
$label = null;
}
// 校验 sort_order
if ($sort_order !== null && $sort_order !== '') {
if (!is_numeric($sort_order)) {
return $this->response->json([
'code' => 400,
'message' => 'sort_order 必须为整数',
])->withStatus(400);
}
$sort_order = (int) $sort_order;
} else {
$sort_order = 0;
}
$group = RouteGroup::query()->create([
'name' => $name,
'label' => $label,
'description' => is_string($description) ? trim($description) : null,
'sort_order' => $sort_order,
]);
return [
'code' => 0,
'message' => '创建成功',
'data' => $group,
];
}
/**
* 更新路由组
*
* @param int $id 路由组 ID
*/
#[OA\Put(
path: '/route-groups/{id}',
summary: '更新路由组',
security: [['bearerAuth' => []]],
tags: ['Route Groups'],
parameters: [
new OA\Parameter(name: 'id', in: 'path', required: true, description: '路由组 ID', schema: new OA\Schema(type: 'integer')),
],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(properties: [
new OA\Property(property: 'name', type: 'string', maxLength: 100, example: 'user-management'),
new OA\Property(property: 'label', type: 'string', maxLength: 200, nullable: true, example: '用户管理'),
new OA\Property(property: 'description', type: 'string', nullable: true, example: '用户相关路由'),
new OA\Property(property: 'sort_order', type: 'integer', example: 0),
])
),
responses: [
new OA\Response(
response: 200,
description: '更新成功',
content: new OA\JsonContent(properties: [
new OA\Property(property: 'code', type: 'integer', example: 0),
new OA\Property(property: 'message', type: 'string', example: '更新成功'),
new OA\Property(property: 'data', properties: [
new OA\Property(property: 'id', type: 'integer', example: 1),
new OA\Property(property: 'name', type: 'string', example: 'user-management'),
new OA\Property(property: 'label', type: 'string', nullable: true, example: '用户管理'),
new OA\Property(property: 'description', type: 'string', nullable: true),
new OA\Property(property: 'sort_order', type: 'integer', example: 0),
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
], type: 'object'),
])
),
new OA\Response(response: 400, description: '参数校验失败或名称重复', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
new OA\Response(response: 404, description: '路由组不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
]
)]
#[RequestMapping(path: "{id}", methods: "PUT")]
#[Middleware(AuthMiddleware::class)]
#[Middleware(PermissionMiddleware::class)]
public function update(int $id): ResponseInterface|array
{
$group = RouteGroup::query()->find($id);
if (!$group) {
return $this->response->json([
'code' => 404,
'message' => '路由组不存在',
])->withStatus(404);
}
$name = $this->request->input('name');
$label = $this->request->input('label');
$description = $this->request->input('description');
$sort_order = $this->request->input('sort_order');
$updates = [];
if ($name !== null) {
if (!is_string($name) || trim($name) === '') {
return $this->response->json([
'code' => 400,
'message' => 'name 不能为空',
])->withStatus(400);
}
$name = trim($name);
if (strlen($name) > 100) {
return $this->response->json([
'code' => 400,
'message' => 'name 长度不能超过 100 个字符',
])->withStatus(400);
}
if (RouteGroup::query()->where('name', $name)->where('id', '!=', $group->id)->exists()) {
return $this->response->json([
'code' => 400,
'message' => '路由组名称已存在',
])->withStatus(400);
}
$updates['name'] = $name;
}
if ($label !== null) {
if (!is_string($label)) {
return $this->response->json([
'code' => 400,
'message' => 'label 必须为字符串',
])->withStatus(400);
}
$updates['label'] = trim($label);
}
if ($description !== null) {
$updates['description'] = is_string($description) ? trim($description) : null;
}
if ($sort_order !== null) {
if (!is_numeric($sort_order)) {
return $this->response->json([
'code' => 400,
'message' => 'sort_order 必须为整数',
])->withStatus(400);
}
$updates['sort_order'] = (int) $sort_order;
}
if ($updates === []) {
return $this->response->json([
'code' => 400,
'message' => '缺少可更新字段',
])->withStatus(400);
}
$group->fill($updates);
$group->save();
$group->refresh();
return [
'code' => 0,
'message' => '更新成功',
'data' => $group,
];
}
/**
* 删除路由组
*
* 组内路由 group_id 自动设为 NULLON DELETE SET NULL
* role_route_groups 关联自动级联删除(ON DELETE CASCADE
*
* @param int $id 路由组 ID
*/
#[OA\Delete(
path: '/route-groups/{id}',
summary: '删除路由组',
description: '删除路由组,组内路由 group_id 自动设为 NULLrole_route_groups 关联自动级联删除',
security: [['bearerAuth' => []]],
tags: ['Route Groups'],
parameters: [
new OA\Parameter(name: 'id', in: 'path', required: true, description: '路由组 ID', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: '删除成功',
content: new OA\JsonContent(properties: [
new OA\Property(property: 'code', type: 'integer', example: 0),
new OA\Property(property: 'message', type: 'string', example: '删除成功'),
])
),
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
new OA\Response(response: 404, description: '路由组不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
]
)]
#[RequestMapping(path: "{id}", methods: "DELETE")]
#[Middleware(AuthMiddleware::class)]
#[Middleware(PermissionMiddleware::class)]
public function destroy(int $id): ResponseInterface|array
{
$group = RouteGroup::query()->find($id);
if (!$group) {
return $this->response->json([
'code' => 404,
'message' => '路由组不存在',
])->withStatus(404);
}
$group->delete();
return [
'code' => 0,
'message' => '删除成功',
];
}
/**
* 全部路由列表
*
* 含分组信息,支持按 group_id 筛选(传 0 或 "ungrouped" 筛选未分组路由)
*/
#[OA\Get(
path: '/routes',
summary: '路由列表',
description: '获取全部路由列表,含分组信息,支持按 group_id、method、path 筛选',
security: [['bearerAuth' => []]],
tags: ['Route Groups'],
parameters: [
new OA\Parameter(name: 'group_id', in: 'query', required: false, description: '路由组 ID,传 0 或 "ungrouped" 筛选未分组路由', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'method', in: 'query', required: false, description: 'HTTP 方法筛选', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'path', in: 'query', required: false, description: '路径模糊搜索', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: '获取成功',
content: new OA\JsonContent(properties: [
new OA\Property(property: 'code', type: 'integer', example: 0),
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
new OA\Property(property: 'data', type: 'array', items: new OA\Items(properties: [
new OA\Property(property: 'id', type: 'integer', example: 1),
new OA\Property(property: 'method', type: 'string', example: 'GET'),
new OA\Property(property: 'path', type: 'string', example: '/api/v1/users'),
new OA\Property(property: 'group_id', type: 'integer', nullable: true, example: 1),
new OA\Property(property: 'group', nullable: true, properties: [
new OA\Property(property: 'id', type: 'integer', example: 1),
new OA\Property(property: 'name', type: 'string', example: 'user-management'),
new OA\Property(property: 'label', type: 'string', nullable: true, example: '用户管理'),
], type: 'object'),
])),
])
),
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
]
)]
#[RequestMapping(path: "/api/v1/routes", methods: "GET")]
#[Middleware(AuthMiddleware::class)]
#[Middleware(PermissionMiddleware::class)]
public function routes(): array
{
$query = Route::query()->with('group');
// 按 group_id 筛选
$group_id = $this->request->input('group_id');
if ($group_id !== null && $group_id !== '') {
if ($group_id === '0' || $group_id === 'ungrouped') {
// 未分组路由
$query->whereNull('group_id');
} else {
$query->where('group_id', (int) $group_id);
}
}
// 按 method 筛选
$method = $this->request->input('method');
if ($method !== null && $method !== '') {
$query->where('method', strtoupper($method));
}
// 按 path 模糊搜索
$path = $this->request->input('path');
if ($path !== null && $path !== '') {
$query->where('path', 'like', '%' . $path . '%');
}
$routes = $query->orderBy('path')->orderBy('method')->get();
return [
'code' => 0,
'message' => '获取成功',
'data' => $routes,
];
}
/**
* 将路由分配到路由组
*
* 传 group_id=null 表示从分组中移出
*
* @param int $id 路由 ID
*/
#[OA\Put(
path: '/routes/{id}/group',
summary: '分配路由到路由组',
description: '将路由分配到指定路由组,传 group_id=null 表示从分组中移出',
security: [['bearerAuth' => []]],
tags: ['Route Groups'],
parameters: [
new OA\Parameter(name: 'id', in: 'path', required: true, description: '路由 ID', schema: new OA\Schema(type: 'integer')),
],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['group_id'],
properties: [
new OA\Property(property: 'group_id', type: 'integer', nullable: true, description: '路由组 ID,传 null 移出分组', example: 1),
]
)
),
responses: [
new OA\Response(
response: 200,
description: '分配成功',
content: new OA\JsonContent(properties: [
new OA\Property(property: 'code', type: 'integer', example: 0),
new OA\Property(property: 'message', type: 'string', example: '分配成功'),
new OA\Property(property: 'data', properties: [
new OA\Property(property: 'id', type: 'integer', example: 1),
new OA\Property(property: 'method', type: 'string', example: 'GET'),
new OA\Property(property: 'path', type: 'string', example: '/api/v1/users'),
new OA\Property(property: 'group_id', type: 'integer', nullable: true, example: 1),
new OA\Property(property: 'group', nullable: true, properties: [
new OA\Property(property: 'id', type: 'integer', example: 1),
new OA\Property(property: 'name', type: 'string', example: 'user-management'),
new OA\Property(property: 'label', type: 'string', nullable: true, example: '用户管理'),
], type: 'object'),
], type: 'object'),
])
),
new OA\Response(response: 400, description: '参数校验失败', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
new OA\Response(response: 404, description: '路由或目标路由组不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
]
)]
#[RequestMapping(path: "/api/v1/routes/{id}/group", methods: "PUT")]
#[Middleware(AuthMiddleware::class)]
#[Middleware(PermissionMiddleware::class)]
public function assignRouteGroup(int $id): ResponseInterface|array
{
$route = Route::query()->find($id);
if (!$route) {
return $this->response->json([
'code' => 404,
'message' => '路由不存在',
])->withStatus(404);
}
// group_id 可以为 null(移出分组)或整数(分配到指定分组)
$body = $this->request->getParsedBody();
if (!array_key_exists('group_id', $body)) {
return $this->response->json([
'code' => 400,
'message' => '缺少 group_id 参数',
])->withStatus(400);
}
$group_id = $body['group_id'];
if ($group_id !== null) {
if (!is_numeric($group_id)) {
return $this->response->json([
'code' => 400,
'message' => 'group_id 必须为整数或 null',
])->withStatus(400);
}
$group_id = (int) $group_id;
// 校验目标路由组存在
if (!RouteGroup::query()->where('id', $group_id)->exists()) {
return $this->response->json([
'code' => 404,
'message' => '目标路由组不存在',
])->withStatus(404);
}
}
$route->group_id = $group_id;
$route->save();
$route->load('group');
return [
'code' => 0,
'message' => $group_id === null ? '已从分组中移出' : '分配成功',
'data' => $route,
];
}
}