1.freemarker页面静态化技术
1.1 freemarker 介绍
FreeMarker 是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是像PHP那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。
模板 + 数据 = 静态文件
常用的java模板引擎还有哪些?
Jsp、Freemarker、Thymeleaf 、Velocity 等。
-
Jsp 为 Servlet 专用,不能单独进行使用。
-
Thymeleaf 为新技术,功能较为强大,但是执行的效率比较低。
-
Velocity从2010年更新完 2.0 版本后,便没有在更新。Spring Boot 官方在 1.4 版本后对此也不在支持,虽然 Velocity 在 2017 年版本得到迭代,但为时已晚。
1.2 环境搭建&&快速入门
freemarker作为springmvc一种视图格式,默认情况下SpringMVC支持freemarker视图格式。
需要创建Spring Boot+Freemarker工程用于测试模板。
1.2.1 创建测试工程
创建一个freemarker-test 的测试工程专门用于freemarker的功能测试与模板的测试。
pom.xml如下
<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!-- apache 对 java io 的封装工具库 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-io</artifactId> <version>1.3.2</version> </dependency> </dependencies>
1.2.2 配置文件
配置application.yml
server: port: 8881 #服务端口 spring: application: name: freemarker-test #指定服务名 freemarker: cache: false #关闭模板缓存,方便测试 settings: template_update_delay: 0 #检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试 suffix: .ftl #指定Freemarker模板文件的后缀名
1.2.3 创建模型类
在freemarker的测试工程下创建模型类型用于测试
package com.heima.pojo; import lombok.Data; import java.util.Date; @Data public class Student { private String name;//姓名 private int age;//年龄 private Date birthday;//生日 private Float money;//钱包 }
1.2.4 创建模板
在resources下创建templates,此目录为freemarker的默认模板存放目录。
在templates下创建模板文件 01-basic.ftl ,模板中的插值表达式最终会被freemarker替换成具体的数据。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello World!</title> </head> <body> <b>普通文本 String 展示:</b><br><br> Hello ${name} <br> <hr> <b>对象Student中的数据展示:</b><br/> 姓名:${stu.name}<br/> 年龄:${stu.age} <hr> </body> </html>
1.2.5 创建controller
创建Controller类,向Map中添加name,最后返回模板文件。
package com.heima.freemarker.controller; import com.heima.freemarker.pojo.Student; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import java.util.Date; /** * @Description: * @Version: V1.0 */ @Controller // *** public class HelloController { @GetMapping("/basic") public String basic(Model model) { // String数据 model.addAttribute("name", "黑马程序员"); //对象数据 Student student = new Student(); student.setAge(20); student.setBirthday(new Date()); student.setMoney(9000F); student.setName("小明"); model.addAttribute("stu", student); // 找到页面 return "01-basic"; } }
01-basic.ftl,使用插值表达式填充数据
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello World!</title> </head> <body> <b>普通文本 String 展示:</b><br><br> Hello ${name} <br> <hr> <b>对象Student中的数据展示:</b><br/> 姓名:${stu.name}<br/> 年龄:${stu.age} <hr> </body> </html>
1.2.6 创建启动类
package com.heima.freemarker; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class FreemarkerApplication { public static void main(String[] args) { SpringApplication.run(FreemarkerDemotApplication.class,args); } }
1.2.7 测试
请求:http://localhost:8881/basic
1.3 freemarker基础
1.3.1 基础语法种类
1、注释,即<#-- -->,介于其之间的内容会被freemarker忽略
<#--我是一个freemarker注释-->
2、插值(Interpolation):即 ${..}
部分,freemarker会用真实的值代替${..}
Hello
3、FTL指令:和HTML标记类似,名字前加#予以区分,Freemarker会解析标签中的表达式或逻辑。
<# >FTL指令</#>
4、文本,仅文本信息,这些不是freemarker的注释、插值、FTL指令的内容会被freemarker忽略解析,直接输出内容。
<#--freemarker中的普通文本--> 我是一个普通的文本
1.3.2 集合指令(List和Map)
1、数据模型:
在HelloController中新增如下方法:
@GetMapping("/list") public String list(Model model){ //------------------------------------ Student stu1 = new Student(); stu1.setName("小明"); stu1.setAge(18); stu1.setMoney(1000.86f); stu1.setBirthday(new Date()); //小红对象模型数据 Student stu2 = new Student(); stu2.setName("小红"); stu2.setMoney(200.1f); stu2.setAge(19); //将两个对象模型数据存放到List集合中 List<Student> stus = new ArrayList<>(); stus.add(stu1); stus.add(stu2); //向model中存放List集合数据 model.addAttribute("stus",stus); //------------------------------------ //创建Map数据 HashMap<String,Student> stuMap = new HashMap<>(); stuMap.put("stu1",stu1); stuMap.put("stu2",stu2); // 3.1 向model中存放Map数据 model.addAttribute("stuMap", stuMap); return "02-list"; }
2、模板:
在templates中新增02-list.ftl
文件
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello World!</title> </head> <body> <#-- list 数据的展示 --> <b>展示list中的stu数据:</b> <br> <br> <table> <tr> <td>序号</td> <td>姓名</td> <td>年龄</td> <td>钱包</td> </tr> </table> <hr> <#-- Map 数据的展示 --> <b>map数据的展示:</b> <br/><br/> <a href="###">方式一:通过map['keyname'].property</a><br/> 输出stu1的学生信息:<br/> 姓名:<br/> 年龄:<br/> <br/> <a href="###">方式二:通过map.keyname.property</a><br/> 输出stu2的学生信息:<br/> 姓名:<br/> 年龄:<br/> <br/> <a href="###">遍历map中两个学生信息:</a><br/> <table> <tr> <td>序号</td> <td>姓名</td> <td>年龄</td> <td>钱包</td> </tr> </table> <hr> </body> </html>
实例代码:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello World!</title> </head> <body> <#-- list 数据的展示 --> <b>展示list中的stu数据:</b> <br> <br> <table> <tr> <td>序号</td> <td>姓名</td> <td>年龄</td> <td>钱包</td> </tr> <#list stus as stu> <tr> <td>${stu_index+1}</td> <td>${stu.name}</td> <td>${stu.age}</td> <td>${stu.money}</td> </tr> </#list> </table> <hr> <#-- Map 数据的展示 --> <b>map数据的展示:</b> <br/><br/> <a href="###">方式一:通过map['keyname'].property</a><br/> 输出stu1的学生信息:<br/> 姓名:${stuMap['stu1'].name}<br/> 年龄:${stuMap['stu1'].age}<br/> <br/> <a href="###">方式二:通过map.keyname.property</a><br/> 输出stu2的学生信息:<br/> 姓名:${stuMap.stu2.name}<br/> 年龄:${stuMap.stu2.age}<br/> <br/> <a href="###">遍历map中两个学生信息:</a><br/> <table> <tr> <td>序号</td> <td>姓名</td> <td>年龄</td> <td>钱包</td> </tr> <#list stuMap?keys as key > <tr> <td>${key_index}</td> <td>${stuMap[key].name}</td> <td>${stuMap[key].age}</td> <td>${stuMap[key].money}</td> </tr> </#list> </table> <hr> </body> </html>
👆上面代码解释:
${k_index}: index:得到循环的下标,使用方法是在stu后边加"_index",它的值是从0开始
1.3.3 if指令
if 指令即判断指令,是常用的FTL指令,freemarker在解析时遇到if会进行判断,条件为真则输出if中间的内容,否则跳过内容不再输出。
-
指令格式
<#if 表达式> true内容显示 <#else > false内容显示 </if>
1、数据模型:
使用list指令中测试数据模型,判断名称为小红的数据字体显示为红色。
2、模板:
<table> <tr> <td>姓名</td> <td>年龄</td> <td>钱包</td> </tr> <#list stus as stu> <tr> <td >${stu.name}</td> <td>${stu.age}</td> <td >${stu.mondy}</td> </tr> </#list> </table>
实例代码:
<table> <tr> <td>姓名</td> <td>年龄</td> <td>钱包</td> </tr> <#list stus as stu > <#if stu.name='小红'> <tr style="color: red"> <td>${stu_index}</td> <td>${stu.name}</td> <td>${stu.age}</td> <td>${stu.money}</td> </tr> <#else > <tr> <td>${stu_index}</td> <td>${stu.name}</td> <td>${stu.age}</td> <td>${stu.money}</td> </tr> </#if> </#list> </table>
3、输出:
姓名为“小明”则字体颜色显示为红色。
1.3.4 运算符
1、算数运算符
FreeMarker表达式中完全支持算术运算,FreeMarker支持的算术运算符包括:
-
加法:
+
-
减法:
-
-
乘法:
*
-
除法:
/
-
求模 (求余):
%
模板代码
<b>算数运算符</b> <br/><br/> 100+5 运算: ${100 + 5 }<br/> 100 - 5 * 5运算:${100 - 5 * 5}<br/> 5 / 2运算:${5 / 2}<br/> 12 % 10运算:${12 % 10}<br/> <hr>
除了 + 运算以外,其他的运算只能和 number 数字类型的计算。
2、比较运算符
-
=
或者==
:判断两个值是否相等. -
!=
:判断两个值是否不等. -
>
或者gt
:判断左边值是否大于右边值 -
>=
或者gte
:判断左边值是否大于等于右边值 -
<
或者lt
:判断左边值是否小于右边值 -
<=
或者lte
:判断左边值是否小于等于右边值
Controller 的 数据模型代码
@GetMapping("operation") public String testOperation(Model model) { //构建 Date 数据 Date now = new Date(); model.addAttribute("date1", now); model.addAttribute("date2", now); return "03-operation"; }
= 和 == 模板代码
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello World!</title> </head> <body> <b>比较运算符</b> <br/> <br/> <dl> <dt> =/== 和 != 比较:</dt> <dd> <#if "xiaoming" == "xiaoming"> 字符串的比较 "xiaoming" == "xiaoming" </#if> </dd> <dd> <#if 10 != 100> 数值的比较 10 != 100 </#if> </dd> </dl> <dl> <dt>其他比较</dt> <dd> <#if 10 gt 5 > 形式一:使用特殊字符比较数值 10 gt 5 </#if> </dd> <dd> <#-- 日期的比较需要通过?date将属性转为data类型才能进行比较 --> <#if (date1?date >= date2?date)> 形式二:使用括号形式比较时间 date1?date >= date2?date </#if> </dd> </dl> <br/> <hr> </body> </html>
比较运算符注意
-
=
和!=
可以用于字符串、数值和日期来比较是否相等 -
=
和!=
两边必须是相同类型的值,否则会产生错误 -
字符串
"x"
、"x "
、"X"
比较是不等的.因为FreeMarker是精确比较 -
其它的运行符可以作用于数字和日期,但不能作用于字符串
-
使用
gt
等字母运算符代替>
会有更好的效果,因为 FreeMarker会把>
解释成FTL标签的结束字符 -
可以使用括号来避免这种情况,如:
<#if (x>y)>
3、逻辑运算符
-
逻辑与:&&
-
逻辑或:||
-
逻辑非:!
逻辑运算符只能作用于布尔值,否则将产生错误 。
模板代码
<b>逻辑运算符</b> <br/> <br/> <#if (10 lt 12 )&&( 10 gt 5 ) > (10 lt 12 )&&( 10 gt 5 ) 显示为 true </#if> <br/> <br/> <#if !false> false 取反为true </#if> <hr>
1.3.5 空值处理
1、判断某变量是否存在使用 “??”
用法为:variable??,如果该变量存在,返回true,否则返回false
例:为防止stus为空报错可以加上判断如下:
<#if stus??> <#list stus as stu> ...... </#list> </#if>
2、缺失变量默认值使用 “!”
-
使用!要以指定一个默认值,当变量为空时显示默认值
例: ${name!''}表示如果name为空显示空字符串。
-
如果是嵌套对象则建议使用()括起来
例: ${(stu.bestFriend.name)!''}表示,如果stu或bestFriend或name为空默认显示空字符串。
1.3.6 内建函数
内建函数语法格式: 变量+?+函数名称
1、和到某个集合的大小
${集合名?size}
2、日期格式化
显示年月日: ${today?date}
显示时分秒:${today?time}
显示日期+时间:${today?datetime}
自定义格式化: ${today?string("yyyy年MM月")}
3、内建函数c
model.addAttribute("point", 102920122);
point是数字型,使用${point}会显示这个数字的值,每三位使用逗号分隔。
如果不想显示为每三位分隔的数字,可以使用c函数将数字型转成字符串输出
${point?c}
4、将json字符串转成对象
一个例子:
其中用到了 assign标签,assign的作用是定义一个变量。
<#assign text="{'bank':'工商银行','account':'10101920201920212'}" /> <#assign data=text?eval /> 开户行:${data.bank} 账号:${data.account}
模板代码:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>inner Function</title> </head> <body> <b>获得集合大小</b><br> 集合大小: <hr> <b>获得日期</b><br> 显示年月日: <br> 显示时分秒:<br> 显示日期+时间:<br> 自定义格式化: <br> <hr> <b>内建函数C</b><br> 没有C函数显示的数值: <br> 有C函数显示的数值: <hr> <b>声明变量assign</b><br> <hr> </body> </html>
内建函数模板页面:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>inner Function</title> </head> <body> <b>获得集合大小</b><br> 集合大小:${stus?size} <hr> <b>获得日期</b><br> 显示年月日: ${today?date} <br> 显示时分秒:${today?time}<br> 显示日期+时间:${today?datetime}<br> 自定义格式化: ${today?string("yyyy年MM月")}<br> <hr> <b>内建函数C</b><br> 没有C函数显示的数值:${point} <br> 有C函数显示的数值:${point?c} <hr> <b>声明变量assign</b><br> <#assign text="{'bank':'工商银行','account':'10101920201920212'}" /> <#assign data=text?eval /> 开户行:${data.bank} 账号:${data.account} <hr> </body> </html>
内建函数Controller数据模型:
@GetMapping("innerFunc") public String testInnerFunc(Model model) { //1.1 小强对象模型数据 Student stu1 = new Student(); stu1.setName("小强"); stu1.setAge(18); stu1.setMoney(1000.86f); stu1.setBirthday(new Date()); //1.2 小红对象模型数据 Student stu2 = new Student(); stu2.setName("小红"); stu2.setMoney(200.1f); stu2.setAge(19); //1.3 将两个对象模型数据存放到List集合中 List<Student> stus = new ArrayList<>(); stus.add(stu1); stus.add(stu2); model.addAttribute("stus", stus); // 2.1 添加日期 Date date = new Date(); model.addAttribute("today", date); // 3.1 添加数值 model.addAttribute("point", 102920122); return "04-innerFunc"; }
更多语法学习参考官网:FreeMarker 中文官方参考手册
1.4 静态化测试
之前的测试都是SpringMVC将Freemarker作为视图解析器(ViewReporter)来集成到项目中,工作中,有的时候需要使用Freemarker原生Api来生成静态内容,下面一起来学习下原生Api生成文本文件。
1.4.1 需求分析
使用freemarker原生Api将页面生成html文件,本节测试html文件生成的方法:
1.4.2 静态化测试
根据模板文件生成html文件
①:修改application.yml文件,添加以下模板存放位置的配置信息,完整配置如下:
server: port: 8881 #服务端口 spring: application: name: freemarker-demo #指定服务名 freemarker: cache: false #关闭模板缓存,方便测试 settings: template_update_delay: 0 #检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试 suffix: .ftl #指定Freemarker模板文件的后缀名 template-loader-path: classpath:/templates/ #模板存放位置
②:在test下创建测试类
package com.heima.freemarker.test; import com.heima.freemarker.FreemarkerDemoApplication; import com.heima.freemarker.entity.Student; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.io.FileWriter; import java.io.IOException; import java.util.*; @SpringBootTest(classes = FreemarkerDemoApplication.class) @RunWith(SpringRunner.class) public class FreemarkerTest { @Autowired private Configuration configuration; @Test public void test() throws IOException, TemplateException { //freemarker的模板对象,获取模板 Template template = configuration.getTemplate("02-list.ftl"); Map params = getData(); //合成 //第一个参数 数据模型 //第二个参数 输出流 template.process(params, new FileWriter("d:/list.html")); } private Map getData() { Map<String, Object> map = new HashMap<>(); //对象模型数据 Student stu1 = new Student(); stu1.setName("小明"); stu1.setAge(18); stu1.setMoney(1000.86f); stu1.setBirthday(new Date()); //小红对象模型数据 Student stu2 = new Student(); stu2.setName("小红"); stu2.setMoney(200.1f); stu2.setAge(19); //将两个对象模型数据存放到List集合中 List<Student> stus = new ArrayList<>(); stus.add(stu1); stus.add(stu2); //向map中存放List集合数据 map.put("stus", stus); //创建Map数据 HashMap<String, Student> stuMap = new HashMap<>(); stuMap.put("stu1", stu1); stuMap.put("stu2", stu2); //向map中存放Map数据 map.put("stuMap", stuMap); //返回Map return map; } }
对象存储服务MinIO
1 MinIO简介
OSS html 自动下载
MinIO基于Apache License v2.0开源协议的对象存储服务,可以做为云存储的解决方案用来保存海量的图片,视频,文档。由于采用Golang实现,服务端可以工作在Windows,Linux, OS X和FreeBSD上。配置简单,基本是复制可执行程序,单行命令可以运行起来。
MinIO兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
S3 ( Simple Storage Service简单存储服务)
基本概念
-
bucket – 类比于文件系统的目录
-
Object – 类比文件系统的文件
-
Keys – 类比文件名
官网文档:MinIO对象存储 Kubernetes — MinIO中文文档 | MinIO Kubernetes中文文档
2 MinIO特点
-
数据保护
Minio使用Minio Erasure Code(纠删码)来防止硬件故障。即便损坏一半以上的driver,但是仍然可以从中恢复。
-
高性能
作为高性能对象存储,在标准硬件条件下它能达到55GB/s的读、35GB/s的写速率
-
可扩容
不同MinIO集群可以组成联邦,并形成一个全局的命名空间,并跨越多个数据中心
-
SDK支持
基于Minio轻量的特点,它得到类似Java、Python或Go等语言的sdk支持
-
有操作页面
面向用户友好的简单操作界面,非常方便的管理Bucket及里面的文件资源
-
功能简单
这一设计原则让MinIO不容易出错、更快启动
-
丰富的API
支持文件资源的分享连接及分享链接的过期策略、存储桶操作、文件列表访问及文件上传下载的基本功能等。
-
文件变化主动通知
存储桶(Bucket)如果发生改变,比如上传对象和删除对象,可以使用存储桶事件通知机制进行监控,并通过以下方式发布出去:AMQP、MQTT、Elasticsearch、Redis、NATS、MySQL、Kafka、Webhooks等。
3 开箱使用
3.1 安装启动
我们提供的镜像中已经有minio的环境
我们可以使用docker进行环境部署和启动
docker pull minio/minio:RELEASE.2021-06-14T01-29-23Z docker run -p 9090:9000 --name minio -d --restart=always -e "MINIO_ACCESS_KEY=minio" -e "MINIO_SECRET_KEY=minio123" -v /home/data:/data -v /home/config:/root/.minio minio/minio:RELEASE.2021-06-14T01-29-23Z server /data
3.2 管理控制台
假设我们的服务器地址为http://192.168.200.130:9090,我们在地址栏输入:http://http://192.168.200.130:9090/ 即可进入登录界面。
Access Key为minio Secret_key 为minio123 进入系统后可以看到主界面
点击右下角的“+”号 ,点击下面的图标,创建一个桶
4 快速入门
4.1 创建工程,导入pom依赖
创建minio-test,对应pom如下
<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>7.1.0</version> </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> </dependency> </dependencies>
引导类:
package com.heima.minio; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MinIOApplication { public static void main(String[] args) { SpringApplication.run(MinIOApplication.class,args); } }
创建测试类,上传html文件
package com.heima.minio.test; import io.minio.MinioClient; import io.minio.PutObjectArgs; import java.io.FileInputStream; public class MinIOTest { public static void main(String[] args) { FileInputStream fileInputStream = null; try { fileInputStream = new FileInputStream("D:\\list.html");; //1.创建minio链接客户端 MinioClient minioClient = MinioClient.builder().credentials("minio", "minio123").endpoint("http://192.168.200.129:9090").build(); //2.上传 PutObjectArgs putObjectArgs = PutObjectArgs.builder() .object("list.html")//文件名 .contentType("text/html")//文件类型 .bucket("leadnews")//桶名词 与minio创建的名词一致 .stream(fileInputStream, fileInputStream.available(), -1) //文件流 .build(); minioClient.putObject(putObjectArgs); System.out.println("http://192.168.200.129:9090/leadnews/list.html"); } catch (Exception ex) { ex.printStackTrace(); } } }
5 封装MinIO为starter
5.1 改造heima-file-spring-boot-starter模块
新增依赖
<dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>7.1.0</version> </dependency>
5.2 替换配置
拷贝资料 中 config 及 service文件夹, 替换原有文件
5.3 加入自动配置
在resources中新建META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.heima.file.service.impl.OSSAliyunFileStorageService,com.heima.file.service.impl.MinIOFileStorageService
5.4 其他微服务使用
第一,导入heima-file-spring-boot-starter的依赖
第二,在wemedia微服务中添加minio所需要的配置
file: minio: accessKey: minio secretKey: minio123 bucket: leadnews endpoint: http://192.168.200.130:9090/ readPath: http://192.168.200.130:9090/
第三,在对应使用的业务类中注入FileStorageService,样例如下:
package com.heima.wemedia; import com.heima.file.service.FileStorageService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import javax.annotation.Resource; import java.io.FileInputStream; import java.io.FileNotFoundException; @SpringBootTest @RunWith(SpringRunner.class) public class MinIoTest { // 指定MinIo实现 @Resource(name = "minIOFileStorageService") FileStorageService fileStorageService; // 不指定 beanName 注入的是OSS的实现 @Autowired FileStorageService fileStorageService2; @Test public void uploadToMinIo() throws FileNotFoundException { System.out.println(fileStorageService); System.out.println(fileStorageService2); // 准备好一个静态页 FileInputStream fileInputStream = new FileInputStream("D://list.html"); // 将静态页上传到minIO文件服务器中 文件名称 文件类型 文件流 fileStorageService.store("aa","list.html","text/html",fileInputStream); } }
修改下实现类的细节
第七章 平台自动审核自媒体文章
今日目标
-
能够掌握自媒体文章审核的流程
-
能够使用阿里云安全服务检测文章内容
-
能够完成自媒体文章审核的功能
-
能够完成自媒体发布文章与审核对接
-
能够完成文章发布成功后生成静态页面
1 自媒体文章自动审核需求说明
1.1 自媒体文章自动审核流程
做为内容类产品,内容安全非常重要,所以需要进行对自媒体用户发布的文章进行审核以后才能到app端展示给用户。
审核的流程如下:也可以查看当前讲义文件夹下:自媒体文章发布时序图.pdf
1.当自媒体用户提交发布文章之后,会发消息给kafka提交审核,平台运营端接收文章信息
2.根据自媒体文章id查询文章信息
3.如果当前文章的状态为4(人工审核通过),则无需再进行自动审核审核,
保存app文章相关数据即可
4.文章状态为8,发布时间小于等于当前时间,则直接保存app文章相关数据
5.文章状态为1,则进行自动审核
5.1 文章内容中是否有自管理的敏感词,如果有则审核不通过,修改自媒体文章状态为2
5.2 调用阿里云文本反垃圾服务,进行文本审核,如果审核不成功或需要人工审核,
修改自媒体文章状态为2
5.3 调用阿里云图片审核服务,如果审核不通过或需要人工审核,修改自媒体文章状态为2
5.4 自媒体文章发布时间大于当前时间,修改自媒体文章状态为8(审核通过待发布状态)
5.5 审核通过,修改自媒体文章状态为 9 (审核通过)同时回填app文章id到自媒体表中
6.保存app相关数据
如果存在自媒体文章中存在articleId则是修改,没有articleId为新增
ap_article_config 文章配置
ap_article 文章
ap_article_content 文章内容
7.异步创建索引(为后续app端的搜索功能做数据准备)
8.异步生成静态页面(为后续app端的文章展示功能做数据准备)
1.2 表结构
文章审核通过之后发布APP文章实质:把wm_news 数据保存到article数据库的三张表中。
(1)wm_news 自媒体文章表 在自媒体库
status字段:0 草稿 1 待审核 2 审核失败 3 人工审核 4 人工审核通过 8 审核通过(待发布) 9 已发布
(2)ap_author 文章作者表 在article库
(3)ap_article_config 文章配置表 在article库
对应实体:
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#c88fd0">package</span> <span style="color:#8d8df0">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">model</span>.<span style="color:#b8bfc6">article</span>.<span style="color:#b8bfc6">pojos</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">baomidou</span>.<span style="color:#b8bfc6">mybatisplus</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">IdType</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">baomidou</span>.<span style="color:#b8bfc6">mybatisplus</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">TableField</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">baomidou</span>.<span style="color:#b8bfc6">mybatisplus</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">TableId</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">baomidou</span>.<span style="color:#b8bfc6">mybatisplus</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">TableName</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">lombok</span>.<span style="color:#b8bfc6">Data</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* <p></span>
<span style="color:#da924a">* APP已发布文章配置表</span>
<span style="color:#da924a">* </p></span>
<span style="color:#da924a">* @author itheima</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@Data</span>
<span style="color:#b7b3b3">@TableName</span>(<span style="color:#d26b6b">"ap_article_config"</span>)
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">class</span> <span style="color:#8d8df0">ApArticleConfig</span> {
<span style="color:#b7b3b3">@TableId</span>(<span style="color:#b8bfc6">value</span> <span style="color:#b8bfc6">=</span> <span style="color:#d26b6b">"id"</span>,<span style="color:#b8bfc6">type</span> <span style="color:#b8bfc6">=</span> <span style="color:#b8bfc6">IdType</span>.<span style="color:#b8bfc6">ID_WORKER</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Long</span> <span style="color:#b8bfc6">id</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 文章id</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@TableField</span>(<span style="color:#d26b6b">"article_id"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Long</span> <span style="color:#b8bfc6">articleId</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 是否可评论</span>
<span style="color:#da924a">* true: 可以评论 1</span>
<span style="color:#da924a">* false: 不可评论 0</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@TableField</span>(<span style="color:#d26b6b">"is_comment"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Boolean</span> <span style="color:#b8bfc6">isComment</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 是否转发</span>
<span style="color:#da924a">* true: 可以转发 1</span>
<span style="color:#da924a">* false: 不可转发 0</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@TableField</span>(<span style="color:#d26b6b">"is_forward"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Boolean</span> <span style="color:#b8bfc6">isForward</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 是否下架</span>
<span style="color:#da924a">* true: 下架 1</span>
<span style="color:#da924a">* false: 没有下架 0</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@TableField</span>(<span style="color:#d26b6b">"is_down"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Boolean</span> <span style="color:#b8bfc6">isDown</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 是否已删除</span>
<span style="color:#da924a">* true: 删除 1</span>
<span style="color:#da924a">* false: 没有删除 0</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@TableField</span>(<span style="color:#d26b6b">"is_delete"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Boolean</span> <span style="color:#b8bfc6">isDelete</span>;
}</span></span>
(4)ap_article 文章信息表 在article库
-
layout 文章布局 0 无图文章 1 单图文章 2或3 多图文章
-
flag 文章标记 0 普通文章 1 热点文章 2 置顶文章 3 精品文章 4 大V 文章
-
images 文章图片 多张逗号分隔
对应实体
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#c88fd0">package</span> <span style="color:#8d8df0">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">model</span>.<span style="color:#b8bfc6">article</span>.<span style="color:#b8bfc6">pojos</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">baomidou</span>.<span style="color:#b8bfc6">mybatisplus</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">IdType</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">baomidou</span>.<span style="color:#b8bfc6">mybatisplus</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">TableField</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">baomidou</span>.<span style="color:#b8bfc6">mybatisplus</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">TableId</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">baomidou</span>.<span style="color:#b8bfc6">mybatisplus</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">TableName</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">lombok</span>.<span style="color:#b8bfc6">Data</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">java</span>.<span style="color:#b8bfc6">util</span>.<span style="color:#b8bfc6">Date</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* <p></span>
<span style="color:#da924a">* 文章信息表,存储已发布的文章</span>
<span style="color:#da924a">* </p></span>
<span style="color:#da924a">*</span>
<span style="color:#da924a">* @author itheima</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@Data</span>
<span style="color:#b7b3b3">@TableName</span>(<span style="color:#d26b6b">"ap_article"</span>)
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">class</span> <span style="color:#8d8df0">ApArticle</span> {
<span style="color:#b7b3b3">@TableId</span>(<span style="color:#b8bfc6">value</span> <span style="color:#b8bfc6">=</span> <span style="color:#d26b6b">"id"</span>,<span style="color:#b8bfc6">type</span> <span style="color:#b8bfc6">=</span> <span style="color:#b8bfc6">IdType</span>.<span style="color:#b8bfc6">ID_WORKER</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Long</span> <span style="color:#b8bfc6">id</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 标题</span>
<span style="color:#da924a">*/</span>
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">String</span> <span style="color:#b8bfc6">title</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 作者id</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@TableField</span>(<span style="color:#d26b6b">"author_id"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Long</span> <span style="color:#b8bfc6">authorId</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 作者名称</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@TableField</span>(<span style="color:#d26b6b">"author_name"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">String</span> <span style="color:#b8bfc6">authorName</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 频道id</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@TableField</span>(<span style="color:#d26b6b">"channel_id"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Integer</span> <span style="color:#b8bfc6">channelId</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 频道名称</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@TableField</span>(<span style="color:#d26b6b">"channel_name"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">String</span> <span style="color:#b8bfc6">channelName</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 文章布局 0 无图文章 1 单图文章 2 多图文章</span>
<span style="color:#da924a">*/</span>
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Short</span> <span style="color:#b8bfc6">layout</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 文章标记 0 普通文章 1 热点文章 2 置顶文章 3 精品文章 4 大V 文章</span>
<span style="color:#da924a">*/</span>
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Byte</span> <span style="color:#b8bfc6">flag</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 文章封面图片 多张逗号分隔</span>
<span style="color:#da924a">*/</span>
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">String</span> <span style="color:#b8bfc6">images</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 标签</span>
<span style="color:#da924a">*/</span>
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">String</span> <span style="color:#b8bfc6">labels</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 点赞数量</span>
<span style="color:#da924a">*/</span>
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Integer</span> <span style="color:#b8bfc6">likes</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 收藏数量</span>
<span style="color:#da924a">*/</span>
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Integer</span> <span style="color:#b8bfc6">collection</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 评论数量</span>
<span style="color:#da924a">*/</span>
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Integer</span> <span style="color:#b8bfc6">comment</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 阅读数量</span>
<span style="color:#da924a">*/</span>
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Integer</span> <span style="color:#b8bfc6">views</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 省市</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@TableField</span>(<span style="color:#d26b6b">"province_id"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Integer</span> <span style="color:#b8bfc6">provinceId</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 市区</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@TableField</span>(<span style="color:#d26b6b">"city_id"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Integer</span> <span style="color:#b8bfc6">cityId</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 区县</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@TableField</span>(<span style="color:#d26b6b">"county_id"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Integer</span> <span style="color:#b8bfc6">countyId</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 创建时间</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@TableField</span>(<span style="color:#d26b6b">"created_time"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#b8bfc6">Date</span> <span style="color:#b8bfc6">createdTime</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 发布时间</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@TableField</span>(<span style="color:#d26b6b">"publish_time"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#b8bfc6">Date</span> <span style="color:#b8bfc6">publishTime</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 同步状态</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@TableField</span>(<span style="color:#d26b6b">"sync_status"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Boolean</span> <span style="color:#b8bfc6">syncStatus</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 来源</span>
<span style="color:#da924a">*/</span>
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Boolean</span> <span style="color:#b8bfc6">origin</span>;
}</span></span>
(5)ap_article_content 文章内容表 在article库
对应实体:
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#c88fd0">package</span> <span style="color:#8d8df0">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">model</span>.<span style="color:#b8bfc6">article</span>.<span style="color:#b8bfc6">pojos</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">baomidou</span>.<span style="color:#b8bfc6">mybatisplus</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">IdType</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">baomidou</span>.<span style="color:#b8bfc6">mybatisplus</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">TableField</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">baomidou</span>.<span style="color:#b8bfc6">mybatisplus</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">TableId</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">baomidou</span>.<span style="color:#b8bfc6">mybatisplus</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">TableName</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">lombok</span>.<span style="color:#b8bfc6">Data</span>;
<span style="color:#b7b3b3">@Data</span>
<span style="color:#b7b3b3">@TableName</span>(<span style="color:#d26b6b">"ap_article_content"</span>)
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">class</span> <span style="color:#8d8df0">ApArticleContent</span> {
<span style="color:#b7b3b3">@TableId</span>(<span style="color:#b8bfc6">value</span> <span style="color:#b8bfc6">=</span> <span style="color:#d26b6b">"id"</span>,<span style="color:#b8bfc6">type</span> <span style="color:#b8bfc6">=</span> <span style="color:#b8bfc6">IdType</span>.<span style="color:#b8bfc6">ID_WORKER</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Long</span> <span style="color:#b8bfc6">id</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 文章id</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@TableField</span>(<span style="color:#d26b6b">"article_id"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">Long</span> <span style="color:#b8bfc6">articleId</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 文章内容</span>
<span style="color:#da924a">*/</span>
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">String</span> <span style="color:#b8bfc6">content</span>;
}</span></span>
2 文章自动审核功能实现
2.1 文章审核功能-准备工作
2.1.1 自媒体feign接口
(1)需求说明
在自动审核的时候需要自媒体的远程接口,如下:
1 根据文章id查询自媒体文章的数据
2 在审核的过程中,审核失败或者成功需要修改自媒体文章的状态
3 在文章进行保存的时候需要查询作者信息,需要通过自媒体用户关联查询作者信息
(2)自媒体文章接口准备
在自媒体端的WmNewsController中新增一个方法,并处理
<span style="background-color:#333333"><span style="color:#b8bfc6"> <span style="color:#da924a">/**</span>
<span style="color:#da924a">* 修改文章</span>
<span style="color:#da924a">* @param wmNews</span>
<span style="color:#da924a">* @return</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@ApiOperation</span>(<span style="color:#d26b6b">"根据id修改自媒体文章"</span>)
<span style="color:#b7b3b3">@PutMapping</span>(<span style="color:#d26b6b">"/update"</span>)
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">ResponseResult</span> <span style="color:#8d8df0">updateWmNews</span>(<span style="color:#b7b3b3">@RequestBody</span> <span style="color:#b8bfc6">WmNews</span> <span style="color:#b8bfc6">wmNews</span>) {
<span style="color:#b8bfc6">wmNewsService</span>.<span style="color:#b8bfc6">updateById</span>(<span style="color:#b8bfc6">wmNews</span>);
<span style="color:#c88fd0">return</span> <span style="color:#b8bfc6">ResponseResult</span>.<span style="color:#b8bfc6">okResult</span>();
}</span></span>
(3)自媒体用户接口准备
在自媒体端的WmUserController中新增方法,并处理
<span style="background-color:#333333"><span style="color:#b8bfc6"> <span style="color:#b7b3b3">@ApiOperation</span>(<span style="color:#d26b6b">"根据id查询自媒体用户信息"</span>)
<span style="color:#b7b3b3">@GetMapping</span>(<span style="color:#d26b6b">"/findOne/{id}"</span>)
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">ResponseResult</span><span style="color:#b8bfc6"><</span><span style="color:#b8bfc6">WmUser</span><span style="color:#b8bfc6">></span> <span style="color:#8d8df0">findWmUserById</span>(<span style="color:#b7b3b3">@PathVariable</span>(<span style="color:#d26b6b">"id"</span>) <span style="color:#1cc685">Integer</span> <span style="color:#b8bfc6">id</span>) {
<span style="color:#c88fd0">return</span> <span style="color:#b8bfc6">ResponseResult</span>.<span style="color:#b8bfc6">okResult</span>(<span style="color:#b8bfc6">wmUserService</span>.<span style="color:#b8bfc6">getById</span>(<span style="color:#b8bfc6">id</span>));
}</span></span>
在feign模块下新增远程接口:
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#c88fd0">package</span> <span style="color:#8d8df0">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">feigns</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">config</span>.<span style="color:#b8bfc6">HeimaFeignAutoConfiguration</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">feigns</span>.<span style="color:#b8bfc6">fallback</span>.<span style="color:#b8bfc6">WemediaFeignFallback</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">model</span>.<span style="color:#b8bfc6">common</span>.<span style="color:#b8bfc6">dtos</span>.<span style="color:#b8bfc6">ResponseResult</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">model</span>.<span style="color:#b8bfc6">wemedia</span>.<span style="color:#b8bfc6">pojos</span>.<span style="color:#b8bfc6">WmNews</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">model</span>.<span style="color:#b8bfc6">wemedia</span>.<span style="color:#b8bfc6">pojos</span>.<span style="color:#b8bfc6">WmUser</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">cloud</span>.<span style="color:#b8bfc6">openfeign</span>.<span style="color:#b8bfc6">FeignClient</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">web</span>.<span style="color:#b8bfc6">bind</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">*</span>;
<span style="color:#b7b3b3">@FeignClient</span>(
<span style="color:#b8bfc6">value</span> <span style="color:#b8bfc6">=</span> <span style="color:#d26b6b">"leadnews-wemedia"</span>,
<span style="color:#b8bfc6">fallbackFactory</span> <span style="color:#b8bfc6">=</span> <span style="color:#b8bfc6">WemediaFeignFallback</span>.<span style="color:#c88fd0">class</span>,
<span style="color:#b8bfc6">configuration</span> <span style="color:#b8bfc6">=</span> <span style="color:#b8bfc6">HeimaFeignAutoConfiguration</span>.<span style="color:#c88fd0">class</span>
)
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">interface</span> <span style="color:#8d8df0">WemediaFeign</span> {
<span style="color:#da924a">//=====================新增远程接口=======================</span>
<span style="color:#b7b3b3">@GetMapping</span>(<span style="color:#d26b6b">"/api/v1/news/one/{id}"</span>)
<span style="color:#b8bfc6">ResponseResult</span><span style="color:#b8bfc6"><</span><span style="color:#b8bfc6">WmNews</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">findById</span>(<span style="color:#b7b3b3">@PathVariable</span>(<span style="color:#d26b6b">"id"</span>) <span style="color:#1cc685">Integer</span> <span style="color:#b8bfc6">id</span>);
<span style="color:#b7b3b3">@PutMapping</span>(<span style="color:#d26b6b">"/api/v1/news/update"</span>)
<span style="color:#b8bfc6">ResponseResult</span> <span style="color:#b8bfc6">updateWmNews</span>(<span style="color:#b7b3b3">@RequestBody</span> <span style="color:#b8bfc6">WmNews</span> <span style="color:#b8bfc6">wmNews</span>);
<span style="color:#b7b3b3">@GetMapping</span>(<span style="color:#d26b6b">"/api/v1/user/findOne/{id}"</span>)
<span style="color:#b8bfc6">ResponseResult</span><span style="color:#b8bfc6"><</span><span style="color:#b8bfc6">WmUser</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">findWmUserById</span>(<span style="color:#b7b3b3">@PathVariable</span>(<span style="color:#d26b6b">"id"</span>) <span style="color:#1cc685">Integer</span> <span style="color:#b8bfc6">id</span>);
<span style="color:#da924a">//=====================新增远程接口=======================</span>
<span style="color:#b7b3b3">@PostMapping</span>(<span style="color:#d26b6b">"/api/v1/user/save"</span>)
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">ResponseResult</span><span style="color:#b8bfc6"><</span><span style="color:#b8bfc6">WmUser</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">save</span>(<span style="color:#b7b3b3">@RequestBody</span> <span style="color:#b8bfc6">WmUser</span> <span style="color:#b8bfc6">wmUser</span>);
<span style="color:#b7b3b3">@GetMapping</span>(<span style="color:#d26b6b">"/api/v1/user/findByName/{name}"</span>)
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">ResponseResult</span><span style="color:#b8bfc6"><</span><span style="color:#b8bfc6">WmUser</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">findByName</span>(<span style="color:#b7b3b3">@PathVariable</span>(<span style="color:#d26b6b">"name"</span>) <span style="color:#1cc685">String</span> <span style="color:#b8bfc6">name</span>);
}</span></span>
修改降级方法
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#c88fd0">package</span> <span style="color:#8d8df0">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">feigns</span>.<span style="color:#b8bfc6">fallback</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">feigns</span>.<span style="color:#b8bfc6">WemediaFeign</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">model</span>.<span style="color:#b8bfc6">common</span>.<span style="color:#b8bfc6">dtos</span>.<span style="color:#b8bfc6">ResponseResult</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">model</span>.<span style="color:#b8bfc6">common</span>.<span style="color:#b8bfc6">enums</span>.<span style="color:#b8bfc6">AppHttpCodeEnum</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">model</span>.<span style="color:#b8bfc6">wemedia</span>.<span style="color:#b8bfc6">pojos</span>.<span style="color:#b8bfc6">WmNews</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">model</span>.<span style="color:#b8bfc6">wemedia</span>.<span style="color:#b8bfc6">pojos</span>.<span style="color:#b8bfc6">WmUser</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">feign</span>.<span style="color:#b8bfc6">hystrix</span>.<span style="color:#b8bfc6">FallbackFactory</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">lombok</span>.<span style="color:#b8bfc6">extern</span>.<span style="color:#b8bfc6">slf4j</span>.<span style="color:#b8bfc6">Slf4j</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">stereotype</span>.<span style="color:#b8bfc6">Component</span>;
<span style="color:#b7b3b3">@Component</span>
<span style="color:#b7b3b3">@Slf4j</span>
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">class</span> <span style="color:#8d8df0">WemediaFeignFallback</span> <span style="color:#c88fd0">implements</span> <span style="color:#b8bfc6">FallbackFactory</span><span style="color:#b8bfc6"><</span><span style="color:#b8bfc6">WemediaFeign</span><span style="color:#b8bfc6">></span> {
<span style="color:#b7b3b3">@Override</span>
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">WemediaFeign</span> <span style="color:#b8bfc6">create</span>(<span style="color:#b8bfc6">Throwable</span> <span style="color:#b8bfc6">throwable</span>) {
<span style="color:#c88fd0">return</span> <span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">WemediaFeign</span>() {
<span style="color:#b7b3b3">@Override</span>
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">ResponseResult</span><span style="color:#b8bfc6"><</span><span style="color:#b8bfc6">WmNews</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">findById</span>(<span style="color:#1cc685">Integer</span> <span style="color:#b8bfc6">id</span>) {
<span style="color:#b8bfc6">log</span>.<span style="color:#b8bfc6">error</span>(<span style="color:#d26b6b">"参数: {}"</span>,<span style="color:#b8bfc6">id</span>);
<span style="color:#b8bfc6">log</span>.<span style="color:#b8bfc6">error</span>(<span style="color:#d26b6b">"自媒体 WmNews findById 远程调用出错啦 ~~~ !!!! {} "</span>,<span style="color:#b8bfc6">throwable</span>);
<span style="color:#c88fd0">return</span> <span style="color:#b8bfc6">ResponseResult</span>.<span style="color:#b8bfc6">errorResult</span>(<span style="color:#b8bfc6">AppHttpCodeEnum</span>.<span style="color:#b8bfc6">SERVER_ERROR</span>);
}
<span style="color:#b7b3b3">@Override</span>
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">ResponseResult</span> <span style="color:#b8bfc6">updateWmNews</span>(<span style="color:#b8bfc6">WmNews</span> <span style="color:#b8bfc6">wmNews</span>) {
<span style="color:#b8bfc6">log</span>.<span style="color:#b8bfc6">error</span>(<span style="color:#d26b6b">"参数: {}"</span>,<span style="color:#b8bfc6">wmNews</span>);
<span style="color:#b8bfc6">log</span>.<span style="color:#b8bfc6">error</span>(<span style="color:#d26b6b">"自媒体 wmNews updateWmNews 远程调用出错啦 ~~~ !!!! {} "</span>,<span style="color:#b8bfc6">throwable</span>);
<span style="color:#c88fd0">return</span> <span style="color:#b8bfc6">ResponseResult</span>.<span style="color:#b8bfc6">errorResult</span>(<span style="color:#b8bfc6">AppHttpCodeEnum</span>.<span style="color:#b8bfc6">SERVER_ERROR</span>);
}
<span style="color:#b7b3b3">@Override</span>
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">ResponseResult</span><span style="color:#b8bfc6"><</span><span style="color:#b8bfc6">WmUser</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">findWmUserById</span>(<span style="color:#1cc685">Integer</span> <span style="color:#b8bfc6">id</span>) {
<span style="color:#b8bfc6">log</span>.<span style="color:#b8bfc6">error</span>(<span style="color:#d26b6b">"参数: {}"</span>,<span style="color:#b8bfc6">id</span>);
<span style="color:#b8bfc6">log</span>.<span style="color:#b8bfc6">error</span>(<span style="color:#d26b6b">"自媒体 WmUser findWmUserById 远程调用出错啦 ~~~ !!!! {} "</span>,<span style="color:#b8bfc6">throwable</span>);
<span style="color:#c88fd0">return</span> <span style="color:#b8bfc6">ResponseResult</span>.<span style="color:#b8bfc6">errorResult</span>(<span style="color:#b8bfc6">AppHttpCodeEnum</span>.<span style="color:#b8bfc6">SERVER_ERROR</span>);
}
<span style="color:#b7b3b3">@Override</span>
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">ResponseResult</span><span style="color:#b8bfc6"><</span><span style="color:#b8bfc6">WmUser</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">save</span>(<span style="color:#b8bfc6">WmUser</span> <span style="color:#b8bfc6">wmUser</span>) {
<span style="color:#b8bfc6">log</span>.<span style="color:#b8bfc6">error</span>(<span style="color:#d26b6b">"参数: {}"</span>,<span style="color:#b8bfc6">wmUser</span>);
<span style="color:#b8bfc6">log</span>.<span style="color:#b8bfc6">error</span>(<span style="color:#d26b6b">"自媒体 save 远程调用出错啦 ~~~ !!!! {} "</span>,<span style="color:#b8bfc6">throwable</span>.<span style="color:#b8bfc6">getMessage</span>());
<span style="color:#c88fd0">return</span> <span style="color:#b8bfc6">ResponseResult</span>.<span style="color:#b8bfc6">errorResult</span>(<span style="color:#b8bfc6">AppHttpCodeEnum</span>.<span style="color:#b8bfc6">SERVER_ERROR</span>);
}
<span style="color:#b7b3b3">@Override</span>
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">ResponseResult</span><span style="color:#b8bfc6"><</span><span style="color:#b8bfc6">WmUser</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">findByName</span>(<span style="color:#1cc685">String</span> <span style="color:#b8bfc6">name</span>) {
<span style="color:#b8bfc6">log</span>.<span style="color:#b8bfc6">error</span>(<span style="color:#d26b6b">"参数: {}"</span>,<span style="color:#b8bfc6">name</span>);
<span style="color:#b8bfc6">log</span>.<span style="color:#b8bfc6">error</span>(<span style="color:#d26b6b">"自媒体 findByName 远程调用出错啦 ~~~ !!!! {} "</span>,<span style="color:#b8bfc6">throwable</span>);
<span style="color:#c88fd0">return</span> <span style="color:#b8bfc6">ResponseResult</span>.<span style="color:#b8bfc6">errorResult</span>(<span style="color:#b8bfc6">AppHttpCodeEnum</span>.<span style="color:#b8bfc6">SERVER_ERROR</span>);
}
};
}
}</span></span>
2.1.2 引入阿里云内容安全服务
在admin微服务中加入如下配置
文章审核需要调用阿里云服务的云安全服务来审核文章和图片
已整合到admin
2.2 文章审核功能-业务层接口定义
新建自动审核接口:
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#c88fd0">package</span> <span style="color:#8d8df0">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">admin</span>.<span style="color:#b8bfc6">service</span>;
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">interface</span> <span style="color:#8d8df0">WemediaNewsAutoScanService</span> {
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 自媒体文章审核</span>
<span style="color:#da924a">* @param wmNewsId</span>
<span style="color:#da924a">*/</span>
<span style="color:#c88fd0">public</span> <span style="color:#1cc685">void</span> <span style="color:#b8bfc6">autoScanByMediaNewsId</span>(<span style="color:#1cc685">Integer</span> <span style="color:#b8bfc6">wmNewsId</span>);
}</span></span>
2.3 文章审核功能-业务逻辑实现
2.3.1 抽取文章内容中文本和图片
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.admin.service.impl;
import com.alibaba.fastjson.JSON;
import com.heima.admin.service.WemediaNewsAutoScanService;
import com.heima.aliyun.GreenImageScan;
import com.heima.aliyun.GreenTextScan;
import com.heima.common.exception.CustException;
import com.heima.feigns.WemediaFeign;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.wemedia.pojos.WmNews;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service
@Slf4j
public class WemediaNewsAutoScanServiceImpl implements WemediaNewsAutoScanService {
@Autowired
WemediaFeign wemediaFeign;
@Autowired
GreenTextScan greenTextScan;
@Autowired
GreenImageScan greenImageScan;
@Value("${file.oss.web-site}")
private String webSite;
/**
* 自媒体文章自动审核
* @param wmNewsId wmNews 文章ID
*/
@Override
public void autoScanByMediaNewsId(Integer wmNewsId) {
//参数校验
if (wmNewsId == null) {
log.error("当前待审核 文章ID不能为空");
CustException.cust(AppHttpCodeEnum.PARAM_INVALID,"当前的审核ID不能为空");
}
//1 根据WmNes文章id查询文章
ResponseResult<WmNews> wmNewsResult = wemediaFeign.findById(wmNewsId);
if(wmNewsResult.getCode().intValue()!=0){
CustException.cust(AppHttpCodeEnum.REMOTE_SERVER_ERROR);
}
WmNews wmNews = wmNewsResult.getData();
if (wmNews == null) {
log.error("审核的自媒体文章不存在,自媒体的id:{}", wmNewsId);
CustException.cust(AppHttpCodeEnum.DATA_NOT_EXIST,"当前的审核文章不存在");
}
//判断文章的状态
//2 文章状态=4 人工审核通过,直接保存文章
if (wmNews.getStatus().shortValue()==WmNews.Status.ADMIN_SUCCESS.getCode() && wmNews.getPublishTime().getTime() <= System.currentTimeMillis()) {
//保存数据
publishArticle(wmNews);
return;
}
//3 文章状态=8 审核通过 但是发布时间小于等于当前时间,直接保存文章
if (wmNews.getStatus().shortValue()==WmNews.Status.SUCCESS.getCode()
&& wmNews.getPublishTime().getTime() <= System.currentTimeMillis()) {
//保存数据
publishArticle(wmNews);
return;
}
//4 文章状态=1 待审核
if (wmNews.getStatus().shortValue()==WmNews.Status.SUBMIT.getCode()) {
// 抽取文章内容中的文本和图片
Map<String, Object> contentAndImagesResult = handleTextAndImages(wmNews);
//TODO 4.1 自管理敏感词审核
//TODO 4.2 阿里云审核文本内容
//TODO 4.3 阿里云图片审核
//TODO 4.4 判断文章发布时间大于当前时间,修改状态8
//TODO 5 保存文章相关数据
//TODO 6 同步索引库
//TODO 7 生成文章静态页面
}
}
/**
* 抽取文章的 文本和图片(包含封面的图片[不带前缀])
* @param wmNews
* @return Map 图片带前缀
*/
private Map<String, Object> handleTextAndImages(WmNews wmNews) {
//1 获取文章内容并转换对象
String content = wmNews.getContent();
if (StringUtils.isBlank(content) || content.length()>10000) {
log.error("文章内容 校验错误 为空 或 长度大于10000");
CustException.cust(AppHttpCodeEnum.PARAM_INVALID,"文章内容不合法");
}
List<Map> contentList = JSON.parseArray(content, Map.class);
//2 解析内容文本 Stream 流优化 --> String
// 中美
// 国家英语 美_hmtt_国?
String contents = contentList.stream()
.filter(x -> x.get("type").equals("text"))
.map(y -> y.get("value").toString())
.collect(Collectors.joining("_"));
// 文章标题 参与审核
contents = wmNews.getTitle()+"_"+contents;
//3 解析内容图片
List<String> images = contentList.stream()
.filter(x -> x.get("type").equals("image"))
.map(y -> y.get("value").toString())
.collect(Collectors.toList());
//4 处理封面前缀 1.jpg 2.jpg
String imageCover = wmNews.getImages();
if (StringUtils.isNotBlank(imageCover)) {
// 添加到内容图片列表
images.addAll(
Stream.of(imageCover.split(","))
.map(x -> webSite+x)
.collect(Collectors.toList())
);
}
//5 封装结果
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("content", contents);
resultMap.put("images", images);
return resultMap;
}
/**
* 保存APP端文章相关数据
* @param wmNews
*/
private void publishArticle(WmNews wmNews) {
}
}</span></span>
配置文件 application.yml 添加 文章图片前缀:
<span style="background-color:#333333"><span style="color:#b8bfc6">
file:
oss:
web-site: http://heimaleadnewsoss.oss-cn-shanghai.aliyuncs.com/</span></span>
2.3.2 自管理敏感词审核
(1)在AdSensitiveMapper 新增方法
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.heima.model.admin.pojos.AdSensitive;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @Description: AdSensitiveMapper
* @Version: V1.0
*/
public interface AdSensitiveMapper extends BaseMapper<AdSensitive> {
@Select("select sensitives from ad_sensitive")
public List<String> findAllSensitive();
}</span></span>
(2)具体业务实现
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.admin.service.impl;
import com.alibaba.fastjson.JSON;
import com.heima.admin.service.WemediaNewsAutoScanService;
import com.heima.common.aliyun.GreeTextScan;
import com.heima.common.aliyun.GreenImageScan;
import com.heima.common.exception.CustException;
import com.heima.feigns.wemedia.WemediaFeign;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.wemedia.pojos.WmNews;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
public class WemediaNewsAutoScanServiceImpl implements WemediaNewsAutoScanService {
/**
* 自媒体文章自动审核
* @param wmNewsId wmNews 文章ID
*/
@Override
public void autoScanByMediaNewsId(Integer wmNewsId) {
//参数校验
//1 根据WmNes文章id查询文章
//判断文章的状态
//2 文章状态=4 人工审核通过,直接保存文章
//3 文章状态=8 审核通过 但是发布时间小于等于当前时间,直接保存文章
//4 文章状态=1 待审核
if (wmNews.getStatus().shortValue()==WmNews.Status.SUBMIT.getCode()) {
// 抽取文章内容中的文本和图片
//4.1 自管理敏感词审核
boolean isSensitiveScan = handleSensitive((String) contentAndImagesResult.get("content"), wmNews);
if (!isSensitiveScan) return;
//TODO 4.2 阿里云审核文本内容
//TODO 4.3 阿里云图片审核
//TODO 4.4 判断文章发布时间大于当前时间,修改状态8
//TODO 5 保存文章相关数据
//TODO 6 同步索引库
}
}
@Autowired
AdSensitiveMapper adSensitiveMapper;
/**
* 自管理敏感词审核
* @param content
* @param wmNews
* @return
*/
private boolean handleSensitive(String content, WmNews wmNews) {
boolean flag = true;
// 查询所有敏感词
List<String> adSensitives = adSensitiveMapper.findAllSensitive();
// 初始化 DFA搜索
SensitiveWordUtil.initMap(adSensitives);
// 匹配敏感词
Map<String, Integer> map = SensitiveWordUtil.matchWords(content);
if (map.size() > 0) {
flag = false;
updateWmNews(wmNews, (short)2,"文章中包含了敏感词:"+map);
}
return flag;
}
/**
* 修改文章
* @param wmNews
* @param status
* @param msg
*/
private void updateWmNews(WmNews wmNews, Short status, String msg) {
wmNews.setStatus(status);
wmNews.setReason(msg);
ResponseResult responseResult = wemediaFeign.updateWmNews(wmNews);
if(responseResult.getCode().intValue()!=0){
log.error("远程调用 updateWmNews 方法失败 参数:{}",wmNews);
CustException.cust(AppHttpCodeEnum.SERVER_ERROR,responseResult.getErrorMessage());
}
}
}</span></span>
2.3.3 阿里云文本审核
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.admin.service.impl;
import com.alibaba.fastjson.JSON;
import com.heima.admin.service.WemediaNewsAutoScanService;
import com.heima.common.aliyun.GreeTextScan;
import com.heima.common.aliyun.GreenImageScan;
import com.heima.common.exception.CustException;
import com.heima.feigns.wemedia.WemediaFeign;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.wemedia.pojos.WmNews;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
public class WemediaNewsAutoScanServiceImpl implements WemediaNewsAutoScanService {
/**
* 自媒体文章自动审核
* @param wmNewsId wmNews 文章ID
*/
@Override
public void autoScanByMediaNewsId(Integer wmNewsId) {
//参数校验
//1 根据WmNes文章id查询文章
//判断文章的状态
//2 文章状态=4 人工审核通过,直接保存文章
//3 文章状态=8 审核通过 但是发布时间小于等于当前时间,直接保存文章
//4 文章状态=1 待审核
if (wmNews.getStatus().shortValue()==WmNews.Status.SUBMIT.getCode()) {
// 抽取文章内容中的文本和图片
//4.1 自管理敏感词审核
//4.2 阿里云审核文本内容
boolean isTextScan = handleTextScan((String) contentAndImagesResult.get("content"), wmNews);
if (!isTextScan) return;
//TODO 4.3 阿里云图片审核
//TODO 4.4 判断文章发布时间大于当前时间,修改状态8
//TODO 5 保存文章相关数据
//TODO 6 同步索引库
}
}
/**
* 审核文内容-阿里云
* @param content
* @param wmNews
* @return
*/
private boolean handleTextScan(String content, WmNews wmNews) {
boolean flag = true;
try {
Map map = greenTextScan.greenTextScan(content);
//审核不通过
String suggestion = (String)map.get("suggestion");
switch (suggestion){
case "block":
flag = false;
updateWmNews(wmNews, WmNews.Status.FAIL.getCode(), "文本中内容有敏感词汇");
break;
case "review":
flag = false;
updateWmNews(wmNews, WmNews.Status.ADMIN_AUTH.getCode(), "文本中内容需要人工审核");
break;
}
} catch (Exception e) {
e.printStackTrace();
log.error("文章阿里云内容审核失败,转入人工审核, e:{}",e);
flag = false;
updateWmNews(wmNews, WmNews.Status.ADMIN_AUTH.getCode(), "文章阿里云内容审核失败,转入需要人工审核");
}
return flag;
}
}</span></span>
2.3.4 阿里云图片审核
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.admin.service.impl;
import com.alibaba.fastjson.JSON;
import com.heima.admin.service.WemediaNewsAutoScanService;
import com.heima.common.aliyun.GreeTextScan;
import com.heima.common.aliyun.GreenImageScan;
import com.heima.common.exception.CustException;
import com.heima.feigns.wemedia.WemediaFeign;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.wemedia.pojos.WmNews;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
public class WemediaNewsAutoScanServiceImpl implements WemediaNewsAutoScanService {
/**
* 自媒体文章自动审核
*
* @param wmNewsId wmNews 文章ID
*/
@Override
public void autoScanByMediaNewsId(Integer wmNewsId) {
//参数校验
//1 根据WmNes文章id查询文章
//判断文章的状态
//2 文章状态=4 人工审核通过,直接保存文章
//3 文章状态=8 审核通过 但是发布时间小于等于当前时间,直接保存文章
//4 文章状态=1 待审核
if (wmNews.getStatus().shortValue()==WmNews.Status.SUBMIT.getCode()) {
// 抽取文章内容中的文本和图片
//4.1 自管理敏感词审核
//4.2 阿里云审核文本内容
//4.3 阿里云图片审核
boolean isImageScan = handleImagesScan((List) contentAndImagesResult.get("images"), wmNews);
if (!isImageScan) return;
//4.4 如果发布时间 大于 当前时间, 修改wmNews 文章状态=8
if (wmNews.getPublishTime().after(new Date())) {
updateWmNews(wmNews, WmNews.Status.SUCCESS.getCode(), "文章审核通过,待发布");
return;
}
//5 审核通过,保存文章相关信息(三张表), 修改文章的状态=9 已发布
publishArticle(wmNews);
//TODO 6 发消息生成文章静态页面
//TODO 7 同步索引库
}
}
/**
* 图片审核-阿里云
* @param images
* @param wmNews
* @return
*/
private boolean handleImagesScan(List<String> images, WmNews wmNews) {
// 无图则无需审核
if(images==null || images.size() == 0){
return true;
}
boolean flag = true;
try {
images=images.stream().distinct()
.collect(Collectors.toList());
Map map = greenImageScan.imageUrlScan(images);
//审核不通过
if (map.get("suggestion").equals("block")) {
updateWmNews(wmNews, 2, "文章图片有违规");
flag = false;
}
//人工审核
if (map.get("suggestion").equals("review")) {
updateWmNews(wmNews,3, "文章图片有不确定元素");
flag = false;
}
} catch (Exception e) {
e.printStackTrace();
updateWmNews(wmNews, 3, "阿里云审核失败,转入人工审核");
flag = false;
}
return flag;
}
}</span></span>
2.3.5 审核阶段单元测试
启动article和wemedia微服务
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.admin.test;
import com.heima.admin.AdminApplication;
import com.heima.admin.service.WemediaNewsAutoScanService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest(classes = AdminApplication.class)
@RunWith(SpringRunner.class)
public class WemediaNewsAutoScanServiceTest {
@Autowired
private WemediaNewsAutoScanService wemediaNewsAutoScanService;
@Test
public void testScanNews(){
wemediaNewsAutoScanService.autoScanByMediaNewsId(6203);
}
}</span></span>
2.4 文章微服务feign接口
2.4.1 分布式id
随着业务的增长,文章表可能要占用很大的物理存储空间,为了解决该问题,后期使用数据库分片技术。将一个数据库进行拆分,通过数据库中间件连接。如果数据库中该表选用ID自增策略,则可能产生重复的ID,此时应该使用分布式ID生成策略来生成ID。
雪花算法实现
snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0
mybatis-plus已经集成了雪花算法,完成以下两步即可在项目中集成雪花算法
第一:在实体类中的id上加入如下配置,指定类型为id_worker
<span style="background-color:#333333"><span style="color:#b8bfc6">@TableId(value = "id",type = IdType.ID_WORKER)
private Long id;</span></span>
第二:在article服务的application.yml文件中配置数据中心id和机器id
<span style="background-color:#333333"><span style="color:#b8bfc6">mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
# 设置别名包扫描路径,通过该属性可以给包中的类注册别名
type-aliases-package: com.heima.model.article.pojos
global-config:
datacenter-id: 1
workerId: 1</span></span>
datacenter-id:数据中心id(取值范围:0-31)
workerId:机器id(取值范围:0-31)
2.4.2 需求说明和思路分析
在文章审核成功以后需要在app的article库中新增文章数据
-
保存文章信息 ap_article
-
保存文章配置信息 ap_article_config
-
保存文章内容 ap_article_content
具体实现的思路:
当前接口具备保存三个表数据的功能,如果已经有了文章id,则是修改,如果没有id为新增数据。
同时需要把资料文件夹中的类拷贝到文章微服务下:
2.4.3 文章微服务参数准备
ArticleDto
在远程调用当前接口的时候,因为ap_article表中并没有保存content的字段,在远程调用传递的过程中,需要把这个内容传递到文章微服务,再保存到ap_article_content表中
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.model.article.dtos;
import com.heima.model.article.pojos.ApArticle;
import lombok.Data;
@Data
public class ArticleDto extends ApArticle {
/**
* 文章内容
*/
private String content;
/**
* 自媒体用户id
*/
private Integer wmUserId;
}</span></span>
在 WemediaNewsAutoScanServiceImpl 编写 publishArticle 方法,执行保存文章相关数据
<span style="background-color:#333333"><span style="color:#b8bfc6"> @Autowired
ArticleFeign articleFeign;
/**
* 发布文章方法
* @param wmNews
*/
private void publishArticle(WmNews wmNews) {
//1. 远程调用saveArticle 保存3张表的信息
ResponseResult responseResult = saveArticle(wmNews);
if (responseResult.getCode().intValue()!=0) {
log.error("远程保存apArticle信息失败: {}",responseResult.getErrorMessage());
CustException.cust(AppHttpCodeEnum.SERVER_ERROR,"远程保存apArticle信息失败");
}
//2. 修改自媒体文章状态 9
wmNews.setArticleId((Long)responseResult.getData());
updateWmNews(wmNews,(short) 9 ,"发布成功");
//TODO 3. 通知ES索引库 添加对应文章
}
@Autowired
AdChannelMapper adChannelMapper;
/**
* 远程调用feign保存三张表信息
* @param wmNews
* @return
*/
private ResponseResult saveArticle(WmNews wmNews) {
//1. 封装dto参数
ArticleDto articleDto = new ArticleDto();
BeanUtils.copyProperties(wmNews,articleDto);
// 如果之前没发布过 id为null 发布过为之前关联的articleId
articleDto.setId(wmNews.getArticleId());
articleDto.setLayout(wmNews.getType()); // 布局
if(wmNews.getChannelId()!=null){
// 设置频道信息
AdChannel channel = adChannelMapper.selectById(wmNews.getChannelId());
articleDto.setChannelName(channel.getName());
}
// 0 普通文章
articleDto.setFlag((byte) 0);
// 自媒体用户id 用于在article服务中查询相关作者信息
articleDto.setWmUserId(wmNews.getUserId());
// 远程调用 article微服务保存3张表方法
return articleFeign.saveArticle(articleDto);
}</span></span>
2.4.4 文章端定义接口
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.article.controller.v1;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.common.dtos.ResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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;
@Api(value = "app文章管理API",tags = "app文章管理API")
@RestController
@RequestMapping("/api/v1/article")
public class ApArticleController {
@ApiOperation(value = "保存app文章",notes = "保存 app_article信息,ap_article_config信息,ap_article_content信息")
@PostMapping("/save")
public ResponseResult saveArticle(@RequestBody ArticleDto articleDto) {
return null;
}
}</span></span>
2.4.5 业务层
在ApArticleService接口中新增如下方法
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.article.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.article.pojos.ApArticle;
import com.heima.model.common.dtos.ResponseResult;
public interface ApArticleService extends IService<ApArticle> {
/**
* 保存或修改文章
* @param articleDto
* @return
*/
public ResponseResult saveArticle(ArticleDto articleDto);
}</span></span>
实现类:
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.article.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.article.mapper.ApArticleConfigMapper;
import com.heima.article.mapper.ApArticleContentMapper;
import com.heima.article.mapper.ApArticleMapper;
import com.heima.article.mapper.AuthorMapper;
import com.heima.article.service.ApArticleService;
import com.heima.common.exception.CustException;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.article.pojos.ApArticle;
import com.heima.model.article.pojos.ApArticleConfig;
import com.heima.model.article.pojos.ApArticleContent;
import com.heima.model.article.pojos.ApAuthor;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class ApArticleServiceImpl extends ServiceImpl<ApArticleMapper, ApArticle> implements ApArticleService {
@Autowired
AuthorMapper authorMapper;
@Autowired
ApArticleConfigMapper apArticleConfigMapper;
@Autowired
ApArticleContentMapper apArticleContentMapper;
/**
* 保存app文章
* @param articleDto
* @return
*/
@Override
public ResponseResult saveArticle(ArticleDto articleDto) {
// 1. 封装 article对象
ApArticle apArticle = dtoToArticle(articleDto);
// 2. 保存或修改文章
Long articleId = saveOrUpdateArticle(apArticle);
// 3. 保存 文章 内容 和 配置 信息
saveContentAndConfig(articleId,articleDto.getContent());
//返回文章ID
return ResponseResult.okResult(apArticle.getId());
}
/**
* 基于dto封装 Article
* @param articleDto
* @return
*/
private ApArticle dtoToArticle(ArticleDto articleDto) {
ApArticle apArticle = new ApArticle();
BeanUtils.copyProperties(articleDto, apArticle);
// 根据自媒体用户id 查询 作者id
ApAuthor author = authorMapper.selectOne(Wrappers.<ApAuthor>lambdaQuery()
.eq(ApAuthor::getWmUserId, articleDto.getWmUserId()));
if (author == null) {
CustException.cust(AppHttpCodeEnum.DATA_NOT_EXIST);
}
apArticle.setAuthorName(author.getName());
apArticle.setAuthorId(Long.valueOf(author.getId()));
return apArticle;
}
/**
* 保存文章配置 及 文章内容
* @param articleId
* @param content
*/
private void saveContentAndConfig(Long articleId, String content) {
//保存article_config 数据
ApArticleConfig apArticleConfig = new ApArticleConfig();
apArticleConfig.setIsForward(true); // 是否允许转发
apArticleConfig.setIsComment(true);// 是否允许评论
apArticleConfig.setIsDown(false);// 是否下架
apArticleConfig.setIsDelete(false);// 是否删除
apArticleConfig.setArticleId(articleId);// 文章id
apArticleConfigMapper.insert(apArticleConfig);
//添加文章内容
//保存文章article_content 数据
ApArticleContent apArticleContent = new ApArticleContent();
apArticleContent.setContent(content);// 文章内容
apArticleContent.setArticleId(articleId); // 文章id
apArticleContentMapper.insert(apArticleContent);
}
/**
* 保存或修改文章信息
* @param apArticle
* @return
*/
private Long saveOrUpdateArticle(ApArticle apArticle) {
if (apArticle.getId() == null) {
// 普通文章
apArticle.setComment(0);
apArticle.setViews(0);
apArticle.setCollection(0);
apArticle.setLikes(0);
// 保存
save(apArticle); // 返回ID
} else {
// 5. 如果文章ID不为null 修改文章
// 并删除之前配置 和 文章内容
ApArticle article = getById(apArticle.getId());
if (article == null) {
log.info("文章信息为空 : {}", apArticle.getId());
CustException.cust(AppHttpCodeEnum.DATA_NOT_EXIST);
}
// 删除之前配置信息
apArticleConfigMapper.delete(Wrappers.<ApArticleConfig>lambdaQuery().eq(ApArticleConfig::getArticleId, apArticle.getId()));
// 删除之前内容信息
apArticleContentMapper.delete(Wrappers.<ApArticleContent>lambdaQuery().eq(ApArticleContent::getArticleId, apArticle.getId()));
// 修改文章
updateById(apArticle);
}
return apArticle.getId();
}
}</span></span>
2.4.6 控制层
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.article.controller.v1;
import com.heima.article.service.ApArticleService;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.common.dtos.ResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Api(value = "app文章管理API",tags = "app文章管理API")
@RestController
@RequestMapping("/api/v1/article")
public class ApArticleController {
@Autowired
ApArticleService apArticleService;
@ApiOperation(value = "保存app文章",notes = "保存 app_article信息,ap_article_config信息,ap_article_content信息")
@PostMapping("/save")
public ResponseResult saveArticle(@RequestBody ArticleDto articleDto) {
return apArticleService.saveArticle(articleDto);
}
}</span></span>
2.4.7 在feign端定义远程接口
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.feigns;
import com.heima.config.HeimaFeignAutoConfiguration;
import com.heima.feigns.fallback.ArticleFeignFallback;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.article.pojos.ApAuthor;
import com.heima.model.common.dtos.ResponseResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@FeignClient(value = "leadnews-article",
fallbackFactory = ArticleFeignFallback.class,
configuration = HeimaFeignAutoConfiguration.class
)
public interface ArticleFeign {
@GetMapping("/api/v1/author/findByUserId/{userId}")
ResponseResult<ApAuthor> findByUserId(@PathVariable("userId") Integer userId);
@PostMapping("/api/v1/author/save")
ResponseResult save(@RequestBody ApAuthor apAuthor);
// =================新增接口=====================
@PostMapping("/api/v1/article/save")
ResponseResult saveArticle(@RequestBody ArticleDto articleDto);
// =================新增接口=====================
}</span></span>
服务降级方法
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.feigns.fallback;
import com.heima.feigns.ArticleFeign;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.article.pojos.ApAuthor;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class ArticleFeignFallback implements FallbackFactory<ArticleFeign> {
@Override
public ArticleFeign create(Throwable throwable) {
return new ArticleFeign() {
@Override
public ResponseResult<ApAuthor> findByUserId(Integer userId) {
log.error("参数 userId : {}",userId);
log.error("ArticleFeign findByUserId 远程调用出错啦 ~~~ !!!! {} ",throwable.getMessage());
return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR);
}
@Override
public ResponseResult save(ApAuthor apAuthor) {
log.error("参数 apAuthor: {}",apAuthor);
log.error("ArticleFeign save 远程调用出错啦 ~~~ !!!! {} ",throwable.getMessage());
return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR);
}
@Override
public ResponseResult saveArticle(ArticleDto articleDto) {
log.error("参数 articleDto: {}",articleDto);
log.error("ArticleFeign saveArticle 远程调用出错啦 ~~~ !!!! {} ",throwable.getMessage());
return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR);
}
};
}
}</span></span>
2.5 文章审核功能-app文章保存测试
启动article和wemedia微服务
注意查看作者表 是否关联wmUser表id ,如果没有 手动添加对应自媒体用户id
查看以下结果:
-
wm_news 数据状态是否发生变化
-
ap_article 表新增数据
-
ap_article_content 表新增数据
-
ap_article_config 表新增数据
3 文章审核功能-文章提交审核&监听接收消息
在审核文章流程的第一步,当自媒体人发布一篇文章后会马上进行审核,这个时候是通过消息中间件进行数据的传递的。所以说需要配置生产者和消费者。
-
自媒体微服务就是生产者发送文章的id
-
admin就是消费者接收文章id
3.1 配置kafka环境
在services
聚合模块中添加依赖
<span style="background-color:#333333"><span style="color:#b8bfc6"><!-- kafkfa -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency></span></span>
3.2 配置生产者
在wemedia模块中的application.yml加入kafka的配置
<span style="background-color:#333333"><span style="color:#b8bfc6">spring:
application:
name: leadnews-wemedia
kafka:
bootstrap-servers: 192.168.200.130:9092
producer:
retries: 0
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer</span></span>
在发布文章修改代码,发送消息,提交审核
定义topic名称
新建常量类:
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.common.constants.message;
public class NewsAutoScanConstants {
public static final String WM_NEWS_AUTO_SCAN_TOPIC="wm.news.auto.scan.topic";
}</span></span>
在wemedia微服务中修改业务层实现类WmNewsServiceImpl的saveWmNews方法,修改如下代码,发送消息进行文章审核
<span style="background-color:#333333"><span style="color:#b8bfc6">@Autowired
KafkaTemplate<String,String> kafkaTemplate;
@Override
public ResponseResult submitNews(WmNewsDto dto) {
//3.3 关联文章封面中的图片和素材关系 封面可能是选择自动或者是无图
if (status == WmNews.Status.SUBMIT.getCode()) {
saveRelativeInfoForCover(wmNewsDto,materials, wmNews);
Map<String, Integer> map = new HashMap<>();
map.put("newsId", wmNews.getId());
kafkaTemplate.send(NewsAutoScanConstants.WM_NEWS_AUTO_SCAN_TOPIC, JSON.toJSONString(map));
}
return ResponseResult.okResult();
}</span></span>
3.3 配置消费者
(1)在admin-service
微服务中修改application.yml
文件,增加kafka相关配置
<span style="background-color:#333333"><span style="color:#b8bfc6">spring:
application:
name: leadnews-admin
kafka:
bootstrap-servers: 192.168.200.130:9092
consumer:
group-id: ${spring.application.name}-kafka-group
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer</span></span>
(2)自媒体文章发布以后会发消息过来自动进行审核,需要在admin端来接收消息,处理审核
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.admin.listen;
import com.alibaba.fastjson.JSON;
import com.heima.admin.service.WemediaNewsAutoScanService;
import com.heima.common.constants.message.NewsAutoScanConstants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @Description:
* @Version: V1.0
*/
@Component
@Slf4j
public class WemediaNewsAutoListener {
@Autowired
WemediaNewsAutoScanService wemediaNewsAutoScanService;
@KafkaListener(topics = NewsAutoScanConstants.WM_NEWS_AUTO_SCAN_TOPIC,errorHandler = "kafkaExceptionHandler")
public void recevieMessage(String message) {
if (StringUtils.isNotBlank(message)) {
log.info("====WemediaNewsAutoListener recevieMessage :{}", message);
Map map = JSON.parseObject(message, Map.class);
Object wmNewsId = map.get("wmNewsId");
if (wmNewsId != null) {
wemediaNewsAutoScanService.autoScanByMediaNewsId((Integer) wmNewsId);
}
log.info("====autoScanByMediaNewsId success====");
}
}
}</span></span>
注意: Spring 为消息队列提供了AOP,在监听方法中 如果出现异常 Spring会触发消费重试 默认10次
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.admin.listen;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.Consumer;
import org.springframework.kafka.listener.KafkaListenerErrorHandler;
import org.springframework.kafka.listener.ListenerExecutionFailedException;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
@Component("kafkaExceptionHandler")
@Slf4j
public class KafkaExceptionHandler implements KafkaListenerErrorHandler {
@Override
public Object handleError(Message<?> message, ListenerExecutionFailedException e) {
log.error("kafka处理消息出现异常 消息内容==> {} 异常信息==> {} ",message.getPayload().toString(),e.getMessage());
return null;
}
@Override
public Object handleError(Message<?> message, ListenerExecutionFailedException exception, Consumer<?, ?> consumer) {
log.error("kafka处理消息出现异常 消息内容==> {} 异常信息==> {} ",message.getPayload().toString(),exception.getMessage());
return null;
}
}
</span></span>
4 文章审核功能-解决分布式事务
目前项目中已经全部集成了seata,没有集成的需要按照之前的步骤在项目中配置,然后在对应的业务方法上进行注解控制即可。
admin微服务添加seata依赖:
<span style="background-color:#333333"><span style="color:#b8bfc6"><dependency>
<groupId>com.heima</groupId>
<artifactId>heima-seata-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency></span></span>
application.yml配置文件:
<span style="background-color:#333333"><span style="color:#b8bfc6">spring:
cloud:
alibaba:
seata:
tx-service-group: heima_leadnews_tx_group
autoconfigure:
exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration</span></span>
在WeMediaNewsAutoScanServiceImpl的autoScanByMediaNewsId方法加上注解@GlobalTransactional
5 文章审核功能-综合测试
服务启动列表:
-
nacos
-
seata
-
zookeeper&kafka
-
article微服务
-
wemedia微服务
-
admin微服务
-
wemedia网关微服务
-
启动前端系统wemedia
测试动作:在自媒体前端进行发布文章
结果:
-
审核成功后,app端的article相关数据是否可以正常插入
-
审核成功或失败后,wm_news表中的状态是否改变,成功和失败原因正常插入
6 文章详情页面静态化
6.1 需求分析
6.2 实现方案
6.2.1 基于数据库查询方案
用户某一条文章,根据文章的id去查询文章内容表,返回渲染页面
优点:
-
实现简单
-
保证数据强一致性
缺点:
-
无法支撑高并发
6.2.2 页面静态化方案
优点:
-
支撑高并发,高可用
-
页面响应快,用户体验好
缺点:
-
强一致性较弱,但能够保证最终一致性
6.3 静态页面生成
6.3.1 修改apArticle实体类及表
添加用于存储静态页url字段
修改实体类
<span style="background-color:#333333"><span style="color:#b8bfc6">@TableField("static_url")
private String staticUrl;</span></span>
6.3.2 article微服务集成文件存储
修改pom依赖
<span style="background-color:#333333"><span style="color:#b8bfc6"> <dependencies>
<dependency>
<artifactId>heima-seata-spring-boot-starter</artifactId>
<groupId>com.heima</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.heima</groupId>
<artifactId>heima-file-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
</dependencies></span></span>
修改application.yml 新增freemarker 及 文件存储配置
<span style="background-color:#333333"><span style="color:#b8bfc6">server:
port: 9003
spring:
application:
name: leadnews-article
cloud:
nacos:
discovery:
server-addr: 192.168.200.150:8848
alibaba:
seata:
tx-service-group: heima_leadnews_tx_group
# ===============新增依赖 start ===================
freemarker:
cache: false #关闭模板缓存,方便测试
settings:
template_update_delay: 0 #检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试
suffix: .ftl #指定Freemarker模板文件的后缀名
template-loader-path: classpath:/templates/
# ===============新增依赖 end ===================
autoconfigure:
exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.200.150:3306/leadnews_article?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: root
# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
# 设置别名包扫描路径,通过该属性可以给包中的类注册别名
type-aliases-package: com.heima.model.article.pojos
global-config:
datacenter-id: 1
workerId: 1
# ===============新增依赖 start ===================
file:
oss:
bucket-name: hmtt122
access-key-id: LTAI5tQpuFC6ZMTkUnzCoYHY
access-key-secret: rOx9bai1cmEPdrIOpF9aa8OEhTROlz
endpoint: oss-cn-shanghai.aliyuncs.com
web-site: https://hmtt122.oss-cn-shanghai.aliyuncs.com/
proxy-username: aliyun-sdk-java
socket-timeout: 10000
idle-connection-time: 10000
connection-request-timeout: 4000
max-connections: 2048
max-error-retry: 5
white-list: 127.0.0.1
connection-timeout: 10000
prefix: article
minio:
accessKey: minio
secretKey: minio123
bucket: hmtt122
endpoint: http://192.168.200.150:9090/
readPath: http://192.168.200.150:9090/
prefix: article
# ===============新增依赖 end ===================</span></span>
导入资料中 文章freemarker模板
6.3.3 基于freemarker生成静态页
静态页service
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.article.service;
public interface GeneratePageService {
/**
* 生成文章静态页
* @param articleId
*/
public void generateArticlePage(Long articleId);
}</span></span>
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.article.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.heima.article.mapper.ApArticleContentMapper;
import com.heima.article.mapper.ApArticleMapper;
import com.heima.article.mapper.AuthorMapper;
import com.heima.article.service.GeneratePageService;
import com.heima.file.service.FileStorageService;
import com.heima.model.article.pojos.ApArticle;
import com.heima.model.article.pojos.ApArticleContent;
import com.heima.model.article.pojos.ApAuthor;
import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class GeneratePageServiceImpl implements GeneratePageService {
@Autowired
private Configuration configuration;
@Resource(name = "minIOFileStorageService")
private FileStorageService fileStorageService;
@Value("${file.oss.prefix}")
private String prefix;
@Autowired
ApArticleContentMapper apArticleContentMapper;
@Autowired
ApArticleMapper apArticleMapper;
@Autowired
AuthorMapper authorMapper;
/**
* 根据文章id生成文章静态页面
* @param articleId
*/
@Async
public void generateArticlePage(Long articleId) {
//1.获取文章内容
try {
ApArticleContent apArticleContent = apArticleContentMapper.selectOne(Wrappers.<ApArticleContent>lambdaQuery()
.eq(ApArticleContent::getArticleId,articleId));
if (apArticleContent != null && StringUtils.isNotBlank(apArticleContent.getContent())) {
ApArticle apArticle = apArticleMapper.selectById(articleId);
ApAuthor author = authorMapper.selectById(apArticle.getAuthorId());
//2. 模板
Template template = null;
template = configuration.getTemplate("article.ftl");
//3. 数据
Map<String, Object> params = new HashMap<>();
params.put("content", JSONArray.parseArray(apArticleContent.getContent()));// 文章详情
params.put("article", apArticle); // 文章信息
params.put("authorApUserId", author.getUserId());// 作者 对应的 apUserId
StringWriter out = new StringWriter();
template.process(params, out);
InputStream is = new ByteArrayInputStream(out.toString().getBytes());
//4.生成页面把html文件上传到minio中
String path = fileStorageService.store(prefix, apArticleContent.getArticleId() + ".html", "text/html", is);
System.out.println(path);
//5.修改ap_article表,保存static_url字段
apArticle.setStaticUrl(path);
apArticleMapper.updateById(apArticle);
}
} catch (Exception e) {
e.printStackTrace();
log.error("文章详情静态页生成失败=====>articleId : {} ========> {}",articleId,e.getCause());
}
}
}</span></span>
新增文章时,异步调用生成页面方法
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.article.service.impl;
@Slf4j
@Service
public class ApArticleServiceImpl extends ServiceImpl<ApArticleMapper, ApArticle> implements ApArticleService {
// -----------------前面代码省略------------------------------------
@Autowired
GeneratePageService generatePageService;
/**
* 保存app文章
* @param articleDto
* @return
*/
@Override
public ResponseResult saveArticle(ArticleDto articleDto) {
// -----------------前面代码省略------------------------------------
//返回文章ID
generatePageService.generateArticlePage(apArticle.getId());
return ResponseResult.okResult(apArticle.getId());
}
}</span></span>
启用异步注解
<span style="background-color:#333333"><span style="color:#b8bfc6">@SpringBootApplication
@MapperScan("com.heima.article.mapper")
// 启用异步注解
@EnableAsync
public class ArticleApplication {
public static void main(String[] args) {
SpringApplication.run(ArticleApplication.class, args);
}
@Bean
PaginationInterceptor paginationInterceptor(){
return new PaginationInterceptor();
}
}</span></span>
6.3.3 前后端集成测试
服务启动列表:
-
nacos
-
seata
-
zookeeper&kafka
-
article微服务
-
wemedia微服务
-
admin微服务
-
wemedia网关微服务
-
启动前端系统wemedia
测试动作:在自媒体前端进行发布文章
结果:
-
审核成功后,app端的article相关数据是否可以正常插入
-
审核成功或失败后,wm_news表中的状态是否改变,成功和失败原因正常插入
-
如果文章审核成功是否可以正常生成静态页面并上传到MinIO
访问生成后的静态文件:
资料中找到plugins文件夹 手动上传到MinIO article文件夹下
将存储的路径修改为相对路径
测试静态页能否访问
听课笔记:
<span style="background-color:#333333"><span style="color:#b8bfc6">1.自媒体文章整体审核的思路
</span></span>
第八章 分布式任务调度&人工审核
今日目标
-
能够理解什么是分布式任务调度
-
能够掌握xxl-job的基本使用
-
能够使用xxl-job解决黑马头条项目中定时任务的功能
-
能够完成自媒体文章人工审核功能
-
能够完成自媒体端文章上下架同步的问题
1 分布式任务调度
详细查看资料文件夹中的xxl-job相关文档。
2 自媒体文章审核-定时任务扫描待发布文章
2.1 需求分析
-
前期回顾:在自媒体文章审核的时候,审核通过后,判断了文章的发布时间大于当前时间,这个时候并没有真正的发布文章,
-
把文章的状态设置为了8(审核通过待发布)
-
如果是人工审核通过,把状态改为了4,也没有发布。
-
-
定时任务的作用就是每分钟去扫描这些待发布的文章,如果当前文章的状态为8或为4的,且发布时间小于等于当前时间 的,立刻发布当前文章。
2.2 自媒体文章数据准备
自动审核的代码都是通过自媒体文章的id进行审核的,这个时候需要在admin端远程调用自媒体端查询文章状态为8或4且发布时间小于当前时间的文章id列表
(1)在WmNewsController新增方法
<span style="background-color:#333333"><span style="color:#b8bfc6"> <span style="color:#da924a">/**</span>
<span style="color:#da924a">* 查询需要发布的文章id列表</span>
<span style="color:#da924a">* @return</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@ApiOperation</span>(<span style="color:#d26b6b">"查询待发布文章id列表"</span>)
<span style="color:#b7b3b3">@GetMapping</span>(<span style="color:#d26b6b">"/findRelease"</span>)
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">ResponseResult</span><span style="color:#b8bfc6"><</span><span style="color:#b8bfc6">List</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">Integer</span><span style="color:#b8bfc6">>></span> <span style="color:#8d8df0">findRelease</span>() {
}</span></span>
(2)业务层
在自媒体微服务中的WmNewsService新增方法
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#da924a">/**</span>
<span style="color:#da924a">* 查询需要发布的文章id列表</span>
<span style="color:#da924a">* @return</span>
<span style="color:#da924a">*/</span>
<span style="color:#b8bfc6">List</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">Integer</span><span style="color:#b8bfc6">></span> <span style="color:#8d8df0">findRelease</span>();</span></span>
实现方法:
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#da924a">/**</span>
<span style="color:#da924a">* 查询需要发布的文章id列表 wm_news 4 8 publishTime < = now()</span>
<span style="color:#da924a">* @return</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@Override</span>
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">List</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">Integer</span><span style="color:#b8bfc6">></span> <span style="color:#8d8df0">findRelease</span>() {
<span style="color:#da924a">//查询状态为4或为8的数据且发布时间小于当前时间</span>
<span style="color:#b8bfc6">List</span><span style="color:#b8bfc6"><</span><span style="color:#b8bfc6">WmNews</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">list</span> <span style="color:#b8bfc6">=</span> <span style="color:#b8bfc6">list</span>(<span style="color:#b8bfc6">Wrappers</span>.<span style="color:#b8bfc6"><</span><span style="color:#b8bfc6">WmNews</span><span style="color:#b8bfc6">></span><span style="color:#b8bfc6">lambdaQuery</span>()
.<span style="color:#b8bfc6">le</span>(<span style="color:#b8bfc6">WmNews</span>::<span style="color:#b8bfc6">getPublishTime</span>, <span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">Date</span>())
.<span style="color:#b8bfc6">in</span>(<span style="color:#b8bfc6">WmNews</span>::<span style="color:#b8bfc6">getStatus</span>, <span style="color:#64ab8f">4</span>, <span style="color:#64ab8f">8</span>)
.<span style="color:#b8bfc6">select</span>(<span style="color:#b8bfc6">WmNews</span>::<span style="color:#b8bfc6">getId</span>));
<span style="color:#b8bfc6">List</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">Integer</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">idList</span> <span style="color:#b8bfc6">=</span> <span style="color:#b8bfc6">list</span>.<span style="color:#b8bfc6">stream</span>().<span style="color:#b8bfc6">map</span>(<span style="color:#b8bfc6">WmNews</span>::<span style="color:#b8bfc6">getId</span>)
.<span style="color:#b8bfc6">collect</span>(<span style="color:#b8bfc6">Collectors</span>.<span style="color:#b8bfc6">toList</span>());
<span style="color:#c88fd0">return</span> <span style="color:#b8bfc6">idList</span>;
}</span></span>
(3)控制器
WmNewsController中调用service
<span style="background-color:#333333"><span style="color:#b8bfc6"> <span style="color:#da924a">/**</span>
<span style="color:#da924a">* 查询需要发布的文章id列表</span>
<span style="color:#da924a">* @return</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@ApiOperation</span>(<span style="color:#d26b6b">"查询待发布文章id列表"</span>)
<span style="color:#b7b3b3">@GetMapping</span>(<span style="color:#d26b6b">"/findRelease"</span>)
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">ResponseResult</span><span style="color:#b8bfc6"><</span><span style="color:#b8bfc6">List</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">Integer</span><span style="color:#b8bfc6">>></span> <span style="color:#8d8df0">findRelease</span>() {
<span style="color:#c88fd0">return</span> <span style="color:#b8bfc6">ResponseResult</span>.<span style="color:#b8bfc6">okResult</span>(<span style="color:#b8bfc6">wmNewsService</span>.<span style="color:#b8bfc6">findRelease</span>());
}</span></span>
(4)在feign模块下添加远程调用feign接口
修改WemediaFeign接口添加方法
<span style="background-color:#333333"><span style="color:#b8bfc6"> <span style="color:#da924a">//=====================新增远程接口=======================</span>
<span style="color:#b7b3b3">@GetMapping</span>(<span style="color:#d26b6b">"/api/v1/news/findRelease"</span>)
<span style="color:#b8bfc6">ResponseResult</span><span style="color:#b8bfc6"><</span><span style="color:#b8bfc6">List</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">Integer</span><span style="color:#b8bfc6">>></span> <span style="color:#8d8df0">findRelease</span>();
<span style="color:#da924a">//=====================新增远程接口=======================</span></span></span>
2.3 xxl-job调度中心创建调度任务
(1)新建执行器,原则上一个项目一个执行器,方便管理
执行器:leadnews-admin-executor
(2)新建任务
如下图:
执行器:选择自己新建的执行器
路由策略:轮询
JobHandler名称:wemediaAutoScanJob
Cron: 0 0/1 * * * ?
每分钟执行一次
2.4 xxl-job集成到项目中
(1)引入依赖信息
在 heima-schedule-spring-boot-starter
中引入依赖
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#999977"><</span><span style="color:#7df46a">dependencies</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>com.xuxueli<span style="color:#999977"></</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>xxl-job-core<span style="color:#999977"></</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>
<span style="color:#da924a"><!-- 版本: 2.2.0 --></span>
<span style="color:#999977"></</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>org.springframework.boot<span style="color:#999977"></</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>spring-boot-autoconfigure<span style="color:#999977"></</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>
<span style="color:#999977"></</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>org.springframework.boot<span style="color:#999977"></</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>spring-boot-starter<span style="color:#999977"></</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>
<span style="color:#999977"></</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>org.springframework.boot<span style="color:#999977"></</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>spring-boot-configuration-processor<span style="color:#999977"></</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">optional</span><span style="color:#999977">></span>true<span style="color:#999977"></</span><span style="color:#7df46a">optional</span><span style="color:#999977">></span>
<span style="color:#999977"></</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>org.springframework.boot<span style="color:#999977"></</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>spring-boot-starter-actuator<span style="color:#999977"></</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>
<span style="color:#999977"></</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#999977"></</span><span style="color:#7df46a">dependencies</span><span style="color:#999977">></span></span></span>
(2)创建配置类
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#c88fd0">package</span> <span style="color:#8d8df0">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">jobs</span>.<span style="color:#b8bfc6">config</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">com</span>.<span style="color:#b8bfc6">xxl</span>.<span style="color:#b8bfc6">job</span>.<span style="color:#b8bfc6">core</span>.<span style="color:#b8bfc6">executor</span>.<span style="color:#b8bfc6">impl</span>.<span style="color:#b8bfc6">XxlJobSpringExecutor</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">lombok</span>.<span style="color:#b8bfc6">extern</span>.<span style="color:#b8bfc6">log4j</span>.<span style="color:#b8bfc6">Log4j2</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">beans</span>.<span style="color:#b8bfc6">factory</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">Value</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">context</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">Bean</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">context</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">Configuration</span>;
<span style="color:#b7b3b3">@Log4j2</span>
<span style="color:#b7b3b3">@Configuration</span>
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">class</span> <span style="color:#8d8df0">XxlJobConfig</span> {
<span style="color:#b7b3b3">@Value</span>(<span style="color:#d26b6b">"${xxljob.admin.addresses}"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">String</span> <span style="color:#b8bfc6">adminAddresses</span>;
<span style="color:#b7b3b3">@Value</span>(<span style="color:#d26b6b">"${xxljob.executor.appname}"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">String</span> <span style="color:#b8bfc6">appName</span>;
<span style="color:#b7b3b3">@Value</span>(<span style="color:#d26b6b">"${xxljob.executor.port}"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">int</span> <span style="color:#b8bfc6">port</span>;
<span style="color:#b7b3b3">@Value</span>(<span style="color:#d26b6b">"${xxljob.executor.logPath}"</span>)
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">String</span> <span style="color:#b8bfc6">logPath</span>;
<span style="color:#b7b3b3">@Bean</span>
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">XxlJobSpringExecutor</span> <span style="color:#b8bfc6">xxlJobExecutor</span>() {
<span style="color:#b8bfc6">log</span>.<span style="color:#b8bfc6">info</span>(<span style="color:#d26b6b">">>>>>>>>>>> xxl-job config init."</span>);
<span style="color:#b8bfc6">XxlJobSpringExecutor</span> <span style="color:#b8bfc6">xxlJobSpringExecutor</span> <span style="color:#b8bfc6">=</span> <span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">XxlJobSpringExecutor</span>();
<span style="color:#b8bfc6">xxlJobSpringExecutor</span>.<span style="color:#b8bfc6">setAdminAddresses</span>(<span style="color:#b8bfc6">adminAddresses</span>);
<span style="color:#b8bfc6">xxlJobSpringExecutor</span>.<span style="color:#b8bfc6">setAppname</span>(<span style="color:#b8bfc6">appName</span>);
<span style="color:#b8bfc6">xxlJobSpringExecutor</span>.<span style="color:#b8bfc6">setPort</span>(<span style="color:#b8bfc6">port</span>);
<span style="color:#b8bfc6">xxlJobSpringExecutor</span>.<span style="color:#b8bfc6">setLogRetentionDays</span>(<span style="color:#64ab8f">30</span>);
<span style="color:#b8bfc6">xxlJobSpringExecutor</span>.<span style="color:#b8bfc6">setLogPath</span>(<span style="color:#b8bfc6">logPath</span>);
<span style="color:#c88fd0">return</span> <span style="color:#b8bfc6">xxlJobSpringExecutor</span>;
}
}</span></span>
3 人工审核文章
(4)自动化配置类META-INF/spring.factories
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#8d8df0">org.springframework.boot.autoconfigure.EnableAutoConfiguration</span>=<span style="color:#57ac57">\</span>
<span style="color:#57ac57"> com.heima.jobs.config.XxlJobConfig</span></span></span>
2.5 创建调度任务,定时审核
(1)admin端添加依赖:
<span style="background-color:#333333"><span style="color:#b8bfc6"><dependency>
<groupId>com.heima</groupId>
<artifactId>heima-schedule1-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency></span></span>
(2)application.yml 添加配置
<span style="background-color:#333333"><span style="color:#b8bfc6">xxljob:
admin:
addresses: http://192.168.200.130:8888/xxl-job-admin
executor:
appname: leadnews-admin-executor
port: 9999
logPath: /Users/itcast/itheima/logs</span></span>
创建任务,查询自媒体文章后进行审核
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.admin.job;
import com.heima.admin.service.WemediaNewsAutoScanService;
import com.heima.feigns.WemediaFeign;
import com.heima.model.common.dtos.ResponseResult;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@Slf4j
public class WeMediaNewsAutoScanJob {
@Autowired
private WemediaNewsAutoScanService wemediaNewsAutoScanService;
@Autowired
private WemediaFeign wemediaFeign;
/**
* 定时扫描待发布文章
* @param param 任务参数
* @return 定时任务结果
* @throws Exception
*/
@XxlJob("wemediaAutoScanJob")
public ReturnT<String> autoScanJob(String param) throws Exception {
log.info("自媒体文章审核调度任务开始执行....");
ResponseResult<List<Integer>> release = wemediaFeign.findRelease();
if(release.getCode().intValue()!=0){
log.error("定时调度任务执行失败 ===================> "+release.getErrorMessage());
return ReturnT.FAIL;
}
List<Integer> releaseIdList = release.getData();
if(null!=releaseIdList && !releaseIdList.isEmpty()){
for (Integer id : releaseIdList) {
wemediaNewsAutoScanService.autoScanByMediaNewsId(id);
}
}
log.info("自媒体文章审核调度任务执行结束....");
return ReturnT.SUCCESS;
}
}</span></span>
2.6 测试
在数据库中准备好数据,数据状态为8且发布时间小于当前时间
3 admin端-人工审核文章
3.1 需求分析
自媒体文章如果没有自动审核成功,而是到了人工审核(自媒体文章状态为1),需要在admin端人工处理文章的审核
平台管理员可以查看待人工审核的文章信息,可以通过(状态改为4)或驳回(状态改为2)
也可以通过点击查看按钮,查看文章详细信息,查看详情后可以根据内容判断是否需要通过审核
3.2 媒体审核功能实现
3.2.1 查询文章列表
查询文章列表时候,不仅仅要返回文章数据,也要返回作者名称,这个时候返回的数据,需要包含作者名称
-
需要使用wm_news表与wm_user表做关联查询
-
在返回的结果的时候,单独再封装一个类,用于包装用户名称和文章数据 WmNewsVo
vo:view object 视图对象 是一个表现层对象,主要对应页面显示(web页面)的数据对象
-
由于需要做关联查询,mybatis-plus暂时不支持关联查询,需要自定义mapper实现
-
查询全部时,要排除草稿状态文章
WmNewsVo
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.model.wemedia.vos;
import com.heima.model.wemedia.pojos.WmNews;
import lombok.Data;
@Data
public class WmNewsVo extends WmNews {
/**
* 作者名称
*/
private String authorName;
}</span></span>
(1)WmNewsController新增方法
<span style="background-color:#333333"><span style="color:#b8bfc6">/**
* 查询文章列表
* @param dto
* @return
*/
@PostMapping("/list_vo")
public ResponseResult findList(@RequestBody NewsAuthDto dto) {
}</span></span>
NewsAuthDto
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.model.wemedia.dtos;
import com.heima.model.common.dtos.PageRequestDto;
import lombok.Data;
@Data
public class NewsAuthDto extends PageRequestDto {
/**
* 文章标题
*/
private String title;
/**
* 状态
*/
private Short status;
}</span></span>
(2) mapper
在WmNewsMapper中定义两个方法
<span style="background-color:#333333"><span style="color:#b8bfc6">public interface WmNewsMapper extends BaseMapper<WmNews> {
List<WmNewsVo> findListAndPage(@Param("dto") NewsAuthDto dto);
long findListCount(@Param("dto") NewsAuthDto dto);
}</span></span>
对应的映射文件
<span style="background-color:#333333"><span style="color:#b8bfc6"><?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.heima.wemedia.mapper.WmNewsMapper">
<select id="findListAndPage" resultType="com.heima.model.wemedia.vos.WmNewsVo" parameterType="com.heima.model.wemedia.dtos.NewsAuthDto">
SELECT
wn.*, wu.`name` authorName
FROM
wm_news wn
LEFT JOIN wm_user wu ON wn.user_id = wu.id
<where>
<if test="dto.title != null and dto.title != ''">
and wn.title like #{dto.title}
</if>
<if test="dto.status != null">
and wn.status = #{dto.status}
</if>
</where>
LIMIT #{dto.page},#{dto.size}
</select>
<select id="findListCount" resultType="long" parameterType="com.heima.model.wemedia.dtos.NewsAuthDto">
SELECT
count(1)
FROM
wm_news wn
LEFT JOIN wm_user wu ON wn.user_id = wu.id
<where>
<if test="dto.title != null and dto.title != ''">
and wn.title like #{dto.title}
</if>
<if test="dto.status != null and dto.status != ''">
and wn.status = #{dto.status}
</if>
</where>
</select>
</mapper></span></span>
(3) 业务层
在WmNewsService中新增方法
<span style="background-color:#333333"><span style="color:#b8bfc6"> /**
* 查询文章列表
* @param dto
* @return
*/
public ResponseResult findList(NewsAuthDto dto);</span></span>
实现方法
<span style="background-color:#333333"><span style="color:#b8bfc6">/**
* 查询文章列表
* @param dto
* @return
*/
@Override
public ResponseResult findList(NewsAuthDto dto) {
//1.检查参数
dto.checkParam();
//记录当前页
int currentPage = dto.getPage();
//设置起始页
dto.setPage((dto.getPage()-1)*dto.getSize());
if(StringUtils.isNotBlank(dto.getTitle())){
dto.setTitle("%"+dto.getTitle()+"%");
}
//2.分页查询
List<WmNewsVo> wmNewsVoList = wmNewsMapper.findListAndPage(dto);
//统计多少条数据
long count = wmNewsMapper.findListCount(dto);
//3.结果返回
ResponseResult result = new PageResponseResult(currentPage, dto.getSize(), count, wmNewsVoList);
result.setHost(webSite);
return result;
}</span></span>
(4)控制层
在WmNewsController中新增方法
<span style="background-color:#333333"><span style="color:#b8bfc6"> /**
* 查询文章列表
* @param dto
* @return
* 1、按照创建时间降序
* 2、后端查询的文章不应该包含草稿
*/
@ApiOperation(value = "查询自媒体文章列表",notes = "返回值带作者信息,主要运营管理端调用")
@PostMapping("/list_vo")
public ResponseResult findList(@RequestBody NewsAuthDto dto) {
return wmNewsService.findList(dto);
}</span></span>
(5)在admin网关中添加自媒体的路由
<span style="background-color:#333333"><span style="color:#b8bfc6"># 自媒体
- id: wemedia
uri: lb://leadnews-wemedia
predicates:
- Path=/wemedia/**
filters:
- StripPrefix= 1</span></span>
(6) 访问前端测试
3.2.2 查询文章详情
查询文章详情,是根据文章id查询,返回的vo对象
(1)在WmNewsController中新增方法
<span style="background-color:#333333"><span style="color:#b8bfc6">/**
* 查询文章详情
* @param id
* @return
*/
@GetMapping("/one_vo/{id}")
public ResponseResult findWmNewsVo(@PathVariable("id") Integer id) {
}</span></span>
(2)mapper
使用mybatis-plus自带
(3) 业务层
在WmNewsService中新增方法
<span style="background-color:#333333"><span style="color:#b8bfc6"> /**
* 查询文章详情
* @param id
* @return
*/
public ResponseResult findWmNewsVo(Integer id) ;</span></span>
实现类:
<span style="background-color:#333333"><span style="color:#b8bfc6">@Autowired
private WmUserMapper wmUserMapper;
/**
* 查询文章详情
* @param id
* @return
*/
@Override
public ResponseResult findWmNewsVo(Integer id) {
//1参数检查
if(id == null){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
//2.查询文章信息
WmNews wmNews = getById(id);
if(wmNews == null){
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
}
//3.查询作者
WmUser wmUser = null;
if(wmNews.getUserId() != null){
wmUser = wmUserMapper.selectById(wmNews.getUserId());
}
//4.封装vo信息返回
WmNewsVo wmNewsVo = new WmNewsVo();
BeanUtils.copyProperties(wmNews,wmNewsVo);
if(wmUser != null){
wmNewsVo.setAuthorName(wmUser.getName());
}
ResponseResult responseResult = ResponseResult.okResult(wmNewsVo);
responseResult.setHost(webSite);
return responseResult;
}</span></span>
(4)控制层
<span style="background-color:#333333"><span style="color:#b8bfc6"> /**
* 查询文章详情
* @param id
* @return
*/
@ApiOperation(value = "查询自媒体文章详情",notes = "返回值带作者信息,主要运营管理端调用")
@GetMapping("/one_vo/{id}")
public ResponseResult findWmNewsVo(@PathVariable("id") Integer id) {
return wmNewsService.findWmNewsVo(id);
}</span></span>
(5)打开页面测试
3.2.3 修改文章
当文章是人工审核的时候,可以进行审核
审核成功,把文章状态改为4
审核失败,把文章状态改为2 给出失败原因
(1)在WmNewsController定义两个方法
<span style="background-color:#333333"><span style="color:#b8bfc6">/**
* 文章审核成功
* @param dto
* @return
*/
@ApiOperation("人工审核通过 状态:4")
@PostMapping("/auth_pass")
public ResponseResult authPass(@RequestBody NewsAuthDto dto) {
}
/**
* 文章审核失败
* @param dto
* @return
*/
@ApiOperation("人工审核失败 状态:2")
@PostMapping("/auth_fail")
public ResponseResult authFail(@RequestBody NewsAuthDto dto) {
}</span></span>
在NewsAuthDto新增两个参数,完整如下:
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.model.wemedia.dtos;
import com.heima.model.common.dtos.PageRequestDto;
import lombok.Data;
@Data
public class NewsAuthDto extends PageRequestDto {
/**
* 文章标题
*/
private String title;
/**
* 状态
*/
private Short status;
/**
* 文章id
*/
private Integer id;
/**
* 失败原因
*/
private String msg;
}</span></span>
(2)mapper
无须定义
(3)业务层
在WmNewsService新增方法
<span style="background-color:#333333"><span style="color:#b8bfc6">/**
* 自媒体文章人工审核
* @param status 2 审核失败 4 审核成功
* @param dto
* @return
*/
public ResponseResult updateStatus(Short status, NewsAuthDto dto);</span></span>
实现类
<span style="background-color:#333333"><span style="color:#b8bfc6">/**
* 自媒体文章人工审核
* @param status 2 审核失败 4 审核成功
* @param dto
* @return
*/
@Override
public ResponseResult updateStatus(Short status, NewsAuthDto dto) {
//1.参数检查
if(dto == null || dto.getId() == null){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
//2.查询文章
WmNews wmNews = getById(dto.getId());
if(wmNews == null){
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
}
//3.修改文章状态
wmNews.setStatus(status);
if(StringUtils.isNotBlank(dto.getMsg())){
wmNews.setReason(dto.getMsg());
}
updateById(wmNews);
return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}</span></span>
(4)控制器
先在WemediaContants类中新增两个常量
<span style="background-color:#333333"><span style="color:#b8bfc6">public static final Short WM_NEWS_AUTH_PASS = 4;
public static final Short WM_NEWS_AUTH_FAIL = 2;</span></span>
在WmNewsController类中新增方法
<span style="background-color:#333333"><span style="color:#b8bfc6"> /**
* 文章审核成功
* @param dto
* @return
*/
@ApiOperation("人工审核通过 状态:4")
@PostMapping("/auth_pass")
public ResponseResult authPass(@RequestBody NewsAuthDto dto) {
return wmNewsService.updateStatus(WemediaConstants.WM_NEWS_AUTH_PASS,dto);
}
/**
* 文章审核失败
* @param dto
* @return
*/
@ApiOperation("人工审核失败 状态:2")
@PostMapping("/auth_fail")
public ResponseResult authFail(@RequestBody NewsAuthDto dto) {
return wmNewsService.updateStatus(WemediaConstants.WM_NEWS_AUTH_FAIL,dto);
}</span></span>
可打开页面直接测试
4 自媒体端-文章上下架
4.1 思路分析
在自媒体文章管理中有文章上下架的操作,上下架是文章已经审核通过发布之后的文章,目前自动审核文章和人工审核文章都已完成,可以把之前代码补充,使用异步的方式,修改app端文章的配置信息即可。
4.2 wemedia服务-消息生产方
发消息通知下架文章
修改WmNewsServiceImpl中的downOrUp方法,发送消息
<span style="background-color:#333333"><span style="color:#b8bfc6">@Override
public ResponseResult downOrUp(WmNewsDto dto) {
//1.检查参数
//2.查询文章
//3.判断文章是否发布
//4.修改文章状态,同步到app端(后期做)TODO
// ***发消息
if(wmNews.getArticleId()!=null){
Map<String,Object> mesMap = new HashMap<>();
mesMap.put("enable",dto.getEnable());
mesMap.put("articleId",wmNews.getArticleId());
kafkaTemplate.send(WmNewsMessageConstants.WM_NEWS_UP_OR_DOWN_TOPIC,JSON.toJSONString(mesMap));
}
return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}</span></span>
常量类中定义topic
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.common.constans.message;
public class WmNewsMessageConstants {
public static final String WM_NEWS_UP_OR_DOWN_TOPIC="wm.news.up.or.down.topic";
}</span></span>
4.3 article服务-消息消费方
文章微服务需要接收消息
在application.yml文件中添加kafka消费者的配置
<span style="background-color:#333333"><span style="color:#b8bfc6">spring:
kafka:
bootstrap-servers: 192.168.200.129:9092
consumer:
group-id: ${spring.application.name}-kafka-group
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer</span></span>
编写listener,如下:
<span style="background-color:#333333"><span style="color:#b8bfc6">@Component
@Slf4j
public class ArticleIsDownListener {
@Autowired
private ApArticleConfigService apArticleConfigService;
@KafkaListener(topics = WmNewsMessageConstants.WM_NEWS_UP_OR_DOWN_TOPIC)
public void receiveMessage(String message) {
log.info("收到 主题: {} 消息, 参数为: {}",WmNewsMessageConstants.WM_NEWS_UP_OR_DOWN_TOPIC,message);
Map map = JSON.parseObject(message, Map.class);
Object enable = map.get("enable");
boolean isDown = enable.equals(1) ? false : true;
apArticleConfigService.update(Wrappers.<ApArticleConfig>lambdaUpdate()
.eq(ApArticleConfig::getArticleId, map.get("articleId")).set(ApArticleConfig::getIsDown, isDown));
}
}</span></span>
4.4 集成测试
启动服务列表:
-
nacos
-
kafka/zk
-
seata
-
自媒体wemedia
-
文章服务 article
-
admin平台服务
-
自媒体网关
-
自媒体前端
当前文章定时发布存在以下问题
-
延时发布问题
-
文章库数据量比较大,对数据性能有损耗,定时1分钟查询一次数据库
解决方案:延迟消息队列解决
生产方:
-
wemedia,文章提交审核并且文章的状态为审核通过, 发延迟消息
消费方:
-
监听死信队列
-
完成文章审核和发布
线程池
<span style="background-color:#333333"><span style="color:#b8bfc6">好处:
1.新建线程消耗资源和时间
2.如果线程多的时候,CPU核数没有那么多的线程,===》抢占式的线程调度机制;
</span></span>
xxl-Job分布式任务调度
1.概述
1.1 什么是任务调度
我们可以先思考一下业务场景的解决方案:
-
某电商系统需要在每天上午10点,下午3点,晚上8点发放一批优惠券。
-
某银行系统需要在信用卡到期还款日的前三天进行短信提醒。
-
某财务系统需要在每天凌晨0:10结算前一天的财务数据,统计汇总。
-
12306会根据车次的不同,设置某几个时间点进行分批放票。
以上业务场景的解决方案就是任务调度。
任务调度是指系统为了自动完成特定任务,在约定的特定时刻去执行任务的过程。有了任务调度即可解放更多的人力,而是由系统自动去执行任务。
如何实现任务调度?
-
多线程方式,结合sleep
-
JDK提供的API,例如:Timer、ScheduledExecutor
-
框架,例如Quartz ,它是一个功能强大的任务调度框架,可以满足更多更复杂的调度需求
-
spring task
入门案例
spring框架中默认就支持了一个任务调度,springtask
每隔5s触发 一个方法 : eat() { 每隔5s 开始吃饭 我要变成一个胖子 }
(1)创建一个工程:springtask-test
pom文件
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#999977"><</span><span style="color:#7df46a">dependencies</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>org.springframework.boot<span style="color:#999977"></</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>spring-boot-starter-web<span style="color:#999977"></</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>
<span style="color:#999977"></</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#999977"></</span><span style="color:#7df46a">dependencies</span><span style="color:#999977">></span></span></span>
(2)引导类:
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#c88fd0">package</span> <span style="color:#8d8df0">com</span>.<span style="color:#b8bfc6">itheima</span>.<span style="color:#b8bfc6">task</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">boot</span>.<span style="color:#b8bfc6">SpringApplication</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">boot</span>.<span style="color:#b8bfc6">autoconfigure</span>.<span style="color:#b8bfc6">SpringBootApplication</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">scheduling</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">EnableScheduling</span>;
<span style="color:#b7b3b3">@SpringBootApplication</span>
<span style="color:#b7b3b3">@EnableScheduling</span>
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">class</span> <span style="color:#8d8df0">TaskApplication</span> {
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">static</span> <span style="color:#1cc685">void</span> <span style="color:#b8bfc6">main</span>(<span style="color:#1cc685">String</span>[] <span style="color:#b8bfc6">args</span>) {
<span style="color:#b8bfc6">SpringApplication</span>.<span style="color:#b8bfc6">run</span>(<span style="color:#b8bfc6">TaskApplication</span>.<span style="color:#c88fd0">class</span>,<span style="color:#b8bfc6">args</span>);
}
}</span></span>
(3)编写案例
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#c88fd0">package</span> <span style="color:#8d8df0">com</span>.<span style="color:#b8bfc6">itheima</span>.<span style="color:#b8bfc6">task</span>.<span style="color:#b8bfc6">job</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">scheduling</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">Scheduled</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">stereotype</span>.<span style="color:#b8bfc6">Component</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">java</span>.<span style="color:#b8bfc6">util</span>.<span style="color:#b8bfc6">Date</span>;
<span style="color:#b7b3b3">@Component</span>
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">class</span> <span style="color:#8d8df0">HelloJob</span> {
<span style="color:#b7b3b3">@Scheduled</span>(<span style="color:#b8bfc6">cron</span> <span style="color:#b8bfc6">=</span> <span style="color:#d26b6b">"0/5 * * * * ?"</span>)
<span style="color:#c88fd0">public</span> <span style="color:#1cc685">void</span> <span style="color:#b8bfc6">eat</span>(){
<span style="color:#b8bfc6">System</span>.<span style="color:#b8bfc6">out</span>.<span style="color:#b8bfc6">println</span>(<span style="color:#d26b6b">"5秒中吃一次饭,我想成为一个胖子"</span><span style="color:#b8bfc6">+</span><span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">Date</span>());
}
}</span></span>
测试:启动项目,每隔5秒中会执行一次eat方法
-
集群状态下各个服务都会执行当前任务
1.2 cron表达式
cron表达式是一个字符串, 用来设置定时规则, 由七部分组成, 每部分中间用空格隔开, 每部分的含义如下表所示:
组成部分 | 含义 | 取值范围 |
---|---|---|
第一部分 | Seconds (秒) | 0-59 |
第二部分 | Minutes(分) | 0-59 |
第三部分 | Hours(时) | 0-23 |
第四部分 | Day-of-Month(天) | 1-31 |
第五部分 | Month(月) | 0-11或JAN-DEC |
第六部分 | Day-of-Week(星期) | 1-7(1表示星期日)或SUN-SAT |
第七部分 | Year(年) 可选 | 1970-2099 |
另外, cron表达式还可以包含一些特殊符号来设置更加灵活的定时规则, 如下表所示:
符号 | 含义 |
---|---|
? | 表示不确定的值。当两个子表达式其中一个被指定了值以后,为了避免冲突,需要将另外一个的值设为“?”。例如:想在每月20日触发调度,不管20号是星期几,只能用如下写法:0 0 0 20 * ?,其中最后以为只能用“?” |
* | 代表所有可能的值 |
, | 设置多个值,例如”26,29,33”表示在26分,29分和33分各自运行一次任务 |
- | 设置取值范围,例如”5-20”,表示从5分到20分钟每分钟运行一次任务 |
/ | 设置频率或间隔,如"1/15"表示从1分开始,每隔15分钟运行一次任务 |
L | 用于每月,或每周,表示每月的最后一天,或每个月的最后星期几,例如"6L"表示"每月的最后一个星期五" |
W | 表示离给定日期最近的工作日,例如"15W"放在每月(day-of-month)上表示"离本月15日最近的工作日" |
# | 表示该月第几个周X。例如”6#3”表示该月第3个周五 |
为了让大家更熟悉cron表达式的用法, 接下来我们给大家列举了一些例子, 如下表所示:
cron表达式 | 含义 |
---|---|
*/5 * * * * ? | 每隔5秒运行一次任务 |
0 0 23 * * ? | 每天23点运行一次任务 |
0 0 1 1 * ? | 每月1号凌晨1点运行一次任务 |
0 0 23 L * ? | 每月最后一天23点运行一次任务 |
0 26,29,33 * * * ? | 在26分、29分、33分运行一次任务 |
0 0/30 9-17 * * ? | 朝九晚五工作时间内每半小时运行一次任务 |
0 15 10 ? * 6#3 | 每月的第三个星期五上午10:15运行一次任务 |
1.3 什么是分布式任务调度
当前软件的架构已经开始向分布式架构转变,将单体结构拆分为若干服务,服务之间通过网络交互来完成业务处理。在分布式架构下,一个服务往往会部署多个实例来运行我们的业务,如果在这种分布式系统环境下运行任务调度,我们称之为分布式任务调度。
将任务调度程序分布式构建,这样就可以具有分布式系统的特点,并且提高任务的调度处理能力:
1、并行任务调度
并行任务调度实现靠多线程,如果有大量任务需要调度,此时光靠多线程就会有瓶颈了,因为一台计算机CPU的处理能力是有限的。
如果将任务调度程序分布式部署,每个结点还可以部署为集群,这样就可以让多台计算机共同去完成任务调度,我们可以将任务分割为若干个分片,由不同的实例并行执行,来提高任务调度的处理效率。
2、高可用
若某一个实例宕机,不影响其他实例来执行任务。
3、弹性扩容
当集群中增加实例就可以提高并执行任务的处理效率。
4、任务管理与监测
对系统中存在的所有定时任务进行统一的管理及监测。让开发人员及运维人员能够时刻了解任务执行情况,从而做出快速的应急处理响应。
分布式任务调度面临的问题:
当任务调度以集群方式部署,同一个任务调度可能会执行多次,例如:电商系统定期发放优惠券,就可能重复发放优惠券,对公司造成损失,信用卡还款提醒就会重复执行多次,给用户造成烦恼,所以我们需要控制相同的任务在多个运行实例上只执行一次。常见解决方案:
-
分布式锁,多个实例在任务执行前首先需要获取锁,如果获取失败那么就证明有其他服务已经在运行,如果获取成功那么证明没有服务在运行定时任务,那么就可以执行。
-
ZooKeeper选举,利用ZooKeeper对Leader实例执行定时任务,执行定时任务的时候判断自己是否是Leader,如果不是则不执行,如果是则执行业务逻辑,这样也能达到目的。
1.4 xxl-Job简介
针对分布式任务调度的需求,市场上出现了很多的产品:
1) TBSchedule:淘宝推出的一款非常优秀的高性能分布式调度框架,目前被应用于阿里、京东、支付宝、国美等很多互联网企业的流程调度系统中。但是已经多年未更新,文档缺失严重,缺少维护。
2) XXL-Job:大众点评的分布式任务调度平台,是一个轻量级分布式任务调度平台, 其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
3)Elastic-job:当当网借鉴TBSchedule并基于quartz 二次开发的弹性分布式任务调度系统,功能丰富强大,采用zookeeper实现分布式协调,具有任务高可用以及分片功能。
4)Saturn: 唯品会开源的一个分布式任务调度平台,基于Elastic-job,可以全域统一配置,统一监 控,具有任务高可用以及分片功能。
XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
源码地址:xxl-job: 一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
文档地址:分布式任务调度平台XXL-JOB
特性
-
简单灵活 提供Web页面对任务进行管理,管理系统支持用户管理、权限控制; 支持容器部署; 支持通过通用HTTP提供跨平台任务调度;
-
丰富的任务管理功能 支持页面对任务CRUD操作; 支持在页面编写脚本任务、命令行任务、Java代码任务并执行; 支持任务级联编排,父任务执行结束后触发子任务执行; 支持设置指定任务执行节点路由策略,包括轮询、随机、广播、故障转移、忙碌转移等; 支持Cron方式、任务依赖、调度中心API接口方式触发任务执行
-
高性能 任务调度流程全异步化设计实现,如异步调度、异步运行、异步回调等,有效对密集调度进行流量削峰;
-
高可用 任务调度中心、任务执行节点均 集群部署,支持动态扩展、故障转移 支持任务配置路由故障转移策略,执行器节点不可用是自动转移到其他节点执行 支持任务超时控制、失败重试配置 支持任务处理阻塞策略:调度当任务执行节点忙碌时来不及执行任务的处理策略,包括:串行、抛弃、覆盖策略
-
易于监控运维 支持设置任务失败邮件告警,预留接口支持短信、钉钉告警; 支持实时查看任务执行运行数据统计图表、任务进度监控数据、任务完整执行日志;
2.XXL-Job
在分布式架构下,通过XXL-Job实现定时任务
调度中心:负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。
任务执行器:负责接收调度请求并执行任务逻辑。
任务:专注于任务的处理。
调度中心会发出调度请求,任务执行器接收到请求之后会去执行任务,任务则专注于任务业务的处理。
2.1 环境搭建(可选)
2.1.1 调度中心环境要求
-
Maven3+
-
Jdk1.8+
-
Mysql5.7+
2.1.2 源码仓库地址
源码仓库地址 | Release Download |
---|---|
GitHub - xuxueli/xxl-job: A distributed task scheduling framework.(分布式任务调度平台XXL-JOB) | Download |
xxl-job: 一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。 | Download |
也可以使用资料文件夹中的源码
2.1.3 初始化“调度数据库”
请下载项目源码并解压,获取 “调度数据库初始化SQL脚本” 并执行即可。
位置:/xxl-job/doc/db/tables_xxl_job.sql
共8张表
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#b8bfc6">-</span> <span style="color:#b8bfc6">xxl_job_lock:任务调度锁表;</span>
<span style="color:#b8bfc6">-</span> <span style="color:#b8bfc6">xxl_job_group:执行器信息表,维护任务执行器信息;</span>
<span style="color:#b8bfc6">-</span> <span style="color:#b8bfc6">xxl_job_info:调度扩展信息表:</span> <span style="color:#b8bfc6">用于保存XXL</span><span style="color:#b8bfc6">-</span><span style="color:#b8bfc6">JOB调度任务的扩展信息,如任务分组、任务名、机器地址、执行器、执行入参和报警邮件等等;</span>
<span style="color:#b8bfc6">-</span> <span style="color:#b8bfc6">xxl_job_log:调度日志表:</span> <span style="color:#b8bfc6">用于保存XXL</span><span style="color:#b8bfc6">-</span><span style="color:#b8bfc6">JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等;</span>
<span style="color:#b8bfc6">-</span> <span style="color:#b8bfc6">xxl_job_logglue:任务GLUE日志:用于保存GLUE更新历史,用于支持GLUE的版本回溯功能;</span>
<span style="color:#b8bfc6">-</span> <span style="color:#b8bfc6">xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息;</span>
<span style="color:#b8bfc6">-</span> <span style="color:#b8bfc6">xxl_job_user:系统用户表;</span></span></span>
调度中心支持集群部署,集群情况下各节点务必连接同一个mysql实例;
如果mysql做主从,调度中心集群节点务必强制走主库;
2.1.4 编译源码(可不做)
解压源码,按照maven格式将源码导入IDE, 使用maven进行编译即可,源码结构如下:
安装到本地仓库:mvn clean -DskipTests install
2.1.5 配置部署“调度中心”
调度中心项目:xxl-job-admin
作用:统一管理任务调度平台上调度任务,负责触发调度执行,并且提供任务管理平台。
步骤一:调度中心配置
调度中心配置文件地址:/xxl-job/xxl-job-admin/src/main/resources/application.properties
数据库的连接信息修改为自己的数据库
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#da924a">### web</span>
<span style="color:#8d8df0">server.port</span>=<span style="color:#57ac57">8888</span>
<span style="color:#8d8df0">server.servlet.context-path</span>=<span style="color:#57ac57">/xxl-job-admin</span>
<span style="color:#da924a">### actuator</span>
<span style="color:#8d8df0">management.server.servlet.context-path</span>=<span style="color:#57ac57">/actuator</span>
<span style="color:#8d8df0">management.health.mail.enabled</span>=<span style="color:#57ac57">false</span>
<span style="color:#da924a">### resources</span>
<span style="color:#8d8df0">spring.mvc.servlet.load-on-startup</span>=<span style="color:#57ac57">0</span>
<span style="color:#8d8df0">spring.mvc.static-path-pattern</span>=<span style="color:#57ac57">/static/**</span>
<span style="color:#8d8df0">spring.resources.static-locations</span>=<span style="color:#57ac57">classpath</span>:<span style="color:#57ac57">/static/</span>
<span style="color:#da924a">### freemarker</span>
<span style="color:#8d8df0">spring.freemarker.templateLoaderPath</span>=<span style="color:#57ac57">classpath</span>:<span style="color:#57ac57">/templates/</span>
<span style="color:#8d8df0">spring.freemarker.suffix</span>=<span style="color:#57ac57">.ftl</span>
<span style="color:#8d8df0">spring.freemarker.charset</span>=<span style="color:#57ac57">UTF-8</span>
<span style="color:#8d8df0">spring.freemarker.request-context-attribute</span>=<span style="color:#57ac57">request</span>
<span style="color:#8d8df0">spring.freemarker.settings.number_format</span>=<span style="color:#57ac57">0.##########</span>
<span style="color:#da924a">### mybatis</span>
<span style="color:#8d8df0">mybatis.mapper-locations</span>=<span style="color:#57ac57">classpath</span>:<span style="color:#57ac57">/mybatis-mapper/*Mapper.xml</span>
<span style="color:#da924a">#mybatis.type-aliases-package=com.xxl.job.admin.core.model</span>
<span style="color:#da924a">### xxl-job, datasource</span>
<span style="color:#8d8df0">spring.datasource.url</span>=<span style="color:#57ac57">jdbc</span>:<span style="color:#57ac57">mysql</span>:<span style="color:#57ac57">//127.0.0.1</span>:<span style="color:#57ac57">3306/xxl_job?Unicode</span>=<span style="color:#57ac57">true&serverTimezone</span>=<span style="color:#57ac57">Asia/Shanghai&characterEncoding</span>=<span style="color:#57ac57">UTF-8</span>
<span style="color:#8d8df0">spring.datasource.username</span>=<span style="color:#57ac57">root</span>
<span style="color:#8d8df0">spring.datasource.password</span>=<span style="color:#57ac57">root</span>
<span style="color:#8d8df0">spring.datasource.driver-class-name</span>=<span style="color:#57ac57">com.mysql.cj.jdbc.Driver</span>
<span style="color:#da924a">### datasource-pool</span>
<span style="color:#8d8df0">spring.datasource.type</span>=<span style="color:#57ac57">com.zaxxer.hikari.HikariDataSource</span>
<span style="color:#8d8df0">spring.datasource.hikari.minimum-idle</span>=<span style="color:#57ac57">10</span>
<span style="color:#8d8df0">spring.datasource.hikari.maximum-pool-size</span>=<span style="color:#57ac57">30</span>
<span style="color:#8d8df0">spring.datasource.hikari.auto-commit</span>=<span style="color:#57ac57">true</span>
<span style="color:#8d8df0">spring.datasource.hikari.idle-timeout</span>=<span style="color:#57ac57">30000</span>
<span style="color:#8d8df0">spring.datasource.hikari.pool-name</span>=<span style="color:#57ac57">HikariCP</span>
<span style="color:#8d8df0">spring.datasource.hikari.max-lifetime</span>=<span style="color:#57ac57">900000</span>
<span style="color:#8d8df0">spring.datasource.hikari.connection-timeout</span>=<span style="color:#57ac57">10000</span>
<span style="color:#8d8df0">spring.datasource.hikari.connection-test-query</span>=<span style="color:#57ac57">SELECT 1</span>
<span style="color:#da924a">### xxl-job, email</span>
<span style="color:#8d8df0">spring.mail.host</span>=<span style="color:#57ac57">smtp.qq.com</span>
<span style="color:#8d8df0">spring.mail.port</span>=<span style="color:#57ac57">25</span>
<span style="color:#8d8df0">spring.mail.username</span>=<span style="color:#57ac57">xxx@qq.com</span>
<span style="color:#8d8df0">spring.mail.password</span>=<span style="color:#57ac57">xxx</span>
<span style="color:#8d8df0">spring.mail.properties.mail.smtp.auth</span>=<span style="color:#57ac57">true</span>
<span style="color:#8d8df0">spring.mail.properties.mail.smtp.starttls.enable</span>=<span style="color:#57ac57">true</span>
<span style="color:#8d8df0">spring.mail.properties.mail.smtp.starttls.required</span>=<span style="color:#57ac57">true</span>
<span style="color:#8d8df0">spring.mail.properties.mail.smtp.socketFactory.class</span>=<span style="color:#57ac57">javax.net.ssl.SSLSocketFactory</span>
<span style="color:#da924a">### xxl-job, access token</span>
<span style="color:#8d8df0">xxl.job.accessToken</span>=
<span style="color:#da924a">### xxl-job, i18n (default is zh_CN, and you can choose "zh_CN", "zh_TC" and "en")</span>
<span style="color:#8d8df0">xxl.job.i18n</span>=<span style="color:#57ac57">zh_CN</span>
<span style="color:#da924a">## xxl-job, triggerpool max size</span>
<span style="color:#8d8df0">xxl.job.triggerpool.fast.max</span>=<span style="color:#57ac57">200</span>
<span style="color:#8d8df0">xxl.job.triggerpool.slow.max</span>=<span style="color:#57ac57">100</span>
<span style="color:#da924a">### xxl-job, log retention days</span>
<span style="color:#8d8df0">xxl.job.logretentiondays</span>=<span style="color:#57ac57">30</span></span></span>
步骤二:部署项目
如果已经正确进行上述配置,可将项目编译打包部署。
启动方式一:这是一个springboot项目,可以在idea中直接启动,不推荐使用
启动方式二:
-
执行maven打包命令:package
-
打完包以后,从项目的target目录中找到jar包拷贝到不带空格和中文的目录下
-
执行以下命令,启动项目
<span style="background-color:#333333"><span style="color:#b8bfc6">java <span style="color:#7575e4">-jar</span> xxl-job-admin-2.2.0-SNAPSHOT.jar</span></span>
调度中心访问地址:http://localhost:8888/xxl-job-admin (该地址执行器将会使用到,作为回调地址)
启动方式三:docker部署微服务
-
初始化数据库
位置:
/xxl-job/doc/db/tables_xxl_job.sql
共8张表 -
创建容器
<span style="background-color:#333333">docker run <span style="color:#7575e4">-e</span> <span style="color:#8d8df0">PARAMS</span><span style="color:#b8bfc6">=</span><span style="color:#d26b6b">"--spring.datasource.url=jdbc:mysql://192.168.200.130:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai --spring.datasource.username=root --spring.datasource.password=root"</span> <span style="color:#7575e4">-p</span> <span style="color:#64ab8f">8888</span>:8080 <span style="color:#7575e4">-v</span> /tmp:/data/applogs <span style="color:#7575e4">--name</span> xxl-job-admin <span style="color:#7575e4">--privileged</span><span style="color:#b8bfc6">=</span><span style="color:#84b6cb">true</span> <span style="color:#7575e4">-id</span> xuxueli/xxl-job-admin:2.2.0</span>
默认登录账号 “admin/123456”, 登录后运行界面如下图所示。
至此“调度中心”项目已经部署成功。
2.2 入门案例编写
2.2.1 配置执行器
在任务调度中心,点击进入"执行器管理"界面, 如下图:
1、此处的AppName,会在创建任务时被选择,每个任务必然要选择一个执行器。 2、"执行器列表" 中显示在线的执行器列表, 支持编辑删除。
以下是执行器的属性说明:
属性名称 | 说明 |
---|---|
AppName | 是每个执行器集群的唯一标示AppName, 执行器会周期性以AppName为对象进行自动注册。可通过该配置自动发现注册成功的执行器, 供任务调度时使用; |
名称 | 执行器的名称, 因为AppName限制字母数字等组成,可读性不强, 名称为了提高执行器的可读性; |
排序 | 执行器的排序, 系统中需要执行器的地方,如任务新增, 将会按照该排序读取可用的执行器列表; |
注册方式 | 调度中心获取执行器地址的方式; |
机器地址 | 注册方式为"手动录入"时有效,支持人工维护执行器的地址信息; |
具体操作:
(1)新增执行器:
(2)自动注册和手动注册的区别和配置
2.2.2 在调度中心新建任务
在任务管理->新建,填写以下内容
-
执行器:任务的绑定的执行器,任务触发调度时将会自动发现注册成功的执行器, 实现任务自动发现功能; 另一方面也可以方便的进行任务分组。每个任务必须绑定一个执行器, 可在 "执行器管理" 进行设置
-
任务描述:任务的描述信息,便于任务管理
路由策略:当执行器集群部署时,提供丰富的路由策略,包括
-
FIRST(第一个):固定选择第一个机器;
-
LAST(最后一个):固定选择最后一个机器;
-
ROUND(轮询):
-
RANDOM(随机):随机选择在线的机器;
-
CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。
-
LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举;
-
LEAST_RECENTLY_USED(最近最久未使用):最久为使用的机器优先被选举;
-
FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;
-
BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;
-
SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;
-
-
Cron:触发任务执行的Cron表达式;
-
运行模式:
-
BEAN模式:任务以JobHandler方式维护在执行器端;需要结合 "JobHandler" 属性匹配执行器中任务;
-
GLUE模式(Java):任务以源码方式维护在调度中心;该模式的任务实际上是一段继承自IJobHandler的Java类代码并 "groovy" 源码方式维护,它在执行器项目中运行,可使用@Resource/@Autowire注入执行器里中的其他服务;
-
GLUE模式(Shell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "shell" 脚本;
-
GLUE模式(Python):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "python" 脚本;
-
GLUE模式(PHP):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "php" 脚本;
-
GLUE模式(NodeJS):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "nodejs" 脚本;
-
GLUE模式(PowerShell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "PowerShell" 脚本;
-
-
JobHandler:运行模式为 "BEAN模式" 时生效,对应执行器中新开发的JobHandler类“@JobHandler”注解自定义的value值;
-
阻塞处理策略:调度过于密集执行器来不及处理时的处理策略;
-
单机串行(默认):调度请求进入单机执行器后,调度请求进入FIFO队列并以串行方式运行;
-
丢弃后续调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败;
-
覆盖之前调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务;
-
-
子任务:每个任务都拥有一个唯一的任务ID(任务ID可以从任务列表获取),当本任务执行结束并且执行成功时,将会触发子任务ID所对应的任务的一次主动调度。
-
任务超时时间:支持自定义任务超时时间,任务运行超时将会主动中断任务;
-
失败重试次数;支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;
-
报警邮件:任务调度失败时邮件通知的邮箱地址,支持配置多邮箱地址,配置多个邮箱地址时用逗号分隔;
-
负责人:任务的负责人;
-
执行参数:任务执行所需的参数;
2.2.3 搭建springboot项目
新建项目:xxljob-test
(1)pom文件
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#999977"><</span><span style="color:#7df46a">dependencies</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>org.springframework.boot<span style="color:#999977"></</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>spring-boot-starter-web<span style="color:#999977"></</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>
<span style="color:#999977"></</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#da924a"><!-- xxl-job --></span>
<span style="color:#999977"><</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>com.xuxueli<span style="color:#999977"></</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>xxl-job-core<span style="color:#999977"></</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">version</span><span style="color:#999977">></span>2.2.0<span style="color:#999977"></</span><span style="color:#7df46a">version</span><span style="color:#999977">></span>
<span style="color:#999977"></</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#999977"></</span><span style="color:#7df46a">dependencies</span><span style="color:#999977">></span></span></span>
注意:如果项目中没有找到xxl-job-core
这个依赖,需要把这个依赖安装到本地的maven仓库
或者直接进入源码包目录下执行:mvn clean -DskipTests install
(2)配置有两个,一个是application.properties,另外一个是日志配置:logback.xml
application.properties
<span style="background-color:#333333"><span style="color:#b8bfc6"># web port
server.port=${port:8801}
# no web
#spring.main.web-environment=false
# log config
logging.config=classpath:logback.xml
### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
xxl.job.admin.addresses=http://localhost:8888/xxl-job-admin
### xxl-job, access token
xxl.job.accessToken=
### xxl-job executor appname
xxl.job.executor.appname=xxl-job-executor-sample
### xxl-job executor registry-address: default use address to registry , otherwise use ip:port if address is null
xxl.job.executor.address=
### xxl-job executor server-info
xxl.job.executor.ip=
xxl.job.executor.port=${executor.port:9999}
### xxl-job executor log-path 创建文件路径 D:/logs
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### xxl-job executor log-retention-days
xxl.job.executor.logretentiondays=30
</span></span>
logback.xml
<span style="background-color:#333333"><span style="color:#b8bfc6"><?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true" scanPeriod="1 seconds">
<contextName>logback</contextName>
<property name="log.path" value="/xxl-job.log"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}.%d{yyyy-MM-dd}.zip</fileNamePattern>
</rollingPolicy>
<encoder>
<pattern>%date %level [%thread] %logger{36} [%file : %line] %msg%n
</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="file"/>
</root>
</configuration></span></span>
(3)引导类:
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.itheima.xxljob;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class XxlJobApplication {
public static void main(String[] args) {
SpringApplication.run(XxlJobApplication.class,args);
}
}</span></span>
2.2.4 添加xxl-job配置
添加配置类:
这个类主要是创建了任务执行器,参考官方案例编写,无须改动
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.itheima.xxljob.config;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* xxl-job config
*
* @author xuxueli 2017-04-28
*/
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appName;
@Value("${xxl.job.executor.address}")
private String address;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appName);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
/**
* 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP;
*
* 1、引入依赖:
* <dependency>
* <groupId>org.springframework.cloud</groupId>
* <artifactId>spring-cloud-commons</artifactId>
* <version>${version}</version>
* </dependency>
*
* 2、配置文件,或者容器启动变量
* spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
*
* 3、获取IP
* String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
*/
}</span></span>
2.2.5 创建任务
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.itheima.xxljob.job;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class HelloJob {
@Value("${server.port}")
private String appPort;
@XxlJob("helloJob")
public ReturnT<String> hello(String param) throws Exception {
System.out.println("helloJob:"+ LocalDateTime.now()+",端口号"+appPort);
return ReturnT.SUCCESS;
}
}
</span></span>
@XxlJob("helloJob")
这个一定要与调度中心新建任务的JobHandler的值保持一致,如下图:
2.2.6 测试
(1)首先启动调度中心
(2)启动xxljob-test项目,为了展示更好的效果,可以同时启动三个项目,用同一个JobHandler,查看处理方式。
在启动多个项目的时候,端口需要切换,连接xxl-job的执行器端口不同相同
服务一:默认启动8801端口,执行器端口为9999
idea中不用其他配置,直接启动项目即可
服务二:项目端口:8802,执行器端口:9998
idea配置如下:
-
编辑配置,Edit Configurations...
-
选中XxlJobApplication,点击复制
-
修改参数
-
启动:选中8802启动项目
服务三:项目端口:8803,执行器端口:9997
(3)测试效果
三个项目启动后,可以查看到是轮询的方式分别去执行当前调度任务。
2.3 广播任务和动态分片
2.3.1 什么是作业分片
作业分片是指任务的分布式执行,需要将一个任务拆分为多个独立的任务项,然后由分布式的应用实例 分别执行某一个或几个分片项。
2.3.2 XXL-JOB分片
执行器集群部署时,任务路由策略选择”分片广播”情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;
“分片广播” 以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。
“分片广播” 和普通任务开发流程一致,不同之处在于可以获取分片参数,获取分片参数进行分片业务处理。
2.3.3 XXL-JOB支持分片的好处
-
分片项与业务处理解耦 XXL-JOB并不直接提供数据处理的功能,框架只会将分片项分配至各个运行中的作业服务器,开发者需 要自行处理分片项与真实数据的对应关系。
-
最大限度利用资源 基于业务需求配置合理数量的执行器服务,合理设置分片,作业将会最大限度合理的利用分布式资源。
2.3.4 适用场景
-
分片任务场景:10个执行器的集群来处理10w条数据,每台机器只需要处理1w条数据,耗时降低10倍;
-
广播任务场景:广播执行器机器运行shell脚本、广播集群节点进行缓存更新等
2.3.5 分片广播案例演示
目标:实现XXL-JOB作业分片的演示
方案分析:规划一个任务,两个分片,对应两个执行器,每个分片处理一部分任务。
实现步骤:
-
创建分片执行器
-
创建任务
指定刚才创建的分片执行器,在路由策略这一栏选择分片广播
-
分片广播代码
分片参数属性说明:
-
index:当前分片序号(从0开始),执行器集群列表中当前执行器的序号;
-
total:总分片数,执行器集群的总机器数量;
目前有一万条数据,使用两个分片同时执行
<span style="background-color:#333333">/** * 2、分片广播任务 */ @XxlJob("shardingJobHandler") public ReturnT<String> shardingJobHandler(String param) throws Exception { // 分片参数 ShardingUtil.ShardingVO shardingVO = ShardingUtil.getShardingVo(); XxlJobLogger.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardingVO.getIndex(), shardingVO.getTotal()); List<Integer> list = getList(); for (Integer integer : list) { if(integer % shardingVO.getTotal() == shardingVO.getIndex()){ System.out.println("第"+shardingVO.getIndex()+"分片执行,执行数据为:"+integer); } } return ReturnT.SUCCESS; } public static List<Integer> getList(){ List<Integer> list = new ArrayList<>(); for (int i = 0; i < 10000 ; i++) { list.add(i); } return list; }</span>
-
结论:
-
如果没有设定分片的执行逻辑,默认情况下是广播形式执行,即集群中的每一个节点都会执行任务
-
如果设定了分片执行逻辑,则会把任务划分到执行器的集群中执行
第九章 app端基本功能展示
今日目标
-
能够完成app端登录的功能
-
能够完成app端文章列表展示功能开发
-
能够完成app端文章详情的功能开发
-
能够掌握解决long类型丢失精度的问题
-
能够掌握关注作者功能
1 app端网关搭建
可以直接参考自媒体网关复制修改即可。
(1)heima-leadnews-gateways
新建模块,名称:app-gateway
(2)application.yml
server: port: 5001 spring: application: name: leadnews-app-gateway cloud: nacos: discovery: server-addr: 192.168.200.130:8848 gateway: globalcors: cors-configurations: '[/**]': # 匹配所有请求 allowCredentials: true allowedOrigins: "*" allowedMethods: "*" allowedHeaders: "*" add-to-simple-url-handler-mapping: true routes: #文章微服务 - id: leadnews-article uri: lb://leadnews-article predicates: - Path=/article/** filters: - StripPrefix= 1 - id: leadnews-user uri: lb://leadnews-user predicates: - Path=/user/** filters: - StripPrefix= 1
(4)引导类:
package com.heima.app.gateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient public class AppGatewayApplication { public static void main(String[] args) { SpringApplication.run(AppGatewayApplication.class,args); } }
完整代码结构:
2 app端登录功能
2.1 app端登录-需求分析
-
点击登录可以根据app端的手机号和密码进行登录
-
点击不登录,先看看可以在无登录状态下进入app
2.2 app端登录-思路分析
概念介绍:用户设备,即当前用户所使用的终端设备。
-
用户点击登录
-
用户输入手机号和密码到后端进行校验,校验成功生成token返给前端
-
其他请求需要带着token到app网关去校验jwt,校验成功,放行
-
用户点击不登录,先看看
-
获取用户的设备id到后端根据设备id生成token,设置jwt存储的id为0
-
其他请求需要带着token到app网关去校验jwt,校验成功,放行
2.3 app端登录-功能实现
此功能在user-service
模块中实现
2.3.1 接口定义
@Api(value = "app端用户登录api",tags = "app端用户登录api") @RestController @RequestMapping("/api/v1/login") public class ApUserLoginController { @ApiOperation("登录") @PostMapping("/login_auth") public ResponseResult login(@RequestBody LoginDto dto) { } }
LoginDto
package com.heima.model.user.dtos; import lombok.Data; @Data public class LoginDto { /** * 设备id */ private Integer equipmentId; /** * 手机号 */ private String phone; /** * 密码 */ private String password; }
2.3.2 mapper
校验用户登录的时候需要查询appUser表,此mapper之前课程已经定义完成
2.3.3 业务层
新建接口:
package com.heima.user.service; import com.heima.model.common.dtos.ResponseResult; import com.heima.model.user.dtos.LoginDto; public interface ApUserLoginService { /** * app端登录 * @param dto * @return */ public ResponseResult login(LoginDto dto); }
实现类:
package com.heima.user.service.impl; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.heima.model.common.dtos.ResponseResult; import com.heima.model.common.enums.AppHttpCodeEnum; import com.heima.model.user.dtos.LoginDto; import com.heima.model.user.pojos.ApUser; import com.heima.user.mapper.ApUserMapper; import com.heima.user.service.ApUserLoginService; import com.heima.utils.common.AppJwtUtil; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.DigestUtils; import java.util.HashMap; import java.util.Map; @Service public class ApUserLoginServiceImpl implements ApUserLoginService { @Autowired ApUserMapper apUserMapper; /** * app端登录 * @param dto * @return */ @Override public ResponseResult login(LoginDto dto) { //1.校验参数 if(dto.getEquipmentId() == null){ return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID); } // 查询登录用户 if (StringUtils.isNotBlank(dto.getPhone()) && StringUtils.isNotBlank(dto.getPassword())) { ApUser apUser = apUserMapper.selectOne(Wrappers.<ApUser>lambdaQuery().eq(ApUser::getStatus, 0).eq(ApUser::getPhone, dto.getPhone())); if (apUser == null) { return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST, "请检查手机号"); } // 数据库密码 String dbpassword = apUser.getPassword(); String newPassword = DigestUtils.md5DigestAsHex((dto.getPassword() + apUser.getSalt()).getBytes()); if (!dbpassword.equals(newPassword)) { return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST, "手机号或密码错误"); } Map<String, Object> map = new HashMap<>(); apUser.setPassword(""); apUser.setSalt(""); map.put("token", AppJwtUtil.getToken(apUser.getId().longValue())); map.put("user", apUser); return ResponseResult.okResult(map); }else { Map<String,Object> map = new HashMap<>(); // 通过设备ID登录的 userId存0 map.put("token",AppJwtUtil.getToken(0L)); return ResponseResult.okResult(map); } } }
2.3.4 控制器
新建app用户登录控制器
@Api(value = "app端用户登录api",tags = "app端用户登录api") @RestController @RequestMapping("/api/v1/login") public class ApUserLoginController { @Autowired ApUserLoginService apUserLoginService; @ApiOperation("登录") @PostMapping("/login_auth") public ResponseResult login(@RequestBody LoginDto dto) { return apUserLoginService.login(dto); } }
2.3.5 网关校验
网关校验流程:
参考其他网关设置,在 app-gateway
模块中新建过滤器,如下:
-
需要修改登录拦截uri的设置,需要把之前jwt的工具类拷贝过来
-
参考其他网关把全局过滤器拷贝过来
2.3.6 网关路由配置
(1)修改全局过滤器,根据user的实际登录地址修改网关中登录放行的uri
2.3.7 获取登录用户
在heima-leadnews-model
模块中添加存储App登录用户的ThreadLocal
工具类
package com.heima.model.threadlocal; import com.heima.model.user.pojos.ApUser; public class AppThreadLocalUtils { private final static ThreadLocal<ApUser> userThreadLocal = new ThreadLocal<>(); /** * 设置当前线程中的用户 * @param user */ public static void setUser(ApUser user){ userThreadLocal.set(user); } /** * 获取线程中的用户 * @return */ public static ApUser getUser( ){ return userThreadLocal.get(); } /** * 清空线程中的用户信息 */ public static void clear(){ userThreadLocal.remove(); } }
在user模块 filter中新增 AppTokenFilter 过滤器
package com.heima.user.filter; import com.heima.model.threadlocal.AppThreadLocalUtils; import com.heima.model.user.pojos.ApUser; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.filter.GenericFilterBean; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import java.io.IOException; @Slf4j @Order(1) @WebFilter(filterName = "appTokenFilter",urlPatterns = "/*") @Component public class AppTokenFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { // 1. 获取请求对象 HttpServletRequest request = (HttpServletRequest) servletRequest; // 2. 查看请求header中是否有userId属性 String userId = request.getHeader("userId");// 如果是设备登录 存的userId是0 // 3. 如果userId有值存入到ThreadLocal中 if(StringUtils.isNotBlank(userId) && Integer.valueOf(userId)!=0){ ApUser apUser = new ApUser(); apUser.setId(Integer.valueOf(userId)); AppThreadLocalUtils.setUser(apUser); } // 4. 放行 filterChain.doFilter(servletRequest,servletResponse); // 5. 清空登录信息 AppThreadLocalUtils.clear(); } }
3 app端-环境搭建
3.1 weex
app端的前端项目使用的阿里的前端框架weex开发的。
Weex 致力于使开发者能基于通用跨平台的 Web 开发语言和开发经验,来构建 Android、iOS 和 Web 应用。简单来说,在集成了 WeexSDK 之后,你可以使用 JavaScript 语言和前端开发经验来开发移动应用。
官网:Weex Incubation Status - Apache Incubator
3.2 前后端文章列表联调测试
使用Vscode打开已下载的文件
3.2.1 环境准备
app的前端添加了频道列表查询,需要下载git最新代码 或 拷贝资料中的app压缩包
(1)安装依赖
在项目的根目录使用命令cnpm install
命令安装项目所依赖的js文件
3.2.2 启动项目
打开 VsCode terminal 控制台运行:npm run serve
启动前端项目
启动项目后:
如果当前电脑和手机在一个局域网下下载weex的app,就可以扫码在手机端查看效果
app下载地址:Weex Incubation Status - Apache Incubator
效果如下:
扫描二维码查看效果
3.2.3 测试
可以直接测试登录,不登录和登录的情况
3.4 准备app端频道列表(暂时前端有bug)
接口说明:
请求方式 : GET
接口路径 : admin/api/v1/channel/channels/
后台提供接口时 , 需要查询启动状态的频道,并按照ord字段升序排序
@ApiOperation("查询全部频道") @GetMapping("/channels") public ResponseResult findAll() { List<AdChannel> list = channelService.list(Wrappers.<AdChannel>lambdaQuery() .eq(AdChannel::getStatus,true).orderByAsc(AdChannel::getOrd)); return ResponseResult.okResult(list); }
app网关 添加admin的路由
- id: leadnews-admin uri: lb://leadnews-admin predicates: - Path=/admin/** filters: - StripPrefix= 1
打开前端页面测试频道列表是否可以展示:
4 app端-文章列表
4.1 app端文章列表-需求分析
在手机端可以查看文章信息
在默认频道展示10条文章信息
可以切换频道查看不同种类文章
当用户上拉可以加载更多的文章信息(按照发布时间)
分页
本页文章列表中发布时间为最小的时间为依据
当用户下拉可以加载最新的文章
本页文章列表中发布时间最大的时间为依据
如果加载首页数据,前端会默认给传递一些参数
4.2 app端文章列表-功能实现
4.2.1 定义接口
在article模块中新增ArticleHomeController接口
package com.heima.article.controller.v1; import com.heima.apis.article.ArticleHomeControllerApi; import com.heima.article.service.ApArticleService; import com.heima.model.article.dtos.ArticleHomeDto; import com.heima.model.common.constants.ArticleConstants; import com.heima.model.common.dtos.ResponseResult; 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("/api/v1/article") public class ArticleHomeController { @Autowired private ApArticleService articleService; /** * 查询首页文章 * @param dto * @return */ @PostMapping("/load") public ResponseResult load(@RequestBody ArticleHomeDto dto) { return articleService.load(ArticleConstants.LOADTYPE_LOAD_MORE,dto ); } @PostMapping("/loadmore") public ResponseResult loadMore(@RequestBody ArticleHomeDto dto) { return articleService.load(ArticleConstants.LOADTYPE_LOAD_MORE,dto); } @PostMapping("/loadnew") public ResponseResult loadNew(@RequestBody ArticleHomeDto dto) { return articleService.load(ArticleConstants.LOADTYPE_LOAD_NEW,dto); } }
定义常量类
package com.heima.common.constants; public class ArticleConstants { public static final Short LOADTYPE_LOAD_MORE = 0; // 加载更多 public static final Short LOADTYPE_LOAD_NEW = 1; // 加载最新 public static final String DEFAULT_TAG = "__all__"; }
ArticleHomeDto
package com.heima.model.article.dtos; import lombok.Data; import java.util.Date; @Data public class ArticleHomeDto { // 最大时间 Date maxBehotTime; // 最小时间 Date minBehotTime; // 分页size Integer size; // 频道ID String tag ; }
4.2.2 mapper
文章展示并不是直接查询ap_article文章表数据,需要关联查询文章的配置表信息,如果是已下架或者标明已删除的文章则不被查询出来
在之前定义好的ApArticleMapper接口中新增方法
package com.heima.article.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.heima.model.article.dtos.ArticleHomeDto; import com.heima.model.article.pojos.ApArticle; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; public interface ApArticleMapper extends BaseMapper<ApArticle> { /** * 查询文章列表 * @param dto * @param type 0:加载更多 1:加载最新 * @return */ public List<ApArticle> loadArticleList(@Param("dto") ArticleHomeDto dto,@Param("type") Short type); }
在resources\mapper目录下新建ApArticleMapper.xml文件
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.heima.article.mapper.ApArticleMapper"> <resultMap id="resultMap" type="com.heima.model.article.pojos.ApArticle"> <id column="id" property="id"/> <result column="title" property="title"/> <result column="author_id" property="authorId"/> <result column="author_name" property="authorName"/> <result column="channel_id" property="channelId"/> <result column="channel_name" property="channelName"/> <result column="layout" property="layout"/> <result column="flag" property="flag"/> <result column="images" property="images"/> <result column="labels" property="labels"/> <result column="likes" property="likes"/> <result column="collection" property="collection"/> <result column="comment" property="comment"/> <result column="views" property="views"/> <result column="province_id" property="provinceId"/> <result column="city_id" property="cityId"/> <result column="county_id" property="countyId"/> <result column="created_time" property="createdTime"/> <result column="publish_time" property="publishTime"/> <result column="sync_status" property="syncStatus"/> <result column="static_url" property="staticUrl"/> </resultMap> <select id="loadArticleList" resultMap="resultMap"> SELECT aa.* FROM `ap_article` aa LEFT JOIN ap_article_config aac ON aa.id = aac.article_id <where> and aac.is_delete != 1 and aac.is_down != 1 <!-- loadmore --> <if test="type != null and type == 0"> and aa.publish_time <![CDATA[<]]> #{dto.minBehotTime} </if> <if test="type != null and type == 1"> and aa.publish_time <![CDATA[>]]> #{dto.maxBehotTime} </if> <if test="dto.tag != '__all__'"> and aa.channel_id = #{dto.tag} </if> </where> order by aa.publish_time desc limit #{dto.size} </select> </mapper>
4.2.3 业务层
在ApArticleService中新增一个方法
/** * 根据参数加载文章列表 * @param loadtype 0为加载更多 1为加载最新 * @param dto * @return */ ResponseResult load(Short loadtype, ArticleHomeDto dto);
实现类方法
package com.heima.article.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.heima.article.mapper.ApArticleMapper; import com.heima.article.service.ApArticleService; import com.heima.common.constans.article.ArticleConstans; import com.heima.model.article.dtos.ArticleHomeDto; import com.heima.model.article.pojos.ApArticle; import com.heima.model.common.dtos.ResponseResult; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.Date; import java.util.List; @Service public class ApArticleServiceImpl extends ServiceImpl<ApArticleMapper, ApArticle> implements ApArticleService { @Autowired ApArticleMapper apArticleMapper; @Value("${file.oss.web-site}") private String webSite; /** * 根据参数加载文章列表 * @param loadtype 1为加载更多 2为加载最新 * @param dto * @return */ @Override public ResponseResult load(Short loadtype, ArticleHomeDto dto) { //1 参数检查 if (dto == null) { dto = new ArticleHomeDto(); } // 页大小 Integer size = dto.getSize(); if (size == null || size <= 0) { size = 10; } dto.setSize(size); // 频道 if (StringUtils.isBlank(dto.getTag())) { dto.setTag(ArticleConstants.DEFAULT_TAG); } // 时间 if (dto.getMaxBehotTime() == null) { dto.setMaxBehotTime(new Date()); } if (dto.getMinBehotTime() == null) { dto.setMinBehotTime(new Date()); } // 类型判断 if (!loadtype.equals(ArticleConstants.LOADTYPE_LOAD_MORE) && !loadtype.equals(ArticleConstants.LOADTYPE_LOAD_NEW)) { loadtype = ArticleConstants.LOADTYPE_LOAD_MORE; } //2 执行查询 List<ApArticle> articleList = apArticleMapper.loadArticleList(dto, loadtype); //3 返回结果 ResponseResult result = ResponseResult.okResult(articleList); result.setHost(webSite); return result; } }
4.3 app端文章列表-测试(采用静态化方式已无此问题)
4.4 Long类型转换精度丢失问题解决
4.4.1 解决方案分析:
方案一:将文章的id的由long类型手动改为String类型,可以解决此问题。(需要修改表结构)
方案二:可以使用jackson进行序列化和反序列化解决(本项目采用这种方案)
4.4.2 jackson序列化和反序列化原理说明
-
当后端响应给前端的数据中包含了id或者特殊标识(可自定义)的时候,把当前数据进行转换为String类型
-
当后端响应前端articleId字段时,使用jackson序列化,指定该字段序列化设置为序列化为String
heima-leadnews-model
中添加jackson依赖
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>
修改ApArticle实体类
package com.heima.model.article.pojos; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; @Data @TableName("ap_article") public class ApArticle { @TableId(value = "id",type = IdType.ID_WORKER) @JsonSerialize(using = ToStringSerializer.class) private Long id; //..... 略 ..... }
重启项目,使用app前端再次访问文章详情,问题解决。
5 app端-关注作者或取消关注
5.1 需求分析
如上效果:
当前登录后的用户可以关注作者对应的APUser ID,也可以取消关注作者对应的APUser ID
5.2 解决方案
方案一:基于数据库保存对应的关注关系
方案二:基于Redis保存。
项目中使用方案二。
一个用户关注了作者,作者是由用户实名认证以后开通的作者权限,才有了作者信息,作者肯定是app中的一个用户。
从用户的角度出发:一个用户可以关注其他多个作者 —— 我的关注
从作者的角度出发:一个用户(同是作者)也可以拥有很多个粉丝 —— 我的粉丝
如上图所示,在好友关注关系中,主要有以上三种状态,即:
-
我的粉丝(fans)
-
我的关注(follow)
-
互粉(mutual)
假设两个用户。用户ID分别为1和2。
考虑问题:
1、一对多
2、关注时间有序
3、集合
4、不重复
5、集合运算(交并补)
Zset:分数按照分数排序
关注文章作者: 1 2
-
将对方写入我的关注中。
-
将我写入对方的粉丝中。即:
示例:
ZADD follow:1 time(时间戳) 2 ZADD fans:2 time(时间戳) 1
取消关注: 1 2
-
将对方从我的关注中移除
-
将我从对方的粉丝中移除
示例:
ZREM follow:1 2 ZREM fans:2 1
查看关注列表:
ZRANGE follow:1 0 -1
查看粉丝列表:
ZRANGE fans:1 0 -1
关注数量:
ZCARD follow:1
粉丝数量:
ZCARD fans:1
人物关系:
1、我单向关注Ta。即我关注的Ta,但是Ta并没有关注我的
ZSCORE follow:1 2 #ture ZSCORE fans:1 2 #false # 第一条成立,第二条不成立,说明我单向关注了对方(1关注了2,而1的粉丝中没有2,说明2并没有关注1)
2、Ta单向关注我。即Ta关注我了,我并没有关注Ta
ZSCORE follow:1 2 #false ZSCORE fans:1 2 #true # 第一条不成立,第二条成立,说明对方单向关注了我(1没有关注2,而1的粉丝中有2,说明2关注了1)
3、互相关注。即我关注了Ta,Ta也关注了我
ZSCORE follow:1 2 #true ZSCORE fans:1 2 #true # 上面两条都成立,即说明互相关注了(1关注了2,并且1的粉丝中有2,说明2也关注1了)
5.3 功能实现
5.3.0 准备redis起步依赖
准备redis
# 设置redis密码 root 设置开启: AOF 持久化方式 docker run --name redis -p 6379:6379 -id --restart=always redis:latest redis-server --appendonly yes # 如果想加密码 在接一个参数 --requirepass root
在basic下创建 缓存工程: heima-cache-spring-boot-starter
并引入依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies>
准备配置类
package com.heima.cache.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; @Configuration public class RedisConfiguration { /** * 配置redisTemplate * @param redisConnectionFactory * @return */ @Bean RedisTemplate<String,String> redisTemplate(RedisConnectionFactory redisConnectionFactory){ RedisTemplate<String,String> redisTemplate = new RedisTemplate<>(); // 设置redis连接信息 redisTemplate.setConnectionFactory(redisConnectionFactory); // 设置开启事务支持 redisTemplate.setEnableTransactionSupport(true); // 设置key的序列化方式 redisTemplate.setKeySerializer(RedisSerializer.string()); // 设置value的序列化方式 redisTemplate.setValueSerializer(RedisSerializer.string()); return redisTemplate; } }
准备META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.heima.cache.config.RedisConfiguration
5.3.1 接口定义
@Api(value = "用户关注API",tags = "用户关注API") @RestController @RequestMapping("/api/v1/user") public class UserRelationController { @ApiOperation("关注 或 取关") @PostMapping("/user_follow") public ResponseResult follow(@RequestBody UserRelationDto dto){ } }
UserRelationDto
package com.heima.model.user.dtos; import lombok.Data; @Data public class UserRelationDto { // 文章作者ID Integer authorId; // 作者对应的apuserid Integer authorApUserId; // 文章id Long articleId; /** * 操作方式 * 0 关注 * 1 取消 */ Short operation; }
5.3.2 代码实现
1、在user-service
服务下引入Redis起步依赖
<dependency> <artifactId>heima-cache-spring-boot-starter</artifactId> <groupId>com.heima</groupId> <version>1.0-SNAPSHOT</version> </dependency>
在user-service微服务 添加redis的连接参数
spring: application: name: leadnews-user redis: host: 192.168.200.130
2、创建ApUserRelationService接口
package com.heima.user.service; import com.heima.model.common.dtos.ResponseResult; import com.heima.model.user.dtos.UserRelationDto; public interface ApUserRelationService { /** * 用户关注/取消关注 * @param dto * @return */ public ResponseResult follow(UserRelationDto dto); }
3、创建ApUserRelationService实现类
package com.heima.user.service.impl; import com.heima.common.constants.user.UserRelationConstants; import com.heima.common.exception.CustException; import com.heima.model.common.dtos.ResponseResult; import com.heima.model.common.enums.AppHttpCodeEnum; import com.heima.model.threadlocal.AppThreadLocalUtils; import com.heima.model.user.dtos.UserRelationDto; import com.heima.model.user.pojos.ApUser; import com.heima.user.service.ApUserRelationService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @Service public class ApUserRelationServiceImpl implements ApUserRelationService { @Autowired RedisTemplate<String,String> redisTemplate; @Override public ResponseResult follow(UserRelationDto dto) { // 1. 校验参数 authorApUserId 必须登录 operation 0 1 if(dto.getAuthorApUserId() == null){ CustException.cust(AppHttpCodeEnum.PARAM_INVALID,"作者对应的userId不存在"); } Short operation = dto.getOperation(); if(operation == null || (operation.intValue()!=0 && operation.intValue()!=1)){ CustException.cust(AppHttpCodeEnum.PARAM_INVALID,"关注类型错误"); } ApUser user = AppThreadLocalUtils.getUser(); if(user == null){ CustException.cust(AppHttpCodeEnum.NEED_LOGIN); } Integer loginId = user.getId(); Integer followId = dto.getAuthorApUserId(); // 判断 自己不可以关注自己 if(loginId.equals(followId)){ CustException.cust(AppHttpCodeEnum.DATA_NOT_ALLOW,"不可以自己关注自己哦~"); } // 校验之前有没有关注过 zscore // 参数1: key 参数2: 要查询集合元素 Double score = redisTemplate.opsForZSet() .score(UserRelationConstants.FOLLOW_LIST + loginId, String.valueOf(followId)); if(operation.intValue() == 0&&score!=null){ CustException.cust(AppHttpCodeEnum.DATA_EXIST,"您已关注,请勿重复关注"); } try { // 开启 redis 的事务 redisTemplate.multi(); // 2. 判断operation 是0 是1 if(operation.intValue() == 0){ // 没有关注过 zadd follow:我的id 作者id // 参数1: key 参数2 集合元素 参数3: score redisTemplate.opsForZSet().add(UserRelationConstants.FOLLOW_LIST + loginId,String.valueOf(followId),System.currentTimeMillis()); // zadd fans:作者id 我的id redisTemplate.opsForZSet().add(UserRelationConstants.FANS_LIST + followId,String.valueOf(loginId),System.currentTimeMillis()); }else { // 2.2 是1 取关 // zrem follow:我的id 作者id redisTemplate.opsForZSet().remove(UserRelationConstants.FOLLOW_LIST + loginId,String.valueOf(followId)); // zrem fans:作者id 我的id redisTemplate.opsForZSet().remove(UserRelationConstants.FANS_LIST + followId,String.valueOf(loginId)); } // 提交事务 redisTemplate.exec(); }catch (Exception e){ e.printStackTrace(); // 如果有异常 取消事务 redisTemplate.discard(); // 取消事务 throw e; } return ResponseResult.okResult(); } }
涉及常量类:
package com.heima.common.constants.user; public class UserRelationConstants { // 用户关注key public static final String FOLLOW_LIST = "apuser:follow:"; // 用户粉丝key public static final String FANS_LIST = "apuser:fans:"; }
-
重新生成新的 静态页面,打开app端项目进行联调测试。