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()); - } - } }