Browse Source

修改图片审核方式为异步

glx_phase2
glx 5 days ago
parent
commit
3455b64f54
  1. 32
      src/main/java/com/youlai/boot/admin/job/AsyncAuditPollJob.java
  2. 2
      src/main/java/com/youlai/boot/admin/job/AuditTimeoutJob.java
  3. 36
      src/main/java/com/youlai/boot/admin/job/VideoAuditPollJob.java
  4. 12
      src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditConfig.java
  5. 4
      src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditTask.java
  6. 6
      src/main/java/com/youlai/boot/admin/service/AuditExecutorService.java
  7. 4
      src/main/java/com/youlai/boot/admin/service/ContentAuditTaskService.java
  8. 329
      src/main/java/com/youlai/boot/admin/service/impl/AuditExecutorServiceImpl.java
  9. 8
      src/main/java/com/youlai/boot/admin/service/impl/ContentAuditTaskServiceImpl.java
  10. 112
      src/main/java/com/youlai/boot/admin/service/impl/OssCallbackServiceImpl.java
  11. 35
      src/main/java/com/youlai/boot/common/util/AliyunContentAuditUtil.java
  12. 4
      src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java
  13. 58
      src/test/java/com/youlai/boot/admin/AliyunContentAuditTest.java

32
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;
/**
* 异步审核结果轮询任务图片 + 视频
* <p>
* 一次 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);
}
}
}

2
src/main/java/com/youlai/boot/admin/job/AuditTimeoutJob.java

@ -20,7 +20,7 @@ import java.util.concurrent.TimeUnit;
* 扫描长时间停留在 reviewing 状态的审核记录自动转为人工审核
* 主要覆盖场景
* - 机审 API 部分任务调用失败导致 audit 汇总卡在 reviewing
* - 视频异步审核长时间未回调
* - 图片/视频异步审核长时间未回调
* - 其他异常导致的审核流程中断
*/
@Component

36
src/main/java/com/youlai/boot/admin/job/VideoAuditPollJob.java

@ -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;
/**
* 视频审核异步结果轮询任务
* <p>
* 视频审核为异步模式提交后阿里云返回 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);
}
}
}

12
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;
}

4
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;
}

6
src/main/java/com/youlai/boot/admin/service/AuditExecutorService.java

@ -18,10 +18,10 @@ public interface AuditExecutorService {
Map<String, Object> executeAudit(String moduleCode, Long bizId, AuditContentDTO content, String triggerType);
/**
* 轮询所有待处理的视频审核异步结果更新任务和汇总状态
* 轮询所有待处理的异步审核结果图片+视频一次查询按类型分流处理
*
* @return 本次处理的视频任务数量
* @return 本次处理的任务数量
*/
int pollVideoAuditResults();
int pollAsyncAuditResults();
}

4
src/main/java/com/youlai/boot/admin/service/ContentAuditTaskService.java

@ -17,7 +17,7 @@ public interface ContentAuditTaskService extends IService<MiniContentAuditTask>
/** 查询某个审核汇总下的所有任务明细 */
List<MiniContentAuditTask> listTasksByAuditId(Long auditId);
/** 查询待轮询的视频审核任务(contentType=video, status=reviewing, taskId不为空) */
List<MiniContentAuditTask> getPendingVideoTasks();
/** 查询待轮询的异步审核任务(contentType in (image, video), status=reviewing, taskId不为空) */
List<MiniContentAuditTask> getPendingAsyncTasks();
}

