Browse Source

统一审核任务状态

glx_phase2
glx 1 week ago
parent
commit
bfef04ae63
  1. 55
      src/main/java/com/youlai/boot/admin/constant/AuditConstants.java
  2. 1
      src/main/java/com/youlai/boot/admin/controller/AdoptionApplicationManageController.java
  3. 33
      src/main/java/com/youlai/boot/admin/controller/ContentAuditConfigController.java
  4. 12
      src/main/java/com/youlai/boot/admin/job/AuditTimeoutJob.java
  5. 14
      src/main/java/com/youlai/boot/admin/model/entity/MiniContentAudit.java
  6. 4
      src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditConfig.java
  7. 10
      src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditTask.java
  8. 2
      src/main/java/com/youlai/boot/admin/model/form/AuditConfigForm.java
  9. 4
      src/main/java/com/youlai/boot/admin/model/vo/AuditConfigVO.java
  10. 2
      src/main/java/com/youlai/boot/admin/service/AuditExecutorService.java
  11. 2
      src/main/java/com/youlai/boot/admin/service/ContentAuditService.java
  12. 4
      src/main/java/com/youlai/boot/admin/service/ContentAuditTaskService.java
  13. 2
      src/main/java/com/youlai/boot/admin/service/OssCallbackService.java
  14. 478
      src/main/java/com/youlai/boot/admin/service/impl/AuditExecutorServiceImpl.java
  15. 6
      src/main/java/com/youlai/boot/admin/service/impl/ContentAuditAppealServiceImpl.java
  16. 5
      src/main/java/com/youlai/boot/admin/service/impl/ContentAuditServiceImpl.java
  17. 9
      src/main/java/com/youlai/boot/admin/service/impl/ContentAuditTaskServiceImpl.java
  18. 108
      src/main/java/com/youlai/boot/admin/service/impl/OssCallbackServiceImpl.java
  19. 50
      src/main/java/com/youlai/boot/common/util/AliyunContentAuditUtil.java
  20. 4
      src/main/java/com/youlai/boot/mini/service/impl/MiniContentAuditAppealServiceImpl.java
  21. 6
      src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java
  22. 83
      src/test/java/com/youlai/boot/admin/AliyunContentAuditTest.java
  23. BIN
      tmp/178065053524697cu0r6a

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

@ -7,53 +7,36 @@ public final class AuditConstants {
private AuditConstants() {}
/** 审核状态: 机审中 */
public static final String AUDIT_REVIEWING = "reviewing";
/** 审核状态: 通过 */
public static final String AUDIT_PASSED = "passed";
/** 审核状态: 不通过 */
public static final String AUDIT_FAILED = "failed";
/** 审核状态: 待人工 */
public static final String AUDIT_MANUAL_REVIEW = "manual_review";
/** 审核状态: 申诉中 */
public static final String AUDIT_APPEALING = "appealing";
/** 任务状态: 审核中 */
public static final String TASK_REVIEWING = "reviewing";
/** 任务状态: 机审成功 */
public static final String TASK_SUCCESS = "success";
/** 任务状态: 转人工 */
public static final String TASK_TO_MANUAL = "manual";
/** 任务结果: 通过 */
public static final String RESULT_PASSED = "passed";
/** 任务结果: 未通过 */
public static final String RESULT_FAILED = "failed";
/** 风险等级: 无风险 */
// ======================== 通用状态(task + audit 共用) ========================
/** 审核中 */
public static final String STATUS_REVIEWING = "reviewing";
/** 审核通过 */
public static final String STATUS_PASSED = "passed";
/** 审核不通过 */
public static final String STATUS_REJECTED = "rejected";
/** 待人工审核 */
public static final String STATUS_MANUAL_REVIEW = "manual_review";
// ======================== audit 专用状态 ========================
/** 申诉中 */
public static final String STATUS_APPEALING = "appealing";
// ======================== 风险等级 ========================
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";
/** 触发类型: 自动触发(内容创建/修改时) */
public static final String TRIGGER_AUTO = "auto";
/** 触发类型: 举报触发 */
// ======================== 触发类型 ========================
public static final String TRIGGER_CREATE = "create";
public static final String TRIGGER_REPORT = "report";
}

1
src/main/java/com/youlai/boot/admin/controller/AdoptionApplicationManageController.java

@ -37,4 +37,5 @@ public class AdoptionApplicationManageController {
adoptionApplicationService.auditApplication(form);
return Result.success();
}
}

33
src/main/java/com/youlai/boot/admin/controller/ContentAuditConfigController.java

