[]]], 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'), new OA\Property(property: 'users_count', type: 'integer', example: 5), ], 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() ->withCount('users') ->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)] public function assignRole(int $id, AuthManager $auth): ResponseInterface|array { $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 降级自己 $current_user = $auth->guard('jwt')->user(); if ($current_user instanceof User && $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'); OperationLogService::log( user_id: OperationLogService::getCurrentUserId(), action: 'role.update', target_type: 'user', target_id: $id, description: "用户 #{$id} 角色变更为 {$new_role->name}", detail: ['role_id' => $role_id, 'role_name' => $new_role->name], ip: RequestHelper::getClientIp($this->request), ); 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); // 校验 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); } } // 使用 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, ]; } }