From 5970ad084724ff98acc27a5c3a60c1e092e33c27 Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Mon, 1 Jun 2026 17:35:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AE=A1=E6=A0=B8=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../boot/admin/constant/AuditConstants.java | 40 ++ .../ContentAuditConfigController.java | 59 +++ .../admin/converter/AuditConfigConverter.java | 18 + .../boot/admin/model/dto/AuditContentDTO.java | 21 + .../admin/model/entity/MiniContentAudit.java | 2 +- .../model/entity/MiniContentAuditConfig.java | 4 +- .../model/entity/MiniContentAuditTask.java | 2 +- .../admin/model/form/AuditConfigForm.java | 28 ++ .../admin/model/query/AuditConfigQuery.java | 16 + .../boot/admin/model/vo/AuditConfigVO.java | 47 +++ .../admin/service/AuditExecutorService.java | 19 + .../service/ContentAuditConfigService.java | 21 + .../admin/service/ContentAuditService.java | 14 + .../service/ContentAuditTaskService.java | 19 + .../impl/AuditExecutorServiceImpl.java | 398 ++++++++++++++++++ .../impl/ContentAuditConfigServiceImpl.java | 105 +++++ .../service/impl/ContentAuditServiceImpl.java | 51 +++ .../impl/ContentAuditTaskServiceImpl.java | 100 +++++ .../boot/common/enums/LogModuleEnum.java | 7 +- .../model/entity/MiniContentAuditAppeal.java | 2 +- .../boot/mini/model/entity/MiniReport.java | 2 +- 22 files changed, 969 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/youlai/boot/admin/constant/AuditConstants.java create mode 100644 src/main/java/com/youlai/boot/admin/controller/ContentAuditConfigController.java create mode 100644 src/main/java/com/youlai/boot/admin/converter/AuditConfigConverter.java create mode 100644 src/main/java/com/youlai/boot/admin/model/dto/AuditContentDTO.java create mode 100644 src/main/java/com/youlai/boot/admin/model/form/AuditConfigForm.java create mode 100644 src/main/java/com/youlai/boot/admin/model/query/AuditConfigQuery.java create mode 100644 src/main/java/com/youlai/boot/admin/model/vo/AuditConfigVO.java create mode 100644 src/main/java/com/youlai/boot/admin/service/AuditExecutorService.java create mode 100644 src/main/java/com/youlai/boot/admin/service/ContentAuditConfigService.java create mode 100644 src/main/java/com/youlai/boot/admin/service/ContentAuditService.java create mode 100644 src/main/java/com/youlai/boot/admin/service/ContentAuditTaskService.java create mode 100644 src/main/java/com/youlai/boot/admin/service/impl/AuditExecutorServiceImpl.java create mode 100644 src/main/java/com/youlai/boot/admin/service/impl/ContentAuditConfigServiceImpl.java create mode 100644 src/main/java/com/youlai/boot/admin/service/impl/ContentAuditServiceImpl.java create mode 100644 src/main/java/com/youlai/boot/admin/service/impl/ContentAuditTaskServiceImpl.java diff --git a/.gitignore b/.gitignore index 84b9e58..9a9f240 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ docker/xxljob/logs application-youlai.yml .claude CLAUDE.md +content-audit-design.md diff --git a/src/main/java/com/youlai/boot/admin/constant/AuditConstants.java b/src/main/java/com/youlai/boot/admin/constant/AuditConstants.java new file mode 100644 index 0000000..a57f5af --- /dev/null +++ b/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"; + +} diff --git a/src/main/java/com/youlai/boot/admin/controller/ContentAuditConfigController.java b/src/main/java/com/youlai/boot/admin/controller/ContentAuditConfigController.java new file mode 100644 index 0000000..91789a1 --- /dev/null +++ b/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 listConfig(@Valid AuditConfigQuery query) { + IPage result = contentAuditConfigService.pageConfig(query); + return PageResult.success(result); + } + + @Operation(summary = "新增审核配置") + @PostMapping("/add") + @Log(module = LogModuleEnum.CONTENT_AUDIT_CONFIG, value = ActionTypeEnum.INSERT) + public Result 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 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 deleteConfig(@PathVariable String uuid) { + contentAuditConfigService.deleteConfig(uuid); + return Result.success(); + } + +} diff --git a/src/main/java/com/youlai/boot/admin/converter/AuditConfigConverter.java b/src/main/java/com/youlai/boot/admin/converter/AuditConfigConverter.java new file mode 100644 index 0000000..4b32d10 --- /dev/null +++ b/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 toPageVo(Page page); + + AuditConfigVO toVo(MiniContentAuditConfig entity); + + MiniContentAuditConfig toEntity(AuditConfigForm form); + +} diff --git a/src/main/java/com/youlai/boot/admin/model/dto/AuditContentDTO.java b/src/main/java/com/youlai/boot/admin/model/dto/AuditContentDTO.java new file mode 100644 index 0000000..68d1507 --- /dev/null +++ b/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 texts; + + @Schema(description = "待审核图片URL列表") + private List images; + + @Schema(description = "待审核视频URL列表") + private List videos; + +} diff --git a/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAudit.java b/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAudit.java index 5149f49..b9fca9b 100644 --- a/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAudit.java +++ b/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAudit.java @@ -85,7 +85,7 @@ public class MiniContentAudit implements Serializable { @TableField("is_deleted") @Schema(description = "逻辑删除标识(0-未删除 1-已删除)") - private Byte isDeleted; + private Boolean deleted; } diff --git a/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditConfig.java b/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditConfig.java index 626a796..b9fca45 100644 --- a/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditConfig.java +++ b/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditConfig.java @@ -34,7 +34,7 @@ public class MiniContentAuditConfig implements Serializable { @TableField("audit_enable") @Schema(description = "是否开启审核, 0-开启, 1-关闭") - private Byte auditEnable; + private Boolean auditEnable; @TableField("audit_type") @Schema(description = "审核类型: machine机器 / manual手动 / mixed混合") @@ -64,7 +64,7 @@ public class MiniContentAuditConfig implements Serializable { @TableField("is_deleted") @Schema(description = "逻辑删除标识(0-未删除 1-已删除)") - private Byte isDeleted; + private Boolean deleted; } diff --git a/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditTask.java b/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditTask.java index d3b4d11..93b6edb 100644 --- a/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditTask.java +++ b/src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditTask.java @@ -96,7 +96,7 @@ public class MiniContentAuditTask implements Serializable { @TableField("is_deleted") @Schema(description = "逻辑删除标识(0-未删除 1-已删除)") - private Byte isDeleted; + private Boolean deleted; } diff --git a/src/main/java/com/youlai/boot/admin/model/form/AuditConfigForm.java b/src/main/java/com/youlai/boot/admin/model/form/AuditConfigForm.java new file mode 100644 index 0000000..b3e9758 --- /dev/null +++ b/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; + +} diff --git a/src/main/java/com/youlai/boot/admin/model/query/AuditConfigQuery.java b/src/main/java/com/youlai/boot/admin/model/query/AuditConfigQuery.java new file mode 100644 index 0000000..e1511de --- /dev/null +++ b/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; + +} diff --git a/src/main/java/com/youlai/boot/admin/model/vo/AuditConfigVO.java b/src/main/java/com/youlai/boot/admin/model/vo/AuditConfigVO.java new file mode 100644 index 0000000..95ed389 --- /dev/null +++ b/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; + +} diff --git a/src/main/java/com/youlai/boot/admin/service/AuditExecutorService.java b/src/main/java/com/youlai/boot/admin/service/AuditExecutorService.java new file mode 100644 index 0000000..cf97f3a --- /dev/null +++ b/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 executeAudit(String moduleCode, String bizId, AuditContentDTO content); + +} diff --git a/src/main/java/com/youlai/boot/admin/service/ContentAuditConfigService.java b/src/main/java/com/youlai/boot/admin/service/ContentAuditConfigService.java new file mode 100644 index 0000000..0351c7a --- /dev/null +++ b/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 { + + IPage pageConfig(AuditConfigQuery query); + + AuditConfigVO getConfigByUuid(String uuid); + + AuditConfigVO addConfig(AuditConfigForm form); + + AuditConfigVO updateConfig(String uuid, AuditConfigForm form); + + void deleteConfig(String uuid); +} diff --git a/src/main/java/com/youlai/boot/admin/service/ContentAuditService.java b/src/main/java/com/youlai/boot/admin/service/ContentAuditService.java new file mode 100644 index 0000000..6fc78d9 --- /dev/null +++ b/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 createAudit(String moduleCode, String bizId, String auditType); + + void updateAuditStatus(Long auditId, String status, String finalResult); + + MiniContentAudit getByModuleAndBizId(String moduleCode, String bizId); + +} diff --git a/src/main/java/com/youlai/boot/admin/service/ContentAuditTaskService.java b/src/main/java/com/youlai/boot/admin/service/ContentAuditTaskService.java new file mode 100644 index 0000000..a5139c8 --- /dev/null +++ b/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 { + + /** 批量创建审核任务(按内容类型拆分:text/image/video) */ + void batchCreateTasks(Long auditId, String auditType, List texts, List images, List videos); + + /** 更新单个任务的机审结果 */ + void updateTaskMachineResult(Long taskId, String machineResult, String riskLevel, String result, String status); + + /** 查询某个审核汇总下的所有任务明细 */ + List listTasksByAuditId(Long auditId); + +} diff --git a/src/main/java/com/youlai/boot/admin/service/impl/AuditExecutorServiceImpl.java b/src/main/java/com/youlai/boot/admin/service/impl/AuditExecutorServiceImpl.java new file mode 100644 index 0000000..055bd3e --- /dev/null +++ b/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.*; + +/** + * 内容审核核心执行器 + *

+ * 职责:编排审核全流程——查配置 → 创建汇总 → 拆解内容创建任务 → 调用阿里云机审 → 按策略判定结果 → 汇总最终状态 + * 供业务层(创建/修改内容时)注入调用。 + */ +@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 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 tasks = contentAuditTaskService.listTasksByAuditId(auditId); + for (MiniContentAuditTask task : tasks) { + executeSingleTaskAudit(task, riskStrategy); + } + + // 5) 重新加载任务(机审结果已回填),汇总判定 audit 的最终状态 + List updatedTasks = contentAuditTaskService.listTasksByAuditId(auditId); + return aggregateTaskResultsAndUpdateAudit(auditId, updatedTasks); + } + + // ================================================================ + // 配置查询 + // ================================================================ + + /** + * 查找审核配置。 + * LambdaQueryWrapper 构建条件:module_code = ? AND deleted = false(未逻辑删除) + */ + private MiniContentAuditConfig findAuditConfig(String moduleCode) { + LambdaQueryWrapper 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 同步返回 suggestion,video 异步返回 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。 + *

+ * 如果 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。 + *

+     *   none 策略:   pass→passed, 其余→failed       (机审直接决定,无人工环节)
+     *   medium策略:  pass→passed, review→to_manual, block→failed
+     *   high策略:    pass→passed, 其余→to_manual     (中高风险一律转人工)
+     * 
+ * + * @param suggestion 阿里云机审建议:pass / review / block + * @param strategy 审核配置中的 risk_strategy:none / medium / high + * @return passed / failed / to_manual + */ + private String applyRiskStrategy(String suggestion, String strategy) { + boolean isPassSuggestion = AuditConstants.SUGGESTION_PASS.equals(suggestion); + + if ("none".equals(strategy)) { + // none策略:pass→通过,其余→不通过 + if (isPassSuggestion) { + return AuditConstants.RESULT_PASSED; + } + return AuditConstants.RESULT_FAILED; + } + + if ("high".equals(strategy)) { + // high策略:pass→通过,其余→转人工 + if (isPassSuggestion) { + return AuditConstants.RESULT_PASSED; + } + return AuditConstants.TASK_TO_MANUAL; + } + + // 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 最终状态。 + *

+ * 汇总规则: + * - 任一 task.result = failed → audit.status = failed + * - 有 task.result = to_manual → audit.status = manual_review + * - 全部 task.result = passed → audit.status = passed + * - 视频任务未完成(result 为 null)→ 保持 reviewing + * + *

+ * MiniContentAudit 字段含义: + * - status: reviewing(机审中) / passed(通过) / failed(不通过) / manual_review(待人工) / appealing(申诉中) + * - final_result: passed / failed / manual_review + */ + private Map aggregateTaskResultsAndUpdateAudit(Long auditId, List 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 resultMap = new HashMap<>(); + resultMap.put("status", auditStatus); + resultMap.put("auditId", auditId); + return resultMap; + } +} diff --git a/src/main/java/com/youlai/boot/admin/service/impl/ContentAuditConfigServiceImpl.java b/src/main/java/com/youlai/boot/admin/service/impl/ContentAuditConfigServiceImpl.java new file mode 100644 index 0000000..c1ce923 --- /dev/null +++ b/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 implements ContentAuditConfigService { + + private final AuditConfigConverter auditConfigConverter; + + @Override + public IPage pageConfig(AuditConfigQuery query) { + Page page = new Page<>(query.getPageNum(), query.getPageSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(query.getModuleCode() != null && !query.getModuleCode().isBlank(), + MiniContentAuditConfig::getModuleCode, query.getModuleCode()); + wrapper.orderByDesc(MiniContentAuditConfig::getCreateTime); + Page 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() + .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() + .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() + .eq(MiniContentAuditConfig::getUuid, uuid)); + if (entity == null) { + throw new MsgException("审核配置不存在"); + } + return entity; + } +} diff --git a/src/main/java/com/youlai/boot/admin/service/impl/ContentAuditServiceImpl.java b/src/main/java/com/youlai/boot/admin/service/impl/ContentAuditServiceImpl.java new file mode 100644 index 0000000..25d7de4 --- /dev/null +++ b/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 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() + .eq(MiniContentAudit::getModuleCode, moduleCode) + .eq(MiniContentAudit::getBizId, bizId)); + } +} diff --git a/src/main/java/com/youlai/boot/admin/service/impl/ContentAuditTaskServiceImpl.java b/src/main/java/com/youlai/boot/admin/service/impl/ContentAuditTaskServiceImpl.java new file mode 100644 index 0000000..1b6c3c8 --- /dev/null +++ b/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 implements ContentAuditTaskService { + + @Override + @Transactional(rollbackFor = Exception.class) + public void batchCreateTasks(Long auditId, String auditType, List texts, List images, List videos) { + List 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 listTasksByAuditId(Long auditId) { + // LambdaQueryWrapper:构建查询条件 content_audit_id = auditId + return this.list(new LambdaQueryWrapper() + .eq(MiniContentAuditTask::getContentAuditId, auditId)); + } +} diff --git a/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java b/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java index 1ed2f3d..35fa752 100644 --- a/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java +++ b/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java @@ -38,7 +38,12 @@ public enum LogModuleEnum implements IBaseEnum { ADOPTION_DIARY_INFO(112, "领养日记信息"), USER_POST_INFO(113, "用户作品信息"), 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 private final Integer value; diff --git a/src/main/java/com/youlai/boot/mini/model/entity/MiniContentAuditAppeal.java b/src/main/java/com/youlai/boot/mini/model/entity/MiniContentAuditAppeal.java index e1447d3..56af536 100644 --- a/src/main/java/com/youlai/boot/mini/model/entity/MiniContentAuditAppeal.java +++ b/src/main/java/com/youlai/boot/mini/model/entity/MiniContentAuditAppeal.java @@ -85,7 +85,7 @@ public class MiniContentAuditAppeal implements Serializable { @TableField("is_deleted") @Schema(description = "逻辑删除标识(0-未删除 1-已删除)") - private Byte isDeleted; + private Boolean deleted; } diff --git a/src/main/java/com/youlai/boot/mini/model/entity/MiniReport.java b/src/main/java/com/youlai/boot/mini/model/entity/MiniReport.java index 95c3657..f88bfe1 100644 --- a/src/main/java/com/youlai/boot/mini/model/entity/MiniReport.java +++ b/src/main/java/com/youlai/boot/mini/model/entity/MiniReport.java @@ -97,7 +97,7 @@ public class MiniReport implements Serializable { @TableField("is_deleted") @Schema(description = "逻辑删除标识(0-未删除 1-已删除)") - private Byte isDeleted; + private Boolean deleted; }