From b756b1a86d0d39db69fea8dc961af1c205ec4d18 Mon Sep 17 00:00:00 2001 From: glx <783262171@qq.com> Date: Thu, 4 Jun 2026 18:00:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0Oss=E5=AE=A1=E6=A0=B8?= =?UTF-8?q?=E5=9B=9E=E8=B0=83=20=E7=94=A8=E6=88=B7=E4=B8=BE=E6=8A=A5?= =?UTF-8?q?=E7=94=B3=E8=AF=89=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boot/admin/constant/AuditConstants.java | 5 + .../ContentAuditAppealController.java | 43 +++ .../ContentAuditConfigController.java | 36 ++ .../controller/ReportManageController.java | 43 +++ .../boot/admin/job/VideoAuditPollJob.java | 4 +- .../admin/model/entity/MiniContentAudit.java | 4 + .../model/entity/MiniContentAuditTask.java | 2 +- .../admin/model/form/AppealHandleForm.java | 20 + .../admin/model/form/ReportHandleForm.java | 20 + .../boot/admin/model/query/AppealQuery.java | 16 + .../boot/admin/model/query/ReportQuery.java | 19 + .../youlai/boot/admin/model/vo/AppealVO.java | 50 +++ .../youlai/boot/admin/model/vo/ReportVO.java | 59 +++ .../admin/service/AuditExecutorService.java | 3 +- .../service/ContentAuditAppealService.java | 16 + .../admin/service/ContentAuditService.java | 2 +- .../service/ContentAuditTaskService.java | 3 +- .../admin/service/ContentLookupService.java | 10 + .../admin/service/OssCallbackService.java | 8 + .../admin/service/ReportManageService.java | 16 + .../impl/AuditExecutorServiceImpl.java | 346 ++++++++++++------ .../impl/ContentAuditAppealServiceImpl.java | 96 +++++ .../service/impl/ContentAuditServiceImpl.java | 3 +- .../impl/ContentAuditTaskServiceImpl.java | 12 +- .../impl/ContentLookupServiceImpl.java | 154 ++++++++ .../service/impl/OssCallbackServiceImpl.java | 192 ++++++++++ .../service/impl/ReportManageServiceImpl.java | 118 ++++++ .../common/util/AliyunContentAuditUtil.java | 140 ++++++- .../MiniContentAuditAppealController.java | 34 ++ .../mini/controller/MiniReportController.java | 34 ++ .../mini/model/form/AppealSubmitForm.java | 25 ++ .../mini/model/form/ReportSubmitForm.java | 32 ++ .../MiniContentAuditAppealService.java | 10 + .../boot/mini/service/MiniReportService.java | 10 + .../MiniContentAuditAppealServiceImpl.java | 69 ++++ .../service/impl/MiniReportServiceImpl.java | 58 +++ .../service/impl/UserPostServiceImpl.java | 2 +- src/main/resources/application-dev.yml | 4 + 38 files changed, 1568 insertions(+), 150 deletions(-) create mode 100644 src/main/java/com/youlai/boot/admin/controller/ContentAuditAppealController.java create mode 100644 src/main/java/com/youlai/boot/admin/controller/ReportManageController.java create mode 100644 src/main/java/com/youlai/boot/admin/model/form/AppealHandleForm.java create mode 100644 src/main/java/com/youlai/boot/admin/model/form/ReportHandleForm.java create mode 100644 src/main/java/com/youlai/boot/admin/model/query/AppealQuery.java create mode 100644 src/main/java/com/youlai/boot/admin/model/query/ReportQuery.java create mode 100644 src/main/java/com/youlai/boot/admin/model/vo/AppealVO.java create mode 100644 src/main/java/com/youlai/boot/admin/model/vo/ReportVO.java create mode 100644 src/main/java/com/youlai/boot/admin/service/ContentAuditAppealService.java create mode 100644 src/main/java/com/youlai/boot/admin/service/ContentLookupService.java create mode 100644 src/main/java/com/youlai/boot/admin/service/OssCallbackService.java create mode 100644 src/main/java/com/youlai/boot/admin/service/ReportManageService.java create mode 100644 src/main/java/com/youlai/boot/admin/service/impl/ContentAuditAppealServiceImpl.java create mode 100644 src/main/java/com/youlai/boot/admin/service/impl/ContentLookupServiceImpl.java create mode 100644 src/main/java/com/youlai/boot/admin/service/impl/OssCallbackServiceImpl.java create mode 100644 src/main/java/com/youlai/boot/admin/service/impl/ReportManageServiceImpl.java create mode 100644 src/main/java/com/youlai/boot/mini/controller/MiniContentAuditAppealController.java create mode 100644 src/main/java/com/youlai/boot/mini/controller/MiniReportController.java create mode 100644 src/main/java/com/youlai/boot/mini/model/form/AppealSubmitForm.java create mode 100644 src/main/java/com/youlai/boot/mini/model/form/ReportSubmitForm.java create mode 100644 src/main/java/com/youlai/boot/mini/service/MiniContentAuditAppealService.java create mode 100644 src/main/java/com/youlai/boot/mini/service/MiniReportService.java create mode 100644 src/main/java/com/youlai/boot/mini/service/impl/MiniContentAuditAppealServiceImpl.java create mode 100644 src/main/java/com/youlai/boot/mini/service/impl/MiniReportServiceImpl.java diff --git a/src/main/java/com/youlai/boot/admin/constant/AuditConstants.java b/src/main/java/com/youlai/boot/admin/constant/AuditConstants.java index df22024..6af23d2 100644 --- a/src/main/java/com/youlai/boot/admin/constant/AuditConstants.java +++ b/src/main/java/com/youlai/boot/admin/constant/AuditConstants.java @@ -51,4 +51,9 @@ public final class AuditConstants { /** 审核类型: 混合审核(AI 给出风险提示,交由人工判定) */ public static final String AUDIT_TYPE_MIXED = "mixed"; + /** 触发类型: 自动触发(内容创建/修改时) */ + public static final String TRIGGER_AUTO = "auto"; + /** 触发类型: 举报触发 */ + public static final String TRIGGER_REPORT = "report"; + } diff --git a/src/main/java/com/youlai/boot/admin/controller/ContentAuditAppealController.java b/src/main/java/com/youlai/boot/admin/controller/ContentAuditAppealController.java new file mode 100644 index 0000000..55ae18f --- /dev/null +++ b/src/main/java/com/youlai/boot/admin/controller/ContentAuditAppealController.java @@ -0,0 +1,43 @@ +package com.youlai.boot.admin.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.youlai.boot.admin.model.form.AppealHandleForm; +import com.youlai.boot.admin.model.query.AppealQuery; +import com.youlai.boot.admin.model.vo.AppealVO; +import com.youlai.boot.admin.service.ContentAuditAppealService; +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/appeal") +@RequiredArgsConstructor +public class ContentAuditAppealController { + + private final ContentAuditAppealService appealService; + + @Operation(summary = "分页查询申诉列表") + @GetMapping("/list") + @Log(module = LogModuleEnum.CONTENT_AUDIT_APPEAL, value = ActionTypeEnum.LIST) + public PageResult listAppeal(@Valid AppealQuery query) { + IPage result = appealService.pageAppeal(query); + return PageResult.success(result); + } + + @Operation(summary = "处理申诉") + @PostMapping("/handle/{id}") + @Log(module = LogModuleEnum.CONTENT_AUDIT_APPEAL, value = ActionTypeEnum.UPDATE) + public Result handleAppeal(@PathVariable Long id, @Valid @RequestBody AppealHandleForm form) { + appealService.handleAppeal(id, form); + return Result.success(); + } + +} diff --git a/src/main/java/com/youlai/boot/admin/controller/ContentAuditConfigController.java b/src/main/java/com/youlai/boot/admin/controller/ContentAuditConfigController.java index 91789a1..d0c03b6 100644 --- a/src/main/java/com/youlai/boot/admin/controller/ContentAuditConfigController.java +++ b/src/main/java/com/youlai/boot/admin/controller/ContentAuditConfigController.java @@ -5,6 +5,7 @@ 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.admin.service.OssCallbackService; import com.youlai.boot.common.annotation.Log; import com.youlai.boot.common.enums.ActionTypeEnum; import com.youlai.boot.common.enums.LogModuleEnum; @@ -14,15 +15,21 @@ 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; + private final OssCallbackService ossCallbackService; @Operation(summary = "分页查询审核配置列表") @GetMapping("/list") @@ -56,4 +63,33 @@ public class ContentAuditConfigController { return Result.success(); } + @Operation(summary = "阿里云OSS增量图片审核回调") + @PostMapping("/images/callback") + public Result 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); + } + return Result.success(); + } + } diff --git a/src/main/java/com/youlai/boot/admin/controller/ReportManageController.java b/src/main/java/com/youlai/boot/admin/controller/ReportManageController.java new file mode 100644 index 0000000..da9ee8e --- /dev/null +++ b/src/main/java/com/youlai/boot/admin/controller/ReportManageController.java @@ -0,0 +1,43 @@ +package com.youlai.boot.admin.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.youlai.boot.admin.model.form.ReportHandleForm; +import com.youlai.boot.admin.model.query.ReportQuery; +import com.youlai.boot.admin.model.vo.ReportVO; +import com.youlai.boot.admin.service.ReportManageService; +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/report") +@RequiredArgsConstructor +public class ReportManageController { + + private final ReportManageService reportService; + + @Operation(summary = "分页查询举报列表") + @GetMapping("/list") + @Log(module = LogModuleEnum.REPORT, value = ActionTypeEnum.LIST) + public PageResult listReport(@Valid ReportQuery query) { + IPage result = reportService.pageReport(query); + return PageResult.success(result); + } + + @Operation(summary = "处理举报") + @PostMapping("/handle/{id}") + @Log(module = LogModuleEnum.REPORT, value = ActionTypeEnum.UPDATE) + public Result handleReport(@PathVariable Long id, @Valid @RequestBody ReportHandleForm form) { + reportService.handleReport(id, form); + return Result.success(); + } + +} diff --git a/src/main/java/com/youlai/boot/admin/job/VideoAuditPollJob.java b/src/main/java/com/youlai/boot/admin/job/VideoAuditPollJob.java index 67f1961..147bb84 100644 --- a/src/main/java/com/youlai/boot/admin/job/VideoAuditPollJob.java +++ b/src/main/java/com/youlai/boot/admin/job/VideoAuditPollJob.java @@ -20,9 +20,9 @@ public class VideoAuditPollJob { private final AuditExecutorService auditExecutorService; /** - * 每 30 秒轮询一次待处理的视频审核异步结果 + * 每 300 秒轮询一次待处理的视频审核异步结果 */ - @Scheduled(fixedDelay = 30000) + @Scheduled(fixedDelay = 300000) public void pollVideoAuditResult() { try { int count = auditExecutorService.pollVideoAuditResults(); 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 468b2ed..8fd5788 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 @@ -40,6 +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申诉中") private String status; 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 d3e98c9..1534092 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 @@ -58,7 +58,7 @@ public class MiniContentAuditTask implements Serializable { @TableField("confidence") @Schema(description = "机审信任度") - private Integer confidence; + private Float confidence; @TableField("description") @Schema(description = "机审描述") diff --git a/src/main/java/com/youlai/boot/admin/model/form/AppealHandleForm.java b/src/main/java/com/youlai/boot/admin/model/form/AppealHandleForm.java new file mode 100644 index 0000000..8dbf9fb --- /dev/null +++ b/src/main/java/com/youlai/boot/admin/model/form/AppealHandleForm.java @@ -0,0 +1,20 @@ +package com.youlai.boot.admin.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Schema(description = "处理申诉表单") +@Getter +@Setter +public class AppealHandleForm { + + @NotBlank(message = "处理结果不能为空") + @Schema(description = "处理结果: approved通过 / rejected驳回") + private String result; + + @Schema(description = "处理备注") + private String handleRemark; + +} diff --git a/src/main/java/com/youlai/boot/admin/model/form/ReportHandleForm.java b/src/main/java/com/youlai/boot/admin/model/form/ReportHandleForm.java new file mode 100644 index 0000000..03febfd --- /dev/null +++ b/src/main/java/com/youlai/boot/admin/model/form/ReportHandleForm.java @@ -0,0 +1,20 @@ +package com.youlai.boot.admin.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Schema(description = "处理举报表单") +@Getter +@Setter +public class ReportHandleForm { + + @NotBlank(message = "处理结果不能为空") + @Schema(description = "处理结果: processed已处理 / dismissed已驳回") + private String result; + + @Schema(description = "处理备注") + private String handleRemark; + +} diff --git a/src/main/java/com/youlai/boot/admin/model/query/AppealQuery.java b/src/main/java/com/youlai/boot/admin/model/query/AppealQuery.java new file mode 100644 index 0000000..d617276 --- /dev/null +++ b/src/main/java/com/youlai/boot/admin/model/query/AppealQuery.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 AppealQuery extends BaseQuery { + + @Schema(description = "处理状态: pending待处理 / approved通过 / rejected驳回") + private String status; + +} diff --git a/src/main/java/com/youlai/boot/admin/model/query/ReportQuery.java b/src/main/java/com/youlai/boot/admin/model/query/ReportQuery.java new file mode 100644 index 0000000..be7ab92 --- /dev/null +++ b/src/main/java/com/youlai/boot/admin/model/query/ReportQuery.java @@ -0,0 +1,19 @@ +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 ReportQuery extends BaseQuery { + + @Schema(description = "举报目标类型") + private String targetType; + + @Schema(description = "处理状态: pending待处理 / processed已处理 / dismissed已驳回") + private String status; + +} diff --git a/src/main/java/com/youlai/boot/admin/model/vo/AppealVO.java b/src/main/java/com/youlai/boot/admin/model/vo/AppealVO.java new file mode 100644 index 0000000..e16db16 --- /dev/null +++ b/src/main/java/com/youlai/boot/admin/model/vo/AppealVO.java @@ -0,0 +1,50 @@ +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 AppealVO { + + @Schema(description = "申诉ID") + private Long id; + + @Schema(description = "UUID") + private String uuid; + + @Schema(description = "关联审核ID") + private Long auditId; + + @Schema(description = "申诉人用户ID") + private Long userId; + + @Schema(description = "申诉原因") + private String reason; + + @Schema(description = "证据图片URL") + private String evidence; + + @Schema(description = "处理状态: pending / approved / rejected") + private String status; + + @Schema(description = "处理人ID") + private Long handlerUserId; + + @Schema(description = "处理备注") + private String handleRemark; + + @Schema(description = "处理时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date handleTime; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; + +} diff --git a/src/main/java/com/youlai/boot/admin/model/vo/ReportVO.java b/src/main/java/com/youlai/boot/admin/model/vo/ReportVO.java new file mode 100644 index 0000000..3aa2437 --- /dev/null +++ b/src/main/java/com/youlai/boot/admin/model/vo/ReportVO.java @@ -0,0 +1,59 @@ +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 ReportVO { + + @Schema(description = "举报ID") + private Long id; + + @Schema(description = "UUID") + private String uuid; + + @Schema(description = "举报目标类型") + private String targetType; + + @Schema(description = "被举报内容ID") + private String targetId; + + @Schema(description = "举报人用户ID") + private Long reporterUserId; + + @Schema(description = "举报原因类别") + private String reasonCategory; + + @Schema(description = "证据图片URL") + private String evidence; + + @Schema(description = "举报补充描述") + private String description; + + @Schema(description = "处理状态: pending / processed / dismissed") + private String status; + + @Schema(description = "关联审核ID") + private Long auditId; + + @Schema(description = "处理人ID") + private Long handlerUserId; + + @Schema(description = "处理备注") + private String handleRemark; + + @Schema(description = "处理时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date handleTime; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; + +} diff --git a/src/main/java/com/youlai/boot/admin/service/AuditExecutorService.java b/src/main/java/com/youlai/boot/admin/service/AuditExecutorService.java index 36cef67..52fc599 100644 --- a/src/main/java/com/youlai/boot/admin/service/AuditExecutorService.java +++ b/src/main/java/com/youlai/boot/admin/service/AuditExecutorService.java @@ -12,9 +12,10 @@ 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举报 * @return {status: "passed"|"failed"|"manual_review", auditId: Long},配置关闭或无配置时返回null */ - Map executeAudit(String moduleCode, Long bizId, AuditContentDTO content); + Map executeAudit(String moduleCode, Long bizId, AuditContentDTO content, String triggerType); /** * 轮询所有待处理的视频审核异步结果,更新任务和汇总状态 diff --git a/src/main/java/com/youlai/boot/admin/service/ContentAuditAppealService.java b/src/main/java/com/youlai/boot/admin/service/ContentAuditAppealService.java new file mode 100644 index 0000000..46f21fd --- /dev/null +++ b/src/main/java/com/youlai/boot/admin/service/ContentAuditAppealService.java @@ -0,0 +1,16 @@ +package com.youlai.boot.admin.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.youlai.boot.admin.model.form.AppealHandleForm; +import com.youlai.boot.admin.model.query.AppealQuery; +import com.youlai.boot.admin.model.vo.AppealVO; + +public interface ContentAuditAppealService { + + /** 分页查询申诉列表 */ + IPage pageAppeal(AppealQuery query); + + /** 处理申诉(通过/驳回) */ + void handleAppeal(Long id, AppealHandleForm form); + +} diff --git a/src/main/java/com/youlai/boot/admin/service/ContentAuditService.java b/src/main/java/com/youlai/boot/admin/service/ContentAuditService.java index 93fc67a..a988cb6 100644 --- a/src/main/java/com/youlai/boot/admin/service/ContentAuditService.java +++ b/src/main/java/com/youlai/boot/admin/service/ContentAuditService.java @@ -5,7 +5,7 @@ import com.youlai.boot.admin.model.entity.MiniContentAudit; public interface ContentAuditService extends IService { - MiniContentAudit createAudit(String moduleCode, Long bizId, String auditType); + MiniContentAudit createAudit(String moduleCode, Long bizId, String auditType, String triggerType); void updateAuditStatus(Long auditId, String status, String finalResult); diff --git a/src/main/java/com/youlai/boot/admin/service/ContentAuditTaskService.java b/src/main/java/com/youlai/boot/admin/service/ContentAuditTaskService.java index 576ea28..3dd3ae9 100644 --- a/src/main/java/com/youlai/boot/admin/service/ContentAuditTaskService.java +++ b/src/main/java/com/youlai/boot/admin/service/ContentAuditTaskService.java @@ -11,7 +11,8 @@ public interface ContentAuditTaskService extends IService void batchCreateTasks(Long auditId, String auditType, List texts, List images, List videos); /** 更新单个任务的机审结果 */ - void updateTaskMachineResult(Long taskId, String machineResult, String riskLevel, String result, String status); + void updateTaskMachineResult(Long taskId, String machineResult, String riskLevel, String result, String status, + String label, Float confidence, String description, String requestId); /** 查询某个审核汇总下的所有任务明细 */ List listTasksByAuditId(Long auditId); diff --git a/src/main/java/com/youlai/boot/admin/service/ContentLookupService.java b/src/main/java/com/youlai/boot/admin/service/ContentLookupService.java new file mode 100644 index 0000000..a939fc8 --- /dev/null +++ b/src/main/java/com/youlai/boot/admin/service/ContentLookupService.java @@ -0,0 +1,10 @@ +package com.youlai.boot.admin.service; + +import com.youlai.boot.admin.model.dto.AuditContentDTO; + +public interface ContentLookupService { + + /** 根据模块编码和业务ID查询待审核的原始内容 */ + AuditContentDTO lookupContent(String moduleCode, Long bizId); + +} diff --git a/src/main/java/com/youlai/boot/admin/service/OssCallbackService.java b/src/main/java/com/youlai/boot/admin/service/OssCallbackService.java new file mode 100644 index 0000000..189fee5 --- /dev/null +++ b/src/main/java/com/youlai/boot/admin/service/OssCallbackService.java @@ -0,0 +1,8 @@ +package com.youlai.boot.admin.service; + +public interface OssCallbackService { + + /** 处理OSS图片审核增量回调 */ + void handleImageCallback(String checksum, String content); + +} diff --git a/src/main/java/com/youlai/boot/admin/service/ReportManageService.java b/src/main/java/com/youlai/boot/admin/service/ReportManageService.java new file mode 100644 index 0000000..ba200b7 --- /dev/null +++ b/src/main/java/com/youlai/boot/admin/service/ReportManageService.java @@ -0,0 +1,16 @@ +package com.youlai.boot.admin.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.youlai.boot.admin.model.form.ReportHandleForm; +import com.youlai.boot.admin.model.query.ReportQuery; +import com.youlai.boot.admin.model.vo.ReportVO; + +public interface ReportManageService { + + /** 分页查询举报列表 */ + IPage pageReport(ReportQuery query); + + /** 处理举报(受理/驳回) */ + void handleReport(Long id, ReportHandleForm form); + +} 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 index 074c005..ce10705 100644 --- a/src/main/java/com/youlai/boot/admin/service/impl/AuditExecutorServiceImpl.java +++ b/src/main/java/com/youlai/boot/admin/service/impl/AuditExecutorServiceImpl.java @@ -17,6 +17,7 @@ import org.springframework.stereotype.Service; import java.lang.reflect.Method; import java.util.*; +import java.util.stream.Collectors; /** * 内容审核核心执行器 @@ -43,7 +44,7 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { * @return {status, auditId};配置关闭或无配置时返回 null,调用方以此判断是否跳过审核 */ @Override - public Map executeAudit(String moduleCode, Long bizId, AuditContentDTO content) { + public Map executeAudit(String moduleCode, Long bizId, AuditContentDTO content, String triggerType) { // 1) 查询对应模块的审核配置 MiniContentAuditConfig config = findAuditConfig(moduleCode); if (config == null || isAuditDisabled(config)) { @@ -53,7 +54,7 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { String auditType = config.getAuditType(); // 2) 创建审核汇总记录 - MiniContentAudit audit = contentAuditService.createAudit(moduleCode, bizId, auditType); + MiniContentAudit audit = contentAuditService.createAudit(moduleCode, bizId, auditType, triggerType); Long auditId = audit.getId(); // 3) 拆解待审核内容,批量创建审核任务明细 @@ -71,9 +72,7 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { // machine(默认):调 AI 并按策略裁决 String strictness = config.getRiskStrategy() != null ? config.getRiskStrategy() : AuditConstants.STRATEGY_NORMAL; List tasks = contentAuditTaskService.listTasksByAuditId(auditId); - for (MiniContentAuditTask task : tasks) { - executeSingleTaskAudit(task, strictness); - } + executeBatchAuditByType(tasks, strictness); List updatedTasks = contentAuditTaskService.listTasksByAuditId(auditId); return aggregateTaskResultsAndUpdateAudit(auditId, updatedTasks); } @@ -98,13 +97,7 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { */ private Map executeMixedAudit(Long auditId) { List tasks = contentAuditTaskService.listTasksByAuditId(auditId); - for (MiniContentAuditTask task : tasks) { - try { - recordMachineResultOnly(task); - } catch (Exception e) { - log.error("mixed审核AI分析失败, taskId={}", task.getId(), e); - } - } + executeBatchMixedAuditByType(tasks); contentAuditService.updateAuditStatus(auditId, AuditConstants.AUDIT_MANUAL_REVIEW, null); Map result = new HashMap<>(); result.put("status", AuditConstants.AUDIT_MANUAL_REVIEW); @@ -113,27 +106,85 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { } /** - * 仅记录机审结果,不判定(mixed 模式用) + * 按类型分组批量执行 mixed 机审(仅记录风险信息,不裁决) */ - private void recordMachineResultOnly(MiniContentAuditTask task) { - String contentType = task.getContentType(); - String contentValue = task.getContentValue(); - - switch (contentType) { - case "text" -> { - TextModerationPlusResponse r = aliyunContentAuditUtil.textModerationPlus(contentValue); - String riskLevel = extractRiskLevelFromResponse(r); + private void executeBatchMixedAuditByType(List tasks) { + Map> grouped = tasks.stream() + .collect(Collectors.groupingBy(MiniContentAuditTask::getContentType)); + + List textTasks = grouped.getOrDefault("text", List.of()); + if (!textTasks.isEmpty()) { + try { + List contents = textTasks.stream().map(MiniContentAuditTask::getContentValue).toList(); + TextModerationPlusResponse r = aliyunContentAuditUtil.batchTextModerationPlus(contents); + processBatchTextResponseForMixed(textTasks, r); + } catch (Exception e) { + log.error("mixed批量文本审核失败", e); + } + } + + List imageTasks = grouped.getOrDefault("image", List.of()); + if (!imageTasks.isEmpty()) { + try { + List urls = imageTasks.stream().map(MiniContentAuditTask::getContentValue).toList(); + ImageModerationResponse r = aliyunContentAuditUtil.batchImageModeration(urls); + processBatchImageResponseForMixed(imageTasks, r); + } catch (Exception e) { + log.error("mixed批量图片审核失败", e); + } + } + + List 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 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(), JSON.toJSONString(r), riskLevel, null, null); + task.getId(), machineResultJson, riskLevel, null, null, + label, confidence, description, requestId); } - case "image" -> { - ImageModerationResponse r = aliyunContentAuditUtil.imageModeration(contentValue); - String riskLevel = extractRiskLevelFromResponse(r); + } + } + + /** + * 批量图片审核结果仅记录风险信息(mixed 模式) + */ + private void processBatchImageResponseForMixed(List 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(), JSON.toJSONString(r), riskLevel, null, null); + task.getId(), machineResultJson, riskLevel, null, null, + label, confidence, description, requestId); } - case "video" -> handleVideoAudit(task, contentValue); - default -> log.warn("未知内容类型: {}", contentType); } } @@ -158,58 +209,114 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { } /** - * 单条任务机审 - * 对一条审核任务调用阿里云内容安全API,并将结果回填到任务记录。 - * text/image 同步返回 suggestion,video 异步返回 taskId 后续轮询。 - * 依赖方法说明: - * - AliyunContentAuditUtil.textModeration(String) → 阿里云文本审核(同步) - * - AliyunContentAuditUtil.imageModeration(String) → 阿里云图片审核(同步) - * - AliyunContentAuditUtil.videoModeration(String) → 阿里云视频审核(异步,返回taskId) + * 按类型分组批量执行机审(machine 模式:调 AI 并按策略裁决) */ - private void executeSingleTaskAudit(MiniContentAuditTask task, String strictness) { - String contentType = task.getContentType(); - String contentValue = task.getContentValue(); + private void executeBatchAuditByType(List tasks, String strictness) { + Map> grouped = tasks.stream() + .collect(Collectors.groupingBy(MiniContentAuditTask::getContentType)); - try { - switch (contentType) { - case "text" -> handleTextAudit(task, contentValue, strictness); - case "image" -> handleImageAudit(task, contentValue, strictness); - case "video" -> handleVideoAudit(task, contentValue); - default -> log.warn("未知内容类型: {}", contentType); + List textTasks = grouped.getOrDefault("text", List.of()); + if (!textTasks.isEmpty()) { + try { + List contents = textTasks.stream().map(MiniContentAuditTask::getContentValue).toList(); + TextModerationPlusResponse r = aliyunContentAuditUtil.batchTextModerationPlus(contents); + processBatchTextResponse(textTasks, r, strictness); + } catch (Exception e) { + log.error("批量文本审核失败", e); + for (MiniContentAuditTask task : textTasks) { + contentAuditTaskService.updateTaskMachineResult( + task.getId(), null, null, null, AuditConstants.TASK_TO_MANUAL, + null, null, null, null); + } } - } catch (Exception e) { - log.error("机审调用失败, taskId={}, contentType={}", task.getId(), contentType, e); - // 机审异常时直接将任务标记为转人工,避免卡在 reviewing 状态 - contentAuditTaskService.updateTaskMachineResult( - task.getId(), null, null, null, AuditConstants.TASK_TO_MANUAL); + } + + List imageTasks = grouped.getOrDefault("image", List.of()); + if (!imageTasks.isEmpty()) { + try { + List urls = imageTasks.stream().map(MiniContentAuditTask::getContentValue).toList(); + ImageModerationResponse r = aliyunContentAuditUtil.batchImageModeration(urls); + processBatchImageResponse(imageTasks, r, strictness); + } catch (Exception e) { + log.error("批量图片审核失败", e); + for (MiniContentAuditTask task : imageTasks) { + contentAuditTaskService.updateTaskMachineResult( + task.getId(), null, null, null, AuditConstants.TASK_TO_MANUAL, + null, null, null, null); + } + } + } + + List videoTasks = grouped.getOrDefault("video", List.of()); + for (MiniContentAuditTask task : videoTasks) { + handleVideoAudit(task, task.getContentValue()); } } /** - * 文本审核(同步)。 + * 批量文本审核结果处理(machine 模式:按策略裁决) */ - private void handleTextAudit(MiniContentAuditTask task, String textContent, String strictness) { - TextModerationPlusResponse response = aliyunContentAuditUtil.textModerationPlus(textContent); - String riskLevel = extractRiskLevelFromResponse(response); + private void processBatchTextResponse(List tasks, TextModerationPlusResponse response, String strictness) { String machineResultJson = JSON.toJSONString(response); - applyAuditResultToTask(task, riskLevel, machineResultJson, strictness); + 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); + if (riskLevel == null) { + contentAuditTaskService.updateTaskMachineResult( + task.getId(), machineResultJson, null, null, AuditConstants.TASK_TO_MANUAL, + label, confidence, description, requestId); + } else { + String result = applyStrategy(riskLevel, strictness); + String taskStatus = AuditConstants.RESULT_PASSED.equals(result) + ? AuditConstants.TASK_SUCCESS : AuditConstants.TASK_TO_MANUAL; + contentAuditTaskService.updateTaskMachineResult( + task.getId(), machineResultJson, riskLevel, result, taskStatus, + label, confidence, description, requestId); + } + } + } } /** - * 图片审核(同步)。 + * 批量图片审核结果处理(machine 模式:按策略裁决) */ - private void handleImageAudit(MiniContentAuditTask task, String imageUrl, String strictness) { - ImageModerationResponse response = aliyunContentAuditUtil.imageModeration(imageUrl); - String riskLevel = extractRiskLevelFromResponse(response); + private void processBatchImageResponse(List tasks, ImageModerationResponse response, String strictness) { String machineResultJson = JSON.toJSONString(response); - applyAuditResultToTask(task, riskLevel, machineResultJson, strictness); + 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); + if (riskLevel == null) { + contentAuditTaskService.updateTaskMachineResult( + task.getId(), machineResultJson, null, null, AuditConstants.TASK_TO_MANUAL, + label, confidence, description, requestId); + } else { + String result = applyStrategy(riskLevel, strictness); + String taskStatus = AuditConstants.RESULT_PASSED.equals(result) + ? AuditConstants.TASK_SUCCESS : AuditConstants.TASK_TO_MANUAL; + contentAuditTaskService.updateTaskMachineResult( + task.getId(), machineResultJson, riskLevel, result, taskStatus, + label, confidence, description, requestId); + } + } + } } /** * 视频审核(异步)。 - * VideoModerationResponse.getBody().getData().getTaskId() 返回异步任务ID, - * 写入 task.taskId 供后续 VideoModerationResult 轮询查询结果。 - * 视频不参与本轮汇总,task.result 暂不设置。 */ private void handleVideoAudit(MiniContentAuditTask task, String videoUrl) { VideoModerationResponse response = aliyunContentAuditUtil.videoModeration(videoUrl); @@ -217,7 +324,6 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { log.warn("视频审核请求返回null, taskId={}", task.getId()); return; } - // 提取异步 taskId 并写入任务记录 String asyncTaskId = extractVideoTaskIdFromResponse(response); if (asyncTaskId != null) { task.setTaskId(asyncTaskId); @@ -225,72 +331,78 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { } } - /** - * 从 VideoModerationResponse 中提取异步 taskId。 - * 调用路径:response.getBody().getData().getTaskId() - */ + // ================================================================ + // 从阿里云响应中提取数据 + // ================================================================ + private String extractVideoTaskIdFromResponse(VideoModerationResponse response) { - if (response.getBody() == null) { + if (response.getBody() == null) return null; + if (response.getBody().getData() == null) return null; + return response.getBody().getData().getTaskId(); + } + + /** 从响应的 body 中提取 requestId */ + private String extractRequestIdFromResponse(Object response) { + try { + Object body = invokeGetter(response, "getBody"); + if (body == null) return null; + Object requestId = invokeGetter(body, "getRequestId"); + return requestId != null ? requestId.toString() : null; + } catch (Exception e) { return null; } - if (response.getBody().getData() == null) { + } + + /** 从响应的 body 中提取 data 列表 */ + private List extractDataListFromResponse(Object response) { + try { + Object body = invokeGetter(response, "getBody"); + if (body == null) return null; + Object data = invokeGetter(body, "getData"); + if (data instanceof List list) return list; + return null; + } catch (Exception e) { return null; } - // VideoModerationResponseBody.getData() 返回 VideoModerationResponseBodyData,含 getTaskId() - return response.getBody().getData().getTaskId(); } - /** - * 将机审结果应用到任务记录:riskLevel + strictness → result → 任务最终 status - */ - private void applyAuditResultToTask(MiniContentAuditTask task, String riskLevel, String machineResultJson, String strictness) { - if (riskLevel == null) { - log.warn("机审riskLevel为null, 转人工处理, taskId={}", task.getId()); - contentAuditTaskService.updateTaskMachineResult( - task.getId(), machineResultJson, null, null, AuditConstants.TASK_TO_MANUAL); - return; + /** 从 data 列表中单个元素提取 riskLevel */ + private String extractRiskLevelFromDataItem(Object dataItem) { + try { + Object riskLevel = invokeGetter(dataItem, "getRiskLevel"); + return riskLevel != null ? riskLevel.toString() : null; + } catch (Exception e) { + return null; } - - // 1. riskLevel + strictness → result(passed / failed) - String result = applyStrategy(riskLevel, strictness); - - // 2. result 决定任务最终状态 - String taskStatus = AuditConstants.RESULT_PASSED.equals(result) - ? AuditConstants.TASK_SUCCESS : AuditConstants.TASK_TO_MANUAL; - - // 回填任务表:machine_result(JSON) / risk_level / result / status - contentAuditTaskService.updateTaskMachineResult(task.getId(), machineResultJson, riskLevel, result, taskStatus); } - // ================================================================ - // 从阿里云响应中提取 riskLevel - // ================================================================ + /** 从 data 列表中单个元素提取 label */ + private String extractLabelFromDataItem(Object dataItem) { + try { + Object label = invokeGetter(dataItem, "getLabel"); + return label != null ? label.toString() : null; + } catch (Exception e) { + return null; + } + } - /** - * 从阿里云文本/图片审核响应中提取 riskLevel(none/medium/high)。 - * 使用反射调用 getBody() → getData() → getRiskLevel()。 - * 如果 Data 为 List 类型,取第一个元素。 - */ - private String extractRiskLevelFromResponse(Object response) { - if (response == 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 body = invokeGetter(response, "getBody"); - if (body == null) { - return null; - } - Object data = invokeGetter(body, "getData"); - if (data == null) { - return null; - } - if (data instanceof List dataList && !dataList.isEmpty()) { - data = dataList.get(0); - } - Object riskLevel = invokeGetter(data, "getRiskLevel"); - return riskLevel != null ? riskLevel.toString() : null; + Object description = invokeGetter(dataItem, "getDescription"); + return description != null ? description.toString() : null; } catch (Exception e) { - log.error("提取riskLevel失败", e); return null; } } @@ -509,7 +621,8 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { // mixed 类型:记录机审结果,更新审核状态 manual,不裁决 if (AuditConstants.AUDIT_TYPE_MIXED.equals(audit.getAuditType())) { contentAuditTaskService.updateTaskMachineResult( - task.getId(), dataJson, riskLevel, null, "manual"); + task.getId(), dataJson, riskLevel, null, "manual", + null, null, null, null); log.info("视频任务AI分析完成(mixed), taskId={}, riskLevel={}", task.getId(), riskLevel); return true; } @@ -526,7 +639,8 @@ public class AuditExecutorServiceImpl implements AuditExecutorService { ? AuditConstants.TASK_SUCCESS : AuditConstants.TASK_TO_MANUAL; contentAuditTaskService.updateTaskMachineResult( - task.getId(), dataJson, riskLevel, result, taskStatus); + task.getId(), dataJson, riskLevel, result, taskStatus, + null, null, null, null); log.info("视频任务审核完成, taskId={}, riskLevel={}, strictness={}, result={}", task.getId(), riskLevel, strictness, result); diff --git a/src/main/java/com/youlai/boot/admin/service/impl/ContentAuditAppealServiceImpl.java b/src/main/java/com/youlai/boot/admin/service/impl/ContentAuditAppealServiceImpl.java new file mode 100644 index 0000000..4e568ec --- /dev/null +++ b/src/main/java/com/youlai/boot/admin/service/impl/ContentAuditAppealServiceImpl.java @@ -0,0 +1,96 @@ +package com.youlai.boot.admin.service.impl; + +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.youlai.boot.admin.constant.AuditConstants; +import com.youlai.boot.admin.model.entity.MiniContentAudit; +import com.youlai.boot.admin.model.form.AppealHandleForm; +import com.youlai.boot.admin.model.query.AppealQuery; +import com.youlai.boot.admin.model.vo.AppealVO; +import com.youlai.boot.admin.service.ContentAuditAppealService; +import com.youlai.boot.admin.service.ContentAuditService; +import com.youlai.boot.common.exception.MsgException; +import com.youlai.boot.framework.security.util.SecurityUtils; +import com.youlai.boot.mini.mapper.MiniContentAuditAppealMapper; +import com.youlai.boot.mini.model.entity.MiniContentAuditAppeal; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ContentAuditAppealServiceImpl implements ContentAuditAppealService { + + private final MiniContentAuditAppealMapper appealMapper; + private final ContentAuditService contentAuditService; + + @Override + public IPage pageAppeal(AppealQuery query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (query.getStatus() != null && !query.getStatus().isBlank()) { + wrapper.eq(MiniContentAuditAppeal::getStatus, query.getStatus()); + } + wrapper.orderByDesc(MiniContentAuditAppeal::getCreateTime); + + Page page = new Page<>(query.getPageNum(), query.getPageSize()); + Page result = appealMapper.selectPage(page, wrapper); + + return result.convert(entity -> AppealVO.builder() + .id(entity.getId()) + .uuid(entity.getUuid()) + .auditId(entity.getAuditId()) + .userId(entity.getUserId()) + .reason(entity.getReason()) + .evidence(entity.getEvidence()) + .status(entity.getStatus()) + .handlerUserId(entity.getHandlerUserId()) + .handleRemark(entity.getHandleRemark()) + .handleTime(entity.getHandleTime()) + .createTime(entity.getCreateTime()) + .build()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void handleAppeal(Long id, AppealHandleForm form) { + MiniContentAuditAppeal appeal = appealMapper.selectById(id); + if (appeal == null) { + throw new MsgException("申诉记录不存在"); + } + if (!"pending".equals(appeal.getStatus())) { + throw new MsgException("申诉已处理,无法重复操作"); + } + + Long adminId = SecurityUtils.getUserId(); + Date now = new Date(); + + appeal.setHandlerUserId(adminId); + appeal.setHandleRemark(form.getHandleRemark()); + appeal.setHandleTime(now); + appeal.setStatus(form.getResult()); + appeal.setUpdateBy(adminId); + appeal.setUpdateTime(now); + appeal.setUpdateTimestamp(System.currentTimeMillis()); + appealMapper.updateById(appeal); + + // 更新审核汇总状态 + MiniContentAudit audit = contentAuditService.getById(appeal.getAuditId()); + if (audit != null) { + if ("approved".equals(form.getResult())) { + contentAuditService.updateAuditStatus(appeal.getAuditId(), + AuditConstants.AUDIT_PASSED, AuditConstants.RESULT_PASSED); + log.info("申诉通过, auditId={}, 审核状态已改为passed", appeal.getAuditId()); + } else { + contentAuditService.updateAuditStatus(appeal.getAuditId(), + AuditConstants.AUDIT_FAILED, AuditConstants.RESULT_FAILED); + log.info("申诉驳回, auditId={}, 审核状态维持failed", appeal.getAuditId()); + } + } + } + +} 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 index 3a2175f..a16c6dd 100644 --- a/src/main/java/com/youlai/boot/admin/service/impl/ContentAuditServiceImpl.java +++ b/src/main/java/com/youlai/boot/admin/service/impl/ContentAuditServiceImpl.java @@ -16,12 +16,13 @@ import java.util.Date; public class ContentAuditServiceImpl extends ServiceImpl implements ContentAuditService { @Override - public MiniContentAudit createAudit(String moduleCode, Long bizId, String auditType) { + public MiniContentAudit createAudit(String moduleCode, Long bizId, String auditType, String triggerType) { MiniContentAudit entity = new MiniContentAudit(); entity.setUuid(IdUtil.fastSimpleUUID()); entity.setModuleCode(moduleCode); entity.setBizId(bizId); entity.setAuditType(auditType); + entity.setTriggerType(triggerType); entity.setStatus(AuditConstants.AUDIT_REVIEWING); entity.setCreateBy(SecurityUtils.getUserId()); entity.setCreateTime(new Date()); 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 index 99a0cdb..8cb9447 100644 --- a/src/main/java/com/youlai/boot/admin/service/impl/ContentAuditTaskServiceImpl.java +++ b/src/main/java/com/youlai/boot/admin/service/impl/ContentAuditTaskServiceImpl.java @@ -74,20 +74,20 @@ public class ContentAuditTaskServiceImpl extends ServiceImpl lookupUserPost(bizId); + case "adoption_diary" -> lookupAdoptionDiary(bizId); + case "animal_note" -> lookupAnimalNote(bizId); + case "post_comment" -> lookupPostComment(bizId); + case "diary_comment" -> lookupDiaryComment(bizId); + case "note_comment" -> lookupNoteComment(bizId); + default -> { + log.warn("未知业务模块编码: {}", moduleCode); + yield emptyResult(); + } + }; + } + + private AuditContentDTO lookupUserPost(Long id) { + MiniUserPost post = userPostMapper.selectById(id); + if (post == null) return emptyResult(); + + List texts = new ArrayList<>(); + if (post.getTitle() != null) texts.add(post.getTitle()); + if (post.getContent() != null) texts.add(post.getContent()); + + return buildContentResult(texts, lookupMedia(userPostMediaMapper, + new LambdaQueryWrapper().eq(MiniUserPostMedia::getPostId, id))); + } + + private AuditContentDTO lookupAdoptionDiary(Long id) { + MiniAdoptionDiary diary = adoptionDiaryMapper.selectById(id); + if (diary == null) return emptyResult(); + + List texts = new ArrayList<>(); + if (diary.getTitle() != null) texts.add(diary.getTitle()); + if (diary.getContent() != null) texts.add(diary.getContent()); + + return buildContentResult(texts, lookupMedia(adoptionDiaryMediaMapper, + new LambdaQueryWrapper().eq(MiniAdoptionDiaryMedia::getDiaryId, id))); + } + + private AuditContentDTO lookupAnimalNote(Long id) { + MiniStrayAnimalNote note = strayAnimalNoteMapper.selectById(id); + if (note == null) return emptyResult(); + + List texts = new ArrayList<>(); + if (note.getTitle() != null) texts.add(note.getTitle()); + if (note.getContent() != null) texts.add(note.getContent()); + + return buildContentResult(texts, lookupMedia(strayAnimalNoteMediaMapper, + new LambdaQueryWrapper().eq(MiniStrayAnimalNoteMedia::getNoteId, id))); + } + + private AuditContentDTO lookupPostComment(Long id) { + MiniUserPostComment comment = userPostCommentMapper.selectById(id); + if (comment == null) return emptyResult(); + AuditContentDTO dto = new AuditContentDTO(); + dto.setTexts(comment.getContent() != null ? List.of(comment.getContent()) : Collections.emptyList()); + return dto; + } + + private AuditContentDTO lookupDiaryComment(Long id) { + MiniAdoptionDiaryComment comment = adoptionDiaryCommentMapper.selectById(id); + if (comment == null) return emptyResult(); + AuditContentDTO dto = new AuditContentDTO(); + dto.setTexts(comment.getContent() != null ? List.of(comment.getContent()) : Collections.emptyList()); + return dto; + } + + private AuditContentDTO lookupNoteComment(Long id) { + MiniStrayAnimalNoteComment comment = strayAnimalNoteCommentMapper.selectById(id); + if (comment == null) return emptyResult(); + AuditContentDTO dto = new AuditContentDTO(); + dto.setTexts(comment.getContent() != null ? List.of(comment.getContent()) : Collections.emptyList()); + return dto; + } + + /** 分离媒体文件的图片和视频 URL */ + private AuditContentDTO buildContentResult(List texts, + List mediaList) { + AuditContentDTO dto = new AuditContentDTO(); + dto.setTexts(texts); + List images = new ArrayList<>(); + List videos = new ArrayList<>(); + for (MediaUrlPair m : mediaList) { + if ("video".equals(m.type)) { + videos.add(m.url); + } else { + images.add(m.url); + } + } + dto.setImages(images.isEmpty() ? null : images); + dto.setVideos(videos.isEmpty() ? null : videos); + return dto; + } + + private record MediaUrlPair(String type, String url) {} + + /** 通用媒体查询 */ + private List lookupMedia(com.baomidou.mybatisplus.core.mapper.BaseMapper mapper, + LambdaQueryWrapper wrapper) { + List mediaList = mapper.selectList(wrapper); + List result = new ArrayList<>(); + for (M media : mediaList) { + try { + String type = (String) media.getClass().getMethod("getMediaType").invoke(media); + String url = (String) media.getClass().getMethod("getSourceUrl").invoke(media); + if (url != null && !url.isBlank()) { + result.add(new MediaUrlPair(type, url)); + } + } catch (Exception e) { + log.warn("提取媒体信息失败", e); + } + } + return result; + } + + private AuditContentDTO emptyResult() { + return new AuditContentDTO(); + } + +} diff --git a/src/main/java/com/youlai/boot/admin/service/impl/OssCallbackServiceImpl.java b/src/main/java/com/youlai/boot/admin/service/impl/OssCallbackServiceImpl.java new file mode 100644 index 0000000..c6565a6 --- /dev/null +++ b/src/main/java/com/youlai/boot/admin/service/impl/OssCallbackServiceImpl.java @@ -0,0 +1,192 @@ +package com.youlai.boot.admin.service.impl; + +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.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; + + @Value("${audit.aliyun.oss.callbackSeed}") + private String callbackSeed; + + public OssCallbackServiceImpl(ContentAuditTaskService contentAuditTaskService, + ContentAuditService contentAuditService, + ContentAuditConfigService contentAuditConfigService) { + this.contentAuditTaskService = contentAuditTaskService; + this.contentAuditService = contentAuditService; + this.contentAuditConfigService = contentAuditConfigService; + } + + @Override + public void handleImageCallback(String checksum, String content) { + // 1. SHA256 验签 + if (!verifyChecksum(content, checksum)) { + log.warn("OSS回调验签失败, checksum={}, content={}", checksum, content); + return; + } + + // 2. 解析回调内容 + JSONObject payload = JSON.parseObject(content); + Integer code = payload.getInteger("Code"); + if (code == null || code != 200) { + log.warn("OSS回调Code非200, code={}", code); + return; + } + + JSONObject data = payload.getJSONObject("Data"); + if (data == null) { + log.warn("OSS回调Data为空"); + return; + } + + String ossObjectName = data.getString("OssObjectName"); + String riskLevel = data.getString("RiskLevel"); + JSONArray results = data.getJSONArray("Results"); + String requestId = payload.getString("RequestId"); + + log.info("OSS图片审核回调, object={}, riskLevel={}, requestId={}", ossObjectName, riskLevel, requestId); + + // 3. 根据 ossObjectName 匹配审核任务 //TODO 这个接口回调的是 OSS增量图片数据,如果用OSS内容审核,那么审核任务将不会存在,暂时先打印一下数据就行,另外想方案; + //TODO ,如果上传图片就创建一个审核任务,这样确实能这样处理,暂时只讨论方案不要实现; + MiniContentAuditTask task = findTaskByImageName(ossObjectName); + if (task == null) { + log.warn("未找到匹配的审核任务, ossObjectName={}", ossObjectName); + return; + } + + // 4. 提取 label / confidence / description(取第一个 Result) + String label = null; + Float confidence = null; + String description = null; + if (results != null && !results.isEmpty()) { + JSONObject firstResult = results.getJSONObject(0); + JSONArray subResults = firstResult.getJSONArray("Result"); + if (subResults != null && !subResults.isEmpty()) { + JSONObject sub = subResults.getJSONObject(0); + label = sub.getString("Label"); + description = sub.getString("Description"); + if (sub.containsKey("Confidence")) { + confidence = sub.getFloat("Confidence"); + } + } + } + + // 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() + .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 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); + } + + /** SHA256(uid + seed + content) 验签,对应阿里云控制台配置的加密算法 */ + private boolean verifyChecksum(String content, String checksum) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + String input = callbackUid + callbackSeed + content; + byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + StringBuilder hex = new StringBuilder(); + for (byte b : hash) { + hex.append(String.format("%02x", b)); + } + return hex.toString().equals(checksum); + } catch (Exception e) { + log.error("SHA256验签异常", e); + return false; + } + } + + /** 阿里云 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; + return contentAuditTaskService.getOne(new LambdaQueryWrapper() + .eq(MiniContentAuditTask::getContentType, "image") + .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; + } +} diff --git a/src/main/java/com/youlai/boot/admin/service/impl/ReportManageServiceImpl.java b/src/main/java/com/youlai/boot/admin/service/impl/ReportManageServiceImpl.java new file mode 100644 index 0000000..cc1799d --- /dev/null +++ b/src/main/java/com/youlai/boot/admin/service/impl/ReportManageServiceImpl.java @@ -0,0 +1,118 @@ +package com.youlai.boot.admin.service.impl; + +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.youlai.boot.admin.constant.AuditConstants; +import com.youlai.boot.admin.model.dto.AuditContentDTO; +import com.youlai.boot.admin.model.form.ReportHandleForm; +import com.youlai.boot.admin.model.query.ReportQuery; +import com.youlai.boot.admin.model.vo.ReportVO; +import com.youlai.boot.admin.service.AuditExecutorService; +import com.youlai.boot.admin.service.ContentLookupService; +import com.youlai.boot.admin.service.ReportManageService; +import com.youlai.boot.common.exception.MsgException; +import com.youlai.boot.framework.security.util.SecurityUtils; +import com.youlai.boot.mini.mapper.MiniReportMapper; +import com.youlai.boot.mini.model.entity.MiniReport; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ReportManageServiceImpl implements ReportManageService { + + private final MiniReportMapper reportMapper; + private final ContentLookupService contentLookupService; + private final AuditExecutorService auditExecutorService; + + @Override + public IPage pageReport(ReportQuery query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (query.getTargetType() != null && !query.getTargetType().isBlank()) { + wrapper.eq(MiniReport::getTargetType, query.getTargetType()); + } + if (query.getStatus() != null && !query.getStatus().isBlank()) { + wrapper.eq(MiniReport::getStatus, query.getStatus()); + } + wrapper.orderByDesc(MiniReport::getCreateTime); + + Page page = new Page<>(query.getPageNum(), query.getPageSize()); + Page result = reportMapper.selectPage(page, wrapper); + + return result.convert(entity -> ReportVO.builder() + .id(entity.getId()) + .uuid(entity.getUuid()) + .targetType(entity.getTargetType()) + .targetId(entity.getTargetId()) + .reporterUserId(entity.getReporterUserId()) + .reasonCategory(entity.getReasonCategory()) + .evidence(entity.getEvidence()) + .description(entity.getDescription()) + .status(entity.getStatus()) + .auditId(entity.getAuditId()) + .handlerUserId(entity.getHandlerUserId()) + .handleRemark(entity.getHandleRemark()) + .handleTime(entity.getHandleTime()) + .createTime(entity.getCreateTime()) + .build()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void handleReport(Long id, ReportHandleForm form) { + MiniReport report = reportMapper.selectById(id); + if (report == null) { + throw new MsgException("举报记录不存在"); + } + if (!"pending".equals(report.getStatus())) { + throw new MsgException("举报已处理,无法重复操作"); + } + + Long adminId = SecurityUtils.getUserId(); + Date now = new Date(); + + if ("processed".equals(form.getResult())) { + // 受理举报:自动查询目标内容 → 创建审核 → 执行机审 + String moduleCode = report.getTargetType(); + Long bizId = parseBizId(report.getTargetId()); + + AuditContentDTO content = contentLookupService.lookupContent(moduleCode, bizId); + Map auditResult = auditExecutorService.executeAudit( + moduleCode, bizId, content, AuditConstants.TRIGGER_REPORT); + + if (auditResult != null && auditResult.get("auditId") != null) { + report.setAuditId((Long) auditResult.get("auditId")); + log.info("举报受理, 已自动创建审核并执行机审, auditId={}, moduleCode={}, bizId={}", + auditResult.get("auditId"), moduleCode, bizId); + } else { + log.warn("举报受理, 审核配置关闭或不存在, moduleCode={}, bizId={}", moduleCode, bizId); + } + } + + report.setStatus(form.getResult()); + report.setHandlerUserId(adminId); + report.setHandleRemark(form.getHandleRemark()); + report.setHandleTime(now); + report.setUpdateBy(adminId); + report.setUpdateTime(now); + report.setUpdateTimestamp(System.currentTimeMillis()); + reportMapper.updateById(report); + } + + private Long parseBizId(String targetId) { + try { + return Long.parseLong(targetId); + } catch (NumberFormatException e) { + log.warn("targetId无法转为Long: {}", targetId); + return null; + } + } + +} diff --git a/src/main/java/com/youlai/boot/common/util/AliyunContentAuditUtil.java b/src/main/java/com/youlai/boot/common/util/AliyunContentAuditUtil.java index f0606a4..14aa61c 100644 --- a/src/main/java/com/youlai/boot/common/util/AliyunContentAuditUtil.java +++ b/src/main/java/com/youlai/boot/common/util/AliyunContentAuditUtil.java @@ -1,11 +1,13 @@ package com.youlai.boot.common.util; -import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; + +import java.util.List; import com.aliyun.green20220302.Client; import com.aliyun.green20220302.models.*; import com.aliyun.teaopenapi.models.Config; +import com.youlai.boot.common.exception.MsgException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -42,6 +44,12 @@ public class AliyunContentAuditUtil { private static final String REGION_BACKUP = "cn-beijing"; private static final String ENDPOINT_BACKUP = "green-cip.cn-beijing.aliyuncs.com"; + // 批量审核上限 + 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; + /** * 构建客户端 */ @@ -100,7 +108,6 @@ public class AliyunContentAuditUtil { } // ===================== 文本审核 ===================== - // public TextModerationResponse textModeration(String content) { return executeWithFailover(client -> { JSONObject params = new JSONObject(); @@ -114,20 +121,45 @@ public class AliyunContentAuditUtil { }); } + /** + * 批量文本审核(上限100条,单条不超过600字) + */ + public TextModerationResponse batchTextModeration(List contents) { + if (contents == null || contents.isEmpty()) { + return null; + } + if (contents.size() > BATCH_TEXT_MAX) { + throw new MsgException("批量文本审核单次上限" + BATCH_TEXT_MAX + "条,当前" + contents.size() + "条"); + } + for (int i = 0; i < contents.size(); i++) { + String text = contents.get(i); + if (text != null && text.length() > BATCH_TEXT_SINGLE_MAX_CHARS) { + throw new MsgException("第" + (i + 1) + "条文本超过" + BATCH_TEXT_SINGLE_MAX_CHARS + "字上限"); + } + } + return executeWithFailover(client -> { + JSONObject params = new JSONObject(); + JSONArray arr = new JSONArray(); + arr.addAll(contents); + params.put("content", arr); + + TextModerationRequest request = new TextModerationRequest() + .setService("comment_detection") + .setServiceParameters(params.toJSONString()); + + return client.textModeration(request); + }); + } + // ===================== 文本审核Plus ===================== //nickname_detection_pro 用户昵称检测_专业版(用户昵称) ;ugc_moderation_byllm_pro UGC场景文本审核大模型服务_专业版(个人简介 / 作品内容) ; //comment_detection_pro 公聊评论内容检测_专业版(评论); ugc_moderation_byllm UGC场景文本审核大模型服务 (作品标题) public TextModerationPlusResponse textModerationPlus(String content) { return executeWithFailover(client -> { JSONObject params = new JSONObject(); -// params.put("content", content); - - JSONArray arr = new JSONArray(); - arr.add("狗日的"); - arr.add(content); - params.put("content", arr); + params.put("content", content); - TextModerationPlusRequest textModerationPlusRequest = new TextModerationPlusRequest () + TextModerationPlusRequest textModerationPlusRequest = new TextModerationPlusRequest() .setService("ugc_moderation_byllm_pro") .setServiceParameters(params.toJSONString()); @@ -135,6 +167,36 @@ public class AliyunContentAuditUtil { }); } + /** + * 批量文本审核Plus(上限100条,单条不超过600字) + */ + public TextModerationPlusResponse batchTextModerationPlus(List contents) { + if (contents == null || contents.isEmpty()) { + return null; + } + if (contents.size() > BATCH_TEXT_MAX) { + throw new MsgException("批量文本审核单次上限" + BATCH_TEXT_MAX + "条,当前" + contents.size() + "条"); + } + for (int i = 0; i < contents.size(); i++) { + String text = contents.get(i); + if (text != null && text.length() > BATCH_TEXT_SINGLE_MAX_CHARS) { + throw new MsgException("第" + (i + 1) + "条文本超过" + BATCH_TEXT_SINGLE_MAX_CHARS + "字上限"); + } + } + return executeWithFailover(client -> { + JSONObject params = new JSONObject(); + JSONArray arr = new JSONArray(); + arr.addAll(contents); + params.put("content", arr); + + TextModerationPlusRequest request = new TextModerationPlusRequest() + .setService("ugc_moderation_byllm_pro") + .setServiceParameters(params.toJSONString()); + + return client.textModerationPlus(request); + }); + } + // ===================== 图片审核 ===================== // 头像图片检测:profilePhotoCheck (头像) ; AI生成图片鉴别_含隐式标识版:aigcDetectorFull(AI生成图片) ; // 大小模型融合图片审核服务:postlmageCheckByVL (用户上传的图片) ; OSS基线检测(OSS普惠版专用):oss_baselineCheck @@ -142,9 +204,6 @@ public class AliyunContentAuditUtil { public ImageModerationResponse imageModeration(String imageUrl) { return executeWithFailover(client -> { JSONObject params = new JSONObject(); -// JSONArray arr = new JSONArray(); -// arr.add(imageUrl); -// params.put("images", arr); params.put("imageUrl", imageUrl); ImageModerationRequest request = new ImageModerationRequest() @@ -155,6 +214,30 @@ public class AliyunContentAuditUtil { }); } + /** + * 批量图片审核(上限100张) + */ + public ImageModerationResponse batchImageModeration(List 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); + }); + } + // ===================== 视频审核(异步) ===================== //视频文件检测:videoDetection ; AI生成视频判定:videoAigcDetector ; 视频文件检测_大模型版:videoDetectionByVL public VideoModerationResponse videoModeration(String videoUrl) { @@ -170,6 +253,30 @@ public class AliyunContentAuditUtil { }); } + /** + * 批量视频审核(异步,上限10个) + */ + public VideoModerationResponse batchVideoModeration(List 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); + }); + } + // ===================== 查询视频审核结果 ===================== public VideoModerationResultResponse videoModerationResult(String taskId) { return executeWithFailover(client -> { @@ -184,7 +291,7 @@ public class AliyunContentAuditUtil { }); } - // ===================== 取消视频任务(直播流) ===================== + // ===================== 取消视频直播流检测任务 ===================== public VideoModerationCancelResponse cancelVideoTask(String taskId) { return executeWithFailover(client -> { JSONObject params = new JSONObject(); @@ -198,13 +305,6 @@ public class AliyunContentAuditUtil { }); } - /** - * 输出调试日志 - */ - public static void print(Object result) { - System.out.println(JSON.toJSONString(result, true)); - } - /** * 函数式接口:用于统一封装 SDK 调用逻辑 */ diff --git a/src/main/java/com/youlai/boot/mini/controller/MiniContentAuditAppealController.java b/src/main/java/com/youlai/boot/mini/controller/MiniContentAuditAppealController.java new file mode 100644 index 0000000..5fb19b5 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/controller/MiniContentAuditAppealController.java @@ -0,0 +1,34 @@ +package com.youlai.boot.mini.controller; + +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.Result; +import com.youlai.boot.mini.model.form.AppealSubmitForm; +import com.youlai.boot.mini.service.MiniContentAuditAppealService; +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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "小程序端-内容申诉相关接口") +@RestController +@RequestMapping("/api/v1/mini/appeal") +@RequiredArgsConstructor +public class MiniContentAuditAppealController { + + private final MiniContentAuditAppealService appealService; + + @Operation(summary = "提交申诉") + @PostMapping("/submit") + @Log(module = LogModuleEnum.CONTENT_AUDIT_APPEAL, value = ActionTypeEnum.INSERT) + public Result submitAppeal(@Valid @RequestBody AppealSubmitForm form) { + appealService.submitAppeal(form); + return Result.success(); + } + +} diff --git a/src/main/java/com/youlai/boot/mini/controller/MiniReportController.java b/src/main/java/com/youlai/boot/mini/controller/MiniReportController.java new file mode 100644 index 0000000..4d8ce5c --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/controller/MiniReportController.java @@ -0,0 +1,34 @@ +package com.youlai.boot.mini.controller; + +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.Result; +import com.youlai.boot.mini.model.form.ReportSubmitForm; +import com.youlai.boot.mini.service.MiniReportService; +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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "小程序端-举报相关接口") +@RestController +@RequestMapping("/api/v1/mini/report") +@RequiredArgsConstructor +public class MiniReportController { + + private final MiniReportService reportService; + + @Operation(summary = "提交举报") + @PostMapping("/submit") + @Log(module = LogModuleEnum.REPORT, value = ActionTypeEnum.INSERT) + public Result submitReport(@Valid @RequestBody ReportSubmitForm form) { + reportService.submitReport(form); + return Result.success(); + } + +} diff --git a/src/main/java/com/youlai/boot/mini/model/form/AppealSubmitForm.java b/src/main/java/com/youlai/boot/mini/model/form/AppealSubmitForm.java new file mode 100644 index 0000000..7763c3b --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/AppealSubmitForm.java @@ -0,0 +1,25 @@ +package com.youlai.boot.mini.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 AppealSubmitForm { + + @NotNull(message = "审核ID不能为空") + @Schema(description = "审核汇总ID") + private Long auditId; + + @NotBlank(message = "申诉原因不能为空") + @Schema(description = "申诉原因") + private String reason; + + @Schema(description = "证据图片URL列表,逗号分隔") + private String evidence; + +} diff --git a/src/main/java/com/youlai/boot/mini/model/form/ReportSubmitForm.java b/src/main/java/com/youlai/boot/mini/model/form/ReportSubmitForm.java new file mode 100644 index 0000000..1764760 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/model/form/ReportSubmitForm.java @@ -0,0 +1,32 @@ +package com.youlai.boot.mini.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 ReportSubmitForm { + + @NotBlank(message = "举报目标类型不能为空") + @Schema(description = "举报目标类型: animal_note / adoption_diary / user_post / note_comment / diary_comment / post_comment / username") + private String targetType; + + @NotBlank(message = "被举报内容ID不能为空") + @Schema(description = "被举报内容ID") + private String targetId; + + @NotBlank(message = "举报原因类别不能为空") + @Schema(description = "举报原因: 违法违规 / 侵权冒犯 / 垃圾虚假 / 违规操作 / 其他") + private String reasonCategory; + + @Schema(description = "证据图片URL列表,逗号分隔") + private String evidence; + + @Schema(description = "举报补充描述") + private String description; + +} diff --git a/src/main/java/com/youlai/boot/mini/service/MiniContentAuditAppealService.java b/src/main/java/com/youlai/boot/mini/service/MiniContentAuditAppealService.java new file mode 100644 index 0000000..ea68cc8 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/service/MiniContentAuditAppealService.java @@ -0,0 +1,10 @@ +package com.youlai.boot.mini.service; + +import com.youlai.boot.mini.model.form.AppealSubmitForm; + +public interface MiniContentAuditAppealService { + + /** 用户提交申诉 */ + void submitAppeal(AppealSubmitForm form); + +} diff --git a/src/main/java/com/youlai/boot/mini/service/MiniReportService.java b/src/main/java/com/youlai/boot/mini/service/MiniReportService.java new file mode 100644 index 0000000..d332c14 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/service/MiniReportService.java @@ -0,0 +1,10 @@ +package com.youlai.boot.mini.service; + +import com.youlai.boot.mini.model.form.ReportSubmitForm; + +public interface MiniReportService { + + /** 用户提交举报 */ + void submitReport(ReportSubmitForm form); + +} diff --git a/src/main/java/com/youlai/boot/mini/service/impl/MiniContentAuditAppealServiceImpl.java b/src/main/java/com/youlai/boot/mini/service/impl/MiniContentAuditAppealServiceImpl.java new file mode 100644 index 0000000..c6990a9 --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/service/impl/MiniContentAuditAppealServiceImpl.java @@ -0,0 +1,69 @@ +package com.youlai.boot.mini.service.impl; + +import cn.hutool.core.util.IdUtil; +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.service.ContentAuditService; +import com.youlai.boot.framework.security.util.SecurityUtils; +import com.youlai.boot.mini.mapper.MiniContentAuditAppealMapper; +import com.youlai.boot.mini.model.entity.MiniContentAuditAppeal; +import com.youlai.boot.mini.model.form.AppealSubmitForm; +import com.youlai.boot.mini.service.MiniContentAuditAppealService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; + +@Service +@RequiredArgsConstructor +@Slf4j +public class MiniContentAuditAppealServiceImpl implements MiniContentAuditAppealService { + + private final MiniContentAuditAppealMapper appealMapper; + private final ContentAuditService contentAuditService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void submitAppeal(AppealSubmitForm form) { + Long userId = SecurityUtils.getUserId(); + + // 检查审核记录是否存在且状态为 failed(只有不通过的内容才能申诉) + MiniContentAudit audit = contentAuditService.getById(form.getAuditId()); + if (audit == null) { + throw new RuntimeException("审核记录不存在"); + } + if (!AuditConstants.AUDIT_FAILED.equals(audit.getStatus())) { + throw new RuntimeException("当前审核状态不允许申诉"); + } + + // 检查是否已有待处理/已通过的申诉 + Long existingCount = appealMapper.selectCount(new LambdaQueryWrapper() + .eq(MiniContentAuditAppeal::getAuditId, form.getAuditId()) + .in(MiniContentAuditAppeal::getStatus, "pending", "approved")); + if (existingCount != null && existingCount > 0) { + throw new RuntimeException("已存在有效的申诉记录,请勿重复提交"); + } + + // 创建申诉记录 + MiniContentAuditAppeal appeal = new MiniContentAuditAppeal(); + appeal.setUuid(IdUtil.fastSimpleUUID()); + appeal.setAuditId(form.getAuditId()); + appeal.setUserId(userId); + appeal.setReason(form.getReason()); + appeal.setEvidence(form.getEvidence()); + appeal.setStatus("pending"); + appeal.setCreateBy(userId); + appeal.setCreateTime(new Date()); + appeal.setCreateTimestamp(System.currentTimeMillis()); + appealMapper.insert(appeal); + + // 更新审核状态为申诉中 + contentAuditService.updateAuditStatus(form.getAuditId(), AuditConstants.AUDIT_APPEALING, null); + + log.info("用户提交申诉成功, auditId={}, userId={}", form.getAuditId(), userId); + } + +} diff --git a/src/main/java/com/youlai/boot/mini/service/impl/MiniReportServiceImpl.java b/src/main/java/com/youlai/boot/mini/service/impl/MiniReportServiceImpl.java new file mode 100644 index 0000000..6972acc --- /dev/null +++ b/src/main/java/com/youlai/boot/mini/service/impl/MiniReportServiceImpl.java @@ -0,0 +1,58 @@ +package com.youlai.boot.mini.service.impl; + +import cn.hutool.core.util.IdUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.youlai.boot.framework.security.util.SecurityUtils; +import com.youlai.boot.mini.mapper.MiniReportMapper; +import com.youlai.boot.mini.model.entity.MiniReport; +import com.youlai.boot.mini.model.form.ReportSubmitForm; +import com.youlai.boot.mini.service.MiniReportService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; + +@Service +@RequiredArgsConstructor +@Slf4j +public class MiniReportServiceImpl implements MiniReportService { + + private final MiniReportMapper reportMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public void submitReport(ReportSubmitForm form) { + Long userId = SecurityUtils.getUserId(); + + // 检查是否已有待处理的重复举报(同一用户 + 同一目标) + Long existingCount = reportMapper.selectCount(new LambdaQueryWrapper() + .eq(MiniReport::getTargetType, form.getTargetType()) + .eq(MiniReport::getTargetId, form.getTargetId()) + .eq(MiniReport::getReporterUserId, userId) + .eq(MiniReport::getStatus, "pending")); + if (existingCount != null && existingCount > 0) { + throw new RuntimeException("您已举报过此内容,请耐心等待处理"); + } + + // 创建举报记录 + MiniReport report = new MiniReport(); + report.setUuid(IdUtil.fastSimpleUUID()); + report.setTargetType(form.getTargetType()); + report.setTargetId(form.getTargetId()); + report.setReporterUserId(userId); + report.setReasonCategory(form.getReasonCategory()); + report.setEvidence(form.getEvidence()); + report.setDescription(form.getDescription()); + report.setStatus("pending"); + report.setCreateBy(userId); + report.setCreateTime(new Date()); + report.setCreateTimestamp(System.currentTimeMillis()); + reportMapper.insert(report); + + log.info("用户提交举报成功, targetType={}, targetId={}, userId={}", + form.getTargetType(), form.getTargetId(), userId); + } + +} diff --git a/src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java b/src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java index af7ffd1..5b32408 100644 --- a/src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java +++ b/src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java @@ -227,7 +227,7 @@ public class UserPostServiceImpl extends ServiceImpl auditResult = auditExecutorService.executeAudit("user_post", postId, auditContent); + Map auditResult = auditExecutorService.executeAudit("user_post", postId, auditContent, AuditConstants.TRIGGER_AUTO); if (auditResult != null) { log.info("用户作品审核任务已创建, postId={}, auditResult={}", postId, auditResult); } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 5797bcb..cd259e1 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -91,6 +91,7 @@ security: - /api/v1/mini/public/** - /api/v1/mini/homePage/listByBounds - /healthcheck + - /api/v1/admin/auditConfig/images/callback #测试OSS图片审核回调 # 非安全端点路径,完全绕过 Spring Security 的过滤器 unsecured-urls: - ${springdoc.swagger-ui.path} @@ -246,4 +247,7 @@ audit: region-id: cn-shanghai #备用节点 green-cip.cn-beijing.aliyuncs.com endpoint: green-cip.cn-shanghai.aliyuncs.com + oss: + callbackUid: 1518801682066998 + callbackSeed: cbHOfSbeUXrux3uUgnUF1ScU-1DF6Vb timeout-minutes: 30