通过rediss实现用户菜单智能推荐

本人用的框架 SpringCloud+ redis+Oauth2+Security

前言: 整体使用过滤器的思想,获取Request,然后从数据库查到菜单名称和路由以及计算点击次数,最后以list的形式存在redis,设计定时任务,在一定时间后,将redis的数据存在数据库(mysql或者oracle)中。

设计时出现的问题(必看!!):

(一)、因为是微服务框架,所以在设计时想到的是在GateWay使用GlobalFilter对所有服务的请求进行拦截,但是有一个问题是,因为GateWay的pom文件依赖不允许有spring-web也就没办法使用fegin或者其他方式查询数据库,也就获取不到菜单的信息,所以舍弃了这种方法

(二)、那就使用基础模块,让每个服务都去依赖这个模块,就变相的达到了,控制每一个服务的方式。那又没办法想GateWay那样直接实现GlobalFilter拦截所有请求,但是又想到可以将拦截器加在security里,等每次认证结束后,经过过滤器,对请求进行处理,这样就达到了所有的目的

(三)、如果您只是单体的springBoot项目,那就更简单了,直接实现 HandlerInterceptor,然后加到bean里让spring管理

一、先写拦截器内容

java 复制代码
@Slf4j
public class UserFavoriteFunctionFilter extends OncePerRequestFilter {

    // 排除过滤的 uri 地址,nacos自行添加
    private final IgnoreWhiteProperties ignoreWhite;

    public UserFavoriteFunctionFilter(IgnoreWhiteProperties ignoreWhite) {
        this.ignoreWhite = ignoreWhite;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        //  /gAcDeptController/list
        String urlPath = request.getRequestURI();
        log.info("Absolute path:{}", urlPath);
        // 跳过不需要统计的路径
        List<String> whites = ignoreWhite.getWhites();
        if (CollUtil.isEmpty(whites)){
            filterChain.doFilter(request, response);
            return;
        }
        if (StringUtils.matches(urlPath, whites)) {
            log.info("Skip path:{}", urlPath);
            filterChain.doFilter(request, response);
            return;
        }
   RemoteSystemService remoteSystemService = SpringUtils.getBean(RemoteSystemService.class);
        RedisService redisService = SpringUtils.getBean(RedisService.class);
        String prefixKey = "userFavorite:";
        BigDecimal userId = SecurityUtils.getUserId();
        // 获取uri的前半部分
        String[] split = urlPath.split("/");
        String ControllerPath = split[1]; // gAcDeptController
        //  从 G_AC_PERMISSION 查出当前菜单的 perm_no
        ResponseData<String> data = remoteSystemService.getPermNo(ControllerPath);
        if (ObjectUtil.isNull(data)){
            filterChain.doFilter(request, response);
            return;
        }
        String permNo = data.getData();
        // 从redis查询当前的用户菜单点击量
        String key = prefixKey+userId;
        List<clickCountVo> clickCountVos = redisService.getCacheList(key);
        if (CollUtil.isNotEmpty(clickCountVos)){
            Map<String, clickCountVo> clickCountMap = clickCountVos.stream()
                    .collect(Collectors.toMap(
                            clickCountVo::getName,  // 键映射函数
                            vo -> vo            // 值映射函数,直接使用对象本身
                    ));
            clickCountVo clickCountVo = clickCountMap.get(permNo);
            if (ObjectUtil.isNotNull(clickCountVo)) {
                // 当前的点击量
                BigDecimal count = clickCountVo.getCount();
                AtomicLong atomicLong = new AtomicLong(count.longValue());
                long l = atomicLong.incrementAndGet();
                clickCountVo.setCount(new BigDecimal(l));
                clickCountVo.setTime(new Date());
            }else {
                clickCountVo clickVo = new clickCountVo();
                clickVo.setName(permNo);
                clickVo.setTime(new Date());
                clickVo.setCount(BigDecimal.ONE);
                clickCountVos.add(clickVo);
            }
        }else {
            clickCountVo countVo = new clickCountVo();
            countVo.setName(permNo);
            countVo.setTime(new Date());
            countVo.setCount(BigDecimal.ONE);
            clickCountVos.add(countVo);
        }
        redisService.deleteObject(key);
        redisService.setCacheList(key, clickCountVos);
        filterChain.doFilter(request, response);
    }

}

二、创建一个Vo保存菜单信息和点击量

java 复制代码
@Data
public class clickCountVo {

    private String name;

    private BigDecimal count;

    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date time;
}

三、有一些路径我们不需要拦截的,可以在nacos配置一下

java 复制代码
@RefreshScope
@ConfigurationProperties(prefix = "security.ignore")
public class IgnoreWhiteProperties {

    private List<String> whites = new ArrayList<>();

    public List<String> getWhites()
    {
        return whites;
    }

