KekeBlog项目实战后台模块(一)(已完结)

一、后台模块-准备工作

1. 前端工程启动

前端工程下载链接

https://pan.baidu.com/s/1TdFs4TqxlHh4DXyLwYuejQ 
提取码:mfkw

项目sql文件下载链接

链接:https://pan.baidu.com/s/1DQCGN4wISSDlOkqnVWYwxA 
提取码:mfkw

命令行进入keke-vue-admin文件夹

依次执行

npm install
npm run dev

   

后台的前端工程启动完毕 

2. 前端工程启动bug解决

这里后台的前端工程启动如果启动失败,大概率是node的版本过高,建议换至14.的版本,我这里用的是node 14.21.3版本

下载链接:

node 14.21.3版本下载

如果下载出现安装失败问题,可以参见我这个博客,里面有详细的解决方案

node重装-解铃还须系铃人-CSDN博客

3. 后台模块准备工作

第一步: 在keke-admin工程的src/main/java目录新建com.keke.BlogAdminApplication类,作为引导类,写入如下

package com.keke;


import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.keke.mapper")
public class BlogAdminApplication {
     public static void main(String[] args) {
          SpringApplication.run(BlogAdminApplication.class,args);
     }
}

第二步: 在keke-admin工程的resources目录新建File,文件名为application.yml文件,写入如下

server:
  port: 8989

spring:
  # 数据库连接信息
  datasource:
    url: jdbc:mysql://localhost:3306/keke_blog?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password:
    driver-class-name: com.mysql.cj.jdbc.Driver

  servlet:
    # 文件上传
    multipart:
      # 单个上传文件的最大允许大小
      max-file-size: 20MB
      # HTTP请求中包含的所有文件的总大小的最大允许值
      max-request-size: 20MB

mybatis-plus:
  #  configuration:
  #    # 日志
  #    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      # 逻辑删除的字段
      logic-delete-field: delFlag
      # 代表已删除的值
      logic-delete-value: 1
      # 代表未删除的值
      logic-not-delete-value: 0
      # 主键自增策略,以mysql数据库为准
      id-type: auto

第三步: 在keke-framework工程的src/main/java/com.keke.domain.entity目录新建Tag类,写入如下

package com.keke.domain.entity;

import java.util.Date;
import java.io.Serializable;

import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableName;

/**
 * 标签(Tag)表实体类
 *
 * @author makejava
 * @since 2023-10-18 10:20:44
 */
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("ke_tag")
public class Tag {
    private Long id;
    //标签名
    private String name;
    
    private Long createBy;
    
    private Date createTime;
    
    private Long updateBy;
    
    private Date updateTime;
    //删除标志(0代表未删除,1代表已删除)
    private Integer delFlag;
    //备注
    private String remark;

}

第四步:在keke-framework工程的src/main/java/com.keke.mapper目录新建TagMapper接口,写入如下

package com.keke.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.keke.domain.entity.Tag;


/**
 * 标签(Tag)表数据库访问层
 *
 * @author makejava
 * @since 2023-10-18 10:21:07
 */
public interface TagMapper extends BaseMapper<Tag> {

}

第五步: 在keke-framework工程的src/main/java/com.keke.service目录新建TagService接口,写入如下

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.entity.Tag;


/**
 * 标签(Tag)表服务接口
 *
 * @author makejava
 * @since 2023-10-18 10:21:06
 */
public interface TagService extends IService<Tag> {

}

第六步: 在keke-framework工程的src/main/java/com.keke.service目录新建impl.TagServiceImpl类,写入如下

package com.keke.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.domain.entity.Tag;
import com.keke.mapper.TagMapper;
import com.keke.service.TagService;
import org.springframework.stereotype.Service;

/**
 * 标签(Tag)表服务实现类
 *
 * @author makejava
 * @since 2023-10-18 10:21:07
 */
@Service("tagService")
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {

}

第七步: 在keke-admin工程的src/main/java目录新建com.keke.controller.TagController类,写入如下

package com.keke.controller;

import com.keke.domain.ResponseResult;
import com.keke.domain.entity.Tag;
import com.keke.service.TagService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/tag")
public class TagController {

     @Autowired
     private TagService tagService;

     @GetMapping("/test")
     public ResponseResult test(){
          List<Tag> list = tagService.list();
          return ResponseResult.okResult(list);
     }
}

第八步: 由于huanf-framework公共模块里面有security的相关依赖和配置,为了让 '博客后台模块' 在启动时不报错,我们需要把keke-blog工程的security的相关代码提前写道keke-admin工程里面,这些代码我们在huanf-blog工程里面已经学过了,只是简单地在huanf-admin工程里面也弄(复制)一份这样的代码。

在keke-admin工程的src/main/java/com.keke目录新建filter.JwtAuthenticationTokenFilter类,写入如下。不用管下面的代码什么意思,登录功能的时候会学,注意这里的key为login:

package com.keke.filter;

import com.alibaba.fastjson.JSON;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.LoginUser;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.utils.JwtUtil;
import com.keke.utils.RedisCache;
import com.keke.utils.WebUtils;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;


@Component
//博客前台的登录认证过滤器。OncePerRequestFilter是springsecurity提供的类
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    //RedisCache是我们在keke-framework工程写的工具类,用于操作redis
    private RedisCache redisCache;

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

        //获取请求头中的token值
        String token = request.getHeader("token");
        //判断上面那行有没有拿到token值
        if(!StringUtils.hasText(token)){
            //说明该接口不需要登录,直接放行,不拦截
            filterChain.doFilter(request,response);
            return;
        }
        //JwtUtil是我们在keke-framework工程写的工具类。解析获取的token,把原来的密文解析为原文
        Claims claims = null;
        try {
            claims = JwtUtil.parseJWT(token);
        } catch (Exception e) {
            //当token过期或token被篡改就会进入下面那行的异常处理
            e.printStackTrace();
            //报异常之后,把异常响应给前端,需要重新登录。ResponseResult、AppHttpCodeEnum、WebUtils是我们在keke-framework工程写的类
            ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
            WebUtils.renderString(response, JSON.toJSONString(result));
            return;
        }
        String userid = claims.getSubject();

        //在redis中,通过key来获取value,注意key我们是加过前缀的,取的时候也要加上前缀
        LoginUser loginUser = redisCache.getCacheObject("login:" + userid);
        //如果在redis获取不到值,说明登录是过期了,需要重新登录
        if(Objects.isNull(loginUser)){
            ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
            WebUtils.renderString(response, JSON.toJSONString(result));
            return;
        }

        //把从redis获取到的value,存入到SecurityContextHolder(Security官方提供的类)
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        filterChain.doFilter(request,response);

    }
}

在keke-admin工程的src/main/java/com.keke目录新建config.SecurityConfig类,写入如下。不用管下面的代码什么意思,登录功能的时候会学

package com.keke.config;

import com.keke.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
//WebSecurityConfigurerAdapter是Security官方提供的类
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //注入我们在keke-blog工程写的JwtAuthenticationTokenFilter过滤器
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    AccessDeniedHandler accessDeniedHandler;



    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    //把官方的PasswordEncoder密码加密方式替换成BCryptPasswordEncoder
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
//                .antMatchers("/login").anonymous()
//这里新增必须要是登录状态才能访问退出登录的接口,即是认证过的状态
//                .antMatchers("/logout").authenticated()
             //   为方便测试认证过滤器,我们把查询友链的接口设置为需要登录才能访问。然后我们去访问的时候就能测试登录认证功能了
             //   .antMatchers("/link/getAllLink").authenticated()
                //
//                .antMatchers("/user/userInfo").authenticated()
                // 除上面外的所有请求全部不需要认证即可访问
                .anyRequest().permitAll();

        //配置我们自己写的认证和授权的异常处理
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);



        http.logout().disable();
        //将自定义filter加入security过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //允许跨域
        http.cors();
    }

}

第九步: 运行keke-admin工程的BlogAdminApplication类。在postman软件,使用GET请求访问如下

二、后台模块-登录功能

后台跟前台模块,共用一个sys_user表,所以这里的实现方式,和之前我们在前台模块的实现差不多

1. 接口分析

使用SpringSecurity安全框架来实现登录功能,并且实现登录的校验,也就是把数据库的用户表跟页面输入的用户名密码做比较

使用SpringSecurity安全框架来实现登录功能,并且实现登录的校验,也就是把数据库的用户表跟页面输入的用户名密码做比较

请求方式

请求路径

POST

/user/login

请求体:

{
    "userName":"用户名",
    "password":"密码"
}

响应体:

{
    "code": 200,
    "data": {
        "token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0ODBmOThmYmJkNmI0NjM0OWUyZjY2NTM0NGNjZWY2NSIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTY0Mzg3NDMxNiwiZXhwIjoxNjQzOTYwNzE2fQ.ldLBUvNIxQCGemkCoMgT_0YsjsWndTg5tqfJb77pabk"
    },
    "msg": "操作成功"
}

2. 思路分析

登录

①自定义登录接口

调用ProviderManager的方法进行认证 如果认证通过生成jwt

把用户信息存入redis中

②自定义UserDetailsService(之前在前台模块的时候写的UserDetailsService)

在这个实现类中去查询数据库

注意配置passwordEncoder为BCryptPasswordEncoder

校验:

①定义Jwt认证过滤器(之前在前台模块的时候写的JwtAuthenticationTokenFilter)

获取token

解析token获取其中的userid

从redis中获取用户信息

存入SecurityContextHolder

3. 相关依赖

<!--SpringSecurity启动器。需要用到登录功能就解开,不然就注释掉-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!--redis依赖-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--fastjson依赖-->
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>

<!--jwt依赖-->
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt</artifactId>
	<version>0.9.0</version>
</dependency>

4.  登录+校验的代码实现

第一步: 在keke-framework工程的service目录新建SystemLoginService接口,写入如下

package com.keke.service;

import com.keke.domain.ResponseResult;
import com.keke.domain.entity.User;

public interface SystemLoginService {
     ResponseResult login(User user);
}

第二步: 在keke-framework工程的service目录新建impl.SystemLoginServiceImpl类,写入如下

package com.keke.service.impl;

import com.keke.domain.ResponseResult;
import com.keke.domain.entity.LoginUser;
import com.keke.domain.entity.User;
import com.keke.service.BlogLoginService;
import com.keke.service.SystemLoginService;
import com.keke.utils.JwtUtil;
import com.keke.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;


@Service
public class SystemLoginServiceImpl implements SystemLoginService {

     @Autowired
     private AuthenticationManager authenticationManager;

     @Autowired
     private RedisCache redisCache;

     @Override
     public ResponseResult login(User user) {
          UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
          Authentication authenticate = authenticationManager.authenticate(authenticationToken);
          //authenticationManager会默认调用UserDetailsService从内存中进行用户认证,我们实际需求是从数据库,因此我们要重新创建一个UserDetailsService的实现类
          //判断是否认证通过
          if(Objects.isNull(authenticate)){
               throw new RuntimeException("用户名或者密码错误");
          }
          //获取Userid,生成token
          LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
          String userId = loginUser.getUser().getId().toString();
          String jwt = JwtUtil.createJWT(userId);
          //把用户信息存入redis
          redisCache.setCacheObject("login:" + userId,loginUser);
          //把token和userInfo封装返回,因为响应回去的data有这两个属性,所以要封装Vo
          Map<String,String> systemLoginVo = new HashMap<>();
          systemLoginVo.put("token",jwt);
          return ResponseResult.okResult(systemLoginVo);
     }
}

第三步: 把keke-admin工程的SecurityConfig类修改为如下,为了测试校验功能,我们把接口设置为只有通过登录校验才能访问,注意要放行登录接口,其他接口均需认证

package com.keke.config;

import com.keke.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
//WebSecurityConfigurerAdapter是Security官方提供的类
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //注入我们在keke-blog工程写的JwtAuthenticationTokenFilter过滤器
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    AccessDeniedHandler accessDeniedHandler;



    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    //把官方的PasswordEncoder密码加密方式替换成BCryptPasswordEncoder
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                 //对于后台登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                //其他接口均需要认证
                .anyRequest().authenticated();

        //配置我们自己写的认证和授权的异常处理
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);



        http.logout().disable();
        //将自定义filter加入security过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //允许跨域
        http.cors();
    }

}

第四步: 在keke-admin工程的controller目录新建LoginController类,写入如下

package com.keke.controller;


import com.keke.annotation.KekeSystemLog;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.User;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.handler.exception.exception.SystemException;
import com.keke.service.BlogLoginService;
import com.keke.service.SystemLoginService;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Api(tags = "用户登录相关接口")
public class LoginController {

     @Autowired
     private SystemLoginService systemLoginService;

     @PostMapping("/user/login")
     @KekeSystemLog(businessName = "用户登录")
     public ResponseResult login(@RequestBody User user){
          if(!StringUtils.hasText(user.getUserName())){
               //提示必须要传用户名
               throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);
          }
          return systemLoginService.login(user);
     }

}

第五步: 本地打开你的redis,postman

post请求,请求参数如下

{
    "userName":"HFuser",
    "password":"123"
}

可以看到控制台输出的日志信息

再拿着token去访问之前写的/test接口,可以看到访问成功

并且不携带token,接口无法访问

那这样我们登录校验的功能就实现完毕了,跟前台的实现方式几乎一样

三、后台模块-权限控制

1. 接口分析

接口设计。对应用户只能使用自己的权限所允许使用的功能

请求方式

请求地址

请求头

GET

/getInfo

需要token请求头

响应格式如下。如果用户id为1代表管理员,roles 中只需要有admin,permissions中需要有所有菜单类型为C或者F的,状态为正常的,未被删除的权限

{
	"code":200,
	"data":{
		"permissions":[
			"system:user:list",
            "system:role:list",
			"system:menu:list",
			"system:user:query",
			"system:user:add"
            //此次省略1000字
		],
		"roles":[
			"admin"
		],
		"user":{
			"avatar":"http://r7yxkqloa.bkt.clouddn.com/2022/03/05/75fd15587811443a9a9a771f24da458d.png",
			"email":"23412332@qq.com",
			"id":1,
			"nickName":"sg3334",
			"sex":"1"
		}
	},
	"msg":"操作成功"
}

之前在SpringSecurity的学习中就使用过RBAC权限模型。这里我们就是在RBAC权限模型的基础上去实现这个功能

RBAC权限模型最重要最难的就是设计好下面的5张表,有了5张表之后,就是简单的连表查询了

2. 权限表的字段

3. 角色表的字段

4. 用户表的字段

5. 中间表-角色&用户

6. 中间表-角色&权限

7. 代码实现

权限表中权限类型中的M表示目录,目录其实不会进行页面跳转,所以不需要处理

权限控制其实就是Menu表,对应的是后台中的菜单和按钮,如果有这些菜单和按钮的权限,就可以进行相应的操作

第一步: 把keke-framework工程的SystemCanstants类修改为如下,增加了两个判断权限类型的成员变量

package com.keke.constants;


//字面值(代码中的固定值)处理,把字面值都在这里定义成常量
public class SystemConstants {

    /**
     * 文章是草稿
     */
    public static final int ARTICLE_STATUS_DRAFT = 1;

    /**
     * 文章是正常发布状态
     */
    public static final int ARTICLE_STATUS_NORMAL = 0;

    /**
     * 文章列表当前查询页数
     */
    public static final int ARTICLE_STATUS_CURRENT = 1;

    /**
     * 文章列表每页显示的数据条数
     */
    public static final int ARTICLE_STATUS_SIZE = 10;

    /**
     * 分类表的分类状态是正常状态
     */
    public static final String STATUS_NORMAL = "0";

    /**
     * 友联审核通过
     */
    public static final String Link_STATUS_NORMAL = "0";

    /**
     * 评论区的某条评论是根评论
     */
    public static final String COMMENT_ROOT = "-1";

    /**
     * 文章评论
     */
    public static final String ARTICLE_COMMENT = "0";

    /**
     * 友链评论
     */
    public static final String LINK_COMMENT = "1";

    /**
     * redis中的文章浏览量key
     */
    public static final String REDIS_ARTICLE_KEY = "article:viewCount";

    /**
     * 浏览量自增1
     */
    public static final int REDIS_ARTICLE_VIEW_COUNT_INCREMENT = 1;


    /**
     * 菜单权限
     */
    public static final String MENU = "C";

    /**
     * 按钮权限
     */
    public static final String BUTTON = "F";


}

第二步: 在keke-framework工程的domain/vo目录新建AdminUserInfoVo类,写入如下,负责把指定字段返回给前端

package com.keke.domain.vo;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AdminUserInfoVo {
     private List<String> permissions;
     private List<String> roles;
     private UserInfoVo user;
}

第三步: 在keke-framework工程的domain/entity目录新建Menu类,写入如下,让mybatisplus去查询我们的sys_menu权限表

package com.keke.domain.entity;

import java.util.Date;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableName;

/**
 * 菜单权限表(Menu)表实体类
 *
 * @author makejava
 * @since 2023-10-18 20:55:24
 */
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("sys_menu")
public class Menu {
    //菜单ID
    private Long id;
    //菜单名称
    private String menuName;
    //父菜单ID
    private Long parentId;
    //显示顺序
    private Integer orderNum;
    //路由地址
    private String path;
    //组件路径
    private String component;
    //是否为外链(0是 1否)
    private Integer isFrame;
    //菜单类型(M目录 C菜单 F按钮)
    private String menuType;
    //菜单状态(0显示 1隐藏)
    private String visible;
    //菜单状态(0正常 1停用)
    private String status;
    //权限标识
    private String perms;
    //菜单图标
    private String icon;
    //创建者
    private Long createBy;
    //创建时间
    private Date createTime;
    //更新者
    private Long updateBy;
    //更新时间
    private Date updateTime;
    //备注
    private String remark;
    
    private String delFlag;

}

第四步: 在keke-framework工程的domain目录新建Role类,写入如下,让mybatisplus去查询我们的sys_role角色表

package com.keke.domain.entity;

import java.util.Date;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableName;

