SpringBoot+vue 練手項目-- 個人博客系統


dSpringBoot+vue練手項目---博客系統

項目使用技術 :

springboot + mybatisplus+redis+mysql+jwt

項目講解:https://www.bilibili.com/video/BV1Gb4y1d7zb?p=1

1. 工程搭建

前端的工程地址:

鏈接:https://pan.baidu.com/s/1cg_11ctsbbq_WM9BnpcOaQ
提取碼:nrun

npm install
npm run build
npm run dev

1.1 新建maven工程

pom.xml(blog-parent)

<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.jihu</groupId>
    <artifactId>blog-parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>blog-api</module>
    </modules>
    <!--聲明pom代表他是一個父工程-->
    <packaging>pom</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
        <relativePath/>
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>
        
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.2</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/joda-time/joda-time -->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.10.10</version>
        </dependency>
    </dependencies>
</dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

pom.xml(blog-api)

<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>blog-parent</artifactId>
        <groupId>com.jihu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>blog-api</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <!-- 排除 默認使用的logback  -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- log4j2 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

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


        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.2</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/joda-time/joda-time -->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.10.10</version>
        </dependency>
    </dependencies>


</project>

1.2 application.yml

server:
  port: 8888

spring:
  application:
    name: jihu

#數據庫的配置
  datasource:
    url: jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

#mybatis-plus
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  #打印日志 如sql語句
  global-config:
    db-config:
      table-prefix: ms_  #標識表的前綴為ms_
#指定mapper文件的位置
mybatis-plus:
  config-location: classpath:mapper/*.xml

1.3 配置 分頁 和跨域

分頁

package com.jihu.blog.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
//掃包,將此包下的接口生成代理實現類,並且注冊到spring容器中
@MapperScan("com.jihu.blog.mapper")
public class MybatisPlusConfig {
    //分頁插件
    @Bean
    public  MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }

}

跨域

package com.jihu.blog.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
    //實現跨域請求
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET","HEAD","POST","PUT","DELETE","OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}

1.4啟動類

package com.jihu.blog;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BlogApp {
    public static void main(String[] args) {
        SpringApplication.run(BlogApp.class,args);
    }

}

2.首頁-文章列表

2.1 接口說明

接口url:/articles

請求方式:POST

請求參數:

參數名稱 參數類型 說明
page int 當前頁數
pageSize int 每頁顯示的數量

返回數據:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id": 1,
            "title": "springboot介紹以及入門案例",
            "summary": "通過Spring Boot實現的服務,只需要依靠一個Java類,把它打包成jar,並通過`java -jar`命令就可以運行起來。\r\n\r\n這一切相較於傳統Spring應用來說,已經變得非常的輕便、簡單。",
            "commentCounts": 2,
            "viewCounts": 54,
            "weight": 1,
            "createDate": "2609-06-26 15:58",
            "author": "12",
            "body": null,
            "tags": [
                {
                    "id": 5,
                    "avatar": null,
                    "tagName": "444"
                },
                {
                    "id": 7,
                    "avatar": null,
                    "tagName": "22"
                },
                {
                    "id": 8,
                    "avatar": null,
                    "tagName": "11"
                }
            ],
            "categorys": null
        }
    ]
}

2.2 編碼

Spring基於注解的開發
每個注解的作用

2.2.1 表結構

文章表

CREATE TABLE `blog`.`ms_article`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `comment_counts` int(0) NULL DEFAULT NULL COMMENT '評論數量',
  `create_date` bigint(0) NULL DEFAULT NULL COMMENT '創建時間',
  `summary` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '簡介',
  `title` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '標題',
  `view_counts` int(0) NULL DEFAULT NULL COMMENT '瀏覽數量',
  `weight` int(0) NOT NULL COMMENT '是否置頂',
  `author_id` bigint(0) NULL DEFAULT NULL COMMENT '作者id',
  `body_id` bigint(0) NULL DEFAULT NULL COMMENT '內容id',
  `category_id` int(0) NULL DEFAULT NULL COMMENT '類別id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

標簽表

id,文章id,標簽id,通過文章id可以間接查到標簽id

CREATE TABLE `blog`.`ms_tag`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `article_id` bigint(0) NOT NULL,
  `tag_id` bigint(0) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `article_id`(`article_id`) USING BTREE,
  INDEX `tag_id`(`tag_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

用戶表

CREATE TABLE `blog`.`ms_sys_user`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `account` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '賬號',
  `admin` bit(1) NULL DEFAULT NULL COMMENT '是否管理員',
  `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '頭像',
  `create_date` bigint(0) NULL DEFAULT NULL COMMENT '注冊時間',
  `deleted` bit(1) NULL DEFAULT NULL COMMENT '是否刪除',
  `email` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '郵箱',
  `last_login` bigint(0) NULL DEFAULT NULL COMMENT '最后登錄時間',
  `mobile_phone_number` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手機號',
  `nickname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵稱',
  `password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密碼',
  `salt` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '加密鹽',
  `status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '狀態',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

2.2.2 entity層

Article 文章實體類
package com.jihu.blog.dao.pojo;

import lombok.Data;

@Data
public class Article {
    public static final int Article_TOP = 1;

    public static final int Article_Common = 0;

    private Long id;

    private String title;

    private String summary;

    private int commentCounts;

    private int viewCounts;

    /**
     * 作者id
     */
    private Long authorId;
    /**
     * 內容id
     */
    private Long bodyId;
    /**
     *類別id
     */
    private Long categoryId;

    /**
     * 置頂
     */
    private int weight = Article_Common;


    /**
     * 創建時間
     */
    private Long createDate;
}

SysUser 用戶實體類
package com.jihu.blog.dao.pojo;

import lombok.Data;

@Data
public class SysUser {

    private Long id;

    private String account;

    private Integer admin;

    private String avatar;

    private Long createDate;

    private Integer deleted;

    private String email;

    private Long lastLogin;

    private String mobilePhoneNumber;

    private String nickname;

    private String password;

    private String salt;

    private String status;
}
Tag 標簽實體類
package com.jihu.blog.dao.pojo;

import lombok.Data;

@Data
public class Tag {

    private Long id;

    private String avatar;

    private String tagName;

}

2.2.3 Controller層

ArticleController
package com.jihu.blog.controller;

import com.jihu.blog.service.ArticleService;
import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.PageParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("articles")
public class ArticleController {

    @Autowired
    private ArticleService articleService;

    //首頁  文章列表
    @PostMapping
    public Result listArticle(@RequestBody PageParams pageParams){
        //ArticleVo 頁面接收的數據
        return articleService.listArticle(pageParams);

    }

}

2.2.4 Service層

ArticleService
package com.jihu.blog.service;

import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.PageParams;

public interface ArticleService {

    Result listArticle(PageParams pageParams);
}

ArticleServiceImpl
package com.jihu.blog.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jihu.blog.dao.mapper.ArticleMapper;
import com.jihu.blog.dao.mapper.TagMapper;
import com.jihu.blog.dao.pojo.Article;
import com.jihu.blog.service.ArticleService;
import com.jihu.blog.service.SysUserService;
import com.jihu.blog.service.TagService;
import com.jihu.blog.vo.ArticleVo;
import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.PageParams;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

@Service
public class ArticleServiceImpl implements ArticleService {

    @Autowired
    private ArticleMapper articleMapper;

    @Autowired
    private TagService tagService;

    @Autowired
    private SysUserService sysUserService;


    @Override
    public Result listArticle(PageParams pageParams) {

        Page<Article> page = new Page<>(pageParams.getPage(), pageParams.getPageSize());
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();

        //是否置頂進行排序
        queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);

        Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);
        List<Article> records = articlePage.getRecords();
        //能直接返回嗎  肯定不行  所以需要進行如下轉換
       List<ArticleVo> articleVoList = copyList(records,true,true);
        return Result.success(articleVoList);
    }

    private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor) {
        List<ArticleVo> articleVoList = new ArrayList<>();
        for (Article record : records) {
            articleVoList.add(copy(record,isTag,isAuthor));
        }
        return articleVoList;
    }

    private ArticleVo copy(Article article,boolean isTag,boolean isAuthor){
        ArticleVo articleVo = new ArticleVo();
        BeanUtils.copyProperties(article,articleVo);
        articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));

        //並不是所有的接口,都需要標簽,作者信息
        if (isTag){
            Long articleId = article.getId();
            articleVo.setTags(tagService.findTagsByrticleId(articleId));
        }
        if (isAuthor){
            Long authorId = article.getAuthorId();
            articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname());
        }
        
        return articleVo;
    }
  
}
SysUserService
package com.jihu.blog.service;
import com.jihu.blog.dao.pojo.SysUser;
public interface SysUserService {
    SysUser findUserById(Long id);
}

SysUserServiceImpl
package com.jihu.blog.service.impl;

import com.jihu.blog.dao.mapper.SysUserMapper;
import com.jihu.blog.dao.pojo.SysUser;
import com.jihu.blog.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class SysUserServiceImpl implements SysUserService {

    @Autowired
    private SysUserMapper sysUserMapper;

    @Override
    public SysUser findUserById(Long id) {
        SysUser sysUser = sysUserMapper.selectById(id);
        //防止空指針出現 加一個判斷
        if (sysUser == null){
            sysUser = new SysUser();
            sysUser.setNickname("馬神之路");
        }
        return sysUser;
    }
}

TagService
package com.jihu.blog.service;
import com.jihu.blog.vo.TagVo;
import java.util.List;
public interface TagService {
    List<TagVo> findTagsByrticleId(Long articleId);
}

TagServiceImpl
package com.jihu.blog.service.impl;

import com.jihu.blog.dao.mapper.TagMapper;
import com.jihu.blog.dao.pojo.Tag;
import com.jihu.blog.service.TagService;
import com.jihu.blog.vo.TagVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

@Service
public class TagServiceImpl implements TagService {

    @Autowired
    private TagMapper tagMapper;

    @Override
    public List<TagVo> findTagsByrticleId(Long articleId) {
        //mybatisplus  無法進行多表查詢
        List<Tag> tags = tagMapper.findTagsByrticleId(articleId);

        return copyList(tags);
    }

    private List<TagVo> copyList(List<Tag> tags) {
        List<TagVo> tagVoList = new ArrayList<>();
        for (Tag tag : tags) {
            tagVoList.add(copy(tag));
        }
        return  tagVoList;
    }

    private TagVo copy(Tag tag) {
        TagVo tagVo = new TagVo();
        BeanUtils.copyProperties(tag,tagVo);
        return  tagVo;
    }
}

2.2.5 Mapper層

ArticleMapper
package com.jihu.blog.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.pojo.Article;
//BaseMapper mybatisplus中提供的可以讓我們很方便的查詢這張表
public interface ArticleMapper  extends BaseMapper<Article> {

}
SysUserMapper
package com.jihu.blog.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.pojo.SysUser;
public interface SysUserMapper extends BaseMapper<SysUser> {
}
TagMapper
package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.pojo.Tag;

import java.util.List;

public interface TagMapper extends BaseMapper<Tag> {

    /**
     * 根據文章id 查詢標簽列表
     * @param articleId
     * @return
     */
    List<Tag> findTagsByrticleId(Long articleId);
}
         
TagMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!--MyBatis配置文件-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.jihu.blog.dao.mapper.TagMapper">

    <sql id="all">
        id,avatar,tag_name as tagName
    </sql>

<!--        List<Tag> findTagsByArticleId(Long articleId);
在這個文件中,id代表方法名,parameterType表示輸入變量的名字,resultType表示泛型的類型-->

    <select id="findTagsByrticleId" parameterType="long" resultType="com.jihu.blog.dao.pojo.Tag">
        select  id,avatar,tag_name as tagName from ms_tag
        where id in
        (select tag_id from ms_article_tag where article_id=#{articleId})
    </select>
</mapper>

2.2.6 Vo層

Result(統一最后的結果)
package com.jihu.blog.vo;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Result {
    private boolean success;
    private int code ;
    private String msg;
    private  Object data;

    public static Result success(Object data) {
        return new Result(true, 200, "success", data);
    }

    public static Result fail(int code,String msg) {
        return new Result(false, code, msg, null);
    }
}
ArticleVo 建立與前端交互的Vo文件
package com.jihu.blog.vo;

import lombok.Data;

import java.util.List;

//建立與前端交互的Vo文件
@Data
public class ArticleVo {
    private Long id;

    private String title;

    private String summary;

    private int commentCounts;

    private int viewCounts;

    private int weight;
    /**
     * 創建時間
     */
    private String createDate;

    private String author;

//    private ArticleBodyVo body;

    private List<TagVo> tags;

//    private List<CategoryVo> categorys;
}
新建TagVo
package com.jihu.blog.vo;

import lombok.Data;

@Data
public class TagVo {
    private Long id;
    private String tagName;
}
新建PageParams
package com.jihu.blog.vo.params;
import lombok.Data;
@Data
public class PageParams {
    private  int Page =1;  //當前頁數
    private  int PageSize =10;  //每頁顯示的數量
}	

2.2.7 測試:

image-20220404162338575

3.首頁- 最熱標簽

3.1接口說明

接口url:/tags/hot

請求方式:GET

請求參數:無

id: 標簽名稱 ,我們期望點擊標簽關於文章的所有列表都顯示出來

返回數據:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id":1,
            "tagName":"4444"
        }
    ]
}

