news的freemarker;对象存储服务MinIO ;day7自媒体文章自动审核;day08-分布式任务调度&人工审核;day09-app端文章详情&关注作者

1.freemarker页面静态化技术

1.1 freemarker 介绍

FreeMarker 是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。

模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是像PHP那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。

1528820943975

模板 + 数据 = 静态文件

常用的java模板引擎还有哪些?

Jsp、Freemarker、Thymeleaf 、Velocity 等。

  1. Jsp 为 Servlet 专用,不能单独进行使用。

  2. Thymeleaf 为新技术,功能较为强大,但是执行的效率比较低。

  3. 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

1576129529361

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、输出:

姓名为“小明”则字体颜色显示为红色。

1539947776259

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文件生成的方法:

image-20210422163843108

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/ 即可进入登录界面。

image-20210417102204739

Access Key为minio Secret_key 为minio123 进入系统后可以看到主界面

image-20210417102356582

点击右下角的“+”号 ,点击下面的图标,创建一个桶

image-20210417102435088

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 替换配置

拷贝资料 中 configservice文件夹, 替换原有文件

image-20210808212655020

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);
    }
}
​

修改下实现类的细节

image-20210809170943930

 

第七章 平台自动审核自媒体文章

今日目标

  • 能够掌握自媒体文章审核的流程

  • 能够使用阿里云安全服务检测文章内容

  • 能够完成自媒体文章审核的功能

  • 能够完成自媒体发布文章与审核对接

  • 能够完成文章发布成功后生成静态页面

1 自媒体文章自动审核需求说明

1.1 自媒体文章自动审核流程

做为内容类产品,内容安全非常重要,所以需要进行对自媒体用户发布的文章进行审核以后才能到app端展示给用户。

审核的流程如下:也可以查看当前讲义文件夹下:自媒体文章发布时序图.pdf

image-20210116005025080

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 自媒体文章表 在自媒体库

1585372690883

status字段:0 草稿 1 待审核 2 审核失败 3 人工审核 4 人工审核通过 8 审核通过(待发布) 9 已发布

(2)ap_author 文章作者表 在article库

1585372935340

(3)ap_article_config 文章配置表 在article库

1585707181340

对应实体:

<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库

1585707265543

  • 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库

1585707351655

对应实体:

<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。

image-20210603185412857

雪花算法实现

snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0

1598868152279

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库中新增文章数据

  1. 保存文章信息 ap_article

  2. 保存文章配置信息 ap_article_config

  3. 保存文章内容 ap_article_content

具体实现的思路:

当前接口具备保存三个表数据的功能,如果已经有了文章id,则是修改,如果没有id为新增数据。

同时需要把资料文件夹中的类拷贝到文章微服务下:

1606067472437

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

image-20210710113239163

查看以下结果:

  1. wm_news 数据状态是否发生变化

  2. ap_article 表新增数据

  3. ap_article_content 表新增数据

  4. 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

image-20210603173756134

5 文章审核功能-综合测试

服务启动列表:

  1. nacos

  2. seata

  3. zookeeper&kafka

  4. article微服务

  5. wemedia微服务

  6. admin微服务

  7. wemedia网关微服务

  8. 启动前端系统wemedia

测试动作:在自媒体前端进行发布文章

结果:

  1. 审核成功后,app端的article相关数据是否可以正常插入

  2. 审核成功或失败后,wm_news表中的状态是否改变,成功和失败原因正常插入

6 文章详情页面静态化

6.1 需求分析

image-20210602180753705

6.2 实现方案

6.2.1 基于数据库查询方案

用户某一条文章,根据文章的id去查询文章内容表,返回渲染页面

image-20210602180824202

优点:

  • 实现简单

  • 保证数据强一致性

缺点:

  • 无法支撑高并发

6.2.2 页面静态化方案

image-20210602180856833

优点:

  • 支撑高并发,高可用

  • 页面响应快,用户体验好

缺点:

  • 强一致性较弱,但能够保证最终一致性

6.3 静态页面生成

6.3.1 修改apArticle实体类及表

添加用于存储静态页url字段

image-20210710155124127

修改实体类

image-20210710155254398

<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模板

image-20210710155723395

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 前后端集成测试

服务启动列表:

  1. nacos

  2. seata

  3. zookeeper&kafka

  4. article微服务

  5. wemedia微服务

  6. admin微服务

  7. wemedia网关微服务

  8. 启动前端系统wemedia

测试动作:在自媒体前端进行发布文章

结果:

  1. 审核成功后,app端的article相关数据是否可以正常插入

  2. 审核成功或失败后,wm_news表中的状态是否改变,成功和失败原因正常插入

  3. 如果文章审核成功是否可以正常生成静态页面并上传到MinIO

访问生成后的静态文件:

资料中找到plugins文件夹 手动上传到MinIO article文件夹下

image-20210603233019232

将存储的路径修改为相对路径

image-20210712095717045

