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,13 @@
package tech.easyflow.ai.service;
import java.math.BigInteger;
import java.util.Set;
public interface PluginVisibilityService {
Set<BigInteger> getCurrentVisiblePluginIds();
boolean canAccessPlugin(Long createdBy, BigInteger pluginId);
void assertPluginVisible(Long createdBy, BigInteger pluginId, String message);
}

View File

@@ -41,6 +41,7 @@ import tech.easyflow.common.util.UrlEncoderUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
import tech.easyflow.system.service.CategoryPermissionService;
import javax.annotation.Resource;
import java.math.BigInteger;
@@ -107,6 +108,8 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
private McpService mcpService;
@Resource(name = "default")
FileStorageService storageService;
@Resource
private CategoryPermissionService categoryPermissionService;
@Override
public Bot getDetail(String id) {
@@ -144,6 +147,13 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
if (aiBot == null) {
return ChatSseUtil.sendSystemError(conversationId, "聊天助手不存在");
}
if (StpUtil.isLogin()) {
try {
categoryPermissionService.assertCategoryResourceVisible("BOT", aiBot.getCreatedBy(), aiBot.getCategoryId(), "无权限访问聊天助手");
} catch (BusinessException e) {
return ChatSseUtil.sendSystemError(conversationId, e.getMessage());
}
}
if (aiBot.getModelId() == null) {
return ChatSseUtil.sendSystemError(conversationId, "请配置大模型!");
}

View File

@@ -1,29 +1,39 @@
package tech.easyflow.ai.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.ai.entity.*;
import tech.easyflow.ai.mapper.PluginCategoryMappingMapper;
import tech.easyflow.ai.mapper.PluginMapper;
import tech.easyflow.ai.service.BotPluginService;
import tech.easyflow.ai.service.PluginService;
import org.springframework.stereotype.Service;
import tech.easyflow.ai.service.PluginItemService;
import tech.easyflow.ai.service.PluginService;
import tech.easyflow.ai.service.PluginVisibilityService;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.service.CategoryPermissionService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
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;
import static tech.easyflow.ai.entity.table.PluginTableDef.PLUGIN;
/**
* 服务层实现。
*
@@ -46,6 +56,10 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
@Resource
private PluginItemService pluginItemService;
@Resource
private CategoryPermissionService categoryPermissionService;
@Resource
private PluginVisibilityService pluginVisibilityService;
@Override
public Plugin savePlugin(Plugin plugin) {
@@ -104,22 +118,39 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
@Override
public Result<Page<Plugin>> pageByCategory(Long pageNumber, Long pageSize, int category) {
// 通过分类查询插件
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("PLUGIN");
QueryWrapper queryWrapper = QueryWrapper.create().select(PluginCategoryMapping::getPluginId)
.eq(PluginCategoryMapping::getCategoryId, category);
// 分页查询该分类中的插件
Page<BigInteger> pagePluginIds = pluginCategoryMappingMapper.paginateAs(new Page<>(pageNumber, pageSize), queryWrapper, BigInteger.class);
Page<PluginCategoryMapping> paginateCategories = pluginCategoryMappingMapper.paginate(pageNumber, pageSize, queryWrapper);
List<Plugin> plugins = Collections.emptyList();
if (paginateCategories.getRecords().isEmpty()) {
return Result.ok(new Page<>(plugins, pageNumber, pageSize, paginateCategories.getTotalRow()));
List<BigInteger> allCategoryPluginIds = pluginCategoryMappingMapper.selectListByQueryAs(queryWrapper, BigInteger.class)
.stream()
.filter(item -> item != null)
.collect(Collectors.toCollection(ArrayList::new));
if (CollectionUtil.isEmpty(allCategoryPluginIds)) {
return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, 0L));
}
List<BigInteger> pluginIds = pagePluginIds.getRecords();
// 查询对应的插件信息
QueryWrapper queryPluginWrapper = QueryWrapper.create().select()
.in(Plugin::getId, pluginIds);
plugins = pluginMapper.selectListByQuery(queryPluginWrapper);
Page<Plugin> aiPluginPage = new Page<>(plugins, pageNumber, pageSize, paginateCategories.getTotalRow());
List<BigInteger> orderedCategoryPluginIds = new ArrayList<>(new LinkedHashSet<>(allCategoryPluginIds));
List<BigInteger> visiblePluginIds = orderedCategoryPluginIds;
if (access.isRestricted()) {
Set<BigInteger> visiblePluginIdSet = new LinkedHashSet<>(pluginVisibilityService.getCurrentVisiblePluginIds());
List<BigInteger> creatorPluginIds = queryCreatorPluginIds(orderedCategoryPluginIds, access.getAccountIdAsLong());
LinkedHashSet<BigInteger> mergedVisibleIds = orderedCategoryPluginIds.stream()
.filter(pluginId -> visiblePluginIdSet.contains(pluginId) || creatorPluginIds.contains(pluginId))
.collect(Collectors.toCollection(LinkedHashSet::new));
visiblePluginIds = new ArrayList<>(mergedVisibleIds);
}
if (visiblePluginIds.isEmpty()) {
return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, 0L));
}
int fromIndex = Math.max(0, Math.toIntExact((pageNumber - 1) * pageSize));
if (fromIndex >= visiblePluginIds.size()) {
return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, visiblePluginIds.size()));
}
int toIndex = Math.min(visiblePluginIds.size(), Math.toIntExact(fromIndex + pageSize));
List<BigInteger> currentPagePluginIds = new ArrayList<>(visiblePluginIds.subList(fromIndex, toIndex));
List<Plugin> plugins = queryPluginsByIds(currentPagePluginIds);
Page<Plugin> aiPluginPage = new Page<>(plugins, pageNumber, pageSize, visiblePluginIds.size());
return Result.ok(aiPluginPage);
}
@@ -129,5 +160,37 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
return true;
}
private List<BigInteger> queryCreatorPluginIds(List<BigInteger> pluginIds, Long creatorId) {
if (CollectionUtil.isEmpty(pluginIds) || creatorId == null) {
return Collections.emptyList();
}
QueryWrapper creatorPluginWrapper = QueryWrapper.create().select(Plugin::getId)
.in(Plugin::getId, pluginIds)
.eq(Plugin::getCreatedBy, creatorId);
return pluginMapper.selectListByQueryAs(creatorPluginWrapper, BigInteger.class);
}
private List<Plugin> queryPluginsByIds(List<BigInteger> pluginIds) {
if (CollectionUtil.isEmpty(pluginIds)) {
return Collections.emptyList();
}
QueryWrapper queryPluginWrapper = QueryWrapper.create().select().in(Plugin::getId, pluginIds);
List<Plugin> plugins = pluginMapper.selectListByQuery(queryPluginWrapper);
Map<BigInteger, Plugin> pluginMap = plugins.stream().collect(Collectors.toMap(
Plugin::getId,
item -> item,
(left, right) -> left,
LinkedHashMap::new
));
List<Plugin> orderedPlugins = new ArrayList<>();
for (BigInteger pluginId : pluginIds) {
Plugin plugin = pluginMap.get(pluginId);
if (plugin != null) {
orderedPlugins.add(plugin);
}
}
return orderedPlugins;
}
}

View File

@@ -0,0 +1,69 @@
package tech.easyflow.ai.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import tech.easyflow.ai.entity.PluginCategoryMapping;
import tech.easyflow.ai.mapper.PluginCategoryMappingMapper;
import tech.easyflow.ai.service.PluginVisibilityService;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.service.CategoryPermissionService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@Service
public class PluginVisibilityServiceImpl implements PluginVisibilityService {
@Resource
private CategoryPermissionService categoryPermissionService;
@Resource
private PluginCategoryMappingMapper pluginCategoryMappingMapper;
@Override
public Set<BigInteger> getCurrentVisiblePluginIds() {
RoleCategoryAccessSnapshot snapshot = categoryPermissionService.getCurrentAccess("PLUGIN");
if (!snapshot.isRestricted() || CollectionUtil.isEmpty(snapshot.getCategoryIds())) {
return Collections.emptySet();
}
QueryWrapper mappingWrapper = QueryWrapper.create()
.select(PluginCategoryMapping::getPluginId)
.in(PluginCategoryMapping::getCategoryId, snapshot.getCategoryIds());
List<BigInteger> pluginIds = pluginCategoryMappingMapper.selectListByQueryAs(mappingWrapper, BigInteger.class);
return new LinkedHashSet<>(pluginIds);
}
@Override
public boolean canAccessPlugin(Long createdBy, BigInteger pluginId) {
RoleCategoryAccessSnapshot snapshot = categoryPermissionService.getCurrentAccess("PLUGIN");
if (!snapshot.isRestricted()) {
return true;
}
if (createdBy != null && snapshot.getAccountId() != null
&& snapshot.getAccountId().equals(BigInteger.valueOf(createdBy))) {
return true;
}
if (CollectionUtil.isEmpty(snapshot.getCategoryIds())) {
return false;
}
QueryWrapper mappingWrapper = QueryWrapper.create()
.select(PluginCategoryMapping::getPluginId)
.eq(PluginCategoryMapping::getPluginId, pluginId)
.in(PluginCategoryMapping::getCategoryId, snapshot.getCategoryIds());
List<BigInteger> pluginIds = pluginCategoryMappingMapper.selectListByQueryAs(mappingWrapper, BigInteger.class);
return CollectionUtil.isNotEmpty(pluginIds);
}
@Override
public void assertPluginVisible(Long createdBy, BigInteger pluginId, String message) {
if (!canAccessPlugin(createdBy, pluginId)) {
throw new BusinessException(message == null ? "无权限访问该资源" : message);
}
}
}