diff --git a/src/main/java/com/youlai/boot/admin/job/AsyncAuditPollJob.java b/src/main/java/com/youlai/boot/admin/job/AsyncAuditPollJob.java
new file mode 100644
index 0000000..3e00083
--- /dev/null
+++ b/src/main/java/com/youlai/boot/admin/job/AsyncAuditPollJob.java
@@ -0,0 +1,32 @@
+package com.youlai.boot.admin.job;
+
+import com.youlai.boot.admin.service.AuditExecutorService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * 异步审核结果轮询任务(图片 + 视频)
+ *
+ * 一次 DB 查询同时拉取待处理的图片和视频任务,按类型分流处理并汇总。
+ */
+@Component
+@Slf4j
+@RequiredArgsConstructor
+public class AsyncAuditPollJob {
+
+ private final AuditExecutorService auditExecutorService;
+
+ @Scheduled(fixedDelay = 60000)
+ public void pollAuditResult() {
+ try {
+ int count = auditExecutorService.pollAsyncAuditResults();
+ if (count > 0) {
+ log.info("异步审核轮询完成, 本次处理 {} 条", count);
+ }
+ } catch (Exception e) {
+ log.error("异步审核轮询任务异常", e);
+ }
+ }
+}
diff --git a/src/main/java/com/youlai/boot/admin/job/AuditTimeoutJob.java b/src/main/java/com/youlai/boot/admin/job/AuditTimeoutJob.java
index 09f985f..c29c7cb 100644
--- a/src/main/java/com/youlai/boot/admin/job/AuditTimeoutJob.java
+++ b/src/main/java/com/youlai/boot/admin/job/AuditTimeoutJob.java
@@ -20,7 +20,7 @@ import java.util.concurrent.TimeUnit;
* 扫描长时间停留在 reviewing 状态的审核记录,自动转为人工审核。
* 主要覆盖场景:
* - 机审 API 部分任务调用失败,导致 audit 汇总卡在 reviewing
- * - 视频异步审核长时间未回调
+ * - 图片/视频异步审核长时间未回调
* - 其他异常导致的审核流程中断
*/
@Component
diff --git a/src/main/java/com/youlai/boot/admin/job/VideoAuditPollJob.java b/src/main/java/com/youlai/boot/admin/job/VideoAuditPollJob.java
deleted file mode 100644
index 147bb84..0000000
--- a/src/main/java/com/youlai/boot/admin/job/VideoAuditPollJob.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.youlai.boot.admin.job;
-
-import com.youlai.boot.admin.service.AuditExecutorService;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-
-/**
- * 视频审核异步结果轮询任务
- *
- * 视频审核为异步模式,提交后阿里云返回 taskId,
- * 需定时轮询 {@code videoModerationResult} 查询审核结果并回填。
- */
-@Component
-@Slf4j
-@RequiredArgsConstructor
-public class VideoAuditPollJob {
-
- private final AuditExecutorService auditExecutorService;
-
- /**
- * 每 300 秒轮询一次待处理的视频审核异步结果
- */
- @Scheduled(fixedDelay = 300000)
- public void pollVideoAuditResult() {
- try {
- int count = auditExecutorService.pollVideoAuditResults();
- if (count > 0) {
- log.info("视频审核轮询完成, 本次处理 {} 条", count);
- }
- } catch (Exception e) {
- log.error("视频审核轮询任务异常", e);
- }
- }
-}
diff --git a/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditConfig.java b/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditConfig.java
index bab40b1..2c42307 100644
--- a/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditConfig.java
+++ b/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditConfig.java
@@ -66,5 +66,17 @@ public class MiniContentAuditConfig implements Serializable {
@Schema(description = "逻辑删除标识(0-未删除 1-已删除)")
private Boolean deleted;
+ @TableField("text_service")
+ @Schema(description = "文本审核服务")
+ private String textService;
+
+ @TableField("image_service")
+ @Schema(description = "图片审核服务")
+ private String imageService;
+
+ @TableField("video_service")
+ @Schema(description = "视频审核服务")
+ private String videoService;
+
}
diff --git a/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditTask.java b/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditTask.java
index 55633d9..aabef60 100644
--- a/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditTask.java
+++ b/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditTask.java
@@ -110,5 +110,9 @@ public class MiniContentAuditTask implements Serializable {
@Schema(description = "逻辑删除标识(0-未删除 1-已删除)")
private Boolean deleted;
+ @TableField("service_name")
+ @Schema(description = "使用的审核服务名")
+ private String serviceName;
+
}
diff --git a/src/main/java/com/youlai/boot/admin/service/AuditExecutorService.java b/src/main/java/com/youlai/boot/admin/service/AuditExecutorService.java
index cd52c54..1a88f64 100644
--- a/src/main/java/com/youlai/boot/admin/service/AuditExecutorService.java
+++ b/src/main/java/com/youlai/boot/admin/service/AuditExecutorService.java
@@ -18,10 +18,10 @@ public interface AuditExecutorService {
Map executeAudit(String moduleCode, Long bizId, AuditContentDTO content, String triggerType);
/**
- * 轮询所有待处理的视频审核异步结果,更新任务和汇总状态
+ * 轮询所有待处理的异步审核结果(图片+视频),一次查询,按类型分流处理
*
- * @return 本次处理的视频任务数量
+ * @return 本次处理的任务数量
*/
- int pollVideoAuditResults();
+ int pollAsyncAuditResults();
}
diff --git a/src/main/java/com/youlai/boot/admin/service/ContentAuditTaskService.java b/src/main/java/com/youlai/boot/admin/service/ContentAuditTaskService.java
index fb1b554..2e48270 100644
--- a/src/main/java/com/youlai/boot/admin/service/ContentAuditTaskService.java
+++ b/src/main/java/com/youlai/boot/admin/service/ContentAuditTaskService.java
@@ -17,7 +17,7 @@ public interface ContentAuditTaskService extends IService
/** 查询某个审核汇总下的所有任务明细 */
List listTasksByAuditId(Long auditId);
- /** 查询待轮询的视频审核任务(contentType=video, status=reviewing, taskId不为空) */
- List getPendingVideoTasks();
+ /** 查询待轮询的异步审核任务(contentType in (image, video), status=reviewing, taskId不为空) */
+ List getPendingAsyncTasks();
}
diff --git a/src/main/java/com/youlai/boot/admin/service/impl/AuditExecutorServiceImpl.java b/src/main/java/com/youlai/boot/admin/service/impl/AuditExecutorServiceImpl.java
index 7e6fb63..7dffa8e 100644
--- a/src/main/java/com/youlai/boot/admin/service/impl/AuditExecutorServiceImpl.java
+++ b/src/main/java/com/youlai/boot/admin/service/impl/AuditExecutorServiceImpl.java
@@ -71,7 +71,7 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
// machine:调 AI 并按策略裁决
String strictness = config.getRiskStrategy() != null ? config.getRiskStrategy() : AuditConstants.STRATEGY_NORMAL;
List tasks = contentAuditTaskService.listTasksByAuditId(auditId);
- executeBatchAuditByType(tasks, strictness); // 进入机器审核
+ executeBatchAuditByType(tasks, config, strictness);
List updatedTasks = contentAuditTaskService.listTasksByAuditId(auditId);
return aggregateTaskResultsAndUpdateAudit(auditId, updatedTasks);
}
@@ -94,19 +94,26 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
/**
* 按类型分组批量执行机审
*/
- private void executeBatchAuditByType(List tasks, String strictness) {
+ private void executeBatchAuditByType(List tasks, MiniContentAuditConfig config, String strictness) {
Map> grouped = tasks.stream()
.collect(Collectors.groupingBy(MiniContentAuditTask::getContentType));
+ String textService = config.getTextService() != null ? config.getTextService() : "ugc_moderation_byllm_pro";
+ String imageService = config.getImageService() != null ? config.getImageService() : "baselineCheck";
+ String videoService = config.getVideoService() != null ? config.getVideoService() : "videoDetection";
+
List textTasks = grouped.getOrDefault("text", List.of());
if (!textTasks.isEmpty()) {
- try {
- List contents = textTasks.stream().map(MiniContentAuditTask::getContentValue).toList();
- TextModerationPlusResponse r = aliyunContentAuditUtil.batchTextModerationPlus(contents);
- processBatchTextResponse(textTasks, r, strictness);
- } catch (Exception e) {
- log.error("批量文本审核失败", e);
- for (MiniContentAuditTask task : textTasks) {
+ for (MiniContentAuditTask task : textTasks) {
+ try {
+ TextModerationPlusResponse r = aliyunContentAuditUtil.textModerationPlus(task.getContentValue(), textService);
+ processSingleTextResponse(task, r, strictness);
+ contentAuditTaskService.lambdaUpdate()
+ .eq(MiniContentAuditTask::getId, task.getId())
+ .set(MiniContentAuditTask::getServiceName, textService)
+ .update();
+ } catch (Exception e) {
+ log.error("文本审核失败, taskId={}", task.getId(), e);
contentAuditTaskService.updateTaskMachineResult(
task.getId(), null, null, AuditConstants.STATUS_MANUAL_REVIEW,
null, null, null, null);
@@ -118,10 +125,9 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
if (!imageTasks.isEmpty()) {
for (MiniContentAuditTask task : imageTasks) {
try {
- ImageModerationResponse r = aliyunContentAuditUtil.imageModeration(task.getContentValue());
- processSingleImageResponse(task, r, strictness);
+ handleImageAsyncAudit(task, task.getContentValue(), imageService);
} catch (Exception e) {
- log.error("图片审核失败, taskId={}", task.getId(), e);
+ log.error("图片异步审核提交失败, taskId={}", task.getId(), e);
contentAuditTaskService.updateTaskMachineResult(
task.getId(), null, null, AuditConstants.STATUS_MANUAL_REVIEW,
null, null, null, null);
@@ -131,61 +137,59 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
List videoTasks = grouped.getOrDefault("video", List.of());
for (MiniContentAuditTask task : videoTasks) {
- handleVideoAudit(task, task.getContentValue());
+ handleVideoAudit(task, task.getContentValue(), videoService);
}
}
/**
- * 批量文本审核结果处理(machine 模式:按策略裁决)
+ * 单文本审核结果处理(machine 模式:按策略裁决)
*
- * body.data 结构:{ riskLevel: "high", result: [{ label, confidence, description }, ...] }
+ * data.result[] 可能包含多条,取置信度最高的非 nonLabel 标签。
*/
- private void processBatchTextResponse(List tasks, TextModerationPlusResponse response, String strictness) {
+ private void processSingleTextResponse(MiniContentAuditTask task, TextModerationPlusResponse response, String strictness) {
String machineResultJson = JSON.toJSONString(response);
String requestId = extractRequestIdFromResponse(response);
- // body.data 是对象,不是数组
JSONObject dataObj = extractDataObjectFromResponse(response);
if (dataObj == null) {
- log.info("文本审核返回data为空, requestId={}", requestId);
- for (MiniContentAuditTask task : tasks) {
- contentAuditTaskService.updateTaskMachineResult(
- task.getId(), machineResultJson, null, AuditConstants.STATUS_MANUAL_REVIEW,
- null, null, null, requestId);
- }
+ log.info("文本审核返回data为空, taskId={}, requestId={}", task.getId(), requestId);
+ contentAuditTaskService.updateTaskMachineResult(
+ task.getId(), machineResultJson, null, AuditConstants.STATUS_MANUAL_REVIEW,
+ null, null, null, requestId);
return;
}
- String overallRiskLevel = dataObj.getString("riskLevel");
+ String riskLevel = dataObj.getString("riskLevel");
JSONArray resultArray = dataObj.getJSONArray("result");
- log.info("文本审核结果: requestId={}, riskLevel={}, resultSize={}",
- requestId, overallRiskLevel, resultArray != null ? resultArray.size() : 0);
-
- for (int i = 0; i < tasks.size(); i++) {
- MiniContentAuditTask task = tasks.get(i);
- if (resultArray != null && i < resultArray.size()) {
- JSONObject item = resultArray.getJSONObject(i);
- String label = item.getString("label");
- Integer confidence = item.getInteger("confidence");
- String description = item.getString("description");
- log.info("文本审核[{}]: label={}, confidence={}, description={}", i, label, confidence, description);
-
- if (overallRiskLevel == null) {
- contentAuditTaskService.updateTaskMachineResult(
- task.getId(), machineResultJson, null, AuditConstants.STATUS_MANUAL_REVIEW,
- label, confidence, description, requestId);
- } else {
- String status = applyStrategy(overallRiskLevel, strictness);
- contentAuditTaskService.updateTaskMachineResult(
- task.getId(), machineResultJson, overallRiskLevel, status,
- label, confidence, description, requestId);
- }
- }
+ JSONObject best = pickBestFromResultArray(resultArray);
+
+ String label = null;
+ Integer confidence = null;
+ String description = null;
+ if (best != null) {
+ label = best.getString("label");
+ confidence = best.getInteger("confidence");
+ description = best.getString("description");
+ }
+ log.info("文本审核结果: taskId={}, riskLevel={}, label={}, confidence={}, description={}",
+ task.getId(), riskLevel, label, confidence, description);
+
+ if (riskLevel == null) {
+ contentAuditTaskService.updateTaskMachineResult(
+ task.getId(), machineResultJson, null, AuditConstants.STATUS_MANUAL_REVIEW,
+ label, confidence, description, requestId);
+ } else {
+ String status = applyStrategy(riskLevel, strictness);
+ contentAuditTaskService.updateTaskMachineResult(
+ task.getId(), machineResultJson, riskLevel, status,
+ label, confidence, description, requestId);
}
}
/**
* 单张图片审核结果处理(machine 模式:按策略裁决)
+ *
+ * data.result[] 可能包含多条,取置信度最高的非 nonLabel 标签。
*/
private void processSingleImageResponse(MiniContentAuditTask task, ImageModerationResponse response, String strictness) {
String machineResultJson = JSON.toJSONString(response);
@@ -201,9 +205,17 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
}
String riskLevel = dataObj.getString("riskLevel");
- String label = dataObj.getString("label");
- int confidence = dataObj.getIntValue("confidence");
- String description = dataObj.getString("description");
+ JSONArray resultArray = dataObj.getJSONArray("result");
+ JSONObject best = pickBestFromResultArray(resultArray);
+
+ String label = null;
+ Integer confidence = null;
+ String description = null;
+ if (best != null) {
+ label = best.getString("label");
+ confidence = best.getInteger("confidence");
+ description = best.getString("description");
+ }
log.info("图片审核结果: taskId={}, riskLevel={}, label={}, confidence={}, description={}",
task.getId(), riskLevel, label, confidence, description);
@@ -219,19 +231,49 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
}
}
+ /**
+ * 图片审核(异步提交)。
+ */
+ private void handleImageAsyncAudit(MiniContentAuditTask task, String imageUrl, String serviceName) {
+ ImageAsyncModerationResponse response = aliyunContentAuditUtil.imageAsyncModeration(imageUrl, serviceName);
+ if (response == null || response.getBody() == null) {
+ log.warn("图片异步审核请求返回null, taskId={}", task.getId());
+ return;
+ }
+ if (response.getBody().getCode() == null || response.getBody().getCode() != 200) {
+ log.warn("图片异步审核提交失败, taskId={}, code={}", task.getId(), response.getBody().getCode());
+ return;
+ }
+ if (response.getBody().getData() != null && response.getBody().getData().getReqId() != null) {
+ contentAuditTaskService.lambdaUpdate()
+ .eq(MiniContentAuditTask::getId, task.getId())
+ .set(MiniContentAuditTask::getTaskId, response.getBody().getData().getReqId())
+ .set(MiniContentAuditTask::getMachineResult, JSON.toJSONString(response))
+ .set(MiniContentAuditTask::getServiceName, serviceName)
+ .set(MiniContentAuditTask::getUpdateTime, new Date())
+ .set(MiniContentAuditTask::getUpdateTimestamp, System.currentTimeMillis())
+ .update();
+ log.info("图片异步审核已提交, taskId={}, reqId={}, service={}", task.getId(), response.getBody().getData().getReqId(), serviceName);
+ }
+ }
+
/**
* 视频审核(异步)。
*/
- private void handleVideoAudit(MiniContentAuditTask task, String videoUrl) {
- VideoModerationResponse response = aliyunContentAuditUtil.videoModeration(videoUrl);
+ private void handleVideoAudit(MiniContentAuditTask task, String videoUrl, String serviceName) {
+ VideoModerationResponse response = aliyunContentAuditUtil.videoModeration(videoUrl, serviceName);
if (response == null) {
log.warn("视频审核请求返回null, taskId={}", task.getId());
return;
}
String asyncTaskId = extractVideoTaskIdFromResponse(response);
if (asyncTaskId != null) {
- task.setTaskId(asyncTaskId);
- contentAuditTaskService.updateById(task);
+ contentAuditTaskService.lambdaUpdate()
+ .eq(MiniContentAuditTask::getId, task.getId())
+ .set(MiniContentAuditTask::getTaskId, asyncTaskId)
+ .set(MiniContentAuditTask::getServiceName, serviceName)
+ .update();
+ log.info("视频异步审核已提交, taskId={}, aliyunTaskId={}, service={}", task.getId(), asyncTaskId, serviceName);
}
}
@@ -367,32 +409,34 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
}
// ================================================================
- // 视频异步审核结果轮询
+ // 异步审核结果轮询(图片 + 视频合并查询)
// ================================================================
@Override
- public int pollVideoAuditResults() {
- List pendingTasks = contentAuditTaskService.getPendingVideoTasks();
+ public int pollAsyncAuditResults() {
+ List pendingTasks = contentAuditTaskService.getPendingAsyncTasks();
if (pendingTasks.isEmpty()) {
return 0;
}
- log.info("开始轮询视频审核任务, 总数={}", pendingTasks.size());
+ log.info("开始轮询异步审核任务, 总数={}", pendingTasks.size());
Set affectedAuditIds = new HashSet<>();
int processedCount = 0;
for (MiniContentAuditTask task : pendingTasks) {
try {
- if (processSingleVideoTask(task)) {
+ boolean done = "video".equals(task.getContentType())
+ ? processSingleVideoTask(task)
+ : processSingleImageAsyncTask(task);
+ if (done) {
affectedAuditIds.add(task.getContentAuditId());
processedCount++;
}
} catch (Exception e) {
- log.error("轮询视频任务异常, taskId={}", task.getId(), e);
+ log.error("轮询异步任务异常, taskId={}, contentType={}", task.getId(), task.getContentType(), e);
}
}
- // machine 类型需要重新汇总
for (Long auditId : affectedAuditIds) {
try {
MiniContentAudit audit = contentAuditService.getById(auditId);
@@ -405,7 +449,7 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
}
}
- log.info("视频审核轮询完成, 已处理={}, 受影响审核={}", processedCount, affectedAuditIds.size());
+ log.info("异步审核轮询完成, 已处理={}, 受影响审核={}", processedCount, affectedAuditIds.size());
return processedCount;
}
@@ -442,6 +486,39 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
return false;
}
+ // FrameSummarys → AudioSummarys 优先级提取最佳标签;全为 nonLabel 则存空
+ String label = null;
+ Integer confidence = null;
+ String description = null;
+ try {
+ JSONObject dataJsonObj = JSON.parseObject(dataJson);
+ JSONObject frameResult = dataJsonObj.getJSONObject("frameResult");
+ JSONObject audioResult = dataJsonObj.getJSONObject("audioResult");
+
+ JSONArray frameSummarys = frameResult != null ? frameResult.getJSONArray("frameSummarys") : null;
+ JSONObject best = pickBestFromFrameSummarys(frameSummarys);
+ if (best != null) {
+ log.info("视频标签来自frameSummarys, taskId={}, label={}, labelSum={}",
+ task.getId(), best.getString("label"), best.getInteger("labelSum"));
+ } else if (audioResult != null) {
+ JSONArray audioSummarys = audioResult.getJSONArray("audioSummarys");
+ best = pickBestFromFrameSummarys(audioSummarys);
+ if (best != null) {
+ log.info("视频标签来自audioSummarys, taskId={}, label={}, labelSum={}",
+ task.getId(), best.getString("label"), best.getInteger("labelSum"));
+ }
+ }
+ if (best == null) {
+ log.info("视频FrameSummarys和AudioSummarys均无有效违规标签, taskId={}", task.getId());
+ }
+ if (best != null) {
+ label = best.getString("label");
+ description = best.getString("description");
+ }
+ } catch (Exception e) {
+ log.error("提取视频标签失败, taskId={}", task.getId(), e);
+ }
+
MiniContentAudit audit = contentAuditService.getById(task.getContentAuditId());
if (audit == null) {
log.warn("未找到关联审核记录, taskId={}", task.getId());
@@ -458,10 +535,10 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
String status = applyStrategy(riskLevel, strictness);
contentAuditTaskService.updateTaskMachineResult(
task.getId(), dataJson, riskLevel, status,
- null, null, null, null);
+ label, confidence, description, null);
- log.info("视频任务审核完成, taskId={}, riskLevel={}, strictness={}, status={}",
- task.getId(), riskLevel, strictness, status);
+ log.info("视频任务审核完成, taskId={}, riskLevel={}, label={}, strictness={}, status={}",
+ task.getId(), riskLevel, label, strictness, status);
return true;
}
@@ -485,6 +562,69 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
}
}
+ private boolean processSingleImageAsyncTask(MiniContentAuditTask task) {
+ String reqId = task.getTaskId();
+ DescribeImageModerationResultResponse response = aliyunContentAuditUtil.describeImageModerationResult(reqId);
+
+ if (response == null || response.getBody() == null) {
+ log.info("图片审核结果查询返回null, taskId={}", task.getId());
+ return false;
+ }
+
+ Integer code = response.getBody().getCode();
+ if (code == null || code != 200) {
+ log.info("图片审核结果查询失败, taskId={}, code={}", task.getId(), code);
+ return false;
+ }
+
+ Object data = response.getBody().getData();
+ if (data == null) {
+ log.info("图片异步审核未完成(无data), taskId={}", task.getId());
+ return false;
+ }
+
+ String dataJson = JSON.toJSONString(data);
+ JSONObject dataObj = JSON.parseObject(dataJson);
+
+ String riskLevel = dataObj.getString("riskLevel");
+ if (riskLevel == null) {
+ log.info("图片异步审核仍在处理中(无riskLevel), taskId={}", task.getId());
+ return false;
+ }
+
+ JSONArray resultArray = dataObj.getJSONArray("result");
+ JSONObject best = pickBestFromResultArray(resultArray);
+ String label = null;
+ Integer confidence = null;
+ String description = null;
+ if (best != null) {
+ label = best.getString("label");
+ confidence = best.getInteger("confidence");
+ description = best.getString("description");
+ }
+
+ MiniContentAudit audit = contentAuditService.getById(task.getContentAuditId());
+ if (audit == null) {
+ log.warn("未找到关联审核记录, taskId={}", task.getId());
+ return false;
+ }
+
+ String strictness = AuditConstants.STRATEGY_NORMAL;
+ MiniContentAuditConfig config = findAuditConfig(audit.getModuleCode());
+ if (config != null && config.getRiskStrategy() != null) {
+ strictness = config.getRiskStrategy();
+ }
+
+ String status = applyStrategy(riskLevel, strictness);
+ contentAuditTaskService.updateTaskMachineResult(
+ task.getId(), dataJson, riskLevel, status,
+ label, confidence, description, null);
+
+ log.info("图片异步任务审核完成, taskId={}, reqId={}, riskLevel={}, label={}, strictness={}, status={}",
+ task.getId(), reqId, riskLevel, label, strictness, status);
+ return true;
+ }
+
/**
* 查找审核配置。
*/
@@ -503,4 +643,63 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
Boolean auditEnable = config.getAuditEnable();
return Boolean.TRUE.equals(auditEnable);
}
+
+ // ================================================================
+ // 公共提取工具方法
+ // ================================================================
+
+ /**
+ * 从 result 数组中取置信度最高的非 nonLabel 标签。
+ * 全部为 nonLabel 时兜底取第一条。
+ *
+ * @param resultArray data.result[],元素含 label / confidence / description
+ * @return 最佳标签项,或 null
+ */
+ static JSONObject pickBestFromResultArray(JSONArray resultArray) {
+ if (resultArray == null || resultArray.isEmpty()) return null;
+ JSONObject best = null;
+ double bestConf = -1;
+ JSONObject fallback = null;
+ for (int i = 0; i < resultArray.size(); i++) {
+ JSONObject item = resultArray.getJSONObject(i);
+ String label = item.getString("label");
+ if (label == null || label.isEmpty()) continue;
+ if (fallback == null) fallback = item;
+ if ("nonLabel".equals(label)) continue;
+ double conf = item.getDoubleValue("confidence");
+ if (conf > bestConf) {
+ bestConf = conf;
+ best = item;
+ }
+ }
+ if (best != null) return best;
+ return fallback;
+ }
+
+ /**
+ * 从视频 FrameSummarys / AudioSummarys 中取 LabelSum 最大的非 nonLabel 标签。
+ * 全部为 nonLabel 或空 Label 时返回 null。
+ *
+ * @param summarys FrameSummarys[] 或 AudioSummarys[],元素含 Label / Description / LabelSum
+ * @return 最佳标签项,或 null
+ */
+ static JSONObject pickBestFromFrameSummarys(JSONArray summarys) {
+ if (summarys == null || summarys.isEmpty()) return null;
+ JSONObject best = null;
+ int bestSum = -1;
+ JSONObject fallback = null;
+ for (int i = 0; i < summarys.size(); i++) {
+ JSONObject item = summarys.getJSONObject(i);
+ String label = item.getString("label");
+ if (label == null) continue;
+ if (fallback == null) fallback = item;
+ if ("nonLabel".equals(label) || label.isEmpty()) continue;
+ int sum = item.getIntValue("labelSum");
+ if (sum > bestSum) {
+ bestSum = sum;
+ best = item;
+ }
+ }
+ return best != null ? best : fallback;
+ }
}
diff --git a/src/main/java/com/youlai/boot/admin/service/impl/ContentAuditTaskServiceImpl.java b/src/main/java/com/youlai/boot/admin/service/impl/ContentAuditTaskServiceImpl.java
index 113c12d..3af776c 100644
--- a/src/main/java/com/youlai/boot/admin/service/impl/ContentAuditTaskServiceImpl.java
+++ b/src/main/java/com/youlai/boot/admin/service/impl/ContentAuditTaskServiceImpl.java
@@ -98,13 +98,11 @@ public class ContentAuditTaskServiceImpl extends ServiceImpl getPendingVideoTasks() {
+ public List getPendingAsyncTasks() {
return this.list(new LambdaQueryWrapper()
- .eq(MiniContentAuditTask::getContentType, "video")
+ .in(MiniContentAuditTask::getContentType, List.of("image", "video"))
.eq(MiniContentAuditTask::getStatus, AuditConstants.STATUS_REVIEWING)
.isNotNull(MiniContentAuditTask::getTaskId)
- .ne(MiniContentAuditTask::getTaskId, "")
- .and(w -> w.isNull(MiniContentAuditTask::getMachineResult)
- .or().eq(MiniContentAuditTask::getMachineResult, "")));
+ .ne(MiniContentAuditTask::getTaskId, ""));
}
}
diff --git a/src/main/java/com/youlai/boot/admin/service/impl/OssCallbackServiceImpl.java b/src/main/java/com/youlai/boot/admin/service/impl/OssCallbackServiceImpl.java
index ef85017..15fe612 100644
--- a/src/main/java/com/youlai/boot/admin/service/impl/OssCallbackServiceImpl.java
+++ b/src/main/java/com/youlai/boot/admin/service/impl/OssCallbackServiceImpl.java
@@ -4,7 +4,10 @@ import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.youlai.boot.admin.constant.AuditConstants;
+import com.youlai.boot.admin.model.entity.MiniContentAudit;
import com.youlai.boot.admin.model.entity.MiniContentAuditTask;
+import com.youlai.boot.admin.service.ContentAuditService;
import com.youlai.boot.admin.service.ContentAuditTaskService;
import com.youlai.boot.admin.service.OssCallbackService;
import lombok.extern.slf4j.Slf4j;
@@ -20,6 +23,7 @@ import java.security.MessageDigest;
public class OssCallbackServiceImpl implements OssCallbackService {
private final ContentAuditTaskService contentAuditTaskService;
+ private final ContentAuditService contentAuditService;
@Value("${audit.aliyun.oss.callbackUid}")
private String callbackUid;
@@ -27,8 +31,10 @@ public class OssCallbackServiceImpl implements OssCallbackService {
@Value("${audit.aliyun.oss.callbackSeed}")
private String callbackSeed;
- public OssCallbackServiceImpl(ContentAuditTaskService contentAuditTaskService) {
+ public OssCallbackServiceImpl(ContentAuditTaskService contentAuditTaskService,
+ ContentAuditService contentAuditService) {
this.contentAuditTaskService = contentAuditTaskService;
+ this.contentAuditService = contentAuditService;
}
@Override
@@ -87,25 +93,50 @@ public class OssCallbackServiceImpl implements OssCallbackService {
return;
}
- // 4. 提取 label / confidence / description(取第一个 Result)
+ // 4. 展平 Results[].Result[],取置信度最高的非 nonLabel 标签
String label = null;
- Float confidence = null;
+ Integer confidence = null;
String description = null;
if (results != null && !results.isEmpty()) {
- JSONObject firstResult = results.getJSONObject(0);
- JSONArray subResults = firstResult.getJSONArray("Result");
- if (subResults != null && !subResults.isEmpty()) {
- JSONObject sub = subResults.getJSONObject(0);
- label = sub.getString("Label");
- description = sub.getString("Description");
- if (sub.containsKey("Confidence")) {
- confidence = sub.getFloat("Confidence");
+ JSONArray flatResults = new JSONArray();
+ for (int i = 0; i < results.size(); i++) {
+ JSONObject result = results.getJSONObject(i);
+ JSONArray subResults = result.getJSONArray("Result");
+ if (subResults != null) {
+ for (int j = 0; j < subResults.size(); j++) {
+ JSONObject sub = subResults.getJSONObject(j);
+ // OSS 回调 key 为大写,统一转为小写以复用 pickBestFromResultArray
+ JSONObject normalized = new JSONObject();
+ normalized.put("label", sub.getString("Label"));
+ normalized.put("description", sub.getString("Description"));
+ if (sub.containsKey("Confidence")) {
+ normalized.put("confidence", sub.getFloat("Confidence"));
+ }
+ flatResults.add(normalized);
+ }
+ }
+ }
+ JSONObject best = AuditExecutorServiceImpl.pickBestFromResultArray(flatResults);
+ if (best != null) {
+ label = best.getString("label");
+ description = best.getString("description");
+ if (best.containsKey("confidence")) {
+ confidence = best.getInteger("confidence");
}
}
}
- log.info("OSS回调解析完成, taskId={}, ossObjectName={}, riskLevel={}, label={}, confidence={}, description={}, requestId={}",
- task.getId(), ossObjectName, riskLevel, label, confidence, description, requestId);
+ // 5. riskLevel → status 映射,更新任务
+ String status = ossRiskLevelToStatus(riskLevel);
+ contentAuditTaskService.updateTaskMachineResult(
+ task.getId(), JSON.toJSONString(payload), riskLevel != null ? riskLevel.toLowerCase() : null,
+ status, label, confidence, description, requestId);
+
+ // 6. 重新汇总 audit
+ aggregateCallbackAudit(task.getContentAuditId());
+
+ log.info("OSS回调处理完成, taskId={}, ossObjectName={}, riskLevel={}, label={}, confidence={}, description={}, status={}, requestId={}",
+ task.getId(), ossObjectName, riskLevel, label, confidence, description, status, requestId);
}
/** SHA256(uid + seed + content) 验签,对应阿里云控制台配置的加密算法 */
@@ -133,4 +164,59 @@ public class OssCallbackServiceImpl implements OssCallbackService {
.like(MiniContentAuditTask::getContentValue, ossObjectName)
.last("LIMIT 1"));
}
+
+ /** OSS 回调 RiskLevel → task status:none→passed, medium→manual_review, high→rejected */
+ private String ossRiskLevelToStatus(String riskLevel) {
+ if (riskLevel == null) return AuditConstants.STATUS_MANUAL_REVIEW;
+ return switch (riskLevel.toLowerCase()) {
+ case "none" -> AuditConstants.STATUS_PASSED;
+ case "high" -> AuditConstants.STATUS_REJECTED;
+ default -> AuditConstants.STATUS_MANUAL_REVIEW; // medium 及其他
+ };
+ }
+
+ /** 聚合回调任务结果,重新计算所属审核汇总的状态 */
+ private void aggregateCallbackAudit(Long auditId) {
+ if (auditId == null) return;
+ try {
+ MiniContentAudit audit = contentAuditService.getById(auditId);
+ if (audit == null) return;
+ java.util.List tasks = contentAuditTaskService.listTasksByAuditId(auditId);
+ if (tasks.isEmpty()) return;
+
+ boolean hasRejected = false;
+ boolean hasManualReview = false;
+ boolean allPassed = true;
+ for (MiniContentAuditTask t : tasks) {
+ String s = t.getStatus();
+ if (AuditConstants.STATUS_REJECTED.equals(s)) {
+ hasRejected = true;
+ allPassed = false;
+ break;
+ }
+ if (AuditConstants.STATUS_MANUAL_REVIEW.equals(s)) {
+ hasManualReview = true;
+ allPassed = false;
+ } else if (!AuditConstants.STATUS_PASSED.equals(s)) {
+ allPassed = false;
+ }
+ }
+ String auditStatus;
+ if (hasRejected) {
+ auditStatus = AuditConstants.STATUS_REJECTED;
+ } else if (hasManualReview) {
+ auditStatus = AuditConstants.STATUS_MANUAL_REVIEW;
+ } else if (allPassed) {
+ auditStatus = AuditConstants.STATUS_PASSED;
+ } else {
+ auditStatus = AuditConstants.STATUS_REVIEWING;
+ }
+ if (!AuditConstants.STATUS_REVIEWING.equals(auditStatus)) {
+ contentAuditService.updateAuditStatus(auditId, auditStatus);
+ log.info("OSS回调更新审核汇总状态, auditId={}, status={}", auditId, auditStatus);
+ }
+ } catch (Exception e) {
+ log.error("OSS回调汇总审核失败, auditId={}", auditId, e);
+ }
+ }
}
diff --git a/src/main/java/com/youlai/boot/common/util/AliyunContentAuditUtil.java b/src/main/java/com/youlai/boot/common/util/AliyunContentAuditUtil.java
index 7f349c2..c3c3d47 100644
--- a/src/main/java/com/youlai/boot/common/util/AliyunContentAuditUtil.java
+++ b/src/main/java/com/youlai/boot/common/util/AliyunContentAuditUtil.java
@@ -152,13 +152,13 @@ public class AliyunContentAuditUtil {
// ===================== 文本审核Plus =====================
//nickname_detection_pro 用户昵称检测_专业版(用户昵称) ;ugc_moderation_byllm_pro UGC场景文本审核大模型服务_专业版(个人简介 / 作品内容) ;
//comment_detection_pro 公聊评论内容检测_专业版(评论); ugc_moderation_byllm UGC场景文本审核大模型服务 (作品标题)
- public TextModerationPlusResponse textModerationPlus(String content) {
+ public TextModerationPlusResponse textModerationPlus(String content, String service) {
return executeWithFailover(client -> {
JSONObject params = new JSONObject();
params.put("content", content);
TextModerationPlusRequest textModerationPlusRequest = new TextModerationPlusRequest()
- .setService("ugc_moderation_byllm_pro")
+ .setService(service != null ? service : "ugc_moderation_byllm_pro")
.setServiceParameters(params.toJSONString());
return client.textModerationPlus(textModerationPlusRequest);
@@ -199,13 +199,13 @@ public class AliyunContentAuditUtil {
// 头像图片检测:profilePhotoCheck (头像) ; AI生成图片鉴别_含隐式标识版:aigcDetectorFull(AI生成图片) ;
// 大小模型融合图片审核服务:postlmageCheckByVL (用户上传的图片) ; OSS基线检测(OSS普惠版专用):oss_baselineCheck
// 通用图片审核大模型服务:baselineCheckByVL(用户作品);图片万物识别:generalRecognition ;营销素材检测:advertisingCheck
- public ImageModerationResponse imageModeration(String imageUrl) {
+ public ImageModerationResponse imageModeration(String imageUrl, String service) {
return executeWithFailover(client -> {
JSONObject params = new JSONObject();
params.put("imageUrl", imageUrl);
ImageModerationRequest request = new ImageModerationRequest()
- .setService("oss_baselineCheck")
+ .setService(service != null ? service : "baselineCheckByVL")
.setServiceParameters(params.toJSONString());
return client.imageModeration(request);
@@ -214,15 +214,38 @@ public class AliyunContentAuditUtil {
// 图片审核不支持批量,使用 imageModeration 逐个调用
+ // ===================== 图片审核(异步) =====================
+ public ImageAsyncModerationResponse imageAsyncModeration(String imageUrl, String service) {
+ return executeWithFailover(client -> {
+ JSONObject params = new JSONObject();
+ params.put("imageUrl", imageUrl);
+ params.put("dataId", java.util.UUID.randomUUID().toString());
+
+ ImageAsyncModerationRequest request = new ImageAsyncModerationRequest()
+ .setService(service != null ? service : "baselineCheckByVL")
+ .setServiceParameters(params.toJSONString());
+
+ return client.imageAsyncModeration(request);
+ });
+ }
+
+ public DescribeImageModerationResultResponse describeImageModerationResult(String reqId) {
+ return executeWithFailover(client -> {
+ DescribeImageModerationResultRequest request = new DescribeImageModerationResultRequest()
+ .setReqId(reqId);
+ return client.describeImageModerationResult(request);
+ });
+ }
+
// ===================== 视频审核(异步) =====================
//视频文件检测:videoDetection ; AI生成视频判定:videoAigcDetector ; 视频文件检测_大模型版:videoDetectionByVL
- public VideoModerationResponse videoModeration(String videoUrl) {
+ public VideoModerationResponse videoModeration(String videoUrl, String service) {
return executeWithFailover(client -> {
JSONObject params = new JSONObject();
params.put("url", videoUrl);
VideoModerationRequest request = new VideoModerationRequest()
- .setService("videoDetection")
+ .setService(service != null ? service : "videoDetection")
.setServiceParameters(params.toJSONString());
return client.videoModeration(request);
diff --git a/src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java b/src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java
index c769c1a..3e5409f 100644
--- a/src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java
+++ b/src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java
@@ -282,7 +282,7 @@ public class UserPostServiceImpl extends ServiceImpl texts = List.of("我操你吗", "狗日的");
- TextModerationPlusResponse r = auditUtil.batchTextModerationPlus(texts);
- log.info("=== 文本Plus批量 完整响应 ===\n{}", JSON.toJSONString(r, true));
- if (r != null && r.getBody() != null) {
- log.info("body.code={}", r.getBody().getCode());
- log.info("body.data class={}", r.getBody().getData() != null ? r.getBody().getData().getClass().getName() : "null");
- log.info("body.data JSON: {}", JSON.toJSONString(r.getBody().getData(), true));
- }
- }
-
- // ==================== 图片 ====================
-
- @Test
- @DisplayName("图片审核 - 单张")
- void imageModerationSingle() {
- String url = "https://pet-map.oss-cn-beijing.aliyuncs.com/user_post/image/17806506996944g1vs980.png";
- ImageModerationResponse r = auditUtil.imageModeration(url);
- log.info("=== 图片单张 完整响应 ===\n{}", JSON.toJSONString(r, true));
- if (r != null && r.getBody() != null) {
- log.info("body.code={}", r.getBody().getCode());
- }
- }
-
-// @Test
-// @DisplayName("图片审核 - 批量(当前实现,预期400)")
-// void imageModerationBatch() {
-// List urls = List.of(
-// "https://pet-map.oss-cn-beijing.aliyuncs.com/user_post/image/17806506996944g1vs980.png",
-// "https://pet-map.oss-cn-beijing.aliyuncs.com/user_post/image/another.png"
-// );
-// ImageModerationResponse r = auditUtil.batchImageModeration(urls);
-// log.info("=== 图片批量 完整响应 ===\n{}", JSON.toJSONString(r, true));
-// }
-
- // ==================== 视频 ====================
-
- @Test
- @DisplayName("视频审核 - 单条(异步,返回taskId)")
- void videoModerationSingle() {
- String url = "https://pet-map.oss-cn-beijing.aliyuncs.com/user_post/video/1780650699694okhqt697.mp4";
- VideoModerationResponse r = auditUtil.videoModeration(url);
- log.info("=== 视频单条 完整响应 ===\n{}", JSON.toJSONString(r, true));
- if (r != null && r.getBody() != null && r.getBody().getData() != null) {
- log.info("taskId={}", r.getBody().getData().getTaskId());
- }
- }
}