java黑马头条 day5自媒体文章审核 敏感词过滤算法DFA 集成RabbitMQ实现自动审核

这篇具有很好参考价值的文章主要介绍了java黑马头条 day5自媒体文章审核 敏感词过滤算法DFA 集成RabbitMQ实现自动审核。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

自动审核流程介绍

 

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

WmNews 中status 代表自媒体文章的状态

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

java黑马头条 day5自媒体文章审核 敏感词过滤算法DFA 集成RabbitMQ实现自动审核

  • 当自媒体用户提交发布文章之后,会发消息给RabbitMQ提交审核

  • 自媒体微服务提供消息监听,处理自动审核

  • 查询文章数据

  • 判断文章id是否为1 (只有1需要自动审核)

  • 文章内容中是否有自管理的敏感词,如果有则审核不通过,修改自媒体文章状态为2

  • 调用阿里云文本反垃圾服务,进行文本审核 审核不通过 2 人工审核 3

  • 调用阿里云图片审核服务,进行图片审核 审核不通过 2 人工审核 3

  • 如果审核通过 判断发布时间 是否小于等于当前时间 如果小于等于 直接发消息通知 文章微服务 发布文章

  • 如果未到发布时间,将消息发送到RabbitMQ的死信队列 并设置消息失效时间

2 内容安全第三方接口对接

2.1)内容安全接口选型

内容安全是识别服务,支持对图片、视频、文本、语音等对象进行多样化场景检测,有效降低内容违规风险。

黑马头条发布文章中有内容可能违规,如何有效避免风险,可以使用第三方接口进行内容检测。

目前很多平台都支持内容检测,如阿里云、腾讯云、百度AI、网易云等国内大型互联网公司都对外提供了API。

按照性能和收费来看,黑马头条项目使用的就是阿里云的内容安全接口,使用到了图片和文本的审核。

阿里云收费标准:阿里云定价_云产品价格

2.2)阿里云服务介绍

2.2.1 准备工作

您在使用内容检测API之前,需要先注册阿里云账号,添加Access Key并签约云盾内容安全。

操作步骤

  1. 前往阿里云官网注册账号。如果已有注册账号,请跳过此步骤。

    进入阿里云首页后,如果没有阿里云的账户需要先进行注册,才可以进行登录。由于注册较为简单,课程和讲义不在进行体现(注册可以使用多种方式,如淘宝账号、支付宝账号、微博账号等...)。

    需要实名认证和活体认证。

  2. 打开云盾内容安全产品试用页面,单击立即开通,正式开通服务。

  3. 在AccessKey管理页面管理您的AccessKeyID和AccessKeySecret。

    2.2.2 阿里云安全-文本内容垃圾检测

    文本垃圾内容检测接口说明

    示例代码地址:如何使用JavaSDK文本反垃圾接口_内容安全-阿里云帮助中心

    创建项目aliyun-test

    安装sdk

    <dependencies>
        <!-- 阿里云内容安全sdk -->
      <dependency>
          <groupId>com.aliyun</groupId>
          <artifactId>aliyun-java-sdk-core</artifactId>
          <version>4.1.1</version>
      </dependency>
      <dependency>
          <groupId>com.aliyun</groupId>
          <artifactId>aliyun-java-sdk-green</artifactId>
          <version>3.6.2</version>
      </dependency>
      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>fastjson</artifactId>
          <version>1.2.51</version>
      </dependency>
      <dependency>
          <groupId>com.aliyun.oss</groupId>
          <artifactId>aliyun-sdk-oss</artifactId>
          <version>2.8.3</version>
      </dependency>
    ​
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.4</version>
        </dependency>
    ​
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.10</version>
        </dependency>
        <dependency>
         <groupId>commons-logging</groupId>
         <artifactId>commons-logging</artifactId>
         <version>1.2</version>
            </dependency>
    </dependencies>

    示例代码

    package com.itheima.aliyun.util;
    ​
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONArray;
    import com.alibaba.fastjson.JSONObject;
    import com.aliyun.oss.ClientException;
    import com.aliyuncs.DefaultAcsClient;
    import com.aliyuncs.IAcsClient;
    import com.aliyuncs.exceptions.ServerException;
    import com.aliyuncs.green.model.v20180509.TextScanRequest;
    import com.aliyuncs.http.FormatType;
    import com.aliyuncs.http.HttpResponse;
    import com.aliyuncs.profile.DefaultProfile;
    import com.aliyuncs.profile.IClientProfile;
    ​
    import java.util.*;
    ​
    public class Main {
    ​
        public static void main(String[] args) throws Exception {
            IClientProfile profile = DefaultProfile
                .getProfile("cn-shanghai", "LTAI4F1mKL2EKYCGgN2az5M57", "XjgvRoAGzM3rWQxKWDJx198VWOmO0Hz");
            DefaultProfile
                .addEndpoint("cn-shanghai", "cn-shanghai", "Green", "green.cn-shanghai.aliyuncs.com");        
            IAcsClient client = new DefaultAcsClient(profile);
            TextScanRequest textScanRequest = new TextScanRequest();
            textScanRequest.setAcceptFormat(FormatType.JSON); // 指定API返回格式。
            textScanRequest.setHttpContentType(FormatType.JSON);
            textScanRequest.setMethod(com.aliyuncs.http.MethodType.POST); // 指定请求方法。
            textScanRequest.setEncoding("UTF-8");
            textScanRequest.setRegionId("cn-shanghai");
            List<Map<String, Object>> tasks = new ArrayList<Map<String, Object>>();
            Map<String, Object> task1 = new LinkedHashMap<String, Object>();
            task1.put("dataId", UUID.randomUUID().toString());
            /**
             * 待检测的文本,长度不超过10000个字符。
             */
            task1.put("content", "我是一个文本,买卖冰毒是违法的");
            tasks.add(task1);
            JSONObject data = new JSONObject();
    ​
            /**
             * 检测场景。文本垃圾检测请传递antispam。
             **/
            data.put("scenes", Arrays.asList("antispam"));
            data.put("tasks", tasks);
            System.out.println("参数:"+JSON.toJSONString(data, true));
            textScanRequest.setHttpContent(data.toJSONString().getBytes("UTF-8"), "UTF-8", FormatType.JSON);
            // 请务必设置超时时间。
            textScanRequest.setConnectTimeout(3000);
            textScanRequest.setReadTimeout(6000);
            try {
                HttpResponse httpResponse = client.doAction(textScanRequest);
                if(httpResponse.isSuccess()){
                    JSONObject scrResponse = JSON.parseObject(new String(httpResponse.getHttpContent(), "UTF-8"));
                    System.out.println("结果:"+JSON.toJSONString(scrResponse, true));
                    if (200 == scrResponse.getInteger("code")) {
                        JSONArray taskResults = scrResponse.getJSONArray("data");
                        for (Object taskResult : taskResults) {
                            if(200 == ((JSONObject)taskResult).getInteger("code")){
                                JSONArray sceneResults = ((JSONObject)taskResult).getJSONArray("results");
                                for (Object sceneResult : sceneResults) {
                                    String scene = ((JSONObject)sceneResult).getString("scene");
                                    String suggestion = ((JSONObject)sceneResult).getString("suggestion");
                                    //根据scene和suggetion做相关处理。
                                    //suggestion == pass表示未命中垃圾。suggestion == block表示命中了垃圾,可以通过label字段查看命中的垃圾分类。
                                    System.out.println("args = [" + scene + "]");
                                    System.out.println("args = [" + suggestion + "]");
                                }
                            }else{
                                System.out.println("task process fail:" + ((JSONObject)taskResult).getInteger("code"));
                            }
                        }
                    } else {
                        System.out.println("detect not success. code:" + scrResponse.getInteger("code"));
                    }
                }else{
                    System.out.println("response not success. status:" + httpResponse.getStatus());
                }
            } catch (ServerException e) {
                e.printStackTrace();
            } catch (ClientException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    ​
    }

    测试一:输入以上的内容,检测通过    

  4. java黑马头条 day5自媒体文章审核 敏感词过滤算法DFA 集成RabbitMQ实现自动审核

 java黑马头条 day5自媒体文章审核 敏感词过滤算法DFA 集成RabbitMQ实现自动审核

2.2.3 阿里云安全-图片审核

参考阿里云提供的接口文档说明文档地址

示例代码地址

注意事项:如果使用本地文件或者二进制文件检测,请下载并在项目工程中引入Extension.Uploader工具类。

修改后的示例代码

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.green.model.v20180509.ImageSyncScanRequest;
import com.aliyuncs.http.FormatType;
import com.aliyuncs.http.HttpResponse;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.http.ProtocolType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
​
import java.util.*;
​
public class ImgMain {
​
    public static void main(String[] args) throws Exception {
        IClientProfile profile = DefaultProfile
                .getProfile("cn-shanghai", "LTAI4FzL1ddwcgSNDv3GTfJZ1", "13ygpLlW8MUervH5U2it420vGG1AcbF");
        DefaultProfile
                .addEndpoint("cn-shanghai", "cn-shanghai", "Green", "green.cn-shanghai.aliyuncs.com");
        IAcsClient client = new DefaultAcsClient(profile);
​
        ImageSyncScanRequest imageSyncScanRequest = new ImageSyncScanRequest();
        // 指定API返回格式。
        imageSyncScanRequest.setAcceptFormat(FormatType.JSON);
        // 指定请求方法。
        imageSyncScanRequest.setMethod(MethodType.POST);
        imageSyncScanRequest.setEncoding("utf-8");
        // 支持HTTP和HTTPS。
        imageSyncScanRequest.setProtocol(ProtocolType.HTTP);
​
​
        JSONObject httpBody = new JSONObject();
        /**
         * 设置要检测的风险场景。计费依据此处传递的场景计算。
         * 一次请求中可以同时检测多张图片,每张图片可以同时检测多个风险场景,计费按照场景计算。
         * 例如,检测2张图片,场景传递porn和terrorism,计费会按照2张图片鉴黄,2张图片暴恐检测计算。
         * porn:表示鉴黄场景。
         */
        httpBody.put("scenes", Arrays.asList("terrorism"));
​
        /**
         * 设置待检测图片。一张图片对应一个task。
         * 多张图片同时检测时,处理的时间由最后一个处理完的图片决定。
         * 通常情况下批量检测的平均响应时间比单张检测的要长。一次批量提交的图片数越多,响应时间被拉长的概率越高。
         * 这里以单张图片检测作为示例, 如果是批量图片检测,请自行构建多个task。
         */
        JSONObject task = new JSONObject();
        task.put("dataId", UUID.randomUUID().toString());
​
        // 设置图片链接。
        task.put("url", "https://heimaleadnewsoss.oss-cn-shanghai.aliyuncs.com/material/2021/1/20210112/205cd5d3346a48b59352c92808709da1.jpg");
        task.put("time", new Date());
        httpBody.put("tasks", Arrays.asList(task));
​
        imageSyncScanRequest.setHttpContent(org.apache.commons.codec.binary.StringUtils.getBytesUtf8(httpBody.toJSONString()),
            "UTF-8", FormatType.JSON);
​
        /**
         * 请设置超时时间。服务端全链路处理超时时间为10秒,请做相应设置。
         * 如果您设置的ReadTimeout小于服务端处理的时间,程序中会获得一个read timeout异常。
         */
        imageSyncScanRequest.setConnectTimeout(3000);
        imageSyncScanRequest.setReadTimeout(10000);
        HttpResponse httpResponse = null;
        try {
            httpResponse = client.doAction(imageSyncScanRequest);
        } catch (Exception e) {
            e.printStackTrace();
        }
​
        // 服务端接收到请求,完成处理后返回的结果。
        if (httpResponse != null && httpResponse.isSuccess()) {
            JSONObject scrResponse = JSON.parseObject(org.apache.commons.codec.binary.StringUtils.newStringUtf8(httpResponse.getHttpContent()));
            System.out.println(JSON.toJSONString(scrResponse, true));
            int requestCode = scrResponse.getIntValue("code");
            // 每一张图片的检测结果。
            JSONArray taskResults = scrResponse.getJSONArray("data");
            if (200 == requestCode) {
                for (Object taskResult : taskResults) {
                    // 单张图片的处理结果。
                    int taskCode = ((JSONObject) taskResult).getIntValue("code");
                    // 图片对应检测场景的处理结果。如果是多个场景,则会有每个场景的结果。
                    JSONArray sceneResults = ((JSONObject) taskResult).getJSONArray("results");
                    if (200 == taskCode) {
                        for (Object sceneResult : sceneResults) {
                            String scene = ((JSONObject) sceneResult).getString("scene");
                            String suggestion = ((JSONObject) sceneResult).getString("suggestion");
                            // 根据scene和suggestion做相关处理。
                            // 根据不同的suggestion结果做业务上的不同处理。例如,将违规数据删除等。
                            System.out.println("scene = [" + scene + "]");
                            System.out.println("suggestion = [" + suggestion + "]");
                        }
                    } else {
                        // 单张图片处理失败, 原因视具体的情况详细分析。
                        System.out.println("task process fail. task response:" + JSON.toJSONString(taskResult));
                    }
                }
            } else {
                /**
                 * 表明请求整体处理失败,原因视具体的情况详细分析。
                 */
                System.out.println("the whole image scan request failed. response:" + JSON.toJSONString(scrResponse));
            }
        }
    }
​
}

测试:

测试结果,ak47.jpg涉及兵器,审核不通过,itheima.jpg审核通过,如果文章中有任何一张图片审核不通过,则文章审核就不通过。

image1测试结果:不通过

java黑马头条 day5自媒体文章审核 敏感词过滤算法DFA 集成RabbitMQ实现自动审核

 java黑马头条 day5自媒体文章审核 敏感词过滤算法DFA 集成RabbitMQ实现自动审核

3 阿里云安全集成到项目

3.1)依赖引入

创建 heima-aliyunsecurity-spring-boot-starter 模块引入阿里云sdk依赖

<dependencies>
  <dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-sdk-core</artifactId>
  </dependency>
  <dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-sdk-green</artifactId>
  </dependency>
  <!--OSS-->
  <dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.10.2</version>
  </dependency>
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>
</dependencies>

3.2)引入图片上传工具类

从之前测试阿里云服务的工程拷贝到heima-aliyunsecurity-spring-boot-starter中,结构如下:

java黑马头条 day5自媒体文章审核 敏感词过滤算法DFA 集成RabbitMQ实现自动审核

 引入 资料文件夹下:文本内容审核和图片审核 对应的工具类

3.3)新建配置文件

在resources中新建aliyun.properties

aliyun.accessKeyId=LTAI5tEFSvKvjn8WX2jA5FAc
aliyun.secret=QQB92dKoCByJDMd4dpOGKdEvcmeYw9
#aliyun.scenes=porn,terrorism,ad,qrcode,live,logo
aliyun.scenes=porn

参数说明:scenes,当前的这个场景设置,只有在图片审核的时候会用到,可以根据实际情况自由组合

  • porn:图片智能鉴黄

  • terrorism:图片暴恐涉政

  • ad:图文违规

  • qrcode:图片二维码

  • live:图片不良场景

  • logo:图片logo

创建 META-INF/spring.factories 文件:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.heima.aliyun.config.AliyunConfig

3.4)测试

后期需要在admin微服务中使用,可以在admin微服中引用

wemedia-service微服务中添加依赖,支持阿里云接口服务

wemedia的pom添加依赖:

    <dependency>
        <groupId>com.heima</groupId>
        <artifactId>heima-aliyunsecurity-spring-boot-starter</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
​

创建测试类:

分别测试文本垃圾检测接口和图片审核接口

@SpringBootTest
@RunWith(SpringRunner.class)
public class AliyunTest {
    @Autowired
    private GreenTextScan greenTextScan;
    @Autowired
    private GreenImageScan greenImageScan;
    @Test
    public void testText() throws Exception{
        Map map = greenTextScan.greenTextScan("我是一个文本,冰毒买卖是违法的");
        System.out.println(map);
    }
    @Test
    public void testImage() throws Exception {
        List<String> images = new ArrayList<>();
        images.add("https://heimaleadnewsoss.oss-cn-shanghai.aliyuncs.com/material/2021/1/20210112/205cd5d3346a48b59352c92808709da1.jpg");
        Map map = greenImageScan.imageUrlScan(images);
        System.out.println(map);
    }
}

4 敏感词过滤算法DFA

(1)文章审核需求

文章审核功能已经交付了,文章也能正常发布审核。突然,产品经理过来说要开会。

会议的内容核心有以下内容:

  • 文章审核不能过滤一些敏感词:

    私人侦探、针孔摄象、信用卡提现、广告代理、代开发票、刻章办、出售答案、小额贷款…

需要完成的功能:

需要自己维护一套敏感词,在文章审核的时候,需要验证文章是否包含这些敏感词

(2)敏感词-过滤

技术选型

方案 说明
数据库模糊查询 效率太低
String.indexOf("")查找 数据库量大的话也是比较慢
全文检索 分词再匹配
DFA算法 确定有穷自动机(一种数据结构)

4.1)DFA实现原理

DFA全称为:Deterministic Finite Automaton,即确定有穷自动机。

存储:一次性的把所有的敏感词存储到了多个map中,就是下图表示这种结构

敏感词:冰毒、大麻、大坏蛋

java黑马头条 day5自媒体文章审核 敏感词过滤算法DFA 集成RabbitMQ实现自动审核

java黑马头条 day5自媒体文章审核 敏感词过滤算法DFA 集成RabbitMQ实现自动审核

4.2)代码实现

工具类:

package com.heima.utils.common;
import java.util.*;
public class SensitiveWordUtil {
    public static Map<String, Object> dictionaryMap = new HashMap<>();
    /**
     * 生成关键词字典库
     * @param words
     * @return
     */
    public static void initMap(Collection<String> words) {
        if (words == null) {
            System.out.println("敏感词列表不能为空");
            return ;
        }
        // map初始长度words.size(),整个字典库的入口字数(小于words.size(),因为不同的词可能会有相同的首字)
        Map<String, Object> map = new HashMap<>(words.size());
        // 遍历过程中当前层次的数据
        Map<String, Object> curMap = null;
        Iterator<String> iterator = words.iterator();
​
        while (iterator.hasNext()) {
            String word = iterator.next();
            curMap = map;
            int len = word.length();
            for (int i =0; i < len; i++) {
                // 遍历每个词的字
                String key = String.valueOf(word.charAt(i));
                // 当前字在当前层是否存在, 不存在则新建, 当前层数据指向下一个节点, 继续判断是否存在数据
                Map<String, Object> wordMap = (Map<String, Object>) curMap.get(key);
                if (wordMap == null) {
                    // 每个节点存在两个数据: 下一个节点和isEnd(是否结束标志)
                    wordMap = new HashMap<>(2);
                    wordMap.put("isEnd", "0");
                    curMap.put(key, wordMap);
                }
                curMap = wordMap;
                // 如果当前字是词的最后一个字,则将isEnd标志置1
                if (i == len -1) {
                    curMap.put("isEnd", "1");
                }
            }
        }
​
        dictionaryMap = map;
    }
​
    /**
     * 搜索文本中某个文字是否匹配关键词
     * @param text
     * @param beginIndex
     * @return
     */
    private static int checkWord(String text, int beginIndex) {
        if (dictionaryMap == null) {
            throw new RuntimeException("字典不能为空");
        }
        boolean isEnd = false;
        int wordLength = 0;
        Map<String, Object> curMap = dictionaryMap;
        int len = text.length();
        // 从文本的第beginIndex开始匹配
        for (int i = beginIndex; i < len; i++) {
            String key = String.valueOf(text.charAt(i));
            // 获取当前key的下一个节点
            curMap = (Map<String, Object>) curMap.get(key);
            if (curMap == null) {
                break;
            } else {
                wordLength ++;
                if ("1".equals(curMap.get("isEnd"))) {
                    isEnd = true;
                }
            }
        }
        if (!isEnd) {
            wordLength = 0;
        }
        return wordLength;
    }
​
    /**
     * 获取匹配的关键词和命中次数
     * @param text
     * @return
     */
    public static Map<String, Integer> matchWords(String text) {
        Map<String, Integer> wordMap = new HashMap<>();
        int len = text.length();
        for (int i = 0; i < len; i++) {
            int wordLength = checkWord(text, i);
            if (wordLength > 0) {
                String word = text.substring(i, i + wordLength);
                // 添加关键词匹配次数
                if (wordMap.containsKey(word)) {
                    wordMap.put(word, wordMap.get(word) + 1);
                } else {
                    wordMap.put(word, 1);
                }
                i += wordLength - 1;
            }
        }
        return wordMap;
    }
}

新建测试类:

package com.heima.admin;
import com.heima.utils.common.SensitiveWordUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class SensitiveWordUtilTest {
    public static void main(String[] args) {
        // 初始化 敏感词 列表
        List<String> list = new ArrayList<>();
        list.add("冰毒");
        list.add("特朗普");
        SensitiveWordUtil.initMap(list);
        // 待查询文本
        String content="我是一个好人,买卖冰毒是违法的特朗普";
        // 匹配文本
        Map<String, Integer> map = SensitiveWordUtil.matchWords(content);
        System.out.println(map);
    }
}

5 自媒体文章审核

5.1)表结构说明

wm_news 自媒体文章表

java黑马头条 day5自媒体文章审核 敏感词过滤算法DFA 集成RabbitMQ实现自动审核

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

5.2)准备远程调用接口

审核时需要进行自管理的DFA敏感词审核,而敏感词信息是在admin微服务中维护,需要使用feign进行调用

