Browse Source

修改通用审核功能

glx_phase2
glx 2 weeks ago
parent
commit
efff642504
  1. 28
      src/main/java/com/youlai/boot/admin/constant/AuditConstants.java
  2. 75
      src/main/java/com/youlai/boot/admin/job/AuditTimeoutJob.java
  3. 36
      src/main/java/com/youlai/boot/admin/job/VideoAuditPollJob.java
  4. 2
      src/main/java/com/youlai/boot/admin/model/entity/MiniContentAudit.java
  5. 2
      src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditConfig.java
  6. 2
      src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditTask.java
  7. 7
      src/main/java/com/youlai/boot/admin/service/AuditExecutorService.java
  8. 3
      src/main/java/com/youlai/boot/admin/service/ContentAuditTaskService.java
  9. 392
      src/main/java/com/youlai/boot/admin/service/impl/AuditExecutorServiceImpl.java
  10. 15
      src/main/java/com/youlai/boot/admin/service/impl/ContentAuditTaskServiceImpl.java
  11. 90
      src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java
  12. 1
      src/main/resources/application-dev.yml

28
src/main/java/com/youlai/boot/admin/constant/AuditConstants.java

@ -23,18 +23,32 @@ public final class AuditConstants {
/** 任务状态: 机审成功 */
public static final String TASK_SUCCESS = "success";
/** 任务状态: 转人工 */
public static final String TASK_TO_MANUAL = "to_manual";
public static final String TASK_TO_MANUAL = "manual";
/** 任务结果: 通过 */
public static final String RESULT_PASSED = "passed";
/** 任务结果: 未通过 */
public static final String RESULT_FAILED = "failed";
/** 机审建议: 通过 */
public static final String SUGGESTION_PASS = "pass";
/** 机审建议: 复审 */
public static final String SUGGESTION_REVIEW = "review";
/** 机审建议: 违规 */
public static final String SUGGESTION_BLOCK = "block";
/** 风险等级: 无风险 */
public static final String RISK_NONE = "none";
/** 风险等级: 中等风险 */
public static final String RISK_MEDIUM = "medium";
/** 风险等级: 高风险 */
public static final String RISK_HIGH = "high";
/** 审核策略: 机审自决 */
public static final String STRATEGY_AUTO = "auto";
/** 审核策略: 均衡 */
public static final String STRATEGY_NORMAL = "normal";
/** 审核策略: 保守 */
public static final String STRATEGY_CAUTIOUS = "cautious";
/** 审核类型: 机器审核(AI 自行裁决) */
public static final String AUDIT_TYPE_MACHINE = "machine";
/** 审核类型: 人工审核(不调 AI,直接转人工) */
public static final String AUDIT_TYPE_MANUAL = "manual";
/** 审核类型: 混合审核(AI 给出风险提示,交由人工判定) */
public static final String AUDIT_TYPE_MIXED = "mixed";
}

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

@ -0,0 +1,75 @@
package com.youlai.boot.admin.job;
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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 审核超时扫描定时任务
* <p>
* 扫描长时间停留在 reviewing 状态的审核记录自动转为人工审核
* 主要覆盖场景
* - 机审 API 部分任务调用失败导致 audit 汇总卡在 reviewing
* - 视频异步审核长时间未回调
* - 其他异常导致的审核流程中断
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class AuditTimeoutJob {
private final ContentAuditService contentAuditService;
private final ContentAuditTaskService contentAuditTaskService;
@Value("${audit.timeout-minutes:30}")
private int timeoutMinutes;
/**
* 5 分钟扫描一次超时的审核记录
*/
@Scheduled(cron = "0 */5 * * * ?")
public void handleTimeoutAudits() {
long thresholdTimestamp = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(timeoutMinutes);
List<MiniContentAudit> stuckAudits = contentAuditService.lambdaQuery()
.eq(MiniContentAudit::getStatus, AuditConstants.AUDIT_REVIEWING)
.lt(MiniContentAudit::getCreateTimestamp, thresholdTimestamp)
.list();
if (stuckAudits.isEmpty()) {
return;
}
log.info("扫描到 {} 条超时审核记录,开始转为人工审核", stuckAudits.size());
for (MiniContentAudit audit : stuckAudits) {
try {
// 更新 status 为 manual_review,final_result 留空等待人工判定
contentAuditService.updateAuditStatus(audit.getId(),
AuditConstants.AUDIT_MANUAL_REVIEW, null);
// 将关联的 reviewing 状态任务统一更新为 to_manual
contentAuditTaskService.lambdaUpdate()
.eq(MiniContentAuditTask::getContentAuditId, audit.getId())
.eq(MiniContentAuditTask::getStatus, AuditConstants.TASK_REVIEWING)
.set(MiniContentAuditTask::getStatus, AuditConstants.TASK_TO_MANUAL)
.update();
log.info("超时审核已转人工, auditId={}, moduleCode={}, bizId={}",
audit.getId(), audit.getModuleCode(), audit.getBizId());
} catch (Exception e) {
log.error("超时审核转人工失败, auditId={}", audit.getId(), e);
}
}
}
}

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