/**
 * 角色信息表(Role)表实体类
 *
 * @author makejava
 * @since 2023-10-18 21:03:52
 */
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("sys_role")
public class Role {
    //角色ID
    private Long id;
    //角色名称
    private String roleName;
    //角色权限字符串
    private String roleKey;
    //显示顺序
    private Integer roleSort;
    //角色状态(0正常 1停用)
    private String status;
    //删除标志(0代表存在 1代表删除)
    private String delFlag;
    //创建者
    private Long createBy;
    //创建时间
    private Date createTime;
    //更新者
    private Long updateBy;
    //更新时间
    private Date updateTime;
    //备注
    private String remark;

}

第五步: 在keke-framework工程的service目录新建RoleService接口,写入如下,用于查询用户的角色信息

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.entity.Role;


/**
 * 角色信息表(Role)表服务接口
 *
 * @author makejava
 * @since 2023-10-18 21:04:06
 */
public interface RoleService extends IService<Role> {

}

第六步: 在keke-framework工程的service目录新建impl.RoleServiceImpl类,写入如下,是查询用户的角色信息的具体代码

package com.keke.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.domain.entity.Role;
import com.keke.mapper.RoleMapper;
import com.keke.service.RoleService;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * 角色信息表(Role)表服务实现类
 *
 * @author makejava
 * @since 2023-10-18 21:04:06
 */
@Service("roleService")
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {


     //根据用户id查询角色信息
     @Override
     public List<String> selectRoleKeyByUserId(Long userId) {
          //如果userId为1,那么角色权限字符串就只需要返回一个admin
          if(userId==1L){
               List<String> roles = new ArrayList<>();
               roles.add("admin");
               return roles;
          }
          //如果用户id不为1,那么需要根据userId连表查询对应的roleId,然后再去角色表中去查询
          //对应的角色权限字符串
          //这里我们期望RoleMapper中封装一个方法去帮我们实现这个复杂的操作
          RoleMapper roleMapper = getBaseMapper();
          return roleMapper.selectRoleKeyByUserId(userId);
     }
}

第七步: 在keke-framework工程的service目录新建MenuService接口,写入如下,用于查询超级管理员的权限信息

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.entity.Menu;

import java.util.List;


/**
 * 菜单权限表(Menu)表服务接口
 *
 * @author makejava
 * @since 2023-10-18 20:55:48
 */
public interface MenuService extends IService<Menu> {


     List<String> selectPermsByUserId(Long userId);
}

第八步: 在keke-framework工程的service目录新建impl.MenuServiceImpl类,写入如下,是查询用户的权限信息的具体代码

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.constants.SystemConstants;
import com.keke.domain.entity.Menu;
import com.keke.mapper.MenuMapper;
import com.keke.service.MenuService;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 菜单权限表(Menu)表服务实现类
 *
 * @author makejava
 * @since 2023-10-18 20:55:48
 */
@Service("menuService")
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {



     //根据用户id查询权限关键字
     @Override
     public List<String> selectPermsByUserId(Long userId) {
          //如果用户id为1代表管理员,roles 中只需要有admin,
          // permissions中需要有所有菜单类型为C或者F的,状态为正常的,未被删除的权限
          if(userId==1L) {
               LambdaQueryWrapper<Menu> lambdaQueryWrapper = new LambdaQueryWrapper<>();
               lambdaQueryWrapper.in(Menu::getMenuType, SystemConstants.MENU, SystemConstants.BUTTON);
               lambdaQueryWrapper.eq(Menu::getStatus, SystemConstants.STATUS_NORMAL);
               //由于我们的逻辑删除字段已经配置了,所以无需封装lambdaQueryWrapper
               List<Menu> menuList = list(lambdaQueryWrapper);
               //我们需要的是String类型的集合,这里我们要进行数据的处理,这里采用流的方式
               List<String> permissions = menuList.stream()
                       .map(new Function<Menu, String>() {
                            @Override
                            public String apply(Menu menu) {
                                 String perms = menu.getPerms();
                                 return perms;
                            }
                       })
                       .collect(Collectors.toList());
               return permissions;
          }
          //否则返回这个用户所具有的权限
          //这里我们需要进行连表查询,因为我们的用户先和角色关联,然后角色才跟权限关联
          MenuMapper menuMapper = getBaseMapper();
          //我们期望menuMapper中有一个方法可以直接帮我们去实现这个复杂的逻辑,这里直接返回
          return menuMapper.selectPermsByUserId(userId);
     }
}

第九步: 在huanf-framework工程的mapper目录新建MenuMapper接口,写入如下,封装查询非超级管理员的权限信息的具体逻辑

package com.keke.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.keke.domain.entity.Menu;

import java.util.List;


/**
 * 菜单权限表(Menu)表数据库访问层
 *
 * @author makejava
 * @since 2023-10-18 20:55:48
 */
public interface MenuMapper extends BaseMapper<Menu> {

     //Mapper的实现类对应xml映射文件
     List<String> selectPermsByUserId(Long userId);
}

第十步: 在keke-framework工程的resources目录新建mapper/MenuMapper.xml文件,写入如下,查询非超级管理员的权限信息的具体逻辑,即sql语句

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.keke.mapper.MenuMapper">
    <select id="selectPermsByUserId" resultType="java.lang.String">
-- 这里的逻辑是先用userId连表查询roleId,再用roleId连表查询menuId,再根据menuId
-- 查询对应的用户权限
            select
                m.`perms`
            from `sys_user_role` ur
                     left join `sys_role_menu` rm on ur.`role_id`=rm.`role_id`
                     left join `sys_menu` m on m.`id`=rm.`menu_id`
            where
                ur.`user_id`= #{userId} and
                m.`menu_type` in ('C','F') and
                m.`status`=0 and
                m.`del_flag`=0
    </select>
</mapper>

第十一步: 在keke-framework工程的mapper目录新建RoleMapper文件,写入如下,用于查询用户的角色信息

package com.keke.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.keke.domain.entity.Role;

import java.util.List;


/**
 * 角色信息表(Role)表数据库访问层
 *
 * @author makejava
 * @since 2023-10-18 21:04:06
 */
public interface RoleMapper extends BaseMapper<Role> {


     List<String> selectRoleKeyByUserId(Long userId);
}

第十二步: 在keke-framework工程的resources/mapper目录新建RoleMapper.xml文件,写入如下,是查询用户的角色信息的具体代码,即实现功能的复杂sql语句

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.keke.mapper.RoleMapper">
    <select id="selectRoleKeyByUserId" resultType="java.lang.String">
        select
            `sys_role`.role_key
        from `sys_user_role`
                 left join `sys_role` on `sys_user_role`.role_id=`sys_role`.id
        where `sys_user_role`.user_id=#{useId}
          and `sys_role`.status=0
          and	`sys_role`.del_flag=0
    </select>
</mapper>

第十三步: 把keke-admin工程的LoginController类修改为如下,增加了查询角色信息、权限信息的接口

package com.keke.controller;


import com.keke.annotation.KekeSystemLog;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.LoginUser;
import com.keke.domain.entity.User;
import com.keke.domain.vo.AdminUserInfoVo;
import com.keke.domain.vo.UserInfoVo;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.handler.exception.exception.SystemException;
import com.keke.service.BlogLoginService;
import com.keke.service.MenuService;
import com.keke.service.RoleService;
import com.keke.service.SystemLoginService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.SecurityUtils;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@Api(tags = "用户登录相关接口")
public class LoginController {

     @Autowired
     private SystemLoginService systemLoginService;

     @Autowired
     private MenuService menuService;

     @Autowired
     private RoleService roleService;

     @PostMapping("/user/login")
     @KekeSystemLog(businessName = "后台用户登录")
     public ResponseResult login(@RequestBody User user){
          if(!StringUtils.hasText(user.getUserName())){
               //提示必须要传用户名
               throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);
          }
          return systemLoginService.login(user);
     }

     @GetMapping("/getInfo")
     public ResponseResult<AdminUserInfoVo> getInfo(){
          //获取当前登录用户,用我们封装的SecurityUtils
          LoginUser loginUser = SecurityUtils.getLoginUser();
          //根据用户id查询权限信息
          Long userId = loginUser.getUser().getId();
          List<String> permissions = menuService.selectPermsByUserId(userId);
          //根据用户id查询角色信息
          List<String> roles = roleService.selectRoleKeyByUserId(userId);
          //获取userInfo信息
          User user = loginUser.getUser();
          UserInfoVo userInfoVo = BeanCopyUtils.copyBean(user, UserInfoVo.class);
          //创建Vo,封装返回
          AdminUserInfoVo adminUserInfoVo = new AdminUserInfoVo(permissions,roles,userInfoVo);
          return ResponseResult.okResult(adminUserInfoVo);
     }

}

第十四步: 测试本地打开你的redis,postman

首先登录,拿到token

{
    "userName":"sg",
    "password":"1234"
}

然后我们访问 /getInfo 接口

至此,后台的权限控制接口就实现了,整体来说还是比较复杂的,但是我们把每一步都分解开来,一步一步的去完成,完善,就可以去实现 

四、后台模块-动态路由

实现了这个动态路由功能之后,就能在浏览器web页面登录进博客管理后台了

1. 接口分析

后台系统需要能实现不同的用户权限可以看到不同的功能,即左侧的导航栏

请求方式

请求地址

请求头

GET

/getRouters

需要token请求头

响应格式如下: 前端为了实现动态路由的效果,需要后端有接口能返回用户所能访问的菜单数据。注意: 返回的菜单数据需要体现父子菜单的层级关系

如果用户id为1代表管理员,menus中需要有所有菜单类型为C或者M的,C表示菜单,M表示目录,状态为正常的,未被删除的权限

注意这里不返回F的原因是按钮不属于菜单管理,所以接口不用返回

响应体如下:

{
	"code":200,
	"data":{
		"menus":[
			{
				"children":[],
				"component":"content/article/write/index",
				"createTime":"2022-01-08 11:39:58",
				"icon":"build",
				"id":2023,
				"menuName":"写博文",
				"menuType":"C",
				"orderNum":"0",
				"parentId":0,
				"path":"write",
				"perms":"content:article:writer",
				"status":"0",
				"visible":"0"
			},
			{
				"children":[
					{
						"children":[],
						"component":"system/user/index",
						"createTime":"2021-11-12 18:46:19",
						"icon":"user",
						"id":100,
						"menuName":"用户管理",
						"menuType":"C",
						"orderNum":"1",
						"parentId":1,
						"path":"user",
						"perms":"system:user:list",
						"status":"0",
						"visible":"0"
					},
					{
						"children":[],
						"component":"system/role/index",
						"createTime":"2021-11-12 18:46:19",
						"icon":"peoples",
						"id":101,
						"menuName":"角色管理",
						"menuType":"C",
						"orderNum":"2",
						"parentId":1,
						"path":"role",
						"perms":"system:role:list",
						"status":"0",
						"visible":"0"
					},
					{
						"children":[],
						"component":"system/menu/index",
						"createTime":"2021-11-12 18:46:19",
						"icon":"tree-table",
						"id":102,
						"menuName":"菜单管理",
						"menuType":"C",
						"orderNum":"3",
						"parentId":1,
						"path":"menu",
						"perms":"system:menu:list",
						"status":"0",
						"visible":"0"
					}
				],
				"createTime":"2021-11-12 18:46:19",
				"icon":"system",
				"id":1,
				"menuName":"系统管理",
				"menuType":"M",
				"orderNum":"1",
				"parentId":0,
				"path":"system",
				"perms":"",
				"status":"0",
				"visible":"0"
			}
		]
	},
	"msg":"操作成功"
}

2. 代码实现

第一步: 在huanf-framework工程的vo目录新建RoutersVo类,写入如下,负责把指定字段返回给前端

package com.keke.domain.vo;

import com.keke.domain.entity.Menu;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;
import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RoutersVo {
     List<Menu> menus;
}

第二步: 把keke-framework工程的Menu类修改为如下,增加了children字段(成员变量)、增加了

@Accessors(chain = true)注解

package com.keke.domain.entity;

import java.util.Date;
import java.io.Serializable;
import java.util.List;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.experimental.Accessors;

/**
 * 菜单权限表(Menu)表实体类
 *
 * @author makejava
 * @since 2023-10-18 20:55:24
 */
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@TableName("sys_menu")
public class Menu {
    //菜单ID
    private Long id;
    //菜单名称
    private String menuName;
    //父菜单ID
    private Long parentId;
    //显示顺序
    private Integer orderNum;
    //路由地址
    private String path;
    //组件路径
    private String component;
    //是否为外链(0是 1否)
    private Integer isFrame;
    //菜单类型(M目录 C菜单 F按钮)
    private String menuType;
    //菜单状态(0显示 1隐藏)
    private String visible;
    //菜单状态(0正常 1停用)
    private String status;
    //权限标识
    private String perms;
    //菜单图标
    private String icon;
    //创建者
    private Long createBy;
    //创建时间
    private Date createTime;
    //更新者
    private Long updateBy;
    //更新时间
    private Date updateTime;
    //备注
    private String remark;
    
    private String delFlag;

    private List<Menu> children;

}

第三步: 把keke-framework工程的MenuService接口修改为如下,增加了查询用户的路由信息(权限菜单)的接口

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.entity.Menu;

import java.util.List;


/**
 * 菜单权限表(Menu)表服务接口
 *
 * @author makejava
 * @since 2023-10-18 20:55:48
 */
public interface MenuService extends IService<Menu> {


     //查询用户权限信息
     List<String> selectPermsByUserId(Long userId);

     //查询用户的路由信息,也就是权限菜单
     List<Menu> selectRouterMenuTreeByUserId(Long userId);
}

第四步: 把keke-framework工程的MenuServiceImpl类修改为如下,增加了查询用户的路由信息(权限菜单)的具体代码

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.constants.SystemConstants;
import com.keke.domain.entity.Menu;
import com.keke.mapper.MenuMapper;
import com.keke.service.MenuService;
import com.keke.utils.SecurityUtils;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 菜单权限表(Menu)表服务实现类
 *
 * @author makejava
 * @since 2023-10-18 20:55:48
 */
@Service("menuService")
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {



     //根据用户id查询权限关键字
     @Override
     public List<String> selectPermsByUserId(Long userId) {
          //如果用户id为1代表管理员,roles 中只需要有admin,
          // permissions中需要有所有菜单类型为C(菜单)或者F(按钮)的,状态为正常的,未被删除的权限
          if(SecurityUtils.isAdmin()) {
               LambdaQueryWrapper<Menu> lambdaQueryWrapper = new LambdaQueryWrapper<>();
               lambdaQueryWrapper.in(Menu::getMenuType, SystemConstants.MENU, SystemConstants.BUTTON);
               lambdaQueryWrapper.eq(Menu::getStatus, SystemConstants.STATUS_NORMAL);
               //由于我们的逻辑删除字段已经配置了,所以无需封装lambdaQueryWrapper
               List<Menu> menuList = list(lambdaQueryWrapper);
               //我们需要的是String类型的集合,这里我们要进行数据的处理,这里采用流的方式
               List<String> permissions = menuList.stream()
                       .map(new Function<Menu, String>() {
                            @Override
                            public String apply(Menu menu) {
                                 String perms = menu.getPerms();
                                 return perms;
                            }
                       })
                       .collect(Collectors.toList());
               return permissions;
          }
          //否则返回这个用户所具有的权限
          //这里我们需要进行连表查询,因为我们的用户先和角色关联,然后角色才跟权限关联
          MenuMapper menuMapper = getBaseMapper();
          //我们期望menuMapper中有一个方法可以直接帮我们去实现这个复杂的逻辑,这里直接返回
          return menuMapper.selectPermsByUserId(userId);
     }

     @Override
     public List<Menu> selectRouterMenuTreeByUserId(Long userId) {
          MenuMapper menuMapper = getBaseMapper();
          List<Menu> menus = null;
          //如果是管理员,返回所有
          if(SecurityUtils.isAdmin()){
               menus = menuMapper.selectAllRoutersMenu();
          }else {
               //如果不是管理员,返回对应用户的菜单
               menus = menuMapper.selectRoutersMenuTreeByUserId(userId);
          }
          //因为上面的查询都是从数据库进行查询,所以无法封装children,这里构建Tree

          List<Menu> menuTree = buildMenuTree(menus,0L);
          return menuTree;
     }

     /**
      * 构建MenuTree
      * 思路先找第一层级的菜单,就是找到id于parentId的对应关系,然后把parentId设置为Id的children
      * @param menus
      * @return
      */
     private List<Menu> buildMenuTree(List<Menu> menus,Long parentId) {
          //转化流处理
          List<Menu> menuTree = menus.stream()
                  //过滤掉除一级菜单之外的菜单
                  .filter(menu -> menu.getParentId().equals(parentId))
                  //然后将获取其子菜单设置到children字段,并返回
                  .map(m -> m.setChildren(gerChildren(m, menus)))
                  .collect(Collectors.toList());
          return menuTree;
     }

     //获取当前菜单的子菜单
     private List<Menu> gerChildren(Menu menu, List<Menu> menus) {
          //流处理,遍历每一个流对象,筛选出流对象的parentId=menu的id,即过滤
          List<Menu> children = menus.stream()
                  .filter(m -> m.getParentId().equals(menu.getId()))
                  //这里其实不必要写,这一步的逻辑是如果有三级,
                  //可以把流对象中再过筛选出子菜单设置给对应的children并返回
                  .map(m -> m.setChildren(gerChildren(m,menus)))
                  .collect(Collectors.toList());
          return children;
     }
}

第五步: 把keke-framework工程的MenuMapper接口修改为如下,增加了2个(一个查超级管理员,另一个查普通用户)查询权限菜单的接口

package com.keke.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.keke.domain.entity.Menu;

import java.util.List;


/**
 * 菜单权限表(Menu)表数据库访问层
 *
 * @author makejava
 * @since 2023-10-18 20:55:48
 */
public interface MenuMapper extends BaseMapper<Menu> {

     //Mapper的实现类对应xml映射文件
     List<String> selectPermsByUserId(Long userId);

     List<Menu> selectAllRoutersMenu();

     List<Menu> selectRoutersMenuTreeByUserId(Long userId);

}

第六步: 把keke-framework工程的resources/mapper目录下的MenuMapper.xml文件修改为如下,是查询权限菜单的具体代码

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.keke.mapper.MenuMapper">
    <select id="selectPermsByUserId" resultType="java.lang.String">
-- 这里的逻辑是先用userId连表查询roleId,再用roleId连表查询menuId,再根据menuId
-- 查询对应的用户权限
            select
                DISTINCT m.`perms`
            from `sys_user_role` ur
                     left join `sys_role_menu` rm on ur.`role_id`=rm.`role_id`
                     left join `sys_menu` m on m.`id`=rm.`menu_id`
            where
                ur.`user_id`=#{userId} and
                m.`menu_type` in ('C','F') and
                m.`status`=0 and
                m.`del_flag`=0
    </select>
    <select id="selectAllRoutersMenu" resultType="com.keke.domain.entity.Menu">
--         这里与上面的sql差不多,只是menu_type有差别,还有查询的字段个数
        SELECT
            DISTINCT m.id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status,
                     IFNULL(m.perms,'') AS perms, m.is_frame,  m.menu_type, m.icon, m.order_num, m.create_time
        FROM
            `sys_menu` m
        WHERE
--             查询所有的,所以不需要加userId的条件
            m.`menu_type` IN ('C','M') AND
            m.`status` = 0 AND
            m.`del_flag` = 0
        ORDER BY
            m.parent_id,m.order_num
    </select>

    <select id="selectRoutersMenuTreeByUserId" resultType="com.keke.domain.entity.Menu">
--         这里与上面的sql差不多
        SELECT
            DISTINCT m.id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status,
                     IFNULL(m.perms,'') AS perms, m.is_frame,  m.menu_type, m.icon, m.order_num, m.create_time
        FROM
            `sys_user_role` ur
                LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
                LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
        WHERE
            ur.`user_id` = #{userId} AND
            m.`menu_type` IN ('C','M') AND
            m.`status` = 0 AND
            m.`del_flag` = 0
        ORDER BY
            m.parent_id,m.order_num
    </select>

</mapper>

第七步: 把keke-admin工程的LoginController类修改为如下,增加了查询路由信息(权限菜单)的接口

package com.keke.controller;


import com.keke.annotation.KekeSystemLog;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.LoginUser;
import com.keke.domain.entity.Menu;
import com.keke.domain.entity.User;
import com.keke.domain.vo.AdminUserInfoVo;
import com.keke.domain.vo.RoutersVo;
import com.keke.domain.vo.UserInfoVo;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.handler.exception.exception.SystemException;
import com.keke.service.MenuService;
import com.keke.service.RoleService;
import com.keke.service.SystemLoginService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.SecurityUtils;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@Api(tags = "后台用户登录相关接口")
public class LoginController {

     @Autowired
     private SystemLoginService systemLoginService;

     @Autowired
     private MenuService menuService;

     @Autowired
     private RoleService roleService;

     @PostMapping("/user/login")
     @KekeSystemLog(businessName = "后台用户登录")
     public ResponseResult login(@RequestBody User user){
          if(!StringUtils.hasText(user.getUserName())){
               //提示必须要传用户名
               throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);
          }
          return systemLoginService.login(user);
     }

     @GetMapping("getInfo")
     public ResponseResult<AdminUserInfoVo> getInfo(){
          //获取当前登录用户,用我们封装的SecurityUtils
          LoginUser loginUser = SecurityUtils.getLoginUser();
          //根据用户id查询权限信息
          Long userId = loginUser.getUser().getId();
          List<String> permissions = menuService.selectPermsByUserId(userId);
          //根据用户id查询角色信息
          List<String> roles = roleService.selectRoleKeyByUserId(userId);
          //获取userInfo信息
          User user = loginUser.getUser();
          UserInfoVo userInfoVo = BeanCopyUtils.copyBean(user, UserInfoVo.class);
          //创建Vo,封装返回
          AdminUserInfoVo adminUserInfoVo = new AdminUserInfoVo(permissions,roles,userInfoVo);
          return ResponseResult.okResult(adminUserInfoVo);
     }



     @GetMapping("/getRouters")
     public ResponseResult<RoutersVo> getRouters(){
          //获取用户id
          Long userId = SecurityUtils.getUserId();
          //查询menu,结果是tree的形式
          List<Menu> menus = menuService.selectRouterMenuTreeByUserId(userId);
          //封装返回
          RoutersVo routersVo = new RoutersVo(menus);
          return ResponseResult.okResult(routersVo);
     }

}

第八步: 测试

打开redis,启动前端工程,登录后可以看到左侧菜单路由展示出来了,以及一些权限按钮

这里启动前台工程,测试可能是不通过的,可以参见以下问题

AdminUserInfoVo的变量名是否与要求返回的字段一致

前端工程中store目录下的modules目录下的permission.js文件中处理子路由的时候push写成了psuh,导致路由不能渲染。看看你浏览器控制台有没有报psuh的错误,有的话就是这里的问题。

补充

如果测试失误,可以把token删除,然后重新登录测试

五、后台模块-退出登录

1. 接口分析

删除redis中的用户信息

请求方式

请求地址

请求头

POST

/user/logout

需要token请求头

{
    "code": 200,
    "msg": "操作成功"
}

2. 代码实现

第一步: 把keke-framework工程的SystemLoginService接口修改为如下,增加了退出登录的接口

package com.keke.service;

import com.keke.domain.ResponseResult;
import com.keke.domain.entity.User;

public interface SystemLoginService {

     //后台用户登录
     ResponseResult login(User user);

     //后台用户退出登录
     ResponseResult logout();

}

第二步: 把keke-framework工程的SystemLoginServiceImpl类修改为如下,增加了退出登录的具体代码

package com.keke.service.impl;

import com.keke.domain.ResponseResult;
import com.keke.domain.entity.LoginUser;
import com.keke.domain.entity.User;
import com.keke.service.BlogLoginService;
import com.keke.service.SystemLoginService;
import com.keke.utils.JwtUtil;
import com.keke.utils.RedisCache;
import com.keke.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;


@Service
public class SystemLoginServiceImpl implements SystemLoginService {

     @Autowired
     private AuthenticationManager authenticationManager;

     @Autowired
     private RedisCache redisCache;

     @Override
     public ResponseResult login(User user) {
          UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
          Authentication authenticate = authenticationManager.authenticate(authenticationToken);
          //authenticationManager会默认调用UserDetailsService从内存中进行用户认证,我们实际需求是从数据库,因此我们要重新创建一个UserDetailsService的实现类
          //判断是否认证通过
          if(Objects.isNull(authenticate)){
               throw new RuntimeException("用户名或者密码错误");
          }
          //获取Userid,生成token
          LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
          String userId = loginUser.getUser().getId().toString();
          String jwt = JwtUtil.createJWT(userId);
          //把用户信息存入redis
          redisCache.setCacheObject("login:" + userId,loginUser);
          //把token和userInfo封装返回,因为响应回去的data有这两个属性,所以要封装Vo
          Map<String,String> systemLoginVo = new HashMap<>();
          systemLoginVo.put("token",jwt);
          return ResponseResult.okResult(systemLoginVo);
     }

     @Override
     public ResponseResult logout() {
          //删除redis中的登录信息
          Long userId = SecurityUtils.getUserId();
          redisCache.deleteObject("login:" + userId);
          return ResponseResult.okResult();
     }
}

第三步: 那keke-admin工程的LoginController类修改为如下,增加了退出登录的访问接口

package com.keke.controller;


import com.keke.annotation.KekeSystemLog;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.LoginUser;
import com.keke.domain.entity.Menu;
import com.keke.domain.entity.User;
import com.keke.domain.vo.AdminUserInfoVo;
import com.keke.domain.vo.RoutersVo;
import com.keke.domain.vo.UserInfoVo;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.handler.exception.exception.SystemException;
import com.keke.service.MenuService;
import com.keke.service.RoleService;
import com.keke.service.SystemLoginService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.SecurityUtils;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@Api(tags = "后台用户登录相关接口")
public class LoginController {

     @Autowired
     private SystemLoginService systemLoginService;

     @Autowired
     private MenuService menuService;

     @Autowired
     private RoleService roleService;

     @PostMapping("/user/login")
     @KekeSystemLog(businessName = "后台用户登录")
     public ResponseResult login(@RequestBody User user){
          if(!StringUtils.hasText(user.getUserName())){
               //提示必须要传用户名
               throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);
          }
          return systemLoginService.login(user);
     }

     @GetMapping("getInfo")
     public ResponseResult<AdminUserInfoVo> getInfo(){
          //获取当前登录用户,用我们封装的SecurityUtils
          LoginUser loginUser = SecurityUtils.getLoginUser();
          //根据用户id查询权限信息
          Long userId = loginUser.getUser().getId();
          List<String> permissions = menuService.selectPermsByUserId(userId);
          //根据用户id查询角色信息
          List<String> roles = roleService.selectRoleKeyByUserId(userId);
          //获取userInfo信息
          User user = loginUser.getUser();
          UserInfoVo userInfoVo = BeanCopyUtils.copyBean(user, UserInfoVo.class);
          //创建Vo,封装返回
          AdminUserInfoVo adminUserInfoVo = new AdminUserInfoVo(permissions,roles,userInfoVo);
          return ResponseResult.okResult(adminUserInfoVo);
     }



     @GetMapping("/getRouters")
     public ResponseResult<RoutersVo> getRouters(){
          //获取用户id
          Long userId = SecurityUtils.getUserId();
          //查询menu,结果是tree的形式
          List<Menu> menus = menuService.selectRouterMenuTreeByUserId(userId);
          //封装返回
          RoutersVo routersVo = new RoutersVo(menus);
          return ResponseResult.okResult(routersVo);
     }

     @PostMapping("/user/logout")
     public ResponseResult logout(){
          return systemLoginService.logout();
     }

}

第四步: 测试

本地打开你的redis

本地打开postman

运行后台工程

postman测试

启动前端工程,在前端工程中测试如下

退出登录成功,回到登录页面

六、后台模块-标签列表

1. 查询标签

1.1 标签表的字段

1.2 接口分析 

为了方便后期对文章进行管理,需要提供标签的功能,一个文章可以有多个标签。在后台需要分页查询标签功能,要求能根据标签名进行分页查询对应的文章

注意:不能把删除了的标签查询出来。除了可以根据标签名查询文章,后期还要添加根据备注名查询文章

请求方式

请求路径

Get

content/tag/list

请求参数 

Query格式请求参数:

pageNum: 页码

pageSize: 每页条数

name:标签名

remark:备注

响应格式

{
	"code":200,
	"data":{
		"rows":[
			{
				"id":4,
				"name":"Java",
				"remark":"sdad"
			}
		],
		"total":1
	},
	"msg":"操作成功"
}

1.3 代码实现

第一步: 在keke-framework工程的src/main/java目录新建com.huanf.dto.TagListDto类,写入如下

package com.keke.domain.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class TagListDto {
     //标签名
     private String name;
     //备注
     private String remark;
}

第二步, 在keke-framework工程的domain/vo目录下创建TagVo,写入如下,主要作用是响应正确

package com.keke.domain.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class TagVo {
     //id
     private Long id;
     //标签名
     private String name;
     //备注
     private String remark;
}

第三步: 在keke-framework工程的service目录新建TagService接口,写入如下,用于查询标签

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;


/**
 * 标签(Tag)表服务接口
 *
 * @author makejava
 * @since 2023-10-18 10:21:06
 */
public interface TagService extends IService<Tag> {

     ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto);

}

 第四步: 在huanf-framework工程的service目录新建TagServiceImpl类,写入如下,是查询标签的具体代码,模糊+分页查询标签代码

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.domain.vo.TagVo;
import com.keke.mapper.TagMapper;
import com.keke.service.TagService;
import com.keke.utils.BeanCopyUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.List;

/**
 * 标签(Tag)表服务实现类
 *
 * @author makejava
 * @since 2023-10-18 10:21:07
 */
@Service("tagService")
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {

     @Override
     public ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto) {
          //封装lambdaQueryWrapper
          LambdaQueryWrapper<Tag> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //根据标签名(如果有)模糊查询
          lambdaQueryWrapper.eq(StringUtils.hasText(tagListDto.getName()),Tag::getName,tagListDto.getName());
          //根据备注(如果有)模糊查询
          lambdaQueryWrapper.eq(StringUtils.hasText(tagListDto.getRemark()),Tag::getRemark,tagListDto.getRemark());
          //创建mp分页器,设置参数
          Page<Tag> page = new Page(pageNum,pageSize);
          //lambdaQueryWrapper条件和page传入page
          page(page,lambdaQueryWrapper);
          //得到分页数据
          List<Tag> tags = page.getRecords();
          //得到页数据总数
          long total = page.getTotal();
          //bean拷贝为标准响应格式
          List<TagVo> tagVos = BeanCopyUtils.copyBeanList(tags, TagVo.class);
          //封装PageVo
          PageVo pageVo = new PageVo(tagVos,total);
          //响应返回
          return ResponseResult.okResult(pageVo);
     }
}

第四步: 在keke-admin工程的controller目录新建TagController类,写入如下,是查询标签的访问接口

package com.keke.controller;

import com.keke.domain.ResponseResult;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.service.TagService;
import io.swagger.annotations.Api;
import io.swagger.models.auth.In;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@Api(tags = "后台标签相关接口")
public class TagController {

     @Autowired
     private TagService tagService;



     @GetMapping("/test")
     public ResponseResult test(){
          List<Tag> list = tagService.list();
          return ResponseResult.okResult(list);
     }

     @GetMapping("/content/tag/list")
     public ResponseResult<PageVo> list(Integer pageNum, Integer pageSize, TagListDto tagListDto){
          return tagService.pageTagList(pageNum,pageSize,tagListDto);
     }


}

第五步:测试

 本地打开你的redis

本地打开postman

运行后台启动类,先登录,拿token去测接口,如下

前端工程

2. 新增标签

1.1 接口分析

点击标签管理的新增按钮可以实现新增标签的功能

请求方式

请求地址

请求头

POST

/content/tag

需要token请求头

请求体:

{
"name":"标签名",
"remark":"标签的备注名"
}

响应体:

{
	"code":200,
	"msg":"操作成功"
}

2.2 代码实现

第一步: 确保你在keke-framework工程handler/mybatisplus有MyMetaObjectHandler类,并且写入了如下,作用是配置是mybatisplus的字段自增(这步我们在前台工程中已经写过)

package com.keke.handler.mybatisplus;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.keke.utils.SecurityUtils;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.util.Date;


@Component
//这个类是用来配置mybatis的字段自动填充。用于'发送评论'功能,由于我们在评论表无法对下面这四个字段进行插入数据(原因是前端在发送评论时,没有在
//请求体提供下面四个参数,所以后端在往数据库插入数据时,下面四个字段是空值),所有就需要这个类来帮助我们往下面这四个字段自动的插入值,
//只要我们更新了评论表的字段,那么无法插入值的字段就自动有值了
public class MyMetaObjectHandler implements MetaObjectHandler {

     @Override
     //只要对数据库执行了插入语句,那么就会执行到这个方法
     public void insertFill(MetaObject metaObject) {
          Long userId = null;
          try {
               //获取用户id
               userId = SecurityUtils.getUserId();
          } catch (Exception e) {
               e.printStackTrace();
               userId = -1L;//如果异常了,就说明该用户还没注册,我们就把该用户的userid字段赋值d为-1
          }
          //自动把下面四个字段新增了值。
          this.setFieldValByName("createTime", new Date(), metaObject);
          this.setFieldValByName("createBy",userId , metaObject);
          this.setFieldValByName("updateTime", new Date(), metaObject);
          this.setFieldValByName("updateBy", userId, metaObject);
     }

     @Override
     public void updateFill(MetaObject metaObject) {
          this.setFieldValByName("updateTime", new Date(), metaObject);
          this.setFieldValByName(" ", SecurityUtils.getUserId(), metaObject);
     }
}

第二步: 在keke-framework工程的domain目录修改Tag类,写入如下,注意有四个字段是使用了mybatisplus的字段自增

package com.keke.domain.entity;

import java.util.Date;
import java.io.Serializable;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableName;

/**
 * 标签(Tag)表实体类
 *
 * @author makejava
 * @since 2023-10-18 10:20:44
 */
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("ke_tag")
public class Tag {
    private Long id;
    //标签名
    private String name;

    //由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充
    //所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值
    @TableField(fill = FieldFill.INSERT)
    private Long createBy;

    //由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充
    //所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;

    //由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充
    //所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateBy;

    //由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充
    //所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值
    @TableField(fill = FieldFill.INSERT)
    private Date updateTime;

    //删除标志(0代表未删除,1代表已删除)
    private Integer delFlag;
    //备注
    private String remark;

}

第三步: 在keke-framework工程新建AddTagDto类,写入如下,用于接收前端传过来的参数

package com.keke.domain.dto;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AddTagDto {
     //标签名
     private String name;
     //备注
     private String remark;
}

第四步: 把keke-admin工程的TagController类,添加如下,作用是新增标签功能的访问接口

package com.keke.controller;

import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.service.TagService;
import io.swagger.annotations.Api;
import io.swagger.models.auth.In;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.parameters.P;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@Api(tags = "后台标签相关接口")
@RequestMapping("/content/tag")
public class TagController {

     @Autowired
     private TagService tagService;



     @GetMapping("/test")
     public ResponseResult test(){
          List<Tag> list = tagService.list();
          return ResponseResult.okResult(list);
     }

     @GetMapping("/list")
     public ResponseResult<PageVo> list(Integer pageNum, Integer pageSize, TagListDto tagListDto){
          return tagService.pageTagList(pageNum,pageSize,tagListDto);
     }

     @PostMapping
     public ResponseResult addTag(@RequestBody AddTagDto addTagDto){
          return tagService.addTag(addTagDto);
     }
}

第五步:在keke-framework的service/TagService中新增如下

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;


/**
 * 标签(Tag)表服务接口
 *
 * @author makejava
 * @since 2023-10-18 10:21:06
 */
public interface TagService extends IService<Tag> {

     ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto);

     ResponseResult addTag(AddTagDto addTagDto);
}

第六步:在keke-framework的service/impl/TagServiceImpl中实现具体逻辑

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.domain.vo.TagVo;
import com.keke.mapper.TagMapper;
import com.keke.service.TagService;
import com.keke.utils.BeanCopyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.List;

/**
 * 标签(Tag)表服务实现类
 *
 * @author makejava
 * @since 2023-10-18 10:21:07
 */
@Service("tagService")
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {


     @Autowired
     private TagService tagService;

     @Override
     public ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto) {
          //封装lambdaQueryWrapper
          LambdaQueryWrapper<Tag> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //根据标签名(如果有)模糊查询
          lambdaQueryWrapper.like(StringUtils.hasText(tagListDto.getName()),Tag::getName,tagListDto.getName());
          //根据备注(如果有)模糊查询
          lambdaQueryWrapper.like(StringUtils.hasText(tagListDto.getRemark()),Tag::getRemark,tagListDto.getRemark());
          //创建mp分页器,设置参数
          Page<Tag> page = new Page(pageNum,pageSize);
          //lambdaQueryWrapper条件和page传入page
          page(page,lambdaQueryWrapper);
          //得到分页数据
          List<Tag> tags = page.getRecords();
          //得到页数据总数
          long total = page.getTotal();
          //bean拷贝为标准响应格式
          List<TagVo> tagVos = BeanCopyUtils.copyBeanList(tags, TagVo.class);
          //封装PageVo
          PageVo pageVo = new PageVo(tagVos,total);
          //响应返回
          return ResponseResult.okResult(pageVo);
     }

     @Override
     public ResponseResult addTag(AddTagDto addTagDto) {
          Tag tag = BeanCopyUtils.copyBean(addTagDto, Tag.class);
          tagService.save(tag);
          return ResponseResult.okResult();
     }
}

第七步:测试

打开前端工程

新增一条标签点击确定

可以看到新增成功

3. 删除标签

3.1 接口分析

例如content/tag/6 代表删除id为6的标签数据。删除后在列表中是否查看不到该条数据,但是数据库中该条数据还是存在的,只是修改了逻辑删除字段的值

请求方式

请求地址

请求头

DELETE

/content/tag/{id}

需要token请求头

请求方式可以看到是PathVariable形式 

响应体:

{
	"code":200,
	"msg":"操作成功"
}

3.2 代码实现

第一步:在keke-admin的controller包下TagController新增删除标签的接口

package com.keke.controller;

import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.service.TagService;
import io.swagger.annotations.Api;
import io.swagger.models.auth.In;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.parameters.P;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@Api(tags = "后台标签相关接口")
@RequestMapping("/content/tag")
public class TagController {

     @Autowired
     private TagService tagService;



     @GetMapping("/test")
     public ResponseResult test(){
          List<Tag> list = tagService.list();
          return ResponseResult.okResult(list);
     }

     @GetMapping("/list")
     public ResponseResult<PageVo> list(Integer pageNum, Integer pageSize, TagListDto tagListDto){
          return tagService.pageTagList(pageNum,pageSize,tagListDto);
     }

     @PostMapping
     public ResponseResult addTag(@RequestBody AddTagDto addTagDto){
          return tagService.addTag(addTagDto);
     }

     @DeleteMapping("/{id}")
     public ResponseResult deleteTagById(@PathVariable("id") Long id){
          return tagService.deleteTagById(id);
     }
}

第二步:在keke-framework的service包下TagService新增如下

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;


/**
 * 标签(Tag)表服务接口
 *
 * @author makejava
 * @since 2023-10-18 10:21:06
 */
public interface TagService extends IService<Tag> {

     ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto);

     ResponseResult addTag(AddTagDto addTagDto);

     ResponseResult deleteTagById(Long id);
}

第三步:在keke-framework的service/impl包下TagServiceImpl写具体的删除标签逻辑

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.domain.vo.TagVo;
import com.keke.mapper.TagMapper;
import com.keke.service.TagService;
import com.keke.utils.BeanCopyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.List;

/**
 * 标签(Tag)表服务实现类
 *
 * @author makejava
 * @since 2023-10-18 10:21:07
 */
@Service("tagService")
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {


     @Autowired
     private TagService tagService;

     @Override
     public ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto) {
          //封装lambdaQueryWrapper
          LambdaQueryWrapper<Tag> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //根据标签名(如果有)模糊查询
          lambdaQueryWrapper.like(StringUtils.hasText(tagListDto.getName()),Tag::getName,tagListDto.getName());
          //根据备注(如果有)模糊查询
          lambdaQueryWrapper.like(StringUtils.hasText(tagListDto.getRemark()),Tag::getRemark,tagListDto.getRemark());
          //创建mp分页器,设置参数
          Page<Tag> page = new Page(pageNum,pageSize);
          //lambdaQueryWrapper条件和page传入page
          page(page,lambdaQueryWrapper);
          //得到分页数据
          List<Tag> tags = page.getRecords();
          //得到页数据总数
          long total = page.getTotal();
          //bean拷贝为标准响应格式
          List<TagVo> tagVos = BeanCopyUtils.copyBeanList(tags, TagVo.class);
          //封装PageVo
          PageVo pageVo = new PageVo(tagVos,total);
          //响应返回
          return ResponseResult.okResult(pageVo);
     }

     @Override
     public ResponseResult addTag(AddTagDto addTagDto) {
          Tag tag = BeanCopyUtils.copyBean(addTagDto, Tag.class);
          tagService.save(tag);
          return ResponseResult.okResult();
     }

     @Override
     public ResponseResult deleteTagById(Long id) {
          tagService.removeById(id);
          return ResponseResult.okResult();
     }


}

第四步:测试,打开前端工程,删除一个标签,删除成功

4. 修改标签

4.1 接口分析

4.1.1 获取标签信息接口

①根据标签id来获取某一条标签的信息,当用户点击修改按钮时触发,展示在弹框里面。例如:content/tag/6 代表获取id为6的标签数据

请求方式

请求地址

请求头

GET

/content/tag/{id}

需要token请求头

请求方式是PathVariable形式

响应体:

{
	"code":200,
	"data":{
        "id":4,
        "name":"标签名",
        "remark":"标签的备注名"
	},
	"msg":"操作成功"
}
4.1.2 修改标签接口

请求方式

请求地址

请求头

PUT

/content/tag

需要token请求头

请求体:

{
"id":7,"name":"标签名",
"remark":"标签的备注名"
}

响应体:

{
	"code":200,
	"msg":"操作成功"
}

4.2 代码实现

第一步:在keke-admin的controller包下的TagController新增两个接口

package com.keke.controller;

import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.EditTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.service.TagService;
import io.swagger.annotations.Api;
import io.swagger.models.auth.In;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.parameters.P;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/content/tag")
@Api(tags = "后台标签相关接口")
public class TagController {

     @Autowired
     private TagService tagService;



     @GetMapping("/test")
     public ResponseResult test(){
          List<Tag> list = tagService.list();
          return ResponseResult.okResult(list);
     }

     @GetMapping("/list")
     public ResponseResult<PageVo> list(Integer pageNum, Integer pageSize, TagListDto tagListDto){
          return tagService.pageTagList(pageNum,pageSize,tagListDto);
     }

     @PostMapping
     public ResponseResult addTag(@RequestBody AddTagDto addTagDto){
          return tagService.addTag(addTagDto);
     }

     @DeleteMapping("/{id}")
     public ResponseResult deleteTagById(@PathVariable("id") Long id){
          return tagService.deleteTagById(id);
     }

     @GetMapping("/{id}")
     public ResponseResult getTagInformation(@PathVariable("id") Long id){
          return tagService.getTagInformation(id);
     }

     @PutMapping
     public ResponseResult editTag(@RequestBody EditTagDto editTagDto){
          return tagService.editTag(editTagDto);
     }
}

第二步:在keke-framework的domain/dto下新增editTagDto

package com.keke.domain.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class EditTagDto {
     //id
     private Long id;
     //标签名
     private String name;
     //备注
     private String remark;
}

第三步:在keke-framework的service/TagService下新增两个方法

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.EditTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;


/**
 * 标签(Tag)表服务接口
 *
 * @author makejava
 * @since 2023-10-18 10:21:06
 */
public interface TagService extends IService<Tag> {

     ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto);

     ResponseResult addTag(AddTagDto addTagDto);

     ResponseResult deleteTagById(Long id);

     ResponseResult getTagInformation(Long id);

     ResponseResult editTag(EditTagDto editTagDto);
}

第四步:在keke-framework的service/impl/TagServiceImpl下添加具体逻辑

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.EditTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.domain.vo.TagVo;
import com.keke.mapper.TagMapper;
import com.keke.service.TagService;
import com.keke.utils.BeanCopyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.List;

/**
 * 标签(Tag)表服务实现类
 *
 * @author makejava
 * @since 2023-10-18 10:21:07
 */
@Service("tagService")
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {


     @Autowired
     private TagService tagService;

     @Override
     public ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto) {
          //封装lambdaQueryWrapper
          LambdaQueryWrapper<Tag> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //根据标签名(如果有)模糊查询
          lambdaQueryWrapper.like(StringUtils.hasText(tagListDto.getName()),Tag::getName,tagListDto.getName());
          //根据备注(如果有)模糊查询
          lambdaQueryWrapper.like(StringUtils.hasText(tagListDto.getRemark()),Tag::getRemark,tagListDto.getRemark());
          //创建mp分页器,设置参数
          Page<Tag> page = new Page(pageNum,pageSize);
          //lambdaQueryWrapper条件和page传入page
          page(page,lambdaQueryWrapper);
          //得到分页数据
          List<Tag> tags = page.getRecords();
          //得到页数据总数
          long total = page.getTotal();
          //bean拷贝为标准响应格式
          List<TagVo> tagVos = BeanCopyUtils.copyBeanList(tags, TagVo.class);
          //封装PageVo
          PageVo pageVo = new PageVo(tagVos,total);
          //响应返回
          return ResponseResult.okResult(pageVo);
     }

     @Override
     public ResponseResult addTag(AddTagDto addTagDto) {
          Tag tag = BeanCopyUtils.copyBean(addTagDto, Tag.class);
          tagService.save(tag);
          return ResponseResult.okResult();
     }

     @Override
     public ResponseResult deleteTagById(Long id) {
          tagService.removeById(id);
          return ResponseResult.okResult();
     }

     @Override
     public ResponseResult getTagInformation(Long id) {
          Tag tag = tagService.getById(id);
          TagVo tagVo = BeanCopyUtils.copyBean(tag, TagVo.class);
          return ResponseResult.okResult(tagVo);
     }

     @Override
     public ResponseResult editTag(EditTagDto editTagDto) {
          Tag tag = BeanCopyUtils.copyBean(editTagDto, Tag.class);
          tagService.updateById(tag);
          return ResponseResult.okResult();
     }
}

第五步,测试,打开前端工程,点击修改,展示出标签的信息,填入修改后的信息,点击确定,标签修改成功

 七、后台模块-发布文章

需要提供写博文的功能,写博文时需要关联分类和标签。可以上传缩略图,也可以在正文中添加图片。文章可以直接发布,也可以保存到草稿箱

分析下来,这个发布文章的页面总共有四个接口

查询所有分类的接口

查询所有标签的接口

图片上传的接口

新增文章的接口

下面我们一一实现

1. 查询分类接口

1.1 接口分析

请求方式如下,注意: 无请求参数

请求方式

请求地址

请求头

GET

/content/category/listAllCategory

需要token请求头

响应体:

{
	"code":200,
	"data":[
		{
			"description":"wsd",
			"id":1,
			"name":"java"
		},
		{
			"description":"wsd",
			"id":2,
			"name":"PHP"
		}
	],
	"msg":"操作成功"
}

2.2 代码实现

第一步: 把keke-framework工程的CategoryService接口修改为如下,增加了分页查询分类列表的接口

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.Category;
import org.springframework.web.bind.annotation.RequestMapping;


/**
 * 分类表(Category)表服务接口
 *
 * @author makejava
 * @since 2023-10-10 20:42:22
 */
public interface CategoryService extends IService<Category> {

     ResponseResult getCategoryList();

     //后台接口,查询所有文章分类
     ResponseResult listAllCategory();

}

第二步: 把keke-framework工程的CategoryServiceImpl类修改为如下,增加了分页查询分类列表接口的具体代码实现

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.constants.SystemConstants;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.Article;
import com.keke.domain.entity.Category;
import com.keke.domain.vo.AdminCategoryVo;
import com.keke.domain.vo.CategoryVo;
import com.keke.mapper.CategoryMapper;
import com.keke.service.ArticleService;
import com.keke.service.CategoryService;
import com.keke.utils.BeanCopyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 分类表(Category)表服务实现类
 *
 * @author makejava
 * @since 2023-10-10 20:42:22
 */
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {

     @Autowired
     private ArticleService articleService;

     @Override
     public ResponseResult getCategoryList() {
          //查询文章表,状态已发布的文章,但是在CategoryService下,查询文章表,就要注入ArticleService
          LambdaQueryWrapper<Article> articleWrapper = new LambdaQueryWrapper<>();
          articleWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);
          List<Article> articleList = articleService.list(articleWrapper);
          //获取文章的分类id,并去重
          Set<Long> categoryIds = articleList.stream()
                  .map(article -> article.getCategoryId())
                  //toSet可以去除重复的id
                  .collect(Collectors.toSet());
          //查询分类表
          List<Category> categories = listByIds(categoryIds);
          //分类表中只获取正常状态非禁用的分类,用stream流过滤
          categories = categories.stream()
                  .filter(category -> SystemConstants.STATUS_NORMAL.equals(category.getStatus()))
                  .collect(Collectors.toList());
          //封装Vo
          List<CategoryVo> categoryVos = BeanCopyUtils.copyBeanList(categories, CategoryVo.class);
          //封装到响应体中,因为有数据,所以要调用有参okResult(),把参数传进去
          return ResponseResult.okResult(categoryVos);
     }

     @Override
     public ResponseResult listAllCategory() {
          LambdaQueryWrapper<Category> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          lambdaQueryWrapper.eq(Category::getStatus,SystemConstants.STATUS_NORMAL);
          List<Category> categoryList = list(lambdaQueryWrapper);
          List<AdminCategoryVo> adminCategoryVos = BeanCopyUtils.copyBeanList(categoryList, AdminCategoryVo.class);
          return ResponseResult.okResult(adminCategoryVos);
     }
}

第三步: 在keke-framework的domain/vo包下创建AdminCategoryVo,这个是后台中要用到的响应体

package com.keke.domain.vo;


import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AdminCategoryVo {

     private Long id;
     //分类名
     private String name;
     //描述
     private String description;

}

第四步: 把keke-admin工程的CategoryController类修改为如下,增加了分页查询分类功能的访问接口

package com.keke.controller;


import com.keke.domain.ResponseResult;
import com.keke.service.CategoryService;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/content/category")
@Api(tags = "后台标签相关接口")
public class CategoryController {

     @Autowired
     private CategoryService categoryService;

     @GetMapping("/listAllCategory")
     public ResponseResult listAllCategory(){
          return categoryService.listAllCategory();
     }
}

这里我们不测试,到4个接口写完统一测试

2. 查询标签接口

2.1 接口分析

请求方式如下,注意: 无请求参数

请求方式

请求地址

请求头

GET

/content/tag/listAllTag

需要token请求头

响应体: 

{
	"code":200,
	"data":[
		{
			"id":1,
			"name":"Mybatis"
		},
		{
			"id":4,
			"name":"Java"
		}
	],
	"msg":"操作成功"
}

2.2 代码实现

第一步,在keke-framework的domain/vo包下创建AdminTagVo类,用于专门在后台返回接口关于标签的字段

package com.keke.domain.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class AdminTagVo {
     //id
     private Long id;
     //标签名
     private String name;
}

第二步:在keke-blog的TagController中新增查询所有标签的接口

package com.keke.controller;

import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.EditTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;
import com.keke.service.TagService;
import io.swagger.annotations.Api;
import io.swagger.models.auth.In;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.parameters.P;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/content/tag")
@Api(tags = "后台标签相关接口")
public class TagController {

     @Autowired
     private TagService tagService;



     @GetMapping("/test")
     public ResponseResult test(){
          List<Tag> list = tagService.list();
          return ResponseResult.okResult(list);
     }

     @GetMapping("/list")
     public ResponseResult<PageVo> list(Integer pageNum, Integer pageSize, TagListDto tagListDto){
          return tagService.pageTagList(pageNum,pageSize,tagListDto);
     }

     @PostMapping
     public ResponseResult addTag(@RequestBody AddTagDto addTagDto){
          return tagService.addTag(addTagDto);
     }

     @DeleteMapping("/{id}")
     public ResponseResult deleteTagById(@PathVariable("id") Long id){
          return tagService.deleteTagById(id);
     }

     @GetMapping("/{id}")
     public ResponseResult getTagInformation(@PathVariable("id") Long id){
          return tagService.getTagInformation(id);
     }

     @PutMapping
     public ResponseResult editTag(@RequestBody EditTagDto editTagDto){
          return tagService.editTag(editTagDto);
     }

     //发布文章-查询标签接口
     @GetMapping("/listAllTag")
     public ResponseResult selectAllTag(){
          return tagService.selectAllTag();
     }
}

第三步:在keke-framework的service/TagService中新增方法

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.EditTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.PageVo;


/**
 * 标签(Tag)表服务接口
 *
 * @author makejava
 * @since 2023-10-18 10:21:06
 */
public interface TagService extends IService<Tag> {

     ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto);

     ResponseResult addTag(AddTagDto addTagDto);

     ResponseResult deleteTagById(Long id);

     ResponseResult getTagInformation(Long id);

     ResponseResult editTag(EditTagDto editTagDto);

     //写文章-查询所有标签接口
     ResponseResult selectAllTag();

}

第四步:在keke-framework的service/impl包下TagServiceImpl中写具体逻辑

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddTagDto;
import com.keke.domain.dto.EditTagDto;
import com.keke.domain.dto.TagListDto;
import com.keke.domain.entity.Tag;
import com.keke.domain.vo.AdminTagVo;
import com.keke.domain.vo.PageVo;
import com.keke.domain.vo.TagVo;
import com.keke.mapper.TagMapper;
import com.keke.service.TagService;
import com.keke.utils.BeanCopyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.List;

/**
 * 标签(Tag)表服务实现类
 *
 * @author makejava
 * @since 2023-10-18 10:21:07
 */
@Service("tagService")
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {


     @Autowired
     private TagService tagService;

     @Override
     public ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto) {
          //封装lambdaQueryWrapper
          LambdaQueryWrapper<Tag> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //根据标签名(如果有)模糊查询
          lambdaQueryWrapper.like(StringUtils.hasText(tagListDto.getName()),Tag::getName,tagListDto.getName());
          //根据备注(如果有)模糊查询
          lambdaQueryWrapper.like(StringUtils.hasText(tagListDto.getRemark()),Tag::getRemark,tagListDto.getRemark());
          //创建mp分页器,设置参数
          Page<Tag> page = new Page(pageNum,pageSize);
          //lambdaQueryWrapper条件和page传入page
          page(page,lambdaQueryWrapper);
          //得到分页数据
          List<Tag> tags = page.getRecords();
          //得到页数据总数
          long total = page.getTotal();
          //bean拷贝为标准响应格式
          List<TagVo> tagVos = BeanCopyUtils.copyBeanList(tags, TagVo.class);
          //封装PageVo
          PageVo pageVo = new PageVo(tagVos,total);
          //响应返回
          return ResponseResult.okResult(pageVo);
     }

     @Override
     public ResponseResult addTag(AddTagDto addTagDto) {
          Tag tag = BeanCopyUtils.copyBean(addTagDto, Tag.class);
          tagService.save(tag);
          return ResponseResult.okResult();
     }

     @Override
     public ResponseResult deleteTagById(Long id) {
          tagService.removeById(id);
          return ResponseResult.okResult();
     }

     @Override
     public ResponseResult getTagInformation(Long id) {
          Tag tag = tagService.getById(id);
          TagVo tagVo = BeanCopyUtils.copyBean(tag, TagVo.class);
          return ResponseResult.okResult(tagVo);
     }

     @Override
     public ResponseResult editTag(EditTagDto editTagDto) {
          Tag tag = BeanCopyUtils.copyBean(editTagDto, Tag.class);
          tagService.updateById(tag);
          return ResponseResult.okResult();
     }

     @Override
     public ResponseResult selectAllTag() {
          //这里我们创建专门返回给前端的AdminTagVo,
          List<Tag> tagList = list();
          List<AdminTagVo> adminTagVos = BeanCopyUtils.copyBeanList(tagList, AdminTagVo.class);
          return ResponseResult.okResult(adminTagVos);
     }
}

3. 图片上传

3.1 接口分析

请求方式如下。请求参数是img,值为要上传的文件

请求方式

请求地址

请求头

POST

/upload

需要token请求头

请求头:

Content-Type :multipart/form-data;

响应体:

{
    "code": 200,
    "data": "文件访问链接",
    "msg": "操作成功"
}

3.2 代码实现

第一步: 把keke-admin工程的application.yml修改为如下,增加了OSS的相关配置

server:
  port: 8989

spring:
  # 数据库连接信息
  datasource:
    url: jdbc:mysql://localhost:3306/keke_blog?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password:
    driver-class-name: com.mysql.cj.jdbc.Driver

  servlet:
    # 文件上传
    multipart:
      # 单个上传文件的最大允许大小
      max-file-size: 20MB
      # HTTP请求中包含的所有文件的总大小的最大允许值
      max-request-size: 20MB

mybatis-plus:
  #  configuration:
  #    # 日志
  #    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      # 逻辑删除的字段
      logic-delete-field: delFlag
      # 代表已删除的值
      logic-delete-value: 1
      # 代表未删除的值
      logic-not-delete-value: 0
      # 主键自增策略,以mysql数据库为准
      id-type: auto
      
OSS:
  accessKey: 这里填写你自己的accessKey
  secretKey: 这里填写你自己的secretKey
  bucket: keke-blog

第二步: 在keke-admin工程的controller目录新建UploadController类,写入如下

package com.keke.controller;


import com.keke.domain.ResponseResult;
import com.keke.handler.exception.exception.SystemException;
import com.keke.service.OssUploadService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController

public class UploadController {

     @Autowired
     private OssUploadService ossUploadService;

     @PostMapping("/upload")
     public ResponseResult uploadImg(@RequestParam("img") MultipartFile multipartFile){
         try {
              return ossUploadService.uploadImg(multipartFile);
         }catch (Exception e){
              e.printStackTrace();
              throw new RuntimeException("文件上传失败");
         }
     }

}

4. 新增文章

4.1 接口分析

请求方式如下:

请求方式

请求地址

请求头

POST

/content/article

需要token请求头

请求体: 

{
    "title":"测试新增博文",
    "thumbnail":"https://sg-blog-oss.oss-cn-beijing.aliyuncs.com/2022/08/21/4ceebc07e7484beba732f12b0d2c43a9.png",
    "isTop":"0",
    "isComment":"0",
    "content":"# 一级标题\n## 二级标题\n![Snipaste_20220228_224837.png](https://sg-blog-oss.oss-cn-beijing.aliyuncs.com/2022/08/21/c3af554d4a0f4935b4073533a4c26ee8.png)\n正文",
    "tags":[
        1,
        4
    ],
    "categoryId":1,
    "summary":"哈哈",
    "status":"1"
}

响应体:

{
	"code":200,
	"msg":"操作成功"
}

4.2 代码实现

第一步,在keke-blog的controller中新建ArticleController写入写文章接口

package com.keke.controller;

import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.service.ArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ArticleController {

     @Autowired
     private ArticleService articleService;

     @PostMapping("/content/article")
     public ResponseResult add(@RequestBody AddArticleDto addArticleDto){
          return addArticleDto.add(addArticleDto);
     }
}

第二步: 在keke-framework工程的domain/dto目录新建AddArticleDto类,写入如下,用来接受前端传过来的参数,最重要的是tags属性
 

package com.keke.domain.dto;

import com.baomidou.mybatisplus.annotation.TableField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AddArticleDto {

     //标题
     private String title;
     //文章内容
     private String content;
     //文章摘要
     private String summary;
     //所属分类id
     private Long categoryId;
     //缩略图
     private String thumbnail;
     //是否置顶(0否,1是)
     private String isTop;
     //状态(0已发布,1草稿)
     private String status;
     //是否允许评论 1是,0否
     private String isComment;
     //关联标签id们
     List<Long> tags;
}

第三步: 在keke-framework工程的domain/entity目录新建ArticleTag类,写入如下,文章表&标签表的中间表,对应的实体类,并建立ke_article_tag表对应的service mapper serviceImpl类

package com.keke.domain.entity;

import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableName;

/**
 * 文章标签关联表(ArticleTag)表实体类
 *
 * @author makejava
 * @since 2023-10-20 19:37:32
 */
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("ke_article_tag")
public class ArticleTag {
    //文章id
    private Long articleId;
    //标签id
    private Long tagId;
}
package com.keke.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.keke.domain.entity.ArticleTag;


/**
 * 文章标签关联表(ArticleTag)表数据库访问层
 *
 * @author makejava
 * @since 2023-10-20 20:05:17
 */
public interface ArticleTagMapper extends BaseMapper<ArticleTag> {

}
package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.entity.ArticleTag;


/**
 * 文章标签关联表(ArticleTag)表服务接口
 *
 * @author makejava
 * @since 2023-10-20 20:05:17
 */
public interface ArticleTagService extends IService<ArticleTag> {

}
package com.keke.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.domain.entity.ArticleTag;
import com.keke.mapper.ArticleTagMapper;
import com.keke.service.ArticleTagService;
import org.springframework.stereotype.Service;

/**
 * 文章标签关联表(ArticleTag)表服务实现类
 *
 * @author makejava
 * @since 2023-10-20 20:05:17
 */
@Service("articleTagService")
public class ArticleTagServiceImpl extends ServiceImpl<ArticleTagMapper, ArticleTag> implements ArticleTagService {

}

 第四步: 在keke-framework工程的domain/entity的Article类修改为如下,注意有4个字段是使用了mybatisplus属性值自增的注解

package com.keke.domain.entity;

import java.util.Date;
import java.io.Serializable;


import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

/**
 * 文章表(Article)表实体类
 *
 * @author makejava
 * @since 2023-10-10 10:06:59
 */
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("ke_article")
@Accessors(chain = true)
public class Article {

    private Long id;
    //标题
    private String title;
    //文章内容
    private String content;
    //文章摘要
    private String summary;
    //所属分类id
    private Long categoryId;
    //分页查询文章列表时,新增的一个字段,为的是更好的封装,但是数据库中没有该字段,为了避免mp
    //在查询的时候查询这一列,可以添加如下注解
    @TableField(exist = false)//意思是这个字段在数据库表中实际上是不存在的
    private String categoryName;
    //缩略图
    private String thumbnail;
    //是否置顶(0否,1是)
    private String isTop;
    //状态(0已发布,1草稿)
    private String status;
    //访问量
    private Long viewCount;
    //是否允许评论 1是,0否
    private String isComment;

    @TableField(fill = FieldFill.INSERT)
    private Long createBy;

    //由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充
    //所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;

    //由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充
    //所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateBy;

    //由于我们在MyMetaObjectHandler类配置了mybatisplus的字段自动填充
    //所以就能指定哪个字段在什么时候进行自动填充,例如该类其它字段新增了数据,那么createBy字段就会自动填充值
    @TableField(fill = FieldFill.INSERT)
    private Date updateTime;
    //删除标志(0代表未删除,1代表已删除)
    private Integer delFlag;


    public Article(Long id,Long viewCount){
        this.id = id;
        this.viewCount = viewCount;
    }
}

第五步:在keke-framework的service的ArticleService接口新增方法

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.domain.entity.Article;


/**
 * 文章表(Article)表服务接口
 *
 * @author makejava
 * @since 2023-10-10 09:59:37
 */
public interface ArticleService extends IService<Article> {


     ResponseResult hotArticleList();

     ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId);

     ResponseResult getArticleDetail(Long id);

     ResponseResult updateViewCount(Long id);

     ResponseResult add(AddArticleDto addArticleDto);
}

第五步:在keke-framework的service/impl的ArticleServiceImpl写入具体逻辑代码

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.constants.SystemConstants;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.domain.entity.Article;
import com.keke.domain.entity.ArticleTag;
import com.keke.domain.entity.Category;
import com.keke.domain.vo.ArticleDetailVo;
import com.keke.domain.vo.ArticleListVo;
import com.keke.domain.vo.HotArticleVo;
import com.keke.domain.vo.PageVo;
import com.keke.mapper.ArticleMapper;
import com.keke.service.ArticleService;
import com.keke.service.ArticleTagService;
import com.keke.service.CategoryService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 文章表(Article)表服务实现类
 *
 * @author makejava
 * @since 2023-10-10 09:59:39
 */
@Service("articleService")
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {

     @Autowired
     private CategoryService categoryService;

     @Autowired
     private ArticleService articleService;

     //显然redisCache是对redisTemplate的封装
     @Autowired
     private RedisCache redisCache;

     @Autowired
     private ArticleTagService articleTagService;
     //查询热门文章
     @Override
     public ResponseResult hotArticleList() {

          //---------------------------------------每调用这个方法就从redis查询文章的浏览量,展示在热门文章列表--------------------------------------------------------------
          Map<String, Integer> viewCountMap = redisCache.getCacheMap(SystemConstants.REDIS_ARTICLE_KEY);
          List<Article> articleList = viewCountMap.entrySet()
                  .stream()
                  .map(entry -> new Article(Long.valueOf(entry.getKey()), entry.getValue().longValue()))
                  .collect(Collectors.toList());
          articleService.updateBatchById(articleList);
          //-----------------------------------------------------------------------------------------------------
          //查询热门文章 封装成ResponseResult返回
          LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //必须是正式文章
          lambdaQueryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);
          //按照浏览量进行排序
          lambdaQueryWrapper.orderByDesc(Article::getViewCount);
          //最多查询10条,设置mp分页对象的参数分别为1和10
          Page<Article> page = new Page<>(SystemConstants.ARTICLE_STATUS_CURRENT,SystemConstants.ARTICLE_STATUS_SIZE);
          //将page对象和lambdaQueryWrapper查询条件封装成page
          page(page,lambdaQueryWrapper);
          //page.getRecords()获取到所有符合条件的数据(也就是文章)
          List<Article> articles = page.getRecords();
          //BeanCopy
          List<HotArticleVo> hotArticleVos = BeanCopyUtils.copyBeanList(articles, HotArticleVo.class);
          //返回ResponseResult对象
          return ResponseResult.okResult(hotArticleVos);
     }

     //分页查询文章列表,包含首页和分类页面的文章列表分页查询
     @Override
     public ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId) {
          //查询条件
          LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //首先明确categoryId在首页中不传,在分类文章页面中会传,所以我们要判断
          /*
          这里是lambdaQueryWrapper.eq三个参数的写法,第一个参数返回值是boolean类型,如果判断为true
          那么后续就会把后面的判断加入sql语句当中
           */
          lambdaQueryWrapper.eq(Objects.nonNull(categoryId)&&categoryId>0,Article::getCategoryId,categoryId);
          //状态是正式发布的,置顶的文章要显示在最前面,对isTop进行降序
          lambdaQueryWrapper.eq(Article::getStatus,SystemConstants.ARTICLE_STATUS_NORMAL);
          lambdaQueryWrapper.orderByDesc(Article::getIsTop);
          //分页查询
          Page<Article> page = new Page<>(pageNum,pageSize);
          page(page,lambdaQueryWrapper);
          //查询categoryName
          List<Article> articles = page.getRecords();//有categoryId,但无categoryName
          //拿着categoryId去查询categoryName,然后封装到article中
          /*
          for (Article article : articles) {
               Category category = categoryService.getById(article.getCategoryId());
               article.setCategoryName(category.getName());
          }
          */
          articles.stream()
                  //setter返回的是对象
                  .map(article -> article.setCategoryName(categoryService.getById(article.getCategoryId()).getName()))
                  .collect(Collectors.toList());
          //封装Vo
          List<ArticleListVo> articleListVos = BeanCopyUtils.copyBeanList(page.getRecords(), ArticleListVo.class);
          //封装PageVo
          PageVo pageVo = new PageVo(articleListVos,page.getTotal());
          return ResponseResult.okResult(pageVo);
     }

     //查询文章详情
     @Override
     public ResponseResult getArticleDetail(Long id) {
          //根据id查询文章
          Article article = getById(id);
//          //--------------------------------------从redis中获取viewCount--------------------------------------------------------------
//          //获取到的是redis当中的Integer类型的viewCount
//          Integer viewCount = redisCache.getCacheMapValue(SystemConstants.REDIS_ARTICLE_KEY, id.toString());
//          //设置article的viewCount为从redis中查出来的数据
//          article.setViewCount(viewCount.longValue());

          //转化称Vo
          ArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class);
          //根据分类id查询分类名称
          Long categoryId = articleDetailVo.getCategoryId();
          Category category = categoryService.getById(categoryId);
          //如果没有获取到id,就不设置
          if(categoryId!=null){
               articleDetailVo.setCategoryName(category.getName());
          }
          //封装响应体
          return ResponseResult.okResult(articleDetailVo);
     }

     @Override
     public ResponseResult updateViewCount(Long id) {
          //更新浏览量(自增)
          redisCache.incrementCacheMapValue(SystemConstants.REDIS_ARTICLE_KEY,id.toString(),SystemConstants.REDIS_ARTICLE_VIEW_COUNT_INCREMENT);
          return ResponseResult.okResult();
     }

     @Override
     //因为这里做了两次数据库插入操作,必须保证两个操作同时失败或者成功
     public ResponseResult add(AddArticleDto addArticleDto) {
          //添加到文章表
          Article article = BeanCopyUtils.copyBean(addArticleDto, Article.class);
          save(article);
          //添加到文章标签关联表
          List<Long> tags = addArticleDto.getTags();
          //拿到ArticleTag对象集合
          List<ArticleTag> articleTags = tags.stream()
                  .map(new Function<Long, ArticleTag>() {
                       @Override
                       public ArticleTag apply(Long tagId) {
                            ArticleTag articleTag = new ArticleTag(article.getId(), tagId);
                            return articleTag;
                       }
                  })
                  .collect(Collectors.toList());
          //然后批量插入数据库
          articleTagService.saveBatch(articleTags);
          return ResponseResult.okResult();
     }
}

5. 四接口测试

点击发布,发布成功后跳转至文章管理,但这个页面的接口我们还没有写,所以我们需要去数据库中查看刚才的操作是否成功

文章表新增成功

文章标签表新增成功 

 八、后台模块-Excel表格

1. 接口分析

在分类管理中点击导出按钮可以把所有的分类导出到Excel文件

请求方式

请求地址

请求头

GET

/content/category/export

需要token请求头

响应体:

 直接导出一个Excel文件

失败的话响应体如下:

{
	"code":500,
	"msg":"出现错误"
}

2. EasyExcel入门

使用easyExcel实现Excel的导出操作

官方地址: http:// https://github.com/alibaba/easyexcel

快速开始 写Excel | Easy Excel

分析: 把数据库的分类数据查询出来,然后写入到Excel文件中,然后下载这个Excel文件,重点就是怎么往Excel里面写入数据,点击上面提供的快速开始的链接,点击左侧的 '写Excel',就能看到实现的代码了,重点看右侧小导航栏的 'web中的写并且失败的时候返回json'

3. 代码实现

第一步:在keke-framework工程的pom.xml添加如下

<!--easyExcel的依赖-->
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>easyexcel</artifactId>
</dependency>

第二步: 把keke-framework工程的WebUtils类修改为如下

package com.keke.utils;

import org.springframework.web.context.request.RequestContextHolder;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

public class WebUtils {
    /**
     * 将字符串渲染到客户端
     *
     * @param response 渲染对象
     * @param string 待渲染的字符串
     * @return null
     */
    public static void renderString(HttpServletResponse response, String string) {
        try
        {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }


    //easyExcel文件导出
    public static void setDownLoadHeader(String filename, HttpServletResponse response) throws UnsupportedEncodingException {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String fname= URLEncoder.encode(filename,"UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition","attachment; filename="+fname);
    }
}

第三步: 在keke-framework工程的vo目录新建ExcelCategoryVo类,写入如下,用于作为Excel表格的列头

package com.keke.domain.vo;


import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ExcelCategoryVo {
     @ExcelProperty("分类名")
     private String name;

     @ExcelProperty("描述")
     private String description;

     @ExcelProperty("状态:0正常,1禁用")
     private String status;
}

第四步: 把keke-admin工程的CategoryController类修改为如下,增加了easyExcel文件导出的具体代码实现

package com.keke.controller;


import com.alibaba.excel.EasyExcel;
import com.alibaba.fastjson.JSON;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.Category;
import com.keke.domain.vo.ExcelCategoryVo;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.service.CategoryService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.WebUtils;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.util.List;

@RestController
@RequestMapping("/content/category")
@Api(tags = "后台标签相关接口")
public class CategoryController {

     @Autowired
     private CategoryService categoryService;

     @GetMapping("/listAllCategory")
     public ResponseResult listAllCategory(){
          return categoryService.listAllCategory();
     }

     @GetMapping("/export")
     public void export(HttpServletResponse response){

          try {
               //设置下载文件的请求头
               WebUtils.setDownLoadHeader("分类.xlsx",response);
               //获取需要导出的数据
               List<Category> categoryList = categoryService.list();
               List<ExcelCategoryVo> excelCategoryVos = BeanCopyUtils.copyBeanList(categoryList, ExcelCategoryVo.class);
               //把数据写入Excel中
               EasyExcel.write(response.getOutputStream(), ExcelCategoryVo.class).autoCloseStream(Boolean.FALSE).sheet("分类导出")
                       .doWrite(excelCategoryVos);
          } catch (Exception e) {
               //如果出现异常,就返回失败的json数据给前端
               ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR);
               //WebUtils是我们在keke-framework工程写的类,里面的renderString方法是将json字符串写入到请求体,然后返回给前端
               WebUtils.renderString(response, JSON.toJSONString(result));
          }
     }
}

第五步:测试,启动后台工程,redis,前端工程,点击导出按钮

导出成功 

九、SpringSecurity权限控制

由于后台的用户对应不同的角色,所以有不同的权限,例如上面实现的导出excel功能,是超级管理员才有的功能,但是如果普通用户登录,拿着token去调导出excel的接口,也是可以成功的,这里我们就需要用到安全框架中的权限控制

1. 案例

比如我们登录普通用户,拿到token,访问导出excel表格接口,依旧可以导出,但是此用户是没有该权限的

2. 代码实现

第一步: 把keke-framework工程的SystemCanstants类修改为如下,增加了是否是管理员用户的判断常量

package com.keke.constants;


//字面值(代码中的固定值)处理,把字面值都在这里定义成常量
public class SystemConstants {

    /**
     * 文章是草稿
     */
    public static final int ARTICLE_STATUS_DRAFT = 1;

    /**
     * 文章是正常发布状态
     */
    public static final int ARTICLE_STATUS_NORMAL = 0;

    /**
     * 文章列表当前查询页数
     */
    public static final int ARTICLE_STATUS_CURRENT = 1;

    /**
     * 文章列表每页显示的数据条数
     */
    public static final int ARTICLE_STATUS_SIZE = 10;

    /**
     * 分类表的分类状态是正常状态
     */
    public static final String STATUS_NORMAL = "0";

    /**
     * 友联审核通过
     */
    public static final String Link_STATUS_NORMAL = "0";

    /**
     * 评论区的某条评论是根评论
     */
    public static final String COMMENT_ROOT = "-1";

    /**
     * 文章评论
     */
    public static final String ARTICLE_COMMENT = "0";

    /**
     * 友链评论
     */
    public static final String LINK_COMMENT = "1";

    /**
     * redis中的文章浏览量key
     */
    public static final String REDIS_ARTICLE_KEY = "article:viewCount";

    /**
     * 浏览量自增1
     */
    public static final int REDIS_ARTICLE_VIEW_COUNT_INCREMENT = 1;


    /**
     * 菜单权限
     */
    public static final String MENU = "C";

    /**
     * 按钮权限
     */
    public static final String BUTTON = "F";

    /**
     * 后台管理员用户
     */
    public static final String ADMIN = "1";

}

第二步:keke-framework中domain/entity LoginUser修改如下,增加权限信息成员变量

package com.keke.domain.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {

     private User user;
     //用于返回权限信息。现在我们正在实现'认证','权限'后面才用得到。所以返回null即可
     //当要查询用户信息的时候,我们不能单纯返回null,要重写这个方法,作用是返回权限信息
     private List<String> permissions;
     @Override
     public Collection<? extends GrantedAuthority> getAuthorities() {
          return null;
     }

     @Override
     public String getPassword() {
          return user.getPassword();
     }

     @Override
     public String getUsername() {
          return user.getUserName();
     }

     @Override
     public boolean isAccountNonExpired() {
          return true;
     }

     @Override
     public boolean isAccountNonLocked() {
          return true;
     }

     @Override
     public boolean isCredentialsNonExpired() {
          return true;
     }

     @Override
     public boolean isEnabled() {
          return true;
     }
}

第三步: 把keke-admin工程的SecurityConfig修改为如下,增加了@EnableGlobalMethodSecurity注解

package com.keke.config;

import com.keke.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
//WebSecurityConfigurerAdapter是Security官方提供的类
@EnableGlobalMethodSecurity(prePostEnabled = true)//权限控制
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //注入我们在keke-blog工程写的JwtAuthenticationTokenFilter过滤器
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    AccessDeniedHandler accessDeniedHandler;



    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    //把官方的PasswordEncoder密码加密方式替换成BCryptPasswordEncoder
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                 //对于后台登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                //其他接口均需要认证
                .anyRequest().authenticated();

        //配置我们自己写的认证和授权的异常处理
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);


        //关闭security默认的退出登录功能
        http.logout().disable();
        //将自定义filter加入security过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //允许跨域
        http.cors();
    }

}

第四步: 把keke-framework工程的UserDetailsServiceImpl类修改为如下,增加了权限信息的相关实现代码

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.keke.constants.SystemConstants;
import com.keke.domain.entity.LoginUser;
import com.keke.domain.entity.User;
import com.keke.mapper.MenuMapper;
import com.keke.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

     @Autowired
     private UserMapper userMapper;

     @Autowired
     private MenuMapper menuMapper;

     @Override
     public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
          //因为我们自己创建的UserDetailsService注入到容器中,所以会调用我们自己创建的
          //根据用户名从数据库查询用户信息,这里注入userMapper进行查询
          LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          lambdaQueryWrapper.eq(User::getUserName,userName);
          //这里可以看到userMapper也可以传入wrapper进行条件查询
          User user = userMapper.selectOne(lambdaQueryWrapper);
          //判断是否查到用户,如果没查到,抛出异常
          if(Objects.isNull(user)){
               throw new RuntimeException("用户不存在");
          }
          //返回用户信息


          //TODO查询权限信息封装(后台)
          if(user.getType().equals(SystemConstants.ADMIN)){
               //如果是后台管理员,查询权限信息,封装到LoginUser中
               List<String> menuList = menuMapper.selectPermsByUserId(user.getId());
               LoginUser loginUser = new LoginUser(user,menuList);
               return loginUser;
          }
          return new LoginUser(user,null);
     }
}

第五步: 在keke-framework工程的service目录创建impl.PermissionService类,写入如下

package com.keke.service.impl;

import com.keke.utils.SecurityUtils;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("ps")
public class PermissionService {


     /**
      * 判断当前用户是否具有permission
      * @param permission
      * @return
      */

     //否则  获取当前登录用户所具有的权限列表 如何判断是否存在permission
     // 这个permission其实就是sys_menu表的perms字段的值
     public boolean hasPermission(String permission){
          //如果是管理员,直接有权限
          if(SecurityUtils.isAdmin()){
               return true;
          }
          List<String> permissions = SecurityUtils.getLoginUser().getPermissions();
          //contains方法是 'List集合官方' 提供的方法,返回值是布尔值,如果用户具有对应权限就返回true
          return permissions.contains(permission);
     }
}

第六步: 把huanf-admin工程的CategoryController类修改为如下,在export方法的上面添加了@PreAuthorize注解

package com.keke.controller;


import com.alibaba.excel.EasyExcel;
import com.alibaba.fastjson.JSON;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.Category;
import com.keke.domain.vo.ExcelCategoryVo;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.service.CategoryService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.WebUtils;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.util.List;

@RestController
@RequestMapping("/content/category")
@Api(tags = "后台标签相关接口")
public class CategoryController {

     @Autowired
     private CategoryService categoryService;

     @GetMapping("/listAllCategory")
     public ResponseResult listAllCategory(){
          return categoryService.listAllCategory();
     }

     //权限控制,ps是PermissionService类的bean名称
     @PreAuthorize("@ps.hasPermission('content:category:export')")
     @GetMapping("/export")
     public void export(HttpServletResponse response){

          try {
               //设置下载文件的请求头
               WebUtils.setDownLoadHeader("分类.xlsx",response);
               //获取需要导出的数据
               List<Category> categoryList = categoryService.list();
               List<ExcelCategoryVo> excelCategoryVos = BeanCopyUtils.copyBeanList(categoryList, ExcelCategoryVo.class);
               //把数据写入Excel中
               EasyExcel.write(response.getOutputStream(), ExcelCategoryVo.class).autoCloseStream(Boolean.FALSE).sheet("分类导出")
                       .doWrite(excelCategoryVos);
          } catch (Exception e) {
               //如果出现异常,就返回失败的json数据给前端
               ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR);
               //WebUtils是我们在keke-framework工程写的类,里面的renderString方法是将json字符串写入到请求体,然后返回给前端
               WebUtils.renderString(response, JSON.toJSONString(result));
          }
     }
}

3. 测试

管理员访问

正常用管理员登录,拿到token是可以导出excel表格的

普通用户访问

先登录拿到普通用户的token

访问接口,没有权限

下载后的文件,是失败格式的json响应体

十、后台模块-文章列表

1. 查询文章

1.1 接口设计

在 '内容管理 -> 文章管理' 页面查询出有效的已发布文章的信息

请求方式

请求路径

是否需求token头

Get

/content/article/list

请求参数:

pageNum: 页码

pageSize: 每页条数

title:文章标题

summary:文章摘要

响应体:

{
	"code":200,
	"data":{
		"rows":[
			{
				"categoryId":"1",
				"content":"文章内容",
				"createTime":"2023-08-10 07:20:11",
				"id":"1",
				"isComment":"0",
				"isTop":"1",
				"status":"0",
				"summary":"文章摘要",
				"thumbnail":"文章缩略图.png|jpg",
				"title":"文章标题",
				"viewCount":"文章被浏览的次数"
			}
		],
		"total":"1"
	},
	"msg":"操作成功"
}

2.2 代码实现

第一步,在keke-framework的domain/dto包下创建SelectArticleDto

package com.keke.domain.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
//后台查询文章用的Dto
public class SelectArticleDto {
     //标题
     private String title;
     //文章摘要
     private String summary;
}

第二步:在keke-blog的controller包下的ArticleController包下新增分页查询接口接口

package com.keke.controller;

import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.domain.dto.SelectArticleDto;
import com.keke.service.ArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/content/article")
public class ArticleController {

     @Autowired
     private ArticleService articleService;

     @PostMapping
     public ResponseResult add(@RequestBody AddArticleDto addArticleDto){
          return articleService.add(addArticleDto);
     }

     @GetMapping("/list")
     public ResponseResult selectPageArticle(SelectArticleDto selectArticleDto, Integer pageNum, Integer pageSize){
          return articleService.selectPageArticle(selectArticleDto,pageNum,pageSize);
     }
}

第三步,在keke-framework的service包下ArticleService新增如下

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.domain.dto.SelectArticleDto;
import com.keke.domain.entity.Article;
import org.springframework.web.bind.annotation.RequestMapping;


/**
 * 文章表(Article)表服务接口
 *
 * @author makejava
 * @since 2023-10-10 09:59:37
 */
public interface ArticleService extends IService<Article> {


     ResponseResult hotArticleList();

     ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId);

     ResponseResult getArticleDetail(Long id);

     ResponseResult updateViewCount(Long id);

     ResponseResult add(AddArticleDto addArticleDto);


     ResponseResult selectPageArticle(SelectArticleDto selectArticleDto, Integer pageNum, Integer pageSize);
}

第四步,在keke-framework的service/impl包下创建ArticleServiceImpl实现未实现方法如下

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.constants.SystemConstants;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.domain.dto.SelectArticleDto;
import com.keke.domain.entity.Article;
import com.keke.domain.entity.ArticleTag;
import com.keke.domain.entity.Category;
import com.keke.domain.vo.ArticleDetailVo;
import com.keke.domain.vo.ArticleListVo;
import com.keke.domain.vo.HotArticleVo;
import com.keke.domain.vo.PageVo;
import com.keke.mapper.ArticleMapper;
import com.keke.service.ArticleService;
import com.keke.service.ArticleTagService;
import com.keke.service.CategoryService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 文章表(Article)表服务实现类
 *
 * @author makejava
 * @since 2023-10-10 09:59:39
 */
@Service("articleService")
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {

     @Autowired
     private CategoryService categoryService;

     @Autowired
     private ArticleService articleService;

     //显然redisCache是对redisTemplate的封装
     @Autowired
     private RedisCache redisCache;

     @Autowired
     private ArticleTagService articleTagService;
     //查询热门文章
     @Override
     public ResponseResult hotArticleList() {

          //---------------------------------------每调用这个方法就从redis查询文章的浏览量,展示在热门文章列表--------------------------------------------------------------
          Map<String, Integer> viewCountMap = redisCache.getCacheMap(SystemConstants.REDIS_ARTICLE_KEY);
          List<Article> articleList = viewCountMap.entrySet()
                  .stream()
                  .map(entry -> new Article(Long.valueOf(entry.getKey()), entry.getValue().longValue()))
                  .collect(Collectors.toList());
          articleService.updateBatchById(articleList);
          //-----------------------------------------------------------------------------------------------------
          //查询热门文章 封装成ResponseResult返回
          LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //必须是正式文章
          lambdaQueryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);
          //按照浏览量进行排序
          lambdaQueryWrapper.orderByDesc(Article::getViewCount);
          //最多查询10条,设置mp分页对象的参数分别为1和10
          Page<Article> page = new Page<>(SystemConstants.ARTICLE_STATUS_CURRENT,SystemConstants.ARTICLE_STATUS_SIZE);
          //将page对象和lambdaQueryWrapper查询条件封装成page
          page(page,lambdaQueryWrapper);
          //page.getRecords()获取到所有符合条件的数据(也就是文章)
          List<Article> articles = page.getRecords();
          //BeanCopy
          List<HotArticleVo> hotArticleVos = BeanCopyUtils.copyBeanList(articles, HotArticleVo.class);
          //返回ResponseResult对象
          return ResponseResult.okResult(hotArticleVos);
     }

     //分页查询文章列表,包含首页和分类页面的文章列表分页查询
     @Override
     public ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId) {
          //查询条件
          LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //首先明确categoryId在首页中不传,在分类文章页面中会传,所以我们要判断
          /*
          这里是lambdaQueryWrapper.eq三个参数的写法,第一个参数返回值是boolean类型,如果判断为true
          那么后续就会把后面的判断加入sql语句当中
           */
          lambdaQueryWrapper.eq(Objects.nonNull(categoryId)&&categoryId>0,Article::getCategoryId,categoryId);
          //状态是正式发布的,置顶的文章要显示在最前面,对isTop进行降序
          lambdaQueryWrapper.eq(Article::getStatus,SystemConstants.ARTICLE_STATUS_NORMAL);
          lambdaQueryWrapper.orderByDesc(Article::getIsTop);
          //分页查询
          Page<Article> page = new Page<>(pageNum,pageSize);
          page(page,lambdaQueryWrapper);
          //查询categoryName
          List<Article> articles = page.getRecords();//有categoryId,但无categoryName
          //拿着categoryId去查询categoryName,然后封装到article中
          /*
          for (Article article : articles) {
               Category category = categoryService.getById(article.getCategoryId());
               article.setCategoryName(category.getName());
          }
          */
          articles.stream()
                  //setter返回的是对象
                  .map(article -> article.setCategoryName(categoryService.getById(article.getCategoryId()).getName()))
                  .collect(Collectors.toList());
          //封装Vo
          List<ArticleListVo> articleListVos = BeanCopyUtils.copyBeanList(page.getRecords(), ArticleListVo.class);
          //封装PageVo
          PageVo pageVo = new PageVo(articleListVos,page.getTotal());
          return ResponseResult.okResult(pageVo);
     }

     //查询文章详情
     @Override
     public ResponseResult getArticleDetail(Long id) {
          //根据id查询文章
          Article article = getById(id);
//          //--------------------------------------从redis中获取viewCount--------------------------------------------------------------
//          //获取到的是redis当中的Integer类型的viewCount
//          Integer viewCount = redisCache.getCacheMapValue(SystemConstants.REDIS_ARTICLE_KEY, id.toString());
//          //设置article的viewCount为从redis中查出来的数据
//          article.setViewCount(viewCount.longValue());

          //转化称Vo
          ArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class);
          //根据分类id查询分类名称
          Long categoryId = articleDetailVo.getCategoryId();
          Category category = categoryService.getById(categoryId);
          //如果没有获取到id,就不设置
          if(categoryId!=null){
               articleDetailVo.setCategoryName(category.getName());
          }
          //封装响应体
          return ResponseResult.okResult(articleDetailVo);
     }

     @Override
     public ResponseResult updateViewCount(Long id) {
          //更新浏览量(自增)
          redisCache.incrementCacheMapValue(SystemConstants.REDIS_ARTICLE_KEY,id.toString(),SystemConstants.REDIS_ARTICLE_VIEW_COUNT_INCREMENT);
          return ResponseResult.okResult();
     }

     @Override
     //因为这里做了两次数据库插入操作,必须保证两个操作同时失败或者成功
     public ResponseResult add(AddArticleDto addArticleDto) {
          //添加到文章表
          Article article = BeanCopyUtils.copyBean(addArticleDto, Article.class);
          save(article);
          //添加到文章标签关联表
          List<Long> tags = addArticleDto.getTags();
          //拿到ArticleTag对象集合
          List<ArticleTag> articleTags = tags.stream()
                  .map(new Function<Long, ArticleTag>() {
                       @Override
                       public ArticleTag apply(Long tagId) {
                            ArticleTag articleTag = new ArticleTag(article.getId(), tagId);
                            return articleTag;
                       }
                  })
                  .collect(Collectors.toList());
          //然后批量插入数据库
          articleTagService.saveBatch(articleTags);
          return ResponseResult.okResult();
     }

     @Override
     public ResponseResult selectPageArticle(SelectArticleDto selectArticleDto, Integer pageNum, Integer pageSize) {
          LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          lambdaQueryWrapper.like(StringUtils.hasText(selectArticleDto.getTitle()),Article::getTitle,selectArticleDto.getTitle());
          lambdaQueryWrapper.like(StringUtils.hasText(selectArticleDto.getSummary()),Article::getSummary,selectArticleDto.getSummary());
          Page<Article> page = new Page<>(pageNum,pageSize);
          page(page,lambdaQueryWrapper);
          PageVo pageVo = new PageVo(page.getRecords(),page.getTotal());
          return ResponseResult.okResult(pageVo);
     }
}

第四步:测试

打开redis,前端工程,运行后台项目,登录管理员账号密码,访问文章管理页面,文章出来了,并且可以根据标题和摘要进行模糊查询

2. 修改文章

点击文章列表中的修改按钮可以跳转到写博文页面。回显示该文章的具体信息。用户可以在该页面修改文章信息。点击更新按钮后修改文章

这个功能的实现首先需要能够根据文章id查询文章的详细信息这样才能实现文章的回显

2.1 回显文章接口

2.1.1 接口分析

先查询根据文章id查询对应的文章

请求方式

请求路径

是否需求token头

Get

content/article/{id}

请求参数:

id: 文章id

响应格式

{
	"code":200,
	"data":{
		"categoryId":"1",
		"content":"文章内容",
		"createBy":"1",
		"createTime":"2023-08-28 15:15:46",
		"delFlag":0,
		"id":"10",
		"isComment":"0",
		"isTop":"1",
		"status":"0",
		"summary":"文章摘要",
		"tags":[
			"1",
			"4",
			"5"
		],
		"thumbnail":"文章缩略图.png|jpg",
		"title":"文章标题",
		"updateBy":"1",
		"updateTime":"2022-08-28 15:15:46",
		"viewCount":"0"
	},
	"msg":"操作成功"
}
2.1.2 代码实现

第一步:在keke-admin的controller层ArticleController新增查询文章详情接口

package com.keke.controller;

import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.domain.dto.SelectArticleDto;
import com.keke.service.ArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/content/article")
public class ArticleController {

     @Autowired
     private ArticleService articleService;

     @PostMapping
     public ResponseResult add(@RequestBody AddArticleDto addArticleDto){
          return articleService.add(addArticleDto);
     }

     @GetMapping("/list")
     public ResponseResult selectPageArticle(SelectArticleDto selectArticleDto, Integer pageNum, Integer pageSize){
          return articleService.selectPageArticle(selectArticleDto,pageNum,pageSize);
     }

     @GetMapping("/{id}")
     public ResponseResult getInfoById(@PathVariable("id") Long id){
          return articleService.getInfoById(id);
     }
}

第二步:在keke-framework的domain/vo新增AdminArticleInfoVo,用于封装返回数据

package com.keke.domain.vo;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AdminArticleInfoVo {
     //id
     private Long id;
     //标题
     private String title;
     //文章内容
     private String content;
     //文章摘要
     private String summary;
     //所属分类id
     private Long categoryId;
     //缩略图
     private String thumbnail;
     //是否置顶(0否,1是)
     private String isTop;
     //状态(0已发布,1草稿)
     private String status;
     //访问量
     private Long viewCount;
     //是否允许评论 1是,0否
     private String isComment;

     private Long createBy;

     private Date createTime;

     private Long updateBy;

     private Date updateTime;
     //删除标志(0代表未删除,1代表已删除)
     private Integer delFlag;

     private List<Long> tags;
}

第三步:在keke-framework的Service层ArticleService新增

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.domain.dto.SelectArticleDto;
import com.keke.domain.entity.Article;
import org.springframework.web.bind.annotation.RequestMapping;


/**
 * 文章表(Article)表服务接口
 *
 * @author makejava
 * @since 2023-10-10 09:59:37
 */
public interface ArticleService extends IService<Article> {


     ResponseResult hotArticleList();

     ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId);

     ResponseResult getArticleDetail(Long id);

     ResponseResult updateViewCount(Long id);

     ResponseResult add(AddArticleDto addArticleDto);


     ResponseResult selectPageArticle(SelectArticleDto selectArticleDto, Integer pageNum, Integer pageSize);



     ResponseResult getInfoById(Long id);
}

第三步:在keke-framework的Service/impl层ArticleServiceImpl实现未实现的方法

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.constants.SystemConstants;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.domain.dto.SelectArticleDto;
import com.keke.domain.entity.Article;
import com.keke.domain.entity.ArticleTag;
import com.keke.domain.entity.Category;
import com.keke.domain.vo.*;
import com.keke.mapper.ArticleMapper;
import com.keke.service.ArticleService;
import com.keke.service.ArticleTagService;
import com.keke.service.CategoryService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 文章表(Article)表服务实现类
 *
 * @author makejava
 * @since 2023-10-10 09:59:39
 */
@Service("articleService")
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {

     @Autowired
     private CategoryService categoryService;

     @Autowired
     private ArticleService articleService;

     //显然redisCache是对redisTemplate的封装
     @Autowired
     private RedisCache redisCache;

     @Autowired
     private ArticleTagService articleTagService;
     //查询热门文章
     @Override
     public ResponseResult hotArticleList() {

          //---------------------------------------每调用这个方法就从redis查询文章的浏览量,展示在热门文章列表--------------------------------------------------------------
          Map<String, Integer> viewCountMap = redisCache.getCacheMap(SystemConstants.REDIS_ARTICLE_KEY);
          List<Article> articleList = viewCountMap.entrySet()
                  .stream()
                  .map(entry -> new Article(Long.valueOf(entry.getKey()), entry.getValue().longValue()))
                  .collect(Collectors.toList());
          articleService.updateBatchById(articleList);
          //-----------------------------------------------------------------------------------------------------
          //查询热门文章 封装成ResponseResult返回
          LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //必须是正式文章
          lambdaQueryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);
          //按照浏览量进行排序
          lambdaQueryWrapper.orderByDesc(Article::getViewCount);
          //最多查询10条,设置mp分页对象的参数分别为1和10
          Page<Article> page = new Page<>(SystemConstants.ARTICLE_STATUS_CURRENT,SystemConstants.ARTICLE_STATUS_SIZE);
          //将page对象和lambdaQueryWrapper查询条件封装成page
          page(page,lambdaQueryWrapper);
          //page.getRecords()获取到所有符合条件的数据(也就是文章)
          List<Article> articles = page.getRecords();
          //BeanCopy
          List<HotArticleVo> hotArticleVos = BeanCopyUtils.copyBeanList(articles, HotArticleVo.class);
          //返回ResponseResult对象
          return ResponseResult.okResult(hotArticleVos);
     }

     //分页查询文章列表,包含首页和分类页面的文章列表分页查询
     @Override
     public ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId) {
          //查询条件
          LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //首先明确categoryId在首页中不传,在分类文章页面中会传,所以我们要判断
          /*
          这里是lambdaQueryWrapper.eq三个参数的写法,第一个参数返回值是boolean类型,如果判断为true
          那么后续就会把后面的判断加入sql语句当中
           */
          lambdaQueryWrapper.eq(Objects.nonNull(categoryId)&&categoryId>0,Article::getCategoryId,categoryId);
          //状态是正式发布的,置顶的文章要显示在最前面,对isTop进行降序
          lambdaQueryWrapper.eq(Article::getStatus,SystemConstants.ARTICLE_STATUS_NORMAL);
          lambdaQueryWrapper.orderByDesc(Article::getIsTop);
          //分页查询
          Page<Article> page = new Page<>(pageNum,pageSize);
          page(page,lambdaQueryWrapper);
          //查询categoryName
          List<Article> articles = page.getRecords();//有categoryId,但无categoryName
          //拿着categoryId去查询categoryName,然后封装到article中
          /*
          for (Article article : articles) {
               Category category = categoryService.getById(article.getCategoryId());
               article.setCategoryName(category.getName());
          }
          */
          articles.stream()
                  //setter返回的是对象
                  .map(article -> article.setCategoryName(categoryService.getById(article.getCategoryId()).getName()))
                  .collect(Collectors.toList());
          //封装Vo
          List<ArticleListVo> articleListVos = BeanCopyUtils.copyBeanList(page.getRecords(), ArticleListVo.class);
          //封装PageVo
          PageVo pageVo = new PageVo(articleListVos,page.getTotal());
          return ResponseResult.okResult(pageVo);
     }

     //查询文章详情
     @Override
     public ResponseResult getArticleDetail(Long id) {
          //根据id查询文章
          Article article = getById(id);
//          //--------------------------------------从redis中获取viewCount--------------------------------------------------------------
//          //获取到的是redis当中的Integer类型的viewCount
//          Integer viewCount = redisCache.getCacheMapValue(SystemConstants.REDIS_ARTICLE_KEY, id.toString());
//          //设置article的viewCount为从redis中查出来的数据
//          article.setViewCount(viewCount.longValue());

          //转化称Vo
          ArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class);
          //根据分类id查询分类名称
          Long categoryId = articleDetailVo.getCategoryId();
          Category category = categoryService.getById(categoryId);
          //如果没有获取到id,就不设置
          if(categoryId!=null){
               articleDetailVo.setCategoryName(category.getName());
          }
          //封装响应体
          return ResponseResult.okResult(articleDetailVo);
     }

     @Override
     public ResponseResult updateViewCount(Long id) {
          //更新浏览量(自增)
          redisCache.incrementCacheMapValue(SystemConstants.REDIS_ARTICLE_KEY,id.toString(),SystemConstants.REDIS_ARTICLE_VIEW_COUNT_INCREMENT);
          return ResponseResult.okResult();
     }

     @Override
     //因为这里做了两次数据库插入操作,必须保证两个操作同时失败或者成功
     public ResponseResult add(AddArticleDto addArticleDto) {
          //添加到文章表
          Article article = BeanCopyUtils.copyBean(addArticleDto, Article.class);
          save(article);
          //添加到文章标签关联表
          List<Long> tags = addArticleDto.getTags();
          //拿到ArticleTag对象集合
          List<ArticleTag> articleTags = tags.stream()
                  .map(new Function<Long, ArticleTag>() {
                       @Override
                       public ArticleTag apply(Long tagId) {
                            ArticleTag articleTag = new ArticleTag(article.getId(), tagId);
                            return articleTag;
                       }
                  })
                  .collect(Collectors.toList());
          //然后批量插入数据库
          articleTagService.saveBatch(articleTags);
          return ResponseResult.okResult();
     }

     @Override
     public ResponseResult selectPageArticle(SelectArticleDto selectArticleDto, Integer pageNum, Integer pageSize) {
          LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          lambdaQueryWrapper.like(StringUtils.hasText(selectArticleDto.getTitle()),Article::getTitle,selectArticleDto.getTitle());
          lambdaQueryWrapper.like(StringUtils.hasText(selectArticleDto.getSummary()),Article::getSummary,selectArticleDto.getSummary());
          Page<Article> page = new Page<>(pageNum,pageSize);
          page(page,lambdaQueryWrapper);
          PageVo pageVo = new PageVo(page.getRecords(),page.getTotal());
          return ResponseResult.okResult(pageVo);
     }

     @Override
     public ResponseResult getInfoById(Long id) {
          Article article = articleService.getById(id);
          LambdaQueryWrapper<ArticleTag> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          lambdaQueryWrapper.eq(ArticleTag::getArticleId,article.getId());
          List<ArticleTag> articleTags = articleTagService.list(lambdaQueryWrapper);
          List<Long> tags = articleTags.stream()
                  .map(new Function<ArticleTag, Long>() {
                       @Override
                       public Long apply(ArticleTag articleTag) {
                            return articleTag.getTagId();
                       }
                  })
                  .collect(Collectors.toList());
          AdminArticleInfoVo adminArticleInfoVo = BeanCopyUtils.copyBean(article, AdminArticleInfoVo.class);
          adminArticleInfoVo.setTags(tags);
          return ResponseResult.okResult(adminArticleInfoVo);
     }
}
2.1.3 测试

打开redis,前端工程,运行后台项目,登录管理员账号密码,点击文章管理,选一篇文章,看是否回显数据

这里测试成功

2.2 修改文章接口

修改文章中不仅要更新文章表,还要更新文章标签表

2.2.1 接口分析

请求方式

请求路径

是否需求token头

PUT

content/article

请求体参数格式:

{
    "categoryId":"1",
    "content":"文章内容",
    "createBy":"1",
    "createTime":"2023-08-28 15:15:46",
    "delFlag":0,
    "id":"10",
    "isComment":"0",
    "isTop":"1",
    "status":"0",
    "summary":"文章摘要",
    "tags":[
        "1",
        "4",
        "5"
    ],
    "thumbnail":"文章缩略图.png|jpg",
    "title":"文章标题",
    "updateBy":"1",
    "updateTime":"2022-08-28 15:15:46",
    "viewCount":"0"
}

 响应格式:

{
	"code":200,
	"msg":"操作成功"
}
2.2.2 代码实现

第一步:keke-admin的controller的ArticleController层加上修改文章的接口

package com.keke.controller;

import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.domain.dto.EditArticleDto;
import com.keke.domain.dto.SelectArticleDto;
import com.keke.service.ArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/content/article")
public class ArticleController {

     @Autowired
     private ArticleService articleService;

     @PostMapping
     public ResponseResult add(@RequestBody AddArticleDto addArticleDto){
          return articleService.add(addArticleDto);
     }

     @GetMapping("/list")
     public ResponseResult selectPageArticle(SelectArticleDto selectArticleDto, Integer pageNum, Integer pageSize){
          return articleService.selectPageArticle(selectArticleDto,pageNum,pageSize);
     }

     @GetMapping("/{id}")
     public ResponseResult getInfoById(@PathVariable("id") Long id){
          return articleService.getInfoById(id);
     }

     @PutMapping
     public ResponseResult editArticle(@RequestBody EditArticleDto editArticleDto){
          return articleService.editArticle(editArticleDto);
     }
}

第二步:keke-framework的domain/dto包下新增如下

package com.keke.domain.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class EditArticleDto {
     //id
     private Long id;
     //标题
     private String title;
     //文章内容
     private String content;
     //文章摘要
     private String summary;
     //所属分类id
     private Long categoryId;
     //缩略图
     private String thumbnail;
     //是否置顶(0否,1是)
     private String isTop;
     //状态(0已发布,1草稿)
     private String status;
     //访问量
     private Long viewCount;
     //是否允许评论 1是,0否
     private String isComment;

     private Long createBy;

     private Date createTime;

     private Long updateBy;

     private Date updateTime;
     //删除标志(0代表未删除,1代表已删除)
     private Integer delFlag;

     private List<Long> tags;
}

第三步:keke-framework的service包下的ArticleService

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.domain.dto.EditArticleDto;
import com.keke.domain.dto.SelectArticleDto;
import com.keke.domain.entity.Article;
import org.springframework.web.bind.annotation.RequestMapping;


/**
 * 文章表(Article)表服务接口
 *
 * @author makejava
 * @since 2023-10-10 09:59:37
 */
public interface ArticleService extends IService<Article> {


     ResponseResult hotArticleList();

     ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId);

     ResponseResult getArticleDetail(Long id);

     ResponseResult updateViewCount(Long id);

     ResponseResult add(AddArticleDto addArticleDto);


     ResponseResult selectPageArticle(SelectArticleDto selectArticleDto, Integer pageNum, Integer pageSize);



     ResponseResult getInfoById(Long id);

     ResponseResult editArticle(EditArticleDto editArticleDto);
}

第三步:keke-framework的service/impl包下的ArticleServiceImpl实现未实现的方法

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.constants.SystemConstants;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.domain.dto.EditArticleDto;
import com.keke.domain.dto.SelectArticleDto;
import com.keke.domain.entity.Article;
import com.keke.domain.entity.ArticleTag;
import com.keke.domain.entity.Category;
import com.keke.domain.vo.*;
import com.keke.mapper.ArticleMapper;
import com.keke.service.ArticleService;
import com.keke.service.ArticleTagService;
import com.keke.service.CategoryService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 文章表(Article)表服务实现类
 *
 * @author makejava
 * @since 2023-10-10 09:59:39
 */
@Service("articleService")
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {

     @Autowired
     private CategoryService categoryService;

     @Autowired
     private ArticleService articleService;

     //显然redisCache是对redisTemplate的封装
     @Autowired
     private RedisCache redisCache;

     @Autowired
     private ArticleTagService articleTagService;
     //查询热门文章
     @Override
     public ResponseResult hotArticleList() {

          //---------------------------------------每调用这个方法就从redis查询文章的浏览量,展示在热门文章列表--------------------------------------------------------------
          Map<String, Integer> viewCountMap = redisCache.getCacheMap(SystemConstants.REDIS_ARTICLE_KEY);
          List<Article> articleList = viewCountMap.entrySet()
                  .stream()
                  .map(entry -> new Article(Long.valueOf(entry.getKey()), entry.getValue().longValue()))
                  .collect(Collectors.toList());
          articleService.updateBatchById(articleList);
          //-----------------------------------------------------------------------------------------------------
          //查询热门文章 封装成ResponseResult返回
          LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //必须是正式文章
          lambdaQueryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);
          //按照浏览量进行排序
          lambdaQueryWrapper.orderByDesc(Article::getViewCount);
          //最多查询10条,设置mp分页对象的参数分别为1和10
          Page<Article> page = new Page<>(SystemConstants.ARTICLE_STATUS_CURRENT,SystemConstants.ARTICLE_STATUS_SIZE);
          //将page对象和lambdaQueryWrapper查询条件封装成page
          page(page,lambdaQueryWrapper);
          //page.getRecords()获取到所有符合条件的数据(也就是文章)
          List<Article> articles = page.getRecords();
          //BeanCopy
          List<HotArticleVo> hotArticleVos = BeanCopyUtils.copyBeanList(articles, HotArticleVo.class);
          //返回ResponseResult对象
          return ResponseResult.okResult(hotArticleVos);
     }

     //分页查询文章列表,包含首页和分类页面的文章列表分页查询
     @Override
     public ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId) {
          //查询条件
          LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //首先明确categoryId在首页中不传,在分类文章页面中会传,所以我们要判断
          /*
          这里是lambdaQueryWrapper.eq三个参数的写法,第一个参数返回值是boolean类型,如果判断为true
          那么后续就会把后面的判断加入sql语句当中
           */
          lambdaQueryWrapper.eq(Objects.nonNull(categoryId)&&categoryId>0,Article::getCategoryId,categoryId);
          //状态是正式发布的,置顶的文章要显示在最前面,对isTop进行降序
          lambdaQueryWrapper.eq(Article::getStatus,SystemConstants.ARTICLE_STATUS_NORMAL);
          lambdaQueryWrapper.orderByDesc(Article::getIsTop);
          //分页查询
          Page<Article> page = new Page<>(pageNum,pageSize);
          page(page,lambdaQueryWrapper);
          //查询categoryName
          List<Article> articles = page.getRecords();//有categoryId,但无categoryName
          //拿着categoryId去查询categoryName,然后封装到article中
          /*
          for (Article article : articles) {
               Category category = categoryService.getById(article.getCategoryId());
               article.setCategoryName(category.getName());
          }
          */
          articles.stream()
                  //setter返回的是对象
                  .map(article -> article.setCategoryName(categoryService.getById(article.getCategoryId()).getName()))
                  .collect(Collectors.toList());
          //封装Vo
          List<ArticleListVo> articleListVos = BeanCopyUtils.copyBeanList(page.getRecords(), ArticleListVo.class);
          //封装PageVo
          PageVo pageVo = new PageVo(articleListVos,page.getTotal());
          return ResponseResult.okResult(pageVo);
     }

     //查询文章详情
     @Override
     public ResponseResult getArticleDetail(Long id) {
          //根据id查询文章
          Article article = getById(id);
//          //--------------------------------------从redis中获取viewCount--------------------------------------------------------------
//          //获取到的是redis当中的Integer类型的viewCount
//          Integer viewCount = redisCache.getCacheMapValue(SystemConstants.REDIS_ARTICLE_KEY, id.toString());
//          //设置article的viewCount为从redis中查出来的数据
//          article.setViewCount(viewCount.longValue());

          //转化称Vo
          ArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class);
          //根据分类id查询分类名称
          Long categoryId = articleDetailVo.getCategoryId();
          Category category = categoryService.getById(categoryId);
          //如果没有获取到id,就不设置
          if(categoryId!=null){
               articleDetailVo.setCategoryName(category.getName());
          }
          //封装响应体
          return ResponseResult.okResult(articleDetailVo);
     }

     @Override
     public ResponseResult updateViewCount(Long id) {
          //更新浏览量(自增)
          redisCache.incrementCacheMapValue(SystemConstants.REDIS_ARTICLE_KEY,id.toString(),SystemConstants.REDIS_ARTICLE_VIEW_COUNT_INCREMENT);
          return ResponseResult.okResult();
     }

     @Override
     //因为这里做了两次数据库插入操作,必须保证两个操作同时失败或者成功
     public ResponseResult add(AddArticleDto addArticleDto) {
          //添加到文章表
          Article article = BeanCopyUtils.copyBean(addArticleDto, Article.class);
          save(article);
          //添加到文章标签关联表
          List<Long> tags = addArticleDto.getTags();
          //拿到ArticleTag对象集合
          List<ArticleTag> articleTags = tags.stream()
                  .map(new Function<Long, ArticleTag>() {
                       @Override
                       public ArticleTag apply(Long tagId) {
                            ArticleTag articleTag = new ArticleTag(article.getId(), tagId);
                            return articleTag;
                       }
                  })
                  .collect(Collectors.toList());
          //然后批量插入数据库
          articleTagService.saveBatch(articleTags);
          return ResponseResult.okResult();
     }

     @Override
     public ResponseResult selectPageArticle(SelectArticleDto selectArticleDto, Integer pageNum, Integer pageSize) {
          LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          lambdaQueryWrapper.like(StringUtils.hasText(selectArticleDto.getTitle()),Article::getTitle,selectArticleDto.getTitle());
          lambdaQueryWrapper.like(StringUtils.hasText(selectArticleDto.getSummary()),Article::getSummary,selectArticleDto.getSummary());
          Page<Article> page = new Page<>(pageNum,pageSize);
          page(page,lambdaQueryWrapper);
          PageVo pageVo = new PageVo(page.getRecords(),page.getTotal());
          return ResponseResult.okResult(pageVo);
     }

     @Override
     public ResponseResult getInfoById(Long id) {
          Article article = articleService.getById(id);
          LambdaQueryWrapper<ArticleTag> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          lambdaQueryWrapper.eq(ArticleTag::getArticleId,article.getId());
          List<ArticleTag> articleTags = articleTagService.list(lambdaQueryWrapper);
          List<Long> tags = articleTags.stream()
                  .map(new Function<ArticleTag, Long>() {
                       @Override
                       public Long apply(ArticleTag articleTag) {
                            return articleTag.getTagId();
                       }
                  })
                  .collect(Collectors.toList());
          AdminArticleInfoVo adminArticleInfoVo = BeanCopyUtils.copyBean(article, AdminArticleInfoVo.class);
          adminArticleInfoVo.setTags(tags);
          return ResponseResult.okResult(adminArticleInfoVo);
     }

     @Transactional
     @Override
     public ResponseResult editArticle(EditArticleDto editArticleDto) {
          //根据id更新文章
          Article article = BeanCopyUtils.copyBean(editArticleDto, Article.class);
          updateById(article);
          //删除原有的文章标签关联关系
          LambdaQueryWrapper<ArticleTag> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          lambdaQueryWrapper.eq(ArticleTag::getArticleId,article.getId());
          articleTagService.remove(lambdaQueryWrapper);
          //把新的文章标签关联关系批量存到数据库中
          List<Long> tagIds = editArticleDto.getTags();
          List<ArticleTag> articleTags = tagIds.stream()
                  .map(new Function<Long, ArticleTag>() {
                       @Override
                       public ArticleTag apply(Long tagId) {
                            ArticleTag articleTag = new ArticleTag();
                            articleTag.setArticleId(article.getId());
                            articleTag.setTagId(tagId);
                            return articleTag;
                       }
                  })
                  .collect(Collectors.toList());
          articleTagService.saveBatch(articleTags);
          return ResponseResult.okResult();
     }
}

第四步:测试,启动前端工程,redis,修改文章,点击更新

提示更新成功,我们看一下数据库里面的信息,尤其是标签文章关联表 

说明更新成功

3. 删除文章

3.1 接口分析

点击文章后面的删除按钮可以删除该文章。注意: 是逻辑删除不是物理删除

请求方式

请求路径

是否需求token头

DELETE

content/article/{id}

请求参数:

id:要删除的文章id

响应格式:

{
	"code":200,
	"msg":"操作成功"
}

 3.2 代码实现

第一步,在keke-admin的controller包下的ArticleController新增如下

package com.keke.controller;

import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.domain.dto.EditArticleDto;
import com.keke.domain.dto.SelectArticleDto;
import com.keke.service.ArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/content/article")
public class ArticleController {

     @Autowired
     private ArticleService articleService;

     @PostMapping
     public ResponseResult add(@RequestBody AddArticleDto addArticleDto){
          return articleService.add(addArticleDto);
     }

     @GetMapping("/list")
     public ResponseResult selectPageArticle(SelectArticleDto selectArticleDto, Integer pageNum, Integer pageSize){
          return articleService.selectPageArticle(selectArticleDto,pageNum,pageSize);
     }

     @GetMapping("/{id}")
     public ResponseResult getInfoById(@PathVariable("id") Long id){
          return articleService.getInfoById(id);
     }

     @PutMapping
     public ResponseResult editArticle(@RequestBody EditArticleDto editArticleDto){
          return articleService.editArticle(editArticleDto);
     }

     @DeleteMapping("/{id}")
     public ResponseResult deleteArticleById(@PathVariable("id") Long id){
          return articleService.deleteArticleById(id);
     }
}

 第二步:keke-framework的service下的ArticlerService新增如下

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.domain.dto.EditArticleDto;
import com.keke.domain.dto.SelectArticleDto;
import com.keke.domain.entity.Article;
import org.springframework.web.bind.annotation.RequestMapping;


/**
 * 文章表(Article)表服务接口
 *
 * @author makejava
 * @since 2023-10-10 09:59:37
 */
public interface ArticleService extends IService<Article> {


     ResponseResult hotArticleList();

     ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId);

     ResponseResult getArticleDetail(Long id);

     ResponseResult updateViewCount(Long id);

     ResponseResult add(AddArticleDto addArticleDto);


     ResponseResult selectPageArticle(SelectArticleDto selectArticleDto, Integer pageNum, Integer pageSize);



     ResponseResult getInfoById(Long id);

     ResponseResult editArticle(EditArticleDto editArticleDto);

     ResponseResult deleteArticleById(Long id);
}

第三步:在keke-framework的service/impl的ArticleServiceImpl中实现未实现的方法

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.constants.SystemConstants;
import com.keke.domain.ResponseResult;
import com.keke.domain.dto.AddArticleDto;
import com.keke.domain.dto.EditArticleDto;
import com.keke.domain.dto.SelectArticleDto;
import com.keke.domain.entity.Article;
import com.keke.domain.entity.ArticleTag;
import com.keke.domain.entity.Category;
import com.keke.domain.vo.*;
import com.keke.mapper.ArticleMapper;
import com.keke.service.ArticleService;
import com.keke.service.ArticleTagService;
import com.keke.service.CategoryService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 文章表(Article)表服务实现类
 *
 * @author makejava
 * @since 2023-10-10 09:59:39
 */
@Service("articleService")
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {

     @Autowired
     private CategoryService categoryService;

     @Autowired
     private ArticleService articleService;

     //显然redisCache是对redisTemplate的封装
     @Autowired
     private RedisCache redisCache;

     @Autowired
     private ArticleTagService articleTagService;
     //查询热门文章
     @Override
     public ResponseResult hotArticleList() {

          //---------------------------------------每调用这个方法就从redis查询文章的浏览量,展示在热门文章列表--------------------------------------------------------------
          Map<String, Integer> viewCountMap = redisCache.getCacheMap(SystemConstants.REDIS_ARTICLE_KEY);
          List<Article> articleList = viewCountMap.entrySet()
                  .stream()
                  .map(entry -> new Article(Long.valueOf(entry.getKey()), entry.getValue().longValue()))
                  .collect(Collectors.toList());
          articleService.updateBatchById(articleList);
          //-----------------------------------------------------------------------------------------------------
          //查询热门文章 封装成ResponseResult返回
          LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //必须是正式文章
          lambdaQueryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);
          //按照浏览量进行排序
          lambdaQueryWrapper.orderByDesc(Article::getViewCount);
          //最多查询10条,设置mp分页对象的参数分别为1和10
          Page<Article> page = new Page<>(SystemConstants.ARTICLE_STATUS_CURRENT,SystemConstants.ARTICLE_STATUS_SIZE);
          //将page对象和lambdaQueryWrapper查询条件封装成page
          page(page,lambdaQueryWrapper);
          //page.getRecords()获取到所有符合条件的数据(也就是文章)
          List<Article> articles = page.getRecords();
          //BeanCopy
          List<HotArticleVo> hotArticleVos = BeanCopyUtils.copyBeanList(articles, HotArticleVo.class);
          //返回ResponseResult对象
          return ResponseResult.okResult(hotArticleVos);
     }

     //分页查询文章列表,包含首页和分类页面的文章列表分页查询
     @Override
     public ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId) {
          //查询条件
          LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //首先明确categoryId在首页中不传,在分类文章页面中会传,所以我们要判断
          /*
          这里是lambdaQueryWrapper.eq三个参数的写法,第一个参数返回值是boolean类型,如果判断为true
          那么后续就会把后面的判断加入sql语句当中
           */
          lambdaQueryWrapper.eq(Objects.nonNull(categoryId)&&categoryId>0,Article::getCategoryId,categoryId);
          //状态是正式发布的,置顶的文章要显示在最前面,对isTop进行降序
          lambdaQueryWrapper.eq(Article::getStatus,SystemConstants.ARTICLE_STATUS_NORMAL);
          lambdaQueryWrapper.orderByDesc(Article::getIsTop);
          //分页查询
          Page<Article> page = new Page<>(pageNum,pageSize);
          page(page,lambdaQueryWrapper);
          //查询categoryName
          List<Article> articles = page.getRecords();//有categoryId,但无categoryName
          //拿着categoryId去查询categoryName,然后封装到article中
          /*
          for (Article article : articles) {
               Category category = categoryService.getById(article.getCategoryId());
               article.setCategoryName(category.getName());
          }
          */
          articles.stream()
                  //setter返回的是对象
                  .map(article -> article.setCategoryName(categoryService.getById(article.getCategoryId()).getName()))
                  .collect(Collectors.toList());
          //封装Vo
          List<ArticleListVo> articleListVos = BeanCopyUtils.copyBeanList(page.getRecords(), ArticleListVo.class);
          //封装PageVo
          PageVo pageVo = new PageVo(articleListVos,page.getTotal());
          return ResponseResult.okResult(pageVo);
     }

     //查询文章详情
     @Override
     public ResponseResult getArticleDetail(Long id) {
          //根据id查询文章
          Article article = getById(id);
//          //--------------------------------------从redis中获取viewCount--------------------------------------------------------------
//          //获取到的是redis当中的Integer类型的viewCount
//          Integer viewCount = redisCache.getCacheMapValue(SystemConstants.REDIS_ARTICLE_KEY, id.toString());
//          //设置article的viewCount为从redis中查出来的数据
//          article.setViewCount(viewCount.longValue());

          //转化称Vo
          ArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class);
          //根据分类id查询分类名称
          Long categoryId = articleDetailVo.getCategoryId();
          Category category = categoryService.getById(categoryId);
          //如果没有获取到id,就不设置
          if(categoryId!=null){
               articleDetailVo.setCategoryName(category.getName());
          }
          //封装响应体
          return ResponseResult.okResult(articleDetailVo);
     }

     @Override
     public ResponseResult updateViewCount(Long id) {
          //更新浏览量(自增)
          redisCache.incrementCacheMapValue(SystemConstants.REDIS_ARTICLE_KEY,id.toString(),SystemConstants.REDIS_ARTICLE_VIEW_COUNT_INCREMENT);
          return ResponseResult.okResult();
     }

     @Override
     //因为这里做了两次数据库插入操作,必须保证两个操作同时失败或者成功
     public ResponseResult add(AddArticleDto addArticleDto) {
          //添加到文章表
          Article article = BeanCopyUtils.copyBean(addArticleDto, Article.class);
          save(article);
          //添加到文章标签关联表
          List<Long> tags = addArticleDto.getTags();
          //拿到ArticleTag对象集合
          List<ArticleTag> articleTags = tags.stream()
                  .map(new Function<Long, ArticleTag>() {
                       @Override
                       public ArticleTag apply(Long tagId) {
                            ArticleTag articleTag = new ArticleTag(article.getId(), tagId);
                            return articleTag;
                       }
                  })
                  .collect(Collectors.toList());
          //然后批量插入数据库
          articleTagService.saveBatch(articleTags);
          return ResponseResult.okResult();
     }

     @Override
     public ResponseResult selectPageArticle(SelectArticleDto selectArticleDto, Integer pageNum, Integer pageSize) {
          LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          lambdaQueryWrapper.like(StringUtils.hasText(selectArticleDto.getTitle()),Article::getTitle,selectArticleDto.getTitle());
          lambdaQueryWrapper.like(StringUtils.hasText(selectArticleDto.getSummary()),Article::getSummary,selectArticleDto.getSummary());
          Page<Article> page = new Page<>(pageNum,pageSize);
          page(page,lambdaQueryWrapper);
          PageVo pageVo = new PageVo(page.getRecords(),page.getTotal());
          return ResponseResult.okResult(pageVo);
     }

     @Override
     public ResponseResult getInfoById(Long id) {
          Article article = articleService.getById(id);
          LambdaQueryWrapper<ArticleTag> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          lambdaQueryWrapper.eq(ArticleTag::getArticleId,article.getId());
          List<ArticleTag> articleTags = articleTagService.list(lambdaQueryWrapper);
          List<Long> tags = articleTags.stream()
                  .map(new Function<ArticleTag, Long>() {
                       @Override
                       public Long apply(ArticleTag articleTag) {
                            return articleTag.getTagId();
                       }
                  })
                  .collect(Collectors.toList());
          AdminArticleInfoVo adminArticleInfoVo = BeanCopyUtils.copyBean(article, AdminArticleInfoVo.class);
          adminArticleInfoVo.setTags(tags);
          return ResponseResult.okResult(adminArticleInfoVo);
     }

     //加上事务注解,保证修改文章表,和修改文章标签的关联表同时成功或者失败
     @Transactional
     @Override
     public ResponseResult editArticle(EditArticleDto editArticleDto) {
          //根据id更新文章
          Article article = BeanCopyUtils.copyBean(editArticleDto, Article.class);
          updateById(article);
          //删除原有的文章标签关联关系
          LambdaQueryWrapper<ArticleTag> lambdaQueryWrapper = new LambdaQueryWrapper<>();
          lambdaQueryWrapper.eq(ArticleTag::getArticleId,article.getId());
          articleTagService.remove(lambdaQueryWrapper);
          //把新的文章标签关联关系批量存到数据库中
          List<Long> tagIds = editArticleDto.getTags();
          List<ArticleTag> articleTags = tagIds.stream()
                  .map(new Function<Long, ArticleTag>() {
                       @Override
                       public ArticleTag apply(Long tagId) {
                            ArticleTag articleTag = new ArticleTag();
                            articleTag.setArticleId(article.getId());
                            articleTag.setTagId(tagId);
                            return articleTag;
                       }
                  })
                  .collect(Collectors.toList());
          articleTagService.saveBatch(articleTags);
          return ResponseResult.okResult();
     }

     @Override
     public ResponseResult deleteArticleById(Long id) {
          articleService.removeById(id);
          return ResponseResult.okResult();
     }
}

3.3 测试

可以看到删除成功 

数据库中 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值