admin-serviceAdSensitiveMapper 新增方法

public interface AdSensitiveMapper extends BaseMapper<AdSensitive> {
    @Select("select sensitives from ad_sensitive")
    List<String> findAllSensitives();
}

admin-seriviceAdSensitiveService 新增方法

    /**
     * 查询敏感词内容列表
     * @return
     */
    public ResponseResult<List<String>> selectAllSensitives();

admin-seriviceAdSensitiveServiceImpl 实现方法

    @Override
    public ResponseResult selectAllSensitives() {
        return ResponseResult.okResult(adSensitiveMapper.findAllSensitives());
    }

admin-serviceAdSensitiveController新增方法

    @ApiOperation(value = "查询敏感词内容list")
    @PostMapping("/sensitives")
    public ResponseResult sensitives() {
        return sensitiveService.selectAllSensitives();
    }

heima-leadnews-feign 服务中新增feign接口AdminFeign

package com.heima.feigns;
import com.heima.config.HeimaFeignAutoConfiguration;
import com.heima.feigns.fallback.AdminFeignFallback;
import com.heima.model.common.dtos.ResponseResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.List;
@FeignClient(value = "leadnews-admin",
        fallbackFactory = AdminFeignFallback.class,
        configuration = HeimaFeignAutoConfiguration.class
)
public interface AdminFeign {
    // 查询敏感词内容列表
    @PostMapping("/api/v1/sensitive/sensitives")
    public ResponseResult<List<String>> sensitives();
}

heima-leadnews-feign 服务中新增AdminFeign服务降级实现类AdminFeignFallback

package com.heima.feigns.fallback;
import com.heima.feigns.AdminFeign;
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;
import java.util.List;
@Slf4j
@Component
public class AdminFeignFallback implements FallbackFactory<AdminFeign> {
    @Override
    public AdminFeign create(Throwable throwable) {
        throwable.printStackTrace();
        return new AdminFeign() {
            @Override
            public ResponseResult<List<String>> sensitives() {
                log.error("AdminFeign sensitives 远程调用出错啦 ~~~ !!!! {} ",throwable.getMessage());
                return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR);
            }
        };
    }
}

5.3)审核接口实现

wemedia-service 中的service新增接口

package com.heima.wemedia.service;
public interface WmNewsAutoScanService {
    /**
     * 自媒体文章审核
     * @param id  自媒体文章id
     */
    public void autoScanWmNews(Integer id);
}

实现类:

package com.heima.wemedia.service.impl;
​
import com.alibaba.fastjson.JSONArray;
import com.heima.aliyun.GreenImageScan;
import com.heima.aliyun.GreenTextScan;
import com.heima.common.exception.CustException;
import com.heima.feigns.AdminFeign;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.wemedia.pojos.WmNews;
import com.heima.utils.common.SensitiveWordUtil;
import com.heima.wemedia.mapper.WmNewsMapper;
import com.heima.wemedia.service.WmNewsAutoScanService;
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.*;
import java.util.stream.Collectors;
@Service
@Slf4j
public class WmNewsAutoScanServiceImpl implements WmNewsAutoScanService {
    @Autowired
    private WmNewsMapper wmNewsMapper;
    @Value("${file.oss.web-site}")
    String webSite;
    /**
     * 自动审核方法
     * @param wmNewsId
     */
    @Override
    public void autoScanWmNews(Integer wmNewsId) {
        log.info(" 自动审核发布方法 被调用   当前审核发布的文章id==> {}",wmNewsId);
        //1. 根据文章id 远程调用feign查询文章
        if (wmNewsId == null) {
            log.error("自动审核文章失败    文章id为空");
            CustException.cust(AppHttpCodeEnum.PARAM_INVALID);
        }
        WmNews wmNews = wmNewsMapper.selectById(wmNewsId);
        if (wmNews==null) {
            log.error("自动审核文章失败    未查询自媒体文章信息  wmNewsId:{}",wmNewsId);
            CustException.cust(AppHttpCodeEnum.DATA_NOT_EXIST);
        }
        // 2. 判断文章状态是否为待审核状态
        Short status = wmNews.getStatus();
        if(status.shortValue() == WmNews.Status.SUBMIT.getCode()){
            // 抽取出文章中 所有的文本内容 和 所有的图片url集合    Map<String,Object>  content 内容   images List<String>
            Map<String,Object> contentAndImageResult = handleTextAndImages(wmNews);
            // 3.1  敏感词审核    失败   修改文章状态(2)
            boolean isSensivice = handleSensitive((String)contentAndImageResult.get("content"),wmNews);
            if(!isSensivice) return;
            log.info(" 自管理敏感词审核通过  =======   ");
​
            // 3.2  阿里云的文本审核   失败  状态2  不确定 状态3
            boolean isTextScan = handleTextScan((String)contentAndImageResult.get("content"),wmNews);
            if(!isTextScan) return;
            log.info(" 阿里云内容审核通过  =======   ");
​
​
            // 3.3  阿里云的图片审核   失败  状态2  不确定 状态3
            Object images = contentAndImageResult.get("images");
            if(images!=null){
                boolean isImageScan =handleImageScan((List<String>)images,wmNews);
                if(!isImageScan) return;
                log.info(" 阿里云图片审核通过  =======   ");
            }
            // 4. 判断文章发布时间是否大于当前时间   状态 8
            updateWmNews(wmNews,WmNews.Status.SUCCESS.getCode(),"审核成功");
​
            //TODO 5. 通知定时发布文章
        }
    }
    @Autowired
    GreenImageScan greenImageScan;
    /**
     * 阿里云图片审核
     * @param images  待审核的图片列表
     * @return
     */
    private boolean handleImageScan(List<String> images,WmNews wmNews) {
        boolean flag = true;
        try {
            Map map = greenImageScan.imageUrlScan(images);
            String suggestion = (String)map.get("suggestion");
            switch (suggestion){
                case "block":
                    updateWmNews(wmNews,WmNews.Status.FAIL.getCode(),"图片中有违规内容,审核失败");
                    flag = false;
                    break;
                case "review":
                    updateWmNews(wmNews,WmNews.Status.ADMIN_AUTH.getCode(),"图片中有不确定内容,转为人工审核");
                    flag = false;
                    break;
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("阿里云图片审核出现异常 , 原因:{}",e.getMessage());
            updateWmNews(wmNews,WmNews.Status.ADMIN_AUTH.getCode(),"阿里云内容服务异常,转为人工审核");
            flag = false;
        }
        return flag;
    }
    @Autowired
    GreenTextScan greenTextScan;
    /**
     * 阿里云文本
     * @param content    block: 状态2    review: 状态3    异常: 状态3
     * @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":
                    updateWmNews(wmNews,WmNews.Status.FAIL.getCode(),"文本中有违规内容,审核失败");
                    flag = false;
                    break;
                case "review":
                    updateWmNews(wmNews,WmNews.Status.ADMIN_AUTH.getCode(),"文本中有不确定内容,转为人工审核");
                    flag = false;
                    break;
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("阿里云文本审核出现异常 , 原因:{}",e.getMessage());
            updateWmNews(wmNews,WmNews.Status.ADMIN_AUTH.getCode(),"阿里云内容服务异常,转为人工审核");
            flag = false;
        }
        return flag;
    }
​
    
    @Autowired
    AdminFeign adminFeign;
    
    /**
     * 基于DFA 检测内容是否包含敏感词
     * @param content
     * @param wmNews
     * @return
     */
    private boolean handleSensitive(String content, WmNews wmNews) {
        boolean flag = true;
        // 1. 查询出数据库中的敏感词
        ResponseResult<List<String>> allSensitivesResult = adminFeign.sensitives();
        if(allSensitivesResult.getCode().intValue()!=0){
            CustException.cust(AppHttpCodeEnum.REMOTE_SERVER_ERROR,allSensitivesResult.getErrorMessage());
        }
        List<String> allSensitives = allSensitivesResult.getData();
        // 2. 将敏感词集合转发DFA数据模型
        SensitiveWordUtil.initMap(allSensitives);
        // 3. 检测敏感词
        Map<String, Integer> resultMap = SensitiveWordUtil.matchWords(content);
        if(resultMap!=null && resultMap.size() > 0){
            // 将文章状态改为2
            updateWmNews(wmNews,WmNews.Status.FAIL.getCode(),"内容中包含敏感词: " + resultMap);
            flag = false;
        }
        return flag;
    }
​
    
    /**
     * 修改文章状态
     * @param wmNews
     * @param status
     * @param reason
     */
    private void updateWmNews(WmNews wmNews, short status, String reason) {
        wmNews.setStatus(status);
        wmNews.setReason(reason);
        wmNewsMapper.updateById(wmNews);
    }
    
    /**
     * 抽取 文章中所有 文本内容  及 所有图片路径
     * @param wmNews  content  type:text     title
     * @return
     */
    private Map<String, Object> handleTextAndImages(WmNews wmNews) {
        String contentJson = wmNews.getContent(); // [{},{},{}]
        if (StringUtils.isBlank(contentJson)) {
            log.error("自动审核文章失败    文章内容为空");
            CustException.cust(AppHttpCodeEnum.PARAM_INVALID,"文章内容为空");
        }
        List<Map> contentMaps = JSONArray.parseArray(contentJson, Map.class);
        // 1. 抽取文章中所有文本     家乡很美   _hmtt_   国家伟大
        String content = contentMaps.stream()
                .filter(map -> "text".equals(map.get("type")))
                .map(map -> (String) map.get("value"))
                .collect(Collectors.joining("_hmtt_"));
        content = content + "_hmtt_" + wmNews.getTitle();
​
        // 2. 抽取文章中所有图片   content :  全路径       images :  文件名称  + 访问前缀
        List<String> imageList = contentMaps.stream()
                .filter(map -> "image".equals(map.get("type")))
                .map(map -> (String) map.get("value"))
                .collect(Collectors.toList());
        if (StringUtils.isNotBlank(wmNews.getImages())) {
            // 按照 逗号 切割封面字符串  得到数组   基于数组得到stream   将每一条数据都拼接一个前缀 收集成集合
            List<String> urls = Arrays.stream(wmNews.getImages().split(","))
                    .map(url -> webSite + url)
                    .collect(Collectors.toList());
            imageList.addAll(urls);
        }
        // 3. 去重
        imageList = imageList.stream().distinct().collect(Collectors.toList());
​
        Map result = new HashMap();
        result.put("content",content);
        result.put("images",imageList);
        return result;
    }
}
​

6 集成RabbitMQ实现自动审核

6.1) 同步调用与异步调用

同步:就是在发出一个调用时,在没有得到结果之前, 该调用就不返回(实时处理)

异步:调用在发出之后,这个调用就直接返回了,没有返回结果(分时处理)

对于发表文章 及 自动审核,这是属于两个不同业务功能, 如果放到一起写耦合严重。

需要采用异步的方式,当发表文章完成后,向消息队列发送一条消息

而自动审核会监听指定的队列 完成文章的审核操作

6.3)集成RabbitMQ

wemedia-service引入mq依赖

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

nacos配置中心添加共享配置share-rabbit.yml

spring:
  rabbitmq:
    host: ${spring.profiles.ip}
    port: 5672
    username: itcast
    password: 123321
    publisher-confirm-type: correlated  # 开启确认机制回调 必须配置这个才会确认回调
    publisher-returns: true # 开启return机制回调
    listener:
      simple:
        # acknowledge-mode: manual #手动确认
        acknowledge-mode: auto #自动确认    manual #手动确认
        # 重试策略相关配置
        retry:
          enabled: true # 是否开启重试功能
          max-attempts: 5 # 最大重试次数
          # 时间策略乘数因子   0  1  2  4  8
          multiplier: 2.0
          initial-interval: 1000ms # 第一次调用后的等待时间
          max-interval: 20000ms # 最大等待的时间值

添加共享配置

spring:
  application:
    name: leadnews-wemedia # 服务名称
  profiles:
    active: dev # 开发环境配置
    ip: 192.168.200.130  # 环境ip地址
  cloud:
    nacos:
      server-addr: ${spring.profiles.ip}:8848
      discovery: # 注册中心地址配置
        namespace: ${spring.profiles.active}
      config: # 配置中心地址配置
        namespace: ${spring.profiles.active}
        file-extension: yml # data-id 后缀
        name: ${spring.application.name} # data-id名称
        shared-configs: # 共享配置
          - data-id: share-feign.yml # 配置文件名-Data Id
            group: DEFAULT_GROUP   # 默认为DEFAULT_GROUP
            refresh: false   # 是否动态刷新,默认为false
          - data-id: share-seata.yml # 配置文件名-Data Id
            group: DEFAULT_GROUP   # 默认为DEFAULT_GROUP
            refresh: false   # 是否动态刷新,默认为fals
          - data-id: share-file.yml # 配置文件名-Data Id
            group: DEFAULT_GROUP   # 默认为DEFAULT_GROUP
            refresh: false   # 是否动态刷新,默认为fals
          - data-id: share-rabbit.yml # 配置文件名-Data Id
            group: DEFAULT_GROUP   # 默认为DEFAULT_GROUP
            refresh: false   # 是否动态刷新,默认为fals
  servlet:
    multipart:
      max-file-size: 5MB
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

wemedia-service创建com.heima.wemedia.config.RabbitConfig配置类

package com.heima.wemedia.config;
​
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
/**
 * InitializingBean: springbean生命周期接口  代表完成bean装配后 执行的初始化方法
 * 这个类的目的:
 *     设置rabbitmq消息序列化机制  (默认jdk效率差)
 *     设置rabbitmq消息发送确认 回调
 *     设置rabbitmq消息返还 回调
 */
@Component
@Slf4j
public class RabbitConfig implements InitializingBean {
    @Autowired
    RabbitTemplate rabbitTemplate;
    @Override
    public void afterPropertiesSet()  {
        log.info("初始化rabbitMQ配置 ");
        // 设置消息转换器
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        // 设置发送确认 回调方法
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             * @param correlationData 对比数据
             * @param ack  是否成功发送到mq exchange
             * @param cause  原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if (!ack){
                    // TODO 可扩展自动重试
​
                    log.error("发送消息到mq失败  ,原因: {}",cause);
                }
            }
        });
        // 设置消息返还 回调方法
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             * @param message  消息内容
             * @param replyCode  回复状态
             * @param replyText  回复文本提示
             * @param exchange   交换机
             * @param routingKey   路由
             */
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                // TODO 可扩展自动重试
​
                log.error("消息返还回调触发  ,交换机: {} , 路由: {} , 消息内容: {} , 原因: {}  ",exchange,routingKey,message,replyText);
            }
        });
    }
}

heima-leadnews-common中定义常量类NewsAutoScanConstants

可以将所有消息队列常量存入com.heima.common.constants.message包中

public class NewsAutoScanConstants {
    public static final String WM_NEWS_AUTO_SCAN_QUEUE="wm.news.auto.scan.queue";
}

6.4)发送及消费消息

发送消息

wemedia-service 服务 WmNewsServiceImpl.submitNews

    /**
     * 自媒体文章发布
     * @param dto
     * @return
     */
    @Override
    public ResponseResult submitNews(WmNewsDTO dto) {
        // 1 参数校验
        ......略.....
        // 2 保存或修改文章
        ......略.....
        // 3.3 保存关联关系
            ......略.....
        // 3.4 发送待审核消息
            rabbitTemplate.convertAndSend(NewsAutoScanConstants.WM_NEWS_AUTO_SCAN_QUEUE,wmNews.getId());
            log.info("成功发送 待审核消息 ==> 队列:{}, 文章id:{}",NewsAutoScanConstants.WM_NEWS_AUTO_SCAN_TOPIC,wmNews.getId());
        }
        return ResponseResult.okResult();
    }

消费消息

package com.heima.wemedia.listen;
import com.heima.common.constants.message.NewsAutoScanConstants;
import com.heima.wemedia.service.WmNewsAutoScanService;
import com.heima.wemedia.service.WmNewsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@Slf4j
public class WemediaNewsAutoListener {
    @Autowired
    WmNewsAutoScanService wmNewsAutoScanService;
    @Autowired
    WmNewsService wmNewsService;
    /**
     * queues: 监听指定队列
     * queuesToDeclare: 声明并监听指定队列
     * bindings: 声明队列  交换机  并通过路由绑定
     */
    @RabbitListener(queuesToDeclare = {@Queue(name = NewsAutoScanConstants.WM_NEWS_AUTO_SCAN_QUEUE)})
    public void newsAutoScanHandler(String newsId){
        log.info("接收到 自动审核 消息===> {}",newsId);
        // 自动审核
        wmNewsAutoScanService.autoScanWmNews(Integer.valueOf(newsId));
    }
}

6.5)文章自动审核功能-综合测试

服务启动列表:

  1. wemedia微服务

  2. admin微服务

  3. wemedia网关微服务

  4. 启动前端系统wemedia

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

结果:

  1. 查看控制台日志是否触发自动审核,审核的具体结果文章来源地址https://www.toymoban.com/news/detail-458097.html

到了这里,关于java黑马头条 day5自媒体文章审核 敏感词过滤算法DFA 集成RabbitMQ实现自动审核的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 《黑马头条》 内容安全 自动审核 feign 延迟任务精准发布 kafka

    目录 《黑马头条》SpringBoot+SpringCloud+ Nacos等企业级微服务架构项目_黑马头条项目_软工菜鸡的博客-CSDN博客 04自媒体文章-自动审核 1)自媒体文章自动审核流程 2)内容安全第三方接口 2.1)概述 2.2)准备工作 2.3)文本内容审核接口 2.4)图片审核接口 2.5)项目集成 3)app端文章保存接口

    2024年02月15日
    浏览(34)
  • 06-kafka及异步通知文章上下架-黑马头条

    1)自媒体文章上下架 需求分析 2)kafka概述 消息中间件对比 特性 ActiveMQ RabbitMQ RocketMQ Kafka 开发语言 java erlang java scala 单机吞吐量 万级 万级 10万级 100万级 时效性 ms us ms ms级以内 可用性 高(主从) 高(主从) 非常高(分布式) 非常高(分布式) 功能特性 成熟的产品、较全的

    2024年04月11日
    浏览(36)
  • 【黑马头条之kafka及异步通知文章上下架】

    本笔记内容为黑马头条项目的kafka及异步通知文章上下架部分 目录 一、kafka概述 二、kafka安装配置 三、kafka入门 四、kafka高可用设计 1、集群 2、备份机制(Replication) 五、kafka生产者详解 1、发送类型 2、参数详解 六、kafka消费者详解 1、消费者组 2、消息有序性 3、提交和偏移

    2024年02月14日
    浏览(61)
  • 黑马头条-day02

    🌕博客x主页:己不由心王道长🌕! 🌎文章说明:黑马头条开发🌎 ✅系列专栏:微服务项目 🌴本篇内容:对黑马的黑马头条微服务进行开发讲解🌴 ☕️每日一语:这个世界本来就不完美,如果我们再不接受不完美的自己,那我们要怎么活。☕️ 🚩 交流社区: 己不由心王

    2024年02月10日
    浏览(34)
  • 黑马头条-day10

    1.1 ElasticSearch环境搭建 1、启动ElasticSearch docker start elasticsearch 2、启动Kibana docker start kibana 3、kibana测试分词效果 1.2 索引库创建 ①需求分析 用户输入 比如 java 只要文章titile、content包含此就可以搜索出来,搜索 黑马程序员 能把 黑马、程序员 相关都搜索出来 搜索

    2024年02月22日
    浏览(42)
  • 黑马头条---day1

    手机端查看 1、docker删除所有镜像命令 删除所有镜像的命令是Docker中一个非常常见的操作。下面是具体的实现步骤和命令示例: 这里的 docker ps -aq 和 docker images -aq 是一系列用于查找和选择容器和镜像的 Docker 命令。 2. docker删除镜像文件 如果使用 docker rmi 命令时突然出现“

    2024年02月05日
    浏览(38)
  • 黑马头条-day06-kafka

    思考:如果app端文章被投诉,自媒体下架处理后如何同步到APP端? 类似场景:文章点赞,喜欢,不喜欢,点击关注后权重会有变化,如何实时更新? feign远程调用是http请求,是同步调用,跨服务之间的同步变异步,要使用消息队列技术,不能使用线程池 。 kafka不仅支持发布

    2024年02月19日
    浏览(31)
  • 【黑马头条之app端文章搜索ES-MongoDB】

    本笔记内容为黑马头条项目的app端文章搜索部分 目录 一、今日内容介绍 1、App端搜索-效果图 2、今日内容 二、搭建ElasticSearch环境 1、拉取镜像 2、创建容器 3、配置中文分词器 ik 4、使用postman测试 三、app端文章搜索 1、需求分析 2、思路分析 3、创建索引和映射 4、数据初始化

    2024年02月08日
    浏览(35)
  • 《黑马头条》 ElectricSearch 分词器 联想词 MangoDB day08-平台管理[实战]作业

    1.1)App端搜索-效果图 1.2)今日内容 2.1) 拉取镜像 2.2) 创建容器 2.3) 配置中文分词器 ik 因为在 创建elasticsearch容器的时候,映射了目录,所以可以在宿主机上进行配置ik中文分词器 在去选择ik分词器的时候,需要与elasticsearch的版本好对应上 把资料中的elasticsearch-analysis-ik-7.4.0.zi

    2024年02月13日
    浏览(40)
  • 【Java Web】敏感词过滤

    一、前缀树 假设有敏感词:b,abc,abd,bcd,abcd,efg,hii 那么前缀树可以构造为: 二、敏感词过滤器

    2024年02月10日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包