feat: 增加分类权限控制

- 新增角色分类授权模型与超级管理员配置接口

- 接入助手、插件、工作流、知识库、素材的分类可见性过滤

- 增加角色页分类权限树与插件多分类可见性支持
This commit is contained in:
2026-03-29 17:16:37 +08:00
parent aaf4c61ff8
commit f49d94e2fe
46 changed files with 1963 additions and 128 deletions

View File

@@ -0,0 +1,23 @@
package tech.easyflow.system.entity;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Table;
import tech.easyflow.system.entity.base.SysRoleCategoryScopeBase;
import java.math.BigInteger;
import java.util.List;
@Table(value = "tb_sys_role_category_scope", comment = "角色分类权限范围")
public class SysRoleCategoryScope extends SysRoleCategoryScopeBase {
@Column(ignore = true)
private List<BigInteger> categoryIds;
public List<BigInteger> getCategoryIds() {
return categoryIds;
}
public void setCategoryIds(List<BigInteger> categoryIds) {
this.categoryIds = categoryIds;
}
}

View File

@@ -0,0 +1,8 @@
package tech.easyflow.system.entity;
import com.mybatisflex.annotation.Table;
import tech.easyflow.system.entity.base.SysRoleCategoryScopeItemBase;
@Table(value = "tb_sys_role_category_scope_item", comment = "角色分类权限明细")
public class SysRoleCategoryScopeItem extends SysRoleCategoryScopeItemBase {
}

View File

@@ -0,0 +1,102 @@
package tech.easyflow.system.entity.base;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
public class SysRoleCategoryScopeBase implements Serializable {
private static final long serialVersionUID = 1L;
@Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "主键")
private BigInteger id;
@Column(comment = "角色ID")
private BigInteger roleId;
@Column(comment = "资源类型")
private String resourceType;
@Column(comment = "范围模式")
private String scopeMode;
@Column(comment = "创建时间")
private Date created;
@Column(comment = "创建者")
private BigInteger createdBy;
@Column(comment = "修改时间")
private Date modified;
@Column(comment = "修改者")
private BigInteger modifiedBy;
public BigInteger getId() {
return id;
}
public void setId(BigInteger id) {
this.id = id;
}
public BigInteger getRoleId() {
return roleId;
}
public void setRoleId(BigInteger roleId) {
this.roleId = roleId;
}
public String getResourceType() {
return resourceType;
}
public void setResourceType(String resourceType) {
this.resourceType = resourceType;
}
public String getScopeMode() {
return scopeMode;
}
public void setScopeMode(String scopeMode) {
this.scopeMode = scopeMode;
}
public Date getCreated() {
return created;
}
public void setCreated(Date created) {
this.created = created;
}
public BigInteger getCreatedBy() {
return createdBy;
}
public void setCreatedBy(BigInteger createdBy) {
this.createdBy = createdBy;
}
public Date getModified() {
return modified;
}
public void setModified(Date modified) {
this.modified = modified;
}
public BigInteger getModifiedBy() {
return modifiedBy;
}
public void setModifiedBy(BigInteger modifiedBy) {
this.modifiedBy = modifiedBy;
}
}

View File

@@ -0,0 +1,46 @@
package tech.easyflow.system.entity.base;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import java.io.Serializable;
import java.math.BigInteger;
public class SysRoleCategoryScopeItemBase implements Serializable {
private static final long serialVersionUID = 1L;
@Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "主键")
private BigInteger id;
@Column(comment = "范围ID")
private BigInteger scopeId;
@Column(comment = "分类ID")
private BigInteger categoryId;
public BigInteger getId() {
return id;
}
public void setId(BigInteger id) {
this.id = id;
}
public BigInteger getScopeId() {
return scopeId;
}
public void setScopeId(BigInteger scopeId) {
this.scopeId = scopeId;
}
public BigInteger getCategoryId() {
return categoryId;
}
public void setCategoryId(BigInteger categoryId) {
this.categoryId = categoryId;
}
}

View File

