4、后台管理
可以说是若依框架吧
4.1、准备工作
同理前台博客
创建启动类
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.hashiqi.mapper")
@EnableSwagger2
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}
}
创建yml配置文件
server:
port: 8989
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/hashiqi_blog?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
servlet:
multipart:
max-file-size: 5MB
max-request-size: 10MB
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: isDeleted
logic-delete-value: 1
logic-not-delete-value: 0
id-type: auto
SQL表
创建标签表
DROP TABLE IF EXISTS `tb_tag`;
CREATE TABLE `tb_tag` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签名',
`create_by` bigint(20) NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
`update_by` bigint(20) NULL DEFAULT NULL,
`update_time` datetime(0) NULL DEFAULT NULL,
`is_deleted` int(1) NULL DEFAULT 0 COMMENT '删除标志(0代表未删除,1代表已删除)',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '标签' ROW_FORMAT = Dynamic;
Tag
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_tag")
public class Tag implements Serializable {
private static final long serialVersionUID=1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 标签名
*/
private String name;
private Long createBy;
private Date createTime;
private Long updateBy;
private Date updateTime;
/**
* 删除标志(0代表未删除,1代表已删除)
*/
private Integer isDeleted;
/**
* 备注
*/
private String remark;
}
TagService
import com.hashiqi.domain.entity.Tag;
import com.baomidou.mybatisplus.extension.service.IService;
public interface TagService extends IService<Tag> {
}
TagServiceImpl
import com.hashiqi.domain.entity.Tag;
import com.hashiqi.mapper.TagMapper;
import com.hashiqi.service.TagService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {
}
后台管理模块中的TagController
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.service.TagService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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/tag")
@Api(tags = "标签接口管理", value = "标签相关接口")
public class TagController {
@Autowired
private TagService tagService;
@GetMapping("/list")
@ApiOperation(value = "获取所有的标签")
public ResponseResult list() {
return ResponseResult.okResult(tagService.list());
}
}
Security相关配置
config包
import com.hashiqi.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
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf,因为如果是分布式系统,则不适合session
.csrf().disable()
// 不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口,允许匿名访问【即未登录状态下可以访问 .permitAll()代表都可以访问】
// .antMatchers("/login").anonymous()
// // 注销接口需要认证才能访问
// .antMatchers("/logout").authenticated()
// // 个人信息接口必须登录后才可访问
// .antMatchers("/user/userInfo").authenticated()
// 出上面外的所有请求全部不需要认证即可访问
.anyRequest().permitAll();
// 关闭默认的注销功能
http.logout().disable();
// 允许跨域
http.cors();
// 将token检验过滤器放在用户名和密码校验之前
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 配置认证和授权失败处理器
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
}
}
filter包
import com.alibaba.fastjson.JSON;
import com.hashiqi.constants.RedisKeyConstants;
import com.hashiqi.constants.SystemConstants;
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.entity.LoginUser;
import com.hashiqi.enums.AppHttpCodeEnum;
import com.hashiqi.utils.JwtUtil;
import com.hashiqi.utils.RedisCache;
import com.hashiqi.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
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取请求头中的token
String token = request.getHeader(SystemConstants.TOKEN);
if (!StringUtils.hasText(token)) {
// 说明该接口无需token,直接放行即可
filterChain.doFilter(request, response);
return;
}
// 解析获取userId
Claims claims = null;
try {
claims = JwtUtil.parseJWT(token);
} catch (Exception e) {
// token超时
// token非法请求
e.printStackTrace();
// 响应前端,需重新进行登录
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
WebUtils.renderString(response, JSON.toJSONString(result));
return;
}
String userId = claims.getSubject();
// 从redis中获取用户信息
LoginUser loginUser = redisCache.getCacheObject(RedisKeyConstants.ADMIN_LOGIN_ID + userId);
// redis过期
if (Objects.isNull(loginUser)) {
// 说明redis过期,提示重新登录
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
WebUtils.renderString(response, JSON.toJSONString(result));
return;
}
// 存入SecurityContextHolder
// TODO 现阶段未有权限信息
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
RedisKeyConstants
/**
* 后台管理用户登录key前缀
*/
public static final String ADMIN_LOGIN_ID = "admin:login:";
4.2、后台登录
后台的认证授权也使用SpringSecurity安全框架来实现。
4.2.1、需求分析
需要实现登录功能;
后台所有功能都必须登录才能使用。
4.2.2、接口设计
请求方式 | 请求路径 |
---|---|
POST | /user/login |
请求体:
{
"userName":"hashiqi",
"password":"123456"
}
响应格式:
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0ODBmOThmYmJkNmI0NjM0OWUyZjY2NTM0NGNjZWY2NSIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTY0Mzg3NDMxNiwiZXhwIjoxNjQzOTYwNzE2fQ.XASDASDSADWAD545S4AD"
},
"msg": "操作成功"
}
4.2.3、思路分析
登录
-
自定义登录接口
-
调用ProviderManager的方法进行认证 如果认证通过生成jwt
-
把用户信息存入redis中
-
-
自定义UserDetailsService
-
在这个实现类中去查询数据库,注意配置passwordEncoder为BCryptPasswordEncoder。
-
检验
-
定义jwt认证过滤器
-
获取token
-
解析token获取其中的userId
-
从redis中获取用户信息
-
存入SecurityContextHolder
-
4.2.4、准备工作
添加相应依赖
在公共子模块中已经存在,并且已经将该模块引入后台管理模块中。
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
4.2.5、登录接口代码实现
将前台博客模块中的BlogLoginController复制一份,命名为AdminLoginController,将LoginService进行注入。修改其请求路径地址。
AdminLoginController
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.entity.User;
import com.hashiqi.enums.AppHttpCodeEnum;
import com.hashiqi.exception.SystemException;
import com.hashiqi.service.AdminLoginService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 后台登录接口
*/
@RestController
@Api(tags = "后台登录接口管理")
@RequestMapping("/user")
public class AdminLoginController {
@Autowired
private AdminLoginService adminLoginService;
// 登录
@PostMapping("/login")
@ApiOperation(value = "登录")
public ResponseResult login(@RequestBody User user) {
if (!StringUtils.hasText(user.getUserName())) {
throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);
}
return adminLoginService.login(user);
}
}
复制BlogLoginService
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.entity.User;
public interface AdminLoginService {
// 登录
ResponseResult login(User user);
}
复制一份BlogServiceImpl,实现AdminService
import com.hashiqi.constants.RedisKeyConstants;
import com.hashiqi.constants.SystemConstants;
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.entity.LoginUser;
import com.hashiqi.domain.entity.User;
import com.hashiqi.service.AdminLoginService;
import com.hashiqi.utils.JwtUtil;
import com.hashiqi.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.Objects;
@Service
public class AdminLoginServiceImpl implements AdminLoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
/**
* 登录
* @param user 用户
* @return 返回结果集
*/
@Override
public ResponseResult login(User user) {
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 判断是否认证通过
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(RedisKeyConstants.ADMIN_LOGIN_ID + userId, loginUser);
// 把token封装并返回
Map<String, String> map = new HashMap<>();
map.put(SystemConstants.TOKEN, jwt);
return ResponseResult.okResult(map);
}
}
UserDetailServiceImpl
复制前台博客模块中的即可,但后面会有权限加入。
LoginUser
同上理
测试
要将SecurityConfig类中修改代码
.anyRequest().authenticated();
4.3、后台权限控制以及动态路由
4.3.1、需求分析
后台系统需要能实现不同的用户权限可以看到不同的功能。
用户只能使用他的权限所允许使用的功能。
4.3.2、功能设计
基于RBAC权限模型去实现这个功能。
4.3.3、数据库表分析
通过需求去分析需要有哪些字段。
创建菜单表
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
`menu_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '菜单名称',
`parent_id` bigint(20) NULL DEFAULT 0 COMMENT '父菜单ID',
`order_num` int(4) NULL DEFAULT 0 COMMENT '显示顺序',
`path` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '路由地址',
`component` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '组件路径',
`is_frame` int(1) NULL DEFAULT 1 COMMENT '是否为外链(0是 1否)',
`menu_type` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
`visible` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
`status` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
`perms` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '#' COMMENT '菜单图标',
`create_by` bigint(20) NULL DEFAULT NULL COMMENT '创建者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '备注',
`is_deleted` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2031 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜单权限表' ROW_FORMAT = Dynamic;
创建角色表
CREATE TABLE `sys_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`role_name` varchar(30) NOT NULL COMMENT '角色名称',
`role_key` varchar(100) NOT NULL COMMENT '角色权限字符串',
`role_sort` int(4) NOT NULL COMMENT '显示顺序',
`status` char(1) NOT NULL COMMENT '角色状态(0正常 1停用)',
`is_deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8 COMMENT='角色信息表';
创建角色菜单关联表
CREATE TABLE `sys_role_menu` (
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
`menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色和菜单关联表';
创建用户角色关联表
CREATE TABLE `sys_user_role` (
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户和角色关联表';
4.3.4、接口设计
4.3.4.1、getInfo接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
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"
//...
],
"roles":[
"admin"
],
"user":{
"avatar":"http:/xxx.clouddn.com/2022/03/05/75fd15587811443a9a9a771f24da458d.png",
"email":"545487956@qq.com",
"id":1,
"nickName":"xxx",
"sex":"1"
}
},
"msg":"操作成功"
}
4.3.4.2、getRouters接口
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /getRouters | 需要token请求头 |
请求参数:无
响应格式:
前端为了实现动态路由的效果,需要后端有接口能返回用户所能访问的菜单数据。
注意:返回的菜单数据需要体现父子菜单的层级关系
如果用户id为1代表管理员,menus中需要有所有菜单类型为C或者M的,状态为正常的,未被删除的权限。
扩展
“C” 可能表示 “Category”(分类)的缩写。在这种情况下,菜单类型为 “C” 可能指示该菜单项是一个分类或者是一个父菜单,包含其他子菜单项。
“M” 可能表示 “Menu”(菜单)的缩写。在这种情况下,菜单类型为 “M” 可能指示该菜单项是一个实际的菜单项,通常与其他父菜单或分类关联。
4.3.5、代码实现
4.3.5.1、准备工作
生成menu和role表对应的类
4.3.5.2、getInfo接口实现
AdminUserInfoVO
import com.hashiqi.domain.vo.UserInfoVO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class AdminUserInfoVO {
private List<String> permissions;
private List<String> roles;
private UserInfoVO user;
}
AdminLoginController
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.entity.LoginUser;
import com.hashiqi.domain.entity.User;
import com.hashiqi.domain.vo.UserInfoVO;
import com.hashiqi.entity.vo.AdminUserInfoVO;
import com.hashiqi.enums.AppHttpCodeEnum;
import com.hashiqi.exception.SystemException;
import com.hashiqi.service.AdminLoginService;
import com.hashiqi.service.MenuService;
import com.hashiqi.service.RoleService;
import com.hashiqi.utils.BeanCopyUtils;
import com.hashiqi.utils.SecurityUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 后台登录接口
*/
@RestController
@Api(tags = "后台登录接口管理")
@RequestMapping("/user")
public class AdminLoginController {
@Autowired
private AdminLoginService adminLoginService;
@Autowired
private MenuService menuService;
@Autowired
private RoleService roleService;
// 登录
@PostMapping("/login")
@ApiOperation(value = "登录")
public ResponseResult login(@RequestBody User user) {
if (!StringUtils.hasText(user.getUserName())) {
throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);
}
return adminLoginService.login(user);
}
@GetMapping("/getInfo")
@ApiOperation(value = "获取用户信息")
public ResponseResult<AdminUserInfoVO> getInfo() {
// 获取当前登录的用户
LoginUser loginUser = SecurityUtils.getLoginUser();
// 获取用户信息
User user = loginUser.getUser();
Long userId = user.getId();
// 根据用户id查询权限信息
List<String> perms = menuService.selectPermsByUserId(userId);
// 根据用户id查询角色信息
List<String> roleKeyList = roleService.selectRoleKeyByUserId(userId);
// 将User转换成UserInfoVO
UserInfoVO userInfoVO = BeanCopyUtils.copyBean(user, UserInfoVO.class);
// 封装数据并返回
AdminUserInfoVO adminUserInfoVO = new AdminUserInfoVO(perms, roleKeyList, userInfoVO);
return ResponseResult.okResult(adminUserInfoVO);
}
}
SystemConstants
/**
* 用户类型为超级管理员,指定用户id为1
*/
public static final Long BLOG_SUPER_ADMIN_ID = 1L;
/**
* 用户类型为管理员
*/
public static final Long BLOG_ADMIN = 1L;
/**
* 用户类型为普通用户
*/
public static final Long BLOG_USER = 0L;
/**
* 菜单标识
*/
public static final String MENU = "C";
/**
* 按钮标识
*/
public static final String BUTTON = "F";
/**
* 角色类型为管理员
*/
public static final String BLOG_ADMIN_REMARK = "admin";
RoleService
import com.hashiqi.entity.Role;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
public interface RoleService extends IService<Role> {
// 根据用户id查询角色信息
List<String> selectRoleKeyByUserId(Long userId);
}
RoleServiceImpl
import com.hashiqi.constants.SystemConstants;
import com.hashiqi.entity.Role;
import com.hashiqi.mapper.RoleMapper;
import com.hashiqi.service.RoleService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
/**
* 根据用户id查询角色信息
* @param userId 用户id
* @return 角色集合
*/
@Override
public List<String> selectRoleKeyByUserId(Long userId) {
List<String> roleKeys = new ArrayList<>();
// 判断是否是管理员,如果是返回集合中需要有admin
if (SystemConstants.BLOG_SUPER_ADMIN_ID.equals(userId)) {
roleKeys.add(SystemConstants.BLOG_ADMIN_REMARK);
return roleKeys;
}
// 否,则查询用户所对应的角色信息
roleKeys = getBaseMapper().selectRoleKeyByUserId(userId);
return roleKeys;
}
}
RoleMapper
import com.hashiqi.entity.Role;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;
public interface RoleMapper extends BaseMapper<Role> {
// 根据用户id查询角色信息
List<String> selectRoleKeyByUserId(Long userId);
}
RoleMapper.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.hashiqi.mapper.RoleMapper">
<!-- 根据用户id查询角色信息 -->
<select id="selectRoleKeyByUserId" resultType="java.lang.String">
SELECT
r.role_key
FROM
hashiqi_blog.sys_user_role ur
LEFT JOIN
hashiqi_blog.sys_role r ON ur.role_id = r.id
WHERE
ur.user_id = #{userId}
AND
r.status = 0
AND
r.is_deleted = 0
</select>
</mapper>
MenuService
import com.hashiqi.entity.Menu;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
public interface MenuService extends IService<Menu> {
// 根据用户id查询权限信息
List<String> selectPermsByUserId(Long userId);
}
MenuServiceImpl
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hashiqi.constants.SystemConstants;
import com.hashiqi.entity.Menu;
import com.hashiqi.mapper.MenuMapper;
import com.hashiqi.service.MenuService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {
/**
* 根据用户id查询权限信息
* @param userId 用户id
* @return 菜单集合
*/
@Override
public List<String> selectPermsByUserId(Long userId) {
// 如果是超级管理员,返回所有权限
if (SystemConstants.BLOG_SUPER_ADMIN_ID.equals(userId)) {
List<Menu> menus = this.list(new LambdaQueryWrapper<Menu>()
.in(Menu::getMenuType, SystemConstants.MENU, SystemConstants.BUTTON)
.eq(Menu::getStatus, SystemConstants.STATUS_NORMAL));
return menus.stream().map(Menu::getPerms).collect(Collectors.toList());
}
// 如果不是,返回对应的权限
return getBaseMapper().selectPermsByUserId(userId);
}
}
MenuMapper
import com.hashiqi.entity.Menu;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;
public interface MenuMapper extends BaseMapper<Menu> {
// 根据用户id查询权限信息
List<String> selectPermsByUserId(Long userId);
}
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.hashiqi.mapper.MenuMapper">
<!-- 根据用户id查询权限信息 -->
<select id="selectPermsByUserId" resultType="java.lang.String">
SELECT
DISTINCT m.perms
FROM
hashiqi_blog.sys_user_role ur
LEFT JOIN
hashiqi_blog.sys_role_menu rm ON ur.role_id = rm.role_id
LEFT JOIN
hashiqi_blog.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.is_deleted = 0
</select>
</mapper>
4.3.5.3、getRouters接口实现
AdminLoginController
@GetMapping("/getRouters")
@ApiOperation(value = "获取菜单路由树结构")
public ResponseResult<RouterVO> getRouters() {
Long userId = SecurityUtils.getUserId();
// 查询menu,封装成tree结构,并返回
List<Menu> menus = menuService.selectRouterMenuTreeByUserId(userId);
return ResponseResult.okResult(new RouterVO(menus));
}
MenuService
// 获取菜单路由树结构
List<Menu> selectRouterMenuTreeByUserId(Long userId);
MenuServiceImpl
/**
* 获取菜单路由树结构
* @param userId 用户id
* @return 菜单路由树结构
*/
@Override
public List<Menu> selectRouterMenuTreeByUserId(Long userId) {
List<Menu> menus;
// 判断是否是超级管理员
if (SecurityUtils.isAdmin()) {
// 如果是,获取所有符合要求的Menu
menus = getBaseMapper().selectAllRouterMenu();
} else {
// 如果不是,则查询当前用户所具有的的菜单权限
menus = getBaseMapper().selectRouterMenuTreeByUserId(userId);
}
// 构建菜单权限路由树结构
return builderMenuTree(menus);
}
/**
* 构建菜单权限路由树结构
*
* @param menus 菜单权限
* @return 菜单权限路由树结构
*/
private List<Menu> builderMenuTree(List<Menu> menus) {
// 首先,根节点下的子菜单并赋值给children,然后通过其作为父菜单继续往下找其子菜单
return menus.stream().filter(menu -> SystemConstants.ROUTER_ROOT.equals(menu.getParentId()))
.map(menu -> menu.setChildren(getChildren(menu, menus)))
.collect(Collectors.toList());
}
/**
* 获取当前菜单的子菜单
* @param parentMenu 当前菜单
* @return 当前菜单的子菜单
*/
private List<Menu> getChildren(Menu parentMenu, List<Menu> menus) {
return menus.stream().filter(
menu -> menu.getParentId().equals(parentMenu.getId()))
.map(menu -> menu.setChildren(getChildren(menu, menus)))
.collect(Collectors.toList());
}
MenuMapper
// 返回所有符合要求的Menu
List<Menu> selectAllRouterMenu();
// 查询当前用户所具有的的菜单权限
List<Menu> selectRouterMenuTreeByUserId(Long userId);
MenuMapper.xml
<!-- 返回所有符合要求的Menu -->
<select id="selectAllRouterMenu" resultType="com.hashiqi.entity.Menu">
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
hashiqi_blog.sys_menu m
WHERE
m.menu_type IN ('C', 'M')
AND
m.status = 0
AND
m.is_deleted = 0
</select>
<!-- 查询当前用户所具有的的菜单权限 -->
<select id="selectRouterMenuTreeByUserId" resultType="com.hashiqi.entity.Menu">
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
hashiqi_blog.sys_user_role ur
LEFT JOIN
hashiqi_blog.sys_role_menu rm ON ur.role_id = rm.role_id
LEFT JOIN
hashiqi_blog.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.is_deleted = 0
</select>
当然,这也可以根据所设字段进行相应的排序。
RouterVO
import com.hashiqi.entity.Menu;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class RouterVO {
private List<Menu> menus;
}
SystemConstants
/**
* 菜单根节点标识
*/
public static final Long ROUTER_ROOT = 0L;
4.4、退出登录接口
4.4.1、接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
POST | /user/logout | 需要token请求头 |
响应格式:
{
"code": 200,
"msg": "操作成功"
}
4.4.2、代码实现
实现的操作:删除redis中的用户信息
AdminLoginController
@PostMapping("/user/logout")
@ApiOperation(value = "退出登录")
public ResponseResult logout() {
return adminLoginService.logout();
}
AdminLoginService
// 退出登录
ResponseResult logout();
AdminLoginServiceImpl
/**
* 退出登录
* @return 返回结果集
*/
@Override
public ResponseResult logout() {
// 获取token,解析获取userId
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 获取userId
Long userId = loginUser.getUser().getId();
// 删除redis中的用户信息
redisCache.deleteObject(RedisKeyConstants.ADMIN_LOGIN_ID + userId);
return ResponseResult.okResult();
}
SecurityConfig
import com.hashiqi.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
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf,因为如果是分布式系统,则不适合session
.csrf().disable()
// 不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口,允许匿名访问【即未登录状态下可以访问 .permitAll()代表都可以访问】
.antMatchers("/user/login").anonymous()
// // 注销接口需要认证才能访问
// .antMatchers("/logout").authenticated()
// 出上面外的所有请求全部不需要认证即可访问
.anyRequest().authenticated();
// 关闭默认的注销功能
http.logout().disable();
// 允许跨域
http.cors();
// 将token检验过滤器放在用户名和密码校验之前
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 配置认证和授权失败处理器
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
}
}
4.5、查询标签列表
4.5.1、需求分析
为了方便后期对文章进行管理,需要提供标签的功能,一个文章可以有多个标签。
在后台需要分页查询标签功能,要求能根据标签名进行分页查询。 后期可能会增加备注查询等需求。
注意:不能把删除了的标签查询出来。
4.5.2、标签表设计
之前已有。
4.5.3、接口设计
请求方式 | 请求路径 |
---|---|
Get | content/tag/list |
query格式请求参数:
pageNum: 页码
pageSize: 每页条数
name:标签名
remark:备注
响应格式:
{
"code":200,
"data":{
"rows":[
{
"id":4,
"name":"Java",
"remark":"xxx"
}
],
"total":1
},
"msg":"操作成功"
}
4.5.4、代码实现
TagController
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.dto.TagListDTO;
import com.hashiqi.domain.vo.PageVO;
import com.hashiqi.service.TagService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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/tag")
@Api(tags = "标签接口管理", value = "标签相关接口")
public class TagController {
@Autowired
private TagService tagService;
@GetMapping("/list")
@ApiOperation(value = "获取所有的标签【分页】")
public ResponseResult<PageVO> list(
Integer pageNum,
Integer pageSize,
TagListDTO tagListDto) {
return tagService.pageTagList(pageNum, pageSize, tagListDto);
}
}
TagService
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.dto.TagListDTO;
import com.hashiqi.domain.entity.Tag;
import com.baomidou.mybatisplus.extension.service.IService;
import com.hashiqi.domain.vo.PageVO;
public interface TagService extends IService<Tag> {
// 获取所有的标签【分页】
ResponseResult<PageVO> pageTagList(Integer pageNum, Integer pageSize, TagListDTO tagListDto);
}
TagServiceImpl
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.dto.TagListDTO;
import com.hashiqi.domain.entity.Tag;
import com.hashiqi.domain.vo.PageVO;
import com.hashiqi.mapper.TagMapper;
import com.hashiqi.service.TagService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* <p>
* 标签 服务实现类
* </p>
*
* @author hashiqi
* @since 2023-09-16
*/
@Service
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {
/**
* 获取所有的标签【分页】
* @param pageNum 当前页
* @param pageSize 页大小
* @param tagListDto 标签集合
* @return 所有标签分页列表
*/
@Override
public ResponseResult<PageVO> pageTagList(Integer pageNum, Integer pageSize, TagListDTO tagListDto) {
// 分页查询
Page<Tag> page = this.page(
new Page<>(pageNum, pageSize),
new LambdaQueryWrapper<Tag>()
.like(StringUtils.hasText(tagListDto.getName()), Tag::getName, tagListDto.getName())
.like(StringUtils.hasText(tagListDto.getRemark()), Tag::getRemark, tagListDto.getRemark()));
// 封装数据并返回
return ResponseResult.okResult(new PageVO(page.getRecords(), page.getTotal()));
}
}
4.6、新增标签
4.6.1、需求分析
点击标签管理的新增按钮可以实现新增标签的功能。
4.6.2、接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
POST | /content/tag | 需要token请求头 |
请求体格式:
{
"name":"c#",
"remark":"c++++"
}
响应格式:
{
"code":200,
"msg":"操作成功"
}
4.6.3、代码实现
TagController
@PostMapping
@ApiOperation(value = "添加标签")
public ResponseResult addTag(@RequestBody TagVO tagVO) {
return tagService.addTag(tagVO);
}
TagService
// 添加标签
ResponseResult addTag(TagVO tagVO);
TagServiceImpl
/**
* 添加标签
* @param tagVO 标签VO对象
* @return 结果集
*/
@Override
public ResponseResult addTag(TagVO tagVO) {
// 判断tagVO中标签名、备注【可以不写】是否为空
String name = tagVO.getName();
String remark = tagVO.getRemark();
if (!StringUtils.hasText(name)) {
throw new SystemException(AppHttpCodeEnum.TAG_NOT_NULL);
}
if (!StringUtils.hasText(remark)) {
throw new SystemException(AppHttpCodeEnum.REMARK_NOT_NULL);
}
// 判断标签名是否已存在
if (isTagExist(name)) {
throw new SystemException(AppHttpCodeEnum.TAG_EXIST);
}
// 将TagVO转成Tag
this.save(BeanCopyUtils.copyBean(tagVO, Tag.class));
return ResponseResult.okResult();
}
/**
* 判断标签名是否已存在
* @param name 标签名
* @return true:已存在;false:未存在
*/
private boolean isTagExist(String name) {
return this.count(new LambdaQueryWrapper<Tag>().eq(Tag::getName, name)) > 0;
}
TagVO
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TagVO {
private String name;
private String remark;
}
SystemConstants
TAG_NOT_NULL(513, "标签不能为空"),
TAG_EXIST(514, "标签已存在"),
REMARK_NOT_NULL(515, "备注不能为空");
4.7、修改标签
4.7.1、接口设计
4.7.1.1、获取标签信息
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /content/tag/{id} | 需要token请求头 |
请求参数放入请求路径中,使用ResutFul表达式
响应格式:
{
"code":200,
"data":{
"id":4,
"name":"Java",
"remark":"xxx"
},
"msg":"操作成功"
}
4.7.1.2、修改标签接口
请求方式 | 请求地址 | 请求头 |
---|---|---|
PUT | /content/tag | 需要token请求头 |
请求格式:
{
"id":7,
"name":"c#",
"remark":"c++"
}
响应格式:
{
"code":200,
"msg":"操作成功"
}
4.7.2、代码实现
4.7.2.1、获取标签信息代码实现
TagController
@GetMapping("/{id}")
@ApiOperation(value = "获取对应id的标签信息")
public ResponseResult getTag(@PathVariable("id") Long id) {
return ResponseResult.okResult(BeanCopyUtils.copyBean(tagService.getById(id), TagVO.class));
}
TagVO
import com.hashiqi.domain.dto.TagDTO;
public class TagVO extends TagDTO {
}
4.7.2.2、修改标签信息代码实现
TagController
@PutMapping
@ApiOperation(value = "根据标签id更新标签信息")
public ResponseResult updateTag(@RequestBody TagDTO tagDTO) {
return tagService.addOrUpdateTag(tagDTO);
}
经过思考,将修改和增加方法合并,虽然耦合性增强了,但同时减少了冗余代码,当然你也可以选择解耦,使用反射的方法对两个方法就分离。修改如下:
TagService
// 添加/更新标签
ResponseResult addOrUpdateTag(TagDTO tagDTO);
TagServiceImpl
/**
* 添加标签
* @param tagDTO 标签VO对象
* @return 结果集
*/
@Override
public ResponseResult addOrUpdateTag(TagDTO tagDTO) {
// 判断tagDTO中标签名、备注【可以不写】是否为空
String name = tagDTO.getName();
String remark = tagDTO.getRemark();
if (!StringUtils.hasText(name)) {
throw new SystemException(AppHttpCodeEnum.TAG_NOT_NULL);
}
if (!StringUtils.hasText(remark)) {
throw new SystemException(AppHttpCodeEnum.REMARK_NOT_NULL);
}
// 判断标签名是否已存在
if (isTagExist(name)) {
throw new SystemException(AppHttpCodeEnum.TAG_EXIST);
}
// 将TagDTO转成Tag,没有id为新增,相反更新
if (null != tagDTO.getId()) {
this.updateById(BeanCopyUtils.copyBean(tagDTO, Tag.class));
} else {
this.save(BeanCopyUtils.copyBean(tagDTO, Tag.class));
}
return ResponseResult.okResult();
}
TagDTO
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TagDTO {
private Long id;
private String name;
private String remark;
}
4.8、删除标签
4.8.1、需求分析
因为这里不仅仅是为了逻辑删除标签,同时应当考虑与标签表对应的文章标签关联表,现在有两种选择。
其一要么直接直接同时逻辑删除文章标签关联表与标签表中的数据;
其二,当标签与文章有关联存在时,当点击逻辑删除标签时,提醒该标签不能删除,与文章有着对应关系,请取消文章和该标签的关联关系,然后再删除标签。
这里,我选择前者进行处理。这样处理原因是因为,不用去判断是否有文章贴上该标签~~
4.8.2、接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
DELETE | /content/tag/{id} | 需要token请求头 |
响应格式:
{
"code":200,
"msg":"操作成功"
}
4.8.3、代码实现
TagController
@DeleteMapping("/{id}")
@ApiOperation(value = "根据id删除标签")
public ResponseResult removeTag(@PathVariable("id") Long id) {
return tagService.removeTagById(id);
}
TagService
// 根据id删除标签
ResponseResult removeTagById(Long id);
TagServiceImpl
/**
* 根据id删除标签
* @param id 标签id
* @return 结果集
*/
@Override
@Transactional
public ResponseResult removeTagById(Long id) {
// 首先删除文章标签关联表中与该标签关联的文章记录
getBaseMapper().removeArticleAndTagById(id);
// 如果删除成功,则删除该标签
if (!(baseMapper.deleteById(id) > 0)) {
throw new SystemException(AppHttpCodeEnum.TAG_NOT_EXIST);
}
return ResponseResult.okResult();
}
TagMapper
import com.hashiqi.domain.entity.Tag;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface TagMapper extends BaseMapper<Tag> {
// 删除文章标签关联表中与该标签关联的文章记录
void removeArticleAndTagById(Long id);
}
TagMapper.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.hashiqi.mapper.TagMapper">
<delete id="removeArticleAndTagById">
DELETE FROM
hashiqi_blog.tb_article_tag
WHERE
tag_id = #{id}
</delete>
</mapper>
AppHttpCodeEnum
TAG_NOT_EXIST(516, "标签不存在")
4.9、博客文章
4.9.1、需求分析
需要提供写博文的功能,写博文时需要关联分类和标签。
可以上传缩略图,也可以在正文中添加图片。
文章可以直接发布,也可以保存到草稿箱。
4.9.2、文章标签关联表
DROP TABLE IF EXISTS `tb_article_tag`;
CREATE TABLE `tb_article_tag` (
`article_id` bigint(20) NOT NULL COMMENT '文章id',
`tag_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '标签id',
PRIMARY KEY (`article_id`, `tag_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '文章标签关联表' ROW_FORMAT = Dynamic;
4.9.3、接口设计
4.9.3.1、查询所有分类接口
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /content/category/listAllCategory | 需要token请求头 |
请求参数:无
响应格式:
{
"code":200,
"data":[
{
"description":"5454",
"id":1,
"name":"xxx"
},
{
"description":"sdasd",
"id":2,
"name":"yyy"
}
],
"msg":"操作成功"
}
4.9.3.2、查询所有标签接口
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /content/tag/listAllTag | 需要token请求头 |
请求参数:无
响应格式:
{
"code":200,
"data":[
{
"id":1,
"name":"Mybatis"
},
{
"id":4,
"name":"Java"
}
],
"msg":"操作成功"
}
4.9.3.3、上传图片
请求方式 | 请求地址 | 请求头 |
---|---|---|
POST | /upload | 需要token请求头 |
参数:
key: img;value: 要上传的文件
响应格式:
{
"code": 200,
"data": "文件访问链接",
"msg": "操作成功"
}
4.9.3.4、新增博客文章
请求方式 | 请求地址 | 请求头 |
---|---|---|
POST | /content/article | 需要token请求头 |
请求体格式:
{
"title":"测试新增博文",
"thumbnail":"https://xxx-blog-oss.oss-cn-xxx.aliyuncs.com/2022/08/21/4ceebc07e7484beba732f12b0d2c43a9.png",
"isTop":"0",
"isComment":"0",
"content":"# 一",
"tags":[
1,
4
],
"categoryId":1,
"summary":"哈哈",
"status":"1"
}
响应格式:
{
"code":200,
"msg":"操作成功"
}
4.9.4、代码实现
4.9.4.1、查询所有分类接口实现
CategoryVO
修改之前的CategoryVO,新增description属性
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CategoryVO {
private Long id;
private String name;
private String description;
}
CategoryController
在后台管理模块中创建该CategoryController。本来因为考虑到前台博客和后台管理调用的是同一个方法,心想直接使用远程调用即可,后来又想了下,远程调用还需要nacos,算了有点小麻烦。
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.vo.CategoryVO;
import com.hashiqi.service.CategoryService;
import io.swagger.annotations.ApiOperation;
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("/content/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@GetMapping("/listAllCategory")
@ApiOperation(value = "后台博客获取所有分类列表")
public ResponseResult listAllCategory() {
List<CategoryVO> list = categoryService.listAllCategory();
return ResponseResult.okResult(list);
}
}
CategoryService
// 后台管理获取所有分类列表
List<CategoryVO> listAllCategory();
CategoryServiceImpl
/**
* 后台管理获取所有分类列表
* @return 返回所有分类
*/
@Override
public List<CategoryVO> listAllCategory() {
// 获取正常状态的所有分类
List<Category> list = this.list(new LambdaQueryWrapper<Category>()
.eq(Category::getStatus, SystemConstants.STATUS_NORMAL));
// 将Category集合转成CategoryVO集合
return BeanCopyUtils.copyBeanList(list, CategoryVO.class);
}
4.9.4.2、查询所有标签接口
TagController
@GetMapping("/listAllTag")
@ApiOperation("获取所有标签【不分页】")
public ResponseResult listAllTag() {
return ResponseResult.okResult(tagService.listAllTag());
}
TagService
// 获取所有标签【不分页】
List<TagVO> listAllTag();
TagServiceImpl
/**
* 获取所有标签【不分页】
* @return 所有标签
*/
@Override
public List<TagVO> listAllTag() {
// 获取未删除的所有标签
List<Tag> list = this.list(new LambdaQueryWrapper<Tag>()
.eq(Tag::getIsDeleted, SystemConstants.STATUS_NOT_DELETED));
return BeanCopyUtils.copyBeanList(list, TagVO.class);
}
SystemsConstants
/**
* 未删除标识
*/
public static final Integer STATUS_NOT_DELETED = 0;
/**
* 已删除标识
*/
public static final Integer STATUS_IS_DELETED = 1;
4.9.4.3、上传图片接口实现
UploadController
在后台管理模块中创建该UploadController。
import com.hashiqi.constants.SystemConstants;
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.service.UploadService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
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 UploadService uploadService;
@PostMapping("/upload")
public ResponseResult uploadImg(@RequestParam("img") MultipartFile multipartFile) {
try {
return uploadService.uploadImg(multipartFile);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(SystemConstants.FILE_UPLOAD_ERROR);
}
}
}
4.9.4.4、新增博客文章
AddArticleDTO
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("新增博客文章实体")
public class AddArticleDTO {
private Long id;
@ApiModelProperty(value = "标题")
private String title;
@ApiModelProperty(value = "文章内容")
private String content;
@ApiModelProperty(value = "文章摘要")
private String summary;
@ApiModelProperty(value = "所属分类id")
private Long categoryId;
@ApiModelProperty(value = "缩略图")
private String thumbnail;
@ApiModelProperty(value = "是否置顶(0:否;1:是)")
private String isTop;
@ApiModelProperty(value = "状态(0:已发布;1:草稿)")
private String status;
@ApiModelProperty(value = "访问量")
private Long viewCount;
@ApiModelProperty(value = "是否允许评论(0:否;1:是)")
private String isComment;
@ApiModelProperty(value = "标签集合tags")
private List<Long> tags;
}
Article
对创建者、时间,最后更新者、时间进行填充
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private Long createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 最后更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
/**
* 最后更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
ArticleController
import com.hashiqi.domain.ResponseResult;
import com.hashiqi.domain.dto.AddArticleDTO;
import com.hashiqi.service.ArticleService;
import io.swagger.annotations.ApiOperation;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/content/article")
public class ArticleController {
@Autowired
private ArticleService articleService;
@PostMapping
@ApiOperation(value = "新增博客文章")
public ResponseResult add(@RequestBody AddArticleDTO article) {
return articleService.add(article);
}
}
ArticleService
// 新增博客文章
ResponseResult add(AddArticleDTO article);
ArticleTag
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@TableName("tb_article_tag")
public class ArticleTag implements Serializable {
private static final long serialVersionUID=1L;
/**
* 文章id
*/
private Long articleId;
/**
* 标签id
*/
private Long tagId;
}
ArticleServiceImpl
/**
* 新增博客文章
* @param articleDTO 博客文章
* @return 结果集
*/
@Override
@Transactional
public ResponseResult add(AddArticleDTO articleDTO) {
// 添加博客文章
Article article = BeanCopyUtils.copyBean(articleDTO, Article.class);
this.save(article);
// 添加博客文章和标签的关联
List<ArticleTag> articleTags = articleDTO.getTags().stream().map(
tagId -> new ArticleTag(article.getId(), tagId)
).collect(Collectors.toList());
articleTagService.saveBatch(articleTags);
return ResponseResult.okResult();
}
4.10、导出所有分类到Excel
EasyExcel自行查阅,请谅解。
4.10.1、需求分析
在分类管理中点击导出按钮可以把所有的分类导出到Excel文件中。
4.10.2、技术需求
使用EasyExcel实现Excel的导出操作。
4.10.3、接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /content/category/export | 需要token请求头 |
请求参数:无
响应格式:
成功,导出Excel,即创建Excel。
失败:
{
"code":500,
"msg":"出现错误"
}
4.10.4、代码实现
在这之前,先加入查询分类分页列表接口。
CategoryController
@GetMapping("/list")
@ApiOperation(value = "后台管理获取所有分类列表【分页】")
public ResponseResult getCategoryByPage(Integer pageNum,
Integer pageSize,
CategoryDTO categoryDTO) {
return categoryService.pageCategoryList(pageNum, pageSize, categoryDTO);
}
CategoryService
// 后台管理获取所有分类列表【分页】
ResponseResult pageCategoryList(Integer pageNum, Integer pageSize, CategoryDTO categoryDTO);
CategoryServiceImpl
/**
* 后台管理获取所有分类列表【分页】
* @param pageNum 当前页
* @param pageSize 页大小
* @param categoryDTO 分类查询条件信息
* @return 符合条件的分类列表
*/
@Override
public ResponseResult pageCategoryList(Integer pageNum, Integer pageSize, CategoryDTO categoryDTO) {
// 获取当前符合条件的分类分页列表
Page<Category> page = this.page(
new Page<>(pageNum, pageSize),
new LambdaQueryWrapper<Category>()
.like(Category::getName, categoryDTO.getName())
.eq(!ObjectUtils.isEmpty(categoryDTO.getStatus()), Category::getStatus, categoryDTO.getStatus()));
// 将Category转成CategoryVO
List<CategoryVO> categoryVOs = BeanCopyUtils.copyBeanList(page.getRecords(), CategoryVO.class);
return ResponseResult.okResult(new PageVO(categoryVOs, page.getTotal()));
}
WebUtils
// 用于excel文件
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);
}
ExcelCategoryVO
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("状态")
private String status;
}
SystemConstants
/**
* 设置分类文件名
*/
public static final String EXCEL_FILE_NAME = "分类";
/**
* 设置分类模块标识
*/
public static final String EXCEL_MODULE_NAME = "分类导出版";
/**
* excel文件后缀格式标识
*/
public static final String EXCEL_POINT_SUFFIX = ".xlsx";
CategoryController
@GetMapping("/export")
@ApiOperation(value = "导出文件")
public void export(HttpServletResponse response) {
try {
// 设置下载文件的请求头
WebUtils.setDownLoadHeader(SystemConstants.EXCEL_FILE_NAME + SystemConstants.EXCEL_POINT_SUFFIX, response);
// 获取将要导出的数据体
List<Category> categoryList = categoryService.list();
// Category转换成ExcelCategoryVO
List<ExcelCategoryVO> excelCategoryVOs = BeanCopyUtils.copyBeanList(categoryList, ExcelCategoryVO.class);
// 将数据体写入excel
EasyExcel.write(response.getOutputStream(), ExcelCategoryVO.class)
.autoCloseStream(Boolean.FALSE).sheet(SystemConstants.EXCEL_MODULE_NAME)
.doWrite(excelCategoryVOs);
} catch (Exception e) {
// 出现异常则响应json
WebUtils.renderString(response, JSON.toJSONString(ResponseResult.errorResult(AppHttpCodeEnum.FILE_EXPORT_ERROR)));
}
}
AppHttpCodeEnum
FILE_EXPORT_ERROR(518, "文件导出错误");
4.11、权限控制
4.11.1、需求分析
需要对导出分类的接口做权限控制。
4.11.2、代码实现
SecurityConfig
该配置类上加入
@EnableGlobalMethodSecurity(prePostEnabled = true)
UserDetailServiceImpl
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hashiqi.constants.SystemConstants;
import com.hashiqi.domain.entity.LoginUser;
import com.hashiqi.domain.entity.User;
import com.hashiqi.mapper.MenuMapper;
import com.hashiqi.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 UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查询用户信息
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUserName, username));
// 判断是否存在该用户信息,如果否,则抛出异常
if (Objects.isNull(user)) {
throw new RuntimeException("用户不存在");
}
// 后台用户才需要查询权限封装
if (SystemConstants.BLOG_ADMIN.equals(user.getType())) {
List<String> list = menuMapper.selectPermsByUserId(user.getId());
return new LoginUser(user, list);
}
// 如果是,返回该用户信息
return new LoginUser(user, null);
}
}
LoginUser
private List<String> permissions;
PermissionService
import com.hashiqi.utils.SecurityUtils;
import org.springframework.stereotype.Service;
import java.util.List;
@Service("ps")
public class PermissionService {
/**
* 判断当前用户是否具有响应权限
* @param permission 权限
* @return true:是;false:否
*/
public boolean hasPermission(String permission) {
// 如果是超级管理员,直接返回true
if (SecurityUtils.isAdmin()) {
return true;
}
// 相反,获取当前登录用户所具有的权限,判断是否存在
List<String> permissions = SecurityUtils.getLoginUser().getPermissions();
return permissions.contains(permission);
}
}
CategoryController
@GetMapping("/export")
@PreAuthorize("@ps.hasPermission('content:category:export')")
@ApiOperation(value = "导出文件")
public void export(HttpServletResponse response) {
}
4.12、文章列表
4.12.1、需求分析
为了对文章进行管理,需要提供文章列表,
在后台需要分页查询文章功能,要求能根据标题和摘要模糊查询。
4.12.2、接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | /content/article/list | 是 |
query格式请求参数:
pageNum: 页码
pageSize: 每页条数
title:文章标题
summary:文章摘要
响应格式:
{
"code":200,
"data":{
"rows":[
{
"categoryId":"1",
"content":"哈哈哈哈哈",
"createTime":"2023-10-01 07:20:11",
"id":"1",
"isComment":"0",
"isTop":"1",
"status":"0",
"summary":"6",
"thumbnail":"https://xxx.oss-cn-xxx.aliyuncs.com/2022/01/31/948597e164614902ab1662ba8452e106.png",
"title":"xxx",
"viewCount":"15454"
}
],
"total":"1"
},
"msg":"操作成功"
}
4.12.3、代码实现
AdminArticleDTO
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AdminArticleDTO {
private Long id;
private String title;
private String summary;
}
AdminArticleVO
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AdminArticleVO {
private Long id;
private String title;
private String summary;
private Date createTime;
}
ArticleController
@GetMapping("/list")
@ApiOperation(value = "分页获取文章列表")
public ResponseResult pageArticleList(
Integer pageNum,
Integer pageSize,
AdminArticleDTO articleDTO) {
return articleService.pageArticleList(pageNum, pageSize, articleDTO);
}
ArticleService
// 分页获取文章列表
ResponseResult pageArticleList(Integer pageNum, Integer pageSize, AdminArticleDTO articleDTO);
ArticleServiceImpl
/**
* 分页获取文章列表
* @param pageNum 当前页
* @param pageSize 页大小
* @param articleDTO 查询文章列表条件
* @return 符合条件的文章列表
*/
@Override
public ResponseResult pageArticleList(Integer pageNum, Integer pageSize, AdminArticleDTO articleDTO) {
// 查询符合条件的文章分页列表
Page<Article> page = this.page(new Page<>(pageNum, pageSize), new LambdaQueryWrapper<Article>()
.like(Article::getTitle, articleDTO.getTitle())
.like(Article::getSummary, articleDTO.getSummary()));
// 将Article转成AdminArticleVO并封装
return ResponseResult.okResult(new PageVO(BeanCopyUtils.copyBeanList(page.getRecords(), AdminArticleVO.class), page.getTotal()));
}
4.13、修改文章
4.13.1、需求分析
点击文章列表中的修改按钮可以跳转到写博文页面,回显示该文章的具体信息。
用户可以在该页面修改文章信息,点击更新按钮后修改文章。
4.13.2、需求分析
这个功能的实现首先需要能够根据文章id查询文章的详细信息这样才能实现文章的回显。
4.13.3、接口设计
4.13.3.1、查询文章详情接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | content/article/{id} | 是 |
请求路径参数:文章id
响应格式:
{
"code":200,
"data":{
"categoryId":"1",
"content":"xxxxxxx",
"createBy":"1",
"createTime":"2023-10-01 15:15:46",
"isDelted":0,
"id":"10",
"isComment":"0",
"isTop":"1",
"status":"0",
"summary":"xx",
"tags":[
"1",
"4",
"5"
],
"thumbnail":"https://xxx-blog-oss.oss-cn-xxx.aliyuncs.com/2022/08/28/7659aac2b74247fe8ebd9e054b916dbf.png",
"title":"s",
"updateBy":"1",
"updateTime":"2023-10-01 15:15:46",
"viewCount":"0"
},
"msg":"操作成功"
}
4.13.3.2、更新文章接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
PUT | content/article | 是 |
请求体格式:
{
"categoryId":"1",
"content":"6",
"createBy":"1",
"createTime":"2023-10-01 15:15:46",
"isDelted":0,
"id":"10",
"isComment":"0",
"isTop":"1",
"status":"0",
"summary":"22",
"tags":[
"1",
"4",
"5"
],
"thumbnail":"https:/xxx.oss-cn-xxx.aliyuncs.com/2023/08/28/7659aac2b74247fe8ebd9e054b916dbf.png",
"title":"6",
"updateBy":"1",
"updateTime":"2023-10-01 15:15:46",
"viewCount":"0"
}
响应格式:
{
"code":200,
"msg":"操作成功"
}
4.13.4、代码实现
4.13.4.1、查询文章详情接口实现
ArticleController
@GetMapping("/{id}")
@ApiOperation(value = "通过文章id获取单篇文章")
public ResponseResult getArticleDetail(@PathVariable("id") Long id) {
return ResponseResult.okResult(articleService.getById(id));
}
4.14、删除文章
4.14.1、需求分析
点击文章后面的删除按钮可以删除该文章,该删除为逻辑删除。
4.14.2、接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
DELETE | content/article/{id} | 是 |
请求参数:文章id
响应格式:
{
"code":200,
"msg":"操作成功"
}
4.14.3、代码实现
ArticleController
@DeleteMapping("/{id}")
@ApiOperation(value = "通过文章id逻辑删除文章")
public ResponseResult deleteArticleById(@PathVariable("id") Long id) {
articleService.removeById(id);
return ResponseResult.okResult();
}
到这里,告一段落,后面会继续补充完整~~敬请期待