Browse Source

增加审核配置接口

glx_phase2
glx 2 weeks ago
parent
commit
5970ad0847
  1. 1
      .gitignore
  2. 40
      src/main/java/com/youlai/boot/admin/constant/AuditConstants.java
  3. 59
      src/main/java/com/youlai/boot/admin/controller/ContentAuditConfigController.java
  4. 18
      src/main/java/com/youlai/boot/admin/converter/AuditConfigConverter.java
  5. 21
      src/main/java/com/youlai/boot/admin/model/dto/AuditContentDTO.java
  6. 2
      src/main/java/com/youlai/boot/admin/model/entity/MiniContentAudit.java
  7. 4
      src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditConfig.java
  8. 2
      src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditTask.java
  9. 28
      src/main/java/com/youlai/boot/admin/model/form/AuditConfigForm.java
  10. 16
      src/main/java/com/youlai/boot/admin/model/query/AuditConfigQuery.java
  11. 47
      src/main/java/com/youlai/boot/admin/model/vo/AuditConfigVO.java
  12. 19
      src/main/java/com/youlai/boot/admin/service/AuditExecutorService.java
  13. 21
      src/main/java/com/youlai/boot/admin/service/ContentAuditConfigService.java
  14. 14
      src/main/java/com/youlai/boot/admin/service/ContentAuditService.java
  15. 19
      src/main/java/com/youlai/boot/admin/service/ContentAuditTaskService.java
  16. 398
      src/main/java/com/youlai/boot/admin/service/impl/AuditExecutorServiceImpl.java
  17. 105
      src/main/java/com/youlai/boot/admin/service/impl/ContentAuditConfigServiceImpl.java
  18. 51
      src/main/java/com/youlai/boot/admin/service/impl/ContentAuditServiceImpl.java
  19. 100
      src/main/java/com/youlai/boot/admin/service/impl/ContentAuditTaskServiceImpl.java
  20. 7
      src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java
  21. 2
      src/main/java/com/youlai/boot/mini/model/entity/MiniContentAuditAppeal.java
  22. 2
      src/main/java/com/youlai/boot/mini/model/entity/MiniReport.java

1
.gitignore

@ -19,3 +19,4 @@ docker/xxljob/logs
application-youlai.yml application-youlai.yml
.claude .claude
CLAUDE.md CLAUDE.md
content-audit-design.md

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

@ -0,0 +1,40 @@
package com.youlai.boot.admin.constant;
/**
* 内容审核状态常量
*/
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 = "to_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";
}

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

@ -0,0 +1,59 @@
package com.youlai.boot.admin.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.youlai.boot.admin.model.form.AuditConfigForm;
import com.youlai.boot.admin.model.query.AuditConfigQuery;
import com.youlai.boot.admin.model.vo.AuditConfigVO;
import com.youlai.boot.admin.service.ContentAuditConfigService;
import com.youlai.boot.common.annotation.Log;
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.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Tag(name = "管理端-审核配置相关接口")
@RestController
@RequestMapping("/api/v1/admin/auditConfig")
@RequiredArgsConstructor
public class ContentAuditConfigController {
private final ContentAuditConfigService contentAuditConfigService;
@Operation(summary = "分页查询审核配置列表")
@GetMapping("/list")
@Log(module = LogModuleEnum.CONTENT_AUDIT_CONFIG, value = ActionTypeEnum.LIST)
public PageResult<AuditConfigVO> listConfig(@Valid AuditConfigQuery query) {
IPage<AuditConfigVO> result = contentAuditConfigService.pageConfig(query);
return PageResult.success(result);
}
@Operation(summary = "新增审核配置")
@PostMapping("/add")
@Log(module = LogModuleEnum.CONTENT_AUDIT_CONFIG, value = ActionTypeEnum.INSERT)
public Result<AuditConfigVO> addConfig(@Valid @RequestBody AuditConfigForm form) {
AuditConfigVO vo = contentAuditConfigService.addConfig(form);
return Result.success(vo);
}
@Operation(summary = "修改审核配置")
@PostMapping("/update/{uuid}")
@Log(module = LogModuleEnum.CONTENT_AUDIT_CONFIG, value = ActionTypeEnum.UPDATE)
public Result<AuditConfigVO> updateConfig(@PathVariable String uuid, @Valid @RequestBody AuditConfigForm form) {
AuditConfigVO vo = contentAuditConfigService.updateConfig(uuid, form);
return Result.success(vo);
}
@Operation(summary = "删除审核配置")
@PostMapping("/delete/{uuid}")
@Log(module = LogModuleEnum.CONTENT_AUDIT_CONFIG, value = ActionTypeEnum.DELETE)
public Result<Void> deleteConfig(@PathVariable String uuid) {
contentAuditConfigService.deleteConfig(uuid);
return Result.success();
}
}

