前言
设计模式的重要性是无需多言的 几乎各个有名有姓的框架和组件都使用了大量的设计模式 但是 它们是组件或框架 也就是比较底层的地方 对于我们的业务而言 怎么来使用设计模式来让我们的业务项目变得更优秀呢?
这里我会以一个审核模块的设计和实现来让大家将设计模式应用起来
准备
前置知识
本文依旧需要一些前置知识来获得更好的阅读体验
- springboot基础
- 阿里云服务的使用
- 策略和工厂模式的了解
- 事件机制的了解
- MybatisPlus的使用(可以没有这个 本文使用到MP的可替换为其他实现)
阿里云审核服务
审核的实现我们依赖阿里云 因此 在进行之前 读者需要保证自己要有一个阿里云账号 并开启审核相关云服务 这点如果不了解 可以自行去问通义(阿里家的ai) 可以帮助你完成这一步
通义:通义
Maven依赖
作为一个SpringBoot项目中的一部分 模块依赖于下面的依赖坐标
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/>
</parent>
<!--这里就不需要复制了 只复制下面的dependencies即可-->
<groupId>org.fuys</groupId>
<artifactId>fuys-low-coder</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<!--springboot web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--测试依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--阿里云生成凭证依赖-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>credentials-java</artifactId>
<version>LATEST</version>
</dependency>
<!--阿里云OSS依赖-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.1.1</version>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>2.8.3</version>
</dependency>
<!--阿里云内容审核依赖-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-green</artifactId>
<version>3.6.6</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>green20220302</artifactId>
<version>2.2.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
</dependency>
</dependencies>
</project>
流程图
通过这样一个流程 不难解释为什么这样会让整个模块更有拓展性 因为如果之后需要新增新的审核类型 只需要增加 审核任务类型的枚举 和 编写新的审核策略类 以及 审核结果处理类即可 完全不需要更改其他代码 要啥就写啥
编码
定义审核处理过程需要的实体类
既然是审核相关的 那么为了方便进行审核的处理 我们可以定义一个实体类 用于封装需要审核的任务实体
package org.fuys.coder.domain.audit.model.req;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.function.Function;
/**
* @description: 审核任务
* @date: 2024/6/22 16:53
* @version: 1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AuditTask {
//审核任务类型 不同的审核任务类型在结果策略的执行将会不同
private int type;
//审核结果是否需要处理 如果不需要处理 则不处理
private boolean needResultHandle;
//否则 如果需要处理 则执行这个任务需要的执行的回调 这部分由编码者定义
private Function<Void,Boolean> callback;
//审核的任务 为了简化演示 统一设置成字符串(图片和视频将使用oss的url来进行审核)
private String[] tasks;
//杂项 审核过程中需要的补充信息
private Integer userId;
private Integer idType;
private Long otherId;
}
package org.fuys.coder.domain.audit.model.res;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.fuys.coder.domain.audit.model.vo.AuditResultLabels;
import org.fuys.coder.domain.audit.model.vo.AuditResultTypeVO;
/**
* @description: 审核结果
* @date: 2024/6/22 16:20
* @version: 1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AuditResult {
//任务类型
private int type;
//返回信息
private String msg;
//是否结束 有些异步的审核 这个属性将为false
private boolean finish;
//任务id 如果一个审核任务是组合式任务 则需要任务id以标识整个审核任务的进行
private String taskId;
//静态方法 用于快速构建正常审核结果
public static AuditResult normal(String ...id){
return new AuditResult(AuditResultTypeVO.PASS.getIndex()
, AuditResultLabels.NORMAL.getDesc(),true,id.length==0?null:id[0]);
}
//用于快速构建异常审核结果
public static AuditResult abnormal(int type,String labels,String ...id){
final String[] split = labels.split(",");
StringBuilder stringBuilder=new StringBuilder();
for (String s : split) {
stringBuilder.append(AuditResultLabels.getDesc(s));
stringBuilder.append(",");
}
final String msg= stringBuilder.toString();
stringBuilder.deleteCharAt(msg.length()-1);
return new AuditResult(type,msg,true,id.length==0?null:id[0]);
}
}
此外 相信读者在看的时候 也会发现上面用到了很多枚举 这些枚举只是为了标识和分类 这里给出它们的定义 不再详细解释 具体的解释会在代码的注释中
package org.fuys.coder.domain.audit.model.vo;
/**
* @description: 审核结果类型视图类
* @date: 2024/6/22 16:23
* @version: 1.0
*/
public enum AuditResultTypeVO {
PASS(1){
public String getStrategy() {
return "passStrategy";
}
},
BLOCK(2){
public String getStrategy(){
return "blockStrategy";
}
},
REVIEW(3){
public String getStrategy(){
return "reviewStrategy";
}
},
FAILED(4){
public String getStrategy(){
return "failedStrategy";
}
},
ONGOING(5){
public String getStrategy(){
return "ongoingStrategy";
}
};
int index;
AuditResultTypeVO(int index) {
this.index = index;
}
public int getIndex() {
return index;
}
//获取策略对应的bean名称 方便之后ioc利用bean名称注入实例
public String getStrategy(){
return null;
}
public static AuditResultTypeVO getByIdx(int index) {
return AuditResultTypeVO.values()[index-1];
}
}
package org.fuys.coder.domain.audit.model.vo;
/**
* @description: 审核调用之后的建议
* @date: 2024/6/23 11:38
* @version: 1.0
*/
public enum AuditResultSuggestion {
PASS("pass"){
@Override
public int getType() {
return 1;
}
},
BLOCK("block"){
@Override
public int getType() {
return 2;
}
},
REVIEW("review"){
@Override
public int getType() {
return 3;
}
};
String desc;
AuditResultSuggestion(String desc) {
this.desc=desc;
}
public int getType(){
return 0;
}
public static int getType(String desc){
final AuditResultSuggestion[] values = AuditResultSuggestion.values();
for (AuditResultSuggestion value : values) {
if(value.desc.equals(desc)){
return value.getType();
}
}
return 0;
}
}
package org.fuys.coder.domain.audit.model.vo;
/**
* @description: 违规标签枚举
* @date: 2024/6/23 11:54
* @version: 1.0
*/
public enum AuditResultLabels {
NORMAL("normal"){
@Override
public String getDesc() {
return "正常";
}
},
SPAM("spam"){
@Override
public String getDesc() {
return "含有垃圾信息";
}
},
AD("ad"){
@Override
public String getDesc() {
return "存在广告行为";
}
},
POLITICS("politics"){
@Override
public String getDesc() {
return "存在涉政嫌疑";
}
},
TERRORISM("violence"){
@Override
public String getDesc() {
return "存在暴恐内容";
}
},
ABUSE("abuse"){
@Override
public String getDesc() {
return "存在辱骂内容";
}
},
PORN("porn"){
@Override
public String getDesc() {
return "存在色情内容";
}
},
FLOOD("flood"){
@Override
public String getDesc() {
return "存在无营养内容";
}
},
CONTRABAND("contraband"){
@Override
public String getDesc() {
return "存在违禁内容";
}
},
MEANINGLESS("meaningless"){
@Override
public String getDesc() {
return "存在无意义内容";
}
},
HARMFUL("harmful"){
@Override
public String getDesc() {
return "存在不良导向";
}
};
String desc;
AuditResultLabels(String desc) {
this.desc=desc;
}
public String getDesc(){
return "";
}
public static String getDesc(String label){
final AuditResultLabels[] values = AuditResultLabels.values();
for (AuditResultLabels value : values) {
if(value.desc.equals(label)){
return value.getDesc();
}
}
return "";
}
public static String like(String label){
final AuditResultLabels[] values = AuditResultLabels.values();
for (AuditResultLabels value : values) {
if(value.desc.startsWith(label)){
return value.getDesc();
}
}
return "";
}
}
package org.fuys.coder.domain.audit.model.vo;
/**
* @description: 审核任务类型
* @date: 2024/6/22 17:15
* @version: 1.0
*/
public enum AuditTaskTypeVO {
TEXT(1){
public String getStrategy() {
return "textAuditStrategy";
}
},
IMAGE(2){
public String getStrategy(){
return "imageAuditStrategy";
}
},
MULTIPLE(3){
public String getStrategy(){
return "multipleAuditStrategy";
}
};
int index;
AuditTaskTypeVO(int index) {
this.index = index;
}
public int getIndex() {
return index;
}
public String getStrategy(){
return null;
}
public static AuditTaskTypeVO getByIdx(int index) {
return AuditTaskTypeVO.values()[index-1];
}
}
package org.fuys.coder.domain.audit.model.vo;
/**
* @description: 审核任务中的类型id 是用于标识此任务属于哪个类型(用户?)
* @date: 2024/6/30 22:04
* @version: 1.0
*/
public enum AuditTaskIdTypeVO {
INFO(1){
public String getDesc() {
return "信息相关";
}
},
SUBSTANCE(2){
public String getDesc(){
return "内容相关";
}
},
IMAGE(3){
public String getDesc(){
return "图片相关";
}
};
int index;
AuditTaskIdTypeVO(int index) {
this.index = index;
}
public int getIndex() {
return index;
}
public String getDesc(){
return null;
}
public static AuditTaskIdTypeVO getByIdx(int index) {
return AuditTaskIdTypeVO.values()[index-1];
}
}
package org.fuys.coder.domain.audit.model.vo;
/**
* @description: 复审的审核状态
* @date: 2024/7/1 16:48
* @version: 1.0
*/
public enum AuditReviewStatusVO {
PASS(1){
public String getDesc() {
return "passStrategy";
}
},
BLOCK(2){
public String getDesc(){
return "blockStrategy";
}
},
REVIEW(3){
public String getDesc(){
return "reviewStrategy";
}
},
FAILED(4){
public String getDesc(){
//todo 设置审核出错失败策略
return "failedStrategy";
}
},
ONGOING(5){
public String getDesc(){
return "ongoingStrategy";
}
};
int index;
AuditReviewStatusVO(int index) {
this.index = index;
}
public int getIndex() {
return index;
}
public String getDesc(){
return null;
}
public static AuditReviewStatusVO getByIdx(int index) {
return AuditReviewStatusVO.values()[index-1];
}
}
定义审核策略
和之前的提到一样 我们从下向上来进行设计 先写出各个审核策略的逻辑 为了统一管理 定义一个接口 不同种类的策略为它的实现
package org.fuys.coder.domain.audit.service.policy.audit;
import org.fuys.coder.domain.audit.model.req.AuditTask;
import org.fuys.coder.domain.audit.model.res.AuditResult;
/**
* @description: 审核策略接口 适合不同的审核任务 采用策略组合的方式进行审核
* @date: 2024/6/22 16:09
* @version: 1.0
*/
public interface AuditStrategy {
AuditResult execute(AuditTask auditTask);
}
文字审核策略
由于文字审核较快 我们直接就可以返回文本的审核的结果
package org.fuys.coder.domain.audit.service.policy.audit.impl;
import com.alibaba.fastjson2.JSONObject;
import com.aliyun.green20220302.Client;
import com.aliyun.green20220302.models.TextModerationRequest;
import com.aliyun.green20220302.models.TextModerationResponse;
import com.aliyun.green20220302.models.TextModerationResponseBody;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
import org.fuys.coder.common.config.AliYunOssConfig;
import org.fuys.coder.common.config.app.CoderAuditConfig;
import org.fuys.coder.domain.audit.model.req.AuditTask;
import org.fuys.coder.domain.audit.model.res.AuditResult;
import org.fuys.coder.domain.audit.model.vo.AuditResultLabels;
import org.fuys.coder.domain.audit.model.vo.AuditResultTypeVO;
import org.fuys.coder.domain.audit.service.policy.audit.AuditStrategy;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
/**
* @description: 文本审核策略
* @date: 2024/6/22 16:50
* @version: 1.0
*/
@Service("textAuditStrategy")
public class TextAuditStrategy implements AuditStrategy {
@Resource
//设置项目中审核的一些配置 配置类定义我会放到下方
private CoderAuditConfig config;
@Resource
//配置你自己的阿里云配置 配置类定义我会放到下方
private AliYunOssConfig ossConfig;
private Client client;
@PostConstruct
public void initClient(){
Config config=new Config();
config.setAccessKeyId(ossConfig.getAccessKeyId());
config.setAccessKeySecret(ossConfig.getAccessKeySecret());
config.setRegionId(ossConfig.getRegion());
//选择你自己的设置
config.setEndpoint("green-cip.cn-beijing.aliyuncs.com");
//设置响应超时时间
config.setReadTimeout(6000);
config.setConnectTimeout(3000);
try {
client=new Client(config);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public AuditResult execute(AuditTask auditTask) {
final String[] texts = auditTask.getTasks();
//注意: 阿里云文字审核不支持10000字以上 因此传递审核文字时 对文字进行切片 之后合并结果
if(texts.length>1) {
List<AuditResult> list=new ArrayList<>();
for (String text : texts) {
final AuditResult auditResult = doAudit(text);
list.add(auditResult);
}
return merge(list);
}
return doAudit(texts[0]);
}
private AuditResult merge(List<AuditResult> list){
if(list.size()==1){
return list.get(0);
}
AuditResult result=AuditResult.normal();
for (AuditResult auditResult : list) {
if(auditResult.getType()!=AuditResultTypeVO.PASS.getIndex()){
return auditResult;
}
}
return result;
}
private AuditResult doAudit(String text){
RuntimeOptions runtimeOptions=new RuntimeOptions();
runtimeOptions.readTimeout=10000;
runtimeOptions.connectTimeout=10000;
//构造检测参数
JSONObject serviceParameters=new JSONObject();
text=text.trim();
if(text.length()==0){
return AuditResult.normal();
}
serviceParameters.put("content",text);
TextModerationRequest textModerationRequest=new TextModerationRequest();
//文本检测的service模型代码
textModerationRequest.setService(config.getTextService());
textModerationRequest.setServiceParameters(serviceParameters.toJSONString());
//调用客户端进行检测
try{
final TextModerationResponse response = client.textModerationWithOptions(textModerationRequest, runtimeOptions);
if(ObjectUtils.isEmpty(response)){
//这里可以加上你自己的调用失败处理
return null;
}else{
//调用成功 解析响应
if (200==response.getStatusCode()) {
final TextModerationResponseBody result = response.getBody();
final Integer code = result.getCode();
if(!ObjectUtils.isEmpty(code)&&code==200){
//获取响应和它返回的结果
TextModerationResponseBody.TextModerationResponseBodyData data=result.getData();
final String labels = data.getLabels();
//解析违规标签信息
if (!labels.equals("")){
return AuditResult.abnormal(AuditResultTypeVO.BLOCK.getIndex(),
labels);
}else{
return AuditResult.normal();
}
}
}else{
//这里可以加上你自己的调用失败处理
return null;
}
}
}catch (Exception e){
//这里可以加上你自己的调用失败处理
e.printStackTrace();
}
return null;
}
}
图片审核策略
相比于文字审核类 我们的图片审核类需要异步操作 因为它较为耗时 这里的图片审核是和阿里云oss进行绑定的 也就是说我们需要传递文件的文件名(fileKey) 区域id 以及 oss的配置 方便审核服务获取图片信息
异步的流程是怎样的呢? 是通过事件发布机制 当策略拿到一个审核任务 并不会开始调用审核接口 而是直接返回一个审核正在进行的审核结果
审核任务交给策略中定义的线程池进行工作 当任务进行完毕时 无论结果如何 将通过事件传递 被充当上下文的审核调用接口进行监听并处理(下文会讲解这个审核调用接口的实现类)
package org.fuys.coder.domain.audit.service.policy.audit.impl;
import com.alibaba.fastjson2.JSON;
import com.aliyun.green20220302.Client;
import com.aliyun.green20220302.models.ImageModerationRequest;
import com.aliyun.green20220302.models.ImageModerationResponse;
import com.aliyun.green20220302.models.ImageModerationResponseBody;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
import org.fuys.coder.common.config.AliYunOssConfig;
import org.fuys.coder.common.config.app.CoderAuditConfig;
import org.fuys.coder.domain.audit.model.event.AsyncAuditEvent;
import org.fuys.coder.domain.audit.model.event.AsyncTaskIdHolder;
import org.fuys.coder.domain.audit.model.req.AuditTask;
import org.fuys.coder.domain.audit.model.res.AuditResult;
import org.fuys.coder.domain.audit.model.vo.AuditResultLabels;
import org.fuys.coder.domain.audit.model.vo.AuditResultTypeVO;
import org.fuys.coder.domain.audit.service.policy.audit.AuditStrategy;
import org.fuys.coder.domain.audit.service.publisher.AsyncAuditEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.*;
/**
* @description: 图片审核策略 这里从AuditTask中拿出来的应该是一个字符串url
* @date: 2024/6/22 17:02
* @version: 1.0
*/
@Service("imageAuditStrategy")
public class ImageAuditStrategy implements AuditStrategy {
@Resource
private CoderAuditConfig config;
@Resource
private AliYunOssConfig ossConfig;
@Resource
private AsyncAuditEventPublisher publisher;
private Client client;
private ExecutorService threadPoolExecutor;
@PostConstruct
public void initAuditStrategy(){
try {
Config clientConfig=new Config();
clientConfig.setAccessKeyId(ossConfig.getAccessKeyId());
clientConfig.setAccessKeySecret(ossConfig.getAccessKeySecret());
clientConfig.setEndpoint("green-cip.cn-beijing.aliyuncs.com");
this.client=new Client(clientConfig);
threadPoolExecutor= Executors.newFixedThreadPool(config.getImageAuditCores());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public AuditResult execute(AuditTask task) {
final String[] keys = task.getTasks();
//为了保持统一 设置keys的第一个参数为真正的fileKey
String taskId=null;
if(keys.length>1){
taskId=keys[1];
}else{
taskId=UUID.randomUUID().toString();
AsyncTaskIdHolder.setTaskInfo(taskId,task);
}
AuditResult auditResult=new AuditResult();
auditResult.setType(AuditResultTypeVO.ONGOING.getIndex());
auditResult.setFinish(false);
auditResult.setTaskId(taskId);
doAudit(keys[0], auditResult);
return auditResult;
}
private void doAudit(String key,AuditResult auditResult){
//阿里sdk的操作步骤
Callable<AuditResult> callable=()->{
try {
RuntimeOptions runtimeOptions=new RuntimeOptions();
Map<String,String> serviceParas=new HashMap<>();
serviceParas.put("dataId", UUID.randomUUID().toString());
serviceParas.put("ossRegionId", ossConfig.getRegion());
serviceParas.put("ossBucketName",ossConfig.getBucketName());
serviceParas.put("ossObjectName",key);
ImageModerationRequest request=new ImageModerationRequest();
request.setService(config.getImageService());
request.setServiceParameters(JSON.toJSONString(serviceParas));
final ImageModerationResponse imageModerationResponse = client.imageModerationWithOptions(request, runtimeOptions);
if(!ObjectUtils.isEmpty(imageModerationResponse)){
if(imageModerationResponse.getStatusCode()==200){
final ImageModerationResponseBody body = imageModerationResponse.getBody();
if(body.getCode()==200){
final ImageModerationResponseBody.ImageModerationResponseBodyData data = body.getData();
final List<ImageModerationResponseBody.ImageModerationResponseBodyDataResult> result = data.getResult();
for (ImageModerationResponseBody.ImageModerationResponseBodyDataResult imageModerationResponseBodyDataResult : result) {
final String label = imageModerationResponseBodyDataResult.getLabel();
if("nonLabel".equals(label)){
//如果没有标签被设置 说明还是一切正常
continue;
}
//否则 如果存在标签被匹配了 那么说明存在嫌疑点
final String like = AuditResultLabels.like(label);
if(!"".equals(like)){
if(imageModerationResponseBodyDataResult.getConfidence()>config.getImageConfidence()){
auditResult.setType(AuditResultTypeVO.BLOCK.getIndex());
auditResult.setMsg(like);
auditResult.setFinish(true);
return auditResult;
}
}
}
}
auditResult.setType(AuditResultTypeVO.PASS.getIndex());
auditResult.setMsg(AuditResultLabels.NORMAL.getDesc());
auditResult.setFinish(true);
return auditResult;
}
}
return null;
} catch (Exception e) {
e.printStackTrace();
return null;
}
};
final CompletableFuture<AuditResult> auditResultCompletableFuture = CompletableFuture.supplyAsync(() -> {
try {
return callable.call();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}, threadPoolExecutor);
//设置事件发布 这里将发布异步审核完成
auditResultCompletableFuture.thenApply(asyncAuditResult -> {
AsyncAuditEvent event=new AsyncAuditEvent(this);
event.setAuditResult(asyncAuditResult);
publisher.publishAuditEvent(event);
return asyncAuditResult;
});
}
}
混合审核策略
对于多种组合的混合审核 如内容的封面和标题混合 用户更新主页的头像和介绍这种混合 我们单独设立一个类进行处理
为了标识这些小任务点(文字内容和图片内容)属于同一个大任务 我们设置一个taskId 也就是唯一任务id 来通过事件发送机制结合原子计数来标识这样一个大任务完毕 具体来说 如果是一个混合任务类型 我们将生成一个taskId(实为UUID) 这些小任务都将用这个taskId来标识自己 对于异步的图片审核操作 它在审核完成后会发送审核结束的事件 由上下文监听使用此事件 然后进行任务完成数量比对 如果此taskId额定的任务数量已经全部完成 则发送任务结束事件给下面这个类 混合策略类将合并这些审核结果 并最终发送任务结果事件交给上下文进行结果的分析处理
package org.fuys.coder.domain.audit.service.policy.audit.impl;
import org.fuys.coder.common.config.app.CoderAuditConfig;
import org.fuys.coder.domain.audit.model.event.AsyncAuditEvent;
import org.fuys.coder.domain.audit.model.event.AsyncTaskIdHolder;
import org.fuys.coder.domain.audit.model.event.AsyncTasksOverEvent;
import org.fuys.coder.domain.audit.model.req.AuditTask;
import org.fuys.coder.domain.audit.model.res.AuditResult;
import org.fuys.coder.domain.audit.model.vo.AuditResultTypeVO;
import org.fuys.coder.domain.audit.model.vo.AuditTaskTypeVO;
import org.fuys.coder.domain.audit.service.policy.audit.AuditStrategy;
import org.fuys.coder.domain.audit.service.publisher.AsyncAuditEventPublisher;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @description: 内容审核策略
* @date: 2024/6/22 17:20
* @version: 1.0
*/
@Service("multipleAuditStrategy")
public class MultipleAuditStrategy implements AuditStrategy {
@Resource
private ImageAuditStrategy imageAuditStrategy;
@Resource
private TextAuditStrategy textAuditStrategy;
@Resource
private CoderAuditConfig config;
@Resource
private AsyncAuditEventPublisher publisher;
private ExecutorService threadPoolExecutor;
@PostConstruct
public void initAuditStrategy(){
threadPoolExecutor= Executors.newFixedThreadPool(config.getSubstanceAuditCores());
}
@Override
public AuditResult execute(AuditTask task) {
final String[] strings = task.getTasks();
//这里对于内容的审核 则是根据|进行切分 这也没办法 只能使用这种方式来完成策略模式了
List<String> textList=new ArrayList<>();
List<String> imageList=new ArrayList<>();
boolean flag=false;
for (String string : strings) {
if(!flag){
//如果没发现"|" 则说明是文本 一般可能是 简介 标题 内容等
if(string.equals("|")){
flag=true;
continue;
}
textList.add(string);
}else{
imageList.add(string);
}
}
//构造审核结果
AuditResult auditResult=AuditResult.normal();
doAudit(textList,imageList,task);
return null;
}
private void doAudit(List<String> textList,List<String> imageList,AuditTask multipleTask){
//这一步将记录每个内容对应的需要异步审核的数量 当在上下文中发现一个任务id对应的异步审核任务完成的数量和记录所需要的数量匹配
//则发送事件 这里这个内容的事件监听器可以获取到这个事件 从而达成解耦操作
//具体来说 在上下文中 如果发现一个任务id对应的异步审核任务完成的数量和记录所需要的数量匹配
//则 上下文发布合并审核结果事件 然后 由多种审核类型的策略监听此事件并处理
//由于在此之前 这里已经拿到了这些审核结果的引用 因此可以直接进行合并操作
//下面的操作应该放入线程池 但是为了调试方便 这里先不放入
final String taskId = UUID.randomUUID().toString();
AsyncTaskIdHolder.setTaskInfo(taskId,multipleTask);
List<AuditResult> auditResults=new LinkedList<>();
for (String s : textList) {
AuditTask auditTask=new AuditTask();
auditTask.setTasks(new String[]{s});
auditTask.setType(AuditTaskTypeVO.TEXT.getIndex());
auditTask.setUserId(multipleTask.getUserId());
auditTask.setOtherId(multipleTask.getOtherId());
auditTask.setIdType(multipleTask.getIdType());
auditTask.setNeedResultHandle(false);
final AuditResult execute = textAuditStrategy.execute(auditTask);
auditResults.add(execute);
}
for (String s : imageList) {
AuditTask auditTask=new AuditTask();
auditTask.setTasks(new String[]{s, taskId});
auditTask.setType(AuditTaskTypeVO.IMAGE.getIndex());
auditTask.setUserId(multipleTask.getUserId());
auditTask.setOtherId(multipleTask.getOtherId());
auditTask.setIdType(multipleTask.getIdType());
auditTask.setNeedResultHandle(false);
final AuditResult execute = imageAuditStrategy.execute(auditTask);
auditResults.add(execute);
}
AsyncTaskIdHolder.setTasks(taskId, auditResults,imageList.size());
}
//合并这些审核结果
private AuditResult summary(String taskId,List<AuditResult> auditResults){
AsyncTaskIdHolder.removeMultipleTask(taskId);
//注意 这里不传递任务id了 因为到这一步代表着多种审核任务已经完成 不属于某个任务的子任务了
final AuditResult normal = AuditResult.normal();
auditResults.forEach(auditResult -> {
//如果不是通过状态 说明肯定是哪里存在问题
if(auditResult.getType()==AuditResultTypeVO.REVIEW.getIndex()){
//这里需要人工复检 进行相关操作
}
if(auditResult.getType()!=AuditResultTypeVO.BLOCK.getIndex()){
//todo 这里说明审核不通过 记录全部的违规信息 或进行其他操作
}
});
return normal;
}
@Component
public class AsyncTasksOverListener implements ApplicationListener<AsyncTasksOverEvent> {
@Override
public void onApplicationEvent(AsyncTasksOverEvent event) {
final String taskId = event.getTaskId();
final List<AuditResult> tasks = AsyncTaskIdHolder.getTasks(taskId);
final AuditResult summary = summary(taskId,tasks);
final AsyncAuditEvent okEvent = new AsyncAuditEvent(this);
okEvent.setAuditResult(summary);
okEvent.setAuditTask(AsyncTaskIdHolder.getTaskInfo(taskId));
AsyncTaskIdHolder.removeMultipleTask(taskId);
publisher.publishAuditEvent(okEvent);
}
}
}
package org.fuys.coder.domain.audit.model.event;
import org.fuys.coder.common.entity.Pair;
import org.fuys.coder.domain.audit.model.req.AuditTask;
import org.fuys.coder.domain.audit.model.res.AuditResult;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @description: 用于保存内容审核的各个块的审核结果
* @date: 2024/6/24 11:26
* @version: 1.0
*/
public class AsyncTaskIdHolder {
//记录任务和其需要的审核结果引用
private static Map<String, List<AuditResult>> taskMap=new ConcurrentHashMap<>();
private static Map<String, Pair<Integer,AtomicInteger>> taskNumsMap=new ConcurrentHashMap<>();
private static Map<String, AuditTask> taskInfoMap=new ConcurrentHashMap<>();
public static void setTasks(String id, List<AuditResult> tasks, Integer allNums){
taskMap.put(id,tasks);
taskNumsMap.put(id,new Pair<>(allNums,new AtomicInteger(0)));
}
public static List<AuditResult> getTasks(String id){
return taskMap.get(id);
}
public static Integer getTaskNowNums(String id){
return taskNumsMap.get(id).getRight().get();
}
public static Integer incrementTaskNowNums(String id){
return taskNumsMap.get(id).getRight().incrementAndGet();
}
public static Integer getTaskAllNums(String id){
return taskNumsMap.get(id).getLeft();
}
public static void removeMultipleTask(String id){
taskNumsMap.remove(id);
taskMap.remove(id);
}
public static void setTaskInfo(String id,AuditTask task){
taskInfoMap.put(id,task);
}
public static AuditTask getTaskInfo(String id){
return taskInfoMap.get(id);
}
public static void removeTaskInfo(String id){
taskInfoMap.remove(id);
}
}
定义审核调用接口
审核调用的接口就很简单了 如下 接收一个审核任务 返回一个审核结果
package org.fuys.coder.domain.audit.service;
import org.fuys.coder.domain.audit.model.req.AuditTask;
import org.fuys.coder.domain.audit.model.res.AuditResult;
/**
* @description: 审核服务接口 即调用策略的接口
* @date: 2024/6/22 16:52
* @version: 1.0
*/
public interface Audit {
AuditResult doAudit(AuditTask auditTask);
}
定义组合审核任务实现类
这就是我们审核模块的绝对核心 它总的作用就是对接 使用可以直接注入 并调用doAudit方法即可使用 而它内部又对接了审核策略 以及处理审核结果的逻辑 我们直接上代码
package org.fuys.coder.domain.audit.service.impl;
import org.fuys.coder.domain.audit.model.event.AsyncAuditEvent;
import org.fuys.coder.domain.audit.model.event.AsyncTaskIdHolder;
import org.fuys.coder.domain.audit.model.event.AsyncTasksOverEvent;
import org.fuys.coder.domain.audit.model.req.AuditTask;
import org.fuys.coder.domain.audit.model.res.AuditResult;
import org.fuys.coder.domain.audit.model.vo.AuditResultTypeVO;
import org.fuys.coder.domain.audit.model.vo.AuditTaskTypeVO;
import org.fuys.coder.domain.audit.service.Audit;
import org.fuys.coder.domain.audit.service.policy.audit.AuditStrategy;
import org.fuys.coder.domain.audit.service.policy.result.AuditResultStrategy;
import org.fuys.coder.domain.audit.service.publisher.AsyncAuditEventPublisher;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import javax.annotation.Resource;
import javax.validation.constraints.NotNull;
import java.util.Map;
/**
* @description: 动态组合审核策略进行审核
* @date: 2024/6/22 17:07
* @version: 1.0
*/
@Service("dynamicAuditServiceImpl")
public class DynamicAuditServiceImpl implements Audit {
private final Map<String, AuditStrategy> strategyMap;
private final Map<String, AuditResultStrategy> resultStrategyMap;
@Resource
private AsyncAuditEventPublisher publisher;
@Autowired
public DynamicAuditServiceImpl(ListableBeanFactory beanFactory){
//通过IOC来保存已经存在的策略
final Map<String, AuditStrategy> beansOfType = beanFactory.getBeansOfType(AuditStrategy.class);
resultStrategyMap = beanFactory.getBeansOfType(AuditResultStrategy.class);
strategyMap=beansOfType;
}
@Override
public AuditResult doAudit(AuditTask auditTask) {
//这里只负责指挥和调配 也就是说 线程池进行审核的处理需要在具体的逻辑中进行
String strategy = getTaskStrategy(auditTask);
//获取审核类型 以及对应的策略实例
if(strategyMap.containsKey(strategy)){
final AuditStrategy auditStrategy = strategyMap.get(strategy);
final AuditResult execute = auditStrategy.execute(auditTask);
if(!ObjectUtils.isEmpty(execute)&&execute.isFinish()&&auditTask.isNeedResultHandle()) {
//返回审核的结果 并且在这里进行处理审核结果(如果此任务需要的话)
this.resultHandle(execute,auditTask);
}else{
if(ObjectUtils.isEmpty(execute)&&auditTask.getType()!=AuditTaskTypeVO.MULTIPLE.getIndex()){
//否则则是因为种种原因审核失败 交给人工复审
final AuditResultStrategy review = this.resultStrategyMap.get(AuditResultTypeVO.REVIEW.getStrategy());
review.handleResult(execute,auditTask);
}
//如果结果非空 说明一定是此审核任务没有完成 也就说是一个异步审核请求 有可能是和内容审核相关 有可能只是一个大图片审核
}
return execute;
}
return null;
}
private void resultHandle(AuditResult execute,AuditTask task) {
//确定审核任务结束了
if(execute.isFinish()&&task.isNeedResultHandle()) {
final String resultStrategy = this.getResultStrategy(execute);
if (resultStrategyMap.containsKey(resultStrategy)) {
final AuditResultStrategy auditResultStrategy = resultStrategyMap.get(resultStrategy);
auditResultStrategy.handleResult(execute,task);
} else {
//审核结束之后的逻辑
}
}
}
private String getTaskStrategy(AuditTask auditTask){
return AuditTaskTypeVO.getByIdx(auditTask.getType()).getStrategy();
}
private String getResultStrategy(AuditResult auditResult){
return AuditResultTypeVO.getByIdx(auditResult.getType()).getStrategy();
}
@Component
public class AsyncAuditEventListener implements ApplicationListener<AsyncAuditEvent>{
@Override
public void onApplicationEvent(AsyncAuditEvent event) {
final AuditResult auditResult = event.getAuditResult();
final String taskId = auditResult.getTaskId();
if(!ObjectUtils.isEmpty(taskId)){
//如果任务id非空 说明它是一个任务的子任务 将其归位
final Integer taskNowNums = AsyncTaskIdHolder.incrementTaskNowNums(taskId);
if(taskNowNums.equals(AsyncTaskIdHolder.getTaskAllNums(taskId))){
AsyncTasksOverEvent overEvent=new AsyncTasksOverEvent(this);
overEvent.setTaskId(taskId);
publisher.publishTaskOverEvent(overEvent);
return;
}
return;
}
resultHandle(auditResult,event.getAuditTask());
}
}
}
测试
这是我在之前写的一个项目中 在发布内容逻辑使用此审核模块的示例
@Override
@Transactional(rollbackFor = NeedCallBackException.class)
public void publishSubstance(SubstancePublishReq req) {
final String redisKey = RedisConstants.REDIS_FIELD_USER + RedisConstants.REDIS_USE_PUBLISH_TOKEN +
RedisConstants.REDIS_SPLIT + req.getAuthorId();
final boolean flag = tokenService.checkToken(req.getAuthorId(), null, TokenTypeVO.SUBSTANCE_PUBLISH, req.getToken());
if(!flag){
throw new BusinessException(ResultMessageConstants.ILLEGAL_OPERATION);
}
//首先进行插入数据库操作 插入成功与否 都删除此次发布内容的合法token
Long substanceId=substanceRepository.addSubstance(req);
//构建审核任务 此条记录最后是否会被删除取决于审核的结果
AuditTask auditTask=new AuditTask();
List<String> tasks=new ArrayList<>();
tasks.add(req.getTitle());
tasks.add(req.getIntroduce());
tasks.add("|");
tasks.addAll(this.getFileKey(new ArrayList<>(req.getFileMap().values())));
auditTask.setType(AuditTaskTypeVO.MULTIPLE.getIndex());
auditTask.setUserId(req.getAuthorId());
auditTask.setIdType(AuditTaskIdTypeVO.SUBSTANCE.getIndex());
auditTask.setOtherId(substanceId);
auditTask.setNeedResultHandle(true);
auditTask.setTasks(tasks.toArray(new String[0]));
auditTask.setCallback((unused -> {
if(req.getFollowers()<=recommendConfig.getPushCount()){
//通知或其他操作
}
//设置审核通过状态
substanceRepository.setSubstanceStatusById(substanceId, AuditResultTypeVO.PASS);
return null;
}));
audit.doAudit(auditTask);
}
经过前端发送请求后 是可以正常使用的
总结
通过事件监听和策略模式的使用 我们编码出了一个可以方便拓展的审核模块 还初步了解了审核功能的调用 以及设计模式的使用 其实在大部分项目中 策略+工厂模式就足够使用 一般可以再加上模板方法模式
希望各位读者可以获取到提升自己的知识 也欢迎不同想法的讨论 最后 如果觉得对你有帮助 点个关注吧~