@@ -0,0 +1,67 @@
package tech.easyflow.system.entity.vo;
import java.math.BigInteger;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
public class RoleCategoryAccessSnapshot {
private final String resourceType;
private final BigInteger accountId;
private final boolean superAdmin;
private final boolean allAccess;
private final Set<BigInteger> categoryIds;
public RoleCategoryAccessSnapshot(String resourceType,
BigInteger accountId,
boolean superAdmin,
boolean allAccess,
Set<BigInteger> categoryIds) {
this.resourceType = resourceType;
this.accountId = accountId;
this.superAdmin = superAdmin;
this.allAccess = allAccess;
this.categoryIds = categoryIds == null
? Collections.emptySet()
: Collections.unmodifiableSet(new LinkedHashSet<>(categoryIds));
}
public String getResourceType() {
return resourceType;
}
public BigInteger getAccountId() {
return accountId;
}
public Long getAccountIdAsLong() {
return accountId == null ? null : accountId.longValue();
}
public boolean isSuperAdmin() {
return superAdmin;
}
public boolean isAllAccess() {
return allAccess;
}
public Set<BigInteger> getCategoryIds() {
return categoryIds;
}
public boolean isRestricted() {
return !superAdmin && !allAccess;
}
public boolean canAccess(BigInteger createdBy, BigInteger categoryId) {
if (!isRestricted()) {
return true;
}
if (accountId != null && accountId.equals(createdBy)) {
return true;
}
return categoryId != null && categoryIds.contains(categoryId);
}
}

View File

@@ -0,0 +1,38 @@
package tech.easyflow.system.entity.vo;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
public class SysRoleCategoryScopeDetailVo {
private BigInteger roleId;
private boolean editable;
private List<SysRoleCategoryScopeItemVo> scopes = new ArrayList<>();
public BigInteger getRoleId() {
return roleId;
}
public void setRoleId(BigInteger roleId) {
this.roleId = roleId;
}
public boolean isEditable() {
return editable;
}
public void setEditable(boolean editable) {
this.editable = editable;
}
public List<SysRoleCategoryScopeItemVo> getScopes() {
return scopes;
}
public void setScopes(List<SysRoleCategoryScopeItemVo> scopes) {
this.scopes = scopes;
}
}

View File

@@ -0,0 +1,38 @@
package tech.easyflow.system.entity.vo;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
public class SysRoleCategoryScopeItemVo {
private String resourceType;
private String scopeMode;
private List<BigInteger> categoryIds = new ArrayList<>();
public String getResourceType() {
return resourceType;
}
public void setResourceType(String resourceType) {
this.resourceType = resourceType;
}
public String getScopeMode() {
return scopeMode;
}
public void setScopeMode(String scopeMode) {
this.scopeMode = scopeMode;
}
public List<BigInteger> getCategoryIds() {
return categoryIds;
}
public void setCategoryIds(List<BigInteger> categoryIds) {
this.categoryIds = categoryIds;
}
}

View File

@@ -0,0 +1,40 @@
package tech.easyflow.system.enums;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
public enum CategoryResourceType {
BOT("BOT"),
PLUGIN("PLUGIN"),
WORKFLOW("WORKFLOW"),
KNOWLEDGE("KNOWLEDGE"),
RESOURCE("RESOURCE");
private final String code;
CategoryResourceType(String code) {
this.code = code;
}
public String getCode() {
return code;
}
public static CategoryResourceType from(String code) {
if (code == null) {
throw new IllegalArgumentException("resourceType不能为空");
}
String normalized = code.trim().toUpperCase(Locale.ROOT);
return Arrays.stream(values())
.filter(item -> item.code.equals(normalized))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("不支持的resourceType: " + code));
}
public static List<String> allCodes() {
return Arrays.stream(values()).map(CategoryResourceType::getCode).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,31 @@
package tech.easyflow.system.enums;
import java.util.Arrays;
import java.util.Locale;
public enum CategoryScopeMode {
ALL("ALL"),
CUSTOM("CUSTOM");
private final String code;
CategoryScopeMode(String code) {
this.code = code;
}
public String getCode() {
return code;
}
public static CategoryScopeMode from(String code) {
if (code == null) {
return CUSTOM;
}
String normalized = code.trim().toUpperCase(Locale.ROOT);
return Arrays.stream(values())
.filter(item -> item.code.equals(normalized))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("不支持的scopeMode: " + code));
}
}

View File

@@ -0,0 +1,7 @@
package tech.easyflow.system.mapper;
import com.mybatisflex.core.BaseMapper;
import tech.easyflow.system.entity.SysRoleCategoryScopeItem;
public interface SysRoleCategoryScopeItemMapper extends BaseMapper<SysRoleCategoryScopeItem> {
}

View File

@@ -0,0 +1,7 @@
package tech.easyflow.system.mapper;
import com.mybatisflex.core.BaseMapper;
import tech.easyflow.system.entity.SysRoleCategoryScope;
public interface SysRoleCategoryScopeMapper extends BaseMapper<SysRoleCategoryScope> {
}

View File

