7第七章 自媒体文章审核
# 第七章 自媒体文章审核
目标
- 能够掌握自媒体文章审核的流程
- 能够使用阿里云安全服务检测文章内容
- 能够完成自媒体文章审核的功能
- 能够完成自媒体发布文章与审核对接
1 自媒体文章自动审核需求说明
1.1 自媒体文章自动审核流程
做为内容类产品,内容安全非常重要,所以需要进行对自媒体用户发布的文章进行审核以后才能到app端展示给用户。
审核的流程如下:也可以查看当前讲义文件夹下:自媒体文章发布时序图.pdf
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 自媒体文章表 在自媒体库
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库
对应实体类:(注意:这个实体之前也已经创建过了,去找下,不用再次创建了)
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库
对应实体:
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库
对应实体:
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)
- 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)自媒体文章接口准备
- 在自媒体端的WmNewsControllerApi接口中新增方法
/**
* 根据id查询文章
* @param id
* @return
*/
WmNews findById(Integer id);
/**
* 修改文章
* @param wmNews
* @return
*/
ResponseResult updateWmNews(WmNews wmNews);
- 在自媒体端的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)自媒体用户接口准备
- 在自媒体端的WmUserControllerApi接口中新增方法
/**
* 根据id查询自媒体用户
* @param id
* @return
*/
WmUser findWmUserById(Long id);
- 在自媒体端的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
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接口
- 接口定义
在apis服务中新增接口
package com.heima.apis.article;
import com.heima.model.article.pojos.ApArticle;
public interface ApArticleControllerApi {
/**
* 保存app文章
* @param apArticle
* @return
*/
ApArticle saveArticle(ApArticle apArticle);
}
- 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 {
}
- 业务层
业务层接口
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 {
}
- 控制层
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接口
- 接口定义
在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);
}
- 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 {
}
- 业务层
业务层接口:
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 {
}
- 控制层
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接口
- 接口定义
在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);
}
- 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 {
}
- 业务层
业务层接口:
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 {
}
- 控制层
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接口
- 接口定义
在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
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文件夹下
然后修改file.conf
再添加如下yml配置
再添加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
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表中的状态是否改变,成功和失败原因正常插入