    public void setWhites(List<String> whites)
    {
        this.whites = whites;
    }
}

四、最重要的,将我们自定义的拦截器加到Scurity里,这里还搭配了Oauth2.0

java 复制代码
    @Bean
	@Order(Ordered.HIGHEST_PRECEDENCE)
	SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		AntPathRequestMatcher[] requestMatchers = permitAllUrl.getUrls()
				.stream()
				.map(AntPathRequestMatcher::new)
				.toList()
				.toArray(new AntPathRequestMatcher[] {});

		http.authorizeHttpRequests(authorizeRequests -> authorizeRequests
				.requestMatchers(requestMatchers).permitAll().anyRequest()
				.authenticated())
				.oauth2ResourceServer(
						oauth2 -> oauth2
								.authenticationEntryPoint(resourceAuthExceptionEntryPoint)
								.bearerTokenResolver(starBearerTokenExtractor).jwt())
				.addFilterAfter(new UserFavoriteFunctionFilter(whiteProperties),BearerTokenAuthenticationFilter.class)
				.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
				.csrf(AbstractHttpConfigurer::disable);

		return http.build();
	}

.addFilterAfter(new UserFavoriteFunctionFilter(whiteProperties),BearerTokenAuthenticationFilter.class) 这个是添加我们自定义的过滤器的,熟悉Oauth2.0认证的都熟悉BearerTokenAuthenticationFilter,这个过滤器是当用户每一次想资源服务器请求时都会经过的过滤器,这个过滤器也负责处理token以及将用户认证信息存到SecurityContextHolder中,所以我们在这个过滤器后面加上我们自定义的过滤器UserFavoriteFunctionFilter(这个说起来都是泪,我一点一点debug后才发现 addFilterAfter 这个方法

然后你就找一个既又redis又有springWeb依赖的公共模块,将代码放进去就行了。

后面还有一个定时任务的功能,这个主要是为了防止redis数据太多,我们公司是TOB,基本没有什么用户量,也没有高并发什么的,暂时就没有写这个功能。

附带两个查询的方法,返回前端展示

java 复制代码
@RestController
@RequestMapping("/userClickController")
@Tag(name = "获取用户常用菜单功能")
public class UserClickController {

    @Autowired
    private RedisService redisService;


    @GetMapping("/getTop10Info")
    @Operation(summary = "获取点击量最多的前10个菜单信息")
    public List<clickCountVo> getTop10Info(){
        String  key = "userFavorite:";
        BigDecimal userId = SecurityUtils.getUserId();
        key = key+userId;
        List<clickCountVo> cacheList = redisService.getCacheList(key);
        // 按照点击量排序。如果点击量一样就按照时间排序 都是降序
        return cacheList.stream()
                .sorted(Comparator.comparing(clickCountVo::getCount).reversed().thenComparing(Comparator.comparing(clickCountVo::getTime).reversed()))
                .limit(10)
                .collect(Collectors.toList());
    }

    @GetMapping("/getLastWeekInfo")
    @Operation(summary = "获取最近一周点击量的菜单信息")
    public List<clickCountVo> getLastWeekInfo(){
        String  key = "userFavorite:";
        BigDecimal userId = SecurityUtils.getUserId();
        key = key+userId;
        List<clickCountVo> cacheList = redisService.getCacheList(key);
        if (CollUtil.isNotEmpty(cacheList)){
            // 获取上一周的时间
            DateTime dateTime = DateUtil.lastWeek();
            // 按照点击量排序。如果点击量一样就按照时间排序 都是降序
            return cacheList.stream()
                    .filter(da -> da.getTime().toInstant().isAfter(dateTime.toInstant()))
                    .sorted(Comparator.comparing(clickCountVo::getCount).reversed().thenComparing(Comparator.comparing(clickCountVo::getTime).reversed()))
                    .collect(Collectors.toList());
        }
       return cacheList;
    }

}
相关推荐
yuanbenshidiaos25 分钟前
c++---------数据类型
java·jvm·c++
向宇it29 分钟前
【从零开始入门unity游戏开发之——C#篇25】C#面向对象动态多态——virtual、override 和 base 关键字、抽象类和抽象方法
java·开发语言·unity·c#·游戏引擎
Lojarro42 分钟前
【Spring】Spring框架之-AOP
java·mysql·spring
莫名其妙小饼干1 小时前
网上球鞋竞拍系统|Java|SSM|VUE| 前后端分离
java·开发语言·maven·mssql
isolusion1 小时前
Springboot的创建方式
java·spring boot·后端
zjw_rp1 小时前
Spring-AOP
java·后端·spring·spring-aop
Oneforlove_twoforjob2 小时前
【Java基础面试题033】Java泛型的作用是什么?
java·开发语言
TodoCoder2 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
向宇it2 小时前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
小蜗牛慢慢爬行2 小时前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate