|
|
|
@ -1,9 +1,11 @@ |
|
|
|
package com.youlai.boot.admin.service.impl; |
|
|
|
|
|
|
|
import com.alibaba.fastjson.JSON; |
|
|
|
import com.alibaba.fastjson.JSONObject; |
|
|
|
import com.aliyun.green20220302.models.ImageModerationResponse; |
|
|
|
import com.aliyun.green20220302.models.TextModerationResponse; |
|
|
|
import com.aliyun.green20220302.models.VideoModerationResponse; |
|
|
|
import com.aliyun.green20220302.models.VideoModerationResultResponse; |
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|
|
|
import com.youlai.boot.admin.constant.AuditConstants; |
|
|
|
import com.youlai.boot.admin.model.dto.AuditContentDTO; |
|
|
|
@ -48,44 +50,104 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { |
|
|
|
// 1) 查询对应模块的审核配置
|
|
|
|
MiniContentAuditConfig config = findAuditConfig(moduleCode); |
|
|
|
if (config == null || isAuditDisabled(config)) { |
|
|
|
// 无配置 或 审核开关已关闭 → 跳过审核,业务层直接放行
|
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
// 2) 创建审核汇总记录(MiniContentAudit 表)
|
|
|
|
MiniContentAudit audit = contentAuditService.createAudit(moduleCode, bizId, config.getAuditType()); |
|
|
|
String auditType = config.getAuditType(); |
|
|
|
|
|
|
|
// 2) 创建审核汇总记录
|
|
|
|
MiniContentAudit audit = contentAuditService.createAudit(moduleCode, bizId, auditType); |
|
|
|
Long auditId = audit.getId(); |
|
|
|
// 风险策略取自配置,默认为 medium
|
|
|
|
String riskStrategy = config.getRiskStrategy() != null ? config.getRiskStrategy() : "medium"; |
|
|
|
|
|
|
|
// 3) 拆解待审核内容,批量创建审核任务明细(MiniContentAuditTask 表)
|
|
|
|
contentAuditTaskService.batchCreateTasks(auditId, config.getAuditType(), |
|
|
|
// 3) 拆解待审核内容,批量创建审核任务明细
|
|
|
|
contentAuditTaskService.batchCreateTasks(auditId, auditType, |
|
|
|
content.getTexts(), content.getImages(), content.getVideos()); |
|
|
|
|
|
|
|
// 4) 读取已创建的所有任务,逐一调用阿里云内容审核API
|
|
|
|
// 4) 根据审核类型执行不同策略
|
|
|
|
if (AuditConstants.AUDIT_TYPE_MANUAL.equals(auditType)) { |
|
|
|
return executeManualAudit(auditId); |
|
|
|
} |
|
|
|
if (AuditConstants.AUDIT_TYPE_MIXED.equals(auditType)) { |
|
|
|
return executeMixedAudit(auditId); |
|
|
|
} |
|
|
|
|
|
|
|
// machine(默认):调 AI 并按策略裁决
|
|
|
|
String strictness = config.getRiskStrategy() != null ? config.getRiskStrategy() : AuditConstants.STRATEGY_NORMAL; |
|
|
|
List<MiniContentAuditTask> tasks = contentAuditTaskService.listTasksByAuditId(auditId); |
|
|
|
for (MiniContentAuditTask task : tasks) { |
|
|
|
executeSingleTaskAudit(task, riskStrategy); |
|
|
|
executeSingleTaskAudit(task, strictness); |
|
|
|
} |
|
|
|
|
|
|
|
// 5) 重新加载任务(机审结果已回填),汇总判定 audit 的最终状态
|
|
|
|
List<MiniContentAuditTask> updatedTasks = contentAuditTaskService.listTasksByAuditId(auditId); |
|
|
|
return aggregateTaskResultsAndUpdateAudit(auditId, updatedTasks); |
|
|
|
} |
|
|
|
|
|
|
|
// ================================================================
|
|
|
|
// 配置查询
|
|
|
|
// ================================================================
|
|
|
|
/** |
|
|
|
* manual 审核:不调 AI,所有任务直接标记为 manual,audit 状态设为 manual_review |
|
|
|
*/ |
|
|
|
private Map<String, Object> executeManualAudit(Long auditId) { |
|
|
|
contentAuditTaskService.lambdaUpdate() |
|
|
|
.eq(MiniContentAuditTask::getContentAuditId, auditId) |
|
|
|
.set(MiniContentAuditTask::getStatus, AuditConstants.TASK_TO_MANUAL) |
|
|
|
.update(); |
|
|
|
contentAuditService.updateAuditStatus(auditId, AuditConstants.AUDIT_MANUAL_REVIEW, null); |
|
|
|
Map<String, Object> result = new HashMap<>(); |
|
|
|
result.put("status", AuditConstants.AUDIT_MANUAL_REVIEW); |
|
|
|
result.put("auditId", auditId); |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* mixed 审核:调 AI 仅记录 riskLevel + machineResult,不应用策略裁决,交由人工判定 |
|
|
|
*/ |
|
|
|
private Map<String, Object> executeMixedAudit(Long auditId) { |
|
|
|
List<MiniContentAuditTask> tasks = contentAuditTaskService.listTasksByAuditId(auditId); |
|
|
|
for (MiniContentAuditTask task : tasks) { |
|
|
|
try { |
|
|
|
recordMachineResultOnly(task); |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("mixed审核AI分析失败, taskId={}", task.getId(), e); |
|
|
|
} |
|
|
|
} |
|
|
|
contentAuditService.updateAuditStatus(auditId, AuditConstants.AUDIT_MANUAL_REVIEW, null); |
|
|
|
Map<String, Object> result = new HashMap<>(); |
|
|
|
result.put("status", AuditConstants.AUDIT_MANUAL_REVIEW); |
|
|
|
result.put("auditId", auditId); |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 仅记录机审结果,不判定(mixed 模式用) |
|
|
|
*/ |
|
|
|
private void recordMachineResultOnly(MiniContentAuditTask task) { |
|
|
|
String contentType = task.getContentType(); |
|
|
|
String contentValue = task.getContentValue(); |
|
|
|
|
|
|
|
switch (contentType) { |
|
|
|
case "text" -> { |
|
|
|
TextModerationResponse r = aliyunContentAuditUtil.textModeration(contentValue); |
|
|
|
String riskLevel = extractRiskLevelFromResponse(r); |
|
|
|
contentAuditTaskService.updateTaskMachineResult( |
|
|
|
task.getId(), JSON.toJSONString(r), riskLevel, null, null); |
|
|
|
} |
|
|
|
case "image" -> { |
|
|
|
ImageModerationResponse r = aliyunContentAuditUtil.imageModeration(contentValue); |
|
|
|
String riskLevel = extractRiskLevelFromResponse(r); |
|
|
|
contentAuditTaskService.updateTaskMachineResult( |
|
|
|
task.getId(), JSON.toJSONString(r), riskLevel, null, null); |
|
|
|
} |
|
|
|
case "video" -> handleVideoAudit(task, contentValue); |
|
|
|
default -> log.warn("未知内容类型: {}", contentType); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 查找审核配置。 |
|
|
|
* LambdaQueryWrapper 构建条件:module_code = ? AND deleted = false(未逻辑删除) |
|
|
|
*/ |
|
|
|
private MiniContentAuditConfig findAuditConfig(String moduleCode) { |
|
|
|
LambdaQueryWrapper<MiniContentAuditConfig> queryWrapper = new LambdaQueryWrapper<>(); |
|
|
|
queryWrapper.eq(MiniContentAuditConfig::getModuleCode, moduleCode); |
|
|
|
queryWrapper.eq(MiniContentAuditConfig::getDeleted, false); |
|
|
|
// MyBatis-Plus getOne:根据 Wrapper 条件返回单条记录,多于1条时抛异常
|
|
|
|
queryWrapper.last("LIMIT 1"); |
|
|
|
return contentAuditConfigService.getOne(queryWrapper); |
|
|
|
} |
|
|
|
|
|
|
|
@ -98,11 +160,8 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { |
|
|
|
return Boolean.TRUE.equals(auditEnable); |
|
|
|
} |
|
|
|
|
|
|
|
// ================================================================
|
|
|
|
// 单条任务机审
|
|
|
|
// ================================================================
|
|
|
|
|
|
|
|
/** |
|
|
|
* 单条任务机审 |
|
|
|
* 对一条审核任务调用阿里云内容安全API,并将结果回填到任务记录。 |
|
|
|
* text/image 同步返回 suggestion,video 异步返回 taskId 后续轮询。 |
|
|
|
* 依赖方法说明: |
|
|
|
@ -110,42 +169,43 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { |
|
|
|
* - AliyunContentAuditUtil.imageModeration(String) → 阿里云图片审核(同步) |
|
|
|
* - AliyunContentAuditUtil.videoModeration(String) → 阿里云视频审核(异步,返回taskId) |
|
|
|
*/ |
|
|
|
private void executeSingleTaskAudit(MiniContentAuditTask task, String riskStrategy) { |
|
|
|
private void executeSingleTaskAudit(MiniContentAuditTask task, String strictness) { |
|
|
|
String contentType = task.getContentType(); |
|
|
|
String contentValue = task.getContentValue(); |
|
|
|
|
|
|
|
try { |
|
|
|
switch (contentType) { |
|
|
|
case "text" -> handleTextAudit(task, contentValue, riskStrategy); |
|
|
|
case "image" -> handleImageAudit(task, contentValue, riskStrategy); |
|
|
|
case "text" -> handleTextAudit(task, contentValue, strictness); |
|
|
|
case "image" -> handleImageAudit(task, contentValue, strictness); |
|
|
|
case "video" -> handleVideoAudit(task, contentValue); |
|
|
|
default -> log.warn("未知内容类型: {}", contentType); |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("机审调用失败, taskId={}, contentType={}", task.getId(), contentType, e); |
|
|
|
// 机审异常时直接将任务标记为转人工,避免卡在 reviewing 状态
|
|
|
|
contentAuditTaskService.updateTaskMachineResult( |
|
|
|
task.getId(), null, null, null, AuditConstants.TASK_TO_MANUAL); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 文本审核(同步)。 |
|
|
|
* TextModerationResponse.getBody().getData() 包含 suggestion,值为 pass/review/block |
|
|
|
*/ |
|
|
|
private void handleTextAudit(MiniContentAuditTask task, String textContent, String riskStrategy) { |
|
|
|
private void handleTextAudit(MiniContentAuditTask task, String textContent, String strictness) { |
|
|
|
TextModerationResponse response = aliyunContentAuditUtil.textModeration(textContent); |
|
|
|
String suggestion = extractSuggestionFromResponse(response); |
|
|
|
String riskLevel = extractRiskLevelFromResponse(response); |
|
|
|
String machineResultJson = JSON.toJSONString(response); |
|
|
|
applyAuditResultToTask(task, suggestion, machineResultJson, riskStrategy); |
|
|
|
applyAuditResultToTask(task, riskLevel, machineResultJson, strictness); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 图片审核(同步)。 |
|
|
|
* ImageModerationResponse.getBody().getData() 包含 suggestion,值为 pass/review/block |
|
|
|
*/ |
|
|
|
private void handleImageAudit(MiniContentAuditTask task, String imageUrl, String riskStrategy) { |
|
|
|
private void handleImageAudit(MiniContentAuditTask task, String imageUrl, String strictness) { |
|
|
|
ImageModerationResponse response = aliyunContentAuditUtil.imageModeration(imageUrl); |
|
|
|
String suggestion = extractSuggestionFromResponse(response); |
|
|
|
String riskLevel = extractRiskLevelFromResponse(response); |
|
|
|
String machineResultJson = JSON.toJSONString(response); |
|
|
|
applyAuditResultToTask(task, suggestion, machineResultJson, riskStrategy); |
|
|
|
applyAuditResultToTask(task, riskLevel, machineResultJson, strictness); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
@ -164,7 +224,6 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { |
|
|
|
String asyncTaskId = extractVideoTaskIdFromResponse(response); |
|
|
|
if (asyncTaskId != null) { |
|
|
|
task.setTaskId(asyncTaskId); |
|
|
|
// MyBatis-Plus updateById:按主键id更新实体,此处只更新 task_id 字段
|
|
|
|
contentAuditTaskService.updateById(task); |
|
|
|
} |
|
|
|
} |
|
|
|
@ -185,62 +244,53 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 将机审建议应用到任务记录:suggestion → risk_level → 按策略判定 result → 任务最终 status |
|
|
|
* 将机审结果应用到任务记录:riskLevel + strictness → result → 任务最终 status |
|
|
|
*/ |
|
|
|
private void applyAuditResultToTask(MiniContentAuditTask task, String suggestion, String machineResultJson, String riskStrategy) { |
|
|
|
if (suggestion == null) { |
|
|
|
private void applyAuditResultToTask(MiniContentAuditTask task, String riskLevel, String machineResultJson, String strictness) { |
|
|
|
if (riskLevel == null) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 1. suggestion → risk_level(none/medium/high)
|
|
|
|
String riskLevel = mapSuggestionToRiskLevel(suggestion); |
|
|
|
// 1. riskLevel + strictness → result(passed / failed)
|
|
|
|
String result = applyStrategy(riskLevel, strictness); |
|
|
|
|
|
|
|
// 2. risk_level + 策略 → result(passed / failed / to_manual)
|
|
|
|
String result = applyRiskStrategy(suggestion, riskStrategy); |
|
|
|
|
|
|
|
// 3. result 决定任务最终状态
|
|
|
|
boolean needManual = AuditConstants.TASK_TO_MANUAL.equals(result); |
|
|
|
String taskStatus = needManual ? AuditConstants.TASK_TO_MANUAL : AuditConstants.TASK_SUCCESS; |
|
|
|
// 2. result 决定任务最终状态
|
|
|
|
String taskStatus = AuditConstants.TASK_SUCCESS.equals(result) |
|
|
|
? AuditConstants.TASK_SUCCESS : AuditConstants.TASK_TO_MANUAL; |
|
|
|
|
|
|
|
// 回填任务表:machine_result(JSON) / risk_level / result / status
|
|
|
|
contentAuditTaskService.updateTaskMachineResult( |
|
|
|
task.getId(), machineResultJson, riskLevel, result, taskStatus); |
|
|
|
contentAuditTaskService.updateTaskMachineResult(task.getId(), machineResultJson, riskLevel, result, taskStatus); |
|
|
|
} |
|
|
|
|
|
|
|
// ================================================================
|
|
|
|
// 从阿里云响应中提取 suggestion
|
|
|
|
// 从阿里云响应中提取 riskLevel
|
|
|
|
// ================================================================
|
|
|
|
|
|
|
|
/** |
|
|
|
* 从阿里云审核响应中提取 suggestion 值(pass/review/block)。 |
|
|
|
* 使用反射调用 getBody() → getData() → getSuggestion(),兼容 Text 和 Image 两种 Response。 |
|
|
|
* <p> |
|
|
|
* 如果 Data 为 List 类型,取第一个元素的 getSuggestion()。 |
|
|
|
* 从阿里云文本/图片审核响应中提取 riskLevel(none/medium/high)。 |
|
|
|
* 使用反射调用 getBody() → getData() → getRiskLevel()。 |
|
|
|
* 如果 Data 为 List 类型,取第一个元素。 |
|
|
|
*/ |
|
|
|
private String extractSuggestionFromResponse(Object response) { |
|
|
|
private String extractRiskLevelFromResponse(Object response) { |
|
|
|
if (response == null) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
try { |
|
|
|
// 调用 response.getBody()
|
|
|
|
Object body = invokeGetter(response, "getBody"); |
|
|
|
if (body == null) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
// 调用 body.getData()
|
|
|
|
Object data = invokeGetter(body, "getData"); |
|
|
|
if (data == null) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
// 如果 Data 是 List,取第一个元素
|
|
|
|
if (data instanceof List<?> dataList && !dataList.isEmpty()) { |
|
|
|
data = dataList.get(0); |
|
|
|
} |
|
|
|
// 调用 data.getSuggestion()
|
|
|
|
Object suggestion = invokeGetter(data, "getSuggestion"); |
|
|
|
return suggestion != null ? suggestion.toString() : null; |
|
|
|
Object riskLevel = invokeGetter(data, "getRiskLevel"); |
|
|
|
return riskLevel != null ? riskLevel.toString() : null; |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("提取suggestion失败", e); |
|
|
|
log.error("提取riskLevel失败", e); |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
@ -258,61 +308,30 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { |
|
|
|
// ================================================================
|
|
|
|
|
|
|
|
/** |
|
|
|
* 将阿里云机审 suggestion 映射为风险等级。 |
|
|
|
* pass → none(无风险) / review → medium(中等风险) / block → high(高风险) |
|
|
|
*/ |
|
|
|
private String mapSuggestionToRiskLevel(String suggestion) { |
|
|
|
if (AuditConstants.SUGGESTION_PASS.equals(suggestion)) { |
|
|
|
return "none"; |
|
|
|
} |
|
|
|
if (AuditConstants.SUGGESTION_REVIEW.equals(suggestion)) { |
|
|
|
return "medium"; |
|
|
|
} |
|
|
|
if (AuditConstants.SUGGESTION_BLOCK.equals(suggestion)) { |
|
|
|
return "high"; |
|
|
|
} |
|
|
|
return "none"; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 根据 risk_strategy 配置,将机审 suggestion 映射为任务 result。 |
|
|
|
* TODO 根据 strictness 策略,将 riskLevel 映射为任务 result。 |
|
|
|
* <pre> |
|
|
|
* none 策略: pass→passed, 其余→failed (机审直接决定,无人工环节) |
|
|
|
* medium策略: pass→passed, review→to_manual, block→failed |
|
|
|
* high策略: pass→passed, 其余→to_manual (中高风险一律转人工) |
|
|
|
* auto: NONE→passed, MEDIUM→failed, HIGH→failed |
|
|
|
* normal: NONE→passed, MEDIUM→manual, HIGH→failed |
|
|
|
* cautious: NONE→passed, MEDIUM→manual, HIGH→manual |
|
|
|
* </pre> |
|
|
|
* |
|
|
|
* @param suggestion 阿里云机审建议:pass / review / block |
|
|
|
* @param strategy 审核配置中的 risk_strategy:none / medium / high |
|
|
|
* @return passed / failed / to_manual |
|
|
|
*/ |
|
|
|
private String applyRiskStrategy(String suggestion, String strategy) { |
|
|
|
boolean isPassSuggestion = AuditConstants.SUGGESTION_PASS.equals(suggestion); |
|
|
|
|
|
|
|
if ("none".equals(strategy)) { |
|
|
|
// none策略:pass→通过,其余→不通过
|
|
|
|
if (isPassSuggestion) { |
|
|
|
return AuditConstants.RESULT_PASSED; |
|
|
|
} |
|
|
|
return AuditConstants.RESULT_FAILED; |
|
|
|
} |
|
|
|
|
|
|
|
if ("high".equals(strategy)) { |
|
|
|
// high策略:pass→通过,其余→转人工
|
|
|
|
if (isPassSuggestion) { |
|
|
|
return AuditConstants.RESULT_PASSED; |
|
|
|
} |
|
|
|
return AuditConstants.TASK_TO_MANUAL; |
|
|
|
private String applyStrategy(String riskLevel, String strictness) { |
|
|
|
if (AuditConstants.RISK_NONE.equals(riskLevel)) { |
|
|
|
return AuditConstants.RESULT_PASSED; |
|
|
|
} |
|
|
|
|
|
|
|
// medium策略(默认):pass→通过,review→转人工,block→不通过
|
|
|
|
if (AuditConstants.SUGGESTION_REVIEW.equals(suggestion)) { |
|
|
|
return AuditConstants.TASK_TO_MANUAL; |
|
|
|
if (AuditConstants.STRATEGY_CAUTIOUS.equals(strictness)) { |
|
|
|
// cautious: 非 NONE 只更新审核状态 → 一律转人工
|
|
|
|
return null; |
|
|
|
} |
|
|
|
if (AuditConstants.SUGGESTION_BLOCK.equals(suggestion)) { |
|
|
|
if (AuditConstants.STRATEGY_AUTO.equals(strictness)) { |
|
|
|
// auto: 非 NONE 一律不通过
|
|
|
|
return AuditConstants.RESULT_FAILED; |
|
|
|
} |
|
|
|
return AuditConstants.RESULT_PASSED; |
|
|
|
// normal(默认):MEDIUM→ 审核状态转人工,HIGH→不通过
|
|
|
|
if (AuditConstants.RISK_MEDIUM.equals(riskLevel)) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
return AuditConstants.RESULT_FAILED; |
|
|
|
} |
|
|
|
|
|
|
|
// ================================================================
|
|
|
|
@ -331,7 +350,7 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { |
|
|
|
* <p> |
|
|
|
* MiniContentAudit 字段含义: |
|
|
|
* - status: reviewing(机审中) / passed(通过) / failed(不通过) / manual_review(待人工) / appealing(申诉中) |
|
|
|
* - final_result: passed / failed / manual_review |
|
|
|
* - final_result: passed / failed(仅终态;manual_review 时留空,等待人工判定) |
|
|
|
*/ |
|
|
|
private Map<String, Object> aggregateTaskResultsAndUpdateAudit(Long auditId, List<MiniContentAuditTask> tasks) { |
|
|
|
boolean hasFailedTask = false; |
|
|
|
@ -346,8 +365,7 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { |
|
|
|
allTasksPassed = false; |
|
|
|
break; // 已有 failed,无需继续遍历
|
|
|
|
} |
|
|
|
if (AuditConstants.TASK_TO_MANUAL.equals(taskResult) |
|
|
|
|| AuditConstants.TASK_TO_MANUAL.equals(task.getStatus())) { |
|
|
|
if (AuditConstants.TASK_TO_MANUAL.equals(task.getStatus())) { |
|
|
|
hasManualTask = true; |
|
|
|
allTasksPassed = false; |
|
|
|
} else if (!AuditConstants.RESULT_PASSED.equals(taskResult)) { |
|
|
|
@ -373,7 +391,7 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { |
|
|
|
finalResultValue = "failed"; |
|
|
|
} else if (hasManualTask) { |
|
|
|
auditStatus = AuditConstants.AUDIT_MANUAL_REVIEW; |
|
|
|
finalResultValue = "manual_review"; |
|
|
|
finalResultValue = null; // 转人工不是终态,final_result 留空等待人工判定
|
|
|
|
} else if (allTasksPassed && !tasks.isEmpty()) { |
|
|
|
auditStatus = AuditConstants.AUDIT_PASSED; |
|
|
|
finalResultValue = "passed"; |
|
|
|
@ -383,9 +401,8 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { |
|
|
|
finalResultValue = null; |
|
|
|
} |
|
|
|
|
|
|
|
// 汇总结果已确定则更新 audit 表
|
|
|
|
if (finalResultValue != null) { |
|
|
|
// ContentAuditService.updateAuditStatus:按 auditId 更新 status 和 final_result
|
|
|
|
// 非 reviewing 状态下更新 audit(manual_review 虽无 finalResult 也要更新 status)
|
|
|
|
if (!AuditConstants.AUDIT_REVIEWING.equals(auditStatus)) { |
|
|
|
contentAuditService.updateAuditStatus(auditId, auditStatus, finalResultValue); |
|
|
|
} |
|
|
|
|
|
|
|
@ -395,4 +412,153 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { |
|
|
|
resultMap.put("auditId", auditId); |
|
|
|
return resultMap; |
|
|
|
} |
|
|
|
|
|
|
|
// ================================================================
|
|
|
|
// 视频异步审核结果轮询
|
|
|
|
// ================================================================
|
|
|
|
|
|
|
|
/** |
|
|
|
* 轮询所有待处理的视频审核异步结果,更新任务和汇总状态。 |
|
|
|
* 供定时任务 {@code VideoAuditPollJob} 调用。 |
|
|
|
* |
|
|
|
* @return 本次成功处理的视频任务数量 |
|
|
|
*/ |
|
|
|
@Override |
|
|
|
public int pollVideoAuditResults() { |
|
|
|
List<MiniContentAuditTask> pendingTasks = contentAuditTaskService.getPendingVideoTasks(); |
|
|
|
if (pendingTasks.isEmpty()) { |
|
|
|
return 0; |
|
|
|
} |
|
|
|
|
|
|
|
log.info("开始轮询视频审核任务, 总数={}", pendingTasks.size()); |
|
|
|
Set<Long> affectedAuditIds = new HashSet<>(); |
|
|
|
int processedCount = 0; |
|
|
|
|
|
|
|
for (MiniContentAuditTask task : pendingTasks) { |
|
|
|
try { |
|
|
|
if (processSingleVideoTask(task)) { |
|
|
|
affectedAuditIds.add(task.getContentAuditId()); |
|
|
|
processedCount++; |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("轮询视频任务异常, taskId={}", task.getId(), e); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 仅 machine 类型需要重新汇总,mixed 的 audit 已为 manual_review 不参与汇总
|
|
|
|
for (Long auditId : affectedAuditIds) { |
|
|
|
try { |
|
|
|
MiniContentAudit audit = contentAuditService.getById(auditId); |
|
|
|
if (audit != null && AuditConstants.AUDIT_TYPE_MACHINE.equals(audit.getAuditType())) { |
|
|
|
List<MiniContentAuditTask> tasks = contentAuditTaskService.listTasksByAuditId(auditId); |
|
|
|
aggregateTaskResultsAndUpdateAudit(auditId, tasks); |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("重新汇总审核失败, auditId={}", auditId, e); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
log.info("视频审核轮询完成, 已处理={}, 受影响审核={}", processedCount, affectedAuditIds.size()); |
|
|
|
return processedCount; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理单条视频审核任务的异步结果。返回 true 表示结果已回填完成。 |
|
|
|
*/ |
|
|
|
private boolean processSingleVideoTask(MiniContentAuditTask task) { |
|
|
|
String asyncTaskId = task.getTaskId(); |
|
|
|
VideoModerationResultResponse response = aliyunContentAuditUtil.videoModerationResult(asyncTaskId); |
|
|
|
|
|
|
|
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); |
|
|
|
if (!isVideoAuditCompleted(dataJson)) { |
|
|
|
log.debug("视频审核仍在处理中, taskId={}", task.getId()); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
// 从阿里云响应中直接提取 riskLevel(none/medium/high)
|
|
|
|
String riskLevel = extractVideoRiskLevel(dataJson); |
|
|
|
if (riskLevel == null) { |
|
|
|
log.warn("视频审核结果中未找到riskLevel, taskId={}", task.getId()); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
// 查找关联的审核记录
|
|
|
|
MiniContentAudit audit = contentAuditService.getById(task.getContentAuditId()); |
|
|
|
if (audit == null) { |
|
|
|
log.warn("未找到关联审核记录, taskId={}", task.getId()); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
// mixed 类型:记录机审结果,更新审核状态 manual,不裁决
|
|
|
|
if (AuditConstants.AUDIT_TYPE_MIXED.equals(audit.getAuditType())) { |
|
|
|
contentAuditTaskService.updateTaskMachineResult( |
|
|
|
task.getId(), dataJson, riskLevel, null, "manual"); |
|
|
|
log.info("视频任务AI分析完成(mixed), taskId={}, riskLevel={}", task.getId(), riskLevel); |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
// machine 类型:按策略裁决
|
|
|
|
String strictness = AuditConstants.STRATEGY_NORMAL; |
|
|
|
MiniContentAuditConfig config = findAuditConfig(audit.getModuleCode()); |
|
|
|
if (config != null && config.getRiskStrategy() != null) { |
|
|
|
strictness = config.getRiskStrategy(); |
|
|
|
} |
|
|
|
|
|
|
|
String result = applyStrategy(riskLevel, strictness); |
|
|
|
String taskStatus = AuditConstants.TASK_SUCCESS.equals(result) |
|
|
|
? AuditConstants.TASK_SUCCESS : AuditConstants.TASK_TO_MANUAL; |
|
|
|
|
|
|
|
contentAuditTaskService.updateTaskMachineResult( |
|
|
|
task.getId(), dataJson, riskLevel, result, taskStatus); |
|
|
|
|
|
|
|
log.info("视频任务审核完成, taskId={}, riskLevel={}, strictness={}, result={}", |
|
|
|
task.getId(), riskLevel, strictness, result); |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 判断视频异步审核是否已完成。 |
|
|
|
* 阿里云视频审核完成后 data 中会出现 RiskLevel 字段(none/medium/high), |
|
|
|
* 未完成时该字段不存在。 |
|
|
|
*/ |
|
|
|
private boolean isVideoAuditCompleted(String dataJson) { |
|
|
|
try { |
|
|
|
JSONObject json = JSON.parseObject(dataJson); |
|
|
|
return json.containsKey("riskLevel"); |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("解析视频审核完成状态失败", e); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 从视频审核结果JSON中直接提取 riskLevel(none/medium/high)。 |
|
|
|
* 阿里云视频审核完成后 data 中包含小写 riskLevel 字段。 |
|
|
|
*/ |
|
|
|
private String extractVideoRiskLevel(String dataJson) { |
|
|
|
try { |
|
|
|
JSONObject json = JSON.parseObject(dataJson); |
|
|
|
return json.getString("riskLevel"); |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("解析视频审核riskLevel失败", e); |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|