@ -0,0 +1,36 @@
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;
/**
* 30 秒轮询一次待处理的视频审核异步结果
*/
@Scheduled(fixedDelay = 30000)
public void pollVideoAuditResult() {
try {
int count = auditExecutorService.pollVideoAuditResults();
if (count > 0) {
log.info("视频审核轮询完成, 本次处理 {} 条", count);
}
} catch (Exception e) {
log.error("视频审核轮询任务异常", e);
}
}
}

2
src/main/java/com/youlai/boot/admin/model/entity/MiniContentAudit.java

@ -45,7 +45,7 @@ public class MiniContentAudit implements Serializable {
private String status;
@TableField("final_result")
@Schema(description = "最终结果")
@Schema(description = "最终结果:passed通过 / failed未通过")
private String finalResult;
@TableField("operator")

2
src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditConfig.java

@ -41,7 +41,7 @@ public class MiniContentAuditConfig implements Serializable {
private String auditType;
@TableField("risk_strategy")
@Schema(description = "机器审核风险策略:none无,medium中等,high高")
@Schema(description = "机审风险策略:auto--none转passed,medium转failed, high转failed;normal--none转passed,medium转to_manual, high转failed;cautious--none转passed,medium转to_manual, high转to_manual;")
private String riskStrategy;
@TableField("create_time")

2
src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditTask.java

@ -45,7 +45,7 @@ public class MiniContentAuditTask implements Serializable {
private String auditType;
@TableField("status")
@Schema(description = "审核状态:reviewing审核中 / success成功 / to_manual手动")
@Schema(description = "审核状态:reviewing审核中 / success成功 /manual手动")
private String status;
@TableField("risk_level")

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

@ -16,4 +16,11 @@ public interface AuditExecutorService {
*/
Map<String, Object> executeAudit(String moduleCode, String bizId, AuditContentDTO content);
/**
* 轮询所有待处理的视频审核异步结果更新任务和汇总状态
*
* @return 本次处理的视频任务数量
*/
int pollVideoAuditResults();
}

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

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

392
src/main/java/com/youlai/boot/admin/service/impl/AuditExecutorServiceImpl.java

@ -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所有任务直接标记为 manualaudit 状态设为 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 同步返回 suggestionvideo 异步返回 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()
* 从阿里云文本/图片审核响应中提取 riskLevelnone/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 策略: passpassed, 其余failed 机审直接决定无人工环节
* medium策略: passpassed, reviewto_manual, blockfailed
* high策略: passpassed, 其余to_manual 中高风险一律转人工
* auto: NONEpassed, MEDIUMfailed, HIGHfailed
* normal: NONEpassed, MEDIUMmanual, HIGHfailed
* cautious: NONEpassed, MEDIUMmanual, HIGHmanual
* </pre>
*
* @param suggestion 阿里云机审建议pass / review / block
* @param strategy 审核配置中的 risk_strategynone / 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中直接提取 riskLevelnone/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;
}
}
}

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

@ -50,7 +50,7 @@ public class ContentAuditTaskServiceImpl extends ServiceImpl<MiniContentAuditTas
}
if (!tasks.isEmpty()) {
// MyBatis-Plus saveBatch:批量插入任务记录
//批量插入任务记录
this.saveBatch(tasks);
}
}
@ -83,7 +83,7 @@ public class ContentAuditTaskServiceImpl extends ServiceImpl<MiniContentAuditTas
entity.setRiskLevel(riskLevel);
// result 字段:passed(通过) / failed(不通过),由策略映射得到
entity.setResult(result);
// status 字段:success(机审完成) / to_manual(转人工),最终状态
// status 字段:success(机审完成) / manual(转人工),最终状态
entity.setStatus(status);
entity.setUpdateTime(new Date());
entity.setUpdateTimestamp(System.currentTimeMillis());
@ -97,4 +97,15 @@ public class ContentAuditTaskServiceImpl extends ServiceImpl<MiniContentAuditTas
return this.list(new LambdaQueryWrapper<MiniContentAuditTask>()
.eq(MiniContentAuditTask::getContentAuditId, auditId));
}
@Override
public List<MiniContentAuditTask> getPendingVideoTasks() {
return this.list(new LambdaQueryWrapper<MiniContentAuditTask>()
.eq(MiniContentAuditTask::getContentType, "video")
.eq(MiniContentAuditTask::getStatus, AuditConstants.TASK_REVIEWING)
.isNotNull(MiniContentAuditTask::getTaskId)
.ne(MiniContentAuditTask::getTaskId, "")
.and(w -> w.isNull(MiniContentAuditTask::getMachineResult)
.or().eq(MiniContentAuditTask::getMachineResult, "")));
}
}

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

