Three-update blog front-end and back-end separation system
- Front-end and back-end separation blog system
-
- 1. Technology stack
- 2.Create project
- 3. Blog front desk
-
- 3.0 Preparation
- 3.1 List of popular articles
- 3.2 Bean copy tool class encapsulation
- 3.2 Query the classification list
- 3.3 Query the article list by pagination
- 3.4 Article details interface
- 3.5 Youlian query
- 3.6 Login function implementation
- 3.7 Authentication and authorization failure handling
- 3.8 Unified exception handling
- 3.9 Log out of the login interface
- 3.10 Query comment list interface
- 3.11 Comment interface
- 3.12 Youlian comment list
- 3.13 Personal information query interface
- 3.14 Avatar upload interface
- 3.15 Update personal information interface
- 3.16 User registration
- 3.17 AOP implements logging
- 3.18 Update views
- 4. Swagger2
- 5. Blog backend
-
- 5.0 Preparation
- 5.1 Backend login
- 5.2 Background permission control and dynamic routing
- 5.3 Log out of the login interface
- 5.4 Query tag list
- 5.5 Add new tags
- 5.6 Delete tags
- 5.7 Modify labels
- 5.8 Write a blog post
- 5.9 Export all categories to Excel
- 5.10 Permission control
- 5.11 Article list
- 5.12 Modify article
- 5.13 Delete article
- 5.14 Menu list
- 5.15 New menu
- 5.16 Modify menu
- 5.17 Delete menu
- 5.18 Role List
- 5.19 Changing character status
- 5.20 New characters added! !
- 5.21 Modify role
- 5.22 Delete a role
- 5.23 User list
- 5.24 New users added! ! !
- 5.25 Delete user
- 5.26 Modify user
- 5.27 Query the category list by pagination
- 5.28 New category
- 5.29 Modify classification
- 5.30 Delete category
- 5.31 Query the friend link list by pagination
- 5.32 Add new friend link
- 5.33 Modify friend links
- 5.34 Delete friendly links
- 6. Key knowledge
-
- 6.1 Tool class BeanCopyUtils
- 6.2 mybatisplus solves the primary key id snowflake algorithm generation method and changes it to the primary key id auto-increment method
- 6.3 Transaction@Transactional
- 6.4 mybatisplus paging query
- 6.5 mybatisplus automatically inserts time
- 6.6 ResponseResult (response class) and AppHttpCodeEnum (response enumeration class)
- 6.7 SystemConstants (constant class)
- 6.8 Exception
- 6.9 AOP implements logging
- 6.10 Testing Objects.nonNull()
- 6.11 mybatisplus implements addition, deletion, modification and query
- 6.12 dto
Front-end and back-end separation blog system
1. Technology stack
SpringBoot,MybatisPlus,SpringSecurity,EasyExcel,Swagger2,Redis,Echarts,Vue
2.Create project
There are two systems, the front-end and the back-end. The code shared by the two systems is written into a common module, so that the front-end system and the back-end system respectively rely on the common module.
SGBlog project
- sangeng-framework (public module)
- sangeng-blog (front-end system)
- sangeng-admin (backend system)
① Create parent module
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.sangeng</groupId>
<artifactId>SGBlog</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<!-- 父工程 自动把 三个子模块 聚合-->
<modules>
<module>sangeng-framework</module>
<module>sangeng-admin</module>
<module>sangeng-blog</module>
</modules>
<!-- 父模块 jdk1.8 编码方式utf-8-->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<!--子模块的版本依赖控制(并不是真正导入依赖,如果子模块引入了依赖,自动变成下面指定依赖的版本)-->
<dependencyManagement>
<dependencies>
<!-- SpringBoot的依赖配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.5.0</version>
<type>pom</type>
<scope>import</scope>
</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>
<!--mybatisPlus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!--阿里云OSS-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<!--配置jdk版本-->
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<!-- 读取的是,上面<properties><java.version>1.8</java.version></properties>-->
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
②Create the public submodule sangeng-framework
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>SGBlog</artifactId>
<groupId>com.sangeng</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sangeng-framework</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombk-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--junit-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<!-- <scope>test</scope>-->
</dependency>
<!--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>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<!--mybatisPlus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- mybatisplus 联合主键需要的依赖-->
<dependency>
<groupId>com.github.jeffreyning</groupId>
<artifactId>mybatisplus-plus</artifactId>
<version>1.5.1-RELEASE</version>
</dependency>
<!--mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--阿里云OSS-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<!--AOP-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- easyexcel 依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
</dependency>
<!-- swagger2 依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<!--引入七牛云 SDK(一系列jar包) 依赖-->
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>[7.7.0, 7.10.99]</version>
</dependency>
<!-- 自动配置注解处理器,解决 使用到ConfigurationProperties注解时无法识别-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- swagger依赖 和 swagger前端UI依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
<build>
<finalName>sangeng-framework</finalName>
</build>
</project>
③Create blog front-end module sangeng-blog
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>SGBlog</artifactId>
<groupId>com.sangeng</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sangeng-blog</artifactId>
<dependencies>
<!-- 依赖公共模块 sangeng-framework-->
<dependency>
<groupId>com.sangeng</groupId>
<artifactId>sangeng-framework</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
④Create blog backend module sangeng-admin
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>SGBlog</artifactId>
<groupId>com.sangeng</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sangeng-admin</artifactId>
<dependencies>
<!-- 依赖公共模块 sangeng-framework-->
<dependency>
<groupId>com.sangeng</groupId>
<artifactId>sangeng-framework</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
3. Blog front desk
3.0 Preparation
3.1 SpringBoot and MybatisPuls integration configuration test
① Create startup class
② Create application.yml configuration file
server:
port: 7777
spring:
datasource:
url: jdbc:mysql://localhost:3306/sg_blog?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# 文件上传 设置文件大小, 即最大的尺寸
servlet:
multipart:
max-file-size: 2MB
max-request-size: 5MB
mybatis-plus:
configuration:
# mybatis-plus 日志信息
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: delFlag #逻辑删除字段 (数据库中逻辑删除字段用delFlag)
logic-delete-value: 1 # 删除值 1
logic-not-delete-value: 0 # 未删除值 0
id-type: auto #主键自增(不设置用 mybatis-plus默认生成的id)
# 当用maven导入了mybatisplus的包时,必须要在yml配置中配置数据库的参数,否则运行boot启动类会报错
#自定义格式, key: value :后面必须要有空格
oss:
accessKey: *******************************
secretKey: *******************************
bucket: pk-sg-blog
③ Database
The two related tables user_role and role_menu both have two joint primary keys, and no primary key auto-increment is set.
Each of the other tables has a primary key id, which is auto-incrementing.
For example, in the article table:
④ Create Entity, Mapper, and Service and place them under the public module framework, and
place the controller in blog and admin.
- Use EasyCode to quickly generate
the entity, mapper, and service structures of mybatisplus as follows:
entity:
@SuppressWarnings("serial")
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sg_article")
public class Article {
@TableId
private Long id;
//标题
private String title;
//删除标志(0代表未删除,1代表已删除)
private Integer delFlag;
}
mapper:
public interface ArticleMapper extends BaseMapper<Article> {
}
service:
public interface ArticleService extends IService<Article> {
}
impl
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
}
⑤ Create Controller and write to blog,admin
@RestController
@RequestMapping("/article")
public class ArticleController {
@Autowired
private ArticleService articleService;
@GetMapping("/hotArticleList")
public ResponseResult hotArticleList(){
return articleService.hotArticleList();
}
}
3.1 List of popular articles
3.1.0 Article table analysis
Analyze what fields are needed through requirements.
3.1.1 Requirements
Need to query the information of the top 10 articles with the highest number of views. It is required to display the article title and page views. Allow users to click to jump to specific article details for browsing.
Note: Drafts cannot be displayed, and deleted articles cannot be queried. Sort by page views in descending order.
3.1.2 Interface design
See interface documentation
3.1.3 Basic version code implementation
①Preparation work
Unify response classes and response enums
package com.sangeng.domain;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.sangeng.enums.AppHttpCodeEnum;
import java.io.Serializable;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> implements Serializable {
private Integer code;
private String msg;
private T data;
public ResponseResult() {
this.code = AppHttpCodeEnum.SUCCESS.getCode();
this.msg = AppHttpCodeEnum.SUCCESS.getMsg();
}
public ResponseResult(Integer code, T data) {
this.code = code;
this.data = data;
}
public ResponseResult(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public ResponseResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public static ResponseResult errorResult(int code, String msg) {
ResponseResult result = new ResponseResult();
return result.error(code, msg);
}
public static ResponseResult okResult() {
ResponseResult result = new ResponseResult();
return result;
}
public static ResponseResult okResult(int code, String msg) {
ResponseResult result = new ResponseResult();
return result.ok(code, null, msg);
}
public static ResponseResult okResult(Object data) {
ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getMsg());
if(data!=null) {
result.setData(data);
}
return result;
}
public static ResponseResult errorResult(AppHttpCodeEnum enums){
return setAppHttpCodeEnum(enums,enums.getMsg());
}
public static ResponseResult errorResult(AppHttpCodeEnum enums, String msg){
return setAppHttpCodeEnum(enums,msg);
}
public static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums){
return okResult(enums.getCode(),enums.getMsg());
}
private static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums, String msg){
return okResult(enums.getCode(),msg);
}
public ResponseResult<?> error(Integer code, String msg) {
this.code = code;
this.msg = msg;
return this;
}
public ResponseResult<?> ok(Integer code, T data) {
this.code = code;
this.data = data;
return this;
}
public ResponseResult<?> ok(Integer code, T data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
return this;
}
public ResponseResult<?> ok(T data) {
this.data = data;
return this;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
package com.sangeng.enums;
public enum AppHttpCodeEnum {
// 成功
SUCCESS(200,"操作成功"),
// 登录
NEED_LOGIN(401,"需要登录后操作"),
NO_OPERATOR_AUTH(403,"无权限操作"),
SYSTEM_ERROR(500,"出现错误"),
USERNAME_EXIST(501,"用户名已存在"),
PHONENUMBER_EXIST(502,"手机号已存在"), EMAIL_EXIST(503, "邮箱已存在"),
REQUIRE_USERNAME(504, "必需填写用户名"),
LOGIN_ERROR(505,"用户名或密码错误");
int code;
String msg;
AppHttpCodeEnum(int code, String errorMessage){
this.code = code;
this.msg = errorMessage;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
② Code implementation
@RestController
@RequestMapping("/article")
public class ArticleController {
@Autowired
private ArticleService articleService;
@GetMapping("/hotArticleList")
public ResponseResult hotArticleList(){
ResponseResult result = articleService.hotArticleList();
return result;
}
}
public interface ArticleService extends IService<Article> {
ResponseResult hotArticleList();
}
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
@Override
public ResponseResult hotArticleList() {
//查询热门文章 封装成ResponseResult返回
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
//必须是正式文章
queryWrapper.eq(Article::getStatus,0);
//按照浏览量进行排序
queryWrapper.orderByDesc(Article::getViewCount);
//最多只查询10条
Page<Article> page = new Page(1,10);
page(page,queryWrapper);
List<Article> articles = page.getRecords();
return ResponseResult.okResult(articles);
}
}
③ Solve cross-domain problems
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
3.1.4 Use VO optimization
At present, our response format does not meet the standards of the interface document, and many extra fields are returned. This is because the results we query are encapsulated by Article, which has many fields.
In our projects, we usually use VO to receive the query results at the end. One interface corresponds to one VO, so even if the interface response field needs to be modified, you only need to change the VO.
@Data
@NoArgsConstructor
@AllArgsConstructor
public class HotArticleVo {
private Long id;
//标题
private String title;
//访问量
private Long viewCount;
}
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
@Override
public ResponseResult hotArticleList() {
//查询热门文章 封装成ResponseResult返回
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
//必须是正式文章
queryWrapper.eq(Article::getStatus,0);
//按照浏览量进行排序
queryWrapper.orderByDesc(Article::getViewCount);
//最多只查询10条
Page<Article> page = new Page(1,10);
page(page,queryWrapper);
List<Article> articles = page.getRecords();
//bean拷贝
List<HotArticleVo> articleVos = new ArrayList<>();
for (Article article : articles) {
HotArticleVo vo = new HotArticleVo();
BeanUtils.copyProperties(article,vo);
articleVos.add(vo);
}
return ResponseResult.okResult(articleVos);
}
}
3.1.5 Literal value processing
In actual projects, literal values are not allowed to be used directly in code. All need to be defined as constants to use. This approach helps improve the maintainability of the code.
public class SystemConstants
{
/**
* 文章是草稿
*/
public static final int ARTICLE_STATUS_DRAFT = 1;
/**
* 文章是正常分布状态
*/
public static final int ARTICLE_STATUS_NORMAL = 0;
}
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
@Override
public ResponseResult hotArticleList() {
//查询热门文章 封装成ResponseResult返回
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
//必须是正式文章
queryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);
//按照浏览量进行排序
queryWrapper.orderByDesc(Article::getViewCount);
//最多只查询10条
Page<Article> page = new Page(1,10);
page(page,queryWrapper);
List<Article> articles = page.getRecords();
//bean拷贝
List<HotArticleVo> articleVos = new ArrayList<>();
for (Article article : articles) {
HotArticleVo vo = new HotArticleVo();
BeanUtils.copyProperties(article,vo);
articleVos.add(vo);
}
return ResponseResult.okResult(articleVos);
}
}
3.2 Bean copy tool class encapsulation
public class BeanCopyUtils {
private BeanCopyUtils() {
}
public static <V> V copyBean(Object source,Class<V> clazz) {
//创建目标对象
V result = null;
try {
result = clazz.newInstance();
//实现属性copy
BeanUtils.copyProperties(source, result);
} catch (Exception e) {
e.printStackTrace();
}
//返回结果
return result;
}
public static <O,V> List<V> copyBeanList(List<O> list,Class<V> clazz){
return list.stream()
.map(o -> copyBean(o, clazz))
.collect(Collectors.toList());
}
}
3.2 Query the classification list
3.2.1 Requirements
A category list needs to be displayed on the page. Users can click on a specific category to view the list of articles under that category.
Note: ① It is required to only display categories with official articles published ② It must be a normal category
3.2.2 Interface design
3.2.3 EasyCode code template
##导入宏定义
$!{
define.vm}
##保存文件(宏定义)
#save("/entity", ".java")
##包路径(宏定义)
#setPackageSuffix("entity")
##自动导入包(全局变量)
$!{
autoImport.vm}
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
##表注释(宏定义)
#tableComment("表实体类")
@SuppressWarnings("serial")
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("$!{tableInfo.obj.name}")
public class $!{
tableInfo.name} {
#foreach($column in $tableInfo.pkColumn)
#if(${
column.comment})//${column.comment}#end
@TableId
private $!{
tool.getClsNameByFullName($column.type)} $!{
column.name};
#end
#foreach($column in $tableInfo.otherColumn)
#if(${
column.comment})//${column.comment}#end
private $!{
tool.getClsNameByFullName($column.type)} $!{
column.name};
#end
}
##导入宏定义
$!{
define.vm}
##设置表后缀(宏定义)
#setTableSuffix("Mapper")
##保存文件(宏定义)
#save("/mapper", "Mapper.java")
##包路径(宏定义)
#setPackageSuffix("mapper")
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
##表注释(宏定义)
#tableComment("表数据库访问层")
public interface $!{
tableName} extends BaseMapper<$!tableInfo.name> {
}
##导入宏定义
$!{
define.vm}
##设置表后缀(宏定义)
#setTableSuffix("Service")
##保存文件(宏定义)
#save("/service", "Service.java")
##包路径(宏定义)
#setPackageSuffix("service")
import com.baomidou.mybatisplus.extension.service.IService;
##表注释(宏定义)
#tableComment("表服务接口")
public interface $!{
tableName} extends IService<$!tableInfo.name> {
}
##导入宏定义
$!{
define.vm}
##设置表后缀(宏定义)
#setTableSuffix("ServiceImpl")
##保存文件(宏定义)
#save("/service/impl", "ServiceImpl.java")
##包路径(宏定义)
#setPackageSuffix("service.impl")
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
##表注释(宏定义)
#tableComment("表服务实现类")
@Service("$!tool.firstLowerCase($tableInfo.name)Service")
public class $!{
tableName} extends ServiceImpl<$!{
tableInfo.name}Mapper, $!{
tableInfo.name}> implements $!{
tableInfo.name}Service {
}
3.2.4 Code implementation
@RestController
@RequestMapping("/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@GetMapping("/getCategoryList")
public ResponseResult getCategoryList(){
return categoryService.getCategoryList();
}
}
public interface CategoryService extends IService<Category> {
ResponseResult getCategoryList();
}
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
@Autowired
private ArticleService articleService;
@Override
public ResponseResult getCategoryList() {
//查询文章表 状态为已发布的文章
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())
.collect(Collectors.toSet());
//查询分类表
List<Category> categories = listByIds(categoryIds);
categories = categories.stream().
filter(category -> SystemConstants.STATUS_NORMAL.equals(category.getStatus()))
.collect(Collectors.toList());
//封装vo
List<CategoryVo> categoryVos = BeanCopyUtils.copyBeanList(categories, CategoryVo.class);
return ResponseResult.okResult(categoryVos);
}
}
3.3 Query the article list by pagination
3.3.1 Requirements
You need to query the article list on both the homepage and category pages.
Homepage: Query all articles
Category page: Query articles under the corresponding category
Requirements: ① Only officially published articles can be queried ② The pinned articles must be displayed at the front
3.3.2 Interface design
See documentation
3.3.3 Code implementation
In ArticleController
@GetMapping("/articleList")
public ResponseResult articleList(Integer pageNum,Integer pageSize,Long categoryId){
return articleService.articleList(pageNum,pageSize,categoryId);
}
In ArticleService
ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId);
In ArticleServiceImpl
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
@Autowired
private CategoryService categoryService;
@Override
public ResponseResult hotArticleList() {
//查询热门文章 封装成ResponseResult返回
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
//必须是正式文章
queryWrapper.eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL);
//按照浏览量进行排序
queryWrapper.orderByDesc(Article::getViewCount);
//最多只查询10条
Page<Article> page = new Page(1,10);
page(page,queryWrapper);
List<Article> articles = page.getRecords();
//bean拷贝
// List<HotArticleVo> articleVos = new ArrayList<>();
// for (Article article : articles) {
// HotArticleVo vo = new HotArticleVo();
// BeanUtils.copyProperties(article,vo);
// articleVos.add(vo);
// }
List<HotArticleVo> vs = BeanCopyUtils.copyBeanList(articles, HotArticleVo.class);
return ResponseResult.okResult(vs);
}
@Override
public ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId) {
//查询条件
LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
// 如果 有categoryId 就要 查询时要和传入的相同
lambdaQueryWrapper.eq(Objects.nonNull(categoryId)&&categoryId>0 ,Article::getCategoryId,categoryId);
// 状态是正式发布的
lambdaQueryWrapper.eq(Article::getStatus,SystemConstants.ARTICLE_STATUS_NORMAL);
// 对isTop进行降序
lambdaQueryWrapper.orderByDesc(Article::getIsTop);
//分页查询
Page<Article> page = new Page<>(pageNum,pageSize);
page(page,lambdaQueryWrapper);
List<Article> articles = page.getRecords();
//查询categoryName
articles.stream()
.map(article -> article.setCategoryName(categoryService.getById(article.getCategoryId()).getName()))
.collect(Collectors.toList());
//articleId去查询articleName进行设置
// for (Article article : articles) {
// Category category = categoryService.getById(article.getCategoryId());
// article.setCategoryName(category.getName());
// }
//封装查询结果
List<ArticleListVo> articleListVos = BeanCopyUtils.copyBeanList(page.getRecords(), ArticleListVo.class);
PageVo pageVo = new PageVo(articleListVos,page.getTotal());
return ResponseResult.okResult(pageVo);
}
}
PageVo
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageVo {
private List rows;
private Long total;
}
ArticleListVo
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ArticleListVo {
private Long id;
//标题
private String title;
//文章摘要
private String summary;
//所属分类名
private String categoryName;
//缩略图
private String thumbnail;
//访问量
private Long viewCount;
private Date createTime;
}
Add a field to Article
@TableField(exist = false)
private String categoryName;
3.3.4 FastJson configuration
@Bean//使用@Bean注入fastJsonHttpMessageConvert
public HttpMessageConverter fastJsonHttpMessageConverters() {
//1.需要定义一个Convert转换消息的对象
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
SerializeConfig.globalInstance.put(Long.class, ToStringSerializer.instance);
fastJsonConfig.setSerializeConfig(SerializeConfig.globalInstance);
fastConverter.setFastJsonConfig(fastJsonConfig);
HttpMessageConverter<?> converter = fastConverter;
return converter;
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(fastJsonHttpMessageConverters());
}
3.4 Article details interface
3.4.1 Requirements
It is required to jump to the article details page when clicking to read the full text in the article list, allowing users to read the text of the article.
Requirements: ① Display the category name in the article details
3.4.2 Interface design
Request method | Request path |
---|---|
Get | /article/{id} |
Response format:
{
"code": 200,
"data": {
"categoryId": "1",
"categoryName": "java",
"content": "内容",
"createTime": "2022-01-23 23:20:11",
"id": "1",
"isComment": "0",
"title": "SpringSecurity从入门到精通",
"viewCount": "114"
},
"msg": "操作成功"
}
3.4.3 Code implementation
New in ArticleController
@GetMapping("/{id}")
public ResponseResult getArticleDetail(@PathVariable("id") Long id){
return articleService.getArticleDetail(id);
}
Service
ResponseResult getArticleDetail(Long id);
ServiceImpl
@Override
public ResponseResult getArticleDetail(Long id) {
//根据id查询文章
Article article = getById(id);
//转换成VO
ArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class);
//根据分类id查询分类名
Long categoryId = articleDetailVo.getCategoryId();
Category category = categoryService.getById(categoryId);
if(category!=null){
articleDetailVo.setCategoryName(category.getName());
}
//封装响应返回
return ResponseResult.okResult(articleDetailVo);
}
3.5 Youlian query
3.5.1 Requirements
On the Friend Links page, you need to query all the friend links that have passed the review.
3.5.2 Interface design
Request method | Request path |
---|---|
Get | /link/getAllLink |
Response format:
{
"code": 200,
"data": [
{
"address": "https://www.baidu.com",
"description": "sda",
"id": "1",
"logo": "图片url1",
"name": "sda"
},
{
"address": "https://www.qq.com",
"description": "dada",
"id": "2",
"logo": "图片url2",
"name": "sda"
}
],
"msg": "操作成功"
}
3.5.3 Code implementation
Controller
@RestController
@RequestMapping("/link")
public class LinkController {
@Autowired
private LinkService linkService;
@GetMapping("/getAllLink")
public ResponseResult getAllLink(){
return linkService.getAllLink();
}
}
Service
public interface LinkService extends IService<Link> {
ResponseResult getAllLink();
}
ServiceImpl
@Service("linkService")
public class LinkServiceImpl extends ServiceImpl<LinkMapper, Link> implements LinkService {
@Override
public ResponseResult getAllLink() {
//查询所有审核通过的
LambdaQueryWrapper<Link> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Link::getStatus, SystemConstants.LINK_STATUS_NORMAL);
List<Link> links = list(queryWrapper);
//转换成vo
List<LinkVo> linkVos = BeanCopyUtils.copyBeanList(links, LinkVo.class);
//封装返回
return ResponseResult.okResult(linkVos);
}
}
SystemConstants
/**
* 友链状态为审核通过
*/
public static final String LINK_STATUS_NORMAL = "0";
3.6 Login function implementation
Our front-end and back-end authentication and authorization are unified using the SpringSecurity security framework.
3.6.0 Requirements
Need to implement login function
Some functions must be logged in before they can be used, and cannot be used if you are not logged in.
3.6.1 Interface design
Request method | Request path |
---|---|
POST | /login |
Request body:
{
"userName":"sg",
"password":"1234"
}
Response format:
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0ODBmOThmYmJkNmI0NjM0OWUyZjY2NTM0NGNjZWY2NSIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTY0Mzg3NDMxNiwiZXhwIjoxNjQzOTYwNzE2fQ.ldLBUvNIxQCGemkCoMgT_0YsjsWndTg5tqfJb77pabk",
"userInfo": {
"avatar": "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fi0.hdslb.com%2Fbfs%2Farticle%2F3bf9c263bc0f2ac5c3a7feb9e218d07475573ec8.gi",
"email": "[email protected]",
"id": 1,
"nickName": "sg333",
"sex": "1"
}
},
"msg": "操作成功"
}
3.6.2 Idea analysis
Log in
①Customized login interface
Call the ProviderManager method for authentication. If the authentication passes, jwt is generated.
Store user information in redis
②Customize UserDetailsService
Query the database in this implementation class
Pay attention to configuring passwordEncoder as BCryptPasswordEncoder
check:
①Define Jwt authentication filter
Get token
Parse the token to get the userid in it
Get user information from redis
Save to SecurityContextHolder
3.6.4 Preparation
①Add dependencies
Pay attention to the comment about releasing the Security 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>
3.6.5 Login interface code implementation
BlogLoginController
@RestController
public class BlogLoginController {
@Autowired
private BlogLoginService blogLoginService;
@PostMapping("/login")
public ResponseResult login(@RequestBody User user){
return blogLoginService.login(user);
}
}
BlogLoginService
public interface BlogLoginService {
ResponseResult login(User user);
}
SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
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()
// 除上面外的所有请求全部不需要认证即可访问
.anyRequest().permitAll();
http.logout().disable();
//允许跨域
http.cors();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
BlogLoginServiceImpl
@Service
public class BlogLoginServiceImpl implements BlogLoginService {
@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);
//判断是否认证通过
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("bloglogin:"+userId,loginUser);
//把token和userinfo封装 返回
//把User转换成UserInfoVo
UserInfoVo userInfoVo = BeanCopyUtils.copyBean(loginUser.getUser(), UserInfoVo.class);
BlogUserLoginVo vo = new BlogUserLoginVo(jwt,userInfoVo);
return ResponseResult.okResult(vo);
}
}
UserDetailServiceImpl
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询用户信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(queryWrapper);
//判断是否查到用户 如果没查到抛出异常
if(Objects.isNull(user)){
throw new RuntimeException("用户不存在");
}
//返回用户信息
// TODO 查询权限信息封装
return new LoginUser(user);
}
}
LoginUser
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
@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;
}
}
BlogUserLoginVo
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BlogUserLoginVo {
private String token;
private UserInfoVo userInfo;
}
UserInfoVo
@Data
@Accessors(chain = true)
public class UserInfoVo {
/**
* 主键
*/
private Long id;
/**
* 昵称
*/
private String nickName;
/**
* 头像
*/
private String avatar;
private String sex;
private String email;
}
3.6.6 Login verification filter code implementation
Ideas
①Define Jwt authentication filter
Get token
Parse the token to get the userid in it
Get user information from redis
Save to SecurityContextHolder
JwtAuthenticationTokenFilter
@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("token");
if(!StringUtils.hasText(token)){
//说明该接口不需要登录 直接放行
filterChain.doFilter(request, response);
return;
}
//解析获取userid
Claims claims = null;
try {
claims = JwtUtil.parseJWT(token);
} catch (Exception e) {
e.printStackTrace();
//token超时 token非法
//响应告诉前端需要重新登录
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
WebUtils.renderString(response, JSON.toJSONString(result));
return;
}
String userId = claims.getSubject();
//从redis中获取用户信息
LoginUser loginUser = redisCache.getCacheObject("bloglogin:" + userId);
//如果获取不到
if(Objects.isNull(loginUser)){
//说明登录过期 提示重新登录
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
WebUtils.renderString(response, JSON.toJSONString(result));
return;
}
//存入SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()
//jwt过滤器测试用,如果测试没有问题吧这里删除了
.antMatchers("/link/getAllLink").authenticated()
// 除上面外的所有请求全部不需要认证即可访问
.anyRequest().permitAll();
http.logout().disable();
//把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//允许跨域
http.cors();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
3.7 Authentication and authorization failure handling
At present, the Json that our project responds to when there is an authentication error or insufficient permissions is the result of Security's exception handling. But the format of this response definitely does not comply with the interface specification of our project. So you need to customize exception handling.
AuthenticationEntryPoint authentication failure handler
AccessDeniedHandler authorization failure handler
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
authException.printStackTrace();
//InsufficientAuthenticationException
//BadCredentialsException
ResponseResult result = null;
if(authException instanceof BadCredentialsException){
result = ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_ERROR.getCode(),authException.getMessage());
}else if(authException instanceof InsufficientAuthenticationException){
result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
}else{
result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR.getCode(),"认证或授权失败");
}
//响应给前端
WebUtils.renderString(response, JSON.toJSONString(result));
}
}
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
accessDeniedException.printStackTrace();
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NO_OPERATOR_AUTH);
//响应给前端
WebUtils.renderString(response, JSON.toJSONString(result));
}
}
Configure Security exception handler
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
AccessDeniedHandler accessDeniedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()
//jwt过滤器测试用,如果测试没有问题吧这里删除了
.antMatchers("/link/getAllLink").authenticated()
// 除上面外的所有请求全部不需要认证即可访问
.anyRequest().permitAll();
//配置异常处理器
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
http.logout().disable();
//把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//允许跨域
http.cors();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
3.8 Unified exception handling
In fact, we may need to do a lot of judgment and verification during the development process. If an illegal situation occurs, we expect to respond to the corresponding prompts. But it would be very troublesome if we handle it manually every time. We can choose to throw exceptions directly and then handle them uniformly. Encapsulate the information in the exception into ResponseResult and send it to the front end.
SystemException
/**
* @Author 三更 B站: https://space.bilibili.com/663528522
*/
public class SystemException extends RuntimeException{
private int code;
private String msg;
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
public SystemException(AppHttpCodeEnum httpCodeEnum) {
super(httpCodeEnum.getMsg());
this.code = httpCodeEnum.getCode();
this.msg = httpCodeEnum.getMsg();
}
}
GlobalExceptionHandler
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(SystemException.class)
public ResponseResult systemExceptionHandler(SystemException e){
//打印异常信息
log.error("出现了异常! {}",e);
//从异常对象中获取提示信息封装返回
return ResponseResult.errorResult(e.getCode(),e.getMsg());
}
@ExceptionHandler(Exception.class)
public ResponseResult exceptionHandler(Exception e){
//打印异常信息
log.error("出现了异常! {}",e);
//从异常对象中获取提示信息封装返回
return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR.getCode(),e.getMessage());
}
}
3.9 Log out of the login interface
3.9.1 Interface design
Request method | Request address | Request header |
---|---|---|
POST | /logout | Requires token request header |
Response format:
{
"code": 200,
"msg": "操作成功"
}
3.9.2 Code implementation
What to do:
Delete user information in redis
BlogLoginController
@PostMapping("/logout")
public ResponseResult logout(){
return blogLoginService.logout();
}
BlogLoginService
ResponseResult logout();
BlogLoginServiceImpl
@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("bloglogin:"+userId);
return ResponseResult.okResult();
}
SecurityConfig
To turn off the default logout feature. And to configure our logout interface, authentication is required to access it.
@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()
//jwt过滤器测试用,如果测试没有问题吧这里删除了
.antMatchers("/link/getAllLink").authenticated()
// 除上面外的所有请求全部不需要认证即可访问
.anyRequest().permitAll();
//配置异常处理器
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
//关闭默认的注销功能
http.logout().disable();
//把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//允许跨域
http.cors();
}
3.10 Query comment list interface
3.10.1 Requirements
The article details page should display the list of comments under this article.
3.10.2 Interface design
Request method | Request address | Request header |
---|---|---|
GET | /comment/commentList | No token request header is required |
Query format request parameters:
articleId: article id
pageNum: page number
pageSize: number of items per page
Response format:
{
"code": 200,
"data": {
"rows": [
{
"articleId": "1",
"children": [
{
"articleId": "1",
"content": "你说啥?",
"createBy": "1",
"createTime": "2022-01-30 10:06:21",
"id": "20",
"rootId": "1",
"toCommentId": "1",
"toCommentUserId": "1",
"toCommentUserName": "sg333",
"username": "sg333"
}
],
"content": "asS",
"createBy": "1",
"createTime": "2022-01-29 07:59:22",
"id": "1",
"rootId": "-1",
"toCommentId": "-1",
"toCommentUserId": "-1",
"username": "sg333"
}
],
"total": "15"
},
"msg": "操作成功"
}
3.10.4 Code implementation
3.10.4.1 Sub-comments are not considered
CommentController
@RestController
@RequestMapping("/comment")
public class CommentController {
@Autowired
private CommentService commentService;
@GetMapping("/commentList")
public ResponseResult commentList(Long articleId,Integer pageNum,Integer pageSize){
return commentService.commentList(articleId,pageNum,pageSize);
}
}
CommentService
public interface CommentService extends IService<Comment> {
ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize);
}
CommentServiceImpl
@Service("commentService")
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {
@Autowired
private UserService userService;
@Override
public ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize) {
//查询对应文章的根评论
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
//对articleId进行判断
queryWrapper.eq(Comment::getArticleId,articleId);
//根评论 rootId为-1
queryWrapper.eq(Comment::getRootId,-1);
//分页查询
Page<Comment> page = new Page(pageNum,pageSize);
page(page,queryWrapper);
List<CommentVo> commentVoList = toCommentVoList(page.getRecords());
return ResponseResult.okResult(new PageVo(commentVoList,page.getTotal()));
}
private List<CommentVo> toCommentVoList(List<Comment> list){
List<CommentVo> commentVos = BeanCopyUtils.copyBeanList(list, CommentVo.class);
//遍历vo集合
for (CommentVo commentVo : commentVos) {
//通过creatyBy查询用户的昵称并赋值
String nickName = userService.getById(commentVo.getCreateBy()).getNickName();
commentVo.setUsername(nickName);
//通过toCommentUserId查询用户的昵称并赋值
//如果toCommentUserId不为-1才进行查询
if(commentVo.getToCommentUserId()!=-1){
String toCommentUserName = userService.getById(commentVo.getToCommentUserId()).getNickName();
commentVo.setToCommentUserName(toCommentUserName);
}
}
return commentVos;
}
}
CommentVo
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommentVo {
private Long id;
//文章id
private Long articleId;
//根评论id
private Long rootId;
//评论内容
private String content;
//所回复的目标评论的userid
private Long toCommentUserId;
private String toCommentUserName;
//回复目标评论id
private Long toCommentId;
private Long createBy;
private Date createTime;
private String username;
}
3.10.4.2 Query sub-comments
CommentVo added private List children based on the previous one;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommentVo {
private Long id;
//文章id
private Long articleId;
//根评论id
private Long rootId;
//评论内容
private String content;
//所回复的目标评论的userid
private Long toCommentUserId;
private String toCommentUserName;
//回复目标评论id
private Long toCommentId;
private Long createBy;
private Date createTime;
private String username;
private List<CommentVo> children;
}
CommentServiceImpl
@Service("commentService")
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {
@Autowired
private UserService userService;
@Override
public ResponseResult commentList(Long articleId, Integer pageNum, Integer pageSize) {
//查询对应文章的根评论
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
//对articleId进行判断
queryWrapper.eq(Comment::getArticleId,articleId);
//根评论 rootId为-1
queryWrapper.eq(Comment::getRootId,-1);
//分页查询
Page<Comment> page = new Page(pageNum,pageSize);
page(page,queryWrapper);
List<CommentVo> commentVoList = toCommentVoList(page.getRecords());
//查询所有根评论对应的子评论集合,并且赋值给对应的属性
for (CommentVo commentVo : commentVoList) {
//查询对应的子评论
List<CommentVo> children = getChildren(commentVo.getId());
//赋值
commentVo.setChildren(children);
}
return ResponseResult.okResult(new PageVo(commentVoList,page.getTotal()));
}
/**
* 根据根评论的id查询所对应的子评论的集合
* @param id 根评论的id
* @return
*/
private List<CommentVo> getChildren(Long id) {
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Comment::getRootId,id);
queryWrapper.orderByAsc(Comment::getCreateTime);
List<Comment> comments = list(queryWrapper);
List<CommentVo> commentVos = toCommentVoList(comments);
return commentVos;
}
private List<CommentVo> toCommentVoList(List<Comment> list){
List<CommentVo> commentVos = BeanCopyUtils.copyBeanList(list, CommentVo.class);
//遍历vo集合
for (CommentVo commentVo : commentVos) {
//通过creatyBy查询用户的昵称并赋值
String nickName = userService.getById(commentVo.getCreateBy()).getNickName();
commentVo.setUsername(nickName);
//通过toCommentUserId查询用户的昵称并赋值
//如果toCommentUserId不为-1才进行查询
if(commentVo.getToCommentUserId()!=-1){
String toCommentUserName = userService.getById(commentVo.getToCommentUserId()).getNickName();
commentVo.setToCommentUserName(toCommentUserName);
}
}
return commentVos;
}
}
3.11 Comment interface
3.11.1 Requirements
After logging in, users can comment on articles and reply to comments.
After logging in, users can also comment on the friend link page.
3.11.2 Interface design
Request method | Request address | Request header |
---|---|---|
POST | /comment | Requires token header |
Request body:
Replied to the article:
{
"articleId":1,"type":0,"rootId":-1,"toCommentId":-1,"toCommentUserId":-1,"content":"评论了文章"}
Reply to a comment:
{
"articleId":1,"type":0,"rootId":"3","toCommentId":"3","toCommentUserId":"1","content":"回复了某条评论"}
If it is a friend link comment, type should be 1
Response format:
{
"code":200,
"msg":"操作成功"
}
3.11.3 Code implementation
CommentController
@PostMapping
public ResponseResult addComment(@RequestBody Comment comment){
return commentService.addComment(comment);
}
CommentService
ResponseResult addComment(Comment comment);
CommentServiceImpl
@Override
public ResponseResult addComment(Comment comment) {
//评论内容不能为空
if(!StringUtils.hasText(comment.getContent())){
throw new SystemException(AppHttpCodeEnum.CONTENT_NOT_NULL);
}
save(comment);
return ResponseResult.okResult();
}
SecurityUtils
/**
* @Author 三更 B站: https://space.bilibili.com/663528522
*/
public class SecurityUtils
{
/**
* 获取用户
**/
public static LoginUser getLoginUser()
{
return (LoginUser) getAuthentication().getPrincipal();
}
/**
* 获取Authentication
*/
public static Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
public static Boolean isAdmin(){
Long id = getLoginUser().getUser().getId();
return id != null && 1L == id;
}
public static Long getUserId() {
return getLoginUser().getUser().getId();
}
}
3.12 Youlian comment list
3.12.1 Requirements
The friend link page also needs to query the corresponding comment list.
3.12.2 Interface design
Request method | Request address | Request header |
---|---|---|
GET | /comment/linkCommentList | No token request header is required |
Query format request parameters:
pageNum: page number
pageSize: number of items per page
Response format:
{
"code": 200,
"data": {
"rows": [
{
"articleId": "1",
"children": [
{
"articleId": "1",
"content": "回复友链评论3",
"createBy": "1",
"createTime": "2022-01-30 10:08:50",
"id": "23",
"rootId": "22",
"toCommentId": "22",
"toCommentUserId": "1",
"toCommentUserName": "sg333",
"username": "sg333"
}
],
"content": "友链评论2",
"createBy": "1",
"createTime": "2022-01-30 10:08:28",
"id": "22",
"rootId": "-1",
"toCommentId": "-1",
"toCommentUserId": "-1",
"username": "sg333"
}
],
"total": "1"
},
"msg": "操作成功"
}
3.12.3 Code implementation
CommentController modified the previous article comment list interface and added a new Youlian comment interface
@GetMapping("/commentList")
public ResponseResult commentList(Long articleId,Integer pageNum,Integer pageSize){
return commentService.commentList(SystemConstants.ARTICLE_COMMENT,articleId,pageNum,pageSize);
}
@GetMapping("/linkCommentList")
public ResponseResult linkCommentList(Integer pageNum,Integer pageSize){
return commentService.commentList(SystemConstants.LINK_COMMENT,null,pageNum,pageSize);
}
SystemConstants adds two constants
/**
* 评论类型为:文章评论
*/
public static final String ARTICLE_COMMENT = "0";
/**
* 评论类型为:友联评论
*/
public static final String LINK_COMMENT = "1";
CommentService modified the commentList method and added a parameter commentType
ResponseResult commentList(String commentType, Long articleId, Integer pageNum, Integer pageSize);
CommentServiceImpl modifies the code of the commentList method. It only adds articleId judgment when commentType is 0, and adds a comment type.
@Override
public ResponseResult commentList(String commentType, Long articleId, Integer pageNum, Integer pageSize) {
//查询对应文章的根评论
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
//对articleId进行判断
queryWrapper.eq(SystemConstants.ARTICLE_COMMENT.equals(commentType),Comment::getArticleId,articleId);
//根评论 rootId为-1
queryWrapper.eq(Comment::getRootId,-1);
//评论类型
queryWrapper.eq(Comment::getType,commentType);
//分页查询
Page<Comment> page = new Page(pageNum,pageSize);
page(page,queryWrapper);
List<CommentVo> commentVoList = toCommentVoList(page.getRecords());
//查询所有根评论对应的子评论集合,并且赋值给对应的属性
for (CommentVo commentVo : commentVoList) {
//查询对应的子评论
List<CommentVo> children = getChildren(commentVo.getId());
//赋值
commentVo.setChildren(children);
}
return ResponseResult.okResult(new PageVo(commentVoList,page.getTotal()));
}
3.13 Personal information query interface
3.13.1 Requirements
When entering the personal center, you need to be able to view the current user information
3.13.2 Interface design
Request method | Request address | Request header |
---|---|---|
GET | /user/userInfo | Requires token request header |
No parameters required
Response format:
{
"code":200,
"data":{
"avatar":"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fi0.hdslb.com%2Fbfs%2Farticle%2F3bf9c263bc0f2ac5c3a7feb9e218d07475573ec8.gi",
"email":"[email protected]",
"id":"1",
"nickName":"sg333",
"sex":"1"
},
"msg":"操作成功"
}
3.13.3 Code implementation
UserController
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/userInfo")
public ResponseResult userInfo(){
return userService.userInfo();
}
}
UserService adds method definition
public interface UserService extends IService<User> {
ResponseResult userInfo();
}
UserServiceImpl implements userInfo method
@Override
public ResponseResult userInfo() {
//获取当前用户id
Long userId = SecurityUtils.getUserId();
//根据用户id查询用户信息
User user = getById(userId);
//封装成UserInfoVo
UserInfoVo vo = BeanCopyUtils.copyBean(user,UserInfoVo.class);
return ResponseResult.okResult(vo);
}
SecurityConfig configures that the interface must be authenticated before it can be accessed
@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("/user/userInfo").authenticated()
// 除上面外的所有请求全部不需要认证即可访问
.anyRequest().permitAll();
//配置异常处理器
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
//关闭默认的注销功能
http.logout().disable();
//把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//允许跨域
http.cors();
}
3.14 Avatar upload interface
3.14.1 Requirements
You can upload an avatar picture when you click Edit in the personal center. After uploading the avatar, it can be used to update the personal information interface.
3.14.2 OSS
3.14.2.1 Why use OSS
Because if you upload files such as pictures and videos to the web server of your own application, it will take up more resources when reading the pictures. Affect application server performance.
So we generally use OSS (Object Storage Service) to store pictures or videos.
3.14.2.2 Qiniu Cloud basic usage test
3.14.2.3 Qiniu Cloud test code writing
①Add dependencies
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>[7.7.0, 7.7.99]</version>
</dependency>
②Copy and modify the case code
application.yml
oss:
accessKey: xxxx
secretKey: xxxx
bucket: sg-blog
OSSTest.java
@SpringBootTest
@ConfigurationProperties(prefix = "oss")
public class OSSTest {
private String accessKey;
private String secretKey;
private String bucket;
public void setAccessKey(String accessKey) {
this.accessKey = accessKey;
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
public void setBucket(String bucket) {
this.bucket = bucket;
}
@Test
public void testOss(){
//构造一个带指定 Region 对象的配置类
Configuration cfg = new Configuration(Region.autoRegion());
//...其他参数参考类注释
UploadManager uploadManager = new UploadManager(cfg);
//...生成上传凭证,然后准备上传
// String accessKey = "your access key";
// String secretKey = "your secret key";
// String bucket = "sg-blog";
//默认不指定key的情况下,以文件内容的hash值作为文件名
String key = "2022/sg.png";
try {
// byte[] uploadBytes = "hello qiniu cloud".getBytes("utf-8");
// ByteArrayInputStream byteInputStream=new ByteArrayInputStream(uploadBytes);
InputStream inputStream = new FileInputStream("C:\\Users\\root\\Desktop\\Snipaste_2022-02-28_22-48-37.png");
Auth auth = Auth.create(accessKey, secretKey);
String upToken = auth.uploadToken(bucket);
try {
Response response = uploadManager.put(inputStream,key,upToken,null, null);
//解析上传成功的结果
DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
System.out.println(putRet.key);
System.out.println(putRet.hash);
} catch (QiniuException ex) {
Response r = ex.response;
System.err.println(r.toString());
try {
System.err.println(r.bodyString());
} catch (QiniuException ex2) {
//ignore
}
}
} catch (Exception ex) {
//ignore
}
}
}
3.14.2 Interface design
Request method | Request address | Request header |
---|---|---|
POST | /upload | need token |
parameter:
img, the value is the file to be uploaded
Request header:
Content-Type :multipart/form-data;
Content-Type, content type, generally refers to the Content-Type that exists in a web page. It is used to define the type of network files and the encoding of the web page, and determines what form and encoding the browser will read the file in.
Common media format types are as follows:
text/html: HTML format
text/plain: plain text
format text/xml: XML format
image/gif: gif image format
image/jpeg: jpg image format
image/png: png image format
Media format types starting with application:
application/xhtml+xml: XHTML format
application/xml: XML data format
application/atom+xml: Atom XML aggregation format
application/json: JSON data format
application/pdf: pdf format
application/msword: Word document format
application/octet-stream : Binary stream data (such as common file downloads)
application/x-www-form-urlencoded : The default encType in the form data is encoded into key/value format and sent to the server (the default submission data format of the form)
common The media format is used when uploading files:
multipart/form-data: This format needs to be used when uploading files in a form.
Response format:
{
"code": 200,
"data": "文件访问链接",
"msg": "操作成功"
}
3.14.3 Code implementation
@RestController
public class UploadController {
@Autowired
private UploadService uploadService;
@PostMapping("/upload")
public ResponseResult uploadImg(MultipartFile img){
return uploadService.uploadImg(img);
}
}
public interface UploadService {
ResponseResult uploadImg(MultipartFile img);
}
@Service
@Data
@ConfigurationProperties(prefix = "oss")
public class OssUploadService implements UploadService {
@Override
public ResponseResult uploadImg(MultipartFile img) {
//判断文件类型
//获取原始文件名
String originalFilename = img.getOriginalFilename();
//对原始文件名进行判断
if(!originalFilename.endsWith(".png")){
throw new SystemException(AppHttpCodeEnum.FILE_TYPE_ERROR);
}
//如果判断通过上传文件到OSS
String filePath = PathUtils.generateFilePath(originalFilename);
String url = uploadOss(img,filePath);// 2099/2/3/wqeqeqe.png
return ResponseResult.okResult(url);
}
private String accessKey;
private String secretKey;
private String bucket;
private String uploadOss(MultipartFile imgFile, String filePath){
//构造一个带指定 Region 对象的配置类
Configuration cfg = new Configuration(Region.autoRegion());
//...其他参数参考类注释
UploadManager uploadManager = new UploadManager(cfg);
//默认不指定key的情况下,以文件内容的hash值作为文件名
String key = filePath;
try {
InputStream inputStream = imgFile.getInputStream();
Auth auth = Auth.create(accessKey, secretKey);
String upToken = auth.uploadToken(bucket);
try {
Response response = uploadManager.put(inputStream,key,upToken,null, null);
//解析上传成功的结果
DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
System.out.println(putRet.key);
System.out.println(putRet.hash);
return "http://r7yxkqloa.bkt.clouddn.com/"+key;
} catch (QiniuException ex) {
Response r = ex.response;
System.err.println(r.toString());
try {
System.err.println(r.bodyString());
} catch (QiniuException ex2) {
//ignore
}
}
} catch (Exception ex) {
//ignore
}
return "www";
}
}
PathUtils
/**
* @Author 三更 B站: https://space.bilibili.com/663528522
*/
public class PathUtils {
public static String generateFilePath(String fileName){
//根据日期生成路径 2022/1/15/
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd/");
String datePath = sdf.format(new Date());
//uuid作为文件名
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
//后缀和文件后缀一致
int index = fileName.lastIndexOf(".");
// test.jpg -> .jpg
String fileType = fileName.substring(index);
return new StringBuilder().append(datePath).append(uuid).append(fileType).toString();
}
}
3.15 Update personal information interface
3.15.1 Requirements
After editing the personal information, click Save to update the personal information.
3.15.2 Interface design
Request method | Request address | Request header |
---|---|---|
PUT | /user/userInfo | Requires token request header |
parameter
json format data in the request body:
{
"avatar":"https://sg-blog-oss.oss-cn-beijing.aliyuncs.com/2022/01/31/948597e164614902ab1662ba8452e106.png",
"email":"[email protected]",
"id":"1",
"nickName":"sg333",
"sex":"1"
}
Response format:
{
"code":200,
"msg":"操作成功"
}
3.15.3 Code implementation
UserController
@PutMapping("/userInfo")
public ResponseResult updateUserInfo(@RequestBody User user){
return userService.updateUserInfo(user);
}
UserService
ResponseResult updateUserInfo(User user);
UserServiceImpl
@Override
public ResponseResult updateUserInfo(User user) {
updateById(user);
return ResponseResult.okResult();
}
3.16 User registration
3.16.1 Requirements
Requires users to complete user registration on the registration interface. It is required that the user name, nickname, and email address cannot be duplicated with the original data in the database. If a certain item fails to register repeatedly, there should be a corresponding prompt. And it is required that the username, password, nickname, and email address cannot be empty.
Note: Passwords must be stored in ciphertext in the database.
3.16.2 Interface design
Request method | Request address | Request header |
---|---|---|
POST | /user/register | No token request header is required |
parameter
json format data in the request body:
{
"email": "string",
"nickName": "string",
"password": "string",
"userName": "string"
}
Response format:
{
"code":200,
"msg":"操作成功"
}
3.16.3 Code implementation
UserController
@PostMapping("/register")
public ResponseResult register(@RequestBody User user){
return userService.register(user);
}
UserService
ResponseResult register(User user);
UserServiceImpl
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public ResponseResult register(User user) {
//对数据进行非空判断
if(!StringUtils.hasText(user.getUserName())){
throw new SystemException(AppHttpCodeEnum.USERNAME_NOT_NULL);
}
if(!StringUtils.hasText(user.getPassword())){
throw new SystemException(AppHttpCodeEnum.PASSWORD_NOT_NULL);
}
if(!StringUtils.hasText(user.getEmail())){
throw new SystemException(AppHttpCodeEnum.EMAIL_NOT_NULL);
}
if(!StringUtils.hasText(user.getNickName())){
throw new SystemException(AppHttpCodeEnum.NICKNAME_NOT_NULL);
}
//对数据进行是否存在的判断
if(userNameExist(user.getUserName())){
throw new SystemException(AppHttpCodeEnum.USERNAME_EXIST);
}
if(nickNameExist(user.getNickName())){
throw new SystemException(AppHttpCodeEnum.NICKNAME_EXIST);
}
//...
//对密码进行加密
String encodePassword = passwordEncoder.encode(user.getPassword());
user.setPassword(encodePassword);
//存入数据库
save(user);
return ResponseResult.okResult();
}
public enum AppHttpCodeEnum {
// 成功
SUCCESS(200,"操作成功"),
// 登录
NEED_LOGIN(401,"需要登录后操作"),
NO_OPERATOR_AUTH(403,"无权限操作"),
SYSTEM_ERROR(500,"出现错误"),
USERNAME_EXIST(501,"用户名已存在"),
PHONENUMBER_EXIST(502,"手机号已存在"), EMAIL_EXIST(503, "邮箱已存在"),
REQUIRE_USERNAME(504, "必需填写用户名"),
CONTENT_NOT_NULL(506, "评论内容不能为空"),
FILE_TYPE_ERROR(507, "文件类型错误,请上传png文件"),
USERNAME_NOT_NULL(508, "用户名不能为空"),
NICKNAME_NOT_NULL(509, "昵称不能为空"),
PASSWORD_NOT_NULL(510, "密码不能为空"),
EMAIL_NOT_NULL(511, "邮箱不能为空"),
NICKNAME_EXIST(512, "昵称已存在"),
LOGIN_ERROR(505,"用户名或密码错误");
int code;
String msg;
AppHttpCodeEnum(int code, String errorMessage){
this.code = code;
this.msg = errorMessage;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
3.17 AOP实现日志记录
3.17.1 需求
需要通过日志记录接口调用信息。便于后期调试排查。并且可能有很多接口都需要进行日志的记录。
3.17.2 思路分析
相当于是对原有的功能进行增强。并且是批量的增强,这个时候就非常适合用AOP来进行实现。
3.17.3 代码实现
日志打印格式
log.info("=======Start=======");
// 打印请求 URL
log.info("URL : {}",);
// 打印描述信息
log.info("BusinessName : {}", );
// 打印 Http method
log.info("HTTP Method : {}", );
// 打印调用 controller 的全路径以及执行方法
log.info("Class Method : {}.{}", );
// 打印请求的 IP
log.info("IP : {}",);
// 打印请求入参
log.info("Request Args : {}",);
// 打印出参
log.info("Response : {}", );
// 结束后换行
log.info("=======End=======" + System.lineSeparator());
3.18 更新浏览次数
3.18.1 需求
在用户浏览博文时要实现对应博客浏览量的增加。
3.18.2 思路分析
我们只需要在每次用户浏览博客时更新对应的浏览数即可。
但是如果直接操作博客表的浏览量的话,在并发量大的情况下会出现什么问题呢?
如何去优化呢?
①在应用启动时把博客的浏览量存储到redis中
②更新浏览量时去更新redis中的数据
③每隔10分钟把Redis中的浏览量更新到数据库中
④读取文章浏览量时从redis读取
3.18.3 铺垫知识
3.18.3.1 CommandLineRunner实现项目启动时预处理
如果希望在SpringBoot应用启动时进行一些初始化操作可以选择使用CommandLineRunner来进行处理。
我们只需要实现CommandLineRunner接口,并且把对应的bean注入容器。把相关初始化的代码重新到需要重新的方法中。
这样就会在应用启动的时候执行对应的代码。
@Component
public class TestRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("程序初始化");
}
}
3.18.3.2 定时任务
定时任务的实现方式有很多,比如XXL-Job等。但是其实核心功能和概念都是类似的,很多情况下只是调用的API不同而已。
这里就先用SpringBoot为我们提供的定时任务的API来实现一个简单的定时任务,让大家先对定时任务里面的一些核心概念有个大致的了解。
实现步骤
① 使用@EnableScheduling注解开启定时任务功能
我们可以在配置类上加上@EnableScheduling
@SpringBootApplication
@MapperScan("com.sangeng.mapper")
@EnableScheduling
public class SanGengBlogApplication {
public static void main(String[] args) {
SpringApplication.run(SanGengBlogApplication.class,args);
}
}
② 确定定时任务执行代码,并配置任务执行时间
使用@Scheduled注解标识需要定时执行的代码。注解的cron属性相当于是任务的执行时间。目前可以使用 0/5 * * * * ? 进行测试,代表从0秒开始,每隔5秒执行一次。
注意:对应的bean要注入容器,否则不会生效。
@Component
public class TestJob {
@Scheduled(cron = "0/5 * * * * ?")
public void testJob(){
//要执行的代码
System.out.println("定时任务执行了");
}
}
3.18.3.2.1 cron 表达式语法
cron表达式是用来设置定时任务执行时间的表达式。
很多情况下我们可以用 : 在线Cron表达式生成器 来帮助我们理解cron表达式和书写cron表达式。
但是我们还是有需要学习对应的Cron语法的,这样可以更有利于我们书写Cron表达式。
如上我们用到的 0/5 * * * * ? *,cron表达式由七部分组成,中间由空格分隔,这七部分从左往右依次是:
秒(0-59),分钟(0-59),小时(0~23),日期(1-月最后一天),月份(1-12),星期几(1-7,1表示星期日),年份(一般该项不设置,直接忽略掉,即可为空值)
通用特殊字符:, - * / (可以在任意部分使用)
星号表示任意值,例如:
* * * * * ?
表示 “ 每年每月每天每时每分每秒 ” 。
,
可以用来定义列表,例如 :
1,2,3 * * * * ?
表示 “ 每年每月每天每时每分的每个第1秒,第2秒,第3秒 ” 。
定义范围,例如:
1-3 * * * * ?
表示 “ 每年每月每天每时每分的第1秒至第3秒 ”。
/
每隔多少,例如
5/10 * * * * ?
表示 “ 每年每月每天每时每分,从第5秒开始,每10秒一次 ” 。即 “ / ” 的左侧是开始值,右侧是间隔。如果是从 “ 0 ” 开始的话,也可以简写成 “ /10 ”
3.18.4 接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
PUT | /article/updateViewCount/{id} | 不需要token请求头 |
参数
请求路径中携带文章id
响应格式:
{
"code":200,
"msg":"操作成功"
}
3.18.5 代码实现
①在应用启动时把博客的浏览量存储到redis中
实现CommandLineRunner接口,在应用启动时初始化缓存。
@Component
public class ViewCountRunner implements CommandLineRunner {
@Autowired
private ArticleMapper articleMapper;
@Autowired
private RedisCache redisCache;
@Override
public void run(String... args) throws Exception {
//查询博客信息 id viewCount
List<Article> articles = articleMapper.selectList(null);
Map<String, Integer> viewCountMap = articles.stream()
.collect(Collectors.toMap(article -> article.getId().toString(), article -> {
return article.getViewCount().intValue();//
}));
//存储到redis中
redisCache.setCacheMap("article:viewCount",viewCountMap);
}
}
②更新浏览量时去更新redsi中的数据
RedisCache增加方法
public void incrementCacheMapValue(String key,String hKey,long v){
redisTemplate.boundHashOps(key).increment(hKey, v);
}
Add method to Update Reading Count in ArticleController
@PutMapping("/updateViewCount/{id}")
public ResponseResult updateViewCount(@PathVariable("id") Long id){
return articleService.updateViewCount(id);
}
Add method to ArticleService
ResponseResult updateViewCount(Long id);
Implementation methods in ArticleServiceImpl
@Override
public ResponseResult updateViewCount(Long id) {
//更新redis中对应 id的浏览量
redisCache.incrementCacheMapValue("article:viewCount",id.toString(),1);
return ResponseResult.okResult();
}
③ Scheduled tasks update the views in Redis to the database every 10 minutes
Add construction method to Article
public Article(Long id, long viewCount) {
this.id = id;
this.viewCount = viewCount;
}
@Component
public class UpdateViewCountJob {
@Autowired
private RedisCache redisCache;
@Autowired
private ArticleService articleService;
@Scheduled(cron = "0/5 * * * * ?")
public void updateViewCount(){
//获取redis中的浏览量
Map<String, Integer> viewCountMap = redisCache.getCacheMap("article:viewCount");
List<Article> articles = viewCountMap.entrySet()
.stream()
.map(entry -> new Article(Long.valueOf(entry.getKey()), entry.getValue().longValue()))
.collect(Collectors.toList());
//更新到数据库中
articleService.updateBatchById(articles);
}
}
④Read from redis when reading article views
@Override
public ResponseResult getArticleDetail(Long id) {
//根据id查询文章
Article article = getById(id);
//从redis中获取viewCount
Integer viewCount = redisCache.getCacheMapValue("article:viewCount", id.toString());
article.setViewCount(viewCount.longValue());
//转换成VO
ArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class);
//根据分类id查询分类名
Long categoryId = articleDetailVo.getCategoryId();
Category category = categoryService.getById(categoryId);
if(category!=null){
articleDetailVo.setCategoryName(category.getName());
}
//封装响应返回
return ResponseResult.okResult(articleDetailVo);
}
4. Swagger2
4.1 Introduction
Swagger is a set of open source tools based on the OpenAPI specification that can help us design, build, record, and use Rest APIs.
4.2 Why use Swagger
At present, many companies adopt a development model in which front-end and back-end are separated, and the work of front-end and back-end is completed by different engineers. In this development model, maintaining a timely and complete Rest API document will greatly improve our work efficiency. In the traditional sense, documents are manually written by back-end developers. I believe everyone knows that it is difficult to ensure the timeliness of documents in this way. Such documents will lose their reference significance over time, but will also increase our communication costs. Swagger provides us with a brand new way to maintain API documents. Let's take a look at its advantages:
1. The code changes and the documentation changes. With only a small amount of annotations, Swagger can automatically generate API documents based on the code, which ensures the timeliness of the documents.
2. Cross-language, supporting more than 40 languages.
3. Swagger UI presents an interactive API document. We can try API calls directly on the document page, eliminating the need to prepare complex call parameters.
4.3 Quick Start
4.3.1 Introducing dependencies
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
4.3.2 Enable Swagger2
Add the @EnableSwagger2 annotation to the startup class or configuration class
@SpringBootApplication
@MapperScan("com.sangeng.mapper")
@EnableScheduling
@EnableSwagger2
public class SanGengBlogApplication {
public static void main(String[] args) {
SpringApplication.run(SanGengBlogApplication.class,args);
}
}
4.4 Specific configuration
4.4.1 Controller configuration
4.4.1 @Api annotation
Property introduction:
tags set tags
description Set description information
@RestController
@RequestMapping("/comment")
@Api(tags = "评论",description = "评论相关接口")
public class CommentController {
}
4.4.2 Interface configuration
4.4.2.1 Interface description configuration @ApiOperation
@GetMapping("/linkCommentList")
@ApiOperation(value = "友链评论列表",notes = "获取一页友链评论")
public ResponseResult linkCommentList(Integer pageNum,Integer pageSize){
return commentService.commentList(SystemConstants.LINK_COMMENT,null,pageNum,pageSize);
}
4.4.2.2 Interface parameter description
@ApiImplicitParam 用于描述接口的参数,但是一个接口可能有多个参数,所以一般与 @ApiImplicitParams 组合使用。
@GetMapping("/linkCommentList")
@ApiOperation(value = "友链评论列表",notes = "获取一页友链评论")
@ApiImplicitParams({
@ApiImplicitParam(name = "pageNum",value = "页号"),
@ApiImplicitParam(name = "pageSize",value = "每页大小")
}
)
public ResponseResult linkCommentList(Integer pageNum,Integer pageSize){
return commentService.commentList(SystemConstants.LINK_COMMENT,null,pageNum,pageSize);
}
4.4.3 实体类配置
4.4.3.1 实体的描述配置@ApiModel
@ApiModel用于描述实体类。
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(description = "添加评论dto")
public class AddCommentDto{
//..
}
4.4.3.2 实体的属性的描述配置@ApiModelProperty
@ApiModelProperty用于描述实体的属性
@ApiModelProperty(notes = "评论类型(0代表文章评论,1代表友链评论)")
private String type;
4.4.4 文档信息配置
@Configuration
public class SwaggerConfig {
@Bean
public Docket customDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.sangeng.controller"))
.build();
}
private ApiInfo apiInfo() {
Contact contact = new Contact("团队名", "http://www.my.com", "[email protected]");
return new ApiInfoBuilder()
.title("文档标题")
.description("文档描述")
.contact(contact) // 联系方式
.version("1.1.0") // 版本
.build();
}
}
5. 博客后台
5.0 准备工作
前端工程启动
npm run dev
①创建启动类
②创建application.yml配置文件
server:
port: 8989
spring:
datasource:
url: jdbc:mysql://localhost:3306/sg_blog?characterEncoding=utf-8&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
servlet:
multipart:
max-file-size: 2MB
max-request-size: 5MB
mybatis-plus:
configuration:
# mybatis-plus 日志信息
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: delFlag #逻辑删除字段 (数据库中逻辑删除字段用delFlag)
logic-delete-value: 1 # 删除值 1
logic-not-delete-value: 0 # 未删除值 0
id-type: auto #主键自增(不设置用 mybatis-plus默认生成的id)
# 当用maven导入了mybatisplus的包时,必须要在yml配置中配置数据库的参数,否则运行boot启动类会报错
③ 整体目录结构
④ 创建Entity,Mapper,Service
EasyCode自动生成,然后放入指定包中
下面以Tag为例细说,后面类似的看这里。
Entity
Mapper
Service
⑤ Controller
⑥添加security相关类——SecurityConfig
和JwtAuthenticationTokenFilter
使admin模块能够使用SpringSecurity(拦截,密码加密,权限等)
SecurityConfig:
import ...
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // @PreAuthorize和@PostAuthorize注解生效
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
// 为啥注入的不是实现类呢? 不传实现类是为了符合开闭原则
@Autowired
AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
AccessDeniedHandler accessDeniedHandler;
@Bean
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() //对登录放行,登录不需要认证。 接口必须携带token
// .antMatchers("/logout").authenticated()//退出接口 必须认证,即必须携带token才能 发出退出请求
// .antMatchers("/user/userInfo").authenticated() //这个接口需要认证之后才能访问
// .antMatchers("/upload").authenticated() //前端vue上传图片时,没有要求token,所以后端不需要认证,不需要传token即可
// .antMatchers("/link/getAllLink").authenticated() //这个接口需要认证之后才能访问
// 除上面外的所有请求全部不需要认证即可访问
// .anyRequest().permitAll(); //所有需求都允许访问
.anyRequest().authenticated(); //所有需求 都需要认证
//配置异常处理器
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint) //认证失败处理器
.accessDeniedHandler(accessDeniedHandler); //授权失败处理器
http.logout().disable(); //关闭默认 logout功能 禁用security默认的注销功能
//将过滤器 配置到 UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//允许跨域
http.cors();
}
@Override
@Bean //暴露ProvideManager方法注入到spring bean容器中
public AuthenticationManager authenticationManagerBean() throws Exception {
// 这一段配置用于登录时认证,只有使用了这个配置才能自动注入AuthenticationManager,并使用它来进行用户认证
return super.authenticationManagerBean();
}
}
JwtAuthenticationTokenFilter:
import ...
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 登录校验过滤器 定义Jwt认证过滤器 浏览器登录后,再次发送请求时,会携带token,让服务器验证
//获取请求头中的token
String token = request.getHeader("token");
if(!StringUtils.hasText(token)){
//没有token就说明是第一次登录,直接放行
//说明该接口 不需要登录,直接放行
filterChain.doFilter(request,response);
return; //放行,程序到此结束
}
//解析token 获取userid
Claims claims = null;
try {
claims = JwtUtil.parseJWT(token);
} catch (Exception e) {
e.printStackTrace();
//token超时,token非法
//响应告诉前端重新登录 (响应一个json格式)
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);// 401 请重新登录
WebUtils.renderString(response, JSON.toJSONString(result)); //转成json,并把json串写到响应体当中
return;
}
String userId = claims.getSubject(); // 拿到userid
//从redis获取用户信息(如果获取失败,则验证失败)
LoginUser loginUser = redisCache.getCacheObject("login:" + userId);
//如果获取不到
if (Objects.isNull(loginUser)){
//说明登录过期,重新登录
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);// 401 请重新登录
WebUtils.renderString(response, JSON.toJSONString(result)); //转成json,并把json串写到响应体当中
return;
}
//存入SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);//未认证,用两个参数,认证过,用三个参数
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 都执行完了要 放行,让下面的filter处理
filterChain.doFilter(request,response);
}
}
5.1 后台登录
后台的认证授权使用SpringSecurity安全框架来实现。
5.1.0 需求
登录功能
后台所有功能都必须登录才能使用。
5.1.1 接口设计
请求方式 | 请求路径 |
---|---|
POST | /user/login |
请求体:
{
"userName":"sg",
"password":"1234"
}
响应格式:
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0ODBmOThmYmJkNmI0NjM0OWUyZjY2NTM0NGNjZWY2NSIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTY0Mzg3NDMxNiwiZXhwIjoxNjQzOTYwNzE2fQ.ldLBUvNIxQCGemkCoMgT_0YsjsWndTg5tqfJb77pabk"
},
"msg": "操作成功"
}
5.1.2 思路分析
登录
①自定义登录接口
调用ProviderManager的方法进行认证 如果认证通过生成jwt
把用户信息存入redis中
②自定义UserDetailsService
在这个实现类中去查询数据库
Pay attention to configuring passwordEncoder as BCryptPasswordEncoder
check:
①Define Jwt authentication filter
Get token
Parse the token to get the userid in it
Get user information from redis
Save to SecurityContextHolder
5.1.3 Preparation
①Add dependencies, add the following dependencies to the pom of the sangeng-framework module
- redis dependency
- fastjsondependency
- jwt dependency
5.1.4 Login interface code implementation
LoginController
LoginService
SecurityConfig
Add security related classes - SecurityConfig
and JwtAuthenticationTokenFilter
code in the front-end system.
Enable the admin module to use SpringSecurity (interception, password encryption, permissions, etc.)
LoginServiceImpl
@Service
public class SystemLoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;//SecurityConfig配置AuthenticationManager已经注入到容器当中,可以使用
//AuthenticationManager.ProviderManager的方法进行认证
@Autowired
private RedisCache redisCache; //存入redis,需要使用rediscache
@Override
public ResponseResult login(User user) {
// 调用ProviderManager的方法进行认证 如果认证通过生成jwt
// UsernamePasswordAuthenticationToken -> AbstractAuthenticationToken -> Authentication
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());//传用户名 密码
Authentication authenticate = authenticationManager.authenticate(authenticationToken);//调用认证方法 参数是Authentication,使用它的实现类UsernamePasswordAuthenticationToken
// authenticationManager 实际上 会默认调用UserDetailsService接口去认证,所以要重写UserDetailsService接口
//security会使用UserDetailsService实现类中的loadUserByUsername方法进行校验,所以要重写该方法
// return new LoginUser(user) 返回给 Authentication authenticate,包含了 UserDetails对象(权限信息 + 用户信息)
// authenticationManager.authenticate() 认证的时候,自动进行密码比对
//判断是否通过
if (Objects.isNull(authenticate)){
throw new RuntimeException("用户名或密码错误");
}
//获取userid,生成token
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();//获取认证主体 强转成LoginUser
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId); //jwt:把userid加密后的密文 即token
//把用户信息存入redis 格式(bloglogin:id,loginUser) (包含权限信息)
redisCache.setCacheObject("login:"+userId,loginUser);
//把token封装 返回
Map<String,String> map = new HashMap<>();
map.put("token",jwt);
return ResponseResult.okResult(map);
}
@Override
public ResponseResult logout() {
//获取当前登录的用户id
Long userId = SecurityUtils.getUserId();
//删除redis中对应的值
redisCache.deleteObject("login:"+userId);
return ResponseResult.okResult();
}
}
5.2 Background permission control and dynamic routing
The backend system needs to be able to implement different user permissions to see different functions.
A user can only use the functions allowed by his permissions.
feature design
RBAC permission model
table analysis
Interface design
getInfo interface
Request method | Request address | Request header |
---|---|---|
GET | /getInfo | Requires token request header |
Request parameters: None
Response format:
If the user ID is 1, it represents the administrator. Only admin needs to be included in roles, and all permissions with menu type C or F, normal status, and undeleted permissions need to be included in permissions.
{
"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":"[email protected]",
"id":1,
"nickName":"sg3334",
"sex":"1"
}
},
"msg":"操作成功"
}
getRoutersInterface
Request method | Request address | Request header |
---|---|---|
GET | /getRouters | Requires token request header |
Request parameters: None
Response format:
In order to achieve the effect of dynamic routing on the front end, the back end needs to have an interface that can return menu data that users can access.
Note: The returned menu data needs to reflect the hierarchical relationship between parent and child menus
If the user ID is 1, it represents the administrator. Menus needs to have all menu types of C or M, the status is normal, and the permissions have not been deleted.
The data format is as follows:
{
"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":"操作成功"
}
Code
Preparation
Generate classes for menu and role tables
getInfo interface
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class AdminUserInfoVo {
private List<String> permissions;
private List<String> roles;
private UserInfoVo user;
}
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@Autowired
private MenuService menuService;
@Autowired
private RoleService roleService;
@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user){
if(!StringUtils.hasText(user.getUserName())){
//提示 必须要传用户名
throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);
}
return loginService.login(user);
}
@GetMapping("getInfo")
public ResponseResult<AdminUserInfoVo> getInfo(){
//获取当前登录的用户
LoginUser loginUser = SecurityUtils.getLoginUser();
//根据用户id查询权限信息
List<String> perms = menuService.selectPermsByUserId(loginUser.getUser().getId());
//根据用户id查询角色信息
List<String> roleKeyList = roleService.selectRoleKeyByUserId(loginUser.getUser().getId());
//获取用户信息
User user = loginUser.getUser();
UserInfoVo userInfoVo = BeanCopyUtils.copyBean(user, UserInfoVo.class);
//封装数据返回
AdminUserInfoVo adminUserInfoVo = new AdminUserInfoVo(perms,roleKeyList,userInfoVo);
return ResponseResult.okResult(adminUserInfoVo);
}
}
RoleServiceImpl selectRoleKeyByUserId方法
@Service("menuService")
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {
@Override
public List<String> selectPermsByUserId(Long id) {
//如果是管理员,返回所有的权限
if(id == 1L){
LambdaQueryWrapper<Menu> wrapper = new LambdaQueryWrapper<>();
wrapper.in(Menu::getMenuType,SystemConstants.MENU,SystemConstants.BUTTON);
wrapper.eq(Menu::getStatus,SystemConstants.STATUS_NORMAL);
List<Menu> menus = list(wrapper);
List<String> perms = menus.stream()
.map(Menu::getPerms)
.collect(Collectors.toList());
return perms;
}
//否则返回所具有的权限
return getBaseMapper().selectPermsByUserId(id);
}
}
MenuMapper
/**
* 菜单权限表(Menu)表数据库访问层
*
* @author makejava
* @since 2022-08-09 22:32:07
*/
public interface MenuMapper extends BaseMapper<Menu> {
List<String> selectPermsByUserId(Long userId);
}
<?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.sangeng.mapper.MenuMapper">
<select id="selectPermsByUserId" resultType="java.lang.String">
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>
</mapper>
MenuServiceImpl selectPermsByUserId method
@Service("roleService")
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
@Override
public List<String> selectRoleKeyByUserId(Long id) {
//判断是否是管理员 如果是返回集合中只需要有admin
if(id == 1L){
List<String> roleKeys = new ArrayList<>();
roleKeys.add("admin");
return roleKeys;
}
//否则查询用户所具有的角色信息
return getBaseMapper().selectRoleKeyByUserId(id);
}
}
public interface RoleMapper extends BaseMapper<Role> {
List<String> selectRoleKeyByUserId(Long userId);
}
<?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.sangeng.mapper.RoleMapper">
<select id="selectRoleKeyByUserId" resultType="java.lang.String">
SELECT
r.`role_key`
FROM
`sys_user_role` ur
LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
WHERE
ur.`user_id` = #{userId} AND
r.`status` = 0 AND
r.`del_flag` = 0
</select>
</mapper>
getRoutersInterface
RoutersVo
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RoutersVo {
private List<Menu> menus;
}
LoginController
@GetMapping("getRouters")
public ResponseResult<RoutersVo> getRouters(){
Long userId = SecurityUtils.getUserId();
//查询menu 结果是tree的形式
List<Menu> menus = menuService.selectRouterMenuTreeByUserId(userId);
//封装数据返回
return ResponseResult.okResult(new RoutersVo(menus));
}
MenuService
public interface MenuService extends IService<Menu> {
List<String> selectPermsByUserId(Long id);
List<Menu> selectRouterMenuTreeByUserId(Long userId);
}
MenuServiceImpl
@Override
public List<Menu> selectRouterMenuTreeByUserId(Long userId) {
MenuMapper menuMapper = getBaseMapper();
List<Menu> menus = null;
//判断是否是管理员
if(SecurityUtils.isAdmin()){
//如果是 获取所有符合要求的Menu
menus = menuMapper.selectAllRouterMenu();
}else{
//否则 获取当前用户所具有的Menu
menus = menuMapper.selectRouterMenuTreeByUserId(userId);
}
//构建tree
//先找出第一层的菜单 然后去找他们的子菜单设置到children属性中
List<Menu> menuTree = builderMenuTree(menus,0L);
return menuTree;
}
private List<Menu> builderMenuTree(List<Menu> menus, Long parentId) {
List<Menu> menuTree = menus.stream()
.filter(menu -> menu.getParentId().equals(parentId))
.map(menu -> menu.setChildren(getChildren(menu, menus)))
.collect(Collectors.toList());
return menuTree;
}
/**
* 获取存入参数的 子Menu集合
* @param menu
* @param menus
* @return
*/
private List<Menu> getChildren(Menu menu, List<Menu> menus) {
List<Menu> childrenList = menus.stream()
.filter(m -> m.getParentId().equals(menu.getId()))
.map(m->m.setChildren(getChildren(m,menus)))
.collect(Collectors.toList());
return childrenList;
}
MenuMapper.java
List<Menu> selectAllRouterMenu();
List<Menu> selectRouterMenuTreeByUserId(Long userId);
MenuMapper.xml
<select id="selectAllRouterMenu" resultType="com.sangeng.domain.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
`sys_menu` m
WHERE
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="selectRouterMenuTreeByUserId" resultType="com.sangeng.domain.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
`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>
Query columns:
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
Note that you need to sort by parent_id and order_num
5.3 Log out of the login interface
5.3.1 Interface design
Request method | Request address | Request header |
---|---|---|
POST | /user/logout | Requires token request header |
Response format:
{
"code": 200,
"msg": "操作成功"
}
5.3.2 Code implementation
What to do:
Delete user information in redis
LoginController
@PostMapping("/user/logout")
public ResponseResult logout(){
return loginServcie.logout();
}
LoginService
ResponseResult logout();
SystemLoginServiceImpl
@Override
public ResponseResult logout() {
//获取当前登录的用户id
Long userId = SecurityUtils.getUserId();
//删除redis中对应的值
redisCache.deleteObject("login:"+userId);
return ResponseResult.okResult();
}
SecurityConfig
To turn off the default logout feature. And to configure our logout interface, authentication is required to access it.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
// //注销接口需要认证才能访问
// .antMatchers("/logout").authenticated()
// .antMatchers("/user/userInfo").authenticated()
// .antMatchers("/upload").authenticated()
// 除上面外的所有请求全部不需要认证即可访问
.anyRequest().authenticated();
//配置异常处理器
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
//关闭默认的注销功能
http.logout().disable();
//把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//允许跨域
http.cors();
}
5.4 Query tag list
5.4.0 Requirements
In order to facilitate the later management of articles, the tag function needs to be provided. An article can have multiple tags.
In the background, we need a paging tag query function, which requires paging query based on the tag name. In the future, requirements such as remark inquiries may be added .
Note: Deleted tags cannot be queried.
5.4.1 Tag table analysis
Analyze what fields are needed through requirements.
5.4.2 Interface design
Request method | Request path |
---|---|
Get | content/tag/list |
Query format request parameters:
pageNum: page number
pageSize: number of items per page
name: tag name
remark: remarks
Response format:
{
"code":200,
"data":{
"rows":[
{
"id":4,
"name":"Java",
"remark":"sdad"
}
],
"total":1
},
"msg":"操作成功"
}
5.4.3 Code implementation
Controller
@RestController
@RequestMapping("/content/tag")
public class TagController {
@Autowired
private TagService tagService;
@GetMapping("/list")
public ResponseResult<PageVo> list(Integer pageNum, Integer pageSize, TagListDto tagListDto){
return tagService.pageTagList(pageNum,pageSize,tagListDto);
}
}
Service
public interface TagService extends IService<Tag> {
ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto);
}
@Service("tagService")
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {
@Override
public ResponseResult<PageVo> pageTagList(Integer pageNum, Integer pageSize, TagListDto tagListDto) {
//分页查询
LambdaQueryWrapper<Tag> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(StringUtils.hasText(tagListDto.getName()),Tag::getName,tagListDto.getName());
queryWrapper.eq(StringUtils.hasText(tagListDto.getRemark()),Tag::getRemark,tagListDto.getRemark());
Page<Tag> page = new Page<>();
page.setCurrent(pageNum);
page.setSize(pageSize);
page(page, queryWrapper);
//封装数据返回
PageVo pageVo = new PageVo(page.getRecords(),page.getTotal());
return ResponseResult.okResult(pageVo);
}
}
以下代码都是自己手敲的,配有 相关知识点。
5.5 Add new tags
5.5.0 Requirements
Click the new button of tag management to realize the function of adding new tags.
5.5.1 接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
POST | /content/tag | 需要token请求头 |
请求体格式:
{
"name":"c#","remark":"c++++"}
响应格式:
{
"code":200,
"msg":"操作成功"
}
5.5.2 实现
创建时间,修改时间 的日期格式 这里是在 service中实现,也可以在 对应的实体类Tag中添加注解实现。
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
5.6 删除标签
5.6.1 接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
DELETE | /content/tag/{id} | 需要token请求头 |
请求参数在path中
例如:content/tag/6 代表删除id为6的标签数据
响应格式:
{
"code":200,
"msg":"操作成功"
}
5.6.2 实现
删除后在列表中是查看不到该条数据
数据库中该条数据还是存在的,只是修改了逻辑删除字段的值:
需要再配置文件中设置
5.7 修改标签
5.7.1 接口设计
5.7.1.1 获取标签信息
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /content/tag/{id} | 需要token请求头 |
请求参数在path中
例如:content/tag/6 代表获取id为6的标签数据
响应格式:
{
"code":200,
"data":{
"id":4,
"name":"Java",
"remark":"sdad"
},
"msg":"操作成功"
}
实现
5.7.1.2 修改标签接口
请求方式 | 请求地址 | 请求头 |
---|---|---|
PUT | /content/tag | 需要token请求头 |
请求体格式:
{
"id":7,"name":"c#","remark":"c++++"}
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现
5.8 写博文
5.8.1 需求
需要提供写博文的功能,写博文时需要关联分类和标签。
可以上传缩略图,也可以在正文中添加图片。
文章可以直接发布,也可以保存到草稿箱。
5.8.2 表分析
写文章,文章对应有标签,文章还有分类
5.8.2 接口设计
5.8.2.1 查询所有分类接口
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /content/category/listAllCategory | 需要token请求头 |
请求参数:
无
响应格式:
{
"code":200,
"data":[
{
"description":"wsd",
"id":1,
"name":"java"
},
{
"description":"wsd",
"id":2,
"name":"PHP"
}
],
"msg":"操作成功"
}
实现:
5.8.2.2 查询所有标签接口
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /content/tag/listAllTag | 需要token请求头 |
请求参数:
无
响应格式:
{
"code":200,
"data":[
{
"id":1,
"name":"Mybatis"
},
{
"id":4,
"name":"Java"
}
],
"msg":"操作成功"
}
实现
5.8.2.3 上传图片
请求方式 | 请求地址 | 请求头 |
---|---|---|
POST | /upload | 需要token请求头 |
参数:
img,值为要上传的文件
请求头:
Content-Type :multipart/form-data;
响应格式:
{
"code": 200,
"data": "文件访问链接",
"msg": "操作成功"
}
实现
serviceImp
package com.sangeng.service.impl;
import ...
@Service
@Data //生成getter和sertter方法,这里三个成员变量主要使用getter方法进行赋值
@ConfigurationProperties(prefix = "oss") //@ConfigurationProperties 的作用: 让JavaBean中属性值要和配置文件application.xml进行映射
public class OssUploadService implements UploadService {
@Override
public ResponseResult uploadImg(MultipartFile img) {
//img是网络中传过来的流对象
//判断文件类型
//获取原始文件名
String originalFilename = img.getOriginalFilename(); //得到上传时的文件名(本机需要上传的文件名,比如111.jpg上传到oss)
//对原始文件名进行判断(只接受.png .jpg格式文件)
if (!originalFilename.endsWith(".png") && !originalFilename.endsWith(".jpg")){
//如果文件名 不是以 .png 结尾,抛出异常
throw new SystemException(AppHttpCodeEnum.FILE_TYPE_ERROR);
}
//如果判断通过,则上传文件到OSS
// 根据当前文件名,生成一个存放路径 2023/6.14/uuid.png 或.jpg
String filePath = PathUtils.generateFilePath(originalFilename);// 2022/1/15/+uuid+.jpg 得到文件路径
String url = uploadOss(img,filePath); //上传文件至OSS后返回一个外链(可以通过此外链url 访问OSS中上传的图片)
return ResponseResult.okResult(url); //返回结果data是外链 值
}
private String accessKey;
private String secretKey;
private String bucket;
private String uploadOss(MultipartFile imgFile, String filePath){
// 注:用七牛云的oss,所以导包的时候 要导入七牛云的包。 用人家的代码,导人家的包 com.qiniu.storage
//构造一个带指定 Region 对象的配置类
//修改1.Region指定数据存储区域,autoRegion()自动根据七牛云账号找到选的区域(我选的是 华北)
Configuration cfg = new Configuration(Region.autoRegion());
cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2;// 指定分片上传版本
UploadManager uploadManager = new UploadManager(cfg);
//...生成上传凭证,然后准备上传
注:为了安全起见,AK,SK,bucket存储空间名,都是从application.xml 配置文件中读取到的
修改2.复制七牛云官网-个人中心-密钥管理- AK和SK
// String accessKey = "";
// String secretKey = "";
修改3.创建存储空间的名字 pk-sg-blog
// String bucket = "";
//默认不指定key的情况下,以文件内容的hash值作为文件名, 比如上传一张图片,名字为hash值生成的名字
//修改4.指定上传文件到oss时,文件的存储名
String key = filePath; // 指定生成路径
try {
//修改5 注释掉默认上传,改成 前端传过来的文件流
// byte[] uploadBytes = "hello qiniu cloud".getBytes("utf-8");
// ByteArrayInputStream byteInputStream=new ByteArrayInputStream(uploadBytes);
//上传文件——前端传过来的图片
// 获取到网络中传过来的流对象的inputstream
InputStream inputStream = imgFile.getInputStream();//前端传过来的文件转成inputstream流
Auth auth = Auth.create(accessKey, secretKey);//创建凭证
String upToken = auth.uploadToken(bucket); //上传凭证
try {
//修改6 put方法 第一个参数 要放上面 自己定义的 inputStream对象
Response response = uploadManager.put(inputStream,key,upToken,null, null);
//解析上传成功的结果
DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
System.out.println(putRet.key); //111.png key值就是上传后的 文件名
System.out.println(putRet.hash); //Fo2AVLRHugoNbek6XZ8Uy-DCnuSL
// 外链 http://rw7y62wqd.hb-bkt.clouddn.com/111.png
// 外链= http://+ 测试域名+文件名 七牛云测试域名,免费用30天,过期回收域名
return "http://rw7y62wqd.hb-bkt.clouddn.com/"+key; //上传成功,返回一个对应的外链
} catch (QiniuException ex) {
Response r = ex.response;
System.err.println(r.toString());
try {
System.err.println(r.bodyString());
} catch (QiniuException ex2) {
//ignore
}
}
} catch (Exception ex) {
//ignore 异常类型 改大一点
}
return "www";
}
}
5.8.2.4 新增博文
请求方式 | 请求地址 | 请求头 |
---|---|---|
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://img-blog.csdnimg.cn/img_convert/9a98336c43a0605a50ef539e6558f380.png)\n正文",
"tags":[
1,
4
],
"categoryId":1,
"summary":"哈哈",
"status":"1"
}
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现
5.8.3 Dto+自动填充
AddArticleDto
注意增加tags属性用于接收文章关联标签的id
Article 修改这样创建时间创建人修改时间修改人可以自动填充(需要mybatisplus配置文件,配置自动填充拦截器)
@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;
代码
package com.sangeng.handler.mybatisplus;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.sangeng.utils.SecurityUtils;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
//配置自动填充 拦截器
//insert操作时填充方法
@Override
public void insertFill(MetaObject metaObject) {
Long userId = null;
// try {
userId = SecurityUtils.getUserId(); //从token中拿到userid,自动填充
// } catch (Exception e) {
// e.printStackTrace();
// 注册的时候,不能获取当前userid,所以userid设置为-1
// userId = -1L;//表示是自己创建
// }
this.setFieldValByName("createTime", new Date(), metaObject);
this.setFieldValByName("createBy",userId , metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
this.setFieldValByName("updateBy", userId, metaObject);
}
//update操作时填充方法
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", new Date(), metaObject);
this.setFieldValByName(" ", SecurityUtils.getUserId(), metaObject);
}
}
5.9 导出所有分类到Excel
5.9.1 需求
分类管理 导出按钮 把所有分类 导出到 Excel文件中。
5.9.2 技术方案
使用EasyExcel实现Excel的导出操作。
https://github.com/alibaba/easyexcel
5.9.3 接口设计
请求方式 | 请求地址 | 请求头 |
---|---|---|
GET | /content/category/export | 需要token请求头 |
请求参数:
无
响应格式:
成功的话可以直接导出一个Excel文件
失败的话响应格式如下:
{
"code":500,
"msg":"出现错误"
}
5.9.4 代码实现
工具类方法修改
WebUtils
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);
}
CategoryController
@GetMapping("/export")
public void export(HttpServletResponse response){
//设置下载文件的请求头
try {
WebUtils.setDownLoadHeader("分类.xlsx",response); //设置 响应头,excel的名字
//获取需要导出的数据 查询到所有的分类数据
List<Category> categoryVos = categoryService.list();
List<ExcelCategoryVo> excelCategoryVos = BeanCopyUtils.copyBeanList(categoryVos, ExcelCategoryVo.class);
//把数据写入都excel中 write(输出流,封装实体类对象)
EasyExcel.write(response.getOutputStream(), ExcelCategoryVo.class).autoCloseStream(Boolean.FALSE).sheet("sheet_分类导出")
.doWrite(excelCategoryVos); // 要导出的数据excelCategoryVos, 写到excel中
} catch (Exception e) {
//如果出现异常 也要相应json
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR);
WebUtils.renderString(response, JSON.toJSONString(result)); // 把json写入到响应当中
}
}
ExcelCategoryVo
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ExcelCategoryVo {
@ExcelProperty("分类名")
private String name;
//描述
@ExcelProperty("描述")
private String description;
//状态0:正常,1禁用
@ExcelProperty("状态0:正常,1禁用")
private String status;
}
5.10 权限控制
5.10.1 需求
对导出分类的接口 做权限控制。
5.10.2 代码实现
SecurityConfig
@EnableGlobalMethodSecurity(prePostEnabled = true)
UserDetailsServiceImpl
package com.sangeng.service.impl;
import ...
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//security会使用UserDetailsService实现类中的loadUserByUsername方法进行校验
//根据用户名查询用户信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(queryWrapper);
//判断是否查到用户 如果没查到抛出异常
if (Objects.isNull(user)){
throw new RuntimeException("用户不存在"); }
//返回用户信息 返回的是UserDatails对象,后面才做密码校验 密码SpringSecurity自动校验
//TODO 查询权限信息封装 如果是后台用户才需要查询权限封装 (前台用户不需要查询权限)
if (user.getType().equals(SystemConstants.ADMIN)){
List<String> perms = menuMapper.selectPermsByUserId(user.getId()); //权限列表
return new LoginUser(user,perms);
}
// 定义一个loginUser 实现 UserDetails,即可返回
return new LoginUser(user,null); //UserDetails对象(权限信息 + 用户信息)
}
}
LoginUser
增加属性 private List permissions;
import ...
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
// 该类 存储用户信息 + 权限信息
private User user; //user封装成 LoginUser的成员变量,存储用户信息
//(后台管理员)用户权限
private List<String> permission;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//返回一个用户的 权限集合 springsecurity是通过这个方法 获取权限 (可以将permission转换成 该方法类型并返回)
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;
}
}
PermissionService
hasPermisson
@Service("ps") //对应自定义权限中定义的方法
// @PreAuthorize("@ps.hasPermission('content:category:export')") @GetMapping("/export")
public class PermissionService {
// 定义注解hasPermission
/**
* 判断当前用户是否具有permission (权限)
* @param permission 要判断的权限
* @return 是 否 具有权限
*/
public boolean hasPermission(String permission){
//如果是超级管理员,直接返回true
if (SecurityUtils.isAdmin()){
return true;
}
//否则 获取当前登录用户所具有的权限列表,然后 判断是否具有permission
List<String> perms = SecurityUtils.getLoginUser().getPermission();
return perms.contains(permission);
}
}
CategoryController
@PreAuthorize("@ps.hasPermission('content:category:export')") //自定义权限,看登录的用户是否有 这个权限,有才能访问该请求。 要实现@Service("ps") public class PermissionService {}
@GetMapping("/export")
public void export(HttpServletResponse response){
//设置下载文件的请求头
try {
WebUtils.setDownLoadHeader("分类.xlsx",response); //设置 响应头,excel的名字
//获取需要导出的数据 查询到所有的分类数据
List<Category> categoryVos = categoryService.list();
List<ExcelCategoryVo> excelCategoryVos = BeanCopyUtils.copyBeanList(categoryVos, ExcelCategoryVo.class);
//把数据写入都excel中 write(输出流,封装实体类对象)
EasyExcel.write(response.getOutputStream(), ExcelCategoryVo.class).autoCloseStream(Boolean.FALSE).sheet("sheet_分类导出")
.doWrite(excelCategoryVos); // 要导出的数据excelCategoryVos, 写到excel中
} catch (Exception e) {
//如果出现异常 也要相应json
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR);
WebUtils.renderString(response, JSON.toJSONString(result)); // 把json写入到响应当中
}
}
5.11 文章列表
5.10.1 需求
为了对文章进行管理,需要提供文章列表,
在后台需要分页查询文章功能,要求能根据标题和摘要模糊查询。
注意:不能把删除了的文章查询出来
5.10.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | /content/article/list | 是 |
Query格式请求参数:
pageNum: 页码
pageSize: 每页条数
title:文章标题
summary:文章摘要
响应格式:
{
"code":200,
"data":{
"rows":[
{
"categoryId":"1",
"content":"嘻嘻嘻嘻嘻嘻",
"createTime":"2022-01-24 07:20:11",
"id":"1",
"isComment":"0",
"isTop":"1",
"status":"0",
"summary":"SpringSecurity框架教程-Spring Security+JWT实现项目级前端分离认证授权",
"thumbnail":"https://sg-blog-oss.oss-cn-beijing.aliyuncs.com/2022/01/31/948597e164614902ab1662ba8452e106.png",
"title":"SpringSecurity从入门到精通",
"viewCount":"161"
}
],
"total":"1"
},
"msg":"操作成功"
}
实现
5.12 修改文章
5.12.1 需求
点击文章列表中的修改按钮可以跳转到写博文页面。回显示该文章的具体信息。
用户可以在该页面修改文章信息。点击更新按钮后修改文章。
5.12.2 分析
首先根据文章id查询文章的详细信息 实现文章的回显。
5.12.3 接口设计
5.12.3.1 查询文章详情接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | content/article/{id} | 是 |
Path格式请求参数:
id: 文章id
响应格式:
{
"code":200,
"data":{
"categoryId":"1",
"content":"xxxxxxx",
"createBy":"1",
"createTime":"2022-08-28 15:15:46",
"delFlag":0,
"id":"10",
"isComment":"0",
"isTop":"1",
"status":"0",
"summary":"啊实打实",
"tags":[
"1",
"4",
"5"
],
"thumbnail":"https://sg-blog-oss.oss-cn-beijing.aliyuncs.com/2022/08/28/7659aac2b74247fe8ebd9e054b916dbf.png",
"title":"委屈饿驱蚊器",
"updateBy":"1",
"updateTime":"2022-08-28 15:15:46",
"viewCount":"0"
},
"msg":"操作成功"
}
实现
5.12.3.2 更新文章接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
PUT | content/article | 是 |
请求体参数格式:
{
"categoryId":"1",
"content":"![Snipaste_20220228_224837.png](https://img-blog.csdnimg.cn/img_convert/8ce7b01011cc38b1b3d9f6a3597ccb7d.png)\n\n# 十大\n## 时代的",
"createBy":"1",
"createTime":"2022-08-28 15:15:46",
"delFlag":0,
"id":"10",
"isComment":"0",
"isTop":"1",
"status":"0",
"summary":"啊实打实2",
"tags":[
"1",
"4",
"5"
],
"thumbnail":"https://sg-blog-oss.oss-cn-beijing.aliyuncs.com/2022/08/28/7659aac2b74247fe8ebd9e054b916dbf.png",
"title":"委屈饿驱蚊器",
"updateBy":"1",
"updateTime":"2022-08-28 15:15:46",
"viewCount":"0"
}
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现
5.13 删除文章
5.13.1 需求
点击文章后面的删除按钮可以删除该文章
注意:是逻辑删除不是物理删除
5.13.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
DELETE | content/article/{id} | 是 |
Path请求参数:
id:要删除的文章id
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现
5.14 菜单列表
5.14.1 需求
需要展示菜单列表,不需要分页。
可以针对菜单名进行模糊查询
也可以针对菜单的状态进行查询。
菜单要按照父菜单id和orderNum进行排序
5.14.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
GET | system/menu/list | 是 |
Query请求参数:
status : 状态
menuName: 菜单名
响应格式:
{
"code":200,
"data":[
{
"component":"content/article/write/index",
"icon":"build",
"id":"2023",
"isFrame":1,
"menuName":"写博文",
"menuType":"C",
"orderNum":0,
"parentId":"0",
"path":"write",
"perms":"content:article:writer",
"remark":"",
"status":"0",
"visible":"0"
},
...
{
"icon":"#",
"id":"2026",
"isFrame":1,
"menuName":"友链删除",
"menuType":"F",
"orderNum":1,
"parentId":"2022",
"path":"",
"perms":"content:link:remove",
"remark":"",
"status":"0",
"visible":"0"
}
],
"msg":"操作成功"
}
实现
5.15 新增菜单
5.15.1 需求
新增菜单
5.15.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
POST | content/menu | 是 |
请求体参数:
Menu类对应的json格式
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现
5.16 修改菜单
5.16.1 需求
能够修改菜单,但是修改的时候不能把父菜单设置为当前菜单,如果设置了需要给出相应的提示。并且修改失败。
5.16.2 接口设计
5.16.2.1 根据id查询菜单数据
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | system/menu/{id} | 是 |
Path格式请求参数:
id: 菜单id
响应格式:
{
"code":200,
"data":{
"icon":"table",
"id":"2017",
"menuName":"内容管理",
"menuType":"M",
"orderNum":"4",
"parentId":"0",
"path":"content",
"remark":"",
"status":"0",
"visible":"0"
},
"msg":"操作成功"
}
实现
5.16.2.2 更新菜单
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
PUT | system/menu | 是 |
请求体参数:
Menu类对应的json格式
响应格式:
{
"code":200,
"msg":"操作成功"
}
如果把父菜单设置为当前菜单:
{
"code":500,
"msg":"修改菜单'写博文'失败,上级菜单不能选择自己"
}
实现
5.17 删除菜单
5.17.1 需求
能够删除菜单,但是如果要删除的菜单有子菜单则提示:存在子菜单不允许删除 并且删除失败。
5.17.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
DELETE | content/article/{menuId} | 是 |
Path参数:
menuId:要删除菜单的id
响应格式:
{
"code":200,
"msg":"操作成功"
}
如果要删除的菜单有子菜单则
{
"code":500,
"msg":"存在子菜单不允许删除"
}
实现
5.18 角色列表
5.18.1 需求
需要有角色列表分页查询的功能。
要求能够针对角色名称进行模糊查询。
要求能够针对状态进行查询。
要求按照role_sort进行升序排列。
5.18.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
GET | system/role/list | 是 |
Query格式请求参数:
pageNum: 页码
pageSize: 每页条数
roleName:角色名称
status:状态
响应格式:
{
"code":200,
"data":{
"rows":[
{
"id":"12",
"roleKey":"link",
"roleName":"友链审核员",
"roleSort":"1",
"status":"0"
}
],
"total":"1"
},
"msg":"操作成功"
}
实现
5.19 改变角色状态
5.19.1 需求
修改角色的停启用状态
5.19.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
PUT | system/role/changeStatus | 是 |
请求体:
{
"roleId":"11","status":"1"}
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现
5.20 新增角色!!
5.20.1 需求
新增角色时能够直接设置角色所关联的菜单权限。
5.20.2 接口设计
5.20.2.1 获取菜单树接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
GET | /system/menu/treeselect | 是 |
无需请求参数
响应格式:
{
"code":200,
"data":[
{
"children":[],
"id":"2023",
"label":"写博文",
"parentId":"0"
},
{
"children":[
{
"children":[
{
"children":[],
"id":"1001",
"label":"用户查询",
"parentId":"100"
},
...
]
],
"id":"2017",
"label":"内容管理",
"parentId":"0"
}
],
"msg":"操作成功"
}
实现: 三级子菜单功能 + 二级子菜单功能
ServiceImpl 三级子菜单功能
// 增加用户角色时,展示菜单的三级目录
@Override
public ResponseResult treeSelect() {
//1. Menu表中的所有数据 转成MenuTreeVo集合 并设置(id,label,parentId)
// Menu -> MenuTreeVo (id,label,parentId,children=null)
List<MenuTreeVo> menuTreeVo = list().stream()
.map(menu -> new MenuTreeVo(menu.getId(), menu.getMenuName(), menu.getParentId(), null))
.collect(Collectors.toList());
//2. 根据parent_id(参数2) 将列表menuTreeVo(参数1)建立成 3级菜单树 函数名builderMenuTree2
List<MenuTreeVo> menuTreeVo1 = builderMenuTree2(menuTreeVo,0L);
return ResponseResult.okResult(menuTreeVo1);
}
private List<MenuTreeVo> builderMenuTree2(List<MenuTreeVo> menuTreeVo, long parentId) {
//第一层
List<MenuTreeVo> collect = menuTreeVo.stream()
.filter(m -> m.getParentId().equals(parentId)) //过滤出 父菜单
//getChildren2(menuTreeVo, m1) 根据m1 在 menuTreeVo列表中 循环找子菜单(m1表示3个父菜单)
.map(m1 -> m1.setChildren(getChildren2(menuTreeVo, m1))) //第2,3层——循环设置children,
.collect(Collectors.toList());
return collect;
}
private List<MenuTreeVo> getChildren2(List<MenuTreeVo> menuTreeVo, MenuTreeVo m1) {
//getChildren2(menuTreeVo, m1) 根据m1(m1表示3个父菜单) 在 menuTreeVo列表中 循环找子菜单
List<MenuTreeVo> collect2 = menuTreeVo.stream()
.filter(m2 -> m2.getParentId().equals(m1.getId())) //找1个父菜单的子菜单
.map(m3 -> m3.setChildren(getChildren2(menuTreeVo, m3))) //第2,3层递归设置children,
.collect(Collectors.toList());
return collect2;
}
ServiceImpl 二级子菜单功能
// 增加用户角色时,展示菜单的 二级目录
@Override
public ResponseResult treeSelect() {
//1.找出parentId=0的根菜单 存到列表中
LambdaQueryWrapper<Menu> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Menu::getParentId, 0);
Page<Menu> page = new Page<>();
page(page, queryWrapper); //根据queryWrapper查询
//2 转成MenuTreeVo集合 Menu -> MenuTreeVo (children,id,label,parentId)
//2.1 Menu -> MenuTreeVo (id,label,parentId)
List<MenuTreeVo> menuTreeVos = toMenuTreeVo(page.getRecords());
//2.2 查询所有根菜单的子菜单 for循环 找出各个children ,设置到children属性
// 解决(children)
//查询所有根菜单对应的子菜单集合,并且赋值给对应属性
//从parentId != 0 (表示子菜单) 开始查,因为 parentId==0是根菜单,根菜单不可能是 某个菜单的子菜单
for (MenuTreeVo menuTreeVo : menuTreeVos) {
//从根菜单中找 子菜单
List<MenuTreeVo> children = getTreeChildren(menuTreeVo.getId());
// if (treeVos.size() != 0){ //当有子菜单才设置子菜单,否则不设置(不然的话,子菜单为空,将子菜单设置成了一个空地址)
menuTreeVo.setChildren(children);
// }
}
return ResponseResult.okResult(menuTreeVos);
}
private List<MenuTreeVo> getTreeChildren(Long id) {
//菜单的id 等于 根菜单id,则该条菜单 为子菜单
LambdaQueryWrapper<Menu> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Menu::getParentId,id); //Menu表中parentId = id的数据
queryWrapper.orderByAsc(Menu::getId); //根据id升序
List<Menu> menus = list(queryWrapper);
// 转成MenuTreeVo集合
List<MenuTreeVo> menuTreeVos = toMenuTreeVo(menus);
return menuTreeVos;
}
private List<MenuTreeVo> toMenuTreeVo(List<Menu> lists) {
// Menu -> MenuTreeVo (id,label,parentId)
// 解决(id,parentId)
List<MenuTreeVo> menuTreeVos = BeanCopyUtils.copyBeanList(lists, MenuTreeVo.class);
// 解决(children,label)
for (MenuTreeVo menuTreeVo : menuTreeVos) {
//遍历上面的 copyBeanList的列表
// 解决(label)
if (menuTreeVo.getId()!=0){
//如果Id !=0 才查询 (Id = 0 表示根菜单,根据0查用户,会查到null)
Menu menu = menuService.getById(menuTreeVo.getId()); //查到对应的menu
menuTreeVo.setLabel(menu.getMenuName());
}
}
return menuTreeVos;
}
5.20.2.2 新增角色接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
POST | system/role | 是 |
请求体:
{
"roleName":"测试新增角色",
"roleKey":"wds",
"roleSort":0,
"status":"0",
"menuIds":[
"1",
"100"
],
"remark":"我是角色备注"
}
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现
5.21 修改角色
5.21.1 需求
修改角色时可以修改角色所关联的菜单权限
5.21.2 接口设计
5.21.2.1 角色信息回显接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | system/role/{id} | 是 |
Path格式请求参数:
id: 角色id
响应格式:
{
"code":200,
"data":{
"id":"11",
"remark":"嘎嘎嘎",
"roleKey":"aggag",
"roleName":"嘎嘎嘎",
"roleSort":"5",
"status":"0"
},
"msg":"操作成功"
}
实现
5.21.2.2 加载对应角色菜单列表树接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | /system/menu/roleMenuTreeselect/{id} | 是 |
Path格式请求参数:
id: 角色id
响应格式:
字段介绍
menus:菜单树。
checkedKeys:角色所关联的菜单权限id列表。
{
"code":200,
"data":{
"menus":[
{
"children":[],
"id":"2023",
"label":"写博文",
"parentId":"0"
},
{
"children":[
{
"children":[],
"id":"2019",
"label":"文章管理",
"parentId":"2017"
},
{
"children":[
{
"children":[],
"id":"2028",
"label":"导出分类",
"parentId":"2018"
}
],
"id":"2018",
"label":"分类管理",
"parentId":"2017"
},
{
"children":[
{
"children":[],
"id":"2024",
"label":"友链新增",
"parentId":"2022"
},
{
"children":[],
"id":"2025",
"label":"友链修改",
"parentId":"2022"
},
{
"children":[],
"id":"2026",
"label":"友链删除",
"parentId":"2022"
},
{
"children":[],
"id":"2027",
"label":"友链查询",
"parentId":"2022"
}
],
"id":"2022",
"label":"友链管理",
"parentId":"2017"
},
{
"children":[],
"id":"2021",
"label":"标签管理",
"parentId":"2017"
}
],
"id":"2017",
"label":"内容管理",
"parentId":"0"
}
],
"checkedKeys":[
"1001"
]
},
"msg":"操作成功"
}
实现
RoleMenuServiceImpl
@Autowired
private RoleMenuService roleMenuService;
@Override
public ResponseResult roleMenuTreeselectById(Long id) {
//参数id是 角色id
RoleMenuTreeVo roleMenuTreeVo = new RoleMenuTreeVo();
//1.设置menus 把3级菜单展示出来
// Menu -> MenuTreeVo集合
List<MenuTreeVo> menuTreeVo = list().stream()
.map(menu -> new MenuTreeVo(menu.getId(), menu.getMenuName(), menu.getParentId(), null))
.collect(Collectors.toList());
//根据parent_id(参数2) 将列表menuTreeVo(参数1)建立成 3级菜单树 函数名builderMenuTree2
List<MenuTreeVo> menus = builderMenuTree2(menuTreeVo,0);
roleMenuTreeVo.setMenus(menus);
//2.设置checkedKeys(3级菜单中 用户id所具有的权限啊展示出来) 用户角色所关联的 菜单id
LambdaQueryWrapper<RoleMenu> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(RoleMenu::getRoleId,id);
List<RoleMenu> roleMenus = roleMenuService.list(queryWrapper);
List<String> menuIds = roleMenus.stream()
.map(roleMenu -> roleMenu.getMenuId().toString())
.collect(Collectors.toList());
roleMenuTreeVo.setCheckedKeys(menuIds);
return ResponseResult.okResult(roleMenuTreeVo);
}
private List<MenuTreeVo> builderMenuTree2(List<MenuTreeVo> menuTreeVo, long parentId) {
//第一层
List<MenuTreeVo> collect = menuTreeVo.stream()
.filter(m -> m.getParentId().equals(parentId)) //过滤出 父菜单
//getChildren2(menuTreeVo, m1) 根据m1 在 menuTreeVo列表中 循环找子菜单(m1表示3个父菜单)
.map(m1 -> m1.setChildren(getChildren2(menuTreeVo, m1))) //第2,3层——循环设置children,
.collect(Collectors.toList());
return collect;
}
private List<MenuTreeVo> getChildren2(List<MenuTreeVo> menuTreeVo, MenuTreeVo m1) {
//getChildren2(menuTreeVo, m1) 根据m1(m1表示3个父菜单) 在 menuTreeVo列表中 循环找子菜单
List<MenuTreeVo> collect2 = menuTreeVo.stream()
.filter(m2 -> m2.getParentId().equals(m1.getId())) //找1个父菜单的子菜单
.map(m3 -> m3.setChildren(getChildren2(menuTreeVo, m3))) //第2,3层递归设置children,
.collect(Collectors.toList());
return collect2;
}
5.21.2.3 更新角色信息接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
PUT | system/role | 是 |
请求体:
{
"id":"13",
"remark":"我是角色备注",
"roleKey":"wds",
"roleName":"测试新增角色",
"roleSort":0,
"status":"0",
"menuIds":[
"1",
"100",
"1001"
]
}
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现
5.22 删除角色
5.22.1 需求
删除固定的某个角色(逻辑删除)
5.22.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
DELETE | system/role/{id} | 是 |
Path请求参数:
id:要删除的角色id
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现:直接调用IService中的removeById()方法
5.23 用户列表
5.23.1 需求
需要用户分页列表接口。
可以根据用户名模糊搜索。
可以进行手机号的搜索。
可以进行状态的查询。
5.23.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
GET | system/user/list | 是 |
Query格式请求参数:
pageNum: 页码
pageSize: 每页条数
userName:用户名
phonenumber:手机号
status:状态
响应格式:
{
"code":200,
"data":{
"rows":[
{
"avatar":"http://r7yxkqloa.bkt.clouddn.com/2022/03/05/75fd15587811443a9a9a771f24da458d.png",
"createTime":"2022-01-05 17:01:56",
"email":"[email protected]",
"id":"1",
"nickName":"sg3334",
"phonenumber":"18888888888",
"sex":"1",
"status":"0",
"updateBy":"1",
"updateTime":"2022-03-13 21:36:22",
"userName":"sg"
}
],
"total":"1"
},
"msg":"操作成功"
}
实现
5.24 新增用户!!!
5.24.1 需求
需要新增用户功能。新增用户时可以直接关联角色。
注意:新增用户时注意密码加密存储。
用户名不能为空,否则提示:必需填写用户名
用户名必须之前未存在,否则提示:用户名已存在
手机号必须之前未存在,否则提示:手机号已存在
邮箱必须之前未存在,否则提示:邮箱已存在
5.24.2 接口设计
5.24.2.1 查询角色列表接口
注意:查询的是所有状态正常的角色
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
GET | /system/role/listAllRole | 是 |
响应格式:
{
"code":200,
"data":[
{
"createBy":"0",
"createTime":"2021-11-12 18:46:19",
"delFlag":"0",
"id":"2",
"remark":"普通角色",
"roleKey":"common",
"roleName":"普通角色",
"roleSort":"2",
"status":"0",
"updateBy":"0",
"updateTime":"2022-01-02 06:32:58"
},
{
"createTime":"2022-01-06 22:07:40",
"delFlag":"0",
"id":"11",
"remark":"嘎嘎嘎",
"roleKey":"aggag",
"roleName":"嘎嘎嘎",
"roleSort":"5",
"status":"0",
"updateBy":"1",
"updateTime":"2022-09-12 10:00:25"
},
{
"createTime":"2022-01-16 14:49:30",
"delFlag":"0",
"id":"12",
"roleKey":"link",
"roleName":"友链审核员",
"roleSort":"1",
"status":"0",
"updateTime":"2022-01-16 16:05:09"
}
],
"msg":"操作成功"
}
实现
5.24.2.2 新增用户
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
POST | system/user | 是 |
请求体:
{
"userName":"wqeree",
"nickName":"测试新增用户",
"password":"1234343",
"phonenumber":"18889778907",
"email":"[email protected]",
"sex":"0",
"status":"0",
"roleIds":[
"2"
]
}
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现
@Override
@Transactional
public ResponseResult addUser(UserVo2 userVo2) {
//校验
// 1.用户名不能为空,否则提示:必需填写用户名
if (Objects.isNull(userVo2.getUserName())){
throw new SystemException(AppHttpCodeEnum.USERNAME_NOT_NULL);
}
// 2.用户名必须之前未存在,否则提示:用户名已存在
// if (!userMapper.selectUserNameBoolean(userVo2.getUserName())){
// throw new SystemException(AppHttpCodeEnum.USERNAME_EXIST);
// }
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,userVo2.getUserName());
if (getOne(queryWrapper) != null){
throw new SystemException(AppHttpCodeEnum.USERNAME_EXIST);
}
// 3.手机号必须之前未存在,否则提示:手机号已存在
// if (!userMapper.selectPhoneBoolean(userVo2.getPhonenumber())){
// throw new SystemException(AppHttpCodeEnum.PHONENUMBER_EXIST)
// }
LambdaQueryWrapper<User> queryWrapper1 = new LambdaQueryWrapper<>();
queryWrapper1.eq(User::getPhonenumber,userVo2.getPhonenumber());
if (getOne(queryWrapper1) != null){
throw new SystemException(AppHttpCodeEnum.PHONENUMBER_EXIST);
}
// 4.邮箱必须之前未存在,否则提示:邮箱已存在
// if (!userMapper.selectEmailBoolean(userVo2.getEmail())){
// throw new SystemException(AppHttpCodeEnum.EMAIL_EXIST);
// }
LambdaQueryWrapper<User> queryWrapper2 = new LambdaQueryWrapper<>();
queryWrapper2.eq(User::getEmail,userVo2.getEmail());
if (getOne(queryWrapper2) != null){
throw new SystemException(AppHttpCodeEnum.EMAIL_EXIST);
}
//1.保存到 user表
User user = BeanCopyUtils.copyBean(userVo2, User.class);
// 5.新增用户时注意密码加密存储。
//对密码进行加密
String encodePassword = passwordEncoder.encode(user.getPassword());//对明文密码 进行加密,得到密文
user.setPassword(encodePassword); //将password加密后的密文,存到user中
save(user); //新增一条用户记录
//2.保存到user_role表
Long userId = user.getId();
List<UserRole> userRoleList = userVo2.getRoleIds().stream()
.map(roleId -> new UserRole(userId, Long.parseLong(roleId)))
.collect(Collectors.toList());
userRoleService.saveBatch(userRoleList);
return ResponseResult.okResult();
}
5.25 删除用户
5.25.1 需求
删除固定的某个用户(逻辑删除)
5.25.2 接口设计
不能删除当前操作的用户
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
DELETE | /system/user/{id} | 是 |
Path请求参数:
id:要删除的用户id
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现
controller层 事务
5.26 修改用户
5.26.1 需求
需要提供修改用户的功能。修改用户时可以修改用户所关联的角色。
5.26.2 接口设计
5.26.2.1 根据id查询用户信息回显接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | /system/user/{id} | 是 |
Path格式请求参数:
id: 用户id
响应格式:
roleIds:用户所关联的角色id列表
roles:所有角色的列表
user:用户信息
{
"code":200,
"data":{
"roleIds":[
"11"
],
"roles":[
{
"createBy":"0",
"createTime":"2021-11-12 18:46:19",
"delFlag":"0",
"id":"1",
"remark":"超级管理员",
"roleKey":"admin",
"roleName":"超级管理员",
"roleSort":"1",
"status":"0",
"updateBy":"0"
}
...
],
"user":{
"email":"[email protected]",
"id":"14787164048663",
"nickName":"sg777",
"sex":"0",
"status":"0",
"userName":"sg777"
}
},
"msg":"操作成功"
}
实现
5.26.2.2 更新用户信息接口
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
PUT | /system/user | 是 |
请求体:
{
"email":"[email protected]",
"id":"14787164048663",
"nickName":"sg777",
"sex":"1",
"status":"0",
"userName":"sg777",
"roleIds":[
"11"
]
}
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现
5.27 分页查询分类列表
5.27.1 需求
需要分页查询分类列表。
能根据分类名称进行模糊查询。
能根据状态进行查询。
5.27.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
GET | content/category/list | 是 |
Query格式请求参数:
pageNum: 页码
pageSize: 每页条数
name:分类名
status: 状态
响应格式:
{
"code":200,
"data":{
"rows":[
{
"description":"wsd",
"id":"1",
"name":"java",
"status":"0"
},
{
"description":"wsd",
"id":"2",
"name":"PHP",
"status":"0"
}
],
"total":"2"
},
"msg":"操作成功"
}
实现
5.28 新增分类
5.28.1 需求
需要新增分类功能
5.28.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
POST | /content/category | 是 |
请求体:
{
"name":"威威",
"description":"是的",
"status":"0"
}
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现
5.29 修改分类
5.29.1 需求
需要提供修改分类的功能
5.29.2 接口设计
5.29.2.1 根据id查询分类
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | content/category/{id} | 是 |
Path格式请求参数:
id: 分类id
响应格式:
{
"code":200,
"data":{
"description":"qwew",
"id":"4",
"name":"ww",
"status":"0"
},
"msg":"操作成功"
}
实现
5.29.2.2 更新分类
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
PUT | /content/category | 是 |
请求体:
{
"description":"是的",
"id":"3",
"name":"威威2",
"status":"0"
}
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现
5.30 删除分类
5.30.1 需求
删除某个分类(逻辑删除)
5.30.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
DELETE | /content/category/{id} | 是 |
Path请求参数:
id:要删除的分类id
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现
5.31 分页查询友链列表
5.31.1 需求
需要分页查询友链列表。
能根据友链名称进行模糊查询。
能根据状态进行查询。
5.31.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
GET | /content/link/list | 是 |
Query格式请求参数:
pageNum: 页码
pageSize: 每页条数
name:友链名
status:状态
响应格式:
{
"code":200,
"data":{
"rows":[
{
"address":"https://www.baidu.com",
"description":"sda",
"id":"1", "logo":"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fn1.itc.cn%2Fimg8%2Fwb%2Frecom%2F2016%2F05%2F10%2F146286696706220328.PNG&refer=http%3A%2F%2Fn1.itc.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1646205529&t=f942665181eb9b0685db7a6f59d59975",
"name":"sda",
"status":"0"
}
],
"total":"1"
},
"msg":"操作成功"
}
实现
5.32 新增友链
5.32.1 需求
需要新增友链功能
5.32.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
POST | /content/link | 是 |
请求体:
{
"name":"sda",
"description":"weqw",
"address":"wewe",
"logo":"weqe",
"status":"2"
}
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现
5.33 修改友链
5.33.1 需求
需要提供修改友链的功能
5.33.2 接口设计
5.33.2.1 根据id查询友联
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
Get | content/link/{id} | 是 |
Path格式请求参数:
id: 友链id
响应格式:
{
"code":200,
"data":{
"address":"wewe",
"description":"weqw",
"id":"4",
"logo":"weqe",
"name":"sda",
"status":"2"
},
"msg":"操作成功"
}
实现
5.33.2.2 修改友链
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
PUT | /content/link | 是 |
请求体:
{
"address":"https://www.qq.com",
"description":"dada2",
"id":"2",
"logo":"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fn1.itc.cn%2Fimg8%2Fwb%2Frecom%2F2016%2F05%2F10%2F146286696706220328.PNG&refer=http%3A%2F%2Fn1.itc.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1646205529&t=f942665181eb9b0685db7a6f59d59975",
"name":"sda",
"status":"0"
}
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现
5.34 删除友链
5.34.1 需求
删除某个友链(逻辑删除)
5.34.2 接口设计
请求方式 | 请求路径 | 是否需求token头 |
---|---|---|
DELETE | /content/link/{id} | 是 |
Path请求参数:
id:要删除的友链id
响应格式:
{
"code":200,
"msg":"操作成功"
}
实现
6.重点知识
6.1 工具类 BeanCopyUtils
package com.sangeng.utils;
import com.sangeng.domain.entity.Article;
import com.sangeng.domain.vo.HotArticleVo;
import org.springframework.beans.BeanUtils;
import java.util.List;
import java.util.stream.Collectors;
public class BeanCopyUtils {
// 构造方法设置为私有的方法
private BeanCopyUtils() {
}
// 单个实体类拷贝(将一个源对象 拷贝 至 字节码class)
// 通过反射创建目标对象,然后再拷贝
public static <V> V copyBean(Object source,Class<V> clazz) {
//创建目标对象 传过来什么类型,就返回什么类型,使用泛型( <V> V泛型方法,返回值类型 )
V result = null; //提升作用域
try {
result = clazz.newInstance();
//实现属性copy
BeanUtils.copyProperties(source, result);
} catch (Exception e) {
throw new RuntimeException(e);
}
//返回结果
return result;
}
// 集合拷贝
// (后面要用)声明泛型O,V 返回类型 public <T> void say(){} 表明是泛型方法
public static <O,V> List<V> copyBeanList(List<O> list, Class<V> clazz){
// 先将list集合 转成流->流当中元素的转换(转换方式copyBean方法) 返回一个泛型V -> 收集操作,泛型转成list
return list.stream()
.map(o -> copyBean(o, clazz))
.collect(Collectors.toList());
}
}
单个实体类拷贝
//封装成User
User user = BeanCopyUtils.copyBean(userVo2, User.class);//拷贝成 类型User.class
集合拷贝
拷贝整个list集合,集合中的每一个元素 按照单个实体类拷贝。
将page.getRecords()查到的集合 拷贝 成 List集合
List<UserVo> userVos = BeanCopyUtils.copyBeanList(page.getRecords(), UserVo.class);
6.2 mybatisplus 解决 主键id雪花算法生成方式,改成主键id自增方式
使用mybatisplus时,当实体类entity Article
主键id,
如果*不用@TableId标注 主键id *,则插入数据时,默认使用雪花算法生成主键id,即使数据库设置了 主键自增
解决雪花算法问题,使用 自增主键id
6.2.1 数据库表
6.2.2 局部设置 主键id自增方式
6.2.2 全局设置 主键id自增方式
6.3 事务 @Transactional
在方法前面 加个注解@Transactional 即可实现aop事务
6.4 mybatisplus 分页查询
mybatis-plus分页查询(springboot中实现单表和多表查询)
6.5 mybatisplus 自动 插入时间
插入数据时,自动插入 创建时间,更新时间,创建人,更新人。
6.6 ResponseResult (响应类) 和 AppHttpCodeEnum (响应枚举类)
ResponseResult
AppHttpCodeEnum
ResponseResult
package com.sangeng.domain;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.sangeng.enums.AppHttpCodeEnum;
import java.io.Serializable;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> implements Serializable {
// ResponseResult<T> 作用: <T>指定ResponseResult类中 字段data的数据类型 为泛型
private Integer code; //编码
private String msg; //message
private T data; // 泛型, 传入data什么类型,data就是什么类型
// 返回 { 200; "操作成功"; } AppHttpCodeEnum枚举类中 SUCCESS(200,"操作成功"),
public ResponseResult() {
//如果什么参数都不传,则返回 { 200; "操作成功"; }
this.code = AppHttpCodeEnum.SUCCESS.getCode();
this.msg = AppHttpCodeEnum.SUCCESS.getMsg();
}
// 返回 { code; data; }
public ResponseResult(Integer code, T data) {
this.code = code;
this.data = data;
}
// 返回 { code; msg; data;}
public ResponseResult(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
// 返回 { code; msg;}
public ResponseResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public static ResponseResult errorResult(int code, String msg) {
//errorResult 出错方法
ResponseResult result = new ResponseResult();
return result.error(code, msg);
}
public static ResponseResult okResult() {
//errorResult 正常方法
ResponseResult result = new ResponseResult();
return result;
}
public static ResponseResult okResult(int code, String msg) {
//errorResult 正常方法
ResponseResult result = new ResponseResult();
return result.ok(code, null, msg);
}
//errorResult 正常方法
public static ResponseResult okResult(Object data) {
//传入什么类型,则泛型T就为什么类型
ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getMsg());
if(data!=null) {
result.setData(data);
}
return result;
}
public static ResponseResult errorResult(AppHttpCodeEnum enums){
return setAppHttpCodeEnum(enums,enums.getMsg());
}
public static ResponseResult errorResult(AppHttpCodeEnum enums, String msg){
return setAppHttpCodeEnum(enums,msg);
}
public static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums){
return okResult(enums.getCode(),enums.getMsg());
}
private static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums, String msg){
return okResult(enums.getCode(),msg);
}
public ResponseResult<?> error(Integer code, String msg) {
this.code = code;
this.msg = msg;
return this;
}
public ResponseResult<?> ok(Integer code, T data) {
this.code = code;
this.data = data;
return this;
}
public ResponseResult<?> ok(Integer code, T data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
return this;
}
public ResponseResult<?> ok(T data) {
this.data = data;
return this;
}
// getter和 setter
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
AppHttpCodeEnum
- 枚举是一个被命名的整型常数的集合,用于声明一组带标识符的常数。
- 枚举是一个特殊的类,一般表示一组常量
package com.sangeng.enums;
//使用menu定义枚举类
public enum AppHttpCodeEnum {
//必须在枚举类的第一行声明枚举类对象,并且枚举类对象之间以“,”分隔,以“;”结尾。 必需声明枚举对象参数 的字段,并有构造方法
// 成功
SUCCESS(200,"操作成功"),
// 登录
NEED_LOGIN(401,"需要登录后操作"),
NO_OPERATOR_AUTH(403,"无权限操作"),
SYSTEM_ERROR(500,"出现错误"),
USERNAME_EXIST(501,"用户名已存在"),
PHONENUMBER_EXIST(502,"手机号已存在"),
EMAIL_EXIST(503, "邮箱已存在"),
REQUIRE_USERNAME(504, "必需填写用户名"),
LOGIN_ERROR(505,"用户名或密码错误"),
CONTENT_NOT_NULL(506,"评论内容不能为空" ),
FILE_TYPE_ERROR(507,"文件类型错误,请上传png或jpg文件" ),
USERNAME_NOT_NULL(508,"用户名不能为空" ),
NICKNAME_NOT_NULL(509,"昵称不能为空" ),
PASSWORD_NOT_NULL(510,"密码不能为空" ),
EMAIL_NOT_NULL(511,"邮箱不能为空" );
int code;
String msg;
AppHttpCodeEnum(int code, String errorMessage){
//构造方法(两个参数,上面声明枚举才能传两个参数)
this.code = code;
this.msg = errorMessage;
}
//由此处 以上 是枚举类必需的
public int getCode() {
//getter
return code;
}
public String getMsg() {
//getter
return msg;
}
}
- ResponseResult使用
controller
service接口
serviceImpl
- AppHttpCodeEnum使用
6.7 SystemConstants (常量类)
package com.sangeng.constants;
public class SystemConstants
{
/**
* 文章是草稿
*/
public static final int ARTICLE_STATUS_DRAFT = 1;
/**
* 文章是正常分布状态
*/
public static final int ARTICLE_STATUS_NORMAL = 0;
/**
* 分类是正常状态
*/
public static final String STATUS_NORMAL = "0";
/**
* 友链状态为审核通过 0
* ctrl shift U 大小写切换
*/
public static final String LINK_STATUS_NORMAL = "0";
/**
* 评论类型:文章评论 0
*/
public static final String ARTICLE_COMMENT = "0";
/**
* 评论类型:友链评论 1
*/
public static final String LINK_COMMENT = "1";
/**
* 菜单和按钮
*/
public static final String MENU = "C";
public static final String BUTTON = "F";
/**
* 1:表示后台管理员,0 表示前台用户
*/
public static final String ADMIN = "1";
}
- SystemConstants 常量类 使用
6.8 Exception
- 异常类 SystemException
- 异常处理类 GlobalExceptionHandler
异常类 SystemException
package com.sangeng.exception;
import com.sangeng.enums.AppHttpCodeEnum;
public class SystemException extends RuntimeException{
private int code;
private String msg;
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
public SystemException(AppHttpCodeEnum httpCodeEnum) {
//通过枚举类 得到 其中的的常量 填充code,msg
super(httpCodeEnum.getMsg()); //调用父类的super方法,必须写
this.code = httpCodeEnum.getCode();
this.msg = httpCodeEnum.getMsg();
}
// throw new SystemException(507,"必须填写用户名") 抛出异常是传入参数
public SystemException(String message, int code, String msg) {
super(message); //调用父类的super方法,必须写
this.code = code;
this.msg = msg;
}
}
Exception handling class GlobalExceptionHandler
(handles the above exception class SystemException, such as: exception information, exception coding)
package com.sangeng.handler.exception;
import com.sangeng.domain.ResponseResult;
import com.sangeng.enums.AppHttpCodeEnum;
import com.sangeng.exception.SystemException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
//@ControllerAdvice //controller中出现了异常,会在这里统一处理
//@ResponseBody //将处理方法的返回值 放到 响应体中
@Slf4j //日志
@RestControllerAdvice //@RestControllerAdvice = @ControllerAdvice + @ResponseBody
//处理方法的返回值 都会转换成json放到响应体中
public class GlobalExceptionHandler {
@ExceptionHandler(SystemException.class) //处理SystemException异常
public ResponseResult systemExceptionHandler(SystemException e){
//打印异常信息
log.error("出现了异常!{}",e);
//从异常对象中获取提示信息封装返回,(封装到响应体中,因为controller请求方法中 返回类型是ResponseResult )
return ResponseResult.errorResult(e.getCode(), e.getMsg()); //前端返回json对象 { "code":504, "msg":"必需填写用户名" }
}
@ExceptionHandler(Exception.class) //处理Exception异常
public ResponseResult exceptionHandler(Exception e){
//打印异常信息
log.error("出现了异常!{}",e);
//从异常对象中获取提示信息封装返回
return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR.getCode(),e.getMessage());
}
}
Usage example
1. Use enumeration class to obtain exception information
USERNAME_NOT_NULL(508,"用户名不能为空" )
2. Throw an exception and manually pass in the exception information
if(!StringUtils.hasText(user.getUserName())){
throw new SystemException(508,"用户名不能为空" );
}
6.9 AOP implements logging
AOP - Log
aspect class LogAspect
6.10 Testing Objects.nonNull()
- Objects.nonNull(name) determines if name is not empty
- Objects.isNull(name) determines if name is empty