18
src/main/java/com/youlai/boot/admin/converter/AuditConfigConverter.java

@ -0,0 +1,18 @@
package com.youlai.boot.admin.converter;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.admin.model.entity.MiniContentAuditConfig;
import com.youlai.boot.admin.model.form.AuditConfigForm;
import com.youlai.boot.admin.model.vo.AuditConfigVO;
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring")
public interface AuditConfigConverter {
Page<AuditConfigVO> toPageVo(Page<MiniContentAuditConfig> page);
AuditConfigVO toVo(MiniContentAuditConfig entity);
MiniContentAuditConfig toEntity(AuditConfigForm form);
}

21
src/main/java/com/youlai/boot/admin/model/dto/AuditContentDTO.java

@ -0,0 +1,21 @@
package com.youlai.boot.admin.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(description = "审核内容入参")
public class AuditContentDTO {
@Schema(description = "待审核文本列表")
private List<String> texts;
@Schema(description = "待审核图片URL列表")
private List<String> images;
@Schema(description = "待审核视频URL列表")
private List<String> videos;
}

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

@ -85,7 +85,7 @@ public class MiniContentAudit implements Serializable {
@TableField("is_deleted") @TableField("is_deleted")
@Schema(description = "逻辑删除标识(0-未删除 1-已删除)") @Schema(description = "逻辑删除标识(0-未删除 1-已删除)")
private Byte isDeleted; private Boolean deleted;
} }

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

@ -34,7 +34,7 @@ public class MiniContentAuditConfig implements Serializable {
@TableField("audit_enable") @TableField("audit_enable")
@Schema(description = "是否开启审核, 0-开启, 1-关闭") @Schema(description = "是否开启审核, 0-开启, 1-关闭")
private Byte auditEnable; private Boolean auditEnable;
@TableField("audit_type") @TableField("audit_type")
@Schema(description = "审核类型: machine机器 / manual手动 / mixed混合") @Schema(description = "审核类型: machine机器 / manual手动 / mixed混合")
@ -64,7 +64,7 @@ public class MiniContentAuditConfig implements Serializable {
@TableField("is_deleted") @TableField("is_deleted")
@Schema(description = "逻辑删除标识(0-未删除 1-已删除)") @Schema(description = "逻辑删除标识(0-未删除 1-已删除)")
private Byte isDeleted; private Boolean deleted;
} }

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

@ -96,7 +96,7 @@ public class MiniContentAuditTask implements Serializable {
@TableField("is_deleted") @TableField("is_deleted")
@Schema(description = "逻辑删除标识(0-未删除 1-已删除)") @Schema(description = "逻辑删除标识(0-未删除 1-已删除)")
private Byte isDeleted; private Boolean deleted;
} }

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

@ -0,0 +1,28 @@
package com.youlai.boot.admin.model.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "审核配置表单")
@Getter
@Setter
public class AuditConfigForm {
@NotBlank(message = "业务模块编码不能为空")
@Schema(description = "业务模块: animal_note / adoption_diary / user_post / note_comment / diary_comment / post_comment / user_avatar / user_nickname / user_introduction")
private String moduleCode;
@NotNull(message = "是否开启审核不能为空")
@Schema(description = "是否开启审核, false-开启, true-关闭")
private Boolean auditEnable;
@Schema(description = "审核类型: machine / manual / mixed")
private String auditType;
@Schema(description = "机器审核风险策略: none / medium / high")
private String riskStrategy;
}

16
src/main/java/com/youlai/boot/admin/model/query/AuditConfigQuery.java

@ -0,0 +1,16 @@
package com.youlai.boot.admin.model.query;
import com.youlai.boot.common.base.BaseQuery;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Schema(description = "审核配置分页查询")
public class AuditConfigQuery extends BaseQuery {
@Schema(description = "业务模块编码")
private String moduleCode;
}

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

@ -0,0 +1,47 @@
package com.youlai.boot.admin.model.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.Date;
@Data
@Builder
@Schema(description = "审核配置列表VO")
public class AuditConfigVO {
@Schema(description = "主键ID")
private Long id;
@Schema(description = "UUID")
private String uuid;
@Schema(description = "业务模块编码")
private String moduleCode;
@Schema(description = "是否开启审核, false-开启, true-关闭")
private Boolean auditEnable;
@Schema(description = "审核类型: machine / manual / mixed")
private String auditType;
@Schema(description = "机器审核风险策略: none / medium / high")
private String riskStrategy;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
@Schema(description = "创建人ID")
private Long createBy;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
@Schema(description = "修改人ID")
private Long updateBy;
}

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

@ -0,0 +1,19 @@
package com.youlai.boot.admin.service;
import com.youlai.boot.admin.model.dto.AuditContentDTO;
import java.util.Map;
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}
* @return {status: "passed"|"failed"|"manual_review", auditId: Long}配置关闭或无配置时返回null
*/
Map<String, Object> executeAudit(String moduleCode, String bizId, AuditContentDTO content);
}

21
src/main/java/com/youlai/boot/admin/service/ContentAuditConfigService.java

@ -0,0 +1,21 @@
package com.youlai.boot.admin.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.boot.admin.model.entity.MiniContentAuditConfig;
import com.youlai.boot.admin.model.form.AuditConfigForm;
import com.youlai.boot.admin.model.query.AuditConfigQuery;
import com.youlai.boot.admin.model.vo.AuditConfigVO;
public interface ContentAuditConfigService extends IService<MiniContentAuditConfig> {
IPage<AuditConfigVO> pageConfig(AuditConfigQuery query);
AuditConfigVO getConfigByUuid(String uuid);
AuditConfigVO addConfig(AuditConfigForm form);
AuditConfigVO updateConfig(String uuid, AuditConfigForm form);
void deleteConfig(String uuid);
}

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

@ -0,0 +1,14 @@
package com.youlai.boot.admin.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.boot.admin.model.entity.MiniContentAudit;
public interface ContentAuditService extends IService<MiniContentAudit> {
MiniContentAudit createAudit(String moduleCode, String bizId, String auditType);
void updateAuditStatus(Long auditId, String status, String finalResult);
MiniContentAudit getByModuleAndBizId(String moduleCode, String bizId);
}

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

@ -0,0 +1,19 @@
package com.youlai.boot.admin.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.boot.admin.model.entity.MiniContentAuditTask;
import java.util.List;
public interface ContentAuditTaskService extends IService<MiniContentAuditTask> {
/** 批量创建审核任务(按内容类型拆分:text/image/video) */
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);
/** 查询某个审核汇总下的所有任务明细 */
List<MiniContentAuditTask> listTasksByAuditId(Long auditId);
}

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

@ -0,0 +1,398 @@
package com.youlai.boot.admin.service.impl;
import com.alibaba.fastjson.JSON;
import com.aliyun.green20220302.models.ImageModerationResponse;
import com.aliyun.green20220302.models.TextModerationResponse;
import com.aliyun.green20220302.models.VideoModerationResponse;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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.MiniContentAuditConfig;
import com.youlai.boot.admin.model.entity.MiniContentAuditTask;
import com.youlai.boot.admin.service.*;
import com.youlai.boot.common.util.AliyunContentAuditUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.lang.reflect.Method;
import java.util.*;
/**
* 内容审核核心执行器
* <p>
* 职责编排审核全流程查配置 创建汇总 拆解内容创建任务 调用阿里云机审 按策略判定结果 汇总最终状态
* 供业务层创建/修改内容时注入调用
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class AuditExecutorServiceImpl implements AuditExecutorService {
private final ContentAuditConfigService contentAuditConfigService;
private final ContentAuditService contentAuditService;
private final ContentAuditTaskService contentAuditTaskService;
private final AliyunContentAuditUtil aliyunContentAuditUtil;
/**
* 执行内容审核供业务层在内容创建/修改后调用
*
* @param moduleCode 业务模块编码对应 mini_content_audit_config.module_code
* @param bizId 业务数据ID日记ID / 评论ID
* @param content 待审核内容 {texts, images, videos}
* @return {status, auditId}配置关闭或无配置时返回 null调用方以此判断是否跳过审核
*/
@Override
public Map<String, Object> executeAudit(String moduleCode, String bizId, AuditContentDTO content) {
// 1) 查询对应模块的审核配置
MiniContentAuditConfig config = findAuditConfig(moduleCode);
if (config == null || isAuditDisabled(config)) {
// 无配置 或 审核开关已关闭 → 跳过审核,业务层直接放行
return null;
}
// 2) 创建审核汇总记录(MiniContentAudit 表)
MiniContentAudit audit = contentAuditService.createAudit(moduleCode, bizId, config.getAuditType());
Long auditId = audit.getId();
// 风险策略取自配置,默认为 medium
String riskStrategy = config.getRiskStrategy() != null ? config.getRiskStrategy() : "medium";
// 3) 拆解待审核内容,批量创建审核任务明细(MiniContentAuditTask 表)
contentAuditTaskService.batchCreateTasks(auditId, config.getAuditType(),
content.getTexts(), content.getImages(), content.getVideos());
// 4) 读取已创建的所有任务,逐一调用阿里云内容审核API
List<MiniContentAuditTask> tasks = contentAuditTaskService.listTasksByAuditId(auditId);
for (MiniContentAuditTask task : tasks) {
executeSingleTaskAudit(task, riskStrategy);
}
// 5) 重新加载任务(机审结果已回填),汇总判定 audit 的最终状态
List<MiniContentAuditTask> updatedTasks = contentAuditTaskService.listTasksByAuditId(auditId);
return aggregateTaskResultsAndUpdateAudit(auditId, updatedTasks);
}
// ================================================================
// 配置查询
// ================================================================
/**
* 查找审核配置
* 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条时抛异常
return contentAuditConfigService.getOne(queryWrapper);
}
/**
* 判断审核是否已关闭
* MiniContentAuditConfig.auditEnable: false=开启审核, true=关闭审核
*/
private boolean isAuditDisabled(MiniContentAuditConfig config) {
Boolean auditEnable = config.getAuditEnable();
return Boolean.TRUE.equals(auditEnable);
}
// ================================================================
// 单条任务机审
// ================================================================
/**
* 对一条审核任务调用阿里云内容安全API并将结果回填到任务记录
* text/image 同步返回 suggestionvideo 异步返回 taskId 后续轮询
* 依赖方法说明
* - AliyunContentAuditUtil.textModeration(String) 阿里云文本审核同步
* - AliyunContentAuditUtil.imageModeration(String) 阿里云图片审核同步
* - AliyunContentAuditUtil.videoModeration(String) 阿里云视频审核异步返回taskId
*/
private void executeSingleTaskAudit(MiniContentAuditTask task, String riskStrategy) {
String contentType = task.getContentType();
String contentValue = task.getContentValue();
try {
switch (contentType) {
case "text" -> handleTextAudit(task, contentValue, riskStrategy);
case "image" -> handleImageAudit(task, contentValue, riskStrategy);
case "video" -> handleVideoAudit(task, contentValue);
default -> log.warn("未知内容类型: {}", contentType);
}
} catch (Exception e) {
log.error("机审调用失败, taskId={}, contentType={}", task.getId(), contentType, e);
}
}
/**
* 文本审核同步
* TextModerationResponse.getBody().getData() 包含 suggestion值为 pass/review/block
*/
private void handleTextAudit(MiniContentAuditTask task, String textContent, String riskStrategy) {
TextModerationResponse response = aliyunContentAuditUtil.textModeration(textContent);
String suggestion = extractSuggestionFromResponse(response);
String machineResultJson = JSON.toJSONString(response);
applyAuditResultToTask(task, suggestion, machineResultJson, riskStrategy);
}
/**
* 图片审核同步
* ImageModerationResponse.getBody().getData() 包含 suggestion值为 pass/review/block
*/
private void handleImageAudit(MiniContentAuditTask task, String imageUrl, String riskStrategy) {
ImageModerationResponse response = aliyunContentAuditUtil.imageModeration(imageUrl);
String suggestion = extractSuggestionFromResponse(response);
String machineResultJson = JSON.toJSONString(response);
applyAuditResultToTask(task, suggestion, machineResultJson, riskStrategy);
}
/**
* 视频审核异步
* VideoModerationResponse.getBody().getData().getTaskId() 返回异步任务ID
* 写入 task.taskId 供后续 VideoModerationResult 轮询查询结果
* 视频不参与本轮汇总task.result 暂不设置
*/
private void handleVideoAudit(MiniContentAuditTask task, String videoUrl) {
VideoModerationResponse response = aliyunContentAuditUtil.videoModeration(videoUrl);
if (response == null) {
log.warn("视频审核请求返回null, taskId={}", task.getId());
return;
}
// 提取异步 taskId 并写入任务记录
String asyncTaskId = extractVideoTaskIdFromResponse(response);
if (asyncTaskId != null) {
task.setTaskId(asyncTaskId);
// MyBatis-Plus updateById:按主键id更新实体,此处只更新 task_id 字段
contentAuditTaskService.updateById(task);
}
}
/**
* VideoModerationResponse 中提取异步 taskId
* 调用路径response.getBody().getData().getTaskId()
*/
private String extractVideoTaskIdFromResponse(VideoModerationResponse response) {
if (response.getBody() == null) {
return null;
}
if (response.getBody().getData() == null) {
return null;
}
// VideoModerationResponseBody.getData() 返回 VideoModerationResponseBodyData,含 getTaskId()
return response.getBody().getData().getTaskId();
}
/**
* 将机审建议应用到任务记录suggestion risk_level 按策略判定 result 任务最终 status
*/
private void applyAuditResultToTask(MiniContentAuditTask task, String suggestion, String machineResultJson, String riskStrategy) {
if (suggestion == null) {
return;
}
// 1. suggestion → risk_level(none/medium/high)
String riskLevel = mapSuggestionToRiskLevel(suggestion);
// 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;
// 回填任务表:machine_result(JSON) / risk_level / result / status
contentAuditTaskService.updateTaskMachineResult(
task.getId(), machineResultJson, riskLevel, result, taskStatus);
}
// ================================================================
// 从阿里云响应中提取 suggestion
// ================================================================
/**
* 从阿里云审核响应中提取 suggestion pass/review/block
* 使用反射调用 getBody() getData() getSuggestion()兼容 Text Image 两种 Response
* <p>
* 如果 Data List 类型取第一个元素的 getSuggestion()
*/
private String extractSuggestionFromResponse(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;
} catch (Exception e) {
log.error("提取suggestion失败", e);
return null;
}
}
/**
* 反射调用目标对象的 getter 方法
*/
private Object invokeGetter(Object target, String methodName) throws Exception {
Method method = target.getClass().getMethod(methodName);
return method.invoke(target);
}
// ================================================================
// 策略映射
// ================================================================
/**
* 将阿里云机审 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
* <pre>
* none 策略: passpassed, 其余failed 机审直接决定无人工环节
* medium策略: passpassed, reviewto_manual, blockfailed
* high策略: passpassed, 其余to_manual 中高风险一律转人工
* </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;
}
// medium策略(默认):pass→通过,review→转人工,block→不通过
if (AuditConstants.SUGGESTION_REVIEW.equals(suggestion)) {
return AuditConstants.TASK_TO_MANUAL;
}
if (AuditConstants.SUGGESTION_BLOCK.equals(suggestion)) {
return AuditConstants.RESULT_FAILED;
}
return AuditConstants.RESULT_PASSED;
}
// ================================================================
// 结果汇总
// ================================================================
/**
* 汇总所有任务结果更新 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
*/
private Map<String, Object> aggregateTaskResultsAndUpdateAudit(Long auditId, List<MiniContentAuditTask> tasks) {
boolean hasFailedTask = false;
boolean hasManualTask = false;
boolean allTasksPassed = true;
for (MiniContentAuditTask task : tasks) {
String taskResult = task.getResult();
if (AuditConstants.RESULT_FAILED.equals(taskResult)) {
hasFailedTask = true;
allTasksPassed = false;
break; // 已有 failed,无需继续遍历
}
if (AuditConstants.TASK_TO_MANUAL.equals(taskResult)
|| AuditConstants.TASK_TO_MANUAL.equals(task.getStatus())) {
hasManualTask = true;
allTasksPassed = false;
} else if (!AuditConstants.RESULT_PASSED.equals(taskResult)) {
// result 为 null 或其他未知值 → 未完成
allTasksPassed = false;
}
}
// 视频任务 result 为 null 时, 不能算通过
for (MiniContentAuditTask task : tasks) {
if ("video".equals(task.getContentType()) && task.getResult() == null) {
allTasksPassed = false;
break;
}
}
// 根据汇总结果确定 audit 状态
String auditStatus;
String finalResultValue;
if (hasFailedTask) {
auditStatus = AuditConstants.AUDIT_FAILED;
finalResultValue = "failed";
} else if (hasManualTask) {
auditStatus = AuditConstants.AUDIT_MANUAL_REVIEW;
finalResultValue = "manual_review";
} else if (allTasksPassed && !tasks.isEmpty()) {
auditStatus = AuditConstants.AUDIT_PASSED;
finalResultValue = "passed";
} else {
// 有任务未完成(如视频异步等待中)
auditStatus = AuditConstants.AUDIT_REVIEWING;
finalResultValue = null;
}
// 汇总结果已确定则更新 audit 表
if (finalResultValue != null) {
// ContentAuditService.updateAuditStatus:按 auditId 更新 status 和 final_result
contentAuditService.updateAuditStatus(auditId, auditStatus, finalResultValue);
}
// 返回给业务层的审核结果
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("status", auditStatus);
resultMap.put("auditId", auditId);
return resultMap;
}
}

105
src/main/java/com/youlai/boot/admin/service/impl/ContentAuditConfigServiceImpl.java

@ -0,0 +1,105 @@
package com.youlai.boot.admin.service.impl;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.admin.converter.AuditConfigConverter;
import com.youlai.boot.admin.mapper.MiniContentAuditConfigMapper;
import com.youlai.boot.admin.model.entity.MiniContentAuditConfig;
import com.youlai.boot.admin.model.form.AuditConfigForm;
import com.youlai.boot.admin.model.query.AuditConfigQuery;
import com.youlai.boot.admin.model.vo.AuditConfigVO;
import com.youlai.boot.admin.service.ContentAuditConfigService;
import com.youlai.boot.common.exception.MsgException;
import com.youlai.boot.framework.security.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
@RequiredArgsConstructor
@Slf4j
public class ContentAuditConfigServiceImpl extends ServiceImpl<MiniContentAuditConfigMapper, MiniContentAuditConfig> implements ContentAuditConfigService {
private final AuditConfigConverter auditConfigConverter;
@Override
public IPage<AuditConfigVO> pageConfig(AuditConfigQuery query) {
Page<MiniContentAuditConfig> page = new Page<>(query.getPageNum(), query.getPageSize());
LambdaQueryWrapper<MiniContentAuditConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(query.getModuleCode() != null && !query.getModuleCode().isBlank(),
MiniContentAuditConfig::getModuleCode, query.getModuleCode());
wrapper.orderByDesc(MiniContentAuditConfig::getCreateTime);
Page<MiniContentAuditConfig> result = this.page(page, wrapper);
return auditConfigConverter.toPageVo(result);
}
@Override
public AuditConfigVO getConfigByUuid(String uuid) {
MiniContentAuditConfig entity = getByUuidOrThrow(uuid);
return auditConfigConverter.toVo(entity);
}
@Override
public AuditConfigVO addConfig(AuditConfigForm form) {
// 同一 moduleCode 只能有一条配置
long count = this.count(new LambdaQueryWrapper<MiniContentAuditConfig>()
.eq(MiniContentAuditConfig::getModuleCode, form.getModuleCode()));
if (count > 0) {
throw new MsgException("该业务模块已存在审核配置");
}
MiniContentAuditConfig entity = auditConfigConverter.toEntity(form);
entity.setUuid(IdUtil.fastSimpleUUID());
entity.setCreateBy(SecurityUtils.getUserId());
entity.setCreateTime(new Date());
this.save(entity);
return auditConfigConverter.toVo(entity);
}
@Override
public AuditConfigVO updateConfig(String uuid, AuditConfigForm form) {
MiniContentAuditConfig entity = getByUuidOrThrow(uuid);
// 如果修改了 moduleCode,校验唯一性
if (!entity.getModuleCode().equals(form.getModuleCode())) {
long count = this.count(new LambdaQueryWrapper<MiniContentAuditConfig>()
.eq(MiniContentAuditConfig::getModuleCode, form.getModuleCode())
.ne(MiniContentAuditConfig::getId, entity.getId()));
if (count > 0) {
throw new MsgException("该业务模块已存在审核配置");
}
}
entity.setModuleCode(form.getModuleCode());
entity.setAuditEnable(form.getAuditEnable());
entity.setAuditType(form.getAuditType());
entity.setRiskStrategy(form.getRiskStrategy());
entity.setUpdateBy(SecurityUtils.getUserId());
entity.setUpdateTime(new Date());
this.updateById(entity);
return auditConfigConverter.toVo(entity);
}
@Override
public void deleteConfig(String uuid) {
MiniContentAuditConfig entity = getByUuidOrThrow(uuid);
entity.setDeleted(true);
entity.setUpdateBy(SecurityUtils.getUserId());
entity.setUpdateTime(new Date());
this.updateById(entity);
}
private MiniContentAuditConfig getByUuidOrThrow(String uuid) {
MiniContentAuditConfig entity = this.getOne(new LambdaQueryWrapper<MiniContentAuditConfig>()
.eq(MiniContentAuditConfig::getUuid, uuid));
if (entity == null) {
throw new MsgException("审核配置不存在");
}
return entity;
}
}

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

@ -0,0 +1,51 @@
package com.youlai.boot.admin.service.impl;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.admin.constant.AuditConstants;
import com.youlai.boot.admin.mapper.MiniContentAuditMapper;
import com.youlai.boot.admin.model.entity.MiniContentAudit;
import com.youlai.boot.admin.service.ContentAuditService;
import com.youlai.boot.framework.security.util.SecurityUtils;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
public class ContentAuditServiceImpl extends ServiceImpl<MiniContentAuditMapper, MiniContentAudit> implements ContentAuditService {
@Override
public MiniContentAudit createAudit(String moduleCode, String bizId, String auditType) {
MiniContentAudit entity = new MiniContentAudit();
entity.setUuid(IdUtil.fastSimpleUUID());
entity.setModuleCode(moduleCode);
entity.setBizId(bizId);
entity.setAuditType(auditType);
entity.setStatus(AuditConstants.AUDIT_REVIEWING);
entity.setCreateBy(SecurityUtils.getUserId());
entity.setCreateTime(new Date());
entity.setCreateTimestamp(System.currentTimeMillis());
this.save(entity);
return entity;
}
@Override
public void updateAuditStatus(Long auditId, String status, String finalResult) {
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());
this.updateById(entity);
}
@Override
public MiniContentAudit getByModuleAndBizId(String moduleCode, String bizId) {
return this.getOne(new LambdaQueryWrapper<MiniContentAudit>()
.eq(MiniContentAudit::getModuleCode, moduleCode)
.eq(MiniContentAudit::getBizId, bizId));
}
}

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

