黑马头条--day04--文章审核

news2024/11/29 6:27:40

目录

一.审核

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_idLongY请求唯一id
error_codeLongN错误提示码,失败才返回,成功不返回
error_msgStringN错误提示信息,失败才返回,成功不返回
conclusionStringN审核结果,可取值:合规、不合规、疑似、审核失败
conclusionTypeIntegerN审核结果类型,可取值1.合规,2.不合规,3.疑似,4.审核失败
dataArrayN不合规/疑似/命中白名单项详细信息。响应成功并且conclusion为疑似或不合规或命中白名单时才返回,响应失败或conclusion为合规且未命中白名单时不返回。
+error_codeInteger内层错误提示码,底层服务失败才返回,成功不返回
+error_msgString内层错误提示信息,底层服务失败才返回,成功不返回
+typeIntegerN审核主类型,11:百度官方违禁词库、12:文本反作弊、13:自定义文本黑名单、14:自定义文本白名单
+subTypeIntegerN审核子类型,此字段需参照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_idLong请求唯一id,用于问题排查
error_codeLong错误提示码,失败才返回,成功不返回
error_msgString错误提示信息,失败才返回,成功不返回
conclusionString审核结果,可取值描述:合规、不合规、疑似、审核失败
conclusionTypeInteger审核结果类型,可取值1、2、3、4,分别代表1:合规,2:不合规,3:疑似,4:审核失败
dataArray不合规/疑似/命中白名单项详细信息。响应成功并且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-OCRGoogle维护的开源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

 发现在最后插入了一条文章信息,而且有它的网页地址,我们访问一下这个网页:

可以看到成功的获取到了静态页面,至此,文章发布,自动审核,制作静态网页上架大功告成!

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1322193.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【1.8计算机组成与体系结构】磁盘管理

目录 1.磁盘基本结构与存取过程1.1 磁盘基本结构1.2 磁盘的存取过程 2.磁盘优化分布存储3.磁盘单缓冲区与双缓冲区4.磁盘移臂调度算法 1.磁盘基本结构与存取过程 1.1 磁盘基本结构 磁盘&#xff1a;柱面&#xff0c;磁道&#xff0c;扇区。 1.2 磁盘的存取过程 存取时间寻…

C#调用阿里云接口实现动态域名解析,支持IPv6(Windows系统下载可用)

电信宽带一般能申请到公网IP&#xff0c;但是是动态的&#xff0c;基本上每天都要变&#xff0c;所以想到做一个定时任务&#xff0c;随系统启动&#xff0c;网上看了不少博文很多都支持IPv4&#xff0c;自己动手写了一个。 &#xff08;私信可全程指导&#xff09; 部署步骤…

原生JS实现组件切换(不刷新页面)

这是通过原生Es6实现的组件切换&#xff0c;代码很简单&#xff0c;原理和各种框架原理大致相同。 创建文件 ├── component&#xff1a;存放组件 │ ├── home1.js&#xff1a;组件1 │ ├── home2.js&#xff1a;组件2 ├── index.html ├── index.js初始化ht…

台湾虾皮本土店铺:如何在台湾虾皮本土店铺开展电商业务

在台湾地区&#xff0c;虾皮&#xff08;Shopee&#xff09;是一款备受欢迎的电商平台。虾皮拥有强大的技术团队、丰富的电商经验和对市场的深刻理解。虾皮本土店铺凭借其在出售、物流、回款、售后、仓储等方面的一条龙服务&#xff0c;为广大卖家提供了全方位的保障和支持。如…

VSCode报错插件Error lens

1.点击左侧扩展图标→搜索“error lens”→点击“安装” 2.安装成功页面如下&#xff1a; 3.代码测试一下&#xff1a;书写代码的过程中会出现红色提醒或红色报错 4.另外推荐小伙伴们安装中文插件&#xff0c;学习过程中会比较实用方便&#xff0c;需要安装中文插件的小伙伴请点…

【性能测试】资深老鸟带你,一篇打通负载与压力测试的区别...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 负载测试 是通过…

【lesson17】MySQL表的基本操作--表去重、聚合函数和group by

文章目录 MySQL表的基本操作介绍插入结果查询&#xff08;表去重&#xff09;建表插入数据操作 聚合函数建表插入数据操作 group by&#xff08;分组&#xff09;建表插入数据操作 MySQL表的基本操作介绍 CRUD : Create(创建), Retrieve(读取)&#xff0c;Update(更新)&#x…

2.vue学习(8-7)

文章目录 8.数据绑定9.el与data的2种写法 8.数据绑定 单向数据绑定就是我们学的v-bind的方式&#xff0c;vue对象变了&#xff0c;页面才变。但是页面变了&#xff0c;vue对象不会变。 双向数据绑定需要用v-model&#xff0c;就能实现双向的改变。 注意&#xff1a;不是所有的…

