当前位置: 首页 > news >正文

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

自动审核流程介绍

 

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

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

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

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

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

  • 查询文章数据

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

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

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

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

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

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

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

2.1)内容安全接口选型

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

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

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

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

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

2.2)阿里云服务介绍

2.2.1 准备工作

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

操作步骤

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

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

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

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

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

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

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

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

    创建项目aliyun-test

    安装sdk

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

    示例代码

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

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

 

2.2.3 阿里云安全-图片审核

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

示例代码地址

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

修改后的示例代码

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

测试:

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

image1测试结果:不通过

 

3 阿里云安全集成到项目

3.1)依赖引入

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

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

3.2)引入图片上传工具类

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

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

3.3)新建配置文件

在resources中新建aliyun.properties

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

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

  • porn:图片智能鉴黄

  • terrorism:图片暴恐涉政

  • ad:图文违规

  • qrcode:图片二维码

  • live:图片不良场景

  • logo:图片logo

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

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

3.4)测试

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

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

wemedia的pom添加依赖:

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

创建测试类:

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

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

4 敏感词过滤算法DFA

(1)文章审核需求

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

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

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

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

需要完成的功能:

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

(2)敏感词-过滤

技术选型

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

4.1)DFA实现原理

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

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

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

4.2)代码实现

工具类:

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

新建测试类:

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

5 自媒体文章审核

5.1)表结构说明

wm_news 自媒体文章表

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

5.2)准备远程调用接口

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

admin-serviceAdSensitiveMapper 新增方法

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

admin-seriviceAdSensitiveService 新增方法

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

admin-seriviceAdSensitiveServiceImpl 实现方法

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

admin-serviceAdSensitiveController新增方法

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

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

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

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

package com.heima.feigns.fallback;
import com.heima.feigns.AdminFeign;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
@Slf4j
@Component
public class AdminFeignFallback implements FallbackFactory<AdminFeign> {@Overridepublic AdminFeign create(Throwable throwable) {throwable.printStackTrace();return new AdminFeign() {@Overridepublic ResponseResult<List<String>> sensitives() {log.error("AdminFeign sensitives 远程调用出错啦 ~~~ !!!! {} ",throwable.getMessage());return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR);}};}
}

5.3)审核接口实现

wemedia-service 中的service新增接口

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

实现类:

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

6 集成RabbitMQ实现自动审核

6.1) 同步调用与异步调用

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

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

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

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

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

6.3)集成RabbitMQ

wemedia-service引入mq依赖

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

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

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

添加共享配置

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

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

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

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

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

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

6.4)发送及消费消息

发送消息

wemedia-service 服务 WmNewsServiceImpl.submitNews

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

消费消息

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

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

服务启动列表:

  1. wemedia微服务

  2. admin微服务

  3. wemedia网关微服务

  4. 启动前端系统wemedia

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

结果:

  1. 查看控制台日志是否触发自动审核,审核的具体结果

相关文章:

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

自动审核流程介绍 做为内容类产品&#xff0c;内容安全非常重要&#xff0c;所以需要进行对自媒体用户发布的文章进行审核以后才能到app端展示给用户。2 WmNews 中status 代表自媒体文章的状态 status字段&#xff1a;0 草稿 1 待审核 2 审核失败 3 人工审核 4 人工审核通过 …...

python--matplotlib(1)

前言 Matplotlib画图工具的官网地址是 http://matplotlib.org/ Python环境下实现Matlab制图功能的第三方库&#xff0c;需要numpy库的支持&#xff0c;支持用户方便设计出二维、三维数据的图形显示。 正文 1.arange函数 arange函数需要三个参数&#xff0c;分别为起始点、终止…...

华为OD机试题 - 获取最大软件版本号(JavaScript)

最近更新的博客 华为OD机试题 - 任务总执行时长(JavaScript) 华为OD机试题 - 开放日活动(JavaScript) 华为OD机试 - 最近的点 | 备考思路,刷题要点,答疑 【新解法】 华为OD机试题 - 最小步骤数(JavaScript) 华为OD机试题 - 任务混部(JavaScript) 华为OD机试题 - N 进…...

字符函数和字符串函数

字符串以\0为结束标志&#xff0c;strlen函数返回的是’\0’前的字符个数&#xff0c;不包括\0参数的指向的字符串必须是\0为结束标志&#xff0c;不然结果不确定函数的返回类型是size_t(无符号的整型&#xff09;strlen的使用#include <stdio.h> #include <string.h&…...

【猜名次】-C语言-题解

1. 描述&#xff1a; 5位运动员参加了10米台跳水比赛&#xff0c;有人让他们预测比赛结果&#xff1a; A选手说&#xff1a;B第二&#xff0c;我第三&#xff1b; B选手说&#xff1a;我第二&#xff0c;E第四&#xff1b; C选手说&#xff1a;我第一&#xff0c;D第二&#x…...