329
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<MiniContentAuditTask> tasks = contentAuditTaskService.listTasksByAuditId(auditId);
executeBatchAuditByType(tasks, strictness); // 进入机器审核
executeBatchAuditByType(tasks, config, strictness);
List<MiniContentAuditTask> updatedTasks = contentAuditTaskService.listTasksByAuditId(auditId);
return aggregateTaskResultsAndUpdateAudit(auditId, updatedTasks);
}
@ -94,19 +94,26 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
/**
* 按类型分组批量执行机审
*/
private void executeBatchAuditByType(List<MiniContentAuditTask> tasks, String strictness) {
private void executeBatchAuditByType(List<MiniContentAuditTask> tasks, MiniContentAuditConfig config, String strictness) {
Map<String, List<MiniContentAuditTask>> 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<MiniContentAuditTask> textTasks = grouped.getOrDefault("text", List.of());
if (!textTasks.isEmpty()) {
try {
List<String> 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<MiniContentAuditTask> videoTasks = grouped.getOrDefault("video", List.of());
for (MiniContentAuditTask task : videoTasks) {
handleVideoAudit(task, task.getContentValue());
handleVideoAudit(task, task.getContentValue(), videoService);
}
}
/**
* 批量文本审核结果处理machine 模式按策略裁决
* 文本审核结果处理machine 模式按策略裁决
* <p>
* body.data 结构{ riskLevel: "high", result: [{ label, confidence, description }, ...] }
* data.result[] 可能包含多条取置信度最高的非 nonLabel 标签
*/
private void processBatchTextResponse(List<MiniContentAuditTask> 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 模式按策略裁决
* <p>
* 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<MiniContentAuditTask> pendingTasks = contentAuditTaskService.getPendingVideoTasks();
public int pollAsyncAuditResults() {
List<MiniContentAuditTask> pendingTasks = contentAuditTaskService.getPendingAsyncTasks();
if (pendingTasks.isEmpty()) {
return 0;
}
log.info("开始轮询视频审核任务, 总数={}", pendingTasks.size());
log.info("开始轮询异步审核任务, 总数={}", pendingTasks.size());
Set<Long> 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;
}
}

8
src/main/java/com/youlai/boot/admin/service/impl/ContentAuditTaskServiceImpl.java

@ -98,13 +98,11 @@ public class ContentAuditTaskServiceImpl extends ServiceImpl<MiniContentAuditTas
}
@Override
public List<MiniContentAuditTask> getPendingVideoTasks() {
public List<MiniContentAuditTask> getPendingAsyncTasks() {
return this.list(new LambdaQueryWrapper<MiniContentAuditTask>()
.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, ""));
}
}

112
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<MiniContentAuditTask> 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);
}
}
}

35
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);

4
src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java

@ -282,7 +282,7 @@ public class UserPostServiceImpl extends ServiceImpl<MiniUserPostMapper, MiniUse
audit.setModuleCode("user_post");
audit.setBizId(postId);
audit.setAuditType("manual"); // 失败时 全转人工
// audit.setStatus(AuditConstants.AUDIT_MANUAL_REVIEW);
audit.setStatus(AuditConstants.STATUS_MANUAL_REVIEW);
audit.setCreateBy(SecurityUtils.getUserId());
audit.setCreateTime(new Date());
audit.setCreateTimestamp(System.currentTimeMillis());
@ -295,7 +295,7 @@ public class UserPostServiceImpl extends ServiceImpl<MiniUserPostMapper, MiniUse
// 将任务状态统一更新为 to_manual,使人工审核员能看到
contentAuditTaskService.lambdaUpdate()
.eq(MiniContentAuditTask::getContentAuditId, audit.getId())
// .set(MiniContentAuditTask::getStatus, AuditConstants.TASK_TO_MANUAL)
.set(MiniContentAuditTask::getStatus, AuditConstants.STATUS_MANUAL_REVIEW)
.update();
log.info("降级人工审核记录已创建, postId={}, auditId={}", postId, audit.getId());

58
src/test/java/com/youlai/boot/admin/AliyunContentAuditTest.java

@ -22,62 +22,4 @@ class AliyunContentAuditTest {
private AliyunContentAuditUtil auditUtil;
// ==================== 文本 ====================
@Test
@DisplayName("文本审核Plus - 单条")
void textModerationPlusSingle() {
TextModerationPlusResponse r = auditUtil.textModerationPlus("我操你吗");
log.info("=== 文本Plus单条 完整响应 ===\n{}", JSON.toJSONString(r, true));
log.info("body: {}", JSON.toJSONString(r.getBody(), true));
}
@Test
@DisplayName("文本审核Plus - 批量")
void textModerationPlusBatch() {
List<String> 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<String> 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());
}
}
}

Loading…
Cancel
Save