@ -9,6 +9,13 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.admin.constant.AuditConstants;
import com.youlai.boot.admin.model.dto.AuditContentDTO;
import com.youlai.boot.admin.model.entity.MiniContentAudit;
import com.youlai.boot.admin.model.entity.MiniContentAuditTask;
import com.youlai.boot.admin.service.AuditExecutorService;
import com.youlai.boot.admin.service.ContentAuditService;
import com.youlai.boot.admin.service.ContentAuditTaskService;
import com.youlai.boot.common.exception.MsgException;
import com.youlai.boot.common.util.FileUtils;
import com.youlai.boot.common.util.JavaVCUtils;
@ -71,6 +78,9 @@ public class UserPostServiceImpl extends ServiceImpl<MiniUserPostMapper, MiniUse
private final MiniUserPostViewMapper miniUserPostViewMapper;
private final UserMapper userMapper;
private final RedissonClient redissonClient;
private final AuditExecutorService auditExecutorService;
private final ContentAuditService contentAuditService;
private final ContentAuditTaskService contentAuditTaskService;
private static final String BLOOM_VIEW_KEY_PREFIX = "mini:userPost:view:bloom:";
private static final String BLOOM_REBUILD_LOCK_PREFIX = "lock:rebuild:userPost:bloom:";
@ -213,7 +223,85 @@ public class UserPostServiceImpl extends ServiceImpl<MiniUserPostMapper, MiniUse
miniUserPostMediaMapper.update(null, wrapper);
}
return post.getUuid();
// 3. 创建内容审核任务(失败或异常降级为人工审核,不阻塞主流程)
String postUuid = post.getUuid();
try {
AuditContentDTO auditContent = buildAuditContent(formData);
Map<String, Object> auditResult = auditExecutorService.executeAudit("user_post", postUuid, auditContent);
if (auditResult != null) {
log.info("用户作品审核任务已创建, postUuid={}, auditResult={}", postUuid, auditResult);
}
} catch (Exception e) {
log.error("创建用户作品审核任务失败, 降级为人工审核, postUuid={}", postUuid, e);
createManualReviewFallback(postUuid, formData);
}
return postUuid;
}
/**
* 从表单数据构建审核内容DTO
*/
private AuditContentDTO buildAuditContent(UserPostForm formData) {
AuditContentDTO content = new AuditContentDTO();
List<String> texts = new ArrayList<>();
if (formData.getTitle() != null) {
texts.add(formData.getTitle());
}
if (formData.getContent() != null) {
texts.add(formData.getContent());
}
content.setTexts(texts);
List<String> images = new ArrayList<>();
List<String> videos = new ArrayList<>();
if (formData.getMediaUrlList() != null) {
for (String url : formData.getMediaUrlList()) {
String lowerUrl = url.toLowerCase();
if (lowerUrl.matches(".*\\.(mp4|mov|avi|wmv|flv|mkv|webm)(\\?.*)?$")) {
videos.add(url);
} else {
images.add(url);
}
}
}
content.setImages(images);
content.setVideos(videos);
return content;
}
/**
* 审核异常降级直接创建人工审核记录及对应任务确保不丢审
*/
private void createManualReviewFallback(String postUuid, UserPostForm formData) {
try {
MiniContentAudit audit = new MiniContentAudit();
audit.setUuid(UUID.randomUUID().toString());
audit.setModuleCode("user_post");
audit.setBizId(postUuid);
audit.setAuditType("manual"); // 失败时 全转人工
audit.setStatus(AuditConstants.AUDIT_MANUAL_REVIEW);
audit.setCreateBy(SecurityUtils.getUserId());
audit.setCreateTime(new Date());
audit.setCreateTimestamp(System.currentTimeMillis());
contentAuditService.save(audit);
AuditContentDTO auditContent = buildAuditContent(formData);
contentAuditTaskService.batchCreateTasks(audit.getId(), "manual",
auditContent.getTexts(), auditContent.getImages(), auditContent.getVideos());
// 将任务状态统一更新为 to_manual,使人工审核员能看到
contentAuditTaskService.lambdaUpdate()
.eq(MiniContentAuditTask::getContentAuditId, audit.getId())
.set(MiniContentAuditTask::getStatus, AuditConstants.TASK_TO_MANUAL)
.update();
log.info("降级人工审核记录已创建, postUuid={}, auditId={}", postUuid, audit.getId());
} catch (Exception fallbackEx) {
log.error("降级人工审核记录创建也失败了, postUuid={}, 作品将处于漏审状态!", postUuid, fallbackEx);
}
}
@Override

1
src/main/resources/application-dev.yml

@ -245,3 +245,4 @@ audit:
region-id: cn-shanghai
#备用节点 green-cip.cn-beijing.aliyuncs.com
endpoint: green-cip.cn-shanghai.aliyuncs.com
timeout-minutes: 30

Loading…
Cancel
Save