测试静态页能否访问

image-20210710154850972

听课笔记:

<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

1586018109877

(2)新建任务

如下图:

image-20210118004838388

执行器:选择自己新建的执行器

路由策略:轮询

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端人工处理文章的审核

image-20210120084747304

平台管理员可以查看待人工审核的文章信息,可以通过(状态改为4)驳回(状态改为2)

也可以通过点击查看按钮,查看文章详细信息,查看详情后可以根据内容判断是否需要通过审核

1599323199916

3.2 媒体审核功能实现

3.2.1 查询文章列表

查询文章列表时候,不仅仅要返回文章数据,也要返回作者名称,这个时候返回的数据,需要包含作者名称

  1. 需要使用wm_news表与wm_user表做关联查询

  2. 在返回的结果的时候,单独再封装一个类,用于包装用户名称和文章数据 WmNewsVo

vo:view object 视图对象 是一个表现层对象,主要对应页面显示(web页面)的数据对象

  1. 由于需要做关联查询,mybatis-plus暂时不支持关联查询,需要自定义mapper实现

  2. 查询全部时,要排除草稿状态文章

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) 访问前端测试

image-20210323170722644

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端文章的配置信息即可。

1599398923029

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. 延时发布问题

  2. 文章库数据量比较大,对数据性能有损耗,定时1分钟查询一次数据库

解决方案:延迟消息队列解决

生产方:

  • wemedia,文章提交审核并且文章的状态为审核通过, 发延迟消息

    image-20210325160818849

消费方:

  • 监听死信队列

  • 完成文章审核和发布

线程池

<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实现定时任务

1599012049743

调度中心:负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。

任务执行器:负责接收调度请求并执行任务逻辑。

任务:专注于任务的处理。

调度中心会发出调度请求,任务执行器接收到请求之后会去执行任务,任务则专注于任务业务的处理。

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张表

1586003140531

<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进行编译即可,源码结构如下:

1599012209108

安装到本地仓库:mvn clean -DskipTests install

1586003217207

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”, 登录后运行界面如下图所示。

1586003989155

至此“调度中心”项目已经部署成功。

2.2 入门案例编写

2.2.1 配置执行器

在任务调度中心,点击进入"执行器管理"界面, 如下图:

1、此处的AppName,会在创建任务时被选择,每个任务必然要选择一个执行器。 2、"执行器列表" 中显示在线的执行器列表, 支持编辑删除。

以下是执行器的属性说明:

属性名称说明
AppName是每个执行器集群的唯一标示AppName, 执行器会周期性以AppName为对象进行自动注册。可通过该配置自动发现注册成功的执行器, 供任务调度时使用;
名称执行器的名称, 因为AppName限制字母数字等组成,可读性不强, 名称为了提高执行器的可读性;
排序执行器的排序, 系统中需要执行器的地方,如任务新增, 将会按照该排序读取可用的执行器列表;
注册方式调度中心获取执行器地址的方式;
机器地址注册方式为"手动录入"时有效,支持人工维护执行器的地址信息;

具体操作:

(1)新增执行器:

(2)自动注册和手动注册的区别和配置

image-20191122162208722

2.2.2 在调度中心新建任务

在任务管理->新建,填写以下内容

1586009625631

  • 执行器:任务的绑定的执行器,任务触发调度时将会自动发现注册成功的执行器, 实现任务自动发现功能; 另一方面也可以方便的进行任务分组。每个任务必须绑定一个执行器, 可在 "执行器管理" 进行设置

  • 任务描述:任务的描述信息,便于任务管理

    路由策略:当执行器集群部署时,提供丰富的路由策略,包括

    • 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仓库

1599219083442

或者直接进入源码包目录下执行: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的值保持一致,如下图:

1586010782089

2.2.6 测试

(1)首先启动调度中心

(2)启动xxljob-test项目,为了展示更好的效果,可以同时启动三个项目,用同一个JobHandler,查看处理方式。

在启动多个项目的时候,端口需要切换,连接xxl-job的执行器端口不同相同

服务一:默认启动8801端口,执行器端口为9999

idea中不用其他配置,直接启动项目即可

服务二:项目端口:8802,执行器端口:9998

idea配置如下:

  • 编辑配置,Edit Configurations...

1586011170804

  • 选中XxlJobApplication,点击复制

1586011231409

  • 修改参数

1586011073678

  • 启动:选中8802启动项目

1586011290448

服务三:项目端口:8803,执行器端口:9997

1586011104142

(3)测试效果

三个项目启动后,可以查看到是轮询的方式分别去执行当前调度任务。

2.3 广播任务和动态分片

2.3.1 什么是作业分片

作业分片是指任务的分布式执行,需要将一个任务拆分为多个独立的任务项,然后由分布式的应用实例 分别执行某一个或几个分片项。

1606146803343

2.3.2 XXL-JOB分片

执行器集群部署时,任务路由策略选择”分片广播”情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;

“分片广播” 以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。

“分片广播” 和普通任务开发流程一致,不同之处在于可以获取分片参数,获取分片参数进行分片业务处理。

1606146896202

2.3.3 XXL-JOB支持分片的好处

  • 分片项与业务处理解耦 XXL-JOB并不直接提供数据处理的功能,框架只会将分片项分配至各个运行中的作业服务器,开发者需 要自行处理分片项与真实数据的对应关系。

  • 最大限度利用资源 基于业务需求配置合理数量的执行器服务,合理设置分片,作业将会最大限度合理的利用分布式资源。

2.3.4 适用场景

  • 分片任务场景:10个执行器的集群来处理10w条数据,每台机器只需要处理1w条数据,耗时降低10倍;

  • 广播任务场景:广播执行器机器运行shell脚本、广播集群节点进行缓存更新等

2.3.5 分片广播案例演示

目标:实现XXL-JOB作业分片的演示

方案分析:规划一个任务,两个分片,对应两个执行器,每个分片处理一部分任务。

实现步骤:

  • 创建分片执行器

    1606147177624

  • 创建任务

    指定刚才创建的分片执行器,在路由策略这一栏选择分片广播

    1606147259230

  • 分片广播代码

    分片参数属性说明:

    • 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);
    }
}

完整代码结构:

image-20210609015522720

2 app端登录功能

2.1 app端登录-需求分析

  • 点击登录可以根据app端的手机号和密码进行登录

  • 点击不登录,先看看可以在无登录状态下进入app

2.2 app端登录-思路分析

概念介绍:用户设备,即当前用户所使用的终端设备。

  1. 用户点击登录

  • 用户输入手机号和密码到后端进行校验,校验成功生成token返给前端

  • 其他请求需要带着token到app网关去校验jwt,校验成功,放行

  1. 用户点击不登录,先看看

  • 获取用户的设备id到后端根据设备id生成token,设置jwt存储的id为0

  • 其他请求需要带着token到app网关去校验jwt,校验成功,放行

1599641115217

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

image-20210606191412949

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 启动前端项目

image-20210119150707288

启动项目后:

image-20210119150755439

如果当前电脑和手机在一个局域网下下载weex的app,就可以扫码在手机端查看效果

app下载地址:Weex Incubation Status - Apache Incubator

效果如下:

扫描二维码查看效果

1586098390040

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

打开前端页面测试频道列表是否可以展示:

image-20210813100054961

4 app端-文章列表

4.1 app端文章列表-需求分析

在手机端可以查看文章信息

1585750768773

  1. 在默认频道展示10条文章信息

  2. 可以切换频道查看不同种类文章

  3. 当用户上拉可以加载更多的文章信息(按照发布时间)

    • 分页

    • 本页文章列表中发布时间为最小的时间为依据

  4. 当用户下拉可以加载最新的文章

    • 本页文章列表中发布时间最大的时间为依据

1599533161737

如果加载首页数据,前端会默认给传递一些参数

1606314616342

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端文章列表-测试(采用静态化方式已无此问题)

1599471047508

4.4 Long类型转换精度丢失问题解决

4.4.1 解决方案分析:

方案一:将文章的id的由long类型手动改为String类型,可以解决此问题。(需要修改表结构)

方案二:可以使用jackson进行序列化和反序列化解决(本项目采用这种方案)

4.4.2 jackson序列化和反序列化原理说明

1599614716990

  • 当后端响应给前端的数据中包含了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前端再次访问文章详情,问题解决。

1599471110116

5 app端-关注作者或取消关注

5.1 需求分析

1599707964444

如上效果:

当前登录后的用户可以关注作者对应的APUser ID,也可以取消关注作者对应的APUser ID

5.2 解决方案

方案一:基于数据库保存对应的关注关系

方案二:基于Redis保存。

项目中使用方案二。

一个用户关注了作者,作者是由用户实名认证以后开通的作者权限,才有了作者信息,作者肯定是app中的一个用户。

从用户的角度出发:一个用户可以关注其他多个作者 —— 我的关注

从作者的角度出发:一个用户(同是作者)也可以拥有很多个粉丝 —— 我的粉丝

image-20210609003829024

如上图所示,在好友关注关系中,主要有以上三种状态,即:

  • 我的粉丝(fans)

  • 我的关注(follow)

  • 互粉(mutual)

假设两个用户。用户ID分别为1和2。

考虑问题:

1、一对多

2、关注时间有序

3、集合

4、不重复

5、集合运算(交并补)

Zset:分数按照分数排序

关注文章作者: 1 2

  1. 将对方写入我的关注中。

  2. 将我写入对方的粉丝中。即:

示例:

ZADD follow:1 time(时间戳)  2
ZADD fans:2 time(时间戳)    1

取消关注: 1 2

  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:";

}
  1. 重新生成新的 静态页面,打开app端项目进行联调测试。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值