2026-03-13 09:07:42 +08:00
|
|
|
<?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\Role;
|
2026-03-17 15:54:53 +08:00
|
|
|
use App\Service\OperationLogService;
|
2026-03-18 08:49:10 +08:00
|
|
|
use App\Utils\RequestHelper;
|
2026-03-13 09:07:42 +08:00
|
|
|
use App\Model\RoleRouteOverride;
|
|
|
|
|
use App\Model\Route;
|
2026-03-13 09:15:52 +08:00
|
|
|
use App\Model\RouteGroup;
|
2026-03-13 09:07:42 +08:00
|
|
|
use App\Model\User;
|
|
|
|
|
use App\Service\ScopeTableManager;
|
|
|
|
|
use Hyperf\DbConnection\Db;
|
|
|
|
|
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;
|
2026-04-16 14:38:40 +08:00
|
|
|
|
2026-03-13 09:07:42 +08:00
|
|
|
|
|
|
|
|
#[OA\Tag(name: 'Roles', description: '角色与授权管理')]
|
|
|
|
|
#[Controller(prefix: "/api/v1/roles")]
|
|
|
|
|
class RoleController extends AbstractController
|
|
|
|
|
{
|
|
|
|
|
public function __construct(
|
|
|
|
|
protected readonly ScopeTableManager $scopeTableManager,
|
|
|
|
|
) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 角色列表
|
|
|
|
|
*/
|
|
|
|
|
#[OA\Get(
|
|
|
|
|
path: '/roles',
|
|
|
|
|
summary: '角色列表',
|
2026-03-30 10:12:24 +08:00
|
|
|
description: '获取所有角色,包含每个角色的用户数和路由组数',
|
2026-03-13 09:07:42 +08:00
|
|
|
security: [['bearerAuth' => []]],
|
|
|
|
|
tags: ['Roles'],
|
|
|
|
|
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: 'editor'),
|
2026-03-30 10:12:24 +08:00
|
|
|
new OA\Property(property: 'label', type: 'string', example: '编辑者'),
|
|
|
|
|
new OA\Property(property: 'description', type: 'string', example: '可以编辑内容'),
|
2026-03-13 09:07:42 +08:00
|
|
|
new OA\Property(property: 'users_count', type: 'integer', example: 5),
|
2026-03-30 10:12:24 +08:00
|
|
|
new OA\Property(property: 'route_groups_count', type: 'integer', example: 3),
|
2026-03-13 09:07:42 +08:00
|
|
|
],
|
|
|
|
|
type: 'object'
|
|
|
|
|
)),
|
|
|
|
|
])
|
|
|
|
|
),
|
|
|
|
|
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
|
|
|
|
|
{
|
|
|
|
|
$roles = Role::query()
|
2026-03-30 10:12:24 +08:00
|
|
|
->withCount(['users', 'routeGroups'])
|
2026-03-13 09:07:42 +08:00
|
|
|
->orderBy('id')
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'code' => 0,
|
|
|
|
|
'message' => '获取成功',
|
|
|
|
|
'data' => $roles,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 分配用户角色
|
|
|
|
|
*
|
|
|
|
|
* administrator 不允许降级自己的角色(防止锁死)
|
|
|
|
|
* 分配角色后触发 bitmap 重建
|
|
|
|
|
*
|
|
|
|
|
* @param int $id 用户 ID
|
|
|
|
|
*/
|
|
|
|
|
#[OA\Put(
|
|
|
|
|
path: '/users/{id}/role',
|
|
|
|
|
summary: '分配用户角色',
|
|
|
|
|
description: '为指定用户分配角色,administrator 不允许降级自己的角色',
|
|
|
|
|
security: [['bearerAuth' => []]],
|
|
|
|
|
tags: ['Roles'],
|
|
|
|
|
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: ['role_id'],
|
|
|
|
|
properties: [
|
|
|
|
|
new OA\Property(property: 'role_id', type: 'integer', description: '角色 ID', example: 2),
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
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', ref: '#/components/schemas/User'),
|
|
|
|
|
])
|
|
|
|
|
),
|
|
|
|
|
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/users/{id}/role", methods: "PUT")]
|
|
|
|
|
#[Middleware(AuthMiddleware::class)]
|
|
|
|
|
#[Middleware(PermissionMiddleware::class)]
|
2026-04-16 14:38:40 +08:00
|
|
|
public function assignRole(int $id): ResponseInterface|array
|
2026-03-13 09:07:42 +08:00
|
|
|
{
|
|
|
|
|
$target_user = User::query()->with('role')->find($id);
|
|
|
|
|
|
|
|
|
|
if (!$target_user) {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 404,
|
|
|
|
|
'message' => '用户不存在',
|
|
|
|
|
])->withStatus(404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$body = $this->request->getParsedBody();
|
|
|
|
|
|
|
|
|
|
if (!isset($body['role_id'])) {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 400,
|
|
|
|
|
'message' => '缺少 role_id 参数',
|
|
|
|
|
])->withStatus(400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$role_id = (int) $body['role_id'];
|
|
|
|
|
$new_role = Role::query()->find($role_id);
|
|
|
|
|
|
|
|
|
|
if (!$new_role) {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 404,
|
|
|
|
|
'message' => '角色不存在',
|
|
|
|
|
])->withStatus(404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 防止 administrator 降级自己
|
2026-04-16 14:38:40 +08:00
|
|
|
$current_user = $this->getAuthUser();
|
|
|
|
|
if ($current_user
|
2026-03-13 09:07:42 +08:00
|
|
|
&& $current_user->id === $id
|
|
|
|
|
&& $target_user->isAdministrator()
|
|
|
|
|
&& $new_role->name !== 'administrator'
|
|
|
|
|
) {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 400,
|
|
|
|
|
'message' => '不允许降级自己的管理员角色',
|
|
|
|
|
])->withStatus(400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$target_user->role_id = $role_id;
|
|
|
|
|
$target_user->save();
|
|
|
|
|
|
|
|
|
|
// 角色变化影响 scope 计算,重建 bitmap
|
|
|
|
|
$this->scopeTableManager->rebuildUserScope($id);
|
|
|
|
|
|
|
|
|
|
$target_user->refresh();
|
|
|
|
|
$target_user->load('role');
|
|
|
|
|
|
2026-03-17 15:54:53 +08:00
|
|
|
OperationLogService::log(
|
2026-03-18 08:37:14 +08:00
|
|
|
user_id: OperationLogService::getCurrentUserId(),
|
2026-03-17 15:54:53 +08:00
|
|
|
action: 'role.update',
|
|
|
|
|
target_type: 'user',
|
|
|
|
|
target_id: $id,
|
|
|
|
|
description: "用户 #{$id} 角色变更为 {$new_role->name}",
|
|
|
|
|
detail: ['role_id' => $role_id, 'role_name' => $new_role->name],
|
2026-03-18 08:49:10 +08:00
|
|
|
ip: RequestHelper::getClientIp($this->request),
|
2026-03-17 15:54:53 +08:00
|
|
|
);
|
|
|
|
|
|
2026-03-13 09:07:42 +08:00
|
|
|
return [
|
|
|
|
|
'code' => 0,
|
|
|
|
|
'message' => '角色分配成功',
|
|
|
|
|
'data' => $target_user,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 查看角色已授权的路由组
|
|
|
|
|
*
|
|
|
|
|
* @param int $id 角色 ID
|
|
|
|
|
*/
|
|
|
|
|
#[OA\Get(
|
|
|
|
|
path: '/roles/{id}/route-groups',
|
|
|
|
|
summary: '查看角色已授权的路由组',
|
|
|
|
|
description: '获取指定角色已授权的路由组列表',
|
|
|
|
|
security: [['bearerAuth' => []]],
|
|
|
|
|
tags: ['Roles'],
|
|
|
|
|
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\Property(property: 'data', type: 'array', items: new OA\Items(type: 'object')),
|
|
|
|
|
])
|
|
|
|
|
),
|
|
|
|
|
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}/route-groups", methods: "GET")]
|
|
|
|
|
#[Middleware(AuthMiddleware::class)]
|
|
|
|
|
#[Middleware(PermissionMiddleware::class)]
|
|
|
|
|
public function getRouteGroups(int $id): ResponseInterface|array
|
|
|
|
|
{
|
|
|
|
|
$role = Role::query()->find($id);
|
|
|
|
|
|
|
|
|
|
if (!$role) {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 404,
|
|
|
|
|
'message' => '角色不存在',
|
|
|
|
|
])->withStatus(404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$groups = $role->routeGroups()->withCount('routes')->orderBy('sort_order')->get();
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'code' => 0,
|
|
|
|
|
'message' => '获取成功',
|
|
|
|
|
'data' => $groups,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 设置角色的路由组授权(全量替换)
|
|
|
|
|
*
|
|
|
|
|
* 请求体:{ "group_ids": [1, 3, 5] }
|
|
|
|
|
*
|
|
|
|
|
* @param int $id 角色 ID
|
|
|
|
|
*/
|
|
|
|
|
#[OA\Put(
|
|
|
|
|
path: '/roles/{id}/route-groups',
|
|
|
|
|
summary: '设置角色的路由组授权',
|
|
|
|
|
description: '全量替换指定角色的路由组授权,administrator 角色不允许修改',
|
|
|
|
|
security: [['bearerAuth' => []]],
|
|
|
|
|
tags: ['Roles'],
|
|
|
|
|
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_ids'],
|
|
|
|
|
properties: [
|
|
|
|
|
new OA\Property(property: 'group_ids', type: 'array', items: new OA\Items(type: 'integer'), example: [1, 3, 5]),
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
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(type: 'object')),
|
|
|
|
|
])
|
|
|
|
|
),
|
|
|
|
|
new OA\Response(response: 400, description: 'administrator 角色不允许修改或参数错误', 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}/route-groups", methods: "PUT")]
|
|
|
|
|
#[Middleware(AuthMiddleware::class)]
|
|
|
|
|
#[Middleware(PermissionMiddleware::class)]
|
|
|
|
|
public function setRouteGroups(int $id): ResponseInterface|array
|
|
|
|
|
{
|
|
|
|
|
$role = Role::query()->find($id);
|
|
|
|
|
|
|
|
|
|
if (!$role) {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 404,
|
|
|
|
|
'message' => '角色不存在',
|
|
|
|
|
])->withStatus(404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 不允许修改 administrator 角色的路由授权(administrator 拥有全部权限)
|
|
|
|
|
if ($role->name === 'administrator') {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 400,
|
|
|
|
|
'message' => 'administrator 角色拥有全部权限,无需设置路由组授权',
|
|
|
|
|
])->withStatus(400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$body = $this->request->getParsedBody();
|
|
|
|
|
|
|
|
|
|
if (!array_key_exists('group_ids', $body)) {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 400,
|
|
|
|
|
'message' => '缺少 group_ids 参数',
|
|
|
|
|
])->withStatus(400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$group_ids = $body['group_ids'];
|
|
|
|
|
|
|
|
|
|
if (!is_array($group_ids)) {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 400,
|
|
|
|
|
'message' => 'group_ids 必须为数组',
|
|
|
|
|
])->withStatus(400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 过滤并校验 group_ids
|
|
|
|
|
$group_ids = array_map('intval', $group_ids);
|
|
|
|
|
$group_ids = array_unique($group_ids);
|
|
|
|
|
|
2026-03-13 09:15:52 +08:00
|
|
|
// 校验 group_ids 都存在
|
|
|
|
|
if (!empty($group_ids)) {
|
|
|
|
|
$existing_count = RouteGroup::query()->whereIn('id', $group_ids)->count();
|
|
|
|
|
if ($existing_count !== count($group_ids)) {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 400,
|
|
|
|
|
'message' => '包含不存在的 group_id',
|
|
|
|
|
])->withStatus(400);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 09:07:42 +08:00
|
|
|
// 使用 sync 全量替换
|
|
|
|
|
$role->routeGroups()->sync($group_ids);
|
|
|
|
|
|
|
|
|
|
// 返回更新后的路由组列表
|
|
|
|
|
$groups = $role->routeGroups()->withCount('routes')->orderBy('sort_order')->get();
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'code' => 0,
|
|
|
|
|
'message' => '路由组授权更新成功',
|
|
|
|
|
'data' => $groups,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 查看角色的单条路由覆盖
|
|
|
|
|
*
|
|
|
|
|
* @param int $id 角色 ID
|
|
|
|
|
*/
|
|
|
|
|
#[OA\Get(
|
|
|
|
|
path: '/roles/{id}/route-overrides',
|
|
|
|
|
summary: '查看角色的路由覆盖',
|
|
|
|
|
description: '获取指定角色的路由覆盖列表,包含路由详情',
|
|
|
|
|
security: [['bearerAuth' => []]],
|
|
|
|
|
tags: ['Roles'],
|
|
|
|
|
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\Property(property: 'data', type: 'array', items: new OA\Items(
|
|
|
|
|
properties: [
|
|
|
|
|
new OA\Property(property: 'id', type: 'integer'),
|
|
|
|
|
new OA\Property(property: 'role_id', type: 'integer'),
|
|
|
|
|
new OA\Property(property: 'route_id', type: 'integer'),
|
|
|
|
|
new OA\Property(property: 'allowed', type: 'boolean'),
|
|
|
|
|
new OA\Property(property: 'route', type: 'object'),
|
|
|
|
|
],
|
|
|
|
|
type: 'object'
|
|
|
|
|
)),
|
|
|
|
|
])
|
|
|
|
|
),
|
|
|
|
|
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}/route-overrides", methods: "GET")]
|
|
|
|
|
#[Middleware(AuthMiddleware::class)]
|
|
|
|
|
#[Middleware(PermissionMiddleware::class)]
|
|
|
|
|
public function getRouteOverrides(int $id): ResponseInterface|array
|
|
|
|
|
{
|
|
|
|
|
$role = Role::query()->find($id);
|
|
|
|
|
|
|
|
|
|
if (!$role) {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 404,
|
|
|
|
|
'message' => '角色不存在',
|
|
|
|
|
])->withStatus(404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$overrides = RoleRouteOverride::query()
|
|
|
|
|
->where('role_id', $id)
|
|
|
|
|
->with('route')
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'code' => 0,
|
|
|
|
|
'message' => '获取成功',
|
|
|
|
|
'data' => $overrides,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 设置角色的单条路由覆盖(全量替换)
|
|
|
|
|
*
|
|
|
|
|
* 请求体:{ "overrides": [{ "route_id": 12, "allowed": false }, ...] }
|
|
|
|
|
*
|
|
|
|
|
* @param int $id 角色 ID
|
|
|
|
|
*/
|
|
|
|
|
#[OA\Put(
|
|
|
|
|
path: '/roles/{id}/route-overrides',
|
|
|
|
|
summary: '设置角色的路由覆盖',
|
|
|
|
|
description: '全量替换指定角色的路由覆盖规则,administrator 角色不允许修改',
|
|
|
|
|
security: [['bearerAuth' => []]],
|
|
|
|
|
tags: ['Roles'],
|
|
|
|
|
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: ['overrides'],
|
|
|
|
|
properties: [
|
|
|
|
|
new OA\Property(
|
|
|
|
|
property: 'overrides',
|
|
|
|
|
type: 'array',
|
|
|
|
|
items: new OA\Items(
|
|
|
|
|
required: ['route_id', 'allowed'],
|
|
|
|
|
properties: [
|
|
|
|
|
new OA\Property(property: 'route_id', type: 'integer', description: '路由 ID', example: 12),
|
|
|
|
|
new OA\Property(property: 'allowed', type: 'boolean', description: '是否允许', example: false),
|
|
|
|
|
],
|
|
|
|
|
type: 'object'
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
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'),
|
|
|
|
|
new OA\Property(property: 'role_id', type: 'integer'),
|
|
|
|
|
new OA\Property(property: 'route_id', type: 'integer'),
|
|
|
|
|
new OA\Property(property: 'allowed', type: 'boolean'),
|
|
|
|
|
new OA\Property(property: 'route', type: 'object'),
|
|
|
|
|
],
|
|
|
|
|
type: 'object'
|
|
|
|
|
)),
|
|
|
|
|
])
|
|
|
|
|
),
|
|
|
|
|
new OA\Response(response: 400, description: 'route_id 重复、不存在或 administrator 角色不允许修改', 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}/route-overrides", methods: "PUT")]
|
|
|
|
|
#[Middleware(AuthMiddleware::class)]
|
|
|
|
|
#[Middleware(PermissionMiddleware::class)]
|
|
|
|
|
public function setRouteOverrides(int $id): ResponseInterface|array
|
|
|
|
|
{
|
|
|
|
|
$role = Role::query()->find($id);
|
|
|
|
|
|
|
|
|
|
if (!$role) {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 404,
|
|
|
|
|
'message' => '角色不存在',
|
|
|
|
|
])->withStatus(404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 不允许修改 administrator 角色的路由覆盖
|
|
|
|
|
if ($role->name === 'administrator') {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 400,
|
|
|
|
|
'message' => 'administrator 角色拥有全部权限,无需设置路由覆盖',
|
|
|
|
|
])->withStatus(400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$body = $this->request->getParsedBody();
|
|
|
|
|
|
|
|
|
|
if (!array_key_exists('overrides', $body)) {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 400,
|
|
|
|
|
'message' => '缺少 overrides 参数',
|
|
|
|
|
])->withStatus(400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$overrides = $body['overrides'];
|
|
|
|
|
|
|
|
|
|
if (!is_array($overrides)) {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 400,
|
|
|
|
|
'message' => 'overrides 必须为数组',
|
|
|
|
|
])->withStatus(400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 校验每条 override 格式
|
|
|
|
|
$records = [];
|
|
|
|
|
$seen_route_ids = [];
|
|
|
|
|
foreach ($overrides as $item) {
|
|
|
|
|
if (!is_array($item) || !isset($item['route_id']) || !isset($item['allowed'])) {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 400,
|
|
|
|
|
'message' => '每条覆盖规则必须包含 route_id 和 allowed 字段',
|
|
|
|
|
])->withStatus(400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$route_id = (int) $item['route_id'];
|
|
|
|
|
|
|
|
|
|
// 检查重复
|
|
|
|
|
if (in_array($route_id, $seen_route_ids, true)) {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 400,
|
|
|
|
|
'message' => "route_id {$route_id} 重复",
|
|
|
|
|
])->withStatus(400);
|
|
|
|
|
}
|
|
|
|
|
$seen_route_ids[] = $route_id;
|
|
|
|
|
|
|
|
|
|
$records[] = [
|
|
|
|
|
'role_id' => $id,
|
|
|
|
|
'route_id' => $route_id,
|
|
|
|
|
'allowed' => (bool) $item['allowed'],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 校验 route_ids 都存在
|
|
|
|
|
if (!empty($seen_route_ids)) {
|
|
|
|
|
$existing_count = Route::query()->whereIn('id', $seen_route_ids)->count();
|
|
|
|
|
if ($existing_count !== count($seen_route_ids)) {
|
|
|
|
|
return $this->response->json([
|
|
|
|
|
'code' => 400,
|
|
|
|
|
'message' => '包含不存在的 route_id',
|
|
|
|
|
])->withStatus(400);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 事务内全量替换
|
|
|
|
|
Db::beginTransaction();
|
|
|
|
|
try {
|
|
|
|
|
RoleRouteOverride::query()->where('role_id', $id)->delete();
|
|
|
|
|
|
|
|
|
|
if (!empty($records)) {
|
|
|
|
|
RoleRouteOverride::query()->insert($records);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Db::commit();
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
Db::rollBack();
|
|
|
|
|
throw $e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 返回更新后的覆盖列表
|
|
|
|
|
$result = RoleRouteOverride::query()
|
|
|
|
|
->where('role_id', $id)
|
|
|
|
|
->with('route')
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'code' => 0,
|
|
|
|
|
'message' => '路由覆盖更新成功',
|
|
|
|
|
'data' => $result,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|