feat(approle): 角色属性绑定加组维度,一行=用户×组×角色
build / build-push (push) Successful in 3m37s

- sys_user_app_role_rel 新增 group_id:组=绑定发生时所选组(路径不落库,按组ID实时拼)
- 角色用户列表按绑定行返回,新增部门列(groupPath 按查看者数据范围裁剪,学校管理员只看本校)
- 保存改为按组覆盖/增量添加:编辑弹窗按所在组覆盖,角色授权页增量添加不误删其他组绑定
- 调整组不带走角色:旧组绑定保留不生效,调回自动恢复
- collectEnabledRoleModuleIds 只算用户当前所在组上的启用角色(NULL 视为全局兜底)
- UserGroupTree/UserRoles 增加可选 groupId:所属部门按行收窄、角色按组回显
- 附迁移脚本 sys_user_app_role_rel_add_group_dimension.sql(回填主组 + 换唯一约束)
This commit is contained in:
chenxing
2026-09-16 16:33:29 +08:00
parent ce131bf399
commit 9ec65698b6
14 changed files with 354 additions and 74 deletions
@@ -108,16 +108,17 @@ public class AppRoleController {
} }
/** /**
* 查询角色已绑定的用户列表。 * 查询角色已绑定的用户列表(一行 = 一条「用户×组」绑定,含部门路径)。<br>
* 路径查询时按 groupId 实时拼接;非超管按数据范围过滤行并裁剪路径(学校管理员只看到本校路径)。
*/ */
@GetMapping("/RoleUsers") @GetMapping("/RoleUsers")
public List<AppRoleUserVo> roleUsers(@RequestParam Integer roleId) { public List<AppRoleUserVo> roleUsers(@RequestParam Integer roleId) {
accessScopeService.requireAdmin(); // S+B+C:NONE 已被拦截器挡住,双保险 accessScopeService.requireAdmin(); // S+B+C:NONE 已被拦截器挡住,双保险
return appRoleService.findRoleUsers(roleId); return appRoleService.findRoleUsers(roleId, accessScopeService.scopeGroupIds());
} }
/** /**
* 角色绑定用户保存(角色维度,全量覆盖)。 * 角色绑定用户保存(角色维度,增量添加「用户×组」绑定)。
*/ */
@OperationLog("保存角色用户绑定") @OperationLog("保存角色用户绑定")
@PostMapping("SaveRoleUsers") @PostMapping("SaveRoleUsers")
@@ -127,7 +128,7 @@ public class AppRoleController {
} }
/** /**
* 用户绑定角色保存(用户维度,全量覆盖)。 * 用户绑定角色保存(用户维度,按组覆盖:只覆盖该用户在指定组下的绑定)。
*/ */
@OperationLog("保存用户角色绑定") @OperationLog("保存用户角色绑定")
@PostMapping("SaveUserRoles") @PostMapping("SaveUserRoles")
@@ -137,13 +138,14 @@ public class AppRoleController {
} }
/** /**
* 移除单个用户与角色的绑定关系(精确删除单条记录,非全量重提)。<br> * 移除单条「用户×组×角色」绑定(精确删除一行,非全量重提)。<br>
* 前端「用户管理 - 删除」调用此接口。 * 前端「用户管理 - 删除」调用此接口。
*/ */
@OperationLog("移除角色用户绑定") @OperationLog("移除角色用户绑定")
@PostMapping("RemoveRoleUser") @PostMapping("RemoveRoleUser")
public void removeRoleUser(@RequestParam Integer roleId, @RequestParam Integer userId) { public void removeRoleUser(@RequestParam Integer roleId, @RequestParam Integer userId,
@RequestParam Integer groupId) {
accessScopeService.requireAdmin(); // S+B+C:NONE 已被拦截器挡住,双保险 accessScopeService.requireAdmin(); // S+B+C:NONE 已被拦截器挡住,双保险
appRoleService.removeRoleUser(roleId, userId); appRoleService.removeRoleUser(roleId, userId, groupId);
} }
} }
@@ -148,30 +148,35 @@ public class GroupAuthzController {
/** /**
* 权限一览:查询用户所属的业务角色列表(中间下方「所属角色」区域展示用)。<br> * 权限一览:查询用户所属的业务角色列表(中间下方「所属角色」区域展示用)。<br>
* 返回该用户绑定的全部角色(含禁用角色,disabled 字段标记),普通角色回填所属应用名称。 * 返回该用户绑定的全部角色(含禁用角色,disabled 字段标记),普通角色回填所属应用名称。<br>
* groupId 可选:传入时只返回该组下的绑定(编辑弹窗按所在组回显角色用);为空返回全部绑定聚合去重。
*/ */
@GetMapping("/UserRoles") @GetMapping("/UserRoles")
public List<UserRoleVo> userRoles(@RequestParam Integer userId) { public List<UserRoleVo> userRoles(@RequestParam Integer userId,
@RequestParam(required = false) Integer groupId) {
if (!accessScopeService.resolveCurrentScope().isSuper() if (!accessScopeService.resolveCurrentScope().isSuper()
&& !accessScopeService.userIntersectsScope(userId)) { && !accessScopeService.userIntersectsScope(userId)) {
return Collections.emptyList(); return Collections.emptyList();
} }
return groupAuthzService.findUserRoles(userId); return groupAuthzService.findUserRoles(userId, groupId);
} }
/** /**
* 权限一览:查询用户所属部门树(左侧上方「所属部门」区域展示用)。<br> * 权限一览:查询用户所属部门树(左侧上方「所属部门」区域展示用)。<br>
* 树仅由「用户直接归属的组 + 其全部祖先组」构成(如:云平台-学校中学-语文组)。<br> * 树仅由「用户直接归属的组 + 其全部祖先组」构成(如:云平台-学校中学-语文组)。<br>
* groupId 可选(上下文组):传入且用户确属该组时只画该组的祖先链——中间列表每行是「用户×组」对,
* 所属部门只展示行所在组的链路,而非用户全部归属组。<br>
* 非超管:目标用户归属组与数据范围无交集时返回空集合;且树按数据范围剪枝—— * 非超管:目标用户归属组与数据范围无交集时返回空集合;且树按数据范围剪枝——
* 学校管理员视角只展示到学校组(区域组/平台根不在其范围内,不再冒头到根节点)。 * 学校管理员视角只展示到学校组(区域组/平台根不在其范围内,不再冒头到根节点)。
*/ */
@GetMapping("/UserGroupTree") @GetMapping("/UserGroupTree")
public List<UserGroupNodeVo> userGroupTree(@RequestParam Integer userId) { public List<UserGroupNodeVo> userGroupTree(@RequestParam Integer userId,
@RequestParam(required = false) Integer groupId) {
if (!accessScopeService.resolveCurrentScope().isSuper() if (!accessScopeService.resolveCurrentScope().isSuper()
&& !accessScopeService.userIntersectsScope(userId)) { && !accessScopeService.userIntersectsScope(userId)) {
return Collections.emptyList(); return Collections.emptyList();
} }
return groupAuthzService.findUserGroupTree(userId, accessScopeService.scopeGroupIds()); return groupAuthzService.findUserGroupTree(userId, accessScopeService.scopeGroupIds(), groupId);
} }
/** /**
@@ -15,7 +15,9 @@ import javax.validation.constraints.NotNull;
* @author chenx * @author chenx
* @date 2026/7/27 * @date 2026/7/27
* @description 用户-业务角色关系表(逻辑外键,无物理 FK)。<br> * @description 用户-业务角色关系表(逻辑外键,无物理 FK)。<br>
* 一个用户可绑定多个业务角色;一个角色也可绑定多个用户。<br> * 一行 = 用户 u 在组 g 下拥有角色 r;一个用户可在多个组下分别绑定角色,同一组下同一角色唯一。<br>
* 组维度(2026-09-16):groupId = 绑定发生时所选的组(角色授权页添加用户弹窗左侧树选中组 / 用户编辑弹窗所在组),
* NULL = 历史数据视为全局生效(代码侧兜底,不参与按组过滤的排除)。
*/ */
@NoArgsConstructor @NoArgsConstructor
@Data @Data
@@ -24,10 +26,11 @@ import javax.validation.constraints.NotNull;
name = "sys_user_app_role_rel", name = "sys_user_app_role_rel",
indexes = { indexes = {
@Index(name = "idx_user_app_role_user", columnList = "user_id"), @Index(name = "idx_user_app_role_user", columnList = "user_id"),
@Index(name = "idx_user_app_role_role", columnList = "role_id") @Index(name = "idx_user_app_role_role", columnList = "role_id"),
@Index(name = "idx_user_app_role_group", columnList = "group_id")
}, },
uniqueConstraints = { uniqueConstraints = {
@UniqueConstraint(name = "uk_user_app_role", columnNames = {"user_id", "role_id"}) @UniqueConstraint(name = "uk_user_app_role_group", columnNames = {"user_id", "role_id", "group_id"})
} }
) )
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) @EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
@@ -46,4 +49,10 @@ public class SysUserAppRoleRel extends BaseEntity {
@NotNull @NotNull
@Column(name = "role_id", nullable = false) @Column(name = "role_id", nullable = false)
private Integer roleId; private Integer roleId;
/**
* 绑定发生时所选的组ID(逻辑外键 → SysGroup.id);NULL = 历史数据视为全局。<br>
*/
@Column(name = "group_id")
private Integer groupId;
} }
@@ -2,12 +2,14 @@ package com.yunhai.userbase.model.approle;
import lombok.Data; import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.List; import java.util.List;
/** /**
* 角色绑定用户保存入参(全量覆盖,角色维度)。<br> * 角色绑定用户保存入参(增量添加,角色维度)。<br>
* userIds 为该角色本次绑定的用户ID全量列表;传空列表即解绑该角色全部用户。<br> * bindings 为本次新增的「用户×组」绑定明细(组 = 弹窗左侧树当前选中组);已存在的 (userId, groupId) 跳过。<br>
* 组维度改造(2026-09-16)后不再全量覆盖:覆盖会把其他组下的既有绑定一并删掉。
* *
* @author chenx * @author chenx
*/ */
@@ -21,7 +23,7 @@ public class AppRoleUserBindRequest {
private Integer roleId; private Integer roleId;
/** /**
* 绑定的用户ID全量列表。<br> * 本次新增的「用户×组」绑定明细。<br>
*/ */
private List<Integer> userIds; private List<@Valid AppRoleUserBinding> bindings;
} }
@@ -0,0 +1,23 @@
package com.yunhai.userbase.model.approle;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 角色绑定用户明细项:一行 = 用户在指定组下的绑定。<br>
* groupId = 添加用户弹窗左侧树当前选中组(绑定发生时的组),部门列展示时按组ID实时拼路径。
*
* @author chenx
*/
@Data
public class AppRoleUserBinding {
/** 用户ID。 */
@NotNull(message = "用户ID不能为空")
private Integer userId;
/** 绑定发生时所选的组ID。 */
@NotNull(message = "组ID不能为空")
private Integer groupId;
}
@@ -3,7 +3,9 @@ package com.yunhai.userbase.model.approle;
import lombok.Data; import lombok.Data;
/** /**
* 角色已绑定用户列表项视图。<br> * 角色已绑定用户列表项视图(一行 = 一条「用户×组」绑定)。<br>
* groupPath 为查询时按 groupId 实时拼接的组路径(不落库),scope 非空时按查看者数据范围裁剪——
* 学校管理员只看到本校路径(如 /云海小学/教师组),超管看全路径。
* *
* @author chenx * @author chenx
*/ */
@@ -18,4 +20,10 @@ public class AppRoleUserVo {
/** 真实姓名。 */ /** 真实姓名。 */
private String realName; private String realName;
/** 绑定发生时所选的组ID。 */
private Integer groupId;
/** 组路径(查询时按 groupId 现算,scope 裁剪后)。 */
private String groupPath;
} }
@@ -6,8 +6,9 @@ import javax.validation.constraints.NotNull;
import java.util.List; import java.util.List;
/** /**
* 用户绑定角色保存入参(全量覆盖,用户维度)。<br> * 用户绑定角色保存入参(按组覆盖,用户维度)。<br>
* roleIds 为该用户本次绑定的角色ID全量列表;传空列表即解绑该用户全部角色。<br> * roleIds 为该用户在 groupId 组下的角色ID全量列表;传空列表即解绑该用户在该组下的全部角色,
* 不影响其在其他组的绑定;groupId 为空时兜底主组(历史调用方兼容)。<br>
* *
* @author chenx * @author chenx
*/ */
@@ -21,7 +22,12 @@ public class UserAppRoleBindRequest {
private Integer userId; private Integer userId;
/** /**
* 绑定的角色ID全量列表。<br> * 绑定组ID(可选):角色归属的组,空则兜底该用户主组。<br>
*/
private Integer groupId;
/**
* 该组下绑定的角色ID全量列表。<br>
*/ */
private List<Integer> roleIds; private List<Integer> roleIds;
} }
@@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import java.util.Collection;
import java.util.List; import java.util.List;
/** /**
@@ -20,6 +21,24 @@ public interface SysUserAppRoleRelRepository extends JpaRepository<SysUserAppRol
List<SysUserAppRoleRel> findByRoleId(Integer roleId); List<SysUserAppRoleRel> findByRoleId(Integer roleId);
/** 指定用户在指定组下的绑定(编辑弹窗按所在组回显/覆盖保存)。 */
List<SysUserAppRoleRel> findByUserIdAndGroupId(Integer userId, Integer groupId);
/** 指定用户无组维度(NULL=历史数据视为全局)的绑定;用户不属于任何组时权限计算只认这些行。 */
List<SysUserAppRoleRel> findByUserIdAndGroupIdIsNull(Integer userId);
/**
* 权限计算取数:用户「当前所在组」上的绑定 ∪ NULL(历史数据视为全局)。<br>
* groupIds 为空时勿调用(IN () 空集合 SQL 兼容性差),改走 {@link #findByUserIdAndGroupIdIsNull(Integer)}。
*/
@Query("SELECT r FROM SysUserAppRoleRel r WHERE r.userId = :userId "
+ "AND (r.groupId IS NULL OR r.groupId IN :groupIds)")
List<SysUserAppRoleRel> findEffectiveByUserId(@Param("userId") Integer userId,
@Param("groupIds") Collection<Integer> groupIds);
/** 判断 (用户, 角色, 组) 绑定是否已存在(增量添加去重)。 */
boolean existsByUserIdAndRoleIdAndGroupId(Integer userId, Integer roleId, Integer groupId);
/** /**
* 按角色ID批量物理删除绑定记录(bulk delete 直接发 SQL)。<br> * 按角色ID批量物理删除绑定记录(bulk delete 直接发 SQL)。<br>
* 用于「角色绑定用户」全量覆盖式保存的先删后插,以及删除角色时级联清理。<br> * 用于「角色绑定用户」全量覆盖式保存的先删后插,以及删除角色时级联清理。<br>
@@ -43,9 +62,17 @@ public interface SysUserAppRoleRelRepository extends JpaRepository<SysUserAppRol
void deleteByUserIdBulk(@Param("userId") Integer userId); void deleteByUserIdBulk(@Param("userId") Integer userId);
/** /**
* 按角色ID + 用户ID 删除单条绑定记录(移除单个用户的角色关联,避免全量重提)。<br> * 按用户ID + 组ID 批量物理删除该组下的全部绑定(编辑弹窗按组覆盖保存的先删后插)。<br>
*/ */
@Modifying @Modifying
@Query("DELETE FROM SysUserAppRoleRel r WHERE r.roleId = :roleId AND r.userId = :userId") @Query("DELETE FROM SysUserAppRoleRel r WHERE r.userId = :userId AND r.groupId = :groupId")
void deleteByRoleIdAndUserId(@Param("roleId") Integer roleId, @Param("userId") Integer userId); void deleteByUserIdAndGroupIdBulk(@Param("userId") Integer userId, @Param("groupId") Integer groupId);
/**
* 按角色ID + 用户ID + 组ID 删除单条绑定记录(角色用户列表按行移除)。<br>
*/
@Modifying
@Query("DELETE FROM SysUserAppRoleRel r WHERE r.roleId = :roleId AND r.userId = :userId AND r.groupId = :groupId")
void deleteByRoleIdAndUserIdAndGroupId(@Param("roleId") Integer roleId, @Param("userId") Integer userId,
@Param("groupId") Integer groupId);
} }
@@ -74,32 +74,38 @@ public interface IAppRoleService {
void saveRoleModules(AppRoleModuleAssignRequest request); void saveRoleModules(AppRoleModuleAssignRequest request);
/** /**
* 查询角色已绑定的用户列表。<br> * 查询角色已绑定的用户列表(一行 = 一条「用户×组」绑定)。<br>
* groupPath 为查询时按 groupId 实时拼接的组路径;scopeGroupIds 非空(非超管)时滤除
* 绑定组不在范围内的行,且路径只拼到范围内的祖先为止(学校管理员只看到本校路径)。<br>
* *
* @param roleId 角色ID * @param roleId 角色ID
* @return 用户列表 * @param scopeGroupIds 当前管理端数据范围组集合(null=不限制)
* @return 绑定列表
*/ */
List<AppRoleUserVo> findRoleUsers(Integer roleId); List<AppRoleUserVo> findRoleUsers(Integer roleId, java.util.Set<Integer> scopeGroupIds);
/** /**
* 角色绑定用户保存(角色维度,全量覆盖):bulk 先删该角色全部绑定再批量插入。<br> * 角色绑定用户保存(角色维度,增量添加):bindings 为「用户×组」明细,
* 已存在的 (userId, groupId) 跳过;不做全量覆盖——覆盖会把其他组下的既有绑定删掉。<br>
* *
* @param request 角色ID + 用户ID全量列表 * @param request 角色ID + 绑定明细列表
*/ */
void saveRoleUsers(AppRoleUserBindRequest request); void saveRoleUsers(AppRoleUserBindRequest request);
/** /**
* 用户绑定角色保存(用户维度,全量覆盖):bulk 先删该用户全部绑定再批量插入。<br> * 用户绑定角色保存(用户维度,按组覆盖):只覆盖该用户在 groupId 组下的绑定,
* 不影响其在其他组的绑定;groupId 为空时兜底该用户主组。<br>
* *
* @param request 用户ID + 角色ID全量列表 * @param request 用户ID + 组ID + 该组下角色ID全量列表
*/ */
void saveUserRoles(UserAppRoleBindRequest request); void saveUserRoles(UserAppRoleBindRequest request);
/** /**
* 移除单个用户与角色的绑定关系(精确删除单条记录,非全量重提)。<br> * 移除单条「用户×组×角色」绑定(精确删除一行,非全量重提)。<br>
* *
* @param roleId 角色ID * @param roleId 角色ID
* @param userId 用户ID * @param userId 用户ID
* @param groupId 绑定组ID
*/ */
void removeRoleUser(Integer roleId, Integer userId); void removeRoleUser(Integer roleId, Integer userId, Integer groupId);
} }
@@ -104,26 +104,30 @@ public interface IGroupAuthzService {
List<UserPermissionCategoryVo> findUserPermissionTree(Integer groupId, Integer userId); List<UserPermissionCategoryVo> findUserPermissionTree(Integer groupId, Integer userId);
/** /**
* 权限一览:查询用户所属的业务角色列表(中间下方「所属角色」区域展示用)。<br> * 查询用户所属的业务角色列表(权限一览「所属角色」/ 编辑弹窗角色回显)。<br>
* 返回该用户绑定的全部角色(含禁用角色,disabled 字段标记),普通角色回填所属应用名称。<br> * 返回该用户绑定的角色(含禁用角色,disabled 字段标记),普通角色回填所属应用名称,按角色去重。<br>
* *
* @param userId 用户ID * @param userId 用户ID
* @param groupId 组ID(可选):非空时只取该组下的绑定(编辑弹窗按所在组回显);为空取全部绑定聚合
* @return 用户所属角色列表 * @return 用户所属角色列表
*/ */
List<UserRoleVo> findUserRoles(Integer userId); List<UserRoleVo> findUserRoles(Integer userId, Integer groupId);
/** /**
* 权限一览:查询用户所属部门树(左侧上方「所属部门」区域展示用)。<br> * 权限一览:查询用户所属部门树(左侧上方「所属部门」区域展示用)。<br>
* 树仅由「用户直接归属的组 + 其全部祖先组」构成(如:云平台-学校中学-语文组), * 树仅由「用户直接归属的组 + 其全部祖先组」构成(如:云平台-学校中学-语文组),
* 多个归属组时合并为一棵树(公共祖先只出现一次)。<br> * 多个归属组时合并为一棵树(公共祖先只出现一次)。<br>
* 数据范围剪枝:scopeGroupIds 非空时树节点只保留范围内的组——学校管理员视角 * 数据范围剪枝:scopeGroupIds 非空时树节点只保留范围内的组——学校管理员视角
* 区域组/根节点不在其范围内,学校组即成为展示树的根(不再冒头到平台根);null 表示不剪枝(超管)。 * 区域组/根节点不在其范围内,学校组即成为展示树的根(不再冒头到平台根);null 表示不剪枝(超管)。<br>
* 上下文收窄:contextGroupId 非空且该用户确属此组时,只画该组的祖先链(权限一览按行查看
* 「用户×组」对时,所属部门只展示行所在组的链路,而非用户全部归属组);此时「本组」标记也指向该组。
* *
* @param userId 用户ID * @param userId 用户ID
* @param scopeGroupIds 当前管理端数据范围组集合(null=不限制) * @param scopeGroupIds 当前管理端数据范围组集合(null=不限制)
* @param contextGroupId 上下文组ID(null=不收窄,画全部归属组链路)
* @return 所属部门树(根节点列表);用户无组归属时返回空列表 * @return 所属部门树(根节点列表);用户无组归属时返回空列表
*/ */
List<UserGroupNodeVo> findUserGroupTree(Integer userId, java.util.Set<Integer> scopeGroupIds); List<UserGroupNodeVo> findUserGroupTree(Integer userId, java.util.Set<Integer> scopeGroupIds, Integer contextGroupId);
/** /**
* 权限一览:查询<b>组本身</b>的有效已授权应用模块树(点击「所属部门」树节点联动右侧面板用)。<br> * 权限一览:查询<b>组本身</b>的有效已授权应用模块树(点击「所属部门」树节点联动右侧面板用)。<br>
@@ -5,13 +5,16 @@ import com.yunhai.userbase.entity.AppModule;
import com.yunhai.userbase.entity.ApplicationInfo; import com.yunhai.userbase.entity.ApplicationInfo;
import com.yunhai.userbase.entity.SysAppRole; import com.yunhai.userbase.entity.SysAppRole;
import com.yunhai.userbase.entity.SysAppRoleModuleRel; import com.yunhai.userbase.entity.SysAppRoleModuleRel;
import com.yunhai.userbase.entity.SysGroup;
import com.yunhai.userbase.entity.SysUser; import com.yunhai.userbase.entity.SysUser;
import com.yunhai.userbase.entity.SysUserAppRoleRel; import com.yunhai.userbase.entity.SysUserAppRoleRel;
import com.yunhai.userbase.entity.SysUserGroupRel;
import com.yunhai.userbase.event.AppAuthzChangedEvent; import com.yunhai.userbase.event.AppAuthzChangedEvent;
import com.yunhai.userbase.model.approle.AppRoleAuthzAppVo; import com.yunhai.userbase.model.approle.AppRoleAuthzAppVo;
import com.yunhai.userbase.model.approle.AppRoleAuthzModuleVo; import com.yunhai.userbase.model.approle.AppRoleAuthzModuleVo;
import com.yunhai.userbase.model.approle.AppRoleModuleAssignRequest; import com.yunhai.userbase.model.approle.AppRoleModuleAssignRequest;
import com.yunhai.userbase.model.approle.AppRoleUserBindRequest; import com.yunhai.userbase.model.approle.AppRoleUserBindRequest;
import com.yunhai.userbase.model.approle.AppRoleUserBinding;
import com.yunhai.userbase.model.approle.AppRoleUserVo; import com.yunhai.userbase.model.approle.AppRoleUserVo;
import com.yunhai.userbase.model.approle.SysAppRoleQuery; import com.yunhai.userbase.model.approle.SysAppRoleQuery;
import com.yunhai.userbase.model.approle.SysAppRoleRequest; import com.yunhai.userbase.model.approle.SysAppRoleRequest;
@@ -21,9 +24,12 @@ import com.yunhai.userbase.repository.AppModuleRepository;
import com.yunhai.userbase.repository.ApplicationInfoRepository; import com.yunhai.userbase.repository.ApplicationInfoRepository;
import com.yunhai.userbase.repository.SysAppRoleModuleRelRepository; import com.yunhai.userbase.repository.SysAppRoleModuleRelRepository;
import com.yunhai.userbase.repository.SysAppRoleRepository; import com.yunhai.userbase.repository.SysAppRoleRepository;
import com.yunhai.userbase.repository.SysGroupRepository;
import com.yunhai.userbase.repository.SysUserAppRoleRelRepository; import com.yunhai.userbase.repository.SysUserAppRoleRelRepository;
import com.yunhai.userbase.repository.SysUserGroupRelRepository;
import com.yunhai.userbase.repository.SysUserRepository; import com.yunhai.userbase.repository.SysUserRepository;
import com.yunhai.userbase.service.IAppRoleService; import com.yunhai.userbase.service.IAppRoleService;
import com.yunhai.userbase.service.resolve.MainGroupResolver;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -56,7 +62,8 @@ import java.util.Set;
* @description 应用业务角色服务实现。<br> * @description 应用业务角色服务实现。<br>
* 角色创建:普通角色(allAppFlag=false,appId 必填,同应用下重名校验)/ 全应用角色(allAppFlag=true,appId 为空,全应用角色间重名校验)。<br> * 角色创建:普通角色(allAppFlag=false,appId 必填,同应用下重名校验)/ 全应用角色(allAppFlag=true,appId 为空,全应用角色间重名校验)。<br>
* 模块授权:sys_app_role_module_rel 全量覆盖式保存(bulk 先删后插);普通角色仅能授权本应用模块,全应用角色可跨应用。<br> * 模块授权:sys_app_role_module_rel 全量覆盖式保存(bulk 先删后插);普通角色仅能授权本应用模块,全应用角色可跨应用。<br>
* 用户绑定:sys_user_app_role_rel 支持角色维度 / 用户维度两个方向的全量覆盖保存。<br> * 用户绑定:sys_user_app_role_rel 带组维度(一行 = 用户×组×角色,2026-09-16)——
* 角色维度增量添加(绑定发生时的组)、用户维度按组覆盖(编辑弹窗所在组),组路径查询时实时拼接不落库。
*/ */
@Service @Service
public class AppRoleService implements IAppRoleService { public class AppRoleService implements IAppRoleService {
@@ -65,6 +72,9 @@ public class AppRoleService implements IAppRoleService {
* 无应用类型;全应用授权范围须排除它——与 ApplicationInfoRepository「appKey > 100000」口径一致。 */ * 无应用类型;全应用授权范围须排除它——与 ApplicationInfoRepository「appKey > 100000」口径一致。 */
private static final int USER_CENTER_APP_KEY = 100000; private static final int USER_CENTER_APP_KEY = 100000;
/** 组根节点的 parentId 约定值(与 GroupAuthzService.ROOT_PARENT_ID 同语义)。 */
private static final int ROOT_PARENT_ID = 0;
@Autowired @Autowired
private SysAppRoleRepository sysAppRoleRepository; private SysAppRoleRepository sysAppRoleRepository;
@@ -83,6 +93,15 @@ public class AppRoleService implements IAppRoleService {
@Autowired @Autowired
private SysUserRepository sysUserRepository; private SysUserRepository sysUserRepository;
@Autowired
private SysGroupRepository sysGroupRepository;
@Autowired
private SysUserGroupRelRepository sysUserGroupRelRepository;
@Autowired
private MainGroupResolver mainGroupResolver;
@Autowired @Autowired
private ApplicationEventPublisher eventPublisher; private ApplicationEventPublisher eventPublisher;
@@ -274,10 +293,10 @@ public class AppRoleService implements IAppRoleService {
publishAuthzEvent("saveRoleModules"); publishAuthzEvent("saveRoleModules");
} }
// ===================== 用户角色绑定 ===================== // ===================== 用户角色绑定(组维度:一行 = 用户×组×角色) =====================
@Override @Override
public List<AppRoleUserVo> findRoleUsers(Integer roleId) { public List<AppRoleUserVo> findRoleUsers(Integer roleId, Set<Integer> scopeGroupIds) {
Objects.requireNonNull(roleId, "角色ID不能为空"); Objects.requireNonNull(roleId, "角色ID不能为空");
if (!sysAppRoleRepository.existsById(roleId)) { if (!sysAppRoleRepository.existsById(roleId)) {
throw new DException("角色不存在: " + roleId); throw new DException("角色不存在: " + roleId);
@@ -286,16 +305,30 @@ public class AppRoleService implements IAppRoleService {
if (rels.isEmpty()) { if (rels.isEmpty()) {
return Collections.emptyList(); return Collections.emptyList();
} }
// scope 非空(非超管)时只保留绑定组在范围内的行(与 searchUsers 同口径)
List<SysUserAppRoleRel> visibleRels = rels;
if (scopeGroupIds != null) {
visibleRels = new ArrayList<>();
for (SysUserAppRoleRel rel : rels) {
if (rel.getGroupId() != null && scopeGroupIds.contains(rel.getGroupId())) {
visibleRels.add(rel);
}
}
}
Set<Integer> userIds = new LinkedHashSet<>(); Set<Integer> userIds = new LinkedHashSet<>();
for (SysUserAppRoleRel rel : rels) { for (SysUserAppRoleRel rel : visibleRels) {
userIds.add(rel.getUserId()); if (rel.getUserId() != null) {
userIds.add(rel.getUserId());
}
} }
Map<Integer, SysUser> userMap = new HashMap<>(); Map<Integer, SysUser> userMap = new HashMap<>();
for (SysUser user : sysUserRepository.findAllById(userIds)) { for (SysUser user : sysUserRepository.findAllById(userIds)) {
userMap.put(user.getId(), user); userMap.put(user.getId(), user);
} }
// 部门路径不落库:按绑定行的 groupId 实时拼路径,scope 非空时裁到范围内祖先为止
Map<Integer, SysGroup> groupMap = buildAllGroupMap();
List<AppRoleUserVo> result = new ArrayList<>(); List<AppRoleUserVo> result = new ArrayList<>();
for (SysUserAppRoleRel rel : rels) { for (SysUserAppRoleRel rel : visibleRels) {
SysUser user = userMap.get(rel.getUserId()); SysUser user = userMap.get(rel.getUserId());
if (user == null) { if (user == null) {
continue; continue;
@@ -304,6 +337,8 @@ public class AppRoleService implements IAppRoleService {
vo.setId(user.getId()); vo.setId(user.getId());
vo.setAccount(user.getAccount()); vo.setAccount(user.getAccount());
vo.setRealName(user.getRealName()); vo.setRealName(user.getRealName());
vo.setGroupId(rel.getGroupId());
vo.setGroupPath(buildScopedGroupPath(rel.getGroupId(), groupMap, scopeGroupIds));
result.add(vo); result.add(vo);
} }
return result; return result;
@@ -317,21 +352,29 @@ public class AppRoleService implements IAppRoleService {
if (!sysAppRoleRepository.existsById(roleId)) { if (!sysAppRoleRepository.existsById(roleId)) {
throw new DException("角色不存在: " + roleId); throw new DException("角色不存在: " + roleId);
} }
// 去重并校验用户均存在 // 增量添加「用户×组」绑定(组=弹窗左侧树当前选中组),已存在的 (userId, groupId) 跳过;
List<Integer> userIds = new ArrayList<>(new LinkedHashSet<>( // 不做全量覆盖——覆盖会把该角色在其他组下的既有绑定一并删掉
request.getUserIds() == null ? Collections.emptyList() : request.getUserIds())); List<AppRoleUserBinding> bindings = request.getBindings() == null
for (Integer userId : userIds) { ? Collections.emptyList() : request.getBindings();
if (!sysUserRepository.existsById(userId)) {
throw new DException("用户不存在: " + userId);
}
}
// 全量覆盖:bulk 先删旧绑定,再批量插入
sysUserAppRoleRelRepository.deleteByRoleIdBulk(roleId);
List<SysUserAppRoleRel> batch = new ArrayList<>(); List<SysUserAppRoleRel> batch = new ArrayList<>();
for (Integer userId : userIds) { for (AppRoleUserBinding binding : bindings) {
if (binding == null || binding.getUserId() == null || binding.getGroupId() == null) {
continue;
}
if (!sysUserRepository.existsById(binding.getUserId())) {
throw new DException("用户不存在: " + binding.getUserId());
}
if (!sysGroupRepository.existsById(binding.getGroupId())) {
throw new DException("系统组不存在: " + binding.getGroupId());
}
if (sysUserAppRoleRelRepository.existsByUserIdAndRoleIdAndGroupId(
binding.getUserId(), roleId, binding.getGroupId())) {
continue;
}
SysUserAppRoleRel rel = new SysUserAppRoleRel(); SysUserAppRoleRel rel = new SysUserAppRoleRel();
rel.setUserId(userId); rel.setUserId(binding.getUserId());
rel.setRoleId(roleId); rel.setRoleId(roleId);
rel.setGroupId(binding.getGroupId());
batch.add(rel); batch.add(rel);
} }
if (!batch.isEmpty()) { if (!batch.isEmpty()) {
@@ -348,6 +391,26 @@ public class AppRoleService implements IAppRoleService {
if (!sysUserRepository.existsById(userId)) { if (!sysUserRepository.existsById(userId)) {
throw new DException("用户不存在: " + userId); throw new DException("用户不存在: " + userId);
} }
// 绑定组:入参为空兜底主组(历史调用方兼容);组必须属于该用户实际所在组,防止给任意组写绑定
List<SysUserGroupRel> userRels = sysUserGroupRelRepository.findByUserId(userId);
Integer groupId = request.getGroupId();
if (groupId == null) {
SysUserGroupRel mainRel = mainGroupResolver.resolve(userRels);
groupId = mainRel != null ? mainRel.getGroupId() : null;
}
if (groupId == null) {
throw new DException("用户未归属任何组,无法绑定角色");
}
boolean inGroup = false;
for (SysUserGroupRel rel : userRels) {
if (groupId.equals(rel.getGroupId())) {
inGroup = true;
break;
}
}
if (!inGroup) {
throw new DException("绑定组不在该用户所在组范围内: " + groupId);
}
// 去重并校验角色均存在 // 去重并校验角色均存在
List<Integer> roleIds = new ArrayList<>(new LinkedHashSet<>( List<Integer> roleIds = new ArrayList<>(new LinkedHashSet<>(
request.getRoleIds() == null ? Collections.emptyList() : request.getRoleIds())); request.getRoleIds() == null ? Collections.emptyList() : request.getRoleIds()));
@@ -356,13 +419,14 @@ public class AppRoleService implements IAppRoleService {
throw new DException("角色不存在: " + roleId); throw new DException("角色不存在: " + roleId);
} }
} }
// 全量覆盖:bulk 先删旧绑定,再批量插入 // 按组覆盖:只删该用户在该组下的绑定,其他组绑定不受影响
sysUserAppRoleRelRepository.deleteByUserIdBulk(userId); sysUserAppRoleRelRepository.deleteByUserIdAndGroupIdBulk(userId, groupId);
List<SysUserAppRoleRel> batch = new ArrayList<>(); List<SysUserAppRoleRel> batch = new ArrayList<>();
for (Integer roleId : roleIds) { for (Integer roleId : roleIds) {
SysUserAppRoleRel rel = new SysUserAppRoleRel(); SysUserAppRoleRel rel = new SysUserAppRoleRel();
rel.setUserId(userId); rel.setUserId(userId);
rel.setRoleId(roleId); rel.setRoleId(roleId);
rel.setGroupId(groupId);
batch.add(rel); batch.add(rel);
} }
if (!batch.isEmpty()) { if (!batch.isEmpty()) {
@@ -373,16 +437,17 @@ public class AppRoleService implements IAppRoleService {
@Override @Override
@Transactional @Transactional
public void removeRoleUser(Integer roleId, Integer userId) { public void removeRoleUser(Integer roleId, Integer userId, Integer groupId) {
Objects.requireNonNull(roleId, "角色ID不能为空"); Objects.requireNonNull(roleId, "角色ID不能为空");
Objects.requireNonNull(userId, "用户ID不能为空"); Objects.requireNonNull(userId, "用户ID不能为空");
Objects.requireNonNull(groupId, "组ID不能为空");
if (!sysAppRoleRepository.existsById(roleId)) { if (!sysAppRoleRepository.existsById(roleId)) {
throw new DException("角色不存在: " + roleId); throw new DException("角色不存在: " + roleId);
} }
if (!sysUserRepository.existsById(userId)) { if (!sysUserRepository.existsById(userId)) {
throw new DException("用户不存在: " + userId); throw new DException("用户不存在: " + userId);
} }
sysUserAppRoleRelRepository.deleteByRoleIdAndUserId(roleId, userId); sysUserAppRoleRelRepository.deleteByRoleIdAndUserIdAndGroupId(roleId, userId, groupId);
publishAuthzEvent("removeRoleUser"); publishAuthzEvent("removeRoleUser");
} }
@@ -394,6 +459,42 @@ public class AppRoleService implements IAppRoleService {
eventPublisher.publishEvent(new AppAuthzChangedEvent(this, "AppRoleService." + sourceOp)); eventPublisher.publishEvent(new AppAuthzChangedEvent(this, "AppRoleService." + sourceOp));
} }
/** 全量组快照(组表量级小,与 GroupAuthzService.buildAllGroupMap 同思路)。 */
private Map<Integer, SysGroup> buildAllGroupMap() {
Map<Integer, SysGroup> map = new HashMap<>();
for (SysGroup g : sysGroupRepository.findAll()) {
map.put(g.getId(), g);
}
return map;
}
/**
* scope 感知的组路径拼接:自绑定组向上收集祖先名,遇到不在 scope 内的祖先即停(不含)。
* scope 为 null(超管)拼全路径;学校管理员只看到本校路径(如 /云海小学/教师组),
* 平台/局端看到各自的范围内路径。
*/
private String buildScopedGroupPath(Integer groupId, Map<Integer, SysGroup> groupMap, Set<Integer> scopeGroupIds) {
if (groupId == null) {
return "";
}
List<String> names = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
Integer cursor = groupId;
while (cursor != null && cursor != ROOT_PARENT_ID && visited.add(cursor)) {
if (scopeGroupIds != null && !scopeGroupIds.contains(cursor)) {
break;
}
SysGroup g = groupMap.get(cursor);
if (g == null) {
break;
}
names.add(g.getName() == null ? "" : g.getName());
cursor = g.getParentId();
}
Collections.reverse(names);
return names.isEmpty() ? "" : "/" + String.join("/", names);
}
// ===================== 私有工具 ===================== // ===================== 私有工具 =====================
/** /**
@@ -1259,12 +1259,15 @@ public class GroupAuthzService implements IGroupAuthzService {
} }
@Override @Override
public List<UserRoleVo> findUserRoles(Integer userId) { public List<UserRoleVo> findUserRoles(Integer userId, Integer groupId) {
Objects.requireNonNull(userId, "用户ID不能为空"); Objects.requireNonNull(userId, "用户ID不能为空");
if (!sysUserRepository.existsById(userId)) { if (!sysUserRepository.existsById(userId)) {
throw new DException("用户不存在"); throw new DException("用户不存在");
} }
List<SysUserAppRoleRel> rels = sysUserAppRoleRelRepository.findByUserId(userId); // groupId 非空 = 只取该组下的绑定(编辑弹窗按所在组回显);为空 = 全部绑定聚合去重(权限一览展示)
List<SysUserAppRoleRel> rels = groupId == null
? sysUserAppRoleRelRepository.findByUserId(userId)
: sysUserAppRoleRelRepository.findByUserIdAndGroupId(userId, groupId);
if (rels.isEmpty()) { if (rels.isEmpty()) {
return Collections.emptyList(); return Collections.emptyList();
} }
@@ -1310,7 +1313,7 @@ public class GroupAuthzService implements IGroupAuthzService {
} }
@Override @Override
public List<UserGroupNodeVo> findUserGroupTree(Integer userId, Set<Integer> scopeGroupIds) { public List<UserGroupNodeVo> findUserGroupTree(Integer userId, Set<Integer> scopeGroupIds, Integer contextGroupId) {
Objects.requireNonNull(userId, "用户ID不能为空"); Objects.requireNonNull(userId, "用户ID不能为空");
if (!sysUserRepository.existsById(userId)) { if (!sysUserRepository.existsById(userId)) {
throw new DException("用户不存在"); throw new DException("用户不存在");
@@ -1329,6 +1332,13 @@ public class GroupAuthzService implements IGroupAuthzService {
if (selfGroupIds.isEmpty()) { if (selfGroupIds.isEmpty()) {
return Collections.emptyList(); return Collections.emptyList();
} }
// 1.5 上下文收窄:指定组且用户确属该组时只画该组链路(权限一览按行查看「用户×组」对);
// 「本组」标记同步指向该组——主组可能在被收窄掉的链路上,沿用主组会导致整棵树没有「本组」
if (contextGroupId != null && selfGroupIds.contains(contextGroupId)) {
selfGroupIds = new LinkedHashSet<>();
selfGroupIds.add(contextGroupId);
mainGroupId = contextGroupId;
}
// 2. 祖先链并集(含自身);多个归属组共享祖先时只出现一次 // 2. 祖先链并集(含自身);多个归属组共享祖先时只出现一次
Map<Integer, SysGroup> allGroupMap = buildAllGroupMap(); Map<Integer, SysGroup> allGroupMap = buildAllGroupMap();
Set<Integer> nodeIds = new LinkedHashSet<>(); Set<Integer> nodeIds = new LinkedHashSet<>();
@@ -1851,10 +1861,21 @@ public class GroupAuthzService implements IGroupAuthzService {
/** /**
* 收集用户经「启用状态」业务角色获得授权的模块ID集合(角色授权并集的角色侧输入)。<br> * 收集用户经「启用状态」业务角色获得授权的模块ID集合(角色授权并集的角色侧输入)。<br>
* 组维度(2026-09-16):只算「用户当前所在组」上的绑定(group_id ∈ 当前组集合),
* group_id 为 NULL 的历史行视为全局生效(兜底防迁移脚本未跑导致权限丢失)。<br>
* 调整组不删绑定:旧组上的绑定因组关系重建而自然失效,调回该组自动恢复。<br>
* 禁用角色(disabled=true)不参与权限计算;用户无绑定或角色全被禁用时返回空集合。<br> * 禁用角色(disabled=true)不参与权限计算;用户无绑定或角色全被禁用时返回空集合。<br>
*/ */
private Set<Integer> collectEnabledRoleModuleIds(Integer userId) { private Set<Integer> collectEnabledRoleModuleIds(Integer userId) {
List<SysUserAppRoleRel> rels = sysUserAppRoleRelRepository.findByUserId(userId); List<Integer> userGroupIds = new ArrayList<>();
for (SysUserGroupRel rel : sysUserGroupRelRepository.findByUserId(userId)) {
if (rel.getGroupId() != null) {
userGroupIds.add(rel.getGroupId());
}
}
List<SysUserAppRoleRel> rels = userGroupIds.isEmpty()
? sysUserAppRoleRelRepository.findByUserIdAndGroupIdIsNull(userId)
: sysUserAppRoleRelRepository.findEffectiveByUserId(userId, userGroupIds);
if (rels.isEmpty()) { if (rels.isEmpty()) {
return Collections.emptySet(); return Collections.emptySet();
} }
@@ -0,0 +1,44 @@
-- ============================================================================
-- 用户角色绑定加组维度迁移(2026-09-16)· 直执行完整版
-- 适用:新后端尚未启动过(ddl-auto=update 还没建结构)的情况;逐条执行,勿整段粘贴。
-- 与启动先后无关:若新后端已启动过,结构语句会报"已存在",按下述忽略规则处理即可。
-- 不幂等的语句及忽略规则(报错=已做过,视为成功):
-- ① 报 1060 Duplicate column name → group_id 列已存在,忽略
-- ② 报 1061 Duplicate key name → 索引/约束已存在,忽略
-- ④ 报 1091 index doesn't exist → 旧约束已删过,忽略
-- 回填语句(③)本身幂等,重复执行 0 行。
-- ============================================================================
-- ① 加列(只存组ID,不存路径字符串;部门列展示时按组ID实时拼路径)
ALTER TABLE sys_user_app_role_rel ADD COLUMN group_id INT NULL AFTER role_id;
-- ② 加新唯一约束 + 组维度索引
ALTER TABLE sys_user_app_role_rel ADD UNIQUE KEY uk_user_app_role_group (user_id, role_id, group_id);
ALTER TABLE sys_user_app_role_rel ADD INDEX idx_user_app_role_group (group_id);
-- ③ 存量回填:各绑定行挂到该用户的主组(is_main=1 优先,无主组则取最小 rel.id 的组)
UPDATE sys_user_app_role_rel r
JOIN (
SELECT rel.user_id,
SUBSTRING_INDEX(GROUP_CONCAT(rel.group_id ORDER BY rel.is_main DESC, rel.id ASC), ',', 1) + 0 AS main_group_id
FROM sys_user_group_rel rel
WHERE rel.group_id IS NOT NULL
GROUP BY rel.user_id
) m ON m.user_id = r.user_id
SET r.group_id = m.main_group_id
WHERE r.group_id IS NULL;
-- ④ 删除旧二元唯一约束(不删则同用户跨组绑同角色必撞 DuplicateKey)
ALTER TABLE sys_user_app_role_rel DROP INDEX uk_user_app_role;
-- ============================================================================
-- 自检:
-- 1) 应返回 0 行:有组关系的用户却仍挂着 NULL 绑定(回填漏网)
SELECT r.id, r.user_id, r.role_id
FROM sys_user_app_role_rel r
JOIN sys_user_group_rel g ON g.user_id = r.user_id AND g.group_id IS NOT NULL
WHERE r.group_id IS NULL
GROUP BY r.id, r.user_id, r.role_id;
-- 2) 应只剩新约束:uk_user_app_role_group 在列,uk_user_app_role 不在列
SHOW INDEX FROM sys_user_app_role_rel WHERE Key_name LIKE 'uk%';
@@ -4,11 +4,14 @@ import com.yunhai.userbase.entity.AppCategory;
import com.yunhai.userbase.entity.AppModule; import com.yunhai.userbase.entity.AppModule;
import com.yunhai.userbase.entity.SysAppRole; import com.yunhai.userbase.entity.SysAppRole;
import com.yunhai.userbase.entity.SysGroupAppRel; import com.yunhai.userbase.entity.SysGroupAppRel;
import com.yunhai.userbase.entity.SysUserAppRoleRel;
import com.yunhai.userbase.entity.SysUserGroupRel;
import com.yunhai.userbase.event.AppAuthzChangedEvent; import com.yunhai.userbase.event.AppAuthzChangedEvent;
import com.yunhai.userbase.model.appcategory.AppCategoryRequest; import com.yunhai.userbase.model.appcategory.AppCategoryRequest;
import com.yunhai.userbase.model.appmodule.AppModuleRequest; import com.yunhai.userbase.model.appmodule.AppModuleRequest;
import com.yunhai.userbase.model.approle.AppRoleModuleAssignRequest; import com.yunhai.userbase.model.approle.AppRoleModuleAssignRequest;
import com.yunhai.userbase.model.approle.AppRoleUserBindRequest; import com.yunhai.userbase.model.approle.AppRoleUserBindRequest;
import com.yunhai.userbase.model.approle.AppRoleUserBinding;
import com.yunhai.userbase.model.approle.SysAppRoleRequest; import com.yunhai.userbase.model.approle.SysAppRoleRequest;
import com.yunhai.userbase.model.approle.UserAppRoleBindRequest; import com.yunhai.userbase.model.approle.UserAppRoleBindRequest;
import com.yunhai.userbase.model.application.ApplicationInfoRequest; import com.yunhai.userbase.model.application.ApplicationInfoRequest;
@@ -23,9 +26,11 @@ import com.yunhai.userbase.repository.SysGroupRepository;
import com.yunhai.userbase.repository.SysSchoolAppOverrideRepository; import com.yunhai.userbase.repository.SysSchoolAppOverrideRepository;
import com.yunhai.userbase.repository.SysSchoolCategoryOverrideRepository; import com.yunhai.userbase.repository.SysSchoolCategoryOverrideRepository;
import com.yunhai.userbase.repository.SysUserAppRoleRelRepository; import com.yunhai.userbase.repository.SysUserAppRoleRelRepository;
import com.yunhai.userbase.repository.SysUserGroupRelRepository;
import com.yunhai.userbase.repository.SysUserRepository; import com.yunhai.userbase.repository.SysUserRepository;
import com.yunhai.userbase.enums.AccessLevelEnum; import com.yunhai.userbase.enums.AccessLevelEnum;
import com.yunhai.userbase.service.resolve.AccessScope; import com.yunhai.userbase.service.resolve.AccessScope;
import com.yunhai.userbase.service.resolve.MainGroupResolver;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
@@ -35,9 +40,11 @@ import org.mockito.MockitoAnnotations;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -67,6 +74,10 @@ class AppAuthzEventPublishTest {
@Mock @Mock
private SysUserRepository sysUserRepository; private SysUserRepository sysUserRepository;
@Mock @Mock
private SysUserGroupRelRepository sysUserGroupRelRepository;
@Mock
private MainGroupResolver mainGroupResolver;
@Mock
private ApplicationEventPublisher eventPublisher; private ApplicationEventPublisher eventPublisher;
// AppCategoryService // AppCategoryService
@@ -154,9 +165,14 @@ class AppAuthzEventPublishTest {
void appRole_saveRoleUsers_publishes() { void appRole_saveRoleUsers_publishes() {
when(sysAppRoleRepository.existsById(1)).thenReturn(true); when(sysAppRoleRepository.existsById(1)).thenReturn(true);
when(sysUserRepository.existsById(2)).thenReturn(true); when(sysUserRepository.existsById(2)).thenReturn(true);
when(sysGroupRepository.existsById(3)).thenReturn(true);
when(sysUserAppRoleRelRepository.existsByUserIdAndRoleIdAndGroupId(2, 1, 3)).thenReturn(false);
AppRoleUserBinding binding = new AppRoleUserBinding();
binding.setUserId(2);
binding.setGroupId(3);
AppRoleUserBindRequest request = new AppRoleUserBindRequest(); AppRoleUserBindRequest request = new AppRoleUserBindRequest();
request.setRoleId(1); request.setRoleId(1);
request.setUserIds(Collections.singletonList(2)); request.setBindings(Collections.singletonList(binding));
appRoleService.saveRoleUsers(request); appRoleService.saveRoleUsers(request);
assertEquals("AppRoleService.saveRoleUsers", capturedOp().getSourceOp()); assertEquals("AppRoleService.saveRoleUsers", capturedOp().getSourceOp());
} }
@@ -165,8 +181,14 @@ class AppAuthzEventPublishTest {
void appRole_saveUserRoles_publishes() { void appRole_saveUserRoles_publishes() {
when(sysUserRepository.existsById(2)).thenReturn(true); when(sysUserRepository.existsById(2)).thenReturn(true);
when(sysAppRoleRepository.existsById(1)).thenReturn(true); when(sysAppRoleRepository.existsById(1)).thenReturn(true);
SysUserGroupRel rel = new SysUserGroupRel();
rel.setUserId(2);
rel.setGroupId(3);
when(sysUserGroupRelRepository.findByUserId(2)).thenReturn(Collections.singletonList(rel));
when(mainGroupResolver.resolve(anyList())).thenReturn(rel);
UserAppRoleBindRequest request = new UserAppRoleBindRequest(); UserAppRoleBindRequest request = new UserAppRoleBindRequest();
request.setUserId(2); request.setUserId(2);
request.setGroupId(3);
request.setRoleIds(Collections.singletonList(1)); request.setRoleIds(Collections.singletonList(1));
appRoleService.saveUserRoles(request); appRoleService.saveUserRoles(request);
assertEquals("AppRoleService.saveUserRoles", capturedOp().getSourceOp()); assertEquals("AppRoleService.saveUserRoles", capturedOp().getSourceOp());
@@ -176,7 +198,7 @@ class AppAuthzEventPublishTest {
void appRole_removeRoleUser_publishes() { void appRole_removeRoleUser_publishes() {
when(sysAppRoleRepository.existsById(1)).thenReturn(true); when(sysAppRoleRepository.existsById(1)).thenReturn(true);
when(sysUserRepository.existsById(2)).thenReturn(true); when(sysUserRepository.existsById(2)).thenReturn(true);
appRoleService.removeRoleUser(1, 2); appRoleService.removeRoleUser(1, 2, 3);
assertEquals("AppRoleService.removeRoleUser", capturedOp().getSourceOp()); assertEquals("AppRoleService.removeRoleUser", capturedOp().getSourceOp());
} }