diff --git a/tyr-api/src/main/java/cn/axzo/tyr/client/feign/FeatureResourceApi.java b/tyr-api/src/main/java/cn/axzo/tyr/client/feign/FeatureResourceApi.java index bd9abd36..41c228c0 100644 --- a/tyr-api/src/main/java/cn/axzo/tyr/client/feign/FeatureResourceApi.java +++ b/tyr-api/src/main/java/cn/axzo/tyr/client/feign/FeatureResourceApi.java @@ -1,6 +1,7 @@ package cn.axzo.tyr.client.feign; import cn.axzo.framework.domain.web.result.ApiResult; +import cn.axzo.tyr.client.model.req.GetFeatureResourceTreeReq; import cn.axzo.tyr.client.model.req.ResourceSyncReq; import cn.axzo.tyr.client.model.req.FeatureResourceTreeSaveReq; import cn.axzo.tyr.client.model.res.FeatureResourceTreeNode; @@ -35,6 +36,10 @@ public interface FeatureResourceApi { @PostMapping("/api/featureResource/saveOrUpdate") ApiResult saveMenu(@RequestBody FeatureResourceTreeSaveReq req); + /** 查询功能资源树 **/ + @PostMapping("/api/featureResource/getTree") + ApiResult> getTree(@RequestBody @Valid GetFeatureResourceTreeReq req); + /** 删除菜单/页面/组件 **/ @PostMapping("/api/featureResource/delete") ApiResult deleteFeatureResource(@RequestParam Long featureId, @RequestParam Long operatorId); diff --git a/tyr-api/src/main/java/cn/axzo/tyr/client/model/req/GetFeatureResourceTreeReq.java b/tyr-api/src/main/java/cn/axzo/tyr/client/model/req/GetFeatureResourceTreeReq.java new file mode 100644 index 00000000..2c2438d3 --- /dev/null +++ b/tyr-api/src/main/java/cn/axzo/tyr/client/model/req/GetFeatureResourceTreeReq.java @@ -0,0 +1,30 @@ +package cn.axzo.tyr.client.model.req; + +import lombok.*; + +import java.util.List; + +/** + * @author likunpeng + * @version 1.0 + * @date 2024/4/9 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class GetFeatureResourceTreeReq { + + /** 查询搜索关键字 **/ + private String keyword; + + /** 端 **/ + private String terminal; + + /** 展示状态 默认不传返回全部 0-隐藏 1-显示 **/ + private Integer status; + + /** feature类型列表 **/ + private List featureTypes; +} diff --git a/tyr-server/src/main/java/cn/axzo/tyr/server/TyrApplication.java b/tyr-server/src/main/java/cn/axzo/tyr/server/TyrApplication.java index e07a81da..9df76d7a 100644 --- a/tyr-server/src/main/java/cn/axzo/tyr/server/TyrApplication.java +++ b/tyr-server/src/main/java/cn/axzo/tyr/server/TyrApplication.java @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.Environment; @@ -11,6 +12,7 @@ import org.springframework.scheduling.annotation.EnableAsync; @Slf4j @EnableAsync +@EnableCaching @EnableDiscoveryClient @MapperScan(value = {"cn.axzo.tyr.server.repository.mapper"}) @SpringBootApplication(scanBasePackages = "cn.axzo") diff --git a/tyr-server/src/main/java/cn/axzo/tyr/server/controller/permission/FeatureResourceController.java b/tyr-server/src/main/java/cn/axzo/tyr/server/controller/permission/FeatureResourceController.java index 7c551a11..d9971be9 100644 --- a/tyr-server/src/main/java/cn/axzo/tyr/server/controller/permission/FeatureResourceController.java +++ b/tyr-server/src/main/java/cn/axzo/tyr/server/controller/permission/FeatureResourceController.java @@ -3,6 +3,7 @@ package cn.axzo.tyr.server.controller.permission; import cn.axzo.framework.domain.web.result.ApiResult; import cn.axzo.tyr.client.feign.FeatureResourceApi; import cn.axzo.tyr.client.model.req.FeatureResourceTreeSaveReq; +import cn.axzo.tyr.client.model.req.GetFeatureResourceTreeReq; import cn.axzo.tyr.client.model.req.ResourceSyncReq; import cn.axzo.tyr.client.model.res.FeatureResourceTreeNode; import cn.axzo.tyr.server.service.FeatureResourceSyncService; @@ -72,4 +73,9 @@ public class FeatureResourceController implements FeatureResourceApi { return null; } + + @Override + public ApiResult> getTree(GetFeatureResourceTreeReq req) { + return ApiResult.ok(featureResourceService.getTree(req)); + } } diff --git a/tyr-server/src/main/java/cn/axzo/tyr/server/repository/dao/SaasFeatureResourceDao.java b/tyr-server/src/main/java/cn/axzo/tyr/server/repository/dao/SaasFeatureResourceDao.java index e421cdc0..89382aa8 100644 --- a/tyr-server/src/main/java/cn/axzo/tyr/server/repository/dao/SaasFeatureResourceDao.java +++ b/tyr-server/src/main/java/cn/axzo/tyr/server/repository/dao/SaasFeatureResourceDao.java @@ -1,10 +1,17 @@ package cn.axzo.tyr.server.repository.dao; +import cn.axzo.basics.common.constant.enums.TableIsDeleteEnum; +import cn.axzo.tyr.client.model.req.GetFeatureResourceTreeReq; import cn.axzo.tyr.server.repository.entity.SaasFeatureResource; import cn.axzo.tyr.server.repository.mapper.SaasFeatureResourceMapper; +import cn.azxo.framework.common.utils.StringUtils; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Objects; + /** *

* 功能资源表 服务实现类 @@ -23,4 +30,17 @@ public class SaasFeatureResourceDao extends ServiceImpl getByResourceTreeParam(GetFeatureResourceTreeReq req) { + return this.lambdaQuery().select(SaasFeatureResource::getId, SaasFeatureResource::getFeatureCode, + SaasFeatureResource::getFeatureName, SaasFeatureResource::getFeatureType, + SaasFeatureResource::getTerminal, SaasFeatureResource::getParentId, + SaasFeatureResource::getDisplayOrder) + .eq(SaasFeatureResource::getIsDelete, TableIsDeleteEnum.NORMAL.value) + .eq(StringUtils.isNotBlank(req.getTerminal()), SaasFeatureResource::getTerminal, req.getTerminal()) + .eq(Objects.nonNull(req.getStatus()), SaasFeatureResource::getStatus, req.getStatus()) + .in(CollectionUtils.isNotEmpty(req.getFeatureTypes()), SaasFeatureResource::getFeatureType, req.getFeatureTypes()) + .list(); + } + } diff --git a/tyr-server/src/main/java/cn/axzo/tyr/server/service/SaasFeatureResourceService.java b/tyr-server/src/main/java/cn/axzo/tyr/server/service/SaasFeatureResourceService.java index ffa14802..f14a04c0 100644 --- a/tyr-server/src/main/java/cn/axzo/tyr/server/service/SaasFeatureResourceService.java +++ b/tyr-server/src/main/java/cn/axzo/tyr/server/service/SaasFeatureResourceService.java @@ -1,5 +1,7 @@ package cn.axzo.tyr.server.service; +import cn.axzo.tyr.client.model.req.GetFeatureResourceTreeReq; +import cn.axzo.tyr.client.model.res.FeatureResourceTreeNode; import cn.axzo.tyr.server.model.ResourcePermission; import cn.axzo.tyr.server.model.ResourcePermissionQueryDTO; import cn.axzo.tyr.server.repository.entity.SaasFeatureResource; @@ -36,5 +38,8 @@ public interface SaasFeatureResourceService { /** 是否免授权 **/ boolean isAuthFree(Long featureId); + /** 查询资源树 **/ + List getTree(GetFeatureResourceTreeReq req); + SaasFeatureResource getByCode(String featureCode); } diff --git a/tyr-server/src/main/java/cn/axzo/tyr/server/service/impl/SaasFeatureResourceCacheService.java b/tyr-server/src/main/java/cn/axzo/tyr/server/service/impl/SaasFeatureResourceCacheService.java new file mode 100644 index 00000000..e57bbb5d --- /dev/null +++ b/tyr-server/src/main/java/cn/axzo/tyr/server/service/impl/SaasFeatureResourceCacheService.java @@ -0,0 +1,31 @@ +package cn.axzo.tyr.server.service.impl; + +import cn.axzo.tyr.client.model.req.GetFeatureResourceTreeReq; +import cn.axzo.tyr.server.repository.dao.SaasFeatureResourceDao; +import cn.axzo.tyr.server.repository.entity.SaasFeatureResource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @author likunpeng + * @version 1.0 + * @date 2024/4/9 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class SaasFeatureResourceCacheService { + public static final String CACHE_FEATURE_RESOURCE_TREE = "featureResource:tree"; + + private final SaasFeatureResourceDao featureResourceDao; + + @Cacheable(value = CACHE_FEATURE_RESOURCE_TREE, key = "#req.keyword + '_' + #req.terminal+ '_' + #req.featureTypes", unless = "#result.isEmpty()") + public List getByResourceTreeParam (GetFeatureResourceTreeReq req) { + log.info("get feature resource tree has not user cache!"); + return featureResourceDao.getByResourceTreeParam(req); + } +} diff --git a/tyr-server/src/main/java/cn/axzo/tyr/server/service/impl/SaasFeatureResourceServiceImpl.java b/tyr-server/src/main/java/cn/axzo/tyr/server/service/impl/SaasFeatureResourceServiceImpl.java index 3429c952..cd30454e 100644 --- a/tyr-server/src/main/java/cn/axzo/tyr/server/service/impl/SaasFeatureResourceServiceImpl.java +++ b/tyr-server/src/main/java/cn/axzo/tyr/server/service/impl/SaasFeatureResourceServiceImpl.java @@ -1,7 +1,8 @@ package cn.axzo.tyr.server.service.impl; import cn.axzo.basics.common.BeanMapper; -import cn.axzo.basics.common.model.IBaseTree; +import cn.axzo.basics.common.util.StopWatchUtil; +import cn.axzo.basics.common.util.TreeUtil; import cn.axzo.pokonyan.config.mybatisplus.BaseEntity; import cn.axzo.pokonyan.config.redis.RedisClient; import cn.axzo.tyr.client.common.enums.FeatureResourceAuthType; @@ -9,27 +10,32 @@ import cn.axzo.tyr.client.common.enums.FeatureResourceStatus; import cn.axzo.tyr.client.common.enums.FeatureResourceType; import cn.axzo.tyr.client.model.req.FeatureComponentSaveReq; import cn.axzo.tyr.client.model.req.FeatureResourceTreeSaveReq; +import cn.axzo.tyr.client.model.req.GetFeatureResourceTreeReq; +import cn.axzo.tyr.client.model.res.FeatureResourceDTO; +import cn.axzo.tyr.client.model.res.FeatureResourceTreeNode; import cn.axzo.tyr.server.model.ResourcePermission; import cn.axzo.tyr.server.model.ResourcePermissionQueryDTO; import cn.axzo.tyr.server.repository.dao.SaasFeatureResourceDao; import cn.axzo.tyr.server.repository.entity.SaasFeatureResource; import cn.axzo.tyr.server.service.SaasFeatureResourceService; +import cn.azxo.framework.common.utils.StringUtils; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.BooleanUtil; -import io.swagger.models.auth.In; +import com.google.common.collect.Lists; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; import java.util.Collections; import java.util.Comparator; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; @@ -46,9 +52,19 @@ import static cn.axzo.tyr.server.common.constants.CacheConstant.KEY_AUTH_FREE; @Slf4j @Service @RequiredArgsConstructor +@RefreshScope public class SaasFeatureResourceServiceImpl implements SaasFeatureResourceService { private final SaasFeatureResourceDao featureResourceDao; + private final SaasFeatureResourceCacheService saasFeatureResourceCacheService; + + /** + * 功能资源树根节点配置 + * key < - > TerminalInfo定义的terminal + * value < - > 端描述 + */ + @Value("#{${featureResourceTreeRootNodeMap:{'NT_CMS_WEB_GENERAL':'CMS端', 'NT_OMS_WEB':'OMS端'}}}") + private Map treeRootNodeMap; @Override public List listNavByIds(List featureIds) { @@ -108,6 +124,40 @@ public class SaasFeatureResourceServiceImpl implements SaasFeatureResourceServic } @Override + public List getTree(GetFeatureResourceTreeReq req) { + List rootNodes = treeRootNodeMap.entrySet().stream().map(e -> FeatureResourceTreeNode.builder() + .id(0L).terminal(e.getKey()).featureName(e.getValue()).children(Lists.newArrayList()).build()).collect(Collectors.toList()); + + StopWatchUtil watch = StopWatchUtil.createStarted("feature-resource-tree"); + watch.start("dbQuery"); + List saasFeatureResources = saasFeatureResourceCacheService.getByResourceTreeParam(req); + watch.stop(); + if (CollectionUtils.isEmpty(saasFeatureResources)) { + return Collections.emptyList(); + } + + List treeList = TreeUtil.buildTree(saasFeatureResources.stream() + .map(this::featureResource2Node) + .sorted(Comparator.comparing(FeatureResourceDTO::getDisplayOrder)) + .collect(Collectors.toList())); + saasFeatureResources = null; // help GC + + //搜索或需要按授权策略过滤 - 有额外的过滤条件 + watch.start("filter-node"); + List filterResultNode = filterTreeNode(req.getKeyword(), treeList); + watch.stop(); + + //把处理后的树结构添加上根节点 + watch.start("fill-children-to-root"); + List resultNode = fillChildren2Root(rootNodes, filterResultNode); + watch.stop(); + + log.info("feature-resource-tree cost:{} , ms:{}", watch.prettyPrint(), watch.getTotalTimeMillis()); + return resultNode; + } + + @Override + @CacheEvict(value = SaasFeatureResourceCacheService.CACHE_FEATURE_RESOURCE_TREE,allEntries = true) public void saveOrUpdateMenu(FeatureResourceTreeSaveReq req) { SaasFeatureResource baseResource = BeanMapper.copyBean(req, SaasFeatureResource.class); baseResource.setUpdateBy(req.getOperatorId()); @@ -258,4 +308,52 @@ public class SaasFeatureResourceServiceImpl implements SaasFeatureResourceServic featureResourceDao.updateBatchById(parallelFeature); } + + private FeatureResourceTreeNode featureResource2Node(SaasFeatureResource featureResource) { + return FeatureResourceTreeNode.builder() + .id(featureResource.getId()) + .featureCode(featureResource.getFeatureCode()) + .featureName(featureResource.getFeatureName()) + .featureType(featureResource.getFeatureType()) + .terminal(featureResource.getTerminal()) + .parentId(featureResource.getParentId()) + .displayOrder(featureResource.getDisplayOrder()) + .build(); + } + + private List filterTreeNode(String keyword, List treeList) { + if (StringUtils.isBlank(keyword)) { + return treeList; + } + + return treeList.stream().filter(x -> this.recursionFilter(keyword, x)).collect(Collectors.toList()); + } + + private boolean recursionFilter(String keyword, FeatureResourceTreeNode node) { + boolean matched = node.getFeatureName().contains(keyword); + if (CollectionUtils.isEmpty(node.getNodeChildren())) { + return matched; + } + // 过滤子节点 + List filterChildren = node.getChildren().stream().filter(x -> recursionFilter(keyword, x)).collect(Collectors.toList()); + // 重置子节点 + node.setChildren(filterChildren); + if (CollectionUtils.isEmpty(filterChildren)) { + return matched; + } + return true; + } + + private List fillChildren2Root(List rootNodes, List childrenNodes) { + Map rootNodeMap = rootNodes.stream().collect(Collectors.toMap(FeatureResourceDTO::getTerminal, Function.identity(), (v1, v2) -> v1)); + for(FeatureResourceTreeNode child : childrenNodes) { + FeatureResourceTreeNode rootNode = rootNodeMap.get(child.getTerminal()); + if (child.getParentId() > 0 || Objects.isNull(rootNode)) { + continue; + } + rootNode.getChildren().add(child); + } + + return rootNodes; + } } diff --git a/tyr-server/src/main/resources/bootstrap.yml b/tyr-server/src/main/resources/bootstrap.yml index 9bf74eb2..9a6a3cb1 100644 --- a/tyr-server/src/main/resources/bootstrap.yml +++ b/tyr-server/src/main/resources/bootstrap.yml @@ -1,4 +1,6 @@ spring: + cache: + type: redis application: name: tyr cloud: