IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 基于springboot+vue3知识库系统开发 -> 正文阅读

[Java知识库]基于springboot+vue3知识库系统开发

知识库系统开发实战

页面展示

1 首页

在这里插入图片描述在这里插入图片描述

2 登录页面

在这里插入图片描述登录成功后首页面:
在这里插入图片描述

3 用户管理页面

在这里插入图片描述在这里插入图片描述编辑:
在这里插入图片描述在这里插入图片描述

4 电子书管理页面

在这里插入图片描述文档管理:
在这里插入图片描述编辑:
在这里插入图片描述在这里插入图片描述

5 分类管理页面

在这里插入图片描述在这里插入图片描述在这里插入图片描述

6 关于我页面

在这里插入图片描述在这里插入图片描述退出登录:
在这里插入图片描述

一、springboot项目搭建

完成项目后端工程配置。

提示一下因为要用VUE3前后端分离,所以resources目录下的static、templates包都不会用,可以直接删了。

1.1 项目初始化配置

1.配置编码

请添加图片描述

2.设置jdk为1.8

3.配置maven仓库位置和maven的settings文件。

4.配置git。

1.2 代码关联Git远程仓库

? 1.在远程仓库上创建一个新项目

? 2.将本地项目关联到远程仓库

1.3 启动日志优化

1.logback日志样式修改

? 在resources目录下,添加logback-spring.xml文件。修改日志样式。

2.增加启动成功日志

? 修改主启动类

@SpringBootApplication
public class SnowXueApplication {

    private static final Logger LOG = LoggerFactory.getLogger(SnowXueApplication.class);

    public static void main(String[] args) {
//        SpringApplication.run(WikiApplication.class, args);
        SpringApplication app = new SpringApplication(SnowXueApplication.class);
        Environment env = app.run(args).getEnvironment();
        LOG.info("=====================启动成功=======================");
        LOG.info("地址:http://127.0.0.1:{}",env.getProperty("server.port"));
    }

}

3.修改启动图案

? 在resources添加banner.txt

1.4 使用HTTP Client测试接口

? 一般测试接口会使用postman,也可以用idea自带的HTTP Client。

做法:

? 1)新建http包

? 2)在http包下创建一个文件,命名随意,但后缀必须为http,例如test.http

? 3)在该文件中输入(gtr)

GET http://localhost:8080/hello
Accept: application/json

###

? 4)运行主启动类

? 5)点击test.http里面的三角形,就可以进行测试

1.5 集成热部署

? 1)添加依赖

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

? 2)

请添加图片描述

? 3)按两次shift,输入registry
请添加图片描述

请添加图片描述

二、完善后端架构

2.1 数据库配置

1.IDEA配置数据库连接

请添加图片描述

? 作用:可以在IDEA中直接操作数据库

请添加图片描述

? 2)添加sql脚本(以后就将sql脚本写入里面)

? 如何执行sql脚本?选择sql脚本,右键,点击exceute。就可

2.2 集成MyBatis

1.添加依赖

<!-- 集成mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>
<!-- 集成mysql连接 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.22</version>
</dependency>

2.配置数据源

spring:
  datasource:
  # 如果没有配置时区这些信息,可能会报错,注意一下
    url: jdbc:mysql://localhost:3306/snow_xue?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root

3.创建pojo包,里面放表对应的实体类

4.创建mapper包,创建实体类对应的mapper接口

5.在resources下创建mapper包,放mapper.xml文件

6.在主启动类上加上@MapperScan注解

7.配置mapper.xml文件的位置

mybatis:
  mapper-locations: classpath:/mapper/**/*.xml

2.3 Mybatis-Generator

1.添加插件

<!-- mybatis generator 自动生成代码插件 -->
<plugin>
    <groupId>org.mybatis.generator</groupId>
    <artifactId>mybatis-generator-maven-plugin</artifactId>
    <version>1.4.0</version>
    <configuration>
        <configurationFile>src/main/resources/generator/generator-config.xml</configurationFile>
        <overwrite>true</overwrite>
        <verbose>true</verbose>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.22</version>
        </dependency>
    </dependencies>
</plugin>

2.添加generator-config.xml文件,这个别背,网络上有,改改就可以了

3.增加代码生成器对应的启动命令

请添加图片描述

4.点击按钮如下

请添加图片描述

2.4 电子书列表查询接口开发

1.准备数据库表

create table `ebook` (
             `id` bigint not null comment 'id',
             `name` varchar(50) comment '名称',
             `category1_id` bigint comment '分类1',
             `category2_id` bigint comment '分类2',
             `description` varchar(200) comment '描述',
             `cover` varchar(200) comment '封面',
             `doc_count` int comment '文档数',
             `view_count` int comment '阅读数',
             `vote_count` int comment '点赞数',
             primary key (`id`)
) engine = innodb default charset = utf8mb4 comment '电子书';

insert into `ebook`(id,name,description) values (1,'Java核心技术','如果你想正式、系统地学习Java,并打算将Java应用到实际工程项目中,那么这本《Java核心技术》必不可少。');
insert into `ebook`(id,name,description) values (2,'Java语言程序设计','java黑皮书被世界各地的大学选作教材,全球畅销20余年。');
insert into `ebook`(id,name,description) values (3,'Java编程思想','带大家去真正感受到Java语言的灵魂思想。');
insert into `ebook`(id,name,description) values (4,'Effective Java中文版','是Java开发人员案头上的一本不可或缺的参考书。');
insert into `ebook`(id,name,description) values (5,'Java并发编程实战','Java并发编程里程碑著作!从并发编程的基本理论入手,逐步介绍了在设计Java并发程序时各种重要的设计原则、设计模式以及思维模式。');
insert into `ebook`(id,name,description) values (6,'深入了解Java虚拟机','帮助国内数十万Java开发工程师和架构师加深了对JVM的认知,凭借一己之力拉高了 Java 开发者内功水平,把 JVM 带到了初级面试题环节。');

2.修改generator-config.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <!--    配置mysql驱动-->
    <classPathEntry location="E:\mysql\mysql-connector-java-5.1.6.jar"/>

    <context id="DB2Tables" targetRuntime="MyBatis3">

        <!--        不再追加xml内容-->
        <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin"/>

        <commentGenerator>
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>
        <!--连接数据库-->
        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://127.0.0.1:3306/snow_xue?serverTimezone=UTC"
                        userId="root"
                        password="root">
        </jdbcConnection>

        <javaTypeResolver>
            <property name="forceBigDecimals" value="false"/>
        </javaTypeResolver>
        <!--java模型创建器,即创建实体类-->
        <javaModelGenerator targetPackage="com.snow.snow_xue.pojo" targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
            <!--            <property name="trimStrings" value="true" />-->
        </javaModelGenerator>
        <!--mapper.xml文件生成器-->
        <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>
        <!--dao接口生成器-->
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.snow.snow_xue.mapper" targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>
        <!--选择一个或多个table来生成相关文件
        domainObjectName:生成domain类的名称,如果不设置默认用表名驼峰形式
        enableXXX:设置为false,表明不把将XXX不弄出来
        -->
<!--        <table tableName="test" domainObjectName="Test" enableCountByExample="false"-->
<!--               enableDeleteByExample="false" enableSelectByExample="false" enableUpdateByExample="false"/>-->
        <table tableName="ebook" domainObjectName="Ebook"/>

    </context>
</generatorConfiguration>

**注意:**我做了注释的地方,基本上都需要根据自己项目的情况进行修改。

对于想要彻底了解里面的代码的含义,可以看我的另一篇文章:

3.创建Ebook(电子书)对应的service类和接口类

4.创建后端传给前端的数据类(格式化返回类型—与前端数据格式保持一致)

@Data
public class CommonResp<T> {
    /**
     * 业务上的成功或失败
     */
    private boolean success = true;

    /**
     * 返回信息
     */
    private String message;

    /**
     * 返回泛型数据,自定义类型
     */
    private T content;

}

2.5 封装请求参数和返回参数

目前封装请求参数,就是重新写一个类里面只有id和name(感觉有点鸡肋,后面可能会改)。

封装返回参数,就是完全把Ebook复制一份,改名为EbookResp(还是感觉鸡肋,不知道有啥用,好复杂)

2.6 制作CopyUtil封装BeanUtils

public class CopyUtil {

    /**
     * 单体复制
     将source对象转换成clazz类对象
     */
    public static <T> T copy(Object source, Class<T> clazz) {
        if (source == null) {
            return null;
        }
        T obj = null;
        try {
            obj = clazz.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        BeanUtils.copyProperties(source, obj);
        return obj;
    }

    /**
     * 列表复制
     将source列表中元素的类型转换成clazz类型
     */
    public static <T> List<T> copyList(List source, Class<T> clazz) {
        List<T> target = new ArrayList<>();
        if (!CollectionUtils.isEmpty(source)){
            for (Object c: source) {
                T obj = copy(c, clazz);
                target.add(obj);
            }
        }
        return target;
    }
}

BeanUtils:从一个对象拷贝属性到另一个对象中(只拷贝属性相同的字段)

而制作CopyUtil是为了让项目代码开发更简洁。

三、Vue3+Vue CLI项目搭建

3.1 Vue和Vue CLI简介

vue.js和jquery.js使用方法有点像,只需要引入vue.js就可以使用了。但是jquery用起来更像是一个工具,而vue却是一个框架。

? 工具与框架的区别:工具可以简单的理解为工具提供了一堆的东西,项目中可以直接使用这些方法;框架是指它搭了一个架子,我们写的代码是给这个框架来用的。

什么时候用vue.js?

? 一般用在现有的项目里面,jsp、thymeleaf等老项目。这时候想用vue,就可以用vue.js。直接将vue.js引入进来就可以使用vue的代码了。

Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统。(Vue CLI=Vue.js+一堆插件)

3.2 创建Vue CLI项目

1.Vue CLI安装:

? 1)安装Node.js(为了使用nom命令)记得把镜像改成淘宝镜像

? 2)使用下列命令安装vue CLI

? npm install -g @vue/cli@4.5.9

? vue --version查看版本号是不是4.5.9

2.创建web应用:

运行以下命令来创建一个新项目:

? vue create web(注意:在执行这条命令时,要保证是在你的项目路径下)

? 选择Manually select features—>TypeScript+Router+Vuex按空格选中—>回车–>选择3.x---->nny+ESLint(用于检查代码规范,双刃剑)选择ESLint with error prevention only—>Lint on save(什么时候触发检查)—>In dedicated config files(router等配置是放在单独的文件还是一起放到package.json)—>save this as a preset for future projects(是否将上面配置保存成模板)y—>save preset as(起文件名)

? 看到下面的界面就表示web应用创建好了。

请添加图片描述

? 会看到项目目录中多了一个web目录

3.启动web应用:

使用cd web 和npm run serve启动web应用。看到下面的界面说明启动成功

请添加图片描述

点击Local对应的网址就可以看到本地的vue界面了

Idea提供了启动Vue CLI项目的简单方法,打开web目录,找到package.json,右键鼠标选择Show npm Scripts。这样就可以出现一个窗口,点击就可以进行对应的操作

请添加图片描述

以后启动的话,只要双击serve就可以了

3.2 Vue CLI项目结构

请添加图片描述

请添加图片描述

Vue CLI初始执行main.ts,将内容页App.vue渲染到index.html,完成页面显示

Vue CLI需要编译才能发布。(在npm选择build双击一下),编译成功后会在web目录下多一个包叫dist。之后提交的就是dist包中的文件。

3.4 集成Ant Design Vue

创建的Vue CLI项目其实就是一堆组件,有些是能看见的有些看不见。看得见的比如views包下的页面,看不到的:router、store…

Vue CLI的UI界面怎么做?

? 方法一:基于原生的html css js

? 方法二:基于第三方css库,比如bootstrap。bootstrap有些组件要求引入jquery

? 方法三:基于Vue的UI组件,比较知名的就是饿了么的Element UI,但是到目前为止,还不支持Vue 3。因此本项目选择使用Ant Design Vue组件(各种Vue UI组件的用法是想通的,学会一个就都会了)

Ant Design Vue官网:https://2x.antdv.com/docs/vue/introduce

安装Ant Design Vue:

? (需要先进入web路径)输入命令:npm install ant-design-vue@2.2.0 --save

引入Ant Design Vue的两种方法:

? 1)按需加载

? 实例:

import { DatePicker } from "ant-design-vue";
app.use(DatePicker);

引入样式:
import "ant-design-vue/dist/antd.css"; // or 'ant-design-vue/dist/antd.less'

? 2)完整引入:需要修改web目录下的main.ts

请添加图片描述

组件库:https://ant.design/components/button-cn/

案例:查看button组件,选择一个按钮,将其代码拷贝下来,添加到Home.vue中

请添加图片描述

启动项目,查看页面

请添加图片描述

3.5 网站首页布局开发

进入:https://2x.antdv.com/components/layout-cn/#components-layout-demo-top-side-2 选择一个合适的layout布局,将代码赋值到App.vue文件中。将中的css代码进行替换。

3.6 制作Vue自定义组件

在components包下创建the-header.vue

四、前后端交互整合

4.1 集成HTTP库Axios

axios时目前最流行的ajax封装库之一,用于很方便地实现ajax请求的发送。

支持的功能:

  • 从浏览器发出 XMLHttpRequests请求。
  • 从 node.js 发出 http 请求。
  • 支持 Promise API。
  • 能拦截请求和响应。
  • 能转换请求和响应数据。
  • 取消请求。
  • 实现JSON数据的自动转换。
  • 客户端支持防止 XSRF攻击。

**步骤1:**在命令行窗口输入:npm install axios@0.21.0 -save

请添加图片描述

**步骤2:**使用axios库

? 进入web/views/Home.vue(我们要在这个页面把电子书的数据从后端拿出来)

请添加图片描述

? 注意:这样会报错,“No Aceess-Control-Allow-Origin”:前后端分离常见的跨域报错。跨域可以这样理解,来自一个IP端口的页面(vue项目),要访问另一个IP端口的字眼(springboot请求接口),会产生跨域访问。

请添加图片描述

**步骤3:**解决跨域问题

? 在后端代码的config包下加CorsConfig类

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedHeaders(CorsConfiguration.ALL)
                .allowedMethods(CorsConfiguration.ALL)
                .allowCredentials(true)
                .maxAge(3600); // 1小时内不需要再预检(发OPTIONS请求)
    }

}

4.2 Vue3数据绑定显示列表数据

数据绑定是vue的核心功能,前端拿到后端的数据之后,要把它显示到页面上。

vue3使用setup方法进行数据绑定。可以使用{{xxx}}来获取变量。

方法一:使用ref进行数据绑定。进入Home.vue界面

方法二:使用reactive

<template>
    <a-layout>
      <a-layout-sider width="200" style="background: #fff">
        <a-menu
            mode="inline"
            style="height: 100%"
        >
          <a-sub-menu key="sub1">
            <template #title>
                <span>
                  <user-outlined />
                  subnav 1
                </span>
            </template>
            <a-menu-item key="1">option1</a-menu-item>
            <a-menu-item key="2">option2</a-menu-item>
            <a-menu-item key="3">option3</a-menu-item>
            <a-menu-item key="4">option4</a-menu-item>
          </a-sub-menu>
          <a-sub-menu key="sub2">
            <template #title>
                <span>
                  <laptop-outlined />
                  subnav 2
                </span>
            </template>
            <a-menu-item key="5">option5</a-menu-item>
            <a-menu-item key="6">option6</a-menu-item>
            <a-menu-item key="7">option7</a-menu-item>
            <a-menu-item key="8">option8</a-menu-item>
          </a-sub-menu>
          <a-sub-menu key="sub3">
            <template #title>
                <span>
                  <notification-outlined />
                  subnav 3
                </span>
            </template>
            <a-menu-item key="9">option9</a-menu-item>
            <a-menu-item key="10">option10</a-menu-item>
            <a-menu-item key="11">option11</a-menu-item>
            <a-menu-item key="12">option12</a-menu-item>
          </a-sub-menu>
        </a-menu>
      </a-layout-sider>
      <a-layout-content :style="{ padding: '0 24px', minHeight: '280px' }">
        <!--pre标签就是会把里面长什么样子,原封不动的全部给你展示到页面上。-->
        <pre>
{{ ebooks }}
{{ebooks2}}
        </pre>
      </a-layout-content>
    </a-layout>
</template>

<script lang="ts">
import { defineComponent , onMounted, ref, reactive, toRef} from 'vue';
import axios from 'axios';

export default defineComponent({
  name: 'Home',
  // 这个方法是vue3新增的
  setup(){
    console.log("setup");
    // 他是一个响应式的数据。所谓的响应式数据就是说在js里面,动态修改这里面的值
    // 它需要实时反馈到页面上去。用这个ref就可以让它变成一个响应式的数据
    const  ebooks=ref();
    // reactive里面一般是放一个对象(这里放一个空对象)
    // 瘫痪我们拿到这个值以后,要往这个对象里面的,在这个空对象里面添加一个books属性
    // 它对应的值就放一个空数组,就是一个json对象
    const  ebooks2=reactive({books:[]});
    // 舒适化的逻辑都写到onMounted方法里,setup就放一些参数定义、方法定义
    // 因为setup执行的时候界面还没有渲染好,这时候如果去操作界面元素会报错
    onMounted(()=>{
      console.log("onMounted");
      axios.get("http://localhost:8880/ebook/list?name=java").then((response)=>{
        // data=后端的CommonResp
        const data=response.data;
        ebooks2.books=data.content;
        ebooks.value=data.content;
        console.log(response);
    });

    });
    return {
      ebooks,
      // toRef是vue新增内置的
      ebooks2:toRef(ebooks2,"books")
    }
  }
});
</script>

本项目之后会统一会ref,用ref比较麻烦的一点,就是使用变量的话,就都要加一个.value

4.3 电子书列表界面展示

**步骤1:**找Ant Design Vue现成的组件

https://2x.antdv.com/components/list-cn

**步骤2:**将列表数据按组件样式显示到界面上

这里也是参考VUE 组件样式代码进行修改

需要安装图标库:npm install @ant-design/icons-vue@5.1.8 --save

将图标库一次性导入进来,进入main.ts加入:import * as Icons from ‘@ant-design/icons-vue’;和

// 全局使用图标
const icons: any = Icons;
for (const i in icons) {
  app.component(i, icons[i]);
}

修改后端EbookService的list方法,将查询改为动态SQL

public List<EbookResp> list(EbookReq req){
    EbookExample ebookExample=new EbookExample();
    EbookExample.Criteria criteria=ebookExample.createCriteria();
    if(!ObjectUtils.isEmpty(req.getName())){
        criteria.andNameLike("%"+req.getName()+"%");
    }
    List<Ebook> ebookList=ebookMapper.selectByExample(ebookExample);
    List<EbookResp> ebookRespList=new ArrayList<>();
    List<EbookResp> list=CopyUtil.copyList(ebookList,EbookResp.class);
    return list;
}

调整样式的简单方法,打开页面检查页面,点击要调整的样式地方,修改参数。然后把修改后的改到代码中去。

4.4 Vue CLI多环境配置

**步骤1:**增加开发和生成配置文件

目前前端去访问后端的话,请求地址是写死的,但是后面发布到生产的话,前端和后端它不一定在同一台机子上。

请添加图片描述

为了解决这个问题就是增加不同环境的配置。

Vue的话只需要在web目录下,新建一个文件,文件的名字,有.env开头后面加上环境的名称。
请添加图片描述

**步骤2:**修改编译和启动命令让其支持多环境

? 修改package.json
请添加图片描述

? 修改之后:

请添加图片描述

? 刷新一下:

请添加图片描述

**步骤3:**修改axios请求地址支持多环境

请添加图片描述

? but这样需要在每个需要请求的地方写一段有点麻烦,axios提供了全局配置。只需要在main.ts进行配置即可。
请添加图片描述

? 这样的话上面的代码就可以改成:

请添加图片描述

4.5 使用axios拦截器打印前端日志

配置axios拦截器打印请求参数和返回参数。

使用main.ts进行相关的配置。(这段代码很固定,以后可直接用)

/**
 * axios拦截器
 */
axios.interceptors.request.use(function (config) {
    console.log('请求参数:', config);
    return config;
}, error => {
    return Promise.reject(error);
});
axios.interceptors.response.use(function (response) {
    console.log('返回结果:', response);
    return response;
}, error => {
    console.log('返回错误:', error);
    return Promise.reject(error);
});

? 这个是axios拦截器最简单的用法,后面会进行拓展。比如可以放一些token用来验证登录的一些信息。

? 启动项目,看看页面

请添加图片描述

请添加图片描述

4.6 SpringBoot和Web组件的使用

4.6.1 过滤器的使用

配置过滤器,打印接口耗时(接口耗时在应用监控里面,是一个非常重要的监控点。可以看出来应用的处理能力)

过滤器其实是servlet的一个概念。servlet它又是容器的一个概念。因此过滤器是给容器用的。所谓容器,就是例如tomcat,netty。所以我们写的过滤器其实是给tomcat和netty使用的。(过滤器代码也很固定)

@Component
public class LogFilter implements Filter {

    private static final Logger LOG = LoggerFactory.getLogger(LogFilter.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // 打印请求信息
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        LOG.info("------------- LogFilter 开始 -------------");
        LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
        LOG.info("远程地址: {}", request.getRemoteAddr());

        long startTime = System.currentTimeMillis();
        filterChain.doFilter(servletRequest, servletResponse);
        LOG.info("------------- LogFilter 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
    }
}

启动后端,进行测试。

请添加图片描述

4.6.2 拦截器的使用

配置拦截器,打印接口的耗时。

拦截器和过滤器的区别:拦截器前跟后是分成两个方法(preHandle和postHandle)调用业务方法不需要自己写;而过滤器它是整个一起,中间用这个链filterChain.doFilter去调用业务方法。中间的代码跟过滤器差不多。

请添加图片描述

 /**
  * 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印 /login
  */
@Component
public class LogInterceptor implements HandlerInterceptor {

     private static final Logger LOG = LoggerFactory.getLogger(LogInterceptor.class);

     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
         // 打印请求信息
         LOG.info("------------- LogInterceptor 开始 -------------");
         LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
         LOG.info("远程地址: {}", request.getRemoteAddr());

         long startTime = System.currentTimeMillis();
         request.setAttribute("requestStartTime", startTime);
         return true;
     }

     @Override
     public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
         long startTime = (Long) request.getAttribute("requestStartTime");
         LOG.info("------------- LogInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
     }
}

拦截器还需要增加一个配置类

@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
    @Resource
    LogInterceptor logInterceptor;
    
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(logInterceptor).addPathPatterns("/**");
    }
}

测试:

请添加图片描述

4.6.3 AOP的使用

配置AOP,打印接口耗时,请求参数,返回参数。

使用AOP需要添加依赖,如下

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.70</version>
</dependency>
@Aspect
@Component
public class LogAspect {

    private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);

    /** 定义一个切点 */
    @Pointcut("execution(public * com.snow.*.controller..*Controller.*(..))")
    public void controllerPointcut() {}


    @Before("controllerPointcut()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {

        // 开始打印请求日志
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();

        // 打印请求信息
        LOG.info("------------- 开始 -------------");
        LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
        LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
        LOG.info("远程地址: {}", request.getRemoteAddr());


        // 打印请求参数
        Object[] args = joinPoint.getArgs();
      // LOG.info("请求参数: {}", JSONObject.toJSONString(args));

      Object[] arguments  = new Object[args.length];
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof ServletRequest
                    || args[i] instanceof ServletResponse
                    || args[i] instanceof MultipartFile) {
                continue;
            }
            arguments[i] = args[i];
        }
        // 排除字段,敏感字段或太长的字段不显示
        String[] excludeProperties = {"password", "file"};
        PropertyPreFilters filters = new PropertyPreFilters();
        PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
        excludefilter.addExcludes(excludeProperties);
        LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter));
    }

    @Around("controllerPointcut()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();
        // 排除字段,敏感字段或太长的字段不显示
        String[] excludeProperties = {"password", "file"};
        PropertyPreFilters filters = new PropertyPreFilters();
        PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
        excludefilter.addExcludes(excludeProperties);
        LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter));
        LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
        return result;
    }

}

过滤器、拦截器、AOP选择一个使用,本项目使用AOP

五、电子书管理功能开发

5.1 增加电子书管理页面

1.增加电子书页面

? 一般增加页面,是放到web/views下,因为这里做的是管理页面,需要用户登录后才能使用,所以要和其他页面进行区分,因此在下面创建admin包,这个包都是我们管理的页面。

? 创建admin-ebook.vue页面

2.增加电子书菜单(点击某个菜单才会跳到这个页面)

? 在the-header.vue中增加

<a-menu-item key="/">
  <router-link to="/">首页</router-link>
</a-menu-item>
<a-menu-item key="/admin/ebook" >
  <router-link to="/admin/ebook">电子书管理</router-link>
</a-menu-item>
<a-menu-item key="/about">
  <router-link to="/about">关于我们</router-link>
</a-menu-item>

3.增加电子书路由(路由跟页面是绑定起来的)

? 在index.ts中增加

import Home from '../views/home.vue'
import About from '../views/about.vue'
import AdminEbook from '../views/admin/admin-ebook.vue'
const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },{
    path: '/admin/ebook',
    name: 'AdminEbook',
    component: AdminEbook
  }
]

5.2 电子书表格展示

5.3 使用PageHelper实现后端分页

**步骤1:**集成PageHelper插件

? 加入依赖

<!-- pagehelper 插件-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.13</version>
</dependency>

**步骤2:**修改电子书列表接口,支持分页(假分页数据)

实现很简单,

请添加图片描述

? 注意:1)pagehelper分页是从第一页开始,不是0;2)pagehelper只对第一次查询有效,之后就会全部查出来。因此建议将PageHelper与查询语句写在一起。

请添加图片描述

5.4 封装分页请求参数和返回参数

1.请求参数封装,PageReq

@Data
public class PageReq {
    private int page;
    private int size;
}

让EbookReq继承PageReq,然后修改EbookService中PageHelper.startPage中的参数,改为动态的。

2.返回结果封装,PageResp

@Data
public class PageResp<T> {
    private long total;
    private List<T> list;
}

这里controller和service都做了一定的改变。

5.5 前后端分页功能整合

1.前端修改列表查询分页参数

请添加图片描述

请添加图片描述

2.前端修改接收列表查询结果

? 列表查询从data.content变成data.content.list

3.电子书管理页面和首页都需要改

请添加图片描述

5.6 制作电子书表单

点击每一行编辑按钮,弹出编辑框

? https://2x.antdv.com/components/modal-cn#components-modal-demo-async

? 选择异步关闭。
请添加图片描述

编辑框显示电子书表单

https://2x.antdv.com/components/form-cn

5.7 电子书编辑功能

1.增加后端保存接口

controller:

@PostMapping("/save")
    // @RequestBody对应的就是json方式的post提交,必须加这个要不然报错
    // 如果是form表单方式的提交就不需要加注解
    public CommonResp save(@RequestBody EbookSaveReq req) {
        CommonResp resp = new CommonResp<>();
        ebookService.save(req);
        return resp;
    }

查看一下前端控制台,会发现它的content-type是application-json所以这里必须加这个注解。

请添加图片描述

service:

/*
    保存:判断是新增保存,还是更新保存
 */
public void save(EbookSaveReq req) {
    // 将请求参数变成对象实体
    // CopyUtil将一个对象的全部(或部分)属性值copy给另一个对象
    Ebook ebook=CopyUtil.copy(req,Ebook.class);
    if (ObjectUtils.isEmpty(req.getId())) {
        // 新增
        ebookMapper.insert(ebook);
    } else {
        // 更新
        ebookMapper.updateByPrimaryKey(ebook);
    }
}

2.点击保存时,调用保存接口
请添加图片描述

3.保存成功刷新列表

// -------- 表单 ---------
const ebook = ref();
const modalVisible = ref(false);
const modalLoading = ref(false);
const handleModalOk = () => {
  // 点击保存时,先显示一个loading的效果
  modalLoading.value = true;
  axios.post("/ebook/save", ebook.value).then((response)=>{
    const data=response.data; // data=CommonResp
    if(data.success){// 表示保存成功
      // modal框去掉
      modalVisible.value=false;
      // 拿到结果后把loading效果去掉
      modalLoading.value=false;

      // 重新加载列表
      handleQuery({
        page: pagination.value.current,
        size: pagination.value.pageSize
      });
    }
  });
};

5.8 雪花算法与新增功能

1.时间戳概念:

*时间戳*是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。

**2.雪花算法工具类:**用于生成电子书id(Twitter的分布式自增ID雪花算法)

? 雪花算法=时间戳+一些机器码+递增的序列号

? 雪花算法id由时间戳,数据中心,机器中心,序列号四部分组成。

/**
 * Twitter的分布式自增ID雪花算法
 **/
@Component
public class SnowFlake {

    /**
     * 起始的时间戳
     */
    private final static long START_STMP = 1609459200000L; // 2021-01-01 00:00:00

    /**
     * 每一部分占用的位数
     */
    private final static long SEQUENCE_BIT = 12; //序列号占用的位数
    private final static long MACHINE_BIT = 5;   //机器标识占用的位数
    private final static long DATACENTER_BIT = 5;//数据中心占用的位数

    /**
     * 每一部分的最大值
     */
    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);

    /**
     * 每一部分向左的位移
     */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;

    private long datacenterId = 1;  //数据中心
    private long machineId = 1;     //机器标识
    private long sequence = 0L; //序列号
    private long lastStmp = -1L;//上一次时间戳

    public SnowFlake() {
    }

    public SnowFlake(long datacenterId, long machineId) {
        if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.datacenterId = datacenterId;
        this.machineId = machineId;
    }

    /**
     * 产生下一个ID
     *
     * @return
     */
    public synchronized long nextId() {
        long currStmp = getNewstmp();
        if (currStmp < lastStmp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }

        if (currStmp == lastStmp) {
            //相同毫秒内,序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列数已经达到最大
            if (sequence == 0L) {
                currStmp = getNextMill();
            }
        } else {
            //不同毫秒内,序列号置为0
            sequence = 0L;
        }

        lastStmp = currStmp;

        return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
                | datacenterId << DATACENTER_LEFT       //数据中心部分
                | machineId << MACHINE_LEFT             //机器标识部分
                | sequence;                             //序列号部分
    }

    private long getNextMill() {
        long mill = getNewstmp();
        while (mill <= lastStmp) {
            mill = getNewstmp();
        }
        return mill;
    }

    private long getNewstmp() {
        return System.currentTimeMillis();
    }
}

拓展:id有几种算法,一种是最简单的自增,还有一种uuid,还有就是雪花算法。

3.完成新增功能

? 将雪花算法工具类注入到service中
请添加图片描述

? 前端增加一个新增按钮

请添加图片描述

? 添加add方法。

/**
 * 添加
 */
const add = () => {
	//点击新增按钮弹出模态框
  modalVisible.value = true;
  // 将列表清空
  ebook.value={};
};

请添加图片描述

5.9 删除电子书功能

1.后端增加删除接口

// 这是rest风格的格式
@DeleteMapping("/delete/{id}")
public CommonResp delete(@PathVariable Long id) {
    CommonResp resp = new CommonResp<>();
    ebookService.delete(id);
    return resp;
}
public void delete(Long id) {
    ebookMapper.deleteByPrimaryKey(id);
}

2.前端点击删除按钮时调用后端删除接口

<!--给按钮绑定事件,但是delete是关键字,因此不能取名为delete-->
<a-button type="danger" @click="handleDelete(record.id)">
    删除
</a-button>

3.删除时需要有一个确认框

? 重要的业务操作,如删除、审批等,一定要有确认动作。

? 这里使用vue的气泡确认框

https://2x.antdv.com/components/popconfirm-cn

<a-popconfirm
    title="删除后不可恢复,确认删除?"
    ok-text="是"
    cancel-text="否"
    @confirm="handleDelete(record.id)"
>
  <a-button type="danger">
    删除
  </a-button>
</a-popconfirm>

5.10 集成Validation做参数校验

对电子书查询和保存做参数校验:

* 集成spring-boot-starter-validation
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  • 对保存接口和查询接口做参数校验

? 需要先实体类需要校验的地方加上注解,再在controller中开启校验。

请添加图片描述

请添加图片描述

  • 校验不通过时,前端弹出错误提示

? 对于校验不通过的接口,是不会进入到里面的代码的,因此需要创建统一异常处理类来解决这一问题。

/**
 * 统一异常处理、数据预处理等
 */
@ControllerAdvice
public class ControllerExceptionHandler {

    private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class);

    /**
     * 校验异常统一处理:validation相关的异常
     * @param e
     * @return
     */
    @ExceptionHandler(value = BindException.class)
    @ResponseBody
    public CommonResp validExceptionHandler(BindException e) {
        CommonResp commonResp = new CommonResp();
        LOG.warn("参数校验失败:{}", e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
        commonResp.setSuccess(false);
        commonResp.setMessage(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
        return commonResp;
    }

    /**
     * 校验异常统一处理:所有的异常
     * @param e
     * @return
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public CommonResp validExceptionHandler(Exception e) {
        CommonResp commonResp = new CommonResp();
        LOG.error("系统异常:", e);
        commonResp.setSuccess(false);
        commonResp.setMessage("系统出现异常,请联系管理员");
        return commonResp;
    }
}

? 只要写上这样一个类就ok了。

? 只要都可以根据日志出现的XXXException来做统一异常处理,就是把它填到上面这个类里就ok。

? 我希望前端在出现参数校验失败时能够弹出提示框,因此需要首先引入组件:import { message } from ‘ant-design-vue’; 其实在查询那使用message.error((data.message));

5.11 电子书管理功能优化

1.增加名字查询

https://2x.antdv.com/components/form-cn#components-form-demo-horizontal-login

将前端新增那进行了替换

2.编辑时复制对象

? 为了解决目前出现的,点击编辑的时候不保存直接影响了这个列表。

? 添加tools.ts工具类

? 如何使用这个工具类?

请添加图片描述

六、分类管理功能开发

6.1 分类表设计与代码生成

1.分类表设计:本项目里面分类成两级分类,但是表的设计时支持无限极分类的,其实它就是以一个简单的树形结构。

drop table if exists category;
create table category(
    id bigint not null comment 'id',
    parent bigint not null default 0 comment '父id',
    name varchar(50) not null comment '名称',
    sort int comment '顺序',
    primary key(id)
)engine=innodb default charset=utf8mb4 comment='分类';

2.生成持久层代码

使用Mybatis-Generator,生成分类对应的持久层代码
请添加图片描述

请添加图片描述

6.2 分类基本CURD

这里基本上就是把第五章的controller,service复制了一份,然后把缺失的实体补齐就可。

然后也是把第五章的admin-ebook前端页面复制,进行修改。把ebook,Ebook改成category,Category上面一样。

这里修改修改路由和菜单。

6.3 分类表格显示优化

1.不需要分页,一次查出全部数据

? 设计一个展示全部数据的controller,删除跟分页有关的配置pagination

2.改为树形表格展示

https://2x.antdv.com/components/table-cn#components-table-demo-expand-children

6.4 分类编辑功能优化

编辑(新增/修改)分类时,支持选中某一分类作为父分类,或无父分类。

? 将填写父分类那里从文本框改成下拉框:https://2x.antdv.com/components/select-cn

<a-form-item label="父分类">
  <a-input v-model:value="category.parent" />
  <a-select
      v-model:value="category.parent"
      ref="select"
  >
    <a-select-option :value="0">
      无
    </a-select-option>
    <a-select-option v-for="c in level1" :key="c.id" :value="c.id" :disabled="category.id === c.id">
      {{c.name}}
    </a-select-option>
  </a-select>
</a-form-item>

6.5 电子书管理增加分类选择

电子书管理页面,使用级联选择组件Cascader,选择分类

? https://2x.antdv.com/components/cascader-cn

电子书列表应该显示分类名称,而不是分类ID

请添加图片描述

请添加图片描述

6.6 首页显示分类菜单

https://2x.antdv.com/components/menu-cn#components-menu-demo-vertical

设计思路:

? 1.第一步加载数据变成树形结构

? 2.第二步将菜单做成一个动态循环,循环读这些分类数据,展示出来

这里有一个关于axios异步请求的bug:加载完分类后,再加载电子书,否则如果分类树加载很慢,则电子书渲染会报错。

6.7 点击分类菜单显示电子书

首页默认显示欢迎页面

? 点击欢迎时,显示欢迎组件,点击分类时,显示电子书

点击某分类时,显示该分类下的电子书(受menu组件限制,只有点击二级分类时才能显示电子书)

? 点击分类时,重新查询电子书

? 电子书后端接口增加分类参数

七、文档管理功能开发

7.1 文档表设计与代码生成

1.文档表设计

drop table if exists doc;
create table doc(
    id bigint not null comment 'id',
    ebook_id bigint not null default 0 comment '电子书id',
    parent bigint not null default 0 comment '父id',
    name varchar(50) not null comment '名称',
    sort int comment '顺序',
    view_count int default 0 comment '阅读数',
    vote_count int default 0 comment '点赞数',
    primary key(id)
)engine=innodb default charset=utf8mb4 comment='文档';

2.生成持久层代码

打开代码生成器

7.2 完成文档表CRUD

7.3 使用树形选择组件则兼父节点

编辑表单中的父文档选择框改为树形选择组件,完成编辑功能。

https://2x.antdv.com/components/tree-select-cn
请添加图片描述

7.4 Vue页面参数传递完成新增文档功能

在电子书管理页面点击【文档管理】,跳到文档管理页面时,带上当前电子书id参数ebookid

新增文档时,读取电子书id参数ebookid

7.5 增加删除文档功能

删除某个文档时,其下所有的文档也应该删除(之前不可以)

其实现在删除父文档,其下面的子孙文档也会不见,只不过变成了垃圾文档,用也用不到,看也看不到,但是会占内存。—无限级数就有这种问题。

利用之前父文档选择那里用到的递归。

程序设计小技巧,将复杂的算法放到前端来做,减少服务器压力。先设计方案,再做技术调研,验证方案可行性,最后才开始开发。

改造后端接口,能够一次性接收多个id,一次性删除。

顺便做了一个二次确认。

7.6 集成富文本插件wangEditor

1.wangEditor介绍

? 富文本编辑器(Rich Text Editor,RTE)是一种可内嵌于浏览器,所见即所得的文本编辑器。可以实现很多功能,如改变字体颜色,插入图片视频等,应用十分广泛。

? wangEditor是一款轻量级 web 富文本编辑器,配置方便,使用简单,开源免费。

2.集成wangEditor

? 1)下载

npm i wangeditor@4.6.3 --save 
或
yarn add wangeditor --save

? 2)引用到项目

? 使用很简单

就可以,通过js来把它变成富文本。

请添加图片描述

3.文档内容表设计与代码生成

* 文档内容表设计(大字段分表)

富文本对我们来说就是HTML

drop table if exists content;
create table content(
    // 这里的id=doc_id
    id bigint not null comment '文档id',
    // mediumtext媒体文本
    content mediumtext not null comment '内容',
    primary key(id)
)engine=innodb default charset=utf8mb4 comment='文档内容';
  • 生成持久层代码

7.7 文档管理页面布局修改

将文档列表和表单变成左右布局–适合列表不多的情况—用栅格系统。

? 使用弹出层填写‘’内容‘’ 不太方便。

? Flex 布局是基于 24 栅格来定义每一个『盒子』的宽度,但不拘泥于栅格。

? 响应式布局:(根据屏幕的变化而展示不同的页面)

? 参照 Bootstrap 的 响应式设计,预设六个响应尺寸:xs sm md lg xl xxl

https://2x.antdv.com/components/grid-cn

7.8 文档内容的保存

前端获取富文本框的html字符串

? 前端通过editor.txt.html();获取html

保存文档接口里,增加内容参数,保存时同时保存文档和内容

? 修改后端文档保存接口:

@PostMapping("/save")
public CommonResp save(@Valid @RequestBody DocSaveReq req) {
    CommonResp resp = new CommonResp<>();
    docService.save(req);
    return resp;
}
public void save(DocSaveReq req) {
        Doc doc = CopyUtil.copy(req, Doc.class);
        Content content = CopyUtil.copy(req, Content.class);
        if (ObjectUtils.isEmpty(req.getId())) {
            // 新增
            doc.setId(snowFlake.nextId());
            doc.setViewCount(0);
            doc.setVoteCount(0);
            docMapper.insert(doc);

            content.setId(doc.getId());
            contentMapper.insert(content);
        } else {
            // 更新
            docMapper.updateByPrimaryKey(doc);
            // blob代表富文本字段
            int count = contentMapper.updateByPrimaryKeyWithBLOBs(content);
            if (count == 0) {
                contentMapper.insert(content);
            }
        }
    }

7.9 文档内容的显示

增加单独获取内容的接口

@GetMapping("/search-content/{id}")
public CommonResp searchContent(@PathVariable Long id) {
    CommonResp<String> resp = new CommonResp<>();
    String content = docService.searchContent(id);
    resp.setContent(content);
    return resp;
}
public String searchContent(Long id) {
    // 会把content整个实体查出来
    Content content = contentMapper.selectByPrimaryKey(id);
    if (ObjectUtils.isEmpty(content)) {
        return "";
    } else {
        return content.getContent();
    }
}

前端得到html字符串后,放入富文本框中

/**
 * 文本内容查询
 **/
const handleQueryContent = () => {
  axios.get("/doc/search-content/"+doc.value.id).then((response) => {
    const data = response.data;
    if (data.success) {
      editor.txt.html(data.content);
    } else {
      message.error(data.message);
    }
  });
};

7.10 文档页面功能开发

增加文档页面,首页点击电子书时,进到该电子书的文档页面。

? 1)路由新增一个doc(文档页面)

? 2)views包下创建doc.vue页面

<template>
  <a-layout>
    <a-layout-content :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px'}">
      <div class="doc">
        <h1>欢迎来到文档页面</h1>
      </div>
    </a-layout-content>
  </a-layout>
</template>

? 3)修改home.vue

左边显示文档树(使用树形组件显示文档树)修改doc.vue

右边显示第一个节点的内容

若干bug修复

一堆功能优化

八、用户管理与登录

本章完成三部分:用户管理、用户登录、登录校验(界面+接口)

8.1 用户管理

1.用户表设计与持久层代码生成

drop table if exists user;
create table user(
    id bigint not null comment 'ID',
    login_name varchar(50) not null comment '登录名',
    name varchar(50) comment '昵称',
    password char(200) not null comment '密码',
    primary key(id),
    unique key login_name_unique (login_name)
)engine=innodb default charset=utf8mb4 comment='文档内容';

打开mybatis代码生成器,生成持久层代码

2.用户表基本的CURD功能

? 按照电子书管理,复制出一套用户管理的代码。

3.用户名重复校验与自定义异常

* 新增用户时,增加用户名重复校验
* 校验重复时,抛出自定义异常
* 修改时,用户名不能修改

4.密码的两层加密处理

对于密码的加密决定使用spring安全框架提供的md5加密,spring自己提供的那个安全性很差。

spring 安全框架提供的加密方法,可以自动加盐,无需自己保存盐值。

不过需要添加下面的依赖。

注意把数据表中password的长度设长一点,因为经过md5加密后,password会特别长,会出现放不下的报错

? 1)密码加密存储

? 就是改一下controller里面的save方法

? 2)密码加密传输

? 前端添加md5工具包,放到public/js下。需要在index.html将js引入进来

5.增加重置密码功能

? 修改用户时,不能修改密码

? 单独开发重置密码表单和接口

8.2 用户登录

1.单点登录token与JWT介绍:

登录:
	1.前端输入用户名登录
	2.校验用户名密码
	3.生成登录标识
	4.后端保存token
	5.前端保存token
登录标识:就是令牌,就是一个唯一的字符串token

校验:
	1.前端请求时,带上token(放在header)
	2.登录拦截器,校验token(到redis获取token)
	3.校验成功则继续后面的业务
	4.校验失败则回到登录页面

单点登录系统:统一的一个登录的系统,整个集团唯一一个登录系统,所需要登录的地方,全部调用到这个系统来。
	假设一个系统有ABC很多个系统,如果每个系统都去做登录功能,会费时费力,而且不能达到统一。所以一般会做出一个X系统,叫做单点登录系统,当跳到A这个网站的时候,需要登录,就去访问X系统,它提供登录界面,也就是登录直接跳到X登录完成后,再回到A。还有一种就是登录界面A自己提供,只是登录接口由X提供,也就是X只提供接口不提供页面。
	X:用户管理、登录、登录校验、退出登录或是注册,有可能只提供接口,也可能包括界面。

token与JWT
	这里的token就是token+redis的组合:token是没有意义的,只要是唯一即可。
	JWT:token是有意义的,加密的,包含业务信息,一般是用户信息,可以被解出来,JWT不需要存储到redis
	JWT的组成:JWT分成三部分,第一部分是头部(header),第二部分是载荷(payload),第三部分是签名(signature)
		头部:声明的类型、声明的加密算法(通常使用SHA256)
		载荷:存放有效信息,一般包含签发者、所面向的用户、接受方、过期时间、签发时间以及唯一身份标识
		签名:主要由头部、载荷以及秘钥组合加密而成
	要使用JWT只需要引入额外的依赖包。
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.18.2</version>
</dependency>
JWT有两个重要的方法:sign()加密,verity()校验

本项目采用token+redis的方法

2.登录功能开发

? 1)后端增加登录接口

@PostMapping("/login")
public CommonResp login(@Valid @RequestBody UserLoginReq req) {
    CommonResp<UserLoginResp> resp = new CommonResp<>();
    UserLoginResp userLoginResp=userService.login(req);
    resp.setContent(userLoginResp);
    return resp;
}

? 2)前端增加登录模态框(the-header.vue添加)

3.登录成功处理

? 1)后端保护用户信息

? 继承redis

? 登录成功后,生成token,以token为key,以用户信息为value,放入redis中

? 2)前端显示登录用户

? header显示登录昵称

? 使用vuex+sessionStorage保存登录信息

4.退出登录功能

? 将token置为失效:1)后端增加退出登录接口,退出后,清除redis用户信息;2)前端增加退出登录按钮退出后,清除前端用户信息。

5.增加登录校验

? 1)后端接口增加登录校验

* 后端增加拦截器,校验token有效性
* 前端请求增加token参数

? 2)前端页面增加登录校验

* 未登录时,管理菜单要隐藏
* 对路由做判断,防止用户通过手敲url访问管理页面--未登录跳到首页或登录页

6.用户密码初始化

? 将test用户密码初始化为test,并在前端添加了对密码位数的校验。因为经过两次加密后,就算密码只有一位都能达到后端要求。

八、阅读数&点赞数功能开发

阅读文档时,更新阅读数

文档的点赞功能,更新点赞数

更新电子书的文档数,阅读数,点赞数

有文档被点赞时,前端可以收到通知

springboot异步化,WebSocket,RocketMQ。

8.1 文档阅读数更新

前端点击某篇文档时,doc.view_couont+1。在DocService中,修改searchContent方法,即加载文档内容时阅读数加1。这里用不了mapper,使用自定义mapper。

8.2 文档点赞功能开发

前端在文档内容的下方,增加点赞按钮,点击后doc.vote_count+1。

? 1)前端增加一个点赞按钮,点击时调用后端的点赞接口

? 2)后端增加一个点赞接口,只做一件事,文档点赞数+1

一个用户只能点赞一次,之后再点赞,有一个提示:您已点赞过;或者再次点击取消点赞(用户ID+文档ID 校验唯一):

? 1)点赞时,要先看看redis中有没有IP+docId,有表示点赞过,没有就可以点赞;

? 2)点赞成功后往redis中放IP+docId。

? 新增util/RequestContext.java+redisUtil.java

8.3 电子书信息更新

电子书信息:文档数、阅读数、点赞数

更新方式:实时更新(优点:数据准确性高,缺点:改动地方很多)、定时批量更新(一段时间全部更新一次,做起来相对简单,侵入性不强)。

1.springboot定时任务示例

? 启动定时器,不需要引入依赖

? 两种定时器写法

在主启动类上添加注解:@EnableScheduling注解

2.完成电子书信息定时更新功能

? 增加定时器,定时执行电子书信息更新SQL,定时策略:

? 查看cron表达式:https://cron.qqe2.com/—用到定时器里的

3.日志流水号的使用

4.WebSocket使用实例

功能:网站通知

点赞时:前端收到通知

定时轮询(打开一个网站后,网站会发起一个定时器,轮询的去调用后端的接口,优点不暂用连接)&被动通知(用到websocket。网站显示出来之后,会创建websocket连接,跟服务器连接起来,如果不关闭网站,连接会一直存在,缺点会占用服务器连接,优点实时)

添加依赖:

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

添加配置类:WebSocketConfig

写websocket服务端:WebSocketServer(这个类类似controller,controller是开放HTTP接口,而这个是开放websocket连接)。

5.完成点赞通知功能

? 点赞时,组装信息内容,往WS推送

? 前端收到WS消息后,弹出消息内容

8.4 使用异步化解耦点赞通知功能

1.springboot异步化使用:点赞和通知功能太紧密,两个功能代码写在一条线上,会互相影响。可以使用异步线程让两个功能走两条线。

开启注解:@EnableAsync:另外起一个线程来执行后面的内容。

需要在需要异步化的地方加入@Async注解,这个注解使用有很多坑,因为另外用一个类用来写这个方法。

@Service
public class WsService {

    @Resource
    public WebSocketServer webSocketServer;

    @Async
    public void sendInfo(String message, String logId) {
        MDC.put("LOG_ID", logId);
        webSocketServer.sendInfo(message);
    }
}

注意:同一个类中A方法(有事务)调用B ,B加事务注解不生效。

2.使用MQ解耦点赞通知功能

? 使用springboot异步化有什么问题?

业务量很大的话,会使用点赞的那里线程越来越多,一致到把整个服务器塞满,会影响原有的业务内容。原来的点赞功能无法执行,因为服务器的资源已经用完,被异步线程全部占满。

? 使用RocketMQ解耦

MQ:消息队列,和redis一样,是一个中间件,需要单独安装,常见的MQ有rocketmq,kafka,rabbitmq等等。MQ分为发送方和接收方。

九、统计数据收集与Echarts报表

确认报表统计方案:

  • 统计维度:
    • 统计数值:总阅读数、总点赞数、今日阅读数、今日点赞数、今日预计阅读数、今日预计阅读增长
    • 统计报表:30天阅读/点赞趋势图、文档阅读量排名(热门文章)、文档点赞量排名(优质文章)
  • 业务表统计:所有报表数据都是从业务表直接获取的。优点:实时性好(数据准确);缺点:对业务表性能有影响、有些统计功能无法实现。
  • 中间表统计:定时将业务表数据汇总到中间表,报表数据从中间表获取。优点:性能好、可实现多功能统计;缺点:工作量大、步骤多容量出错
  • 数据收集:每小时收集一次,每次收集所有数据
  • 数据展示

复杂SQL的编写,Echarts报表的使用

9.1 电子书快照功能

1.电子书快照表设计:

? 概念:快照 保存当时的信息

? 电子书快照表:一天存储一次快照

drop table if exists ebook_snapshot;
create table ebook_snapshot(
    id bigint auto_increment not null comment 'id',
    ebook_id bigint not null default 0 comment '电子书id',
    date date not null comment '快照日期',
    view_count int not null default 0 comment '阅读数',
    vote_count int not null default 0 comment '点赞数',
    view_increase int not null default 0 comment '阅读增长',
    vote_increase int not null default 0 comment '点赞增长',
    primary key (id)
)engine=innodb default charset=utf8mb4 comment='电子书快照表';

? 生成持久层代码。

2.电子书快照收集脚本编写

从业务表收集数据的SQL尽量简单,不要影响业务表性能

快照分成两部分:

? 总量:总阅读数、总点赞数

? 增量:今日阅读数、今日点赞数

# 方案一(ID不连续)
#     删除今天的数据
#     为所有的电子书生成一条今天的记录
#     更新总阅读数、总点赞数
#     更新今日阅读数、今日点赞数
# 方案二(ID连续)
#     为所有的电子书生成一条今天的记录,如果还没有
#     更新总阅读数、总点赞数
#     更新今日阅读数、今日点赞数
insert into ebook_snapshot(ebook_id, `date`, view_count, vote_count, view_increase, vote_increase)
select id, curdate(), 0, 0, 0, 0
from ebook t1
where not exists(select 1 from ebook_snapshot t2 where t1.id = t2.ebook_id and t2.date = curdate());

update ebook_snapshot t1,ebook t2
set t1.view_count=t2.view_count,
    t1.vote_count=t2.vote_count
where t1.date = curdate()
  and t1.ebook_id = t2.id;

# 获取昨天的数据
select t1.ebook_id,view_count,vote_count from ebook_snapshot t1
where t1.date=date_sub(curdate(),interval 1 day);

update ebook_snapshot t1 left join (select ebook_id, view_count, vote_count
                          from ebook_snapshot
                          where date = date_sub(curdate(), interval 1 day)) t2
on t1.ebook_id = t2.ebook_id
set t1.view_increase=t1.view_count - ifnull(t2.view_count,0),
    t1.vote_increase=t1.vote_count - ifnull(t2.vote_count,0)
where t1.date = curdate();
# 遗留bug,第一天上线无昨天数据,怎么计算增量?新增的book呢?

3.完成电子书快照功能

? 增加定时任务,定时收集数据

? 在mapper.xml文件中,一次只能执行一段sql,对于上面的sql有三段,因此需要在配置文件中增加配置。

url: jdbc:mysql://localhost:3306/snow_xue?serverTimezone=UTC&allowMultiQueries=true

4.首页统计数值功能开发

统计数值:总阅读数、总点赞数、今日阅读数、今日点赞数、今日预计阅读数、今日预计阅读增长

后端获取统计数值接口开发开始:EbookSnapshotController->EbookSnapshotService->EbookSnapshotMapper->mapper.xml

前端统计数值组件展示:ant design view统计数值组件。(将首页换成这个,使用单独的组件,然后引入到home.vue中)

9.2 Echarts报表

1.集成Echarts

? 下载echarts源码.min.js结尾的,放到public/js下。在index.html下引入进来

2.30天趋势图功能开发

? 获取30天前到昨天之间的快照数据

? 30天趋势图展示

? 步骤:1)接口的开发

? 2)功能的展示

3.网站优化
id bigint not null default 0 comment ‘电子书id’,
date date not null comment ‘快照日期’,
view_count int not null default 0 comment ‘阅读数’,
vote_count int not null default 0 comment ‘点赞数’,
view_increase int not null default 0 comment ‘阅读增长’,
vote_increase int not null default 0 comment ‘点赞增长’,
primary key (id)
)engine=innodb default charset=utf8mb4 comment=‘电子书快照表’;


?	生成持久层代码。



**2.电子书快照收集脚本编写**

从业务表收集数据的SQL尽量简单,不要影响业务表性能

快照分成两部分:

?	总量:总阅读数、总点赞数

?	增量:今日阅读数、今日点赞数

```sql
# 方案一(ID不连续)
#     删除今天的数据
#     为所有的电子书生成一条今天的记录
#     更新总阅读数、总点赞数
#     更新今日阅读数、今日点赞数
# 方案二(ID连续)
#     为所有的电子书生成一条今天的记录,如果还没有
#     更新总阅读数、总点赞数
#     更新今日阅读数、今日点赞数
insert into ebook_snapshot(ebook_id, `date`, view_count, vote_count, view_increase, vote_increase)
select id, curdate(), 0, 0, 0, 0
from ebook t1
where not exists(select 1 from ebook_snapshot t2 where t1.id = t2.ebook_id and t2.date = curdate());

update ebook_snapshot t1,ebook t2
set t1.view_count=t2.view_count,
    t1.vote_count=t2.vote_count
where t1.date = curdate()
  and t1.ebook_id = t2.id;

# 获取昨天的数据
select t1.ebook_id,view_count,vote_count from ebook_snapshot t1
where t1.date=date_sub(curdate(),interval 1 day);

update ebook_snapshot t1 left join (select ebook_id, view_count, vote_count
                          from ebook_snapshot
                          where date = date_sub(curdate(), interval 1 day)) t2
on t1.ebook_id = t2.ebook_id
set t1.view_increase=t1.view_count - ifnull(t2.view_count,0),
    t1.vote_increase=t1.vote_count - ifnull(t2.vote_count,0)
where t1.date = curdate();
# 遗留bug,第一天上线无昨天数据,怎么计算增量?新增的book呢?

3.完成电子书快照功能

? 增加定时任务,定时收集数据

? 在mapper.xml文件中,一次只能执行一段sql,对于上面的sql有三段,因此需要在配置文件中增加配置。

url: jdbc:mysql://localhost:3306/snow_xue?serverTimezone=UTC&allowMultiQueries=true

4.首页统计数值功能开发

统计数值:总阅读数、总点赞数、今日阅读数、今日点赞数、今日预计阅读数、今日预计阅读增长

后端获取统计数值接口开发开始:EbookSnapshotController->EbookSnapshotService->EbookSnapshotMapper->mapper.xml

前端统计数值组件展示:ant design view统计数值组件。(将首页换成这个,使用单独的组件,然后引入到home.vue中)

9.2 Echarts报表

1.集成Echarts

? 下载echarts源码.min.js结尾的,放到public/js下。在index.html下引入进来

2.30天趋势图功能开发

? 获取30天前到昨天之间的快照数据

? 30天趋势图展示

? 步骤:1)接口的开发

? 2)功能的展示

3.网站优化

源代码地址(包括数据库、图片等资料):
码云地址

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-08-19 18:50:24  更:2022-08-19 18:54:23 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 13:21:12-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码