@ -0,0 +1,100 @@
package com.youlai.boot.admin.service.impl;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.admin.constant.AuditConstants;
import com.youlai.boot.admin.mapper.MiniContentAuditTaskMapper;
import com.youlai.boot.admin.model.entity.MiniContentAuditTask;
import com.youlai.boot.admin.service.ContentAuditTaskService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Service
public class ContentAuditTaskServiceImpl extends ServiceImpl<MiniContentAuditTaskMapper, MiniContentAuditTask> implements ContentAuditTaskService {
@Override
@Transactional(rollbackFor = Exception.class)
public void batchCreateTasks(Long auditId, String auditType, List<String> texts, List<String> images, List<String> videos) {
List<MiniContentAuditTask> tasks = new ArrayList<>();
Date now = new Date();
Long nowTs = System.currentTimeMillis();
// 文本内容:每条文本单独一个任务
if (texts != null) {
for (String text : texts) {
if (text != null && !text.isBlank()) {
tasks.add(buildTask(auditId, auditType, "text", text, now, nowTs));
}
}
}
// 图片内容:每张图片URL单独一个任务
if (images != null) {
for (String imageUrl : images) {
if (imageUrl != null && !imageUrl.isBlank()) {
tasks.add(buildTask(auditId, auditType, "image", imageUrl, now, nowTs));
}
}
}
// 视频内容:每个视频URL单独一个任务
if (videos != null) {
for (String videoUrl : videos) {
if (videoUrl != null && !videoUrl.isBlank()) {
tasks.add(buildTask(auditId, auditType, "video", videoUrl, now, nowTs));
}
}
}
if (!tasks.isEmpty()) {
// MyBatis-Plus saveBatch:批量插入任务记录
this.saveBatch(tasks);
}
}
/**
* 构建单个审核任务实体
* MiniContentAuditTask字段uuid(前端交互) / contentAuditId(关联汇总) / contentType(内容类型) / contentValue(待审核值)
* / auditType(审核类型) / status(任务状态) / createTime / createTimestamp
*/
private MiniContentAuditTask buildTask(Long auditId, String auditType, String contentType, String contentValue, Date now, Long nowTs) {
MiniContentAuditTask task = new MiniContentAuditTask();
task.setUuid(IdUtil.fastSimpleUUID());
task.setContentAuditId(auditId);
task.setContentType(contentType);
task.setContentValue(contentValue);
task.setAuditType(auditType);
task.setStatus(AuditConstants.TASK_REVIEWING);
task.setCreateTime(now);
task.setCreateTimestamp(nowTs);
return task;
}
@Override
public void updateTaskMachineResult(Long taskId, String machineResult, String riskLevel, String result, String status) {
MiniContentAuditTask entity = new MiniContentAuditTask();
entity.setId(taskId);
// machineResult 字段存储阿里云机审返回的完整JSON,供后续复查
entity.setMachineResult(machineResult);
// riskLevel 字段:none(低风险) / medium(中风险) / high(高风险)
entity.setRiskLevel(riskLevel);
// result 字段:passed(通过) / failed(不通过),由策略映射得到
entity.setResult(result);
// status 字段:success(机审完成) / to_manual(转人工),最终状态
entity.setStatus(status);
entity.setUpdateTime(new Date());
entity.setUpdateTimestamp(System.currentTimeMillis());
// MyBatis-Plus updateById:按主键id更新指定字段
this.updateById(entity);
}
@Override
public List<MiniContentAuditTask> listTasksByAuditId(Long auditId) {
// LambdaQueryWrapper:构建查询条件 content_audit_id = auditId
return this.list(new LambdaQueryWrapper<MiniContentAuditTask>()
.eq(MiniContentAuditTask::getContentAuditId, auditId));
}
}

