7第七章 自媒体文章审核

# 第七章 自媒体文章审核

目标

  • 能够掌握自媒体文章审核的流程
  • 能够使用阿里云安全服务检测文章内容
  • 能够完成自媒体文章审核的功能
  • 能够完成自媒体发布文章与审核对接

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

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

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

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

1683464594861

1.当自媒体用户提交发布文章之后,会发消息给kafka提交审核,平台运营端接收文章信息

2.根据自媒体文章id查询文章信息

3.如果当前文章的状态为4(人工审核通过),则无需再进行自动审核审核,保存app文章相关数据即可

4.文章状态为8,发布时间小于等于当前时间,则直接保存app文章相关数据

5.文章状态为1,则进行自动审核

5.1 调用阿里云文本反垃圾服务,进行文本审核,如果审核不成功或需要人工审核,修改自媒体文章状态

5.2 调用阿里云图片审核服务,如果审核不通过或需要人工审核,修改自媒体文章状态

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

5.4 自媒体文章发布时间大于当前时间,修改自媒体文章状态为8(审核通过待发布状态)

5.5 审核通过,修改自媒体文章状态为 9 (审核通过)

6.保存app相关数据

ap_article_config 文章配置

ap_article 文章

ap_article_content 文章内容

ap_author 文章作者

7.创建索引(为后续app端的搜索功能做数据准备)

1.2 表结构

(1)wm_news 自媒体文章表 在自媒体库

1683464664131

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

对应实体类:(注意:这个实体之前已经创建过了,去找下,不用再次创建了)

package com.heima.model.media.pojos;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.apache.ibatis.type.Alias;

import java.io.Serializable;
import java.util.Date;

/**
 * <p>
 * 自媒体图文内容信息表
 * </p>
 *
 * @author itheima
 */
@Data
@TableName("wm_news")
public class WmNews implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 自媒体用户ID
     */
    @TableField("user_id")
    private Integer userId;

    /**
     * 标题
     */
    @TableField("title")
    private String title;

    /**
     * 图文内容
     */
    @TableField("content")
    private String content;

    /**
     * 文章布局
            0 无图文章
            1 单图文章
            3 多图文章
     */
    @TableField("type")
    private Short type;

    /**
     * 图文频道ID
     */
    @TableField("channel_id")
    private Integer channelId;

    @TableField("labels")
    private String labels;

    /**
     * 创建时间
     */
    @TableField("created_time")
    private Date createdTime;

    /**
     * 提交时间
     */
    @TableField("submited_time")
    private Date submitedTime;

    /**
     * 当前状态
            0 草稿
            1 提交(待审核)
            2 审核失败
            3 人工审核
            4 人工审核通过
            8 审核通过(待发布)
            9 已发布
     */
    @TableField("status")
    private Short status;

    /**
     * 定时发布时间,不定时则为空
     */
    @TableField("publish_time")
    private Date publishTime;

    /**
     * 拒绝理由
     */
    @TableField("reason")
    private String reason;

    /**
     * 发布库文章ID
     */
    @TableField("article_id")
    private Integer articleId;

    /**
     * //图片用逗号分隔
     */
    @TableField("images")
    private String images;

    @TableField("enable")
    private Short enable;
    
    //状态枚举类
    @Alias("WmNewsStatus")
    public enum Status{
        NORMAL((short)0),SUBMIT((short)1),FAIL((short)2),ADMIN_AUTH((short)3),ADMIN_SUCCESS((short)4),SUCCESS((short)8),PUBLISHED((short)9);
        short code;
        Status(short code){
            this.code = code;
        }
        public short getCode(){
            return this.code;
        }
    }

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

1683464716762

对应实体类:(注意:这个实体之前也已经创建过了,去找下,不用再次创建了)

package com.heima.model.article.pojos;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * <p>
 * APP文章作者信息表
 * </p>
 *
 * @author itheima
 */
@Data
@TableName("ap_author")
public class ApAuthor implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 作者名称
     */
    @TableField("name")
    private String name;

    /**
     * 0 爬取数据
            1 签约合作商
            2 平台自媒体人
            
     */
    @TableField("type")
    private Integer type;

    /**
     * 社交账号ID
     */
    @TableField("user_id")
    private Integer userId;

    /**
     * 创建时间
     */
    @TableField("created_time")
    private Date createdTime;

    /**
     * 自媒体账号
     */
    @TableField("wm_user_id")
    private Integer wmUserId;

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

1683464870279

对应实体:

package com.heima.model.article.pojos;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
 * <p>
 * APP已发布文章配置表
 * </p>
 *
 * @author itheima
 */

@Data
@TableName("ap_article_config")
public class ApArticleConfig {

    @TableId(value = "id",type = IdType.ID_WORKER)
    private Long id;

    /**
     * 文章id
     */
    @TableField("article_id")
    private Long articleId;

    /**
     * 是否可评论
     */
    @TableField("is_comment")
    private Boolean isComment;

    /**
     * 是否转发
     */
    @TableField("is_forward")
    private Boolean isForward;

    /**
     * 是否下架
     */
    @TableField("is_down")
    private Boolean isDown;

    /**
     * 是否已删除
     */
    @TableField("is_delete")
    private Boolean isDelete;
}
(4)ap_article_content 文章内容表 在article库

1683464954739

对应实体:

package com.heima.model.article.pojos;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("ap_article_content")
public class ApArticleContent {
    
    @TableId(value = "id",type = IdType.ID_WORKER)
    private Long id;

    /**
     * 文章id
     */
    @TableField("article_id")
    private Long articleId;

    /**
     * 文章内容
     */
    private String content;
}
(5)ap_article 文章信息表 在article库

注:此表和文章配置表、文章内容表都是一对一的(这俩表里面都有文章id)

1683464903944

  • layout 文章布局 0 无图文章 1 单图文章 2 多图文章
  • flag 文章标记 0 普通文章 1 热点文章 2 置顶文章 3 精品文章 4 大V 文章
  • images 文章图片 多张逗号分隔

对应实体

package com.heima.model.article.pojos;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.util.Date;

/**
 * <p>
 * 文章信息表,存储已发布的文章
 * </p>
 *
 * @author itheima
 */

@Data
@TableName("ap_article")
public class ApArticle {

    @TableId(value = "id",type = IdType.ID_WORKER)
    private Long id;


    /**
     * 标题
     */
    private String title;

    /**
     * 作者id
     */
    @TableField("author_id")
    private Long authorId;

    /**
     * 作者名称
     */
    @TableField("author_name")
    private String authorName;

    /**
     * 频道id
     */
    @TableField("channel_id")
    private Integer channelId;

    /**
     * 频道名称
     */
    @TableField("channel_name")
    private String channelName;

    /**
     * 文章布局  0 无图文章   1 单图文章    2 多图文章
     */
    private Short layout;

    /**
     * 文章标记  0 普通文章   1 热点文章   2 置顶文章   3 精品文章   4 大V 文章
     */
    private Byte flag;

    /**
     * 文章封面图片 多张逗号分隔
     */
    private String images;

    /**
     * 标签
     */
    private String labels;

    /**
     * 点赞数量
     */
    private Integer likes;

    /**
     * 收藏数量
     */
    private Integer collection;

    /**
     * 评论数量
     */
    private Integer comment;

    /**
     * 阅读数量
     */
    private Integer views;

    /**
     * 省市
     */
    @TableField("province_id")
    private Integer provinceId;

    /**
     * 市区
     */
    @TableField("city_id")
    private Integer cityId;

    /**
     * 区县
     */
    @TableField("county_id")
    private Integer countyId;

    /**
     * 创建时间
     */
    @TableField("created_time")
    private Date createdTime;

    /**
     * 发布时间
     */
    @TableField("publish_time")
    private Date publishTime;

    /**
     * 同步状态
     */
    @TableField("sync_status")
    private Boolean syncStatus;

    /**
     * 来源
     */
    private Boolean origin;

}

2 文章审核功能实现

2.1 文章审核功能-准备工作

2.1.1 自媒体feign接口

(1)需求说明和feign接口定义

因为根据上面的时序图可见,admin端需要调用远程的自媒体端和app文章端来获取数据

在自动审核的时候需要自媒体的远程接口,如下:

1 根据文章id查询自媒体文章的数据

2 在审核的过程中,审核失败或者成功需要修改自媒体文章的状态

3 在文章进行保存的时候需要查询作者信息,需要通过自媒体用户关联查询作者信息

在admin端添加依赖

<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

开启远程调用,在admin模块的启动引导类中新增注解@EnableFeignClients

在admin端需要定义远程接口:

package com.heima.admin.feign;

import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.wemedia.pojos.WmNews;
import com.heima.model.wemedia.pojos.WmUser;
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;

@FeignClient("leadnews-wemedia")
public interface WemediaFeign {

    @GetMapping("/api/v1/news/findOne/{id}")
    WmNews findById(@PathVariable("id") Integer id);

    @PostMapping("/api/v1/news/update")
    ResponseResult updateWmNews(WmNews wmNews);

    @GetMapping("/api/v1/user/findOne/{id}")
    WmUser findWmUserById(@PathVariable("id") Long id);
}

(2)自媒体文章接口准备

  1. 在自媒体端的WmNewsControllerApi接口中新增方法
/**
* 根据id查询文章
* @param id
* @return
*/
WmNews findById(Integer id);

/**
* 修改文章
* @param wmNews
* @return
*/
ResponseResult updateWmNews(WmNews wmNews);
  1. 在自媒体端的WmNewsController中实现这两个方法,并处理
    @GetMapping("/findOne/{id}")
    @Override
    public WmNews findById(@PathVariable("id") Integer id) {
        return wmNewsService.getById(id);
    }

    @PostMapping("/update")
    @Override
    public ResponseResult updateWmNews(@RequestBody WmNews wmNews) {
        wmNewsService.updateById(wmNews);
        return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
    }

(3)自媒体用户接口准备

  1. 在自媒体端的WmUserControllerApi接口中新增方法
 /**
 * 根据id查询自媒体用户
 * @param id
 * @return
 */
 WmUser findWmUserById(Long id);
  1. 在自媒体端的WmUserController中实现这个方法,并处理
 @GetMapping("/findOne/{id}")
 @Override
 public WmUser findWmUserById(@PathVariable("id") Long id) {
     return userService.getById(id);
 }

2.1.2 文章相关feign接口

(1)分布式id

随着业务的增长,文章表可能要占用很大的物理存储空间,为了解决该问题,后期使用数据库分片技术。将一个数据库进行拆分,通过数据库中间件连接。如果数据库中该表选用ID自增策略,则可能产生重复的ID,此时应该使用分布式ID生成策略来生成ID。

雪花算法实现

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

1683471026053

mybatis-plus已经集成了雪花算法,完成以下两步即可在项目中集成雪花算法

第一:在实体类中的id上加入如下配置,指定类型为id_worker

@TableId(value = "id",type = IdType.ID_WORKER)
private Long id;

第二:在application.yml文件中配置数据中心id和机器id

mybatis-plus:
  mapper-locations: classpath*:mapper/*.xml
  # 设置别名包扫描路径,通过该属性可以给包中的类注册别名
  type-aliases-package: com.heima.model.article.pojos
  global-config:
  	datacenter-id: 1
    workerId: 1  

datacenter-id:数据中心id(取值范围:0-31)

workerId:机器id(取值范围:0-31)

(2)需求说明

在文章审核成功以后需要在app的article库中新增文章数据

1 保存文章信息 ap_article,需要返回当前文章,并且需要获取保存后获取到的主键

2 保存文章配置信息 ap_article_config

3 保存文章内容 ap_article_content

4 在保存文章的时候需要关联作者,需要根据名称查询作者信息

在admin端定义远程接口:

package com.heima.admin.feign;

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

@FeignClient("leadnews-article")
public interface ArticleFeign {

    @PostMapping("/api/v1/article_config/save")
    ResponseResult saveArticleConfig(ApArticleConfig apArticleConfig);

    @GetMapping("/api/v1/author/findByName/{name}")
    ApAuthor selectAuthorByName(@PathVariable("name") String name);

    @PostMapping("/api/v1/article/save")
    ApArticle saveArticle(ApArticle apArticle);

    @PostMapping("/api/v1/article_content/save")
    ResponseResult saveArticleContent(ApArticleContent apArticleContent);
}

(3)文章微服务准备ap_article接口

  1. 接口定义

在apis服务中新增接口

package com.heima.apis.article;

import com.heima.model.article.pojos.ApArticle;

public interface ApArticleControllerApi {
    
    /**
    * 保存app文章
    * @param apArticle
    * @return
    */
   ApArticle saveArticle(ApArticle apArticle);

}
  1. mapper定义
package com.heima.article.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.heima.model.article.pojos.ApArticle;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ApArticleMapper extends BaseMapper {
}
  1. 业务层

业务层接口

package com.heima.article.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.heima.model.article.pojos.ApArticle;

public interface ApArticleService extends IService {
}

实现类:

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.model.article.pojos.ApArticle;
import org.springframework.stereotype.Service;

@Service
public class ApArticleServiceImpl extends ServiceImpl<ApArticleMapper, ApArticle> implements ApArticleService {
}
  1. 控制层
package com.heima.article.controller.v1;

import com.heima.apis.article.ApArticleControllerApi;
import com.heima.article.service.ApArticleService;
import com.heima.model.article.pojos.ApArticle;
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 ApArticleController implements ApArticleControllerApi {


   @Autowired
   private ApArticleService articleService;

   @PostMapping("save")
   @Override
   public ApArticle saveArticle(@RequestBody ApArticle apArticle) {
       articleService.save(apArticle);
       return apArticle;
   }


}

(4)文章微服务准备ap_article_config接口

  1. 接口定义

在apis服务中新增接口

package com.heima.apis.article;

import com.heima.model.article.pojos.ApArticleConfig;
import com.heima.model.common.dtos.ResponseResult;

public interface ApArticleConfigControllerApi {

   /**
    * 保存app端文章配置
    * @param apArticleConfig
    * @return
    */
   ResponseResult saveArticleConfig(ApArticleConfig apArticleConfig);


}
  1. mapper定义
package com.heima.article.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.heima.model.article.pojos.ApArticleConfig;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ApArticleConfigMapper extends BaseMapper {
}
  1. 业务层

业务层接口:

package com.heima.article.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.heima.model.article.pojos.ApArticleConfig;

public interface ApArticleConfigService extends IService {
}

实现类:

package com.heima.article.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.article.mapper.ApArticleConfigMapper;
import com.heima.article.service.ApArticleConfigService;
import com.heima.model.article.pojos.ApArticleConfig;
import org.springframework.stereotype.Service;

@Service
public class ApArticleConfigServiceImpl extends ServiceImpl<ApArticleConfigMapper, ApArticleConfig> implements ApArticleConfigService {
}
  1. 控制层
package com.heima.article.controller.v1;

import com.heima.apis.article.ApArticleConfigControllerApi;
import com.heima.article.service.ApArticleConfigService;
import com.heima.model.article.pojos.ApArticleConfig;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
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_config")
public class ApArticleConfigController implements ApArticleConfigControllerApi {

   @Autowired
   private ApArticleConfigService apArticleConfigService;

   @PostMapping("/save")
   @Override
   public ResponseResult saveArticleConfig(@RequestBody ApArticleConfig apArticleConfig) {
       apArticleConfigService.save(apArticleConfig);
       return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
   }
}

(5)文章微服务准备ap_article_content接口

  1. 接口定义

在apis服务中新增接口

package com.heima.apis.article;

import com.heima.model.article.pojos.ApArticleContent;
import com.heima.model.common.dtos.ResponseResult;

public interface ApArticleContentControllerApi {

   /**
    * 保存app端文章内容
    * @param apArticleContent
    * @return
    */
   ResponseResult saveArticleContent(ApArticleContent apArticleContent);

}
  1. mapper定义
package com.heima.article.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.heima.model.article.pojos.ApArticleContent;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ApArticleContentMapper extends BaseMapper {
}
  1. 业务层

业务层接口:

package com.heima.article.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.heima.model.article.pojos.ApArticleContent;

public interface ApArticleContentService extends IService {
}

实现类:

package com.heima.article.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.article.mapper.ApArticleContentMapper;
import com.heima.article.service.ApArticleContentService;
import com.heima.model.article.pojos.ApArticleContent;
import org.springframework.stereotype.Service;

@Service
public class ApArticleContentServiceImpl extends ServiceImpl<ApArticleContentMapper,ApArticleContent> implements ApArticleContentService {

}
  1. 控制层
package com.heima.article.controller.v1;

import com.heima.apis.article.ApArticleContentControllerApi;
import com.heima.article.service.ApArticleContentService;
import com.heima.model.article.pojos.ApArticleContent;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
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_content")
public class ApArticleContentController implements ApArticleContentControllerApi {

   @Autowired
   private ApArticleContentService apArticleContentService;

   @PostMapping("/save")
   @Override
   public ResponseResult saveArticleContent(@RequestBody ApArticleContent apArticleContent) {
       apArticleContentService.save(apArticleContent);
       return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
   }


}

(6)文章微服务准备ap_author接口

  1. 接口定义

在AuthorControllerApi中新增方法

    /**
     * 根据名称查询作者
     * @param name
     * @return
     */
    public ApAuthor findByName(@PathVariable("id") String name);

控制层AuthorController新增方法

 @GetMapping("/findByName/{name}")
 @Override
 public ApAuthor findByName(@PathVariable("name") String name) {
       ApAuthor apAuthor = authorService.getOne(Wrappers.<ApAuthor>lambdaQuery().eq(ApAuthor::getName, name));
       return apAuthor;
 }

2.1.3 增加配置引入阿里云服务和fastdfs

在admin微服务中加入如下配置

文章审核需要调用阿里云服务的云安全服务来审核文章和图片

package com.heima.admin.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.heima.common.aliyun")
public class AliyunConfig {
}

fastdfs服务引入

在阿里云图片审核中需要将图片先从fastdfs中下载下来再去审核才可以,如果当前fastdfs是一个外网可以访问的则无须再次下载,可以直接审核

package com.heima.admin.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.heima.common.fastdfs")
public class FastDfsConfig {
}

2.1.4查询所有敏感词

这里查询所有敏感词的作用是,在去审核文本内容的时候,从两方面入手,一个是调用阿里云第三方接口审核内容,另外一个是系统自己审核,使用自己维护的敏感词库去匹配发布文章的内容。

修改AdSensitiveMapper,新增方法:findAllSensitive

@Mapper
public interface AdSensitiveMapper extends BaseMapper<AdSensitive> {

    public List<String> findAllSensitive();
}

新增配置文件resources/mapper/AdSensitiveMapper.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.admin.mapper.AdSensitiveMapper">
    <select id="findAllSensitive" resultType="string">
        select sensitives from ad_sensitive
    </select>
</mapper>

2.2 文章审核功能-业务层接口定义

新建自动审核接口:

package com.heima.admin.service;

public interface WemediaNewsAutoScanService {

    /**
     * 自媒体文章审核
     * @param id
     */
    public void autoScanByMediaNewsId(Integer id);
}

2.3 文章审核功能-业务逻辑实现

自动审核实现类:

package com.heima.admin.service.impl;

import com.alibaba.fastjson.JSONArray;
import com.heima.admin.feign.ArticleFeign;
import com.heima.admin.feign.WemediaFeign;
import com.heima.admin.mapper.AdChannelMapper;
import com.heima.admin.mapper.AdSensitiveMapper;
import com.heima.admin.service.WemediaNewsAutoScanService;
import com.heima.common.aliyun.GreeTextScan;
import com.heima.common.aliyun.GreenImageScan;
import com.heima.common.fastdfs.FastDFSClient;
import com.heima.model.admin.pojos.AdChannel;
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.wemedia.pojos.WmNews;
import com.heima.model.wemedia.pojos.WmUser;
import com.heima.utils.common.SensitiveWordUtil;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.*;

@Service
@Log4j2
public class WemediaNewsAutoScanServiceImpl implements WemediaNewsAutoScanService {

    @Autowired
    private WemediaFeign wemediaFeign;


    @GlobalTransactional
    @Override
    public void autoScanByMediaNewsId(Integer id) {
        if (id == null) {
            log.error("当前的审核id空");
            return;
        }
        //1.根据id查询自媒体文章信息
        WmNews wmNews = wemediaFeign.findById(id);
        if (wmNews == null) {
            log.error("审核的自媒体文章不存在,自媒体的id:{}", id);
            return;
        }

        //2.文章状态为4(人工审核通过)直接保存数据和创建索引
        if (wmNews.getStatus() == 4) {
            //保存数据
            saveAppArticle(wmNews);
            return;
        }

        //3.文章状态为8  发布时间小于等于当前时间 直接保存数据
        if (wmNews.getStatus() == 8 && wmNews.getPublishTime().getTime() <= System.currentTimeMillis()) {
            //保存数据
            saveAppArticle(wmNews);
            return;
        }

        //4.文章状态为1,待审核
        if (wmNews.getStatus() == 1) {
            //抽取文章内容中的纯文本和图片
            Map<String, Object> contentAndImagesResult = handleTextAndImages(wmNews);
            //4.1 文本审核
            boolean textScanBoolean = handleTextScan((String) contentAndImagesResult.get("content"), wmNews);
            if (!textScanBoolean) return;
            //4.2 图片审核
            boolean imagesScanBoolean = handleImagesScan((List<String>) contentAndImagesResult.get("images"), wmNews);
            if (!imagesScanBoolean) return;
            //4.3 自管理的敏感词审核
            boolean sensitiveScanBoolean = handleSensitive((String) contentAndImagesResult.get("content"), wmNews);
            if (!sensitiveScanBoolean) return;
            //4.4 发布时间大于当前时间,
            if (wmNews.getPublishTime().getTime() > System.currentTimeMillis()) {
                //修改文章状态为8
                updateWmNews(wmNews, (short) 8, "审核通过,待发布");
                return;
            }
            //5.审核通过,修改自媒体文章状态为9  保存app端相关文章信息
            saveAppArticle(wmNews);

        }


    }

    @Autowired
    private AdSensitiveMapper adSensitiveMapper;

    /**
     * 敏感词审核
     *
     * @param content
     * @param wmNews
     * @return
     */
    private boolean handleSensitive(String content, WmNews wmNews) {

        boolean flag = true;

        List<String> allSensitive = adSensitiveMapper.findAllSensitive();
        //初始化敏感词
        SensitiveWordUtil.initMap(allSensitive);
        //文章内容自管理敏感词过滤
        Map<String, Integer> resultMap = SensitiveWordUtil.matchWords(content);
        if (resultMap.size() > 0) {
            log.error("敏感词过滤没有通过,包含了敏感词:{}", resultMap);
            //找到了敏感词,审核不通过
            updateWmNews(wmNews, (short) 2, "文章中包含了敏感词");
            flag = false;
        }

        return flag;
    }

    @Autowired
    private GreenImageScan greenImageScan;

    @Autowired
    private FastDFSClient fastDFSClient;

    @Value("${fdfs.url}")
    private String fileServerUrl;

    /**
     * 审核图片
     *
     * @param images
     * @param wmNews
     * @return
     */
    private boolean handleImagesScan(List<String> images, WmNews wmNews) {
        if (images == null) {
            return true;
        }

        boolean flag = true;

        List<byte[]> imageList = new ArrayList<>();

        try {
            for (String image : images) {
                String imageName = image.replace(fileServerUrl, "");
                int index = imageName.indexOf("/");
                String groupName = imageName.substring(0, index);
                String imagePath = imageName.substring(index + 1);
                byte[] imageByte = fastDFSClient.download(groupName, imagePath);
                imageList.add(imageByte);
            }
            //阿里云图片审核
            Map map = greenImageScan.imageScan(imageList);
            //审核不通过
            if (!map.get("suggestion").equals("pass")) {
                //审核失败
                if (map.get("suggestion").equals("block")) {
                    //修改自媒体文章的状态,并告知审核失败原因
                    updateWmNews(wmNews, (short) 2, "文章中图片有违规");
                    flag = false;
                }

                //人工审核
                if (map.get("suggestion").equals("review")) {
                    //修改自媒体文章的状态,并告知审核失败原因
                    updateWmNews(wmNews, (short) 3, "文章图片有不确定元素");
                    flag = false;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            flag = false;
        }
        return flag;

    }


    @Autowired
    private GreeTextScan greeTextScan;

    /**
     * 文本审核
     *
     * @param content
     * @param wmNews
     * @return
     */
    private boolean handleTextScan(String content, WmNews wmNews) {
        boolean flag = true;
        try {
            Map map = greeTextScan.greeTextScan(content);
            //审核不通过
            if (!map.get("suggestion").equals("pass")) {
                //审核失败
                if (map.get("suggestion").equals("block")) {
                    //修改自媒体文章的状态,并告知审核失败原因
                    updateWmNews(wmNews, (short) 2, "文章内容中有敏感词汇");
                    flag = false;
                }

                //人工审核
                if (map.get("suggestion").equals("review")) {
                    //修改自媒体文章的状态,并告知审核失败原因
                    updateWmNews(wmNews, (short) 3, "文章内容中有不确定词汇");
                    flag = false;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            flag = false;
        }

        return flag;
    }

    /**
     * 修改自媒体文章
     *
     * @param wmNews
     * @param status
     * @param msg
     */
    private void updateWmNews(WmNews wmNews, short status, String msg) {
        wmNews.setStatus(status);
        wmNews.setReason(msg);
        wemediaFeign.updateWmNews(wmNews);
    }

    /**
     * 提取文本内容和图片
     *
     * @param wmNews
     * @return
     */
    private Map<String, Object> handleTextAndImages(WmNews wmNews) {

        //文章的内容
        String content = wmNews.getContent();

        //存储纯文本内容
        StringBuilder sb = new StringBuilder();
        //存储图片
        List<String> images = new ArrayList<>();

        List<Map> contentList = JSONArray.parseArray(content, Map.class);
        for (Map map : contentList) {
            if (map.get("type").equals("text")) {
                sb.append(map.get("value"));
            }

            if (map.get("type").equals("image")) {
                images.add((String) map.get("value"));
            }
        }

        if (wmNews.getImages() != null && wmNews.getType() != 0) {
            String[] split = wmNews.getImages().split(",");
            images.addAll(Arrays.asList(split));
        }

        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("content", sb.toString());
        resultMap.put("images", images);
        return resultMap;

    }

    @Autowired
    ArticleFeign articleFeign;

    /**
     * 保存app文章相关的数据
     *
     * @param wmNews
     */
    private void saveAppArticle(WmNews wmNews) {
        //保存app文章
        ApArticle apArticle = saveArticle(wmNews);
        //保存app文章配置
        saveArticleConfig(apArticle);
        //保存app文章内容
        saveArticleContent(apArticle,wmNews);

        //修改自媒体文章的状态为9
        wmNews.setArticleId(apArticle.getId());
        updateWmNews(wmNews,(short)9,"审核通过");

        //TODO es索引创建

    }

    /**
     * 创建app端文章内容信息
     * @param apArticle
     * @param wmNews
     */
    private void saveArticleContent(ApArticle apArticle, WmNews wmNews) {
        ApArticleContent apArticleContent = new ApArticleContent();
        apArticleContent.setArticleId(apArticle.getId());
        apArticleContent.setContent(wmNews.getContent());
        articleFeign.saveArticleContent(apArticleContent);
    }

    /**
     * 创建app端文章配置信息
     * @param apArticle
     */
    private void saveArticleConfig(ApArticle apArticle) {
        ApArticleConfig apArticleConfig = new ApArticleConfig();
        apArticleConfig.setArticleId(apArticle.getId());
        apArticleConfig.setIsForward(true);
        apArticleConfig.setIsDelete(false);
        apArticleConfig.setIsDown(true);
        apArticleConfig.setIsComment(true);

        articleFeign.saveArticleConfig(apArticleConfig);
    }

    @Autowired
    AdChannelMapper adChannelMapper;

    /**
     * 保存文章
     * @param wmNews
     * @return
     */
    private ApArticle saveArticle(WmNews wmNews) {
        ApArticle apArticle = new ApArticle();
        apArticle.setTitle(wmNews.getTitle());
        apArticle.setLayout(wmNews.getType());
        apArticle.setImages(wmNews.getImages());
        apArticle.setCreatedTime(new Date());

        //获取作者相关信息
        Integer wmUserId = wmNews.getUserId();
        WmUser wmUser = wemediaFeign.findWmUserById(wmUserId);
        if(wmUser != null){
            String wmUserName = wmUser.getName();
            ApAuthor apAuthor = articleFeign.selectAuthorByName(wmUserName);
            if(apAuthor != null){
                apArticle.setAuthorId(apAuthor.getId().longValue());
                apArticle.setAuthorName(apAuthor.getName());
            }

        }

        //获取频道相关信息
        Integer channelId = wmNews.getChannelId();
        AdChannel channel = adChannelMapper.selectById(channelId);
        if(channel != null){
            apArticle.setChannelId(channel.getId());
            apArticle.setChannelName(channel.getName());
        }

        return articleFeign.saveAparticle(apArticle);
    }

}

发现bug,修改

修改WmNews实体类中的字段articleId的类型为Long

1683550106396

2.4 文章审核功能-单元测试

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(6219);
    }
}

3 文章审核功能-发布文章提交审核&定义监听接收消息

在审核文章流程的第一步,当自媒体人发布一篇文章后会马上进行审核,这个时候是通过消息中间件进行数据的传递的。所以说需要配置生产者和消费者。目前自媒体微服务就是生产者,admin就是消费者

3.1 配置kafka环境

在leadnews-common模块中添加依赖(以前的common组件pom文件里面已经添加了,无需再添加)

<!-- kafkfa -->
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
</dependency>

3.2 配置生产者

在leadnews-wemedia模块中的application.yml加入kafka的配置

spring:
  application:
    name: leadnews-wemedia
  kafka:
    bootstrap-servers: 192.168.200.130:9092
    producer:
      retries: 10
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer

在发布文章修改代码,发送消息,提交审核

定义topic名称

新建常量类:

package com.heima.common.constants.message;

public class NewsAutoScanConstants {

    public static final String WM_NEWS_AUTO_SCAN_TOPIC="wm.news.auto.scan.topic";
}

在heima-leadnews-wemedia微服务中修改业务层实现类WmNewsServiceImpl的saveWmNews方法,修改如下代码,发送消息进行文章审核

@Autowired
private KafkaTemplate kafkaTemplate;

/**
     * 保存或修改文章
     *
     * @param wmNews
     * @param isSubmit
     */
private void saveWmNews(WmNews wmNews, Short isSubmit) {
    wmNews.setStatus(isSubmit);
    wmNews.setUserId(WmThreadLocalUtils.getUser().getId());
    wmNews.setCreatedTime(new Date());
    wmNews.setSubmitedTime(new Date());
    wmNews.setEnable((short) 1);
    boolean flag = false;
    if (wmNews.getId() == null) {
        flag = save(wmNews);
    } else {
        //如果是修改,则先删除素材与文章的关系
        LambdaQueryWrapper<WmNewsMaterial> queryWrapper = new LambdaQueryWrapper();
        queryWrapper.eq(WmNewsMaterial::getNewsId, wmNews.getId());
        wmNewsMaterialMapper.delete(queryWrapper);
        flag = updateById(wmNews);
    }
    //发送消息
    if(flag){
        kafkaTemplate.send(NewsAutoScanConstants.WM_NEWS_AUTO_SCAN_TOPIC,JSON.toJSONString(wmNews.getId()));
    }
}

3.3 配置消费者

(1)在heima-leadnews-admin微服务中修改application.yml文件,增加kafka相关配置

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

(2)自媒体文章发布以后会发消息过来自动进行审核,需要在admin端来接收消息,处理审核

package com.heima.admin.kafka.listener;

import com.heima.admin.service.WemediaNewsAutoScanService;
import com.heima.common.constans.message.NewsAutoScanConstants;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
public class WemediaNewsAutoListener {

    @Autowired
    WemediaNewsAutoScanService wemediaNewsAutoScanService;

    @KafkaListener(topics = NewsAutoScanConstants.WM_NEWS_AUTO_SCAN_TOPIC)
    public void recevieMessage(ConsumerRecord<?,?> record){
        Optional<? extends ConsumerRecord<?, ?>> optional = Optional.ofNullable(record);
        if(optional.isPresent()){
            Object value = record.value();
            wemediaNewsAutoScanService.autoScanByMediaNewsId(Integer.valueOf((String) value));
        }

    }
}

4 文章审核功能-解决分布式事务

目前项目中已经全部集成了seata,没有集成的需要按照之前(wemedia模块中集成seata)的步骤在项目中配置,然后在对应的业务方法上进行注解控制即可。

在admin组件下的pom文件中添加seata依赖

    <dependency>
        <groupId>com.heima</groupId>
        <artifactId>heima-leadnews-seata</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>

参考之前wemedia组件的配置,将file.conf和registry.conf文件复制到admin组件下的resources文件夹下

1683559166757

然后修改file.conf

1683559263245

再添加如下yml配置

1683559368463

再添加seata配置类

package com.heima.admin.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.heima.seata.config")
public class SeataConfig {

}

在WeMediaNewsAutoScanServiceImpl的autoScanByMediaNewsId方法加上注解@GlobalTransactional

1683558928564

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

服务启动列表:

1,nacos

2,seata

3,fastdfs

4,zookeeper&kafka

5,article微服务

6,wemedia微服务

7,启动wemedia网关微服务

8,admin微服务

9,启动前端系统wemedia

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

结果:

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

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

posted @ 2023-06-29 23:17  起跑线小言  阅读(155)  评论(0编辑  收藏  举报