我的4096创作纪念日

机缘 岁月如梭&#xff0c;时光一晃已经在CSDN扎根4096天了。第一次注册CSDN好像还是在2012年&#xff0c;那会还没大学毕业。初入CSDN&#xff0c;只是把他当作自己编程时遇到问题的在线笔记记录而已&#xff0c;没想到无意间还帮助了其他遇到同样问题困扰的同学。而在这4096…

简析555电压检测电路

555定时器的简介 555定时器是一种多用途的数字——模拟混合集成电路&#xff0c;利用它能极方便地构成施密特触发器、单稳态触发器和多谐振荡器。由于使用灵活、方便&#xff0c;所以555定时器在波形的产生与交换、测量与控制、家用电器、电子玩具等许多领域中都得到了广泛应用…

idea添加外部jar包

在日常开发中在lib包的里面添加了外部的jar&#xff0c;如何将外部的包添加到java类库中&#xff0c;这样项目就可以引用相应的jar包&#xff0c;操作如下&#xff1a; 1.先将需要的jar复制到lib包如下&#xff0c;如下截图&#xff0c;图标前面没有箭头&#xff0c;表示还未添…

echart饼状图文字大小位置颜色调整属性

echart饼状图文字大小位置颜色调整属性 文字位置对应属性代码效果图 文字位置对应属性 1.图中‘1’的文字大小调整在‘legend’对象下的‘textStyle’属性里 2.图中‘2’的文字大小调整在‘tooltip’对象下的‘textStyle’属性里 3.图中‘3’的文字大小调整在‘series’对象下的…

flink 读取 apache paimon表,查看source的延迟时间 消费堆积情况

paimon source查看消费的数据延迟了多久 如果没有延迟 则显示0 官方文档 Metrics | Apache Paimon

什么是证券RPA?证券RPA解决什么问题?证券RPA实施难点在哪里?

RPA智能机器人&#xff0c;也称为“机器人流程自动化”、“软件机器人”&#xff0c;使用智能自动化技术来执行人类工人的重复性办公室任务。它结合API和用户界面(UI)交互来集成和执行企业和生产力应用程序之间的重复性任务。只要预先设计好使用规则&#xff0c;RPA就可以模拟人…

.NET 自定义中间件 判断是否存在 AllowAnonymousAttribute 特性 来判断是否需要身份验证

public Task InvokeAsync(HttpContext context){// 获取终点路由特性var endpointFeature context.Features.Get<IEndpointFeature>();// 获取是否定义了特性var attribute endpointFeature?.Endpoint?.Metadata?.GetMetadata<AllowAnonymousAttribute>();if …

项目中webpack优化配置(持续更新)

项目中webpack优化配置 1. 开发效率&#xff0c; 体验 DLL&#xff08;开发过程中减少构建时间和增加应用程序的性能&#xff09; 使用 DllPlugin 进行分包&#xff0c;使用 DllReferencePlugin(索引链接) 对 manifest.json 引用&#xff0c;让一些基本不会改动的代码先打包…

JVM调优排错专题

JVM调优排错专题 1 打开MAT报错 1 打开MAT报错 下载了linux版本的 MAT 软件&#xff0c;1.15.0版本。 下载地址&#xff1a;https://eclipse.dev/mat/downloads.php 运行时报错了。 错误截图 报错日志 wittasus:/usr/develop/mat$ ./MemoryAnalyzer Unrecognized option:…

【PHD申请文书】motivation letter|不限字数|Medical Imaging and Application

本文目录 APPLICATION ESSAYCriticism思考 Ref: https://essayforum.com/letters/tryst-technology-motivation-erasmus-degree-85383/ APPLICATION ESSAY My tryst with technology - motivation letter for Erasmus Degree in Medical Imaging and Application 原文机翻My…

视频素材网站全新上线,海量高清视频等你来探索~

亲爱的视频制作爱好者们&#xff0c;好消息来啦&#xff01;我们的视频素材网站全新上线啦&#xff01;这次我们为大家带来了海量的高清视频素材&#xff0c;无论是风景、城市、人物、动物还是各种特效、背景等&#xff0c;应有尽有&#xff0c;满足您在视频制作过程中的各种需…

WebMvcConfigurer接口详解及使用方式(Spring-WebMvc)

简介 如下图所示WebMvcConfigurer是spring-webmvc jar包下的一个接口&#xff0c;spring-webmvc jar包又来源于spring-boot-starter-web&#xff0c;所以要使用WebMvcConfigurer要引入spring-boot-starter-web依赖。WebMvcConfigurer接口提供了常用的web应用拦截方法。通过实现…