一、后台模块-准备工作
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版本
下载链接:
如果下载出现安装失败问题,可以参见我这个博客,里面有详细的解决方案
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 测试
可以看到删除成功
数据库中