@@ -0,0 +1,19 @@
package tech.easyflow.system.service;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import java.math.BigInteger;
import java.util.Set;
public interface CategoryPermissionService {
boolean isCurrentSuperAdmin();
RoleCategoryAccessSnapshot getCurrentAccess(String resourceType);
Set<BigInteger> getCurrentVisibleCategoryIds(String resourceType);
boolean canAccessCategory(String resourceType, BigInteger createdBy, BigInteger categoryId);
void assertCategoryResourceVisible(String resourceType, BigInteger createdBy, BigInteger categoryId, String message);
}

View File

@@ -0,0 +1,16 @@
package tech.easyflow.system.service;
import com.mybatisflex.core.service.IService;
import tech.easyflow.system.entity.SysRoleCategoryScope;
import tech.easyflow.system.entity.vo.SysRoleCategoryScopeDetailVo;
import tech.easyflow.system.entity.vo.SysRoleCategoryScopeItemVo;
import java.math.BigInteger;
import java.util.List;
public interface SysRoleCategoryScopeService extends IService<SysRoleCategoryScope> {
SysRoleCategoryScopeDetailVo getRoleScopeDetail(BigInteger roleId);
void saveRoleScopes(BigInteger roleId, List<SysRoleCategoryScopeItemVo> scopes, BigInteger operatorId);
}

View File

@@ -0,0 +1,115 @@
package tech.easyflow.system.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollectionUtil;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import tech.easyflow.common.constant.Constants;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.SysRole;
import tech.easyflow.system.entity.SysRoleCategoryScope;
import tech.easyflow.system.entity.SysRoleCategoryScopeItem;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.enums.CategoryScopeMode;
import tech.easyflow.system.mapper.SysRoleCategoryScopeItemMapper;
import tech.easyflow.system.mapper.SysRoleCategoryScopeMapper;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.service.SysRoleService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class CategoryPermissionServiceImpl implements CategoryPermissionService {
@Resource
private SysRoleService sysRoleService;
@Resource
private SysRoleCategoryScopeMapper sysRoleCategoryScopeMapper;
@Resource
private SysRoleCategoryScopeItemMapper sysRoleCategoryScopeItemMapper;
@Override
public boolean isCurrentSuperAdmin() {
if (!StpUtil.isLogin()) {
return false;
}
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
return loginAccount != null && isSuperAdmin(loginAccount.getId());
}
@Override
public RoleCategoryAccessSnapshot getCurrentAccess(String resourceType) {
if (!StpUtil.isLogin()) {
return new RoleCategoryAccessSnapshot(resourceType, null, false, true, Collections.emptySet());
}
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
if (loginAccount == null) {
return new RoleCategoryAccessSnapshot(resourceType, null, false, true, Collections.emptySet());
}
BigInteger accountId = loginAccount.getId();
if (isSuperAdmin(accountId)) {
return new RoleCategoryAccessSnapshot(resourceType, accountId, true, true, Collections.emptySet());
}
List<SysRole> roles = sysRoleService.getRolesByAccountId(accountId);
if (CollectionUtil.isEmpty(roles)) {
return new RoleCategoryAccessSnapshot(resourceType, accountId, false, false, Collections.emptySet());
}
List<BigInteger> roleIds = roles.stream().map(SysRole::getId).collect(Collectors.toList());
QueryWrapper scopeWrapper = QueryWrapper.create()
.in(SysRoleCategoryScope::getRoleId, roleIds)
.eq(SysRoleCategoryScope::getResourceType, resourceType);
List<SysRoleCategoryScope> scopes = sysRoleCategoryScopeMapper.selectListByQuery(scopeWrapper);
if (CollectionUtil.isEmpty(scopes)) {
return new RoleCategoryAccessSnapshot(resourceType, accountId, false, false, Collections.emptySet());
}
boolean allAccess = scopes.stream()
.anyMatch(item -> CategoryScopeMode.ALL.getCode().equalsIgnoreCase(item.getScopeMode()));
if (allAccess) {
return new RoleCategoryAccessSnapshot(resourceType, accountId, false, true, Collections.emptySet());
}
List<BigInteger> scopeIds = scopes.stream().map(SysRoleCategoryScope::getId).collect(Collectors.toList());
if (CollectionUtil.isEmpty(scopeIds)) {
return new RoleCategoryAccessSnapshot(resourceType, accountId, false, false, Collections.emptySet());
}
QueryWrapper itemWrapper = QueryWrapper.create().in(SysRoleCategoryScopeItem::getScopeId, scopeIds);
List<SysRoleCategoryScopeItem> items = sysRoleCategoryScopeItemMapper.selectListByQuery(itemWrapper);
Set<BigInteger> categoryIds = items.stream()
.map(SysRoleCategoryScopeItem::getCategoryId)
.collect(Collectors.toCollection(LinkedHashSet::new));
return new RoleCategoryAccessSnapshot(resourceType, accountId, false, false, categoryIds);
}
@Override
public Set<BigInteger> getCurrentVisibleCategoryIds(String resourceType) {
return getCurrentAccess(resourceType).getCategoryIds();
}
@Override
public boolean canAccessCategory(String resourceType, BigInteger createdBy, BigInteger categoryId) {
RoleCategoryAccessSnapshot snapshot = getCurrentAccess(resourceType);
return snapshot.canAccess(createdBy, categoryId);
}
@Override
public void assertCategoryResourceVisible(String resourceType, BigInteger createdBy, BigInteger categoryId, String message) {
if (!canAccessCategory(resourceType, createdBy, categoryId)) {
throw new BusinessException(message == null ? "无权限访问该资源" : message);
}
}
private boolean isSuperAdmin(BigInteger accountId) {
List<SysRole> roles = sysRoleService.getRolesByAccountId(accountId);
return CollectionUtil.isNotEmpty(roles)
&& roles.stream().anyMatch(item -> Constants.SUPER_ADMIN_ROLE_CODE.equals(item.getRoleKey()));
}
}