@ -11,21 +11,17 @@ import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.result.PageResult;
import com.youlai.boot.common.result.Result;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
@Tag(name = "管理端-审核配置相关接口")
@RestController
@RequestMapping("/api/v1/admin/auditConfig")
@RequiredArgsConstructor
@Slf4j
public class ContentAuditConfigController {
private final ContentAuditConfigService contentAuditConfigService;
@ -63,32 +59,11 @@ public class ContentAuditConfigController {
return Result.success();
}
@Operation(summary = "阿里云OSS增量图片审核回调")
@Hidden
@Operation(summary = "阿里云OSS增量图片审核回调-测试")
@PostMapping("/images/callback")
public Result<Void> imageCallback(@RequestBody String body) {
log.info("OSS图片审核回调, body={}", body);
try {
// 解析 form-urlencoded: checksum=xxx&content=xxx%7B...%7D
String checksum = null;
String content = null;
for (String pair : body.split("&")) {
String[] kv = pair.split("=", 2);
if (kv.length == 2) {
if ("checksum".equals(kv[0])) {
checksum = kv[1];
} else if ("content".equals(kv[0])) {
content = URLDecoder.decode(kv[1], StandardCharsets.UTF_8);
}
}
}
if (content != null) {
ossCallbackService.handleImageCallback(checksum, content);
} else {
log.warn("OSS回调content为空");
}
} catch (Exception e) {
log.error("OSS回调处理异常", e);
}
ossCallbackService.handleImageCallback(body);
return Result.success();
}

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

@ -42,7 +42,7 @@ public class AuditTimeoutJob {
long thresholdTimestamp = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(timeoutMinutes);
List<MiniContentAudit> stuckAudits = contentAuditService.lambdaQuery()
.eq(MiniContentAudit::getStatus, AuditConstants.AUDIT_REVIEWING)
.eq(MiniContentAudit::getStatus, AuditConstants.STATUS_REVIEWING)
.lt(MiniContentAudit::getCreateTimestamp, thresholdTimestamp)
.list();
@ -54,15 +54,15 @@ public class AuditTimeoutJob {
for (MiniContentAudit audit : stuckAudits) {
try {
// 更新 status 为 manual_review,final_result 留空等待人工判定
// 更新 status 为 manual_review
contentAuditService.updateAuditStatus(audit.getId(),
AuditConstants.AUDIT_MANUAL_REVIEW, null);
AuditConstants.STATUS_MANUAL_REVIEW);
// 将关联的 reviewing 状态任务统一更新为 to_manual
// 将关联的 reviewing 状态任务统一更新为 manual_review
contentAuditTaskService.lambdaUpdate()
.eq(MiniContentAuditTask::getContentAuditId, audit.getId())
.eq(MiniContentAuditTask::getStatus, AuditConstants.TASK_REVIEWING)
.set(MiniContentAuditTask::getStatus, AuditConstants.TASK_TO_MANUAL)
.eq(MiniContentAuditTask::getStatus, AuditConstants.STATUS_REVIEWING)
.set(MiniContentAuditTask::getStatus, AuditConstants.STATUS_MANUAL_REVIEW)
.update();
log.info("超时审核已转人工, auditId={}, moduleCode={}, bizId={}",

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

@ -40,18 +40,10 @@ public class MiniContentAudit implements Serializable {
@Schema(description = "审核类型(从config快照)")
private String auditType;
@TableField("trigger_type")
@Schema(description = "触发类型:auto自动 / report举报")
private String triggerType;
@TableField("status")
@Schema(description = "审核状态:reviewing机审中 / passed通过 / failed不通过 / manual_review待人工 / appealing申诉中")
@Schema(description = "审核状态:reviewing审核中 / passed通过 / manual_review 待人工审核 / rejected不通过 / appealing申诉中")
private String status;
@TableField("final_result")
@Schema(description = "最终结果:passed通过 / failed未通过")
private String finalResult;
@TableField("operator")
@Schema(description = "最后操作人ID")
private Long operator;
@ -91,5 +83,9 @@ public class MiniContentAudit implements Serializable {
@Schema(description = "逻辑删除标识(0-未删除 1-已删除)")
private Boolean deleted;
@TableField("trigger_type")
@Schema(description = "触发来源:create发布/report举报")
private String triggerType;
}

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

@ -37,11 +37,11 @@ public class MiniContentAuditConfig implements Serializable {
private Boolean auditEnable;
@TableField("audit_type")
@Schema(description = "审核类型: machine机器 / manual手动 / mixed混合")
@Schema(description = "审核类型: machine机器 / manual手动")
private String auditType;
@TableField("risk_strategy")
@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;")
@Schema(description = "机审风险策略:auto:NONE → passed,MEDIUM → rejected (AI否决),HIGH → rejected (AI否决);normal:NONE → passed,MEDIUM → manual_review (转人工) ,HIGH → rejected (高风险直接否决); cautious: NONE → passed,MEDIUM → manual_review,HIGH → manual_review (全部不确定,交人工);")
private String riskStrategy;
@TableField("create_time")

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

@ -41,11 +41,11 @@ public class MiniContentAuditTask implements Serializable {
private String contentValue;
@TableField("audit_type")
@Schema(description = "审核类型: machine机器 / manual手动 / mixed混合")
@Schema(description = "审核类型: machine机器 / manual手动 / mixed混合(来自mini_content_audit)")
private String auditType;
@TableField("status")
@Schema(description = "审核状态:reviewing审核中 / success成功 /manual手动")
@Schema(description = "审核状态:reviewing审核中 / passed通过 / manual_review待人工审核 / rejected 不通过(机审按策略判定 / 人工驳回)")
private String status;
@TableField("risk_level")
@ -58,7 +58,7 @@ public class MiniContentAuditTask implements Serializable {
@TableField("confidence")
@Schema(description = "机审信任度")
private Float confidence;
private Integer confidence;
@TableField("description")
@Schema(description = "机审描述")
@ -76,10 +76,6 @@ public class MiniContentAuditTask implements Serializable {
@Schema(description = "机审异步视频任务id(轮询设置结果)")
private String taskId;
@TableField("result")
@Schema(description = "passed通过 / failed未通过")
private String result;
@TableField("operator")
@Schema(description = "人工审核操作人")
private Long operator;

2
src/main/java/com/youlai/boot/admin/model/form/AuditConfigForm.java

@ -22,7 +22,7 @@ public class AuditConfigForm {
@Schema(description = "审核类型: machine / manual / mixed")
private String auditType;
@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;")
@Schema(description = "机器审核风险策略: auto / normal / cautious")
private String riskStrategy;
}

4
src/main/java/com/youlai/boot/admin/model/vo/AuditConfigVO.java

@ -24,10 +24,10 @@ public class AuditConfigVO {
@Schema(description = "是否开启审核, false-开启, true-关闭")
private Boolean auditEnable;
@Schema(description = "审核类型: machine / manual / mixed")
@Schema(description = "审核类型: machine / manual")
private String auditType;
@Schema(description = "机器审核风险策略: none / medium / high")
@Schema(description = "机器审核风险策略: auto / normal / cautious")
private String riskStrategy;
@Schema(description = "创建时间")

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

@ -12,7 +12,7 @@ public interface AuditExecutorService {
* @param moduleCode 业务模块: animal_note / adoption_diary / user_post / *_comment / user_avatar / user_nickname / user_introduction
* @param bizId 业务数据ID
* @param content 待审核内容 {text[], images[], video[]}
* @param triggerType 触发类型: auto自动 / report举报
* @param triggerType 触发类型: create发布 / report举报
* @return {status: "passed"|"failed"|"manual_review", auditId: Long}配置关闭或无配置时返回null
*/
Map<String, Object> executeAudit(String moduleCode, Long bizId, AuditContentDTO content, String triggerType);

2
src/main/java/com/youlai/boot/admin/service/ContentAuditService.java

@ -7,7 +7,7 @@ public interface ContentAuditService extends IService<MiniContentAudit> {
MiniContentAudit createAudit(String moduleCode, Long bizId, String auditType, String triggerType);
void updateAuditStatus(Long auditId, String status, String finalResult);
void updateAuditStatus(Long auditId, String status);
MiniContentAudit getByModuleAndBizId(String moduleCode, Long bizId);

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

@ -11,8 +11,8 @@ public interface ContentAuditTaskService extends IService<MiniContentAuditTask>
void batchCreateTasks(Long auditId, String auditType, List<String> texts, List<String> images, List<String> videos);
/** 更新单个任务的机审结果 */
void updateTaskMachineResult(Long taskId, String machineResult, String riskLevel, String result, String status,
String label, Float confidence, String description, String requestId);
void updateTaskMachineResult(Long taskId, String machineResult, String riskLevel, String status,
String label, Integer confidence, String description, String requestId);
/** 查询某个审核汇总下的所有任务明细 */
List<MiniContentAuditTask> listTasksByAuditId(Long auditId);

2
src/main/java/com/youlai/boot/admin/service/OssCallbackService.java

@ -3,6 +3,6 @@ package com.youlai.boot.admin.service;
public interface OssCallbackService {
/** 处理OSS图片审核增量回调 */
void handleImageCallback(String checksum, String content);
void handleImageCallback(String body);
}

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

@ -1,6 +1,7 @@
package com.youlai.boot.admin.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.aliyun.green20220302.models.*;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@ -38,14 +39,14 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
/**
* 执行内容审核供业务层在内容创建/修改后调用
*
* @param moduleCode 业务模块编码对应 mini_content_audit_config.module_code
* @param bizId 业务数据ID日记ID / 评论ID
* @param moduleCode 业务模块编码
* @param bizId 业务数据ID
* @param content 待审核内容 {texts, images, videos}
* @return {status, auditId}配置关闭或无配置时返回 null调用方以此判断是否跳过审核
* @return {status, auditId}配置关闭或无配置时返回 null
*/
@Override
public Map<String, Object> executeAudit(String moduleCode, Long bizId, AuditContentDTO content, String triggerType) {
// 1) 查询对应模块的审核配置
// 1) 查询审核配置
MiniContentAuditConfig config = findAuditConfig(moduleCode);
if (config == null || isAuditDisabled(config)) {
return null;
@ -53,163 +54,45 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
String auditType = config.getAuditType();
// 2) 创建审核汇总记录
// 2) 创建审核汇总
MiniContentAudit audit = contentAuditService.createAudit(moduleCode, bizId, auditType, triggerType);
Long auditId = audit.getId();
// 3) 拆解待审核内容,批量创建审核任务明细
// 3) 拆解内容,批量创建审核任务
contentAuditTaskService.batchCreateTasks(auditId, auditType,
content.getTexts(), content.getImages(), content.getVideos());
// 4) 根据审核类型执行不同策略
// 4) 按审核类型执行
if (AuditConstants.AUDIT_TYPE_MANUAL.equals(auditType)) {
// 人工审核
return executeManualAudit(auditId);
}
if (AuditConstants.AUDIT_TYPE_MIXED.equals(auditType)) {
return executeMixedAudit(auditId);
}
// machine(默认):调 AI 并按策略裁决
// machine:调 AI 并按策略裁决
String strictness = config.getRiskStrategy() != null ? config.getRiskStrategy() : AuditConstants.STRATEGY_NORMAL;
List<MiniContentAuditTask> tasks = contentAuditTaskService.listTasksByAuditId(auditId);
executeBatchAuditByType(tasks, strictness);
executeBatchAuditByType(tasks, strictness); // 进入机器审核
List<MiniContentAuditTask> updatedTasks = contentAuditTaskService.listTasksByAuditId(auditId);
return aggregateTaskResultsAndUpdateAudit(auditId, updatedTasks);
}
/**
* manual 审核不调 AI所有任务直接标记为 manualaudit 状态设为 manual_review
* manual 人工审核不调 AI所有任务直接标记为 manual_reviewaudit 状态设为 manual_review
*/
private Map<String, Object> executeManualAudit(Long auditId) {
contentAuditTaskService.lambdaUpdate()
.eq(MiniContentAuditTask::getContentAuditId, auditId)
.set(MiniContentAuditTask::getStatus, AuditConstants.TASK_TO_MANUAL)
.set(MiniContentAuditTask::getStatus, AuditConstants.STATUS_MANUAL_REVIEW)
.update();
contentAuditService.updateAuditStatus(auditId, AuditConstants.AUDIT_MANUAL_REVIEW, null);
contentAuditService.updateAuditStatus(auditId, AuditConstants.STATUS_MANUAL_REVIEW);
Map<String, Object> result = new HashMap<>();
result.put("status", AuditConstants.AUDIT_MANUAL_REVIEW);
result.put("status", AuditConstants.STATUS_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);
executeBatchMixedAuditByType(tasks);
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 executeBatchMixedAuditByType(List<MiniContentAuditTask> tasks) {
Map<String, List<MiniContentAuditTask>> grouped = tasks.stream()
.collect(Collectors.groupingBy(MiniContentAuditTask::getContentType));
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);
processBatchTextResponseForMixed(textTasks, r);
} catch (Exception e) {
log.error("mixed批量文本审核失败", e);
}
}
List<MiniContentAuditTask> imageTasks = grouped.getOrDefault("image", List.of());
if (!imageTasks.isEmpty()) {
try {
List<String> urls = imageTasks.stream().map(MiniContentAuditTask::getContentValue).toList();
ImageModerationResponse r = aliyunContentAuditUtil.batchImageModeration(urls);
processBatchImageResponseForMixed(imageTasks, r);
} catch (Exception e) {
log.error("mixed批量图片审核失败", e);
}
}
List<MiniContentAuditTask> videoTasks = grouped.getOrDefault("video", List.of());
for (MiniContentAuditTask task : videoTasks) {
try {
handleVideoAudit(task, task.getContentValue());
} catch (Exception e) {
log.error("mixed视频审核失败, taskId={}", task.getId(), e);
}
}
}
/**
* 批量文本审核结果仅记录风险信息mixed 模式
*/
private void processBatchTextResponseForMixed(List<MiniContentAuditTask> tasks, TextModerationPlusResponse response) {
String machineResultJson = JSON.toJSONString(response);
String requestId = extractRequestIdFromResponse(response);
List<?> dataList = extractDataListFromResponse(response);
for (int i = 0; i < tasks.size(); i++) {
MiniContentAuditTask task = tasks.get(i);
if (i < (dataList != null ? dataList.size() : 0)) {
Object data = dataList.get(i);
String riskLevel = extractRiskLevelFromDataItem(data);
String label = extractLabelFromDataItem(data);
Float confidence = extractConfidenceFromDataItem(data);
String description = extractDescriptionFromDataItem(data);
contentAuditTaskService.updateTaskMachineResult(
task.getId(), machineResultJson, riskLevel, null, null,
label, confidence, description, requestId);
}
}
}
/**
* 批量图片审核结果仅记录风险信息mixed 模式
*/
private void processBatchImageResponseForMixed(List<MiniContentAuditTask> tasks, ImageModerationResponse response) {
String machineResultJson = JSON.toJSONString(response);
String requestId = extractRequestIdFromResponse(response);
List<?> dataList = extractDataListFromResponse(response);
for (int i = 0; i < tasks.size(); i++) {
MiniContentAuditTask task = tasks.get(i);
if (i < (dataList != null ? dataList.size() : 0)) {
Object data = dataList.get(i);
String riskLevel = extractRiskLevelFromDataItem(data);
String label = extractLabelFromDataItem(data);
Float confidence = extractConfidenceFromDataItem(data);
String description = extractDescriptionFromDataItem(data);
contentAuditTaskService.updateTaskMachineResult(
task.getId(), machineResultJson, riskLevel, null, null,
label, confidence, description, requestId);
}
}
}
/**
* 查找审核配置
*/
private MiniContentAuditConfig findAuditConfig(String moduleCode) {
LambdaQueryWrapper<MiniContentAuditConfig> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(MiniContentAuditConfig::getModuleCode, moduleCode);
queryWrapper.eq(MiniContentAuditConfig::getDeleted, false);
queryWrapper.last("LIMIT 1");
return contentAuditConfigService.getOne(queryWrapper);
}
/**
* 判断审核是否已关闭
* MiniContentAuditConfig.auditEnable: false=开启审核, true=关闭审核
*/
private boolean isAuditDisabled(MiniContentAuditConfig config) {
Boolean auditEnable = config.getAuditEnable();
return Boolean.TRUE.equals(auditEnable);
}
/**
* 按类型分组批量执行机审machine 模式 AI 并按策略裁决
* 按类型分组批量执行机审
*/
private void executeBatchAuditByType(List<MiniContentAuditTask> tasks, String strictness) {
Map<String, List<MiniContentAuditTask>> grouped = tasks.stream()
@ -225,7 +108,7 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
log.error("批量文本审核失败", e);
for (MiniContentAuditTask task : textTasks) {
contentAuditTaskService.updateTaskMachineResult(
task.getId(), null, null, null, AuditConstants.TASK_TO_MANUAL,
task.getId(), null, null, AuditConstants.STATUS_MANUAL_REVIEW,
null, null, null, null);
}
}
@ -233,15 +116,14 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
List<MiniContentAuditTask> imageTasks = grouped.getOrDefault("image", List.of());
if (!imageTasks.isEmpty()) {
for (MiniContentAuditTask task : imageTasks) {
try {
List<String> urls = imageTasks.stream().map(MiniContentAuditTask::getContentValue).toList();
ImageModerationResponse r = aliyunContentAuditUtil.batchImageModeration(urls);
processBatchImageResponse(imageTasks, r, strictness);
ImageModerationResponse r = aliyunContentAuditUtil.imageModeration(task.getContentValue());
processSingleImageResponse(task, r, strictness);
} catch (Exception e) {
log.error("批量图片审核失败", e);
for (MiniContentAuditTask task : imageTasks) {
log.error("图片审核失败, taskId={}", task.getId(), e);
contentAuditTaskService.updateTaskMachineResult(
task.getId(), null, null, null, AuditConstants.TASK_TO_MANUAL,
task.getId(), null, null, AuditConstants.STATUS_MANUAL_REVIEW,
null, null, null, null);
}
}
@ -255,29 +137,47 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
/**
* 批量文本审核结果处理machine 模式按策略裁决
* <p>
* body.data 结构{ riskLevel: "high", result: [{ label, confidence, description }, ...] }
*/
private void processBatchTextResponse(List<MiniContentAuditTask> tasks, TextModerationPlusResponse response, String strictness) {
String machineResultJson = JSON.toJSONString(response);
String requestId = extractRequestIdFromResponse(response);
List<?> dataList = extractDataListFromResponse(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);
}
return;
}
String overallRiskLevel = 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 (i < (dataList != null ? dataList.size() : 0)) {
Object data = dataList.get(i);
String riskLevel = extractRiskLevelFromDataItem(data);
String label = extractLabelFromDataItem(data);
Float confidence = extractConfidenceFromDataItem(data);
String description = extractDescriptionFromDataItem(data);
if (riskLevel == null) {
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, null, AuditConstants.TASK_TO_MANUAL,
task.getId(), machineResultJson, null, AuditConstants.STATUS_MANUAL_REVIEW,
label, confidence, description, requestId);
} else {
String result = applyStrategy(riskLevel, strictness);
String taskStatus = AuditConstants.RESULT_PASSED.equals(result)
? AuditConstants.TASK_SUCCESS : AuditConstants.TASK_TO_MANUAL;
String status = applyStrategy(overallRiskLevel, strictness);
contentAuditTaskService.updateTaskMachineResult(
task.getId(), machineResultJson, riskLevel, result, taskStatus,
task.getId(), machineResultJson, overallRiskLevel, status,
label, confidence, description, requestId);
}
}
@ -285,35 +185,39 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
}
/**
* 批量图片审核结果处理machine 模式按策略裁决
* 单张图片审核结果处理machine 模式按策略裁决
*/
private void processBatchImageResponse(List<MiniContentAuditTask> tasks, ImageModerationResponse response, String strictness) {
private void processSingleImageResponse(MiniContentAuditTask task, ImageModerationResponse response, String strictness) {
String machineResultJson = JSON.toJSONString(response);
String requestId = extractRequestIdFromResponse(response);
List<?> dataList = extractDataListFromResponse(response);
for (int i = 0; i < tasks.size(); i++) {
MiniContentAuditTask task = tasks.get(i);
if (i < (dataList != null ? dataList.size() : 0)) {
Object data = dataList.get(i);
String riskLevel = extractRiskLevelFromDataItem(data);
String label = extractLabelFromDataItem(data);
Float confidence = extractConfidenceFromDataItem(data);
String description = extractDescriptionFromDataItem(data);
JSONObject dataObj = extractDataObjectFromResponse(response);
if (dataObj == null) {
log.info("图片审核返回data为空, taskId={}", task.getId());
contentAuditTaskService.updateTaskMachineResult(
task.getId(), machineResultJson, null, AuditConstants.STATUS_MANUAL_REVIEW,
null, null, null, requestId);
return;
}
String riskLevel = dataObj.getString("riskLevel");
String label = dataObj.getString("label");
int confidence = dataObj.getIntValue("confidence");
String description = dataObj.getString("description");
log.info("图片审核结果: taskId={}, riskLevel={}, label={}, confidence={}, description={}",
task.getId(), riskLevel, label, confidence, description);
if (riskLevel == null) {
contentAuditTaskService.updateTaskMachineResult(
task.getId(), machineResultJson, null, null, AuditConstants.TASK_TO_MANUAL,
task.getId(), machineResultJson, null, AuditConstants.STATUS_MANUAL_REVIEW,
label, confidence, description, requestId);
} else {
String result = applyStrategy(riskLevel, strictness);
String taskStatus = AuditConstants.RESULT_PASSED.equals(result)
? AuditConstants.TASK_SUCCESS : AuditConstants.TASK_TO_MANUAL;
String status = applyStrategy(riskLevel, strictness);
contentAuditTaskService.updateTaskMachineResult(
task.getId(), machineResultJson, riskLevel, result, taskStatus,
task.getId(), machineResultJson, riskLevel, status,
label, confidence, description, requestId);
}
}
}
}
/**
* 视频审核异步
@ -341,109 +245,62 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
return response.getBody().getData().getTaskId();
}
/** 从响应的 body 中提取 requestId */
/** 反射提取 body.requestId */
private String extractRequestIdFromResponse(Object response) {
try {
Object body = invokeGetter(response, "getBody");
Method getBody = response.getClass().getMethod("getBody");
Object body = getBody.invoke(response);
if (body == null) return null;
Object requestId = invokeGetter(body, "getRequestId");
Method getRequestId = body.getClass().getMethod("getRequestId");
Object requestId = getRequestId.invoke(body);
return requestId != null ? requestId.toString() : null;
} catch (Exception e) {
return null;
}
}
/** 从响应的 body 中提取 data 列表 */
private List<?> extractDataListFromResponse(Object response) {
/** 反射提取 body.data,转为 JSONObject */
private JSONObject extractDataObjectFromResponse(Object response) {
try {
Object body = invokeGetter(response, "getBody");
Method getBody = response.getClass().getMethod("getBody");
Object body = getBody.invoke(response);
if (body == null) return null;
Object data = invokeGetter(body, "getData");
if (data instanceof List<?> list) return list;
return null;
} catch (Exception e) {
return null;
}
}
/** 从 data 列表中单个元素提取 riskLevel */
private String extractRiskLevelFromDataItem(Object dataItem) {
try {
Object riskLevel = invokeGetter(dataItem, "getRiskLevel");
return riskLevel != null ? riskLevel.toString() : null;
} catch (Exception e) {
return null;
}
}
/** 从 data 列表中单个元素提取 label */
private String extractLabelFromDataItem(Object dataItem) {
try {
Object label = invokeGetter(dataItem, "getLabel");
return label != null ? label.toString() : null;
} catch (Exception e) {
return null;
}
}
/** 从 data 列表中单个元素提取 confidence */
private Float extractConfidenceFromDataItem(Object dataItem) {
try {
Object confidence = invokeGetter(dataItem, "getConfidence");
if (confidence instanceof Number n) return n.floatValue();
return null;
} catch (Exception e) {
return null;
}
}
/** 从 data 列表中单个元素提取 description */
private String extractDescriptionFromDataItem(Object dataItem) {
try {
Object description = invokeGetter(dataItem, "getDescription");
return description != null ? description.toString() : null;
Method getData = body.getClass().getMethod("getData");
Object data = getData.invoke(body);
if (data == null) return null;
return JSON.parseObject(JSON.toJSONString(data));
} catch (Exception e) {
return null;
}
}
/**
* 反射调用目标对象的 getter 方法
*/
private Object invokeGetter(Object target, String methodName) throws Exception {
Method method = target.getClass().getMethod(methodName);
return method.invoke(target);
}
// ================================================================
// 策略映射
// ================================================================
/**
* TODO 根据 strictness 策略 riskLevel 映射为任务 result
* 根据 riskLevel + strictness返回 task status
* <pre>
* auto: NONEpassed, MEDIUMfailed, HIGHfailed
* normal: NONEpassed, MEDIUMmanual, HIGHfailed
* cautious: NONEpassed, MEDIUMmanual, HIGHmanual
* auto: NONEpassed, MEDIUMrejected, HIGHrejected
* normal: NONEpassed, MEDIUMmanual_review, HIGHrejected
* cautious: NONEpassed, MEDIUMmanual_review, HIGHmanual_review
* </pre>
*/
private String applyStrategy(String riskLevel, String strictness) {
if (AuditConstants.RISK_NONE.equals(riskLevel)) {
return AuditConstants.RESULT_PASSED;
return AuditConstants.STATUS_PASSED;
}
if (AuditConstants.STRATEGY_CAUTIOUS.equals(strictness)) {
// cautious: 非 NONE 只更新审核状态 → 一律转人工
return null;
return AuditConstants.STATUS_MANUAL_REVIEW;
}
if (AuditConstants.STRATEGY_AUTO.equals(strictness)) {
// auto: 非 NONE 一律不通过
return AuditConstants.RESULT_FAILED;
return AuditConstants.STATUS_REJECTED;
}
// normal(默认):MEDIUM→ 审核状态转人工,HIGH→不通过
// normal(默认):MEDIUM→manual_review, HIGH→rejected
if (AuditConstants.RISK_MEDIUM.equals(riskLevel)) {
return null;
return AuditConstants.STATUS_MANUAL_REVIEW;
}
return AuditConstants.RESULT_FAILED;
return AuditConstants.STATUS_REJECTED;
}
// ================================================================
@ -451,74 +308,58 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
// ================================================================
/**
* 汇总所有任务结果更新 audit 最终状态
* 汇总所有任务状态更新 audit
* <p>
* 汇总规则
* - 任一 task.result = failed audit.status = failed
* - task.result = to_manual audit.status = manual_review
* - 全部 task.result = passed audit.status = passed
* - 视频任务未完成result null 保持 reviewing
* </pre>
* <p>
* MiniContentAudit 字段含义
* - status: reviewing(机审中) / passed(通过) / failed(不通过) / manual_review(待人工) / appealing(申诉中)
* - final_result: passed / failed仅终态manual_review 时留空等待人工判定
* - 任一 task.status = rejected audit.status = rejected
* - task.status = manual_review audit.status = manual_review
* - 全部 task.status = passed audit.status = passed
* - 视频任务未完成status = reviewing 保持 reviewing
*/
private Map<String, Object> aggregateTaskResultsAndUpdateAudit(Long auditId, List<MiniContentAuditTask> tasks) {
boolean hasFailedTask = false;
boolean hasManualTask = false;
boolean allTasksPassed = true;
boolean hasRejected = false;
boolean hasManualReview = false;
boolean allPassed = true;
for (MiniContentAuditTask task : tasks) {
String taskResult = task.getResult();
String taskStatus = task.getStatus();
if (AuditConstants.RESULT_FAILED.equals(taskResult)) {
hasFailedTask = true;
allTasksPassed = false;
break; // 已有 failed,无需继续遍历
if (AuditConstants.STATUS_REJECTED.equals(taskStatus)) {
hasRejected = true;
allPassed = false;
break;
}
if (AuditConstants.TASK_TO_MANUAL.equals(task.getStatus())) {
hasManualTask = true;
allTasksPassed = false;
} else if (!AuditConstants.RESULT_PASSED.equals(taskResult)) {
// result 为 null 或其他未知值 → 未完成
allTasksPassed = false;
if (AuditConstants.STATUS_MANUAL_REVIEW.equals(taskStatus)) {
hasManualReview = true;
allPassed = false;
} else if (!AuditConstants.STATUS_PASSED.equals(taskStatus)) {
allPassed = false;
}
}
// 视频任务 result 为 null 时, 不能算通过
// 视频任务未完成时不能算通过
for (MiniContentAuditTask task : tasks) {
if ("video".equals(task.getContentType()) && task.getResult() == null) {
allTasksPassed = false;
if ("video".equals(task.getContentType()) && AuditConstants.STATUS_REVIEWING.equals(task.getStatus())) {
allPassed = false;
break;
}
}
// 根据汇总结果确定 audit 状态
String auditStatus;
String finalResultValue;
if (hasFailedTask) {
auditStatus = AuditConstants.AUDIT_FAILED;
finalResultValue = "failed";
} else if (hasManualTask) {
auditStatus = AuditConstants.AUDIT_MANUAL_REVIEW;
finalResultValue = null; // 转人工不是终态,final_result 留空等待人工判定
} else if (allTasksPassed && !tasks.isEmpty()) {
auditStatus = AuditConstants.AUDIT_PASSED;
finalResultValue = "passed";
if (hasRejected) {
auditStatus = AuditConstants.STATUS_REJECTED;
} else if (hasManualReview) {
auditStatus = AuditConstants.STATUS_MANUAL_REVIEW;
} else if (allPassed && !tasks.isEmpty()) {
auditStatus = AuditConstants.STATUS_PASSED;
} else {
// 有任务未完成(如视频异步等待中)
auditStatus = AuditConstants.AUDIT_REVIEWING;
finalResultValue = null;
auditStatus = AuditConstants.STATUS_REVIEWING;
}
// 非 reviewing 状态下更新 audit(manual_review 虽无 finalResult 也要更新 status)
if (!AuditConstants.AUDIT_REVIEWING.equals(auditStatus)) {
contentAuditService.updateAuditStatus(auditId, auditStatus, finalResultValue);
if (!AuditConstants.STATUS_REVIEWING.equals(auditStatus)) {
contentAuditService.updateAuditStatus(auditId, auditStatus);
}
// 返回给业务层的审核结果
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("status", auditStatus);
resultMap.put("auditId", auditId);
@ -529,12 +370,6 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
// 视频异步审核结果轮询
// ================================================================
/**
* 轮询所有待处理的视频审核异步结果更新任务和汇总状态
* 供定时任务 {@code VideoAuditPollJob} 调用
*
* @return 本次成功处理的视频任务数量
*/
@Override
public int pollVideoAuditResults() {
List<MiniContentAuditTask> pendingTasks = contentAuditTaskService.getPendingVideoTasks();
@ -557,7 +392,7 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
}
}
// machine 类型需要重新汇总,mixed 的 audit 已为 manual_review 不参与汇总
// machine 类型需要重新汇总
for (Long auditId : affectedAuditIds) {
try {
MiniContentAudit audit = contentAuditService.getById(auditId);
@ -574,9 +409,6 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
return processedCount;
}
/**
* 处理单条视频审核任务的异步结果返回 true 表示结果已回填完成
*/
private boolean processSingleVideoTask(MiniContentAuditTask task) {
String asyncTaskId = task.getTaskId();
VideoModerationResultResponse response = aliyunContentAuditUtil.videoModerationResult(asyncTaskId);
@ -604,54 +436,35 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
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",
null, null, null, null);
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.RESULT_PASSED.equals(result)
? AuditConstants.TASK_SUCCESS : AuditConstants.TASK_TO_MANUAL;
String status = applyStrategy(riskLevel, strictness);
contentAuditTaskService.updateTaskMachineResult(
task.getId(), dataJson, riskLevel, result, taskStatus,
task.getId(), dataJson, riskLevel, status,
null, null, null, null);
log.info("视频任务审核完成, taskId={}, riskLevel={}, strictness={}, result={}",
task.getId(), riskLevel, strictness, result);
log.info("视频任务审核完成, taskId={}, riskLevel={}, strictness={}, status={}",
task.getId(), riskLevel, strictness, status);
return true;
}
/**
* 判断视频异步审核是否已完成
* 阿里云视频审核完成后 data 中会出现 RiskLevel 字段none/medium/high
* 未完成时该字段不存在
*/
private boolean isVideoAuditCompleted(String dataJson) {
try {
JSONObject json = JSON.parseObject(dataJson);
@ -662,10 +475,6 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
}
}
/**
* 从视频审核结果JSON中直接提取 riskLevelnone/medium/high
* 阿里云视频审核完成后 data 中包含小写 riskLevel 字段
*/
private String extractVideoRiskLevel(String dataJson) {
try {
JSONObject json = JSON.parseObject(dataJson);
@ -675,4 +484,23 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
return null;
}
}
/**
* 查找审核配置
*/
private MiniContentAuditConfig findAuditConfig(String moduleCode) {
LambdaQueryWrapper<MiniContentAuditConfig> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(MiniContentAuditConfig::getModuleCode, moduleCode);
queryWrapper.eq(MiniContentAuditConfig::getDeleted, false);
queryWrapper.last("LIMIT 1");
return contentAuditConfigService.getOne(queryWrapper);
}
/**
* 判断审核是否已关闭
*/
private boolean isAuditDisabled(MiniContentAuditConfig config) {
Boolean auditEnable = config.getAuditEnable();
return Boolean.TRUE.equals(auditEnable);
}
}

6
src/main/java/com/youlai/boot/admin/service/impl/ContentAuditAppealServiceImpl.java

@ -83,12 +83,12 @@ public class ContentAuditAppealServiceImpl implements ContentAuditAppealService
if (audit != null) {
if ("approved".equals(form.getResult())) {
contentAuditService.updateAuditStatus(appeal.getAuditId(),
AuditConstants.AUDIT_PASSED, AuditConstants.RESULT_PASSED);
AuditConstants.STATUS_PASSED);
log.info("申诉通过, auditId={}, 审核状态已改为passed", appeal.getAuditId());
} else {
contentAuditService.updateAuditStatus(appeal.getAuditId(),
AuditConstants.AUDIT_FAILED, AuditConstants.RESULT_FAILED);
log.info("申诉驳回, auditId={}, 审核状态维持failed", appeal.getAuditId());
AuditConstants.STATUS_REJECTED);
log.info("申诉驳回, auditId={}, 审核状态维持rejected", appeal.getAuditId());
}
}
}

5
src/main/java/com/youlai/boot/admin/service/impl/ContentAuditServiceImpl.java

@ -23,7 +23,7 @@ public class ContentAuditServiceImpl extends ServiceImpl<MiniContentAuditMapper,
entity.setBizId(bizId);
entity.setAuditType(auditType);
entity.setTriggerType(triggerType);
entity.setStatus(AuditConstants.AUDIT_REVIEWING);
entity.setStatus(AuditConstants.STATUS_REVIEWING);
entity.setCreateBy(SecurityUtils.getUserId());
entity.setCreateTime(new Date());
entity.setCreateTimestamp(System.currentTimeMillis());
@ -32,11 +32,10 @@ public class ContentAuditServiceImpl extends ServiceImpl<MiniContentAuditMapper,
}
@Override
public void updateAuditStatus(Long auditId, String status, String finalResult) {
public void updateAuditStatus(Long auditId, String status) {
MiniContentAudit entity = new MiniContentAudit();
entity.setId(auditId);
entity.setStatus(status);
entity.setFinalResult(finalResult);
entity.setUpdateBy(SecurityUtils.getUserId());
entity.setUpdateTime(new Date());
entity.setUpdateTimestamp(System.currentTimeMillis());

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

@ -67,20 +67,19 @@ public class ContentAuditTaskServiceImpl extends ServiceImpl<MiniContentAuditTas
task.setContentType(contentType);
task.setContentValue(contentValue);
task.setAuditType(auditType);
task.setStatus(AuditConstants.TASK_REVIEWING);
task.setStatus(AuditConstants.STATUS_REVIEWING);
task.setCreateTime(now);
task.setCreateTimestamp(nowTs);
return task;
}
@Override
public void updateTaskMachineResult(Long taskId, String machineResult, String riskLevel, String result, String status,
String label, Float confidence, String description, String requestId) {
public void updateTaskMachineResult(Long taskId, String machineResult, String riskLevel, String status,
String label, Integer confidence, String description, String requestId) {
MiniContentAuditTask entity = new MiniContentAuditTask();
entity.setId(taskId);
entity.setMachineResult(machineResult);
entity.setRiskLevel(riskLevel);
entity.setResult(result);
entity.setStatus(status);
entity.setLabel(label);
entity.setConfidence(confidence);
@ -102,7 +101,7 @@ public class ContentAuditTaskServiceImpl extends ServiceImpl<MiniContentAuditTas
public List<MiniContentAuditTask> getPendingVideoTasks() {
return this.list(new LambdaQueryWrapper<MiniContentAuditTask>()
.eq(MiniContentAuditTask::getContentType, "video")
.eq(MiniContentAuditTask::getStatus, AuditConstants.TASK_REVIEWING)
.eq(MiniContentAuditTask::getStatus, AuditConstants.STATUS_REVIEWING)
.isNotNull(MiniContentAuditTask::getTaskId)
.ne(MiniContentAuditTask::getTaskId, "")
.and(w -> w.isNull(MiniContentAuditTask::getMachineResult)

108
src/main/java/com/youlai/boot/admin/service/impl/OssCallbackServiceImpl.java

@ -4,29 +4,22 @@ 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.MiniContentAuditConfig;
import com.youlai.boot.admin.model.entity.MiniContentAuditTask;
import com.youlai.boot.admin.service.ContentAuditConfigService;
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;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.List;
@Service
@Slf4j
public class OssCallbackServiceImpl implements OssCallbackService {
private final ContentAuditTaskService contentAuditTaskService;
private final ContentAuditService contentAuditService;
private final ContentAuditConfigService contentAuditConfigService;
@Value("${audit.aliyun.oss.callbackUid}")
private String callbackUid;
@ -34,16 +27,32 @@ public class OssCallbackServiceImpl implements OssCallbackService {
@Value("${audit.aliyun.oss.callbackSeed}")
private String callbackSeed;
public OssCallbackServiceImpl(ContentAuditTaskService contentAuditTaskService,
ContentAuditService contentAuditService,
ContentAuditConfigService contentAuditConfigService) {
public OssCallbackServiceImpl(ContentAuditTaskService contentAuditTaskService) {
this.contentAuditTaskService = contentAuditTaskService;
this.contentAuditService = contentAuditService;
this.contentAuditConfigService = contentAuditConfigService;
}
@Override
public void handleImageCallback(String checksum, String content) {
public void handleImageCallback(String body) {
log.info("OSS图片审核回调, body={}", body);
// 解析 form-urlencoded: checksum=xxx&content=xxx%7B...%7D
String checksum = null;
String content = null;
for (String pair : body.split("&")) {
String[] kv = pair.split("=", 2);
if (kv.length == 2) {
if ("checksum".equals(kv[0])) {
checksum = kv[1];
} else if ("content".equals(kv[0])) {
content = URLDecoder.decode(kv[1], StandardCharsets.UTF_8);
}
}
}
if (content == null) {
log.warn("OSS回调content为空");
return;
}
// 1. SHA256 验签
if (!verifyChecksum(content, checksum)) {
log.warn("OSS回调验签失败, checksum={}, content={}", checksum, content);
@ -71,8 +80,7 @@ public class OssCallbackServiceImpl implements OssCallbackService {
log.info("OSS图片审核回调, object={}, riskLevel={}, requestId={}", ossObjectName, riskLevel, requestId);
// 3. 根据 ossObjectName 匹配审核任务 //TODO 这个接口回调的是 OSS增量图片数据,如果用OSS内容审核,那么审核任务将不会存在,暂时先打印一下数据就行,另外想方案;
//TODO ,如果上传图片就创建一个审核任务,这样确实能这样处理,暂时只讨论方案不要实现;
// 3. 根据 ossObjectName 匹配审核任务
MiniContentAuditTask task = findTaskByImageName(ossObjectName);
if (task == null) {
log.warn("未找到匹配的审核任务, ossObjectName={}", ossObjectName);
@ -96,53 +104,8 @@ public class OssCallbackServiceImpl implements OssCallbackService {
}
}
// 5. 查找配置获取策略
MiniContentAudit audit = contentAuditService.getById(task.getContentAuditId());
String normalizedRisk = normalizeRiskLevel(riskLevel);
if (audit != null && AuditConstants.AUDIT_TYPE_MACHINE.equals(audit.getAuditType())) {
// machine:按策略裁决
MiniContentAuditConfig config = contentAuditConfigService.getOne(
new LambdaQueryWrapper<MiniContentAuditConfig>()
.eq(MiniContentAuditConfig::getModuleCode, audit.getModuleCode())
.eq(MiniContentAuditConfig::getDeleted, false)
.last("LIMIT 1"));
String strictness = (config != null && config.getRiskStrategy() != null)
? config.getRiskStrategy() : AuditConstants.STRATEGY_NORMAL;
String result = applyStrategy(normalizedRisk, strictness);
String taskStatus = AuditConstants.RESULT_PASSED.equals(result)
? AuditConstants.TASK_SUCCESS : AuditConstants.TASK_TO_MANUAL;
contentAuditTaskService.updateTaskMachineResult(
task.getId(), payload.toJSONString(), normalizedRisk, result, taskStatus,
label, confidence, description, requestId);
// 重新汇总 audit
List<MiniContentAuditTask> tasks = contentAuditTaskService.listTasksByAuditId(task.getContentAuditId());
boolean hasFailed = false, hasManual = false, allPassed = true;
for (MiniContentAuditTask t : tasks) {
if (AuditConstants.RESULT_FAILED.equals(t.getResult())) { hasFailed = true; break; }
if (AuditConstants.TASK_TO_MANUAL.equals(t.getStatus())) hasManual = true;
if (!AuditConstants.RESULT_PASSED.equals(t.getResult())) allPassed = false;
}
for (MiniContentAuditTask t : tasks) {
if ("video".equals(t.getContentType()) && t.getResult() == null) { allPassed = false; break; }
}
if (hasFailed) {
contentAuditService.updateAuditStatus(audit.getId(), AuditConstants.AUDIT_FAILED, "failed");
} else if (hasManual) {
contentAuditService.updateAuditStatus(audit.getId(), AuditConstants.AUDIT_MANUAL_REVIEW, null);
} else if (allPassed && !tasks.isEmpty()) {
contentAuditService.updateAuditStatus(audit.getId(), AuditConstants.AUDIT_PASSED, "passed");
}
} else {
// mixed / manual:仅记录机审信息
contentAuditTaskService.updateTaskMachineResult(
task.getId(), payload.toJSONString(), normalizedRisk, null, null,
label, confidence, description, requestId);
}
log.info("OSS回调处理完成, taskId={}, riskLevel={}", task.getId(), normalizedRisk);
log.info("OSS回调解析完成, taskId={}, ossObjectName={}, riskLevel={}, label={}, confidence={}, description={}, requestId={}",
task.getId(), ossObjectName, riskLevel, label, confidence, description, requestId);
}
/** SHA256(uid + seed + content) 验签,对应阿里云控制台配置的加密算法 */
@ -162,17 +125,6 @@ public class OssCallbackServiceImpl implements OssCallbackService {
}
}
/** 阿里云 riskLevel 统一为小写 */
private String normalizeRiskLevel(String riskLevel) {
if (riskLevel == null) return null;
return switch (riskLevel.toLowerCase()) {
case "none", "normal" -> AuditConstants.RISK_NONE;
case "medium", "review" -> AuditConstants.RISK_MEDIUM;
case "high", "block" -> AuditConstants.RISK_HIGH;
default -> riskLevel.toLowerCase();
};
}
/** 通过 ossObjectName(如 v2-xxx_r.jpg)匹配审核任务中图片 URL 尾部 */
private MiniContentAuditTask findTaskByImageName(String ossObjectName) {
if (ossObjectName == null) return null;
@ -181,12 +133,4 @@ public class OssCallbackServiceImpl implements OssCallbackService {
.like(MiniContentAuditTask::getContentValue, ossObjectName)
.last("LIMIT 1"));
}
private String applyStrategy(String riskLevel, String strictness) {
if (AuditConstants.RISK_NONE.equals(riskLevel)) return AuditConstants.RESULT_PASSED;
if (AuditConstants.STRATEGY_CAUTIOUS.equals(strictness)) return null;
if (AuditConstants.STRATEGY_AUTO.equals(strictness)) return AuditConstants.RESULT_FAILED;
if (AuditConstants.RISK_MEDIUM.equals(riskLevel)) return null;
return AuditConstants.RESULT_FAILED;
}
}

50
src/main/java/com/youlai/boot/common/util/AliyunContentAuditUtil.java

@ -47,8 +47,6 @@ public class AliyunContentAuditUtil {
// 批量审核上限
private static final int BATCH_TEXT_MAX = 100;
private static final int BATCH_TEXT_SINGLE_MAX_CHARS = 600;
private static final int BATCH_IMAGE_MAX = 100;
private static final int BATCH_VIDEO_MAX = 10;
/**
* 构建客户端
@ -214,29 +212,7 @@ public class AliyunContentAuditUtil {
});
}
/**
* 批量图片审核上限100张
*/
public ImageModerationResponse batchImageModeration(List<String> imageUrls) {
if (imageUrls == null || imageUrls.isEmpty()) {
return null;
}
if (imageUrls.size() > BATCH_IMAGE_MAX) {
throw new MsgException("批量图片审核单次上限" + BATCH_IMAGE_MAX + "张,当前" + imageUrls.size() + "张");
}
return executeWithFailover(client -> {
JSONObject params = new JSONObject();
JSONArray arr = new JSONArray();
arr.addAll(imageUrls);
params.put("images", arr);
ImageModerationRequest request = new ImageModerationRequest()
.setService("oss_baselineCheck")
.setServiceParameters(params.toJSONString());
return client.imageModeration(request);
});
}
// 图片审核不支持批量,使用 imageModeration 逐个调用
// ===================== 视频审核(异步) =====================
//视频文件检测:videoDetection ; AI生成视频判定:videoAigcDetector ; 视频文件检测_大模型版:videoDetectionByVL
@ -253,29 +229,7 @@ public class AliyunContentAuditUtil {
});
}
/**
* 批量视频审核异步上限10个
*/
public VideoModerationResponse batchVideoModeration(List<String> videoUrls) {
if (videoUrls == null || videoUrls.isEmpty()) {
return null;
}
if (videoUrls.size() > BATCH_VIDEO_MAX) {
throw new MsgException("批量视频审核单次上限" + BATCH_VIDEO_MAX + "个,当前" + videoUrls.size() + "个");
}
return executeWithFailover(client -> {
JSONObject params = new JSONObject();
JSONArray arr = new JSONArray();
arr.addAll(videoUrls);
params.put("urls", arr);
VideoModerationRequest request = new VideoModerationRequest()
.setService("videoDetection")
.setServiceParameters(params.toJSONString());
return client.videoModeration(request);
});
}
// 视频审核不支持批量,使用 videoModeration 逐个调用
// ===================== 查询视频审核结果 =====================
public VideoModerationResultResponse videoModerationResult(String taskId) {

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

@ -35,7 +35,7 @@ public class MiniContentAuditAppealServiceImpl implements MiniContentAuditAppeal
if (audit == null) {
throw new RuntimeException("审核记录不存在");
}
if (!AuditConstants.AUDIT_FAILED.equals(audit.getStatus())) {
if (!AuditConstants.STATUS_REJECTED.equals(audit.getStatus())) {
throw new RuntimeException("当前审核状态不允许申诉");
}
@ -61,7 +61,7 @@ public class MiniContentAuditAppealServiceImpl implements MiniContentAuditAppeal
appealMapper.insert(appeal);
// 更新审核状态为申诉中
contentAuditService.updateAuditStatus(form.getAuditId(), AuditConstants.AUDIT_APPEALING, null);
contentAuditService.updateAuditStatus(form.getAuditId(), AuditConstants.STATUS_APPEALING);
log.info("用户提交申诉成功, auditId={}, userId={}", form.getAuditId(), userId);
}

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

@ -227,7 +227,7 @@ public class UserPostServiceImpl extends ServiceImpl<MiniUserPostMapper, MiniUse
Long postId = post.getId();
try {
AuditContentDTO auditContent = buildAuditContent(formData);
Map<String, Object> auditResult = auditExecutorService.executeAudit("user_post", postId, auditContent, AuditConstants.TRIGGER_AUTO);
Map<String, Object> auditResult = auditExecutorService.executeAudit("user_post", postId, auditContent, AuditConstants.TRIGGER_CREATE);
if (auditResult != null) {
log.info("用户作品审核任务已创建, postId={}, auditResult={}", postId, auditResult);
}
@ -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.AUDIT_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.TASK_TO_MANUAL)
.update();
log.info("降级人工审核记录已创建, postId={}, auditId={}", postId, audit.getId());

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

@ -0,0 +1,83 @@
package com.youlai.boot.admin;
import com.alibaba.fastjson.JSON;
import com.aliyun.green20220302.models.*;
import com.youlai.boot.common.util.AliyunContentAuditUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
/**
* 阿里云内容审核 API 在线测试直连真实 API查看返回结构
*/
@Slf4j
@SpringBootTest
class AliyunContentAuditTest {
@Autowired
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());
}
}
}

BIN
tmp/178065053524697cu0r6a

Binary file not shown.
Loading…
Cancel
Save