对 equals() 和 hashCode() 的理解?

在 java.lang.Object 类中有两个非常重要的方法&#xff1a; public native int hashCode(); public boolean equals(Object obj) {return (this obj); }Object 类是类继承结构的基础&#xff0c;是每一个类的父类&#xff0c;都实现了Object 类中定义的方法。 equals()方法…...

IDEA插件安装慢、超时、不成功问题如何解决?

目录 一、打开国内插件的节点IP地址 二、修改本地hosts文件 三、刷新DNS缓存 一、打开国内插件的节点IP地址 国内插件的节点IP地址查询: http://tool.chinaz.com/speedtest/plugins.jetbrains.com 在下方的检测结果中&#xff0c;找到一个解析时间最短的IP地址&#xff0c;解…...

软考高级之信息系统案例分析七重奏-《5》

五十、项目需求管理可能存在的问题。 1、未制定项目需求管理计划; 2、项目沟通存在问题; 3、项目经理缺乏必要的项目管理经验; 4、没有有效地管理需求变更控制; 5、没有有效地维护对需求进行跟踪管理; 6、没有按照规范的需求开发和需求管理的内容和流程开展需求工作…...

JUC并发编程 Ⅳ -- 共享模型之无锁

文章目录CAS 与 volatile问题引入代码分析volatile为什么无锁效率高CAS特点原子整数原子引用ABA 问题及解决原子数组原子(字段)更新器原子累加器UnsafeUnsafe CAS 操作管程即 monitor 是阻塞式的悲观锁实现并发控制&#xff0c;本文我们将通过非阻塞式的乐观锁的来实现并发控制…...

Spring之AOP实现

1. AOP的实现方式 使用AspectJ的编译器来改动class类文件实现增强(使用不广泛) ----- 编译阶段 这种对class类文件增强的, 也可以增强static静态方法, 而通过代理方式就无法实现静态方法的增强 可通过查看编译后class文件反编译后的java代码验证 agent增强(使用不广泛) ----- 类…...

Spring之基于xml的自动装配、基于Autowired注解的自动装配

文章目录基于xml的自动装配①注解②扫描③新建Maven Module④创建Spring配置文件⑤标识组件的常用注解⑥创建组件⑦扫描组件⑧测试⑨组件所对应的bean的id基于注解的自动装配①场景模拟②Autowired注解③Autowired注解其他细节④Autowired工作流程Autowire 注解的原理Qualifier…...

【案例】--(非分布式)轻量级任务调度平台

目录 一、前言说明二、背景2.1、完成任务,顺便搭建了一个任务调度平台三、具体实现解析3.1、技术栈等选型3.2、完成具体功能解析(1)、支持基本任务功能(2)、支持日志收集功能(3)、支持用户异常,选择性关闭调度功能(4)、实时监控正在执行和任务队列的任务情况(5)、实时监控任务…...

key的作用原理与列表的遍历、追加、搜索、排序

目录 一、key的作用原理 二、实现列表遍历并对在列表最前方进行追加元素 三、实现列表过滤搜索 1、用computed计算属性来实现 2、用watch监听输入值的变化来实现 四、按年龄排序输出列表 一、key的作用原理 1. 虚拟DOM中key的作用&#xff1a; key是虚拟DOM对象的标识&a…...

SQL性能优化的47个小技巧,你了解多少?

收录于热门专栏Java基础教程系列&#xff08;进阶篇&#xff09; 1、先了解MySQL的执行过程 了解了MySQL的执行过程&#xff0c;我们才知道如何进行sql优化。 客户端发送一条查询语句到服务器&#xff1b;服务器先查询缓存&#xff0c;如果命中缓存&#xff0c;则立即返回存…...

DPDK — 数据加速方案的核心思想

目录 文章目录 目录DPDK 数据加速方案1、使用用户态协议栈来代替内核协议栈Linux UIO FrameworkDPDK UIO Framework2、使用轮训来代替中断Kernelspace igb_uio DriverUserspace PMD3、使用多核编程代替多线程无锁环队列:CPU 核间无锁通信DPDK 数据加速方案...

[python入门㊽] - 自定义异常 raise 关键字

目录 ❤ 自定义抛出异常关键字 - raise ❤ 使用raise主动引发异常 ❤ raise 关键字的用法 ❤ 触发异常 ❤ 自定义异常类 在前面我们学过异常三个关键字分别是try、except 以及 finally 在编程过程中合理的使用异常可以使得程序正常的执行。有直接抛出异常的形式&…...

DDOS攻击