View File

@@ -0,0 +1,172 @@
package tech.easyflow.system.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.system.entity.SysRoleCategoryScope;
import tech.easyflow.system.entity.SysRoleCategoryScopeItem;
import tech.easyflow.system.entity.vo.SysRoleCategoryScopeDetailVo;
import tech.easyflow.system.entity.vo.SysRoleCategoryScopeItemVo;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.CategoryScopeMode;
import tech.easyflow.system.mapper.SysRoleCategoryScopeItemMapper;
import tech.easyflow.system.mapper.SysRoleCategoryScopeMapper;
import tech.easyflow.system.service.SysRoleCategoryScopeService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class SysRoleCategoryScopeServiceImpl extends ServiceImpl<SysRoleCategoryScopeMapper, SysRoleCategoryScope>
implements SysRoleCategoryScopeService {
@Resource
private SysRoleCategoryScopeMapper sysRoleCategoryScopeMapper;
@Resource
private SysRoleCategoryScopeItemMapper sysRoleCategoryScopeItemMapper;
@Override
public SysRoleCategoryScopeDetailVo getRoleScopeDetail(BigInteger roleId) {
SysRoleCategoryScopeDetailVo detailVo = new SysRoleCategoryScopeDetailVo();
detailVo.setRoleId(roleId);
if (roleId == null) {
detailVo.setScopes(defaultScopes());
return detailVo;
}
QueryWrapper scopeWrapper = QueryWrapper.create().eq(SysRoleCategoryScope::getRoleId, roleId);
List<SysRoleCategoryScope> scopes = list(scopeWrapper);
if (CollectionUtil.isEmpty(scopes)) {
detailVo.setScopes(defaultScopes());
return detailVo;
}
List<BigInteger> scopeIds = scopes.stream().map(SysRoleCategoryScope::getId).collect(Collectors.toList());
Map<BigInteger, List<BigInteger>> scopeItemMap = new LinkedHashMap<>();
if (CollectionUtil.isNotEmpty(scopeIds)) {
QueryWrapper itemWrapper = QueryWrapper.create().in(SysRoleCategoryScopeItem::getScopeId, scopeIds);
List<SysRoleCategoryScopeItem> items = sysRoleCategoryScopeItemMapper.selectListByQuery(itemWrapper);
scopeItemMap = items.stream().collect(Collectors.groupingBy(
SysRoleCategoryScopeItem::getScopeId,
LinkedHashMap::new,
Collectors.mapping(SysRoleCategoryScopeItem::getCategoryId, Collectors.toList())
));
}
Map<String, SysRoleCategoryScope> scopeMap = scopes.stream().collect(Collectors.toMap(
SysRoleCategoryScope::getResourceType,
item -> item,
(left, right) -> left,
LinkedHashMap::new
));
List<SysRoleCategoryScopeItemVo> result = new ArrayList<>();
for (String resourceType : CategoryResourceType.allCodes()) {
SysRoleCategoryScope existing = scopeMap.get(resourceType);
SysRoleCategoryScopeItemVo itemVo = new SysRoleCategoryScopeItemVo();
itemVo.setResourceType(resourceType);
if (existing == null) {
itemVo.setScopeMode(CategoryScopeMode.CUSTOM.getCode());
itemVo.setCategoryIds(new ArrayList<>());
} else {
itemVo.setScopeMode(CategoryScopeMode.from(existing.getScopeMode()).getCode());
itemVo.setCategoryIds(new ArrayList<>(scopeItemMap.getOrDefault(existing.getId(), new ArrayList<>())));
}
result.add(itemVo);
}
detailVo.setScopes(result);
return detailVo;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveRoleScopes(BigInteger roleId, List<SysRoleCategoryScopeItemVo> scopes, BigInteger operatorId) {
QueryWrapper scopeWrapper = QueryWrapper.create().eq(SysRoleCategoryScope::getRoleId, roleId);
List<SysRoleCategoryScope> existingScopes = sysRoleCategoryScopeMapper.selectListByQuery(scopeWrapper);
if (CollectionUtil.isNotEmpty(existingScopes)) {
List<BigInteger> existingScopeIds = existingScopes.stream()
.map(SysRoleCategoryScope::getId)
.collect(Collectors.toList());
if (CollectionUtil.isNotEmpty(existingScopeIds)) {
QueryWrapper itemDeleteWrapper = QueryWrapper.create().in(SysRoleCategoryScopeItem::getScopeId, existingScopeIds);
sysRoleCategoryScopeItemMapper.deleteByQuery(itemDeleteWrapper);
}
sysRoleCategoryScopeMapper.deleteByQuery(scopeWrapper);
}
Date now = new Date();
for (SysRoleCategoryScopeItemVo itemVo : normalizeScopes(scopes)) {
SysRoleCategoryScope scope = new SysRoleCategoryScope();
scope.setRoleId(roleId);
scope.setResourceType(itemVo.getResourceType());
scope.setScopeMode(itemVo.getScopeMode());
scope.setCreated(now);
scope.setCreatedBy(operatorId);
scope.setModified(now);
scope.setModifiedBy(operatorId);
sysRoleCategoryScopeMapper.insert(scope);
if (!CategoryScopeMode.CUSTOM.getCode().equals(itemVo.getScopeMode())
|| CollectionUtil.isEmpty(itemVo.getCategoryIds())) {
continue;
}
for (BigInteger categoryId : itemVo.getCategoryIds()) {
SysRoleCategoryScopeItem scopeItem = new SysRoleCategoryScopeItem();
scopeItem.setScopeId(scope.getId());
scopeItem.setCategoryId(categoryId);
sysRoleCategoryScopeItemMapper.insert(scopeItem);
}
}
}
private List<SysRoleCategoryScopeItemVo> defaultScopes() {
return normalizeScopes(null);
}
private List<SysRoleCategoryScopeItemVo> normalizeScopes(List<SysRoleCategoryScopeItemVo> scopes) {
Map<String, SysRoleCategoryScopeItemVo> scopeMap = new LinkedHashMap<>();
if (CollectionUtil.isNotEmpty(scopes)) {
for (SysRoleCategoryScopeItemVo itemVo : scopes) {
if (itemVo == null) {
continue;
}
String resourceType = CategoryResourceType.from(itemVo.getResourceType()).getCode();
CategoryScopeMode scopeMode = CategoryScopeMode.from(itemVo.getScopeMode());
SysRoleCategoryScopeItemVo normalized = new SysRoleCategoryScopeItemVo();
normalized.setResourceType(resourceType);
normalized.setScopeMode(scopeMode.getCode());
Set<BigInteger> categoryIds = itemVo.getCategoryIds() == null
? new LinkedHashSet<>()
: itemVo.getCategoryIds().stream()
.filter(item -> item != null)
.collect(Collectors.toCollection(LinkedHashSet::new));
normalized.setCategoryIds(new ArrayList<>(categoryIds));
scopeMap.put(resourceType, normalized);
}
}
List<SysRoleCategoryScopeItemVo> result = new ArrayList<>();
for (String resourceType : CategoryResourceType.allCodes()) {
SysRoleCategoryScopeItemVo itemVo = scopeMap.get(resourceType);
if (itemVo == null) {
itemVo = new SysRoleCategoryScopeItemVo();
itemVo.setResourceType(resourceType);
itemVo.setScopeMode(CategoryScopeMode.CUSTOM.getCode());
itemVo.setCategoryIds(new ArrayList<>());
}
result.add(itemVo);
}
return result;
}
}