checkpoint: 权限即时生效——角色/用户变更自动踢下线 + roleless 用户自读修复

- online/service: kick_user_sessions 扫描 USER_SESSION 匹配目标用户,删三键使登录时固化的权限快照即时失效
- role service+controller: 更新/删除/改状态/改权限后踢下线绑定该角色的用户
- user service+controller: 停用/改密等变更后踢下线(对应用户即时 401 重登)
- permission: 无角色用户读自身记录时按 id 匹配放行(修复 409 真 bug)
This commit is contained in:
34047007@qq.com
2026-08-07 21:47:51 +08:00
parent c1fa752d9b
commit 20b7184b44
6 changed files with 132 additions and 20 deletions
@@ -4,6 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
from app.core.exceptions import CustomException
from app.core.logger import logger
from app.utils.common_util import search_to_dict
from app.utils.excel_util import ExcelUtil
@@ -110,12 +111,13 @@ class RoleService:
new_role = await RoleCRUD(self.auth, self.db).create(data=data)
return await self.detail(id=new_role.id)
async def update(self, id: int, data: RoleUpdateSchema) -> RoleOutSchema:
async def update(self, id: int, data: RoleUpdateSchema, redis: Any | None = None) -> RoleOutSchema:
"""更新角色
参数:
- id (int): 角色ID
- data (RoleUpdateSchema): 更新角色模型
- redis (Any | None): Redis 客户端(非 None 时踢下线绑定该角色的用户)
返回:
- RoleOutSchema: 更新后的角色响应模型
@@ -128,13 +130,16 @@ class RoleService:
if exist_code and exist_code.id != id:
raise CustomException(msg="更新失败,角色编码已存在")
await RoleCRUD(self.auth, self.db).update(id=id, data=data)
if redis is not None:
await self._kick_users(redis, [id])
return await self.detail(id=id)
async def delete(self, ids: list[int]) -> None:
async def delete(self, ids: list[int], redis: Any | None = None) -> None:
"""删除角色
参数:
- ids (list[int]): 角色ID列表
- redis (Any | None): Redis 客户端(非 None 时踢下线绑定该角色的用户)
返回:
- None
@@ -148,12 +153,15 @@ class RoleService:
raise CustomException(msg="删除失败,部分ID不存在")
await RoleCRUD(self.auth, self.db).delete(ids=ids)
if redis is not None:
await self._kick_users(redis, ids)
async def set_permission(self, data: RolePermissionSettingSchema) -> None:
async def set_permission(self, data: RolePermissionSettingSchema, redis: Any | None = None) -> None:
"""设置角色权限
参数:
- data (RolePermissionSettingSchema): 角色权限设置模型
- redis (Any | None): Redis 客户端(非 None 时踢下线绑定角色的用户)
返回:
- None
@@ -164,12 +172,15 @@ class RoleService:
# 设置数据权限范围(自定义部门关联已废弃,直接清空)
await RoleCRUD(self.auth, self.db).set(ids=data.role_ids, data_scope=data.data_scope)
await RoleCRUD(self.auth, self.db).set_role_depts_crud(role_ids=data.role_ids, dept_ids=[])
if redis is not None:
await self._kick_users(redis, data.role_ids)
async def set_available(self, data: BatchSetAvailable) -> None:
async def set_available(self, data: BatchSetAvailable, redis: Any | None = None) -> None:
"""设置角色可用状态
参数:
- data (BatchSetAvailable): 批量设置可用状态模型
- redis (Any | None): Redis 客户端(非 None 时踢下线绑定该角色的用户)
返回:
- None
@@ -180,6 +191,23 @@ class RoleService:
if rid not in role_map:
raise CustomException(msg="该数据不存在")
await RoleCRUD(self.auth, self.db).set(ids=data.ids, status=data.status)
if redis is not None:
await self._kick_users(redis, data.ids)
async def _kick_users(self, redis: Any, role_ids: list[int]) -> int:
"""踢下线绑定指定角色的全部用户的会话(角色权限/状态/删除后调用)。"""
roles = await RoleCRUD(self.auth, self.db).get_list(search={"id": ("in", role_ids)}, preload=["users"])
user_ids = sorted({u.id for r in roles for u in r.users})
if not user_ids:
return 0
from app.api.v1.module_monitor.online.service import OnlineService
try:
return await OnlineService.kick_user_sessions(redis=redis, user_ids=user_ids)
except Exception as e:
logger.error(f"角色变更自动踢下线失败: role_ids={role_ids}, err={e!s}")
return 0
@staticmethod
def export_list(role_list: list[dict[str, Any]]) -> bytes: