知识库系统开发实战
页面展示
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 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.添加依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<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.添加插件
<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>
<classPathEntry location="E:\mysql\mysql-connector-java-5.1.6.jar"/>
<context id="DB2Tables" targetRuntime="MyBatis3">
<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>
<javaModelGenerator targetPackage="com.snow.snow_xue.pojo" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaModelGenerator>
<sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<javaClientGenerator type="XMLMAPPER" targetPackage="com.snow.snow_xue.mapper" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<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 {
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;
}
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);
}
}
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去调用业务方法。中间的代码跟过滤器差不多。
@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();
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插件
? 加入依赖
<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")
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) {
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由时间戳,数据中心,机器中心,序列号四部分组成。
@Component
public class SnowFlake {
private final static long START_STMP = 1609459200000L;
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;
}
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 {
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.后端增加删除接口
@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);
@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;
}
@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 bigint not null comment '文档id',
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);
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 = 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尽量简单,不要影响业务表性能
快照分成两部分:
? 总量:总阅读数、总点赞数
? 增量:今日阅读数、今日点赞数
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();
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.网站优化
源代码地址(包括数据库、图片等资料): 码云地址
|