3.2編碼

3.2.1Controller層

package com.jihu.blog.controller;

import com.jihu.blog.service.TagService;
import com.jihu.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("tags")
public class TagsController {

    @Autowired
    private TagService tagService;

    @GetMapping("hot")
    public Result hot(){
        int limit = 6;
        return tagService.hots(limit);
    }
}

3.2.2 Service層

建立service接口

TagService
package com.jihu.blog.service;
import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.TagVo;
import java.util.List;
public interface TagService {
    Result hots(int limit);
}

建立serviceimpl實現類

TagServiceImpl
@Service
public class TagServiceImpl implements TagService {

    @Autowired
    private TagMapper tagMapper;
    
	@Override
    public Result hots(int limit) {
        /*
          1.標簽所擁有的文章數量最多  即為最熱標簽
          2. 查詢  根據tag_id 分組  計數,從大到小  排列  取前 limit個
         */
        List<Long> tagIds = tagMapper.findHotsIds(limit);
        //判斷一下是否為空
        if (tagIds == null){
            return  Result.success(Collections.emptyList());
        }

        //需求的是  tagId 和 tagName  tag對象
        //需要的是這樣的一個sql語句  select * from tag where id in (1,2,3...)
        List<Tag> tagList = tagMapper.findTagdByTagIds(tagIds);

        return Result.success(tagList);
    }
}    

3.2.3 Mapper層

TagMapper
package com.jihu.blog.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.pojo.Tag;
import java.util.List;
public interface TagMapper extends BaseMapper<Tag> {

    /**
     * 查詢最熱的標簽 前limit條
     * @param limit
     * @return
     */
    List<Long> findHotsIds(int limit);
    /*
        根據最熱標簽查詢 最熱文章名字
     */
    List<Tag> findTagdByTagIds(List<Long> tagIds);
}
TagMapper
<?xml version="1.0" encoding="UTF-8" ?>
<!--MyBatis配置文件-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.jihu.blog.dao.mapper.TagMapper">

    <sql id="all">
        id,avatar,tag_name as tagName
    </sql>

    <select id="findHotsIds" parameterType="int" resultType="java.lang.Long">
        SELECT tag_id from ms_article_tag GROUP BY tag_id ORDER BY count(*) DESC limit #{limit}

    </select>

    <select id="findTagdByTagIds" parameterType="list" resultType="com.jihu.blog.dao.pojo.Tag">
        select id,tag_name as tagName  from ms_tag
        where id in
        <foreach collection="collection" item="tagId" separator="," open="("  close=")">
              #{tagId}
        </foreach>
    </select>
</mapper>

3.2.4 測試

image-20220404170243616

4.統一異常處理

不管是controller層還是service,dao層,都有可能報異常,如果是預料中的異常,可以直接捕獲處理,如果是意料之外的異常,需要統一進行處理,進行記錄,並給用戶提示相對比較友好的信息。

AllExceptionHandler

package com.jihu.blog.handler;

import com.jihu.blog.vo.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

//對加了 @Controller 注解方法進行攔截處理   AOP的實現
@ControllerAdvice
public class AllExceptionHandler {

    //進行異常處理,  處理Exception.class的異常
    @ExceptionHandler(Exception.class)
    @ResponseBody  //返回json數據
    public Result doException(Exception ex){
        ex.printStackTrace();
        return Result.fail(-999,"系統異常");
    }

}

image-20220404171112342

5.首頁-最熱文章

5.1 接口說明

接口url:/articles/hot

請求方式:POST

請求參數:無

返回數據:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id": 1,
            "title": "springboot介紹以及入門案例",
        },
        {
            "id": 9,
            "title": "Vue.js 是什么",
        },
        {
            "id": 10,
            "title": "Element相關",
            
        }
    ]
}

5.2 Controller層

ArticleController

@RestController
@RequestMapping("articles")
public class ArticleController {

    @Autowired
    private ArticleService articleService; 
//首頁  最熱文章
    @PostMapping("hot")
    public Result hotArticle(){
        int limit = 5; //取前5條

        return articleService.hotArticle(limit);

    }
}    

5.3 Service層

ArticleService

package com.jihu.blog.service;

import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.PageParams;

public interface ArticleService {

    Result listArticle(PageParams pageParams);

    Result hotArticle(int limit);
}

ArticleServiceImpl

@Service
public class ArticleServiceImpl implements ArticleService {

    @Autowired
    private ArticleMapper articleMapper;
/**
     * 最熱文章查詢
     * @param limit
     * @return
     */

    @Override
    public Result hotArticle(int limit) {
        LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.orderByDesc(Article::getViewCounts);
        lambdaQueryWrapper.select(Article::getId,Article::getTitle);
        lambdaQueryWrapper.last("limit "+ limit);
        //SELECT id, title from ms_article ORDER BY view_counts DESC limit 5
        List<Article> articles = articleMapper.selectList(lambdaQueryWrapper);

        return Result.success(copyList(articles,false,false));
    }
}    

5.4測試

image-20220404180253013

6.首頁-最新文章

和最熱文章非常類似,一個是根據瀏覽量來選擇,一個是根據最新創建時間來選擇

6.1 接口說明

接口url:/articles/new

請求方式:POST

請求參數:無

返回數據:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id": 1,
            "title": "springboot介紹以及入門案例",
        },
        {
            "id": 9,
            "title": "Vue.js 是什么",
        },
        {
            "id": 10,
            "title": "Element相關",
            
        }
    ]
}

6.2 Controller層

在com.jihu.blog.controller.ArticleController中添加

 //首頁  最新文章
    @PostMapping("new")
    public Result newArticle(){
        int limit = 5; //取前5條

        return articleService.newArticle(limit);

    }

6.3ArticleService

Result newArticle(int limit);

6.4ArticleServiceImpl

/**
 * 最新文章查詢
 * @param limit
 * @return
 */
@Override
public Result newArticle(int limit) {
    LambdaQueryWrapper<Article> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.orderByDesc(Article::getCreateDate);
    lambdaQueryWrapper.select(Article::getId,Article::getTitle);
    lambdaQueryWrapper.last("limit "+limit);
    //SELECT id, title from ms_article ORDER BY create_data DESC limit 5
    List<Article> articles = articleMapper.selectList(lambdaQueryWrapper);

    return Result.success(copyList(articles,false,false));
}

6.5測試

image-20220404181514531

7.首頁-文章歸檔

每一篇文章根據創建時間某年某月發表多少篇文章

7.1接口說明

接口url:/articles/listArchives

請求方式:POST

請求參數:無

返回數據:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "year": "2021",
            "month": "6",
            "count": 2
        }
            
    ]
}

7.2 Controller層

com.jihu.blog.controller.ArticleController

 //首頁  文章歸檔
    @PostMapping("listArchives")
    public Result listArchives(){
        return articleService.listArchives();
    }

7.3 ArticleService

Result listArchives();

7.4 ArticleServiceImpl

 //文章歸檔
    @Override
    public Result listArchives() {

       List<Archives>  archivesList  = articleMapper.listArchives();
        return  Result.success(archivesList);

    }

7.5 ArticleMapper

package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.dos.Archives;
import com.jihu.blog.dao.pojo.Article;
import com.jihu.blog.vo.Result;
import java.util.List;

//BaseMapper mybatisplus中提供的可以讓我們很方便的查詢這張表
public interface ArticleMapper  extends BaseMapper<Article> {

        List<Archives> listArchives();
}

7.6 ArticleMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!--MyBatis配置文件-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.jihu.blog.dao.mapper.ArticleMapper">
<!--
    select YEAR(create_date) as year,YEAR(create_date) as month ,count(*) as count from ms_article GROUP BY year,MONTH  這樣查詢不行
-->
    
<!--create_date 為bigint 13位,直接year()不行,需要先轉date型后year()。-->
<select id="listArchives"  resultType="com.jihu.blog.dao.dos.Archives" >
select year(FROM_UNIXTIME(create_date/1000)) year,month(FROM_UNIXTIME(create_date/1000)) month, count(*)
count from ms_article group by year,month;
</select>

</mapper>

7.7測試

image-20220404184134240

8.登錄

8.1 接口說明

接口url:/login

請求方式:POST

請求參數:

參數名稱 參數類型 說明
account string 賬號
password string 密碼

返回數據:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": "token"
}

8.2 JWT

登錄使用JWT技術。

jwt 可以生成 一個加密的token,做為用戶登錄的令牌,當用戶登錄成功之后,發放給客戶端。

請求需要登錄的資源或者接口的時候,將token攜帶,后端驗證token是否合法。

jwt 有三部分組成:A.B.C

A:Header,{“type”:“JWT”,“alg”:“HS256”} 固定

B:playload,存放信息,比如,用戶id,過期時間等等,可以被解密,不能存放敏感信息

C: 簽證,A和B加上秘鑰 加密而成,只要秘鑰不丟失,可以認為是安全的。

jwt 驗證,主要就是驗證C部分 是否合法。

導入依賴包

依賴包:

<dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
</dependency>

JWTUtils(工具類):

com.jihu.blog.utils.JWTUtils

package com.jihu.blog.utils;

import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JWTUtils {

    private static final String jwtToken = "123456Mszlu!@#$$";

    public static String createToken(Long userId){
        Map<String,Object> claims = new HashMap<>();
        claims.put("userId",userId);
        JwtBuilder jwtBuilder = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, jwtToken) // 簽發算法,秘鑰為jwtToken
                .setClaims(claims) // body數據,要唯一,自行設置
                .setIssuedAt(new Date()) // 設置簽發時間
                .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));// 一天的有效時間
        String token = jwtBuilder.compact();
        return token;
    }

    public static Map<String, Object> checkToken(String token){
        try {
            Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
            return (Map<String, Object>) parse.getBody();
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;

    }
    
    //測驗一下
    public static void main(String[] args) {
        String token = JWTUtils.createToken(100L);
        System.out.println(token);
        Map<String, Object> map = JWTUtils.checkToken(token);
        System.out.println(map.get("userId"));
    }
}

8.3 LoginController

package com.jihu.blog.controller;

import com.jihu.blog.service.LoginService;
import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.LoginParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("login")
public class LoginController {

    @Autowired
    private LoginService loginService;

    ////@RequestBody主要用來接收前端傳遞給后端的json字符串中的數據的(請求體中的數據的);
    // 而最常用的使用請求體傳參的無疑是POST請求了,所以使用@RequestBody接收數據時,一般都用POST方式進行提交。
    @PostMapping
    public Result Login(@RequestBody LoginParams loginParams){
        //登錄 驗證用戶   訪問用戶表
        return loginService.login(loginParams);
    }
}

8.4 LoginParam 登錄參數

構造LoginParam也就是我們的請求數據
com.jihu.blog.vo.params.LoginParams

package com.jihu.blog.vo.params;
import lombok.Data;
@Data
public class LoginParams {
    private String account;
    private String password;
}

8.5 LoginService

package com.jihu.blog.service;

import com.jihu.blog.dao.pojo.SysUser;
import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.LoginParams;

public interface LoginService {

    /**
     * 登錄功能
     * @param loginParams
     * @return
     */

    Result login(LoginParams loginParams);
}    

導入依賴包
md5加密的依賴包:

  <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
    </dependency>

8.6 LoginServiceImpl

// @Component – 指示自動掃描組件。
//@Repository – 表示在持久層DAO組件。
//@Service – 表示在業務層服務組件。
//@Controller – 表示在表示層控制器組件。
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private RedisTemplate<String,String> redisTemplate;
    //加密鹽用於加密
    private static final String salt = "mszlu!@#";

    @Override
    public Result login(LoginParams loginParams) {
        /**
         * 1.檢查參數是否合法
         * 2.根據用戶名和密碼去user表中查詢是否存在
         * 3.如果不存在   登錄失敗
         * 4. 如果存在  ,使用jwt生成token 返回給前端
         * 5.把token放入redis中,redis 存儲 token user這個信息,設置過期時間
         * (登錄認證時  先認證token字符串是否合法,再去redis認證是否存在)
         */

        String account = loginParams.getAccount();
        String password = loginParams.getPassword();
        if (StringUtils.isBlank(account) || StringUtils.isBlank(password)){
            return  Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
        }
        //把密碼加密一下
        String pwd = DigestUtils.md5Hex(password + salt);
        SysUser sysUser = sysUserService.findUser(account,pwd);
        if (sysUser == null){
            return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());
        }
        //登錄成功,使用JWT生成token,返回token和redis中
        String token = JWTUtils.createToken(sysUser.getId());
        //JSON.toJSONString :是把 sysUser對象轉換為對應的json字符串 (參考:https://blog.csdn.net/antony9118/article/details/71023009)
        //設置過期時間為1天
        redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS);

        return Result.success(token);
    }

    //生成我們想要的密碼,放於數據庫用於登陸
    public static void main(String[] args) {
        System.out.println(DigestUtils.md5Hex("admin"+salt));
    }
}

8.7 SysUserService

SysUser findUser(String account, String password);

8.8 SysUserServiceImpl

  @Override
    public SysUser findUser(String account, String password) {
        LambdaQueryWrapper<SysUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(SysUser::getAccount,account);
        lambdaQueryWrapper.eq(SysUser::getPassword,password);
        lambdaQueryWrapper.select(SysUser::getAccount,SysUser::getId,SysUser::getAvatar,SysUser::getNickname);
        lambdaQueryWrapper.last("limit 1"); //保證一下查詢效率   要不然可能還會一直往下查詢

        return sysUserMapper.selectOne(lambdaQueryWrapper);
    }

8.9 redis配置

#整合redis
spring.redis.host=192.168.56.130
spring.redis.port=6379

8.10 統一錯誤碼

com.jihu.blog.vo.ErrorCode

package com.jihu.blog.vo;

public enum  ErrorCode {

    PARAMS_ERROR(10001,"參數有誤"),
    ACCOUNT_PWD_NOT_EXIST(10002,"用戶名或密碼不存在"),
    TOKEN_ERROR(10003,"token不合法"),
    NO_PERMISSION(70001,"無訪問權限"),
    SESSION_TIME_OUT(90001,"會話超時"),
    NO_LOGIN(90002,"未登錄"),;

    private int code;
    private String msg;

    ErrorCode(int code, String msg){
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

}

8.11 測試

image-20220405103600243

9.獲取用戶信息

為什么實現完獲取用戶信息才能登陸測試呢?

token前端獲取到之后,會存儲在 storage中 h5 ,本地存儲,存儲好后,拿到storage中的token去獲取用戶信息,如果這個接口沒實現,他就會一直請求陷入死循環

9.1 接口說明

得從http的head里面拿到這個參數,這樣傳參相對來說安全一些,
返回是數據是我們用戶相關的數據,id,賬號、昵稱和頭像

接口url:/users/currentUser

請求方式:GET

請求參數:

參數名稱 參數類型 說明
Authorization string 頭部信息(TOKEN)

返回數據:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": {
        "id":1,
        "account":"1",
        "nickaname":"1",
        "avatar":"ss"
    }
}

9.2 UsersController

package com.jihu.blog.controller;


import com.jihu.blog.service.SysUserService;
import com.jihu.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
//淺談@RequestMapping @ResponseBody 和 @RequestBody 注解的用法與區別?
//https://blog.csdn.net/ff906317011/article/details/78552426?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2.no_search_link
@RestController
@RequestMapping("users")
public class UsersController {

    @Autowired
    private SysUserService sysUserService;

    @GetMapping("currentUser")
    public Result currentUser(@RequestHeader("Authorization") String token){
        return sysUserService.findUserByToken(token);
    }
}

9.3 SysUserService

package com.jihu.blog.service;
import com.jihu.blog.dao.pojo.SysUser;
import com.jihu.blog.vo.Result;
public interface SysUserService {

    /**
     * 根據token查詢用戶信息
     * @param token
     * @return
     */
    Result findUserByToken(String token);
}

9.4 SysUserServiceImpl

 @Override
    public Result findUserByToken(String token) {
        /**
         * 1.token合法性效驗
         *   是否為空,解析是否成功  redis是否存在
         * 2.如果效驗失敗  返回錯誤
         * 3.如果成功,返回對應的結果  LoginUserVo
         */

  	 //去loginservice中去校驗token
        SysUser sysUser = loginService.checkToken(token);
        if (sysUser == null){
            return Result.fail(ErrorCode.TOKEN_ERROR.getCode(),ErrorCode.TOKEN_ERROR.getMsg());
        }

        LoginUserVo loginUserVo = new LoginUserVo();
        loginUserVo.setAccount(sysUser.getAccount());
        loginUserVo.setAvatar(sysUser.getAvatar());
        loginUserVo.setId(sysUser.getId());
        loginUserVo.setNickname(sysUser.getNickname());

        return  Result.success(loginUserVo);

    }

9.5 LoginService

SysUser checkToken(String token);

9.6 LoginServiceImpl

 @Override
    public SysUser checkToken(String token) {
        //token為空返回null
        if (StringUtils.isBlank(token)){
            return null;
        }
        Map<String, Object> stringObjectMap = JWTUtils.checkToken(token);
        //解析失敗
        if (stringObjectMap == null){
            return null;
        }
         //如果成功
        String userJson = redisTemplate.opsForValue().get("TOKEN_" + token);
        if (StringUtils.isBlank(userJson)){
            return null;
        }
        SysUser sysUser = JSON.parseObject(userJson, SysUser.class); //解析為json

        return sysUser;
    }

9.7 LoginUserVo

package com.jihu.blog.vo;
import lombok.Data;
@Data
public class LoginUserVo {
    //與頁面交互

    private Long id;

    private String account;

    private String nickname;

    private String avatar;

}

9.8 測試

image-20220405104755003

10. 退出登錄

登陸一個的對token進行認證,一個是在redis中進行注冊,token字符串沒法更改掉,只能由前端進行清除,后端能做的就是把redis進行清除

10.1 接口說明

接口url:/logout

請求方式:GET

請求參數:

參數名稱 參數類型 說明
Authorization string 頭部信息(TOKEN)

返回數據:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": null
}

10.2 LogoutController

package com.jihu.blog.controller;

import com.jihu.blog.service.LoginService;
import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.LoginParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("logout")
public class LogoutController {

    @Autowired
    private LoginService loginService;

    //獲取頭部信息這樣一個參數
    @GetMapping
    public Result logout(@RequestHeader("Authorization") String token){
        return loginService.logout(token);
    }

}

10.3 LoginService

/**
     * 退出登陸
     * @param token
     * @return
     */

    Result logout(String token);

10.4 LoginServiceImpl

@Override
    public Result logout(String token) {
        //后端直接刪除redis中的token
        redisTemplate.delete("TOKEN_"+token);
        return Result.success(null);
    }

10.5 測試

image-20220405105102937

11. 注冊用戶

11.1 接口說明

接口url:/register

請求方式:POST
post傳參意味着請求參數是按照json方式傳
具體可以看這篇
post和@Requestbody

請求參數:

參數名稱 參數類型 說明
account string 賬號
password string 密碼
nickname string 昵稱

返回數據:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": "token"
}

11.2 RegisterController

package com.jihu.blog.controller;

import com.jihu.blog.service.LoginService;
import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.LoginParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("register")
public class RegisterController {

    @Autowired
    private LoginService loginService;

    @PostMapping
    public Result register(@RequestBody LoginParams loginParams){
       //sso  叫做 單點登錄, 后期如果把登錄注冊功能 提出去(單獨的服務,可以獨立提供接口服務)
        return loginService.register(loginParams);

    }
}	

11.3 LoginService

  /**
     * 注冊
     * @param loginParams
     * @return
     */
    Result register(LoginParams loginParams);

11.4 LoginServiceImpl

@Override
    public Result register(LoginParams loginParams) {
        /**
         * 1.判斷參數是否合法
         * 2.判斷賬戶是否存在,存在  返回賬戶已經被注冊
         * 3.如果賬戶不存在,注冊用戶
         * 4.生成token
         * 5. 存入redis  並返回
         * 6. 注意   加上事務,一旦中間任何過程出現問題,注冊的用戶 需要回滾
         */

        String account = loginParams.getAccount();
        String password = loginParams.getPassword();
        String nickname = loginParams.getNickname();
        if (StringUtils.isBlank(account) || StringUtils.isBlank(password)||
        StringUtils.isBlank(nickname)){
            return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
        }

        SysUser sysUser = sysUserService.findUserByAccount(account);
        if (sysUser != null){
            return  Result.fail(ErrorCode.ACCOUNT_EXIST.getCode(),"賬號已經被注冊了");
        }
        sysUser = new SysUser();
        sysUser.setNickname(nickname);
        sysUser.setAccount(account);
        sysUser.setPassword(DigestUtils.md5Hex(password + salt));
        sysUser.setCreateDate(System.currentTimeMillis());
        sysUser.setLastLogin(System.currentTimeMillis());
        sysUser.setAvatar("/static/img/logo.b3a48c0.png");
        sysUser.setAdmin(1); //1為true
        sysUser.setDeleted(0);  //0為false
        sysUser.setEmail("");
        sysUser.setSalt("");
        sysUser.setStatus("");
        sysUserService.save(sysUser);

        String token = JWTUtils.createToken(sysUser.getId());

        redisTemplate.opsForValue().set("TOKEN_"+token,JSON.toJSONString(sysUser) ,1,TimeUnit.DAYS);

        return Result.success(token);
    }

11.5 ErrorCode

 ACCOUNT_EXIST(10004,"賬號已存在"),

11.6 SysUserService

/**
     * 根據賬號查找用戶
     * @param account
     * @return
     */
    SysUser findUserByAccount(String account);

    /**
     * 保存用戶
     * @param sysUser
     */
    void save(SysUser sysUser);

11.7 SysUserServiceImpl

 @Override
    public SysUser findUserByAccount(String account) {
        LambdaQueryWrapper<SysUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(SysUser::getAccount,account);
        lambdaQueryWrapper.last("limit 1");
        return sysUserMapper.selectOne(lambdaQueryWrapper);
    }

    @Override
    public void save(SysUser sysUser) {
        //保存用戶這 id會自動生成
        //注意:  這個地方 默認生成的id 是分布式id 采用了雪花算法
        sysUserMapper.insert(sysUser);
    }

11.8 加事務

出現錯誤就進行回滾防止添加異常

增加@Transactional注解

com.jihu.blog.service.LoginService

@Service
@Transactional
public class LoginServiceImpl implements LoginService {}

當然 一般建議將事務注解@Transactional加在 接口上,通用一些。

package com.jihu.blog.service;

import com.jihu.blog.dao.pojo.SysUser;
import com.jihu.blog.vo.Result;
import com.jihu.blog.vo.params.LoginParams;
import org.springframework.transaction.annotation.Transactional;

@Transactional
public interface LoginService {

    /**
     * 登錄功能
     * @param loginParams
     * @return
     */

    Result login(LoginParams loginParams);

    SysUser checkToken(String token);
    /**
     * 退出登陸
     * @param token
     * @return
     */

    Result logout(String token);

    /**
     * 注冊
     * @param loginParams
     * @return
     */
    Result register(LoginParams loginParams);
}

11.9 測試

image-20220405141622703

12. 登錄攔截器

每次訪問需要登錄的資源的時候,都需要在代碼中進行判斷,一旦登錄的邏輯有所改變,代碼都得進行變動,非常不合適。

那么可不可以統一進行登錄判斷呢?

springMVC中攔截器

可以,使用攔截器,進行登錄攔截,如果遇到需要登錄才能訪問的接口,如果未登錄,攔截器直接返回,並跳轉登錄頁面。
Javas三大器:過濾器-監聽器-攔截器

12.1 攔截器實現

com.jihu.blog.handler.LoginInterceptor

package com.jihu.blog.handler;

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.jihu.blog.dao.pojo.SysUser;
import com.jihu.blog.service.LoginService;
import com.jihu.blog.vo.ErrorCode;
import com.jihu.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private LoginService loginService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //在執行controller方法(handler)之前進行執行
        /**
         * 1.需要判斷 請求的接口路徑 是否為 HandlerMethod (controller方法),不是的話,放行
         * 2.判斷token 是否為空, 如果為空 未登錄
         * 3.如果token不為空,登錄驗證 loginService checkToken
         * 4.如果認證成功,放行即可
         */

        if (!(handler instanceof HandlerMethod)){
            return  true;
        }
        String token = request.getHeader("Authorization");

        log.info("=================request start===========================");
        String requestURI = request.getRequestURI();
        log.info("request uri:{}",requestURI);
        log.info("request method:{}",request.getMethod());
        log.info("token:{}", token);
        log.info("=================request end===========================");

        if (StringUtils.isBlank(token)){
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        SysUser sysUser = loginService.checkToken(token);
        if (sysUser == null){
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }

        //登錄驗證成功,放行
        return true;
    }
}

12.2 使攔截器生效

com.jihu.blog.config.WebMVCConfig

package com.jihu.blog.config;

import com.jihu.blog.handler.LoginInterceptor;
import com.jihu.blog.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMVCConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    //實現跨域請求
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET","HEAD","POST","PUT","DELETE","OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //攔截test接口,后續實際遇到需要攔截的接口時,在配置為真正的攔截接口
        registry.addInterceptor(loginInterceptor)
                    .addPathPatterns("/test");
    }
}

12.3測試

com.jihu.blog.controller.TestController

@RestController
@RequestMapping("test")
public class TestController {

    @RequestMapping
    public Result test(){
        //得到用戶的信息
        SysUser sysUser = UserThreadLocal.get();
        System.out.println(sysUser);

        return Result.success(null);
    }
}

用postman進行測試,先登錄找到token,然后在驗證,才能成功

image-20220407104057929

13.ThreadLocal保存用戶信息

redis中只放了token我們希望直接獲取用戶信息
好處和如何使用的
使用ThreadLocal保存用戶登錄信息
使用ThreadLocal替代Session完成保存用戶登錄信息功能

使用ThreadLocal替代Session的好處:

 可以在同一線程中很方便的獲取用戶信息,不需要頻繁的傳遞session對象。

具體實現流程:

   在登錄業務代碼中,當用戶登錄成功時,生成一個登錄憑證存儲到redis中,
   將憑證中的字符串保存在cookie中返回給客戶端。
   使用一個攔截器攔截請求,從cookie中獲取憑證字符串與redis中的憑證進行匹配,獲取用戶信息,
   將用戶信息存儲到ThreadLocal中,在本次請求中持有用戶信息,即可在后續操作中使用到用戶信息。

相關問題
Session原理
COOKIE和SESSION有什么區別?

com.jihu.blog.utils.UserThreadLocal

package com.jihu.blog.utils;

import com.jihu.blog.dao.pojo.SysUser;

public class UserThreadLocal {

    private UserThreadLocal(){}
    //ThreadLocal 做線程變量隔離的
    private static final ThreadLocal<SysUser> LOCAL = new ThreadLocal<>();

    public static void put(SysUser sysUser){
        LOCAL.set(sysUser);
    }

    public static SysUser get(){
        return  LOCAL.get();
    }

    public  static void remove(){
        LOCAL.remove();
    }

}

com.jihu.blog.handler.LoginInterceptor

package com.jihu.blog.handler;

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.jihu.blog.dao.pojo.SysUser;
import com.jihu.blog.service.LoginService;
import com.jihu.blog.utils.UserThreadLocal;
import com.jihu.blog.vo.ErrorCode;
import com.jihu.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private LoginService loginService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //在執行controller方法(handler)之前進行執行
        /**
         * 1.需要判斷 請求的接口路徑 是否為 HandlerMethod (controller方法),不是的話,放行
         * 2.判斷token 是否為空, 如果為空 未登錄
         * 3.如果token不為空,登錄驗證 loginService checkToken
         * 4.如果認證成功,放行即可
         */

        if (!(handler instanceof HandlerMethod)){
            return  true;
        }
        String token = request.getHeader("Authorization");

        log.info("=================request start===========================");
        String requestURI = request.getRequestURI();
        log.info("request uri:{}",requestURI);
        log.info("request method:{}",request.getMethod());
        log.info("token:{}", token);
        log.info("=================request end===========================");

        if (StringUtils.isBlank(token)){
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        SysUser sysUser = loginService.checkToken(token);
        if (sysUser == null){
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }

        //登錄驗證成功,放行
        //我希望在controller中  直接獲取用戶的信息 怎么獲取呢?
       UserThreadLocal.put(sysUser);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //如果不刪除 ThreadLocal中用完的信息  會有內存泄漏的風險
        UserThreadLocal.remove();
    }
}

com.jihu.blog.controller.TestController

package com.jihu.blog.controller;

import com.jihu.blog.dao.pojo.SysUser;
import com.jihu.blog.utils.UserThreadLocal;
import com.jihu.blog.vo.Result;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("test")
public class TestController {

    @RequestMapping
    public Result test(){
        //得到用戶的信息
        SysUser sysUser = UserThreadLocal.get();
        System.out.println(sysUser);

        return Result.success(null);
    }
}

測試

在postman中重新發送一次http://localhost:8888/test請求,

可以看到能拿到我們的信息

image-20220407110431934

14.ThreadLocal內存泄漏

ThreadLocal原理及內存泄露預防

image-20220407110553643

實線表強引用,虛線代表弱引用

每一個Thread維護一個ThreadLocalMap, key為使用弱引用的ThreadLocal實例,value為線程變量的副本。

強引用,使用最普遍的引用,一個對象具有強引用,不會被垃圾回收器回收。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不回收這種對象。

如果想取消強引用和某個對象之間的關聯,可以顯式地將引用賦值為null,這樣可以使JVM在合適的時間就會回收該對象。

弱引用,JVM進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。在java中,用java.lang.ref.WeakReference類來表示。

13.文章詳情

13.1接口說明

接口url:/articles/view/{id}

請求方式:POST

請求參數:

參數名稱 參數類型 說明
id long 文章id(路徑參數)

返回數據:

{success: true, code: 200, msg: "success",…}
code: 200
data: {id: "1405916999732707330", title: "SpringBoot入門案例", summary: "springboot入門案例", commentCounts: 0,…}
msg: "success"
success: true

13.2涉及到的表

內容表

content存放makedown格式的信息
content_html存放html格式的信息

CREATE TABLE `blog`.`ms_article_body`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `content` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
  `content_html` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
  `article_id` bigint(0) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `article_id`(`article_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 38 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

類別表
avata分類圖標路徑
category_name圖標分類的名稱
description分類的描述

image-20220407142401240

CREATE TABLE `blog`.`ms_category`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  `category_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

13.3 pojo層

Category

package com.jihu.blog.dao.pojo;

import lombok.Data;

@Data
public class Category {

    private Long id;

    private String avatar;

    private String categoryName;

    private String description;

}

ArticleBody

package com.jihu.blog.dao.pojo;

import lombok.Data;
//內容表
@Data
public class ArticleBody {
    private Long id;
    private String content;
    private String contentHtml;
    private Long articleId;

}

13.4 Controller

com.jihu.blog.controller.ArticleController

//文章詳情
@PostMapping("view/{id}")
public Result findArticleById(@PathVariable("id") Long articleId){
    return articleService.findArticleById(articleId);
}

13.5 Service層

文章表里面只有tiltle以及一些簡介
ms_article 中body_id對應第二張表ms_article_body上的id
ms_category會映射到ms_article 中的category_id
需要做一些相對的關聯查詢

ArticleService

 /**
     * 查看文章詳情
     * @param articleId
     * @return
     */
    Result findArticleById(Long articleId);

ArticleServiceImpl

private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor) {
        List<ArticleVo> articleVoList = new ArrayList<>();
        for (Article record : records) {
            articleVoList.add(copy(record,isTag,isAuthor,false,false));
        }
        return articleVoList;
    }

    //方法重載
    private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor,boolean isBody, boolean isCategory) {
        List<ArticleVo> articleVoList = new ArrayList<>();
        for (Article record : records) {
            articleVoList.add(copy(record,isTag,isAuthor,isBody,isCategory));
        }
        return articleVoList;
    }

    private ArticleVo copy(Article article,boolean isTag,boolean isAuthor,boolean isBody, boolean isCategory){
        ArticleVo articleVo = new ArticleVo();
        BeanUtils.copyProperties(article,articleVo);
        articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));

        //並不是所有的接口,都需要標簽,作者信息
        if (isTag){
            Long articleId = article.getId();
            articleVo.setTags(tagService.findTagsByArticleId(articleId));
        }
        if (isAuthor){
            Long authorId = article.getAuthorId();
            articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname());
        }
        if (isBody){
            Long bodyId = article.getBodyId();
            articleVo.setBody(findArticleBodyById(bodyId));
        }
        if (isCategory){
            Long categoryId = article.getCategoryId();
            articleVo.setCategory(categoryService.findCategoryById(categoryId));
        }

        return articleVo;
    }

    private ArticleBodyVo findArticleBodyById(Long bodyId) {
        ArticleBody articleBody = articleBodyMapper.selectById(bodyId);
        ArticleBodyVo articleBodyVo = new ArticleBodyVo();
        articleBodyVo.setContent(articleBody.getContent());
        return articleBodyVo;
    }


@Override
    public Result findArticleById(Long articleId) {
        /**
         * 1.根據id查詢 文章信息
         * 2.根據bodyId和categoryid 去做關聯查詢
         */
        Article article = articleMapper.selectById(articleId);
        ArticleVo articleVo = copy(article,true,true,true,true);
        //查看完文章了,新增閱讀數,有沒有問題呢?
        //查看完文章之后,本應該直接返回數據了,這時候做了一個更新操作,更新時加寫鎖,阻塞其他的讀操作,性能就會比較低
        // 更新 增加了此次接口的 耗時 如果一旦更新出問題,不能影響 查看文章的操作
        //線程池  可以把更新操作 扔到線程池中去執行,和主線程就不相關了
        threadService.updateArticleViewCount(articleMapper,article);
        return Result.success(articleVo);
    }

CategoryService

package com.jihu.blog.service;
import com.jihu.blog.vo.CategoryVo;
public interface CategoryService {
    CategoryVo findCategoryById(Long id);
}

CategoryServiceImpl

package com.jihu.blog.service.impl;

import com.jihu.blog.dao.mapper.CategoryMapper;
import com.jihu.blog.dao.pojo.Category;
import com.jihu.blog.service.CategoryService;
import com.jihu.blog.vo.CategoryVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CategoryServiceImpl implements CategoryService {

    @Autowired
    private CategoryMapper categoryMapper;

    @Override
    public CategoryVo findCategoryById(Long id) {
        Category category = categoryMapper.selectById(id);
        CategoryVo categoryVo = new CategoryVo();
        //因為category,categoryVo屬性一樣所以可以使用 BeanUtils.copyProperties
        BeanUtils.copyProperties(category,categoryVo);
        return categoryVo;
    }
}

13.6 Vo層

ArticleVo

package com.jihu.blog.vo;

import lombok.Data;

import java.util.List;

//建立與前端交互的Vo文件
@Data
public class ArticleVo {
    private Long id;

    private String title;

    private String summary;

    private int commentCounts;

    private int viewCounts;

    private int weight;
    /**
     * 創建時間
     */
    private String createDate;

    private String author;

    private ArticleBodyVo body;

    private List<TagVo> tags;

    private CategoryVo category;
}

ArticleBodyVo

package com.jihu.blog.vo;

import lombok.Data;

@Data
public class ArticleBodyVo {

    //內容
    private String content;
}

CategoryVo

package com.jihu.blog.vo;

import lombok.Data;

@Data
public class CategoryVo {
    //id,圖標路徑,圖標名稱
    private Long id;

    private String avatar;

    private String categoryName;
}

13.7 mapper層

ArticleBodyMapper

package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.pojo.ArticleBody;

public interface ArticleBodyMapper  extends BaseMapper<ArticleBody> {
}

CategoryMapper

package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.pojo.Category;

public interface CategoryMapper  extends BaseMapper<Category> {
}

13.8 測試

image-20220407164516526

14.使用線程池 更新閱讀次數

/查看完文章了,新增閱讀數,有沒有問題呢?
//查看完文章之后,本應該直接返回數據了,這時候做了一個更新操作,更新時加寫鎖,阻塞其他的讀操作,性能就會比較低(沒辦法解決,增加閱讀數必然要加鎖)
//更新增加了此次接口的耗時(考慮減少耗時)如果一旦更新出問題,不能影響查看操作
想到了一個技術 線程池
可以把更新操作扔到 線程池中去執行和主線程就不相關了

什么是樂觀鎖,什么是悲觀鎖
CAS原理分析

14.1線程池配置

做一個線程池的配置來開啟線程池

com.jihu.blog.config.ThreadPoolConfig

package com.jihu.blog.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

//https://www.jianshu.com/p/0b8443b1adc9   關於@Configuration和@Bean的用法和理解
@Configuration
@EnableAsync
public class ThreadPoolConfig {

    @Bean("taskExecutor")
    public Executor asyncServiceExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 設置核心線程數
        executor.setCorePoolSize(5);
        // 設置最大線程數
        executor.setCorePoolSize(20);
        //配置隊列大小
        executor.setQueueCapacity(Integer.MAX_VALUE);
        // 設置線程活躍時間(秒)
        executor.setKeepAliveSeconds(60);
        // 設置默認線程名稱
        executor.setThreadNamePrefix("碼神之路博客項目");
        // 等待所有任務結束后再關閉線程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        //執行初始化
        executor.initialize();
        return executor;

    }
}

14.2 使用

com.jihu.blog.service.ThreadService

package com.jihu.blog.service;

import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.jihu.blog.dao.mapper.ArticleMapper;
import com.jihu.blog.dao.pojo.Article;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class ThreadService {
    //期望此操作在線程池執行不會影響原有主線程
    //這里線程池不了解可以去看JUC並發編程
    @Async("taskExecutor")
    public void updateArticleViewCount(ArticleMapper articleMapper, Article article) {
        int viewCounts = article.getViewCounts();
        Article articleupdate = new Article();
        articleupdate.setViewCounts(viewCounts+1);
        LambdaUpdateWrapper<Article> updatewrapper = new LambdaUpdateWrapper<>();
        //根據id更新
        updatewrapper.eq(Article::getId,article.getId());
        //設置一個為了在多線程的環境下線程安全
        //改之前再確認這個值有沒有被其他線程搶先修改,類似於CAS操作 cas加自旋,加個循環就是cas
        updatewrapper.eq(Article::getViewCounts,viewCounts);
        // update article set view_count=100 where view_count=99 and id =111
        //實體類加更新條件
        articleMapper.update(articleupdate,updatewrapper);

        try {
            Thread.sleep(5000);
            System.out.println("更新完成了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

com.jihu.blog.service.impl.ArticleServiceImpl

@Autowired
private ThreadService threadService;

@Override
public ArticleVo findArticleById(Long id) {
    Article article = articleMapper.selectById(id);
    //線程池
    threadService.updateViewCount(articleMapper,article);
    return copy(article,true,true,true,true);
}

14.3測試

睡眠 ThredService中的方法 5秒,不會影響主線程的使用,即文章詳情會很快的顯示出來,不受影響

Bug修正
之前Article中的commentCounts,viewCounts,weight 字段為int,會造成更新閱讀次數的時候,將其余兩個字段設為初始值0
mybatisplus在更新文章閱讀次數的時候雖然只設立了articleUpdate.setviewsCounts(viewCounts+1),
但是int默認基本數據類型為0,
mybatisplus但凡不是null就會生成到sql語句中進行更新。會出現

image-20220407165039295

理想中應該是只有views_counts改變但是因為mybatisplus規則所以會出現這個現象
所以將int改為Integer就不會出現這個問題。

15.評論列表

評論表
id評論id
content評論內容
create_date評論時間
article_id評論文章
author_id誰評論的
parent_id蓋樓功能對評論的評論進行回復
to_uid給誰評論
level評論的是第幾層(1級表示最上層的評論,2表示對評論的評論)

CREATE TABLE `blog`.`ms_comment`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  `create_date` bigint(0) NOT NULL,
  `article_id` int(0) NOT NULL,
  `author_id` bigint(0) NOT NULL,
  `parent_id` bigint(0) NOT NULL,
  `to_uid` bigint(0) NOT NULL,
  `level` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `article_id`(`article_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

15.1接口說明

接口url:/comments/article/{id}

請求方式:GET

請求參數:

參數名稱 參數類型 說明
id long 文章id(路徑參數)

返回數據

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id": 53,
            "author": {
                "nickname": "李四",
                "avatar": "http://localhost:8080/static/img/logo.b3a48c0.png",
                "id": 1
            },
            "content": "寫的好",
            "childrens": [
                {
                    "id": 54,
                    "author": {
                        "nickname": "李四",
                        "avatar": "http://localhost:8080/static/img/logo.b3a48c0.png",
                        "id": 1
                    },
                    "content": "111",
                    "childrens": [],
                    "createDate": "1973-11-26 08:52",
                    "level": 2,
                    "toUser": {
                        "nickname": "李四",
                        "avatar": "http://localhost:8080/static/img/logo.b3a48c0.png",
                        "id": 1
                    }
                }
            ],
            "createDate": "1973-11-27 09:53",
            "level": 1,
            "toUser": null
        }
    ]
}

15.2 CommentsController

@RequestMapping("comments")
@RestController
public class CommentsController {

    @Autowired
    private CommentsService commentsService;

    /**
     * 評論列表
     * @param id
     * @return
     */
    @GetMapping("article/{id}")
    public Result comments(@PathVariable Long id){
        return  commentsService.commentsByArticleId(id);

    }
}    

15.3 Service層

15.3.1 CommentsService

public interface CommentsService {

    /**
     * 根據文章id查詢所有的評論列表
     * @param id
     * @return
     */
    Result commentsByArticleId(Long id);
}    

15.3.2 CommentsServiceImpl

package com.jihu.blog.service.impl;

@Service
public class CommentsServiceImpl implements CommentsService {

    @Autowired
    private CommentMapper commentMapper;

    @Autowired
    private SysUserService sysUserService;

    @Override
    public Result commentsByArticleId(Long id) {
        /**
         * 1.根據文章id 查詢 評論列表,從comment表中查詢
         * 2.根據作者的id  查詢作者的信息
         * 3.判斷如果level=1,要去查詢它有沒有子評論
         * 4.如果有 根據評論id 進行查詢(parent_id)
         */
        LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Comment::getArticleId,id);
        queryWrapper.eq(Comment::getLevel,1);
        List<Comment> comments = commentMapper.selectList(queryWrapper);
        List<CommentVo> commentVoList = copyList(comments);
        return Result.success(commentVoList);
    }

    private List<CommentVo> copyList(List<Comment> comments) {
        List<CommentVo> commentVoList = new ArrayList<>();
        for (Comment comment : comments) {
            commentVoList.add(copy(comment));
        }

        return commentVoList;
    }

    private CommentVo copy(Comment comment) {
        CommentVo commentVo = new CommentVo();
        // 相同屬性copy
        BeanUtils.copyProperties(comment,commentVo);
        //作者信息
        Long authorId = comment.getAuthorId();
        UserVo userVo= sysUserService.findUserVoById(authorId);
        commentVo.setAuthor(userVo);

        //子評論
        Integer level = comment.getLevel();
        if (level == 1){
            Long id = comment.getId();
            List<CommentVo> commentVoList = findCommentByParentId(id);
            commentVo.setChildrens(commentVoList);
        }

        //to user  給誰評論
        if (level>1){
            Long toUid = comment.getToUid();
            UserVo toUserVo = sysUserService.findUserVoById(toUid);
            commentVo.setToUser(toUserVo);
        }
        return commentVo;
    }

    //子評論的查詢
    private List<CommentVo> findCommentByParentId(Long id) {
        LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Comment::getParentId,id);
        queryWrapper.eq(Comment::getLevel,2);
        List<Comment> comments = commentMapper.selectList(queryWrapper);
        return copyList(comments);
    }
}

15.3.3 SysUserService

  /**
     * 查詢UserVo的信息
     * 查詢用戶信息的服務:
     * @param id
     * @return
     */
    UserVo findUserVoById(Long id);

15.3.4 SysUserServiceImpl

@Override
    public UserVo findUserVoById(Long id) {

        SysUser sysUser = sysUserMapper.selectById(id);
        if (sysUser  == null){
            sysUser = new SysUser();
            sysUser.setId(1L);
            sysUser.setAvatar("/static/img/logo.b3a48c0.png");
            sysUser.setNickname("馬神之路");
        }
        UserVo userVo = new UserVo();
        BeanUtils.copyProperties(sysUser,userVo);

        return userVo;
    }

15.4 Vo層

用於返回的數據:

CommentVo

com.jihu.blog.vo.CommentVo

package com.jihu.blog.vo;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;

import java.util.List;

@Data
public class CommentVo {
    //防止前端 精度損失  把id轉為string
    //把 id轉化為string類型的  要不然會丟精度  導致錯誤
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
    private UserVo author;
    private String content;
    private List<CommentVo> childrens;
    private String createDate;
    private Integer level;
    private UserVo toUser;
}

UserVo

com.jihu.blog.vo.UserVo

package com.jihu.blog.vo;
import lombok.Data;
@Data
public class UserVo {

    private Long id;
    private String avatar;
    private String nickname;
}

15.5 測試

image-20220407211808734

16.評論

16.1 接口說明

接口url:/comments/create/change

請求方式:POST

請求參數:

參數名稱 參數類型 說明
articleId long 文章id
content string 評論內容
parent long 父評論id
toUserId long 被評論的用戶id

返回數據:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": null
}

16.2加入到登錄攔截器中

com.jihu.blog.config.WebMVCConfig

@Override
    public void addInterceptors(InterceptorRegistry registry) {
        //攔截test接口,后續實際遇到需要攔截的接口時,在配置為真正的攔截接口
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/test").addPathPatterns("/comments/create/change");
    }

16.3 新建CommentParam

構建評論參數對象:

com.jihu.blog.vo.params.CommentParam

package com.jihu.blog.vo.params;

import lombok.Data;

@Data
public class CommentParam {

    private Long articleId;

    private String content;

    private Long parent;

    private Long toUserId;
}

16.4 修改CommentsController

/**
     * 評論
     * @param commentParam
     * @return
     */
    @PostMapping("create/change")
    public Result comment(@RequestBody CommentParam commentParam){
        return  commentsService.comment(commentParam);

    }

16.5 修改CommentsService

 // 評論
 Result comment(CommentParam commentParam);

16.6 修改CommentsServiceImpl

 @Override
    public Result comment(CommentParam commentParam) {
        //拿到當前用戶
        SysUser sysUser = UserThreadLocal.get();
        Comment comment = new Comment();
        comment.setAuthorId(sysUser.getId());
        comment.setArticleId(commentParam.getArticleId());
        comment.setContent(commentParam.getContent());
        comment.setCreateDate(System.currentTimeMillis());
        Long parent = commentParam.getParent();
        if (parent == null || parent == 0 ){
            comment.setLevel(1);
        }else {
            comment.setLevel(2);
        }
        //如果是空,parent就是0
        comment.setParentId(parent == null ? 0 : parent);
        Long toUserId = commentParam.getToUserId();
        comment.setToUid(toUserId == null ? 0 : toUserId);
        commentMapper.insert(comment);
        return Result.success(null);
    }

修改 com.jihu.blog.vo.CommentVo

  //防止前端 精度損失 把id轉為string
// 分布式id 比較長,傳到前端 會有精度損失,必須轉為string類型 進行傳輸,就不會有問題了
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;

16.7 測試

image-20220407211841583

17、寫文章

寫文章由 三部分組成:

  1. 獲取所有文章類別
  2. 獲取所有標簽
  3. 發布文章

17.1 文章分類

17.1.1接口說明

接口url:/categorys

請求方式:GET

請求參數:無

返回數據:

{
    "success":true,
 	"code":200,
    "msg":"success",
    "data":
    [
        {"id":1,"avatar":"/category/front.png","categoryName":"前端"},	
        {"id":2,"avatar":"/category/back.png","categoryName":"后端"},
        {"id":3,"avatar":"/category/lift.jpg","categoryName":"生活"},
        {"id":4,"avatar":"/category/database.png","categoryName":"數據庫"},
        {"id":5,"avatar":"/category/language.png","categoryName":"編程語言"}
    ]
}

17.1.2 CategoryController

package com.jihu.blog.controller;


import com.jihu.blog.service.CategoryService;
import com.jihu.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("categorys")
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    @GetMapping
    public Result categories(){
        return categoryService.findAll();
    }
}

17.1.3 CategoryService

Result findAll();

17.1.4 CategoryServiceImpl

@Override
    public Result findAll() {
        // 沒有任何參數,所有一個空的LambdaQueryWrapper即可
        List<Category> categories = categoryMapper.selectList(new LambdaQueryWrapper<>());
        //頁面交互的對象
        return Result.success(copyList(categories));
    }

    private List<CategoryVo> copyList(List<Category> categories) {
        ArrayList<CategoryVo> categoryVoList = new ArrayList<>();
        for (Category category : categories) {
            categoryVoList.add(copy(category));
        }
        return categoryVoList;
    }

    private CategoryVo copy(Category category) {
        CategoryVo categoryVo = new CategoryVo();
        BeanUtils.copyProperties(category,categoryVo);
        //id不一致要重新設立
        // categoryVo.setId(String.valueOf(category.getId()));
        return categoryVo;
    }

17.1.5 測試

image-20220407224303242

17.2 獲取所有標簽

17.2.1 接口說明

接口url:/tags

請求方式:GET

請求參數:無

返回數據:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id": 5,
            "tagName": "springboot"
        },
        {
            "id": 6,
            "tagName": "spring"
        },
        {
            "id": 7,
            "tagName": "springmvc"
        },
        {
            "id": 8,
            "tagName": "11"
        }
    ]
}

17.2.2 TagsController

@Autowired
    private TagService tagService;
 	@GetMapping
    public Result findAll(){
        /**
     * 查詢所有的文章標簽
     * @return
     */
        return tagService.findAll();
    }

17.2.3 TagService

    /**
     * 查詢所有文章標簽
     * @return
     */
    Result findAll();

17.2.4 TagServiceImpl

	@Override
    public Result findAll() {
        List<Tag> tags = this.tagMapper.selectList(new LambdaQueryWrapper<>());
        return Result.success(copyList(tags));
    }

17.2.5 測試

image-20220407224549033

17.3 發布文章

17.3.1接口說明

請求內容是object({content: “ww”, contentHtml: “ww↵”})是因為本身為makedown的編輯器
id指的是文章id

image-20220407224723527

接口url:/articles/publish

請求方式:POST

請求參數:

image-20220408120619584

返回數據:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": {"id":12232323}
}

17.3.2修改ArticleController

  @PostMapping("publish")
    public Result publish(@RequestBody ArticleParam articleParam){
        return articleService.publish(articleParam);
    }

17.3.3 參數 param

ArticleParam
package com.jihu.blog.vo.params;

import com.jihu.blog.vo.CategoryVo;
import com.jihu.blog.vo.TagVo;
import lombok.Data;

import java.util.List;

@Data
public class ArticleParam {

    private Long id;

    private ArticleBodyParam body;

    private CategoryVo category;

    private String summary;

    private List<TagVo> tags;

    private String title;

}
ArticleBodyParam
package com.jihu.blog.vo.params;

import lombok.Data;

@Data
public class ArticleBodyParam {
    private String content;

    private String contentHtml;
}

17.3.4 Service層

ArticleService
 /**
     * 文章發布服務
     * @param articleParam
     * @return
     */
    Result publish(ArticleParam articleParam);
ArticleServiceImpl
@Override
    public Result publish(ArticleParam articleParam) {
        //注意想要拿到數據必須將接口加入到登錄攔截當中
        SysUser sysUser = UserThreadLocal.get();
        /**
         * 1.發布文章  目的  構建Article對象
         * 2.作者id 當前的登錄用戶
         *3.標簽  要將標簽加入到 關聯表當中
         * 4.body 內容存儲 article bodyId
         */
        Article article = new Article();
        article.setAuthorId(sysUser.getId());
        article.setCategoryId(articleParam.getCategory().getId());
        article.setCreateDate(System.currentTimeMillis());
        article.setCommentCounts(0);
        article.setSummary(articleParam.getSummary());
        article.setTitle(articleParam.getTitle());
        article.setViewCounts(0);
        article.setWeight(Article.Article_Common);
        article.setBodyId(-1L);
        //插入之后 會生成一個文章id(因為新建的文章沒有文章id所以要insert一下
        //官網解釋:"insert后主鍵會自動'set到實體的ID字段。所以你只需要"getid()就好
        //利用主鍵自增,mp的insert操作后id值會回到參數對象中
        //https://blog.csdn.net/HSJ0170/article/details/107982866
        articleMapper.insert(article);

        //tags
        List<TagVo> tags = articleParam.getTags();
        if ( tags != null){
            for (TagVo tag : tags) {
                ArticleTag articleTag = new ArticleTag();
                articleTag.setArticleId(article.getId());
                articleTag.setTagId(tag.getId());
                articleTagMapper.insert(articleTag);
            }
        }

        //body
        ArticleBody articleBody = new ArticleBody();
        articleBody.setContent(articleParam.getBody().getContent());
        articleBody.setContentHtml(articleParam.getBody().getContentHtml());
        articleBody.setArticleId(article.getId());
        articleBodyMapper.insert(articleBody);
        //插入完之后再給一個id
        article.setBodyId(articleBody.getId());
        //MybatisPlus中的save方法什么時候執行insert,什么時候執行update
        // https://www.cxyzjd.com/article/Horse7/103868144
        //只有當更改數據庫時才插入或者更新,一般查詢就可以了

        articleMapper.updateById(article);
        Map<String,String > map = new HashMap<>();
        map.put("id",article.getId().toString());
        //或者這樣也行
//        ArticleVo articleVo = new ArticleVo();
//        articleVo.setId(article.getId());
//        return Result.success(articleVo);

        return Result.success(map);
    }

17.3.5 修改 WebMVCConfig

當然登錄攔截器中,需要加入發布文章的攔截:

 @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //攔截test接口,后續實際遇到需要攔截的接口時,在配置為真正的攔截接口
        registry.addInterceptor(loginInterceptor)
                    .addPathPatterns("/test")
                    .addPathPatterns("/comments/create/change")
                    .addPathPatterns("/articles/publish");
    }

17.3.6 mapper層

ArticleBodyMapper
package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.pojo.ArticleBody;

public interface ArticleBodyMapper  extends BaseMapper<ArticleBody> {
}

ArticleTagMapper
package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.dao.pojo.ArticleTag;

public interface ArticleTagMapper extends BaseMapper<ArticleTag> {
}

17.3.7 ArticleVo

package com.jihu.blog.vo;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;

import java.util.List;

//建立與前端交互的Vo文件
@Data
public class ArticleVo {
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;

    private String title;

    private String summary;

    private int commentCounts;

    private int viewCounts;

    private int weight;
    /**
     * 創建時間
     */
    private String createDate;

    private String author;

    private ArticleBodyVo body;

    private List<TagVo> tags;

    private CategoryVo category;
}

17.3.8 ArticleTag

package com.jihu.blog.dao.pojo;

import lombok.Data;

@Data
public class ArticleTag {

    private Long id;

    private Long articleId;

    private Long tagId;
}

17.3.9 測試

image-20220408124010456

18.AOP日志

IOC是spring的兩大核心概念之一,IOC給我們提供了一個IOCbean容器,這個容器會幫我們自動去創建對象,不需要我們手動創建,IOC實現創建的通過DI(Dependency Injection 依賴注入),我們可以通過寫Java注解代碼或者是XML配置方式,把我們想要注入對象所依賴的一些其他的bean,自動的注入進去,他是通過byName或byType類型的方式來幫助我們注入。正是因為有了依賴注入,使得IOC有這非常強大的好處,解耦。

可以舉個例子,JdbcTemplate 或者 SqlSessionFactory 這種bean,如果我們要把他注入到容器里面,他是需要依賴一個數據源的,如果我們把JdbcTemplate 或者 Druid 的數據源強耦合在一起,會導致一個問題,當我們想要使用jdbctemplate必須要使用Druid數據源,那么依賴注入能夠幫助我們在Jdbc注入的時候,只需要讓他依賴一個DataSource接口,不需要去依賴具體的實現,這樣的好處就是,將來我們給容器里面注入一個Druid數據源,他就會自動注入到JdbcTemplate如果我們注入一個其他的也是一樣的。比如說c3p0也是一樣的,這樣的話,JdbcTemplate和數據源完全的解耦了,不強依賴與任何一個數據源,在spring啟動的時候,就會把所有的bean全部創建好,這樣的話,程序在運行的時候就不需要創建bean了,運行速度會更快,還有IOC管理bean的時候默認是單例的,可以節省時間,提高性能,

Spring IOC ,AOP,MVC 的理解

Springboot AOP日志相關講解

在不改變原有方法基礎上對原有方法進行增強

1. 新建LogAnnotation

com.jihu.blog.commom.aop.LogAnnotation

package com.jihu.blog.commom.aop;

import java.lang.annotation.*;

//type代表可以放在類上面  ,method代表可以放在方法上
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {
    String  module() default  "";
    String  operator() default "";

}

2.修改ArticleController

com.jihu.blog.controller.ArticleController

    //首頁  文章列表
    @PostMapping
    //加上此注解,代表要對此接口記錄日志
    @LogAnnotation(module="文章",operator="獲取文章列表")
    public Result listArticle(@RequestBody PageParams pageParams){
//        int  i =10/0;
        //ArticleVo 頁面接收的數據
        return articleService.listArticle(pageParams);

    }

3.新建LogAspect

com.jihu.blog.commom.aop.LogAspect

package com.jihu.blog.commom.aop;

import com.alibaba.fastjson.JSON;
import com.jihu.blog.utils.HttpContextUtils;
import com.jihu.blog.utils.IpUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

@Component
@Aspect  //切面  定義了通知和切點的關系
@Slf4j
public class LogAspect {

    @Pointcut("@annotation(com.jihu.blog.commom.aop.LogAnnotation)")
    public void pt(){}
    
    //環繞通知
    @Around("pt()")
    public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
        long beginTime = System.currentTimeMillis();
        //執行方法
        Object result = joinPoint.proceed();
        //執行時長(毫秒)
        long time = System.currentTimeMillis() - beginTime;
        //保存日志
        recordLog(joinPoint,time);
        return result;

    }

    private void recordLog(ProceedingJoinPoint joinPoint, long time) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
        log.info("=====================log start================================");
        log.info("module:{}",logAnnotation.module());
        log.info("operation:{}",logAnnotation.operator());

        //請求的方法名
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = signature.getName();
        log.info("request method:{}",className + "." + methodName + "()");

//        //請求的參數
        Object[] args = joinPoint.getArgs();
        String params = JSON.toJSONString(args[0]);
        log.info("params:{}",params);

        //獲取request 設置IP地址
        HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
        log.info("ip:{}", IpUtils.getIpAddr(request));


        log.info("excute time : {} ms",time);
        log.info("=====================log end================================");

    }
}

​ 用到的方法類

4.新建HttpContextUtils

com.jihu.blog.utils.HttpContextUtils

package com.jihu.blog.utils;

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
/**
 * HttpServletRequest
 *
 */
public class HttpContextUtils {
    public static HttpServletRequest getHttpServletRequest() {
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }
}

5.新建IpUtils

com.jihu.blog.utils.IpUtils

package com.jihu.blog.utils;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.http.HttpServletRequest;

/**
 * 獲取Ip
 *
 */
@Slf4j
public class IpUtils {

    /**
     * 獲取IP地址
     * <p>
     * 使用Nginx等反向代理軟件, 則不能通過request.getRemoteAddr()獲取IP地址
     * 如果使用了多級反向代理的話,X-Forwarded-For的值並不止一個,而是一串IP地址,X-Forwarded-For中第一個非unknown的有效IP字符串,則為真實IP地址
     */
    public static String getIpAddr(HttpServletRequest request) {
        String ip = null, unknown = "unknown", seperator = ",";
        int maxLength = 15;
        try {
            ip = request.getHeader("x-forwarded-for");
            if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (StringUtils.isEmpty(ip) || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_CLIENT_IP");
            }
            if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_X_FORWARDED_FOR");
            }
            if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }
        } catch (Exception e) {
            log.error("IpUtils ERROR ", e);
        }

        // 使用代理,則獲取第一個IP地址
        if (StringUtils.isEmpty(ip) && ip.length() > maxLength) {
            int idx = ip.indexOf(seperator);
            if (idx > 0) {
                ip = ip.substring(0, idx);
            }
        }

        return ip;
    }

    /**
     * 獲取ip地址
     *
     * @return
     */
    public static String getIpAddr() {
        HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
        return getIpAddr(request);
    }
}

6.測試

image-20220408152708831

bug修正

因為數據庫中的create_date類型是bigint ,不是date類型的 所有需要進行如下操作。

防止拿到的值是null值,因為拿到的是毫秒值,需要對其進行轉化,Y表示年,m表示月,對時間進行重寫。

image-20220408153106485

相關函數說明

修改ArticleMapper.xml

select FROM_UNIXTIME(create_date/1000,'%Y') as year, FROM_UNIXTIME(create_date/1000,'%m') as month,count(*) as count from ms_article group by year,month

19.文章圖片上傳

19.1接口說明

接口url:/upload

請求方式:POST

請求參數:

參數名稱 參數類型 說明
image file 上傳的文件名稱

返回數據:

{
    "success":true,
 	"code":200,
    "msg":"success",
    "data":"https://static.mszlu.com/aa.png"
}

修改pom文件引入七牛雲的sdk
pom.xml

<dependency>
  <groupId>com.qiniu</groupId>
  <artifactId>qiniu-java-sdk</artifactId>
  <version>[7.7.0, 7.7.99]</version>
</dependency>

19.2 新建UploadController

package com.jihu.blog.controller;

import com.mszlu.blog.utils.QiniuUtils;
import com.mszlu.blog.vo.Result;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.util.UUID;

@RestController
@RequestMapping("upload")
public class UploadController {
    @Autowired
    private QiniuUtils qiniuUtils;

    //https://blog.csdn.net/justry_deng/article/details/80855235 MultipartFile介紹
    @PostMapping
    public Result upload(@RequestParam("image")MultipartFile file){
        //原始文件名稱 比如說aa.png
        String originalFilename = file.getOriginalFilename();
        //唯一的文件名稱
        String fileName =  UUID.randomUUID().toString()+"."+StringUtils.substringAfterLast(originalFilename, ".");
        //上傳文件上傳到那里呢? 七牛雲 雲服務器
        //降低我們自身應用服務器的帶寬消耗
        boolean upload = qiniuUtils.upload(file, fileName);
        if (upload) {
            return Result.success(QiniuUtils.url+fileName);
        }
        return Result.fail(20001,"上傳失敗");

}

19.3 使用七牛雲

注意七牛雲測試域名 https://static.mszlu.com/ 一個月一回收,記得去修改。
springboot默認只上傳1M的圖片大小所以修改文件配置
src/main/resources/application.properties

# 上傳文件總的最大值
spring.servlet.multipart.max-request-size=20MB
# 單個文件的最大值
spring.servlet.multipart.max-file-size=2MB

七牛雲建立存儲空間教程

19.4新建QiniuUtils

com.jihu.blog.utils.QiniuUtils.java

package com.mszlu.blog.utils;

import com.alibaba.fastjson.JSON;
import com.qiniu.http.Response;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

@Component
public class QiniuUtils {

    public static  final String url = "https://static.mszlu.com/";

//修改以下兩個值放到proprietarties中,在密鑰管理中獲取
    @Value("${qiniu.accessKey}")
    private  String accessKey;
    @Value("${qiniu.accessSecretKey}")
    private  String accessSecretKey;

    public  boolean upload(MultipartFile file,String fileName){

        //構造一個帶指定 Region 對象的配置類
        Configuration cfg = new Configuration(Region.huabei());
        //...其他參數參考類注釋
        UploadManager uploadManager = new UploadManager(cfg);
        //...生成上傳憑證,然后准備上傳,修改上傳名稱為自己創立空間的空間名稱(是你自己的)
        String bucket = "mszlu";
        //默認不指定key的情況下,以文件內容的hash值作為文件名
        try {
            byte[] uploadBytes = file.getBytes();
            Auth auth = Auth.create(accessKey, accessSecretKey);
            String upToken = auth.uploadToken(bucket);
                Response response = uploadManager.put(uploadBytes, fileName, upToken);
                //解析上傳成功的結果
                DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);
                return true;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        return false;
    }
}

20.導航-文章分類

20.1 查詢所有的文章分類

20.1.1接口說明

接口url:/categorys/detail

請求方式:GET

請求參數:無

返回數據:

{
    "success": true, 
    "code": 200, 
    "msg": "success", 
    "data": [
        {
            "id": 1, 
            "avatar": "/static/category/front.png", 
            "categoryName": "前端", 
            "description": "前端是什么,大前端"
        }, 
        {
            "id": 2, 
            "avatar": "/static/category/back.png", 
            "categoryName": "后端", 
            "description": "后端最牛叉"
        }
    ]
}

20.1.2 修改CategoryVo

@Data
public class CategoryVo {

    private Long id;

    private String avatar;

    private String categoryName;

    private String description;
}

20.1.3 修改CategoryController

 @GetMapping("detail")
    public Result  categorydetail(){
        return categoryService.findCategoryByDetail();
    }

20.1.4 修改CategoryService

/**
     * 查詢所有文章分類
     * @return
     */
    Result findCategoryByDetail();

20.1.5 修改CategoryServiceImpl

@Override
    public Result findCategoryByDetail() {
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        List<Category> categories = categoryMapper.selectList(queryWrapper);
        return Result.success(copyList(categories));
    }

20.1.6 文章分類顯示

image-20220408194105092

20.2 查詢所有的標簽

20.2.1 接口說明

接口url:/tags/detail

請求方式:GET

請求參數:無

返回數據:

{
    "success": true, 
    "code": 200, 
    "msg": "success", 
    "data": [
        {
            "id": 5, 
            "tagName": "springboot", 
            "avatar": "/static/tag/java.png"
        }, 
        {
            "id": 6, 
            "tagName": "spring", 
            "avatar": "/static/tag/java.png"
        }
    ]
}

20.2.2 修改TagVo

@Data
public class TagVo {

    private Long id;

    private String tagName;

    private String avatar;
}

20.2.3 修改TagsController

@GetMapping("detail")
    public Result findTagDetail(){
        return tagService.findTagDetail();
    }

20.2.4 修改TagService

 /**
     * 查詢所有的標簽
     * @return
     */
    Result findTagDetail();

20.2.5 修改TagServiceImpl

@Override
    public Result findTagDetail() {
        LambdaQueryWrapper<Tag> queryWrapper = new LambdaQueryWrapper<>();
        List<Tag> tags = tagMapper.selectList(queryWrapper);
        return Result.success(copyList(tags));
    }

20.2.6 顯示結果:

image-20220408194358661

21.分類文章列表

21.1接口說明

接口url:/category/detail/{id}

請求方式:GET

請求參數:

參數名稱 參數類型 說明
id 分類id 路徑參數

返回數據:

{
    "success": true, 
    "code": 200, 
    "msg": "success", 
    "data": 
        {
            "id": 1, 
            "avatar": "/static/category/front.png", 
            "categoryName": "前端", 
            "description": "前端是什么,大前端"
        }
}

21.2修改CategoryController

@GetMapping("detail/{id}")
    public Result  categorydetailById(@PathVariable("id") Long id){
        return categoryService.categoryDetailById(id);
    }

21.3修改CategoryService

Result categoryDetailById(Long id);

21.4修改CategoryServiceImpl

  @Override
    public Result categoryDetailById(Long id) {
//        LambdaQueryWrapper<Category> queryWrapper  = new LambdaQueryWrapper<>();
        Category category = categoryMapper.selectById(id);
        return Result.success(copy(category));
    }

完成上面這些只能說是可以顯示文章分類的圖標了

image-20220408200924828

但是如果想顯示后端所有的歸屬內容得在文章查詢列表出進行queryWrapper查找,當文章分類標簽不是null時,加入文章分類標簽這個查詢元素進行分類修改。

21.5修改ArticleServiceImpl

@Override
    public Result listArticle(PageParams pageParams) {

        Page<Article> page = new Page<>(pageParams.getPage(), pageParams.getPageSize());
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();

         //查詢文章的參數 加上分類id,判斷不為空 加上分類條件  
        if (pageParams.getCategoryId() != null){
             //相當於  category_id = #{category_id}
            queryWrapper.eq(Article::getCategoryId,pageParams.getCategoryId());
        }

        //是否置頂進行排序
        queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);

        Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);
        List<Article> records = articlePage.getRecords();
        //能直接返回嗎  肯定不行  所以需要進行如下轉換
       List<ArticleVo> articleVoList = copyList(records,true,true);
        return Result.success(articleVoList);
    }

21.6修改PageParams

@Data
public class PageParams {

    private int page = 1;

    private int pageSize = 10;

    private Long categoryId;

    private Long tagId;
}

最后就可以顯示所有文章分類的每個標簽下的內容了

image-20220408201123124

22.標簽文章列表

22.1接口說明

接口url:/tags/detail/{id}

請求方式:GET

請求參數:

參數名稱 參數類型 說明
id 標簽id 路徑參數

返回數據:

{
    "success": true, 
    "code": 200, 
    "msg": "success", 
    "data": 
        {
            "id": 5, 
            "tagName": "springboot", 
            "avatar": "/static/tag/java.png"
        }
}

22.2 修改TagsController

  /**
     * 查詢所有文章標簽下所有的文章
     * @return
     */
    @GetMapping("detail/{id}")
    public Result findTagDetailById(@PathVariable("id") Long id){
        return tagService.findTagDetailById(id);
    }

22.3修改TagService

Result findTagDetailById(Long id);

22.4修改TagServiceImpl

 @Override
    public Result findTagDetailById(Long id) {
        Tag tag = tagMapper.selectById(id);
        return Result.success(copy(tag));
    }

完成上面這些這保證了文章標簽顯示出來了我們需要重寫文章查詢接口,保證當遇到標簽查詢時我們可以做到正確查詢文章標簽所對應的內容,要不每一個標簽查出來的內容都是一樣的。

image-20220408210356145

22.5修改ArticleServiceImpl

@Override
    public Result listArticle(PageParams pageParams) {

        Page<Article> page = new Page<>(pageParams.getPage(), pageParams.getPageSize());
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();

        //相當於  category_id = #{category_id}
        //根據文章分類獲取文章
        if (pageParams.getCategoryId() != null){
            queryWrapper.eq(Article::getCategoryId,pageParams.getCategoryId());
        }
-----------添加的代碼開始------------
        //根據標簽獲取文章
        ArrayList<Long> articleIdList = new ArrayList<>();
        if (pageParams.getTagId() != null){
            //加入標簽條件查詢
            //article表中並沒有tag字段 一篇文章有多個標簽
            //articie_tog article_id 1:n tag_id
            //我們需要利用一個全新的屬於文章標簽的queryWrapper將這篇文章的article_Tag查出來,保存到一個list當中。
            // 然后再根據queryWrapper的in方法選擇我們需要的標簽即可。
            LambdaQueryWrapper<ArticleTag> queryWrapper1 = new LambdaQueryWrapper<>();
             queryWrapper1.eq(ArticleTag::getTagId, pageParams.getTagId());
            List<ArticleTag> articleTags = articleTagMapper.selectList(queryWrapper1);
            for (ArticleTag articleTag : articleTags) {
                articleIdList.add(articleTag.getArticleId());
            }
            if (articleIdList.size() > 0){
                // and id in (1,2,3)
                queryWrapper.in(Article::getId,articleIdList);
            }
        }
---------添加的代碼結束-----------------
        //是否置頂進行排序
        queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);

        Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);
        List<Article> records = articlePage.getRecords();
        //能直接返回嗎  肯定不行  所以需要進行如下轉換
       List<ArticleVo> articleVoList = copyList(records,true,true);
        return Result.success(articleVoList);
    }

22.6測試

最終的結果如下,每一個標簽下都對應着該標簽所對應的文章

image-20220408210807756

23. 歸檔文章列表

23.1接口說明

接口url:/articles

請求方式:POST

請求參數:

參數名稱 參數類型 說明
year string
month string

返回數據

{
    "success": true, 
    "code": 200, 
    "msg": "success", 
    "data": [文章列表,數據同之前的文章列表接口]
        
}

mybatisplus駝峰命名和mapper.xml使用

23.2修改PageParams

package com.jihu.blog.vo.params;

import lombok.Data;

@Data
public class PageParams {
    private  int Page =1;  //當前頁數
    private  int PageSize =10;  //每頁顯示的數量

    private Long categoryId;
    private Long tagId;

    private String year;
    private String month;

    //為了讓傳遞的值為6變成06
    public String getMonth(){
        if (month != null && this.month.length()==1){
            return "0"+this.month;
        }
        return  this.month;
    }

}

23.3修改ArticleServiceImpl

   @Override
    public Result listArticle(PageParams pageParams) {
        Page<Article> page = new Page<>(pageParams.getPage(),pageParams.getPageSize());
        IPage<Article> articleIPage = articleMapper.listArticle(page, pageParams.getCategoryId(), pageParams.getTagId(), pageParams.getYear(), pageParams.getMonth());
        List<Article> records = articleIPage.getRecords();
        return Result.success(copyList(records,true,true));
    }

23.4修改ArticleMapper

 IPage<Article> listArticle(Page<Article> page,
                               Long categoryId,
                               Long tagId,
                               String year,
                               String month);

23.5修改ArticleMapper.xml

<!--因為與數據庫字段沒法進行一一映射  所以做這么個映射關系-->
<resultMap id="articleMap" type="com.jihu.blog.dao.pojo.Article">
        <id column="id" property="id" />
        <result column="comment_counts" property="commentCounts"/>
        <result column="create_date" property="createDate"/>
        <result column="summary" property="summary"/>
        <result column="title" property="title"/>
        <result column="view_counts" property="viewCounts"/>
        <result column="weight" property="weight"/>
        <result column="author_id" property="authorId"/>
        <result column="body_id" property="bodyId"/>
        <result column="category_id" property="categoryId"/>
    </resultMap>



<select id="listArticle" resultType="com.jihu.blog.dao.pojo.Article" resultMap="articleMap" >
    select  * from ms_article
    <where>
    <!--加  1 = 1 是為了去掉 多余的and  要不然會成為 where and category_id=?  mybatis中不用加,它會自動去掉-->
    1 = 1
    <if test="categoryId != null ">
       and category_id = #{categoryId}
    </if>
    <if test="tagId != null">
        and id in (select article_id from ms_article_tag where tag_id=#{tagId})
     </if>
     <if test="year != null and year.length>0 and month != null and month.length>0">
        and (FROM_UNIXTIME(create_date/1000,'%Y') =#{year} and FROM_UNIXTIME(create_date/1000,'%m')=#{month})
     </if>
    </where>
    order by weight,create_date desc

</select>

resultMap和resultType區別 https://blog.csdn.net/xushiyu1996818/article/details/89075069?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-4.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-4.no_search_link
駝峰命名法 https://blog.csdn.net/A_Java_Dog/article/details/107006391?spm=1001.2101.3001.6650.6&utm_medium=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~default-6.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~default-6.no_search_link
mybatis中xml文件用法 https://blog.csdn.net/weixin_43882997/article/details/85625805
動態sql https://www.jianshu.com/p/e309ae5e4a77
駝峰命名 https://zoutao.blog.csdn.net/article/details/82685918?spm=1001.2101.3001.6650.18&utm_medium=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~default-18.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~default-18.no_search_link

23.6測試

image-20220408223431687

24、統一緩存處理(優化)

內存的訪問速度 遠遠大於 磁盤的訪問速度 (1000倍起)
Spring Cache介紹

Cache

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {
    long expire() default  1 * 60 * 1000;
    String name() default  "";
}

CacheAspect

package com.jihu.blog.commom.cache;

import com.alibaba.fastjson.JSON;
import com.jihu.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.Duration;

@Component
@Aspect  //切面  定義了通知和切點的關系
@Slf4j
public class CacheAspect {

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Pointcut("@annotation(com.jihu.blog.commom.cache.Cache)")
    public void pt(){}

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp){
        try {
            Signature signature = pjp.getSignature();
            //類名
            String className = pjp.getTarget().getClass().getSimpleName();
            //調用的方法名
            String methodName = signature.getName();


            Class[] parameterTypes = new Class[pjp.getArgs().length];
            Object[] args = pjp.getArgs();
            //參數
            String params = "";
            for(int i=0; i<args.length; i++) {
                if(args[i] != null) {
                    params += JSON.toJSONString(args[i]);
                    parameterTypes[i] = args[i].getClass();
                }else {
                    parameterTypes[i] = null;
                }
            }
            if (StringUtils.isNotEmpty(params)) {
                //加密 以防出現key過長以及字符轉義獲取不到的情況
                params = DigestUtils.md5Hex(params);
            }
            Method method = pjp.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);
            //獲取Cache注解
            Cache annotation = method.getAnnotation(Cache.class);
            //緩存過期時間
            long expire = annotation.expire();
            //緩存名稱
            String name = annotation.name();
            //先從redis獲取
            String redisKey = name + "::" + className+"::"+methodName+"::"+params;
            String redisValue = redisTemplate.opsForValue().get(redisKey);
            if (StringUtils.isNotEmpty(redisValue)){
                log.info("走了緩存~~~,{},{}",className,methodName);
                return JSON.parseObject(redisValue, Result.class);
            }
            Object proceed = pjp.proceed();
            redisTemplate.opsForValue().set(redisKey,JSON.toJSONString(proceed), Duration.ofMillis(expire));
            log.info("存入緩存~~~ {},{}",className,methodName);
            return proceed;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return Result.fail(-999,"系統錯誤");

    }
}

使用:

在需要的地方加上就可以了

image-20220409105124796

測試

image-20220409105231282

思考別的優化

mongodb
redis incr

1.文章可以放人es當中,便於后續中文分詞搜索。

2.評論數據,可以考慮放到mongodb當中,電商系統當中 評論數據放入mongdb中

3.閱讀數和評論數,考慮把閱讀數和評論數 增加的時候放入 redis incr 自增,使用定時任務 定時把數據固化到數據庫當中

4.為了加快訪問速度,部署的時候,可以把圖片,js,css等放入七牛雲存儲,加快網站訪問速度。

25.管理后台

25.1搭建項目

25.1.1 新建maven工程 blog-admin

<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>blog-parent</artifactId>
        <groupId>com.jihu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>blog-admin</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <!-- 排除 默認使用的logback  -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- log4j2 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <!--以下倆是工具類-->
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.2</version>
        </dependency>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--時間的依賴-->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.10.10</version>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>org.springframework.boot</groupId>-->
<!--            <artifactId>spring-boot-starter-security</artifactId>-->
<!--        </dependency>-->
    </dependencies>
</project>

25.1.2 application.properties:

#server
server.port= 8889
spring.application.name=mszlu_admin_blog
# datasource
spring.datasource.url=jdbc:mysql://localhost:3306/blog1?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

#mybatis-plus
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.global-config.db-config.table-prefix=ms_

#指定mapper文件的位置
mybatis-plus.mapper-locations=classpath:mapper/*.xml

25.1.3 mybatis-plus配置:

com.jihu.blog.admin.config.MybatisPlusConfig

package com.jihu.blog.admin.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
//掃包,將此包下的接口生成代理實現類,並且注冊到spring容器中
@MapperScan("com.jihu.blog.admin.mapper")
public class MybatisPlusConfig {
    //分頁插件
    @Bean
    public  MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }
}

25.1.4 啟動類

package com.jihu.blog.admin;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AdminApp {

    public static void main(String[] args) {
        SpringApplication.run(AdminApp.class,args);
    }
}

25.1.5導入前端工程

放入resources下的static目錄中

25.1.6新建表

后台管理用戶表

CREATE TABLE `blog`.`ms_admin`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

權限表

CREATE TABLE `blog`.`ms_permission`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  `path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

用戶和權限的關聯表

CREATE TABLE `blog`.`ms_admin_permission`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `admin_id` bigint(0) NOT NULL,
  `permission_id` bigint(0) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

權限管理頁面展示

25.1.7AdminController

package com.jihu.blog.admin.controller;

import com.jihu.blog.admin.model.params.PageParam;
import com.jihu.blog.admin.pojo.Permission;
import com.jihu.blog.admin.service.PermissionService;
import com.jihu.blog.admin.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("admin")
public class AdminController {
    @Autowired
    private PermissionService permissionService;

    @PostMapping("permission/permissionList")
    public Result listpermission(@RequestBody PageParam pageParam){
        return permissionService.listpermission(pageParam);

    }

    @PostMapping("permission/add")
    public Result permissionadd(@RequestBody Permission permission){
        return permissionService.permissionadd(permission);
    }

    @PostMapping("permission/update")
    public Result permissionupdate(@RequestBody Permission permission){
        return permissionService.permissionupdate(permission);
    }

    @GetMapping("permission/delete/{id}")
    public Result permissiondeleteById(@PathVariable("id") Long id){
        return permissionService.permissiondeleteById(id);
    }
}

25.1.8 新建Permission

com.jihu.blog.admin.pojo.Permission

package com.jihu.blog.admin.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

@Data
public class Permission {
    @TableId(type =IdType.AUTO)
    private Long id;

    private String name;

    private String path;

    private String description;
}

25.1.9 新建PageParam

com.jihu.blog.admin.model.params.PageParam

package com.jihu.blog.admin.model.params;
import lombok.Data;
@Data
public class PageParam {
    //當前頁
    private Integer currentPage;
    //頁面個數
    private Integer pageSize;
    //查詢條件
    private String queryString;
}

25.1.10 新建PageResult

package com.jihu.blog.admin.vo;
import lombok.Data;
import java.util.List;
@Data
public class PageResult<T> {
    private List<T> list;
    private Long total;
}

25.1.11 新建Result

package com.jihu.blog.admin.vo;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Result {
    private boolean success;
    private int code ;
    private String msg;
    private  Object data;

    public static Result success(Object data) {

        return new Result(true, 200, "success", data);
    }

    public static Result fail(int code,String msg) {
        return new Result(false, code, msg, null);
    }
}

25.1.12新建PermissionService

package com.jihu.blog.admin.service;

import com.jihu.blog.admin.model.params.PageParam;
import com.jihu.blog.admin.pojo.Permission;
import com.jihu.blog.admin.vo.Result;

public interface PermissionService {
    /**
     * 查詢所有權限列表
     * @param pageParam
     * @return
     */
    Result listpermission(PageParam pageParam);

    /**
     * 添加權限
     * @param permission
     * @return
     */
    Result permissionadd(Permission permission);

    /**
     * 更新權限
     * @param permission
     * @return
     */
    Result permissionupdate(Permission permission);

    /**
     * 刪除權限
     * @param id
     * @return
     */
    Result permissiondeleteById(Long id);
}

25.1.13新建PermissionServiceImpl

package com.jihu.blog.admin.service.Impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jihu.blog.admin.mapper.Permissionmapper;
import com.jihu.blog.admin.model.params.PageParam;
import com.jihu.blog.admin.pojo.Permission;
import com.jihu.blog.admin.service.PermissionService;
import com.jihu.blog.admin.vo.PageResult;
import com.jihu.blog.admin.vo.Result;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PermissionServiceImpl implements PermissionService {

    @Autowired
    private Permissionmapper permissionmapper;

    @Override
    public Result listpermission(PageParam pageParam) {
        /**
         * 要的數據,管理台,表的所有的字段 permission
         *  分頁查詢
         */
        Page<Permission> page = new Page<>(pageParam.getCurrentPage(),pageParam.getPageSize());
        LambdaQueryWrapper<Permission> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        if (StringUtils.isNoneBlank(pageParam.getQueryString())){
            lambdaQueryWrapper.eq(Permission::getName,pageParam.getQueryString());

        }

        Page<Permission> permissionPage = permissionmapper.selectPage(page, lambdaQueryWrapper);
        PageResult<Permission> pageResult = new PageResult<>();
        pageResult.setList(permissionPage.getRecords());
        pageResult.setTotal(permissionPage.getTotal());

        return Result.success(pageResult);
    }

    @Override
    public Result permissionadd(Permission permission) {
        int result = permissionmapper.insert(permission);
        return Result.success(result);
    }


    @Override
    public Result permissionupdate(Permission permission) {
        int result = permissionmapper.updateById(permission);
        return Result.success(result);
    }

    @Override
    public Result permissiondeleteById(Long id) {
        int result = permissionmapper.deleteById(id);
        return Result.success(result);
    }
}

25.1.14測試:

image-20220409142701503

26、Security集成

26.1添加依賴

		 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

26.2 配置

package com.jihu.blog.admin.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return  new BCryptPasswordEncoder();
    }

    public static void main(String[] args) {
        //加密策略 MD5 不安全 彩虹表  MD5 加鹽
        String mszlu = new BCryptPasswordEncoder().encode("mszlu");
        System.out.println(mszlu);
    }
    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests() //開啟登錄認證
//                .antMatchers("/user/findAll").hasRole("admin") //訪問接口需要admin的角色
                .antMatchers("/css/**").permitAll()
                .antMatchers("/img/**").permitAll()
                .antMatchers("/js/**").permitAll()
                .antMatchers("/plugins/**").permitAll()
                .antMatchers("/admin/**").access("@authService.auth(request,authentication)") //自定義service 來去實現實時的權限認證
                .antMatchers("/pages/**").authenticated()
                .and().formLogin()
                .loginPage("/login.html") //自定義的登錄頁面
                .loginProcessingUrl("/login") //登錄處理接口
                .usernameParameter("username") //定義登錄時的用戶名的key 默認為username
                .passwordParameter("password") //定義登錄時的密碼key,默認是password
                .defaultSuccessUrl("/pages/main.html")
                .failureUrl("/login.html")
                .permitAll() //通過 不攔截,更加前面配的路徑決定,這是指和登錄表單相關的接口 都通過
                .and().logout() //退出登錄配置
                .logoutUrl("/logout") //退出登錄接口
                .logoutSuccessUrl("/login.html")
                .permitAll() //退出登錄的接口放行
                .and()
                .httpBasic()
                .and()
                .csrf().disable() //csrf關閉 如果自定義登錄 需要關閉
                .headers().frameOptions().sameOrigin(); //支持iframe 頁面嵌套
    }
}

26.3登錄認證

Admin

package com.jihu.blog.admin.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

@Data
public class Admin {
    @TableId(type = IdType.AUTO)
    private Long id;

    private String username;

    private String password;
}

SecurityUserService

package com.jihu.blog.admin.service;

import com.jihu.blog.admin.pojo.Admin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
@Component
public class SecurityUserService implements UserDetailsService {

    @Autowired
    private  AdminServilce adminServilce;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //登錄的時候,把username傳遞到這里
        //通過username查詢 admin表,如果admin存在 將密碼告訴spring security
        //如果不存在  返回null  認證失敗
        Admin admin = adminServilce.findAdminByUsername(username);
        if (admin == null){
            //登錄失敗
            return  null;
        }
        UserDetails userDetails = new User(username,admin.getPassword(),new ArrayList<>());
        //剩下的認證 就由框架幫我們完成
        return userDetails;
    }

    public static void main(String[] args) {
        System.out.println(new BCryptPasswordEncoder().encode("123456"));
    }
}

AdminServilce

package com.jihu.blog.admin.service;

import com.jihu.blog.admin.pojo.Admin;
import com.jihu.blog.admin.pojo.Permission;

import java.util.List;

public interface AdminServilce {

    Admin findAdminByUsername(String username);

    /**
     * 根據用戶id查詢用戶的權限
     * @param id
     * @return
     */
    List<Permission> findPermissionsByAdminId(Long id);
}

AdminServilceImpl

package com.jihu.blog.admin.service.Impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jihu.blog.admin.mapper.AdminMapper;
import com.jihu.blog.admin.mapper.Permissionmapper;
import com.jihu.blog.admin.pojo.Admin;
import com.jihu.blog.admin.pojo.Permission;
import com.jihu.blog.admin.service.AdminServilce;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class AdminServilceImpl implements AdminServilce {
    @Autowired
    private AdminMapper adminMapper;
    @Autowired
    private Permissionmapper permissionmapper;

    @Override
    public Admin findAdminByUsername(String username){
        LambdaQueryWrapper<Admin> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Admin::getUsername,username);
        queryWrapper.last("limit 1");
        Admin admin = adminMapper.selectOne(queryWrapper);
        return admin;
    }

    @Override
    public List<Permission> findPermissionsByAdminId(Long id) {
        return permissionmapper.findPermissionsByAdminId(id);

    }
}

AdminMapper

package com.jihu.blog.admin.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.admin.pojo.Admin;

public interface AdminMapper  extends BaseMapper<Admin> {
}

Permissionmapper

package com.jihu.blog.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihu.blog.admin.pojo.Permission;
import java.util.List;

public interface Permissionmapper extends BaseMapper<Permission> {
    List<Permission> findPermissionsByAdminId(Long adminId);
}

26.4 權限認證

AuthService

package com.jihu.blog.admin.service;

import com.jihu.blog.admin.pojo.Admin;
import com.jihu.blog.admin.pojo.Permission;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

@Service
@Slf4j
public class AuthService {
    @Autowired
    private AdminServilce adminServilce;

    public  boolean auth(HttpServletRequest request, Authentication authentication){

        //權限認證
        //請求路徑
        String requestURI = request.getRequestURI();
        log.info("request url:{}", requestURI);
        Object principal = authentication.getPrincipal();
        //true代表放行 false 代表攔截
        if (principal == null ||"anonymousUser".equals(principal)){
            //未登錄
            return  false;
        }
        UserDetails userDetails = (UserDetails) principal;
        String username = userDetails.getUsername();
        Admin admin = adminServilce.findAdminByUsername(username);
        if (admin == null){
            return false;
        }
        if (admin.getId() == 1){
            //認為是超級管理員
            return true;
        }
        List<Permission> permissions = adminServilce.findPermissionsByAdminId(admin.getId());
        requestURI = StringUtils.split(requestURI,'?')[0];
        for (Permission permission : permissions) {
            if (requestURI.equals(permission.getPath())){
                //代表有權限
                return true;
            }
        }
        return false;
    }

}

27.作業

添加角色,用戶擁有多個角色,一個角色擁有多個權限

28.總結技術亮點

1、jwt + redis

token令牌的登錄方式,訪問認證速度快,session共享,安全性

redis做了令牌和用戶信息的對應管理,

1,進一步增加了安全性

2、登錄用戶做了緩存

3、靈活控制用戶的過期(續期,踢掉線等)

2、threadLocal使用了保存用戶信息,請求的線程之內,可以隨時獲取登錄的用戶,做了線程隔離

3、在使用完ThreadLocal之后,做了value的刪除,防止了內存泄漏(這面試說強引用。弱引用。不是明擺着讓面試官間JVM嘛)

4·、線程安全-update table set value = newValue where id=1 and value=oldValue

5、線程池應用非常廣,面試7個核心參數(對當前的主業務流程無影響的操作,放入線程池執行)

1.登錄,記錄日志

6·權限系統重點內容

7·統一日志記錄,統一緩存處理


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM