目录
一.审核
1.流程
2.内容安全第三方接口
3.百度AI审核
3.1首先注册百度云账号,然后完成个人信息认证
3.2开启文本审核和图像审核功能
3.3添加sdk依赖
3.4构建测试类
3.5文本审核
3.6图片审核
3.7对百度云审核的封装
3.7.1在自媒体服务的config目录下面配置
3.7.2自己封装MyBaiduCensor;
3.7.3在配置文件中配置百度审核的key
二.分布式id
三.审核功能
1.feign接口
2.功能实现:
3.正式审核
3.1feign远程接口调用
四.服务降级
五.集成自动审核功能
5.2)Springboot集成异步线程调用
文章审核功能-综合测试
6.2)测试情况列表
六.DFA自管理敏感词过滤
7.3)DFA实现原理
7.4)自管理敏感词集成到文章审核中
七.图像文字识别审核
7.2)图片文字识别
7.3在自媒体服务中引入依赖
7.4工具类
在heima-leadnews-wemedia中的配置中添加两个属性
八.异步上传静态html文件
测试
一.审核
1.流程
1 自媒体端发布文章后,开始审核文章
2 审核的主要是审核文章的内容(文本内容和图片)
3 借助第三方提供的接口审核文本
4 借助第三方提供的接口审核图片,由于图片存储到minIO中,需要先下载才能审核
5 如果审核失败,则需要修改自媒体文章的状态,status:2 审核失败 status:3 转到人工审核
6 如果审核成功,则需要在文章微服务中创建app端需要的文章
2.内容安全第三方接口
内容安全是识别服务,支持对图片、视频、文本、语音等对象进行多样化场景检测,有效降低内容违规风险。
目前很多平台都支持内容检测,如阿里云、腾讯云、百度AI、网易云等国内大型互联网公司都对外提供了API。
按照性能和收费来看,黑马头条项目使用的就是阿里云的内容安全接口,使用到了图片和文本的审核。
注意:因为阿里云需要企业认证,而且是收费的,所以我们吧项目中的第三方审核换成了百度云AI审核,只需要个人认证,就可以免费使用审核功能一年
3.百度AI审核
3.1首先注册百度云账号,然后完成个人信息认证
3.2开启文本审核和图像审核功能
然后会生成对应的accessKey和sercetKey,:
3.3添加sdk依赖
<!-- 引入百度审核-->
<dependency>
<groupId>com.baidu.aip</groupId>
<artifactId>java-sdk</artifactId>
<version>4.16.14</version>
</dependency>
3.4构建测试类
public class Sample {
//设置APPID/AK/SK
public static final String APP_ID = "xxxx";
public static final String API_KEY = "xxxxxxx";
public static final String SECRET_KEY = "xxxxxx";
public static void main(String[] args) throws IOException {
// 初始化一个AipContentCensor
AipContentCensor client = new AipContentCensor(APP_ID, API_KEY, SECRET_KEY);
String path = "https://img.aosikaimge.com/20230704/GkgUyt5t/1.jpg";
JSONObject jsonObject = client.imageCensorUserDefined(path, EImgType.URL, null);
System.out.println(jsonObject);
}
}
APIKEY和SECRET_KEY都是自己的key,配置好,然后很简单,只需要创建一个AipContentCensor
然后调用方法即可,我这里是在线审核一个图片url,点击运行,看一下返回的内容:
{
"conclusion":"不合规",
"log_id":17029444908898168,
"data":[
{ "msg":"存在一般色情不合规",
"conclusion":"不合规",
"probability":0.99995315,
"subType":0,
"conclusionType":2,
"type":1
}
],
"isHitMd5":false,
"conclusionType":2
}
很明显,该图片非法, 返回结果中有一些参数,具体的可以去百度云文档查看,我在这里列举一些基本的参数信息:
3.5文本审核
参数名称 | 数据类型 | 是否必须 | 备注 |
---|---|---|---|
log_id | Long | Y | 请求唯一id |
error_code | Long | N | 错误提示码,失败才返回,成功不返回 |
error_msg | String | N | 错误提示信息,失败才返回,成功不返回 |
conclusion | String | N | 审核结果,可取值:合规、不合规、疑似、审核失败 |
conclusionType | Integer | N | 审核结果类型,可取值1.合规,2.不合规,3.疑似,4.审核失败 |
data | Array | N | 不合规/疑似/命中白名单项详细信息。响应成功并且conclusion为疑似或不合规或命中白名单时才返回,响应失败或conclusion为合规且未命中白名单时不返回。 |
+error_code | Integer | 否 | 内层错误提示码,底层服务失败才返回,成功不返回 |
+error_msg | String | 否 | 内层错误提示信息,底层服务失败才返回,成功不返回 |
+type | Integer | N | 审核主类型,11:百度官方违禁词库、12:文本反作弊、13:自定义文本黑名单、14:自定义文本白名单 |
+subType | Integer | N | 审核子类型,此字段需参照type主类型字段决定其含义: 当type=11时subType取值含义: 0:百度官方默认违禁词库 当type=12时subType取值含义: 0:低质灌水、2:文本色情、4:恶意推广、5:低俗辱骂、7:隐私信息 当type=13时subType取值含义: 0:自定义文本黑名单 当type=14时subType取值含义: 0:自定义文本白名单 *完整版subType说明您可联系商务经理或填写咨询表单进行获取 |
3.6图片审核
参数名称 | 数据类型 | 备注 |
---|---|---|
log_id | Long | 请求唯一id,用于问题排查 |
error_code | Long | 错误提示码,失败才返回,成功不返回 |
error_msg | String | 错误提示信息,失败才返回,成功不返回 |
conclusion | String | 审核结果,可取值描述:合规、不合规、疑似、审核失败 |
conclusionType | Integer | 审核结果类型,可取值1、2、3、4,分别代表1:合规,2:不合规,3:疑似,4:审核失败 |
data | Array | 不合规/疑似/命中白名单项详细信息。响应成功并且conclusion为疑似或不合规或命中白名单时才返回,响应失败或conclusion为合规且未命中白名单时不返回。 |
其他的内容需要你自己去查看:
3.7对百度云审核的封装
3.7.1在自媒体服务的config目录下面配置
/**
* 百度云Ai审核
*/
@Configuration
//@ConfigurationProperties(prefix = "baidu")
public class BaiduConfig {
@Value("${baidu.appId}")
private String appId;
@Value("${baidu.apiKey}")
private String apiKey;
@Value("${baidu.secretKey}")
private String secretKey;
@Bean
public AipContentCensor aipContentCensor(){
AipContentCensor aipContentCensor = new AipContentCensor(appId, apiKey, secretKey);
// 可选:设置网络连接参数
// aipContentCensor.setConnectionTimeoutInMillis(2000);
// aipContentCensor.setSocketTimeoutInMillis(60000);
return aipContentCensor;
}
}
3.7.2自己封装MyBaiduCensor;
package com.heima.wemedia.baidu;
import com.baidu.aip.contentcensor.AipContentCensor;
import com.baidu.aip.contentcensor.EImgType;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.annotation.ElementType;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 自定义的百度审核
*/
@Component
public class MyBaiduCensor {
@Autowired
private AipContentCensor aipContentCensor;
/**
* 自定义审核纯文本内容
* @param text
* @return
*/
public String scanText(String text){
JSONObject json = aipContentCensor.textCensorUserDefined(text);
return (String) json.get("conclusion");
}
/**
* 审核图片
* 根据图片的在线url
*/
public String scanImage(String url){
JSONObject result = aipContentCensor.imageCensorUserDefined(url, EImgType.URL, null);
return (String) result.get("conclusion");
}
/**
* 字节数组审核图片
* @param image
* @return
*/
public String scanImage(byte[] image){
JSONObject jsonObject = aipContentCensor.imageCensorUserDefined(image, null);
return (String) jsonObject.get("conclusion");
}
/**
* 审核本地文件
* @param imagePath 传入文件路径
* @return
*/
public String scanImageFile(String imagePath){
JSONObject jsonObject = aipContentCensor.imageCensorUserDefined(imagePath, EImgType.FILE, null);
return (String) jsonObject.get("conclusion");
}
/**
* 批量审核图片数组
* @param imgList
* @return
*/
public String scanImageList(List<byte[]> imgList){
//做一个结果集
List<String> resultList = new ArrayList<>();
for (byte[] bytes : imgList) {
String res = scanImage(bytes);
if(res.equals("不合规")){
//只要有一张是不合规的,就返回不合规
return "不合规";
}
//就是疑似,合规,失败
//添加到结果集中
resultList.add(res);
}
//判断结果集合
//如果有 疑似,则返回
if(resultList.contains("疑似")||resultList.contains("审核失败")){
return "疑似";
}
//都没有,则
return "合规";
}
}
3.7.3在配置文件中配置百度审核的key
#百度ai审核
baidu:
appId: xxxxx
apiKey: xxxxxxxxxxxxxxxxxxx
secretKey: xxxxxxxxxxxxxxxxx
然后就可以在需要的地方直接注入就可以了!
二.分布式id
随着业务的增长,文章表可能要占用很大的物理存储空间,为了解决该问题,后期使用数据库分片技术。将一个数据库进行拆分,通过数据库中间件连接。如果数据库中该表选用ID自增策略,则可能产生重复的ID,此时应该使用分布式ID生成策略来生成ID。
snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0
文章端相关的表都使用雪花算法生成id,包括ap_article、 ap_article_config、 ap_article_content
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)
三.审核功能
在文章审核成功以后需要在app的article库中新增文章数据
1.保存文章信息 ap_article
2.保存文章配置信息 ap_article_config
3.保存文章内容 ap_article_content
1.feign接口
说明 | |
---|---|
接口路径 | /api/v1/article/save |
请求方式 | POST |
参数 | ArticleDto |
响应结果 | ResponseResult |
ArticleDto
package com.heima.model.article.dtos;
import com.heima.model.article.pojos.ApArticle;
import lombok.Data;
@Data
public class ArticleDto extends ApArticle {
/**
* 文章内容
*/
private String content;
}
成功:
{
"code": 200,
"errorMessage" : "操作成功",
"data":"1302864436297442242"
}
失败:
{
"code":501,
"errorMessage":"参数失效",
}
{
"code":501,
"errorMessage":"文章没有找到",
}
2.功能实现:
①:在heima-leadnews-feign-api中新增接口
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
第二:定义文章端的接口
package com.heima.apis.article;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.common.dtos.ResponseResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.io.IOException;
@FeignClient(value = "leadnews-article")
public interface IArticleClient {
@PostMapping("/api/v1/article/save")
public ResponseResult saveArticle(@RequestBody ArticleDto dto) ;
}
②:在heima-leadnews-article中实现该方法
package com.heima.article.feign;
import com.heima.apis.article.IArticleClient;
import com.heima.article.service.ApArticleService;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.common.dtos.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
@RestController
public class ArticleClient implements IArticleClient {
@Autowired
private ApArticleService apArticleService;
@Override
@PostMapping("/api/v1/article/save")
public ResponseResult saveArticle(@RequestBody ArticleDto dto) {
return apArticleService.saveArticle(dto);
}
}
③:拷贝mapper
在资料文件夹中拷贝ApArticleConfigMapper类到mapper文件夹中
同时,修改ApArticleConfig类,添加如下构造函数
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 lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* <p>
* APP已发布文章配置表
* </p>
*
* @author itheima
*/
@Data
@NoArgsConstructor
@TableName("ap_article_config")
public class ApArticleConfig implements Serializable {
public ApArticleConfig(Long articleId){
this.articleId = articleId;
this.isComment = true;
this.isForward = true;
this.isDelete = false;
this.isDown = false;
}
@TableId(value = "id",type = IdType.ID_WORKER)
private Long id;
/**
* 文章id
*/
@TableField("article_id")
private Long articleId;
/**
* 是否可评论
* true: 可以评论 1
* false: 不可评论 0
*/
@TableField("is_comment")
private Boolean isComment;
/**
* 是否转发
* true: 可以转发 1
* false: 不可转发 0
*/
@TableField("is_forward")
private Boolean isForward;
/**
* 是否下架
* true: 下架 1
* false: 没有下架 0
*/
@TableField("is_down")
private Boolean isDown;
/**
* 是否已删除
* true: 删除 1
* false: 没有删除 0
*/
@TableField("is_delete")
private Boolean isDelete;
}
④:在ApArticleService中新增方法
/**
* 保存app端相关文章
* @param dto
* @return
*/
ResponseResult saveArticle(ArticleDto dto) ;
实现类:
@Autowired
private ApArticleConfigMapper apArticleConfigMapper;
@Autowired
private ApArticleContentMapper apArticleContentMapper;
/**
* 保存app端相关文章
* @param dto
* @return
*/
@Override
public ResponseResult saveArticle(ArticleDto dto) {
//1.检查参数
if(dto == null){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
ApArticle apArticle = new ApArticle();
BeanUtils.copyProperties(dto,apArticle);
//2.判断是否存在id
if(dto.getId() == null){
//2.1 不存在id 保存 文章 文章配置 文章内容
//保存文章
save(apArticle);
//保存配置
ApArticleConfig apArticleConfig = new ApArticleConfig(apArticle.getId());
apArticleConfigMapper.insert(apArticleConfig);
//保存 文章内容
ApArticleContent apArticleContent = new ApArticleContent();
apArticleContent.setArticleId(apArticle.getId());
apArticleContent.setContent(dto.getContent());
apArticleContentMapper.insert(apArticleContent);
}else {
//2.2 存在id 修改 文章 文章内容
//修改 文章
updateById(apArticle);
//修改文章内容
ApArticleContent apArticleContent = apArticleContentMapper.selectOne(Wrappers.<ApArticleContent>lambdaQuery().eq(ApArticleContent::getArticleId, dto.getId()));
apArticleContent.setContent(dto.getContent());
apArticleContentMapper.updateById(apArticleContent);
}
//3.结果返回 文章的id
return ResponseResult.okResult(apArticle.getId());
}
⑤:测试
编写junit单元测试,或使用postman进行测试
{
"title":"黑马头条项目背景22222222222222",
"authoId":1102,
"layout":1,
"labels":"黑马头条",
"publishTime":"2028-03-14T11:35:49.000Z",
"images": "http://192.168.200.130:9000/leadnews/2021/04/26/5ddbdb5c68094ce393b08a47860da275.jpg",
"content":"22222222222222222黑马头条项目背景,黑马头条项目背景,黑马头条项目背景,黑马头条项目背景,黑马头条项目背景"
}
可以看到,已经成功的保存到文章信息,配置,内容表中了!
3.正式审核
在heima-leadnews-wemedia中的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.apis.article.IArticleClient;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.wemedia.pojos.WmChannel;
import com.heima.model.wemedia.pojos.WmNews;
import com.heima.model.wemedia.pojos.WmUser;
import com.heima.wemedia.baidu.MyBaiduCensor;
import com.heima.wemedia.mapper.WmChannelMapper;
import com.heima.wemedia.mapper.WmNewsMapper;
import com.heima.wemedia.mapper.WmUserMapper;
import com.heima.wemedia.service.WmNewsAutoScanService;
import com.oss.template.MinIoTemplate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;
/**
* 文章自动审核
*/
@Service
@Transactional
public class WmNewsAutoScanServiceImpl implements WmNewsAutoScanService {
@Resource
private WmNewsMapper wmNewsMapper;
@Resource
private MyBaiduCensor myBaiduCensor;
@Resource
private MinIoTemplate minIoTemplate;
/**
* 自媒体文章审核
*
* @param id 自媒体文章id
*/
@Override
public void autoScanWmNews(Integer id) {
//1.查询自媒体文章
WmNews wmNews = wmNewsMapper.selectById(id);
if(wmNews == null){
throw new RuntimeException("WmNewsAutoScanServiceImpl-文章不存在");
}
if(wmNews.getStatus().equals(WmNews.Status.SUBMIT.getCode())){
//从内容中提取纯文本内容和图片
Map<String,Object> textAndImages = handleTextAndImages(wmNews);
//2.审核文本内容 百度云接口
boolean isTextScan = handleTextScan((String) textAndImages.get("content"),wmNews);
if(!isTextScan)return;
//3.审核图片 百度云接口
boolean isImageScan = handleImageScan((List<String>) textAndImages.get("images"),wmNews);
if(!isImageScan)return;
//4.审核成功,保存app端的相关的文章数据
ResponseResult responseResult = saveAppArticle(wmNews);
if(!responseResult.getCode().equals(200)){
throw new RuntimeException("WmNewsAutoScanServiceImpl-文章审核,保存app端相关文章数据失败");
}
//回填article_id
wmNews.setArticleId((Long) responseResult.getData());
updateWmNews(wmNews,(short) 9,"审核成功");
}
}
@Resource
private IArticleClient articleClient;
@Resource
private WmChannelMapper wmChannelMapper;
@Resource
private WmUserMapper wmUserMapper;
/**
* 保存app端相关的文章数据
* @param wmNews
*/
private ResponseResult saveAppArticle(WmNews wmNews) {
ArticleDto dto = new ArticleDto();
//属性的拷贝
BeanUtils.copyProperties(wmNews,dto);
//文章的布局
dto.setLayout(wmNews.getType());
//频道
WmChannel wmChannel = wmChannelMapper.selectById(wmNews.getChannelId());
if(wmChannel != null){
dto.setChannelName(wmChannel.getName());
}
//作者
dto.setAuthorId(wmNews.getUserId().longValue());
WmUser wmUser = wmUserMapper.selectById(wmNews.getUserId());
if(wmUser != null){
dto.setAuthorName(wmUser.getName());
}
//设置文章id
if(wmNews.getArticleId() != null){
dto.setId(wmNews.getArticleId());
}
dto.setCreatedTime(new Date());
ResponseResult responseResult = articleClient.saveArticle(dto);
return responseResult;
}
/**
* 审核图片
* @param images
* @param wmNews
* @return
*/
private boolean handleImageScan(List<String> images, WmNews wmNews) {
boolean flag = true;
if(images == null || images.size() == 0){
return flag;
}
//下载图片 minIO
//图片去重
images = images.stream().distinct().collect(Collectors.toList());
List<byte[]> imageList = new ArrayList<>();
for (String image : images) {
byte[] bytes = minIoTemplate.downLoadFile(image);
imageList.add(bytes);
}
//审核图片
try {
String res = myBaiduCensor.scanImageList(imageList);
if(res != null){
//审核失败
if(res.equals("不合规")){
flag = false;
updateWmNews(wmNews, (short) 2, "当前文章中存在违规内容");
}
//不确定信息 需要人工审核
if(res.equals("疑似")){
flag = false;
updateWmNews(wmNews, (short) 3, "当前文章中存在不确定内容");
}
}
} catch (Exception e) {
flag = false;
e.printStackTrace();
}
return flag;
}
/**
* 审核纯文本内容
* @param content
* @param wmNews
* @return
*/
private boolean handleTextScan(String content, WmNews wmNews) {
boolean flag = true;
if((wmNews.getTitle()+""+content).length() == 0){
return flag;
}
try {
String res =myBaiduCensor.scanText((wmNews.getTitle()+"-"+content));
if(res != null){
//审核失败
if(res.equals("不合规")){
flag = false;
updateWmNews(wmNews, (short) 2, "当前文章中存在违规内容");
}
//不确定信息 需要人工审核
if(res.equals("疑似")){
flag = false;
updateWmNews(wmNews, (short) 3, "当前文章中存在不确定内容");
}
}
} catch (Exception e) {
flag = false;
e.printStackTrace();
}
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);
}
/**
* 1。从自媒体文章的内容中提取文本和图片
* 2.提取文章的封面图片
* @param wmNews
* @return
*/
private Map<String, Object> handleTextAndImages(WmNews wmNews) {
//存储纯文本内容
StringBuilder stringBuilder = new StringBuilder();
List<String> images = new ArrayList<>();
//1。从自媒体文章的内容中提取文本和图片
if(StringUtils.isNotBlank(wmNews.getContent())){
List<Map> maps = JSONArray.parseArray(wmNews.getContent(), Map.class);
for (Map map : maps) {
if (map.get("type").equals("text")){
stringBuilder.append(map.get("value"));
}
if (map.get("type").equals("image")){
images.add((String) map.get("value"));
}
}
}
//2.提取文章的封面图片
if(StringUtils.isNotBlank(wmNews.getImages())){
String[] split = wmNews.getImages().split(",");
images.addAll(Arrays.asList(split));
}
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("content",stringBuilder.toString());
resultMap.put("images",images);
return resultMap;
}
}
3.1feign远程接口调用
在heima-leadnews-wemedia服务中已经依赖了heima-leadnews-feign-apis工程,只需要在自媒体的引导类中开启feign的远程调用即可
注解为:@EnableFeignClients(basePackages = "com.heima.apis")
需要指向apis这个包
四.服务降级
-
服务降级是服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,确保服务不会崩溃
-
服务降级虽然会导致请求失败,但是不会导致阻塞
①:在heima-leadnews-feign-api编写降级逻辑
package com.heima.apis.article.fallback;
import com.heima.apis.article.IArticleClient;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import org.springframework.stereotype.Component;
/**
* feign失败配置
* @author itheima
*/
@Component
public class IArticleClientFallback implements IArticleClient {
@Override
public ResponseResult saveArticle(ArticleDto dto) {
return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR,"获取数据失败");
}
}
在自媒体微服务中添加类,扫描降级代码类的包
package com.heima.wemedia.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan("com.heima.apis.article.fallback")
public class InitConfig {
}
②:远程接口中指向降级代码
package com.heima.apis.article;
import com.heima.apis.article.fallback.IArticleClientFallback;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.common.dtos.ResponseResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@FeignClient(value = "leadnews-article",fallback = IArticleClientFallback.class)
public interface IArticleClient {
@PostMapping("/api/v1/article/save")
public ResponseResult saveArticle(@RequestBody ArticleDto dto);
}
③:客户端开启降级heima-leadnews-wemedia
在wemedia的nacos配置中心里添加如下内容,开启服务降级,也可以指定服务响应的超时的时间
feign:
# 开启feign对hystrix熔断降级的支持
hystrix:
enabled: true
# 修改调用超时时间
client:
config:
default:
connectTimeout: 2000
readTimeout: 2000
④:测试
在ApArticleServiceImpl类中saveArticle方法添加代码
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
在自媒体端进行审核测试,会出现服务降级的现象
五.集成自动审核功能
异步审核文章
5.2)Springboot集成异步线程调用
①:在自动审核的方法上加上@Async注解(标明要异步调用)
@Override
@Async //标明当前方法是一个异步方法
public void autoScanWmNews(Integer id) {
//代码略
}
②:在文章发布成功后调用审核的方法
@Autowired
private WmNewsAutoScanService wmNewsAutoScanService;
/**
* 发布修改文章或保存为草稿
* @param dto
* @return
*/
@Override
public ResponseResult submitNews(WmNewsDto dto) {
//代码略
//审核文章
wmNewsAutoScanService.autoScanWmNews(wmNews.getId());
return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}
③:在自媒体引导类中使用@EnableAsync注解开启异步调用
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.heima.wemedia.mapper")
@EnableFeignClients(basePackages = "com.heima.apis")
@EnableAsync //开启异步调用
public class WemediaApplication {
}
文章审核功能-综合测试
启动服务
1,nacos服务端
2,article微服务
3,wemedia微服务
4,启动wemedia网关微服务
5,启动前端系统wemedia
6.2)测试情况列表
1,自媒体前端发布一篇正常的文章
审核成功后,app端的article相关数据是否可以正常保存,自媒体文章状态和app端文章id是否回显
2,自媒体前端发布一篇包含敏感词的文章
正常是审核失败, wm_news表中的状态是否改变,成功和失败原因正常保存
3,自媒体前端发布一篇包含敏感图片的文章
正常是审核失败, wm_news表中的状态是否改变,成功和失败原因正常保存
六.DFA自管理敏感词过滤
文章审核功能已经交付了,文章也能正常发布审核。突然,产品经理过来说要开会。
会议的内容核心有以下内容:
-
文章审核不能过滤一些敏感词:
私人侦探、针孔摄象、信用卡提现、广告代理、代开发票、刻章办、出售答案、小额贷款…
需要完成的功能:
需要自己维护一套敏感词,在文章审核的时候,需要验证文章是否包含这些敏感词
技术选型
方案 | 说明 |
---|---|
数据库模糊查询 | 效率太低 |
String.indexOf("")查找 | 数据库量大的话也是比较慢 |
全文检索 | 分词再匹配 |
DFA算法 | 确定有穷自动机(一种数据结构) |
7.3)DFA实现原理
DFA全称为:Deterministic Finite Automaton,即确定有穷自动机。
存储:一次性的把所有的敏感词存储到了多个map中,就是下图表示这种结构
敏感词:冰毒、大麻、大坏蛋
7.4)自管理敏感词集成到文章审核中
/*
Navicat MySQL Data Transfer
Source Server : localhost
Source Server Version : 50721
Source Host : localhost:3306
Source Database : leadnews_wemedia
Target Server Type : MYSQL
Target Server Version : 50721
File Encoding : 65001
Date: 2021-05-23 11:19:37
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for ad_sensitive
-- ----------------------------
DROP TABLE IF EXISTS `wm_sensitive`;
CREATE TABLE `wm_sensitive` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`sensitives` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '敏感词',
`created_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3201 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='敏感词信息表';
-- ----------------------------
-- Records of wm_sensitive
-- ----------------------------
INSERT INTO `wm_sensitive` VALUES ('3104', '冰毒', '2021-05-23 15:38:51');
INSERT INTO `wm_sensitive` VALUES ('3105', '法轮功', '2021-05-23 15:38:51');
INSERT INTO `wm_sensitive` VALUES ('3106', '私人侦探', '2021-05-23 11:09:22');
INSERT INTO `wm_sensitive` VALUES ('3107', '针孔摄象', '2021-05-23 11:09:52');
INSERT INTO `wm_sensitive` VALUES ('3108', '信用卡提现', '2021-05-23 11:10:11');
INSERT INTO `wm_sensitive` VALUES ('3109', '无抵押贷款', '2021-05-23 11:10:41');
INSERT INTO `wm_sensitive` VALUES ('3110', '广告代理', '2021-05-23 11:10:59');
INSERT INTO `wm_sensitive` VALUES ('3111', '代开发票', '2021-05-23 11:11:18');
INSERT INTO `wm_sensitive` VALUES ('3112', '蚁力神', '2021-05-23 11:11:39');
INSERT INTO `wm_sensitive` VALUES ('3113', '售肾', '2021-05-23 11:12:08');
INSERT INTO `wm_sensitive` VALUES ('3114', '刻章办', '2021-05-23 11:12:24');
INSERT INTO `wm_sensitive` VALUES ('3116', '套牌车', '2021-05-23 11:12:37');
INSERT INTO `wm_sensitive` VALUES ('3117', '足球投注', '2021-05-23 11:12:51');
INSERT INTO `wm_sensitive` VALUES ('3118', '地下钱庄', '2021-05-23 11:13:07');
INSERT INTO `wm_sensitive` VALUES ('3119', '出售答案', '2021-05-23 11:13:24');
INSERT INTO `wm_sensitive` VALUES ('3200', '小额贷款', '2021-05-23 11:13:40');
package com.heima.model.wemedia.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>
* 敏感词信息表
* </p>
*
* @author itheima
*/
@Data
@TableName("wm_sensitive")
public class WmSensitive implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 敏感词
*/
@TableField("sensitives")
private String sensitives;
/**
* 创建时间
*/
@TableField("created_time")
private Date createdTime;
}
②:拷贝对应的wm_sensitive的mapper到项目中
package com.heima.wemedia.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.heima.model.wemedia.pojos.WmSensitive;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface WmSensitiveMapper extends BaseMapper<WmSensitive> {
}
③:在文章审核的代码中添加自管理敏感词审核
第一:在WmNewsAutoScanServiceImpl中的autoScanWmNews方法上添加如下代码
//从内容中提取纯文本内容和图片
//.....省略
//自管理的敏感词过滤
boolean isSensitive = handleSensitiveScan((String) textAndImages.get("content"), wmNews);
if(!isSensitive) return;
//2.审核文本内容 阿里云接口
//.....省略
新增自管理敏感词审核代码
@Autowired
private WmSensitiveMapper wmSensitiveMapper;
/**
* 自管理的敏感词审核
* @param content
* @param wmNews
* @return
*/
private boolean handleSensitiveScan(String content, WmNews wmNews) {
boolean flag = true;
//获取所有的敏感词
List<WmSensitive> wmSensitives = wmSensitiveMapper.selectList(Wrappers.<WmSensitive>lambdaQuery().select(WmSensitive::getSensitives));
List<String> sensitiveList = wmSensitives.stream().map(WmSensitive::getSensitives).collect(Collectors.toList());
//初始化敏感词库
SensitiveWordUtil.initMap(sensitiveList);
//查看文章中是否包含敏感词
Map<String, Integer> map = SensitiveWordUtil.matchWords(content);
if(map.size() >0){
updateWmNews(wmNews,(short) 2,"当前文章中存在违规内容"+map);
flag = false;
}
return flag;
}
当我们在发布审核的时候,就可以进行敏感词检测了!
七.图像文字识别审核
文章中包含的图片要识别文字,过滤掉图片文字的敏感词
7.2)图片文字识别
什么是OCR?
OCR (Optical Character Recognition,光学字符识别)是指电子设备(例如扫描仪或数码相机)检查纸上打印的字符,通过检测暗、亮的模式确定其形状,然后用字符识别方法将形状翻译成计算机文字的过程
方案 | 说明 |
---|---|
百度OCR | 收费 |
Tesseract-OCR | Google维护的开源OCR引擎,支持Java,Python等语言调用 |
Tess4J | 封装了Tesseract-OCR ,支持Java调用 |
7.3在自媒体服务中引入依赖
<dependency>
<groupId>net.sourceforge.tess4j</groupId>
<artifactId>tess4j</artifactId>
<version>4.1.1</version>
</dependency>
7.4工具类
package com.heima.common.tess4j;
import lombok.Getter;
import lombok.Setter;
import net.sourceforge.tess4j.ITesseract;
import net.sourceforge.tess4j.Tesseract;
import net.sourceforge.tess4j.TesseractException;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.awt.image.BufferedImage;
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "tess4j")
public class Tess4jClient {
private String dataPath;
private String language;
public String doOCR(BufferedImage image) throws TesseractException {
//创建Tesseract对象
ITesseract tesseract = new Tesseract();
//设置字体库路径
tesseract.setDatapath(dataPath);
//中文识别
tesseract.setLanguage(language);
//执行ocr识别
String result = tesseract.doOCR(image);
//替换回车和tal键 使结果为一行
result = result.replaceAll("\\r|\\n", "-").replaceAll(" ", "");
return result;
}
}
在heima-leadnews-wemedia中的配置中添加两个属性
tess4j:
data-path: D:\workspace\tessdata
language: chi_sim
③:在WmNewsAutoScanServiceImpl中的handleImageScan方法上添加如下代码
try {
for (String image : images) {
byte[] bytes = fileStorageService.downLoadFile(image);
//图片识别文字审核---begin-----
//从byte[]转换为butteredImage
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
BufferedImage imageFile = ImageIO.read(in);
//识别图片的文字
String result = tess4jClient.doOCR(imageFile);
//审核是否包含自管理的敏感词
boolean isSensitive = handleSensitiveScan(result, wmNews);
if(!isSensitive){
return isSensitive;
}
//图片识别文字审核---end-----
imageList.add(bytes);
}
}catch (Exception e){
e.printStackTrace();
}
这样就可以将图片中的敏感词过滤出来,进行自定义过滤器过滤了!
八.异步上传静态html文件
文章端创建app相关文章时,生成文章详情静态页上传到MinIO中
1.新建ArticleFreemarkerService创建静态文件并上传到minIO中
package com.heima.article.service;
import com.heima.model.article.pojos.ApArticle;
public interface ArticleFreemarkerService {
/**
* 生成静态文件上传到minIO中
* @param apArticle
* @param content
*/
public void buildArticleToMinIO(ApArticle apArticle,String content);
}
实现类
package com.heima.article.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.heima.article.mapper.ApArticleContentMapper;
import com.heima.article.service.ApArticleService;
import com.heima.article.service.ArticleFreemarkerService;
import com.heima.model.article.pojos.ApArticle;
import com.oss.template.MinIoTemplate;
import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
@Transactional
public class ArticleFreemarkerServiceImpl implements ArticleFreemarkerService {
@Resource
private ApArticleContentMapper apArticleContentMapper;
@Resource
private Configuration configuration;
@Resource
private MinIoTemplate minIoTemplate;
@Resource
private ApArticleService apArticleService;
/**
* 生成静态文件上传到minIO中
* @param apArticle
* @param content
*/
@Async
@Override
public void buildArticleToMinIO(ApArticle apArticle, String content) {
//已知文章的id
//4.1 获取文章内容
if(StringUtils.isNotBlank(content)){
//4.2 文章内容通过freemarker生成html文件
Template template = null;
StringWriter out = new StringWriter();
try {
template = configuration.getTemplate("article.ftl");
//数据模型
Map<String,Object> contentDataModel = new HashMap<>();
contentDataModel.put("content", JSONArray.parseArray(content));
//合成
template.process(contentDataModel,out);
} catch (Exception e) {
e.printStackTrace();
}
//4.3 把html文件上传到minio中
InputStream in = new ByteArrayInputStream(out.toString().getBytes());
String path = minIoTemplate.uploadHtmlFile("", apArticle.getId() + ".html", in);
//4.4 修改ap_article表,保存static_url字段
apArticleService.update(Wrappers.<ApArticle>lambdaUpdate().eq(ApArticle::getId,apArticle.getId())
.set(ApArticle::getStaticUrl,path));
}
}
}
在apArticleServiceImpl中的saveArticle方法中使用
//修改文章内容
ApArticleContent apArticleContent = apArticleContentMapper.selectOne(Wrappers.<ApArticleContent>lambdaQuery().eq(ApArticleContent::getArticleId, dto.getId()));
apArticleContent.setContent(dto.getContent());
apArticleContentMapper.updateById(apArticleContent);
}
//异步调用 生成静态文件上传到minio中
articleFreemarkerService.buildArticleToMinIO(apArticle,dto.getContent());
//3.结果返回 文章的id
return ResponseResult.okResult(apArticle.getId());
}
当保存或者修改完文章之后,将文章制作成静态文件上传到minio中
测试
这里我已经上传了一篇文章,而且已经审核成功了!我们去看一下数据库中的app_artice表:
1737033426181165057 鸽鸽写的单你别吃 1102 admin 7 其他 1 http://192.168.81.128:9000/leadnews/2023/12/18/7c2fc42e87044d24b171d23645e895b6.jpg 11 2023-12-19 16:53:36 2023-12-19 16:53:30 0 0 http://192.168.81.128:9000/leadnews/2023/12/19/1737033426181165057.html
发现在最后插入了一条文章信息,而且有它的网页地址,我们访问一下这个网页:
可以看到成功的获取到了静态页面,至此,文章发布,自动审核,制作静态网页上架大功告成!