7
src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java

@ -38,7 +38,12 @@ public enum LogModuleEnum implements IBaseEnum<Integer> {
ADOPTION_DIARY_INFO(112, "领养日记信息"), ADOPTION_DIARY_INFO(112, "领养日记信息"),
USER_POST_INFO(113, "用户作品信息"), USER_POST_INFO(113, "用户作品信息"),
USER_POST_COMMENT(114, "用户作品评论"), USER_POST_COMMENT(114, "用户作品评论"),
ADOPTION_APPLICATION(115, "领养申请"); ADOPTION_APPLICATION(115, "领养申请"),
CONTENT_AUDIT_CONFIG(116, "内容审核配置"),
CONTENT_AUDIT(117, "内容审核"),
CONTENT_AUDIT_TASK(118, "审核任务"),
CONTENT_AUDIT_APPEAL(119, "内容申诉"),
REPORT(120, "举报管理");
@EnumValue @EnumValue
private final Integer value; private final Integer value;

2
src/main/java/com/youlai/boot/mini/model/entity/MiniContentAuditAppeal.java

@ -85,7 +85,7 @@ public class MiniContentAuditAppeal implements Serializable {
@TableField("is_deleted") @TableField("is_deleted")
@Schema(description = "逻辑删除标识(0-未删除 1-已删除)") @Schema(description = "逻辑删除标识(0-未删除 1-已删除)")
private Byte isDeleted; private Boolean deleted;
} }

2
src/main/java/com/youlai/boot/mini/model/entity/MiniReport.java

@ -97,7 +97,7 @@ public class MiniReport implements Serializable {
@TableField("is_deleted") @TableField("is_deleted")
@Schema(description = "逻辑删除标识(0-未删除 1-已删除)") @Schema(description = "逻辑删除标识(0-未删除 1-已删除)")
private Byte isDeleted; private Boolean deleted;
} }

Loading…
Cancel
Save