注&#xff1a;本博客只是为了自己的学习&#xff0c;记录自己的学习&#xff0c;请勿用于其他途径、1、winR-->cmd2、ping 网站3、替换IP1 import java.io.BufferedInputStream;2 import java.io.IOException;3 import java.net.MalformedURLException;4 import java.net.U…...

网络编程套接字

文章目录1. socket编程接口1-1 socket 常见API1-2 sockaddr结构2. 简单的UDP网络程序2-1 日志&#xff08;固定用法&#xff1a;标准部分自定义部分&#xff09;2-2 服务器代码实现1. 框架2. 初始化服务器3. 服务器运行4. 调用服务器封装函数&#xff08;UdpServer&#xff09;…...

海量数据相似数据查询方法

1、海量文本常见 海量文本场景&#xff0c;如何寻找一个doc的topn相似doc&#xff0c;一般存在2个问题&#xff0c; 1)、两两对比时间o(n^2) 2)、高维向量比较比较耗时。 文本集可以看成(doc,word)稀疏矩阵&#xff0c;一般常见的方法是构建到排索引&#xff0c;然后进行归并…...

Codeforces Round #822 (Div. 2)

A(签到) - Select Three Sticks 题意&#xff1a; 给你一个长度为 n 的正整数序列&#xff0c;你可以操作任意次&#xff0c;每一次操作可以选择任意一个元素&#xff0c;把它 1 或者 - 1&#xff0c;问最少多少次操作可以使得序列中存在三个相同的数字以构成一个等边三角形.…...

进程地址空间(比特课总结)

一、进程地址空间 1. 环境变量 1 &#xff09;⽤户级环境变量与系统级环境变量 全局属性&#xff1a;环境变量具有全局属性&#xff0c;会被⼦进程继承。例如当bash启动⼦进程时&#xff0c;环 境变量会⾃动传递给⼦进程。 本地变量限制&#xff1a;本地变量只在当前进程(ba…...

STM32+rt-thread判断是否联网

一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...

在 Nginx Stream 层“改写”MQTT ngx_stream_mqtt_filter_module

1、为什么要修改 CONNECT 报文&#xff1f; 多租户隔离&#xff1a;自动为接入设备追加租户前缀&#xff0c;后端按 ClientID 拆分队列。零代码鉴权&#xff1a;将入站用户名替换为 OAuth Access-Token&#xff0c;后端 Broker 统一校验。灰度发布&#xff1a;根据 IP/地理位写…...

江苏艾立泰跨国资源接力:废料变黄金的绿色供应链革命

在华东塑料包装行业面临限塑令深度调整的背景下&#xff0c;江苏艾立泰以一场跨国资源接力的创新实践&#xff0c;重新定义了绿色供应链的边界。 跨国回收网络&#xff1a;废料变黄金的全球棋局 艾立泰在欧洲、东南亚建立再生塑料回收点&#xff0c;将海外废弃包装箱通过标准…...

css的定位(position)详解:相对定位 绝对定位 固定定位

在 CSS 中&#xff0c;元素的定位通过 position 属性控制&#xff0c;共有 5 种定位模式&#xff1a;static&#xff08;静态定位&#xff09;、relative&#xff08;相对定位&#xff09;、absolute&#xff08;绝对定位&#xff09;、fixed&#xff08;固定定位&#xff09;和…...

SpringCloudGateway 自定义局部过滤器

场景&#xff1a; 将所有请求转化为同一路径请求&#xff08;方便穿网配置&#xff09;在请求头内标识原来路径&#xff0c;然后在将请求分发给不同服务 AllToOneGatewayFilterFactory import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; impor…...

根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:

根据万维钢精英日课6的内容&#xff0c;使用AI&#xff08;2025&#xff09;可以参考以下方法&#xff1a; 四个洞见 模型已经比人聪明&#xff1a;以ChatGPT o3为代表的AI非常强大&#xff0c;能运用高级理论解释道理、引用最新学术论文&#xff0c;生成对顶尖科学家都有用的…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

算法:模拟

1.替换所有的问号 1576. 替换所有的问号 - 力扣&#xff08;LeetCode&#xff09; ​遍历字符串​&#xff1a;通过外层循环逐一检查每个字符。​遇到 ? 时处理​&#xff1a; 内层循环遍历小写字母&#xff08;a 到 z&#xff09;。对每个字母检查是否满足&#xff1a; ​与…...

CSS | transition 和 transform的用处和区别

省流总结&#xff1a; transform用于变换/变形&#xff0c;transition是动画控制器 transform 用来对元素进行变形&#xff0c;常见的操作如下&#xff0c;它是立即生效的样式变形属性。 旋转 rotate(角度deg)、平移 translateX(像素px)、缩放 scale(倍数)、倾斜 skewX(角度…...