Browse Source

增加Oss审核回调 用户举报申诉接口

glx_phase2
glx 1 week ago
parent
commit
b756b1a86d
  1. 5
      src/main/java/com/youlai/boot/admin/constant/AuditConstants.java
  2. 43
      src/main/java/com/youlai/boot/admin/controller/ContentAuditAppealController.java
  3. 36
      src/main/java/com/youlai/boot/admin/controller/ContentAuditConfigController.java
  4. 43
      src/main/java/com/youlai/boot/admin/controller/ReportManageController.java
  5. 4
      src/main/java/com/youlai/boot/admin/job/VideoAuditPollJob.java
  6. 4
      src/main/java/com/youlai/boot/admin/model/entity/MiniContentAudit.java
  7. 2
      src/main/java/com/youlai/boot/admin/model/entity/MiniContentAuditTask.java
  8. 20
      src/main/java/com/youlai/boot/admin/model/form/AppealHandleForm.java
  9. 20
      src/main/java/com/youlai/boot/admin/model/form/ReportHandleForm.java
  10. 16
      src/main/java/com/youlai/boot/admin/model/query/AppealQuery.java
  11. 19
      src/main/java/com/youlai/boot/admin/model/query/ReportQuery.java
  12. 50
      src/main/java/com/youlai/boot/admin/model/vo/AppealVO.java
  13. 59
      src/main/java/com/youlai/boot/admin/model/vo/ReportVO.java
  14. 3
      src/main/java/com/youlai/boot/admin/service/AuditExecutorService.java
  15. 16
      src/main/java/com/youlai/boot/admin/service/ContentAuditAppealService.java
  16. 2
      src/main/java/com/youlai/boot/admin/service/ContentAuditService.java
  17. 3
      src/main/java/com/youlai/boot/admin/service/ContentAuditTaskService.java
  18. 10
      src/main/java/com/youlai/boot/admin/service/ContentLookupService.java
  19. 8
      src/main/java/com/youlai/boot/admin/service/OssCallbackService.java
  20. 16
      src/main/java/com/youlai/boot/admin/service/ReportManageService.java
  21. 346
      src/main/java/com/youlai/boot/admin/service/impl/AuditExecutorServiceImpl.java
  22. 96
      src/main/java/com/youlai/boot/admin/service/impl/ContentAuditAppealServiceImpl.java
  23. 3
      src/main/java/com/youlai/boot/admin/service/impl/ContentAuditServiceImpl.java
  24. 12
      src/main/java/com/youlai/boot/admin/service/impl/ContentAuditTaskServiceImpl.java
  25. 154
      src/main/java/com/youlai/boot/admin/service/impl/ContentLookupServiceImpl.java
  26. 192
      src/main/java/com/youlai/boot/admin/service/impl/OssCallbackServiceImpl.java
  27. 118
      src/main/java/com/youlai/boot/admin/service/impl/ReportManageServiceImpl.java
  28. 140
      src/main/java/com/youlai/boot/common/util/AliyunContentAuditUtil.java
  29. 34
      src/main/java/com/youlai/boot/mini/controller/MiniContentAuditAppealController.java
  30. 34
      src/main/java/com/youlai/boot/mini/controller/MiniReportController.java
  31. 25
      src/main/java/com/youlai/boot/mini/model/form/AppealSubmitForm.java
  32. 32
      src/main/java/com/youlai/boot/mini/model/form/ReportSubmitForm.java
  33. 10
      src/main/java/com/youlai/boot/mini/service/MiniContentAuditAppealService.java
  34. 10
      src/main/java/com/youlai/boot/mini/service/MiniReportService.java
  35. 69
      src/main/java/com/youlai/boot/mini/service/impl/MiniContentAuditAppealServiceImpl.java
  36. 58
      src/main/java/com/youlai/boot/mini/service/impl/MiniReportServiceImpl.java
  37. 2
      src/main/java/com/youlai/boot/mini/service/impl/UserPostServiceImpl.java
  38. 4
      src/main/resources/application-dev.yml

5
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";
}

43
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<AppealVO> listAppeal(@Valid AppealQuery query) {
IPage<AppealVO> result = appealService.pageAppeal(query);
return PageResult.success(result);
}
@Operation(summary = "处理申诉")
@PostMapping("/handle/{id}")
@Log(module = LogModuleEnum.CONTENT_AUDIT_APPEAL, value = ActionTypeEnum.UPDATE)
public Result<Void> handleAppeal(@PathVariable Long id, @Valid @RequestBody AppealHandleForm form) {
appealService.handleAppeal(id, form);
return Result.success();
}
}

36
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<Void> imageCallback(@RequestBody String body) {
log.info("OSS图片审核回调, body={}", body);
try {
// 解析 form-urlencoded: checksum=xxx&content=xxx%7B...%7D
String checksum = null;
String content = null;
for (String pair : body.split("&")) {
String[] kv = pair.split("=", 2);
if (kv.length == 2) {
if ("checksum".equals(kv[0])) {
checksum = kv[1];
} else if ("content".equals(kv[0])) {
content = URLDecoder.decode(kv[1], StandardCharsets.UTF_8);
}
}
}
if (content != null) {
ossCallbackService.handleImageCallback(checksum, content);
} else {
log.warn("OSS回调content为空");
}
} catch (Exception e) {
log.error("OSS回调处理异常", e);
}
return Result.success();
}
}

43
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<ReportVO> listReport(@Valid ReportQuery query) {
IPage<ReportVO> result = reportService.pageReport(query);
return PageResult.success(result);
}
@Operation(summary = "处理举报")
@PostMapping("/handle/{id}")
@Log(module = LogModuleEnum.REPORT, value = ActionTypeEnum.UPDATE)
public Result<Void> handleReport(@PathVariable Long id, @Valid @RequestBody ReportHandleForm form) {
reportService.handleReport(id, form);
return Result.success();
}
}

4
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();

4
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;

2
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 = "机审描述")

20
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;
}

20
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;
}

16
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;
}

19
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;
}

50
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;
}

59
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;
}

3
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<String, Object> executeAudit(String moduleCode, Long bizId, AuditContentDTO content);
Map<String, Object> executeAudit(String moduleCode, Long bizId, AuditContentDTO content, String triggerType);
/**
* 轮询所有待处理的视频审核异步结果更新任务和汇总状态

16
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<AppealVO> pageAppeal(AppealQuery query);
/** 处理申诉(通过/驳回) */
void handleAppeal(Long id, AppealHandleForm form);
}

2
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> {
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);

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

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

10
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);
}

8
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);
}

16
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<ReportVO> pageReport(ReportQuery query);
/** 处理举报(受理/驳回) */
void handleReport(Long id, ReportHandleForm form);
}

346
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<String, Object> executeAudit(String moduleCode, Long bizId, AuditContentDTO content) {
public Map<String, Object> 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<MiniContentAuditTask> tasks = contentAuditTaskService.listTasksByAuditId(auditId);
for (MiniContentAuditTask task : tasks) {
executeSingleTaskAudit(task, strictness);
}
executeBatchAuditByType(tasks, strictness);
List<MiniContentAuditTask> updatedTasks = contentAuditTaskService.listTasksByAuditId(auditId);
return aggregateTaskResultsAndUpdateAudit(auditId, updatedTasks);
}
@ -98,13 +97,7 @@ public class AuditExecutorServiceImpl implements AuditExecutorService {
*/
private Map<String, Object> executeMixedAudit(Long auditId) {
List<MiniContentAuditTask> tasks = contentAuditTaskService.listTasksByAuditId(auditId);
for (MiniContentAuditTask task : tasks) {
try {
recordMachineResultOnly(task);
} catch (Exception e) {
log.error("mixed审核AI分析失败, taskId={}", task.getId(), e);
}
}
executeBatchMixedAuditByType(tasks);
contentAuditService.updateAuditStatus(auditId, AuditConstants.AUDIT_MANUAL_REVIEW, null);
Map<String, Object> 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<MiniContentAuditTask> tasks) {
Map<String, List<MiniContentAuditTask>> grouped = tasks.stream()
.collect(Collectors.groupingBy(MiniContentAuditTask::getContentType));
List<MiniContentAuditTask> textTasks = grouped.getOrDefault("text", List.of());
if (!textTasks.isEmpty()) {
try {
List<String> contents = textTasks.stream().map(MiniContentAuditTask::getContentValue).toList();
TextModerationPlusResponse r = aliyunContentAuditUtil.batchTextModerationPlus(contents);
processBatchTextResponseForMixed(textTasks, r);
} catch (Exception e) {
log.error("mixed批量文本审核失败", e);
}
}
List<MiniContentAuditTask> imageTasks = grouped.getOrDefault("image", List.of());
if (!imageTasks.isEmpty()) {
try {
List<String> urls = imageTasks.stream().map(MiniContentAuditTask::getContentValue).toList();
ImageModerationResponse r = aliyunContentAuditUtil.batchImageModeration(urls);
processBatchImageResponseForMixed(imageTasks, r);
} catch (Exception e) {
log.error("mixed批量图片审核失败", e);
}
}
List<MiniContentAuditTask> videoTasks = grouped.getOrDefault("video", List.of());
for (MiniContentAuditTask task : videoTasks) {
try {
handleVideoAudit(task, task.getContentValue());
} catch (Exception e) {
log.error("mixed视频审核失败, taskId={}", task.getId(), e);
}
}
}
/**
* 批量文本审核结果仅记录风险信息mixed 模式
*/
private void processBatchTextResponseForMixed(List<MiniContentAuditTask> tasks, TextModerationPlusResponse response) {
String machineResultJson = JSON.toJSONString(response);
String requestId = extractRequestIdFromResponse(response);
List<?> dataList = extractDataListFromResponse(response);
for (int i = 0; i < tasks.size(); i++) {
MiniContentAuditTask task = tasks.get(i);
if (i < (dataList != null ? dataList.size() : 0)) {
Object data = dataList.get(i);
String riskLevel = extractRiskLevelFromDataItem(data);
String label = extractLabelFromDataItem(data);
Float confidence = extractConfidenceFromDataItem(data);
String description = extractDescriptionFromDataItem(data);
contentAuditTaskService.updateTaskMachineResult(
task.getId(), 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<MiniContentAuditTask> tasks, ImageModerationResponse response) {
String machineResultJson = JSON.toJSONString(response);
String requestId = extractRequestIdFromResponse(response);
List<?> dataList = extractDataListFromResponse(response);
for (int i = 0; i < tasks.size(); i++) {
MiniContentAuditTask task = tasks.get(i);
if (i < (dataList != null ? dataList.size() : 0)) {
Object data = dataList.get(i);
String riskLevel = extractRiskLevelFromDataItem(data);
String label = extractLabelFromDataItem(data);
Float confidence = extractConfidenceFromDataItem(data);
String description = extractDescriptionFromDataItem(data);
contentAuditTaskService.updateTaskMachineResult(
task.getId(), 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 同步返回 suggestionvideo 异步返回 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<MiniContentAuditTask> tasks, String strictness) {
Map<String, List<MiniContentAuditTask>> 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<MiniContentAuditTask> textTasks = grouped.getOrDefault("text", List.of());
if (!textTasks.isEmpty()) {
try {
List<String> contents = textTasks.stream().map(MiniContentAuditTask::getContentValue).toList();
TextModerationPlusResponse r = aliyunContentAuditUtil.batchTextModerationPlus(contents);
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<MiniContentAuditTask> imageTasks = grouped.getOrDefault("image", List.of());
if (!imageTasks.isEmpty()) {
try {
List<String> urls = imageTasks.stream().map(MiniContentAuditTask::getContentValue).toList();
ImageModerationResponse r = aliyunContentAuditUtil.batchImageModeration(urls);
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<MiniContentAuditTask> 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<MiniContentAuditTask> 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<MiniContentAuditTask> 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;
}
}
/**
* 从阿里云文本/图片审核响应中提取 riskLevelnone/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);

96
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<AppealVO> pageAppeal(AppealQuery query) {
LambdaQueryWrapper<MiniContentAuditAppeal> wrapper = new LambdaQueryWrapper<>();
if (query.getStatus() != null && !query.getStatus().isBlank()) {
wrapper.eq(MiniContentAuditAppeal::getStatus, query.getStatus());
}
wrapper.orderByDesc(MiniContentAuditAppeal::getCreateTime);
Page<MiniContentAuditAppeal> page = new Page<>(query.getPageNum(), query.getPageSize());
Page<MiniContentAuditAppeal> 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());
}
}
}
}

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

@ -16,12 +16,13 @@ import java.util.Date;
public class ContentAuditServiceImpl extends ServiceImpl<MiniContentAuditMapper, MiniContentAudit> 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());

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

@ -74,20 +74,20 @@ public class ContentAuditTaskServiceImpl extends ServiceImpl<MiniContentAuditTas
}
@Override
public void updateTaskMachineResult(Long taskId, String machineResult, String riskLevel, String result, String status) {
public void updateTaskMachineResult(Long taskId, String machineResult, String riskLevel, String result, String status,
String label, Float confidence, String description, String requestId) {
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(机审完成) / manual(转人工),最终状态
entity.setStatus(status);
entity.setLabel(label);
entity.setConfidence(confidence);
entity.setDescription(description);
entity.setRequestId(requestId);
entity.setUpdateTime(new Date());
entity.setUpdateTimestamp(System.currentTimeMillis());
// MyBatis-Plus updateById:按主键id更新指定字段
this.updateById(entity);
}

154
src/main/java/com/youlai/boot/admin/service/impl/ContentLookupServiceImpl.java

@ -0,0 +1,154 @@
package com.youlai.boot.admin.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.youlai.boot.admin.model.dto.AuditContentDTO;
import com.youlai.boot.admin.service.ContentLookupService;
import com.youlai.boot.mini.mapper.*;
import com.youlai.boot.mini.model.entity.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Service
@RequiredArgsConstructor
@Slf4j
public class ContentLookupServiceImpl implements ContentLookupService {
private final MiniUserPostMapper userPostMapper;
private final MiniUserPostMediaMapper userPostMediaMapper;
private final MiniAdoptionDiaryMapper adoptionDiaryMapper;
private final MiniAdoptionDiaryMediaMapper adoptionDiaryMediaMapper;
private final MiniStrayAnimalNoteMapper strayAnimalNoteMapper;
private final MiniStrayAnimalNoteMediaMapper strayAnimalNoteMediaMapper;
private final MiniUserPostCommentMapper userPostCommentMapper;
private final MiniAdoptionDiaryCommentMapper adoptionDiaryCommentMapper;
private final MiniStrayAnimalNoteCommentMapper strayAnimalNoteCommentMapper;
@Override
public AuditContentDTO lookupContent(String moduleCode, Long bizId) {
if (bizId == null) {
return emptyResult();
}
return switch (moduleCode) {
case "user_post" -> 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<String> 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<MiniUserPostMedia>().eq(MiniUserPostMedia::getPostId, id)));
}
private AuditContentDTO lookupAdoptionDiary(Long id) {
MiniAdoptionDiary diary = adoptionDiaryMapper.selectById(id);
if (diary == null) return emptyResult();
List<String> 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<MiniAdoptionDiaryMedia>().eq(MiniAdoptionDiaryMedia::getDiaryId, id)));
}
private AuditContentDTO lookupAnimalNote(Long id) {
MiniStrayAnimalNote note = strayAnimalNoteMapper.selectById(id);
if (note == null) return emptyResult();
List<String> 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<MiniStrayAnimalNoteMedia>().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 <T> AuditContentDTO buildContentResult(List<String> texts,
List<MediaUrlPair> mediaList) {
AuditContentDTO dto = new AuditContentDTO();
dto.setTexts(texts);
List<String> images = new ArrayList<>();
List<String> 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 <M> List<MediaUrlPair> lookupMedia(com.baomidou.mybatisplus.core.mapper.BaseMapper<M> mapper,
LambdaQueryWrapper<M> wrapper) {
List<M> mediaList = mapper.selectList(wrapper);
List<MediaUrlPair> 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();
}
}

192
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<MiniContentAuditConfig>()
.eq(MiniContentAuditConfig::getModuleCode, audit.getModuleCode())
.eq(MiniContentAuditConfig::getDeleted, false)
.last("LIMIT 1"));
String strictness = (config != null && config.getRiskStrategy() != null)
? config.getRiskStrategy() : AuditConstants.STRATEGY_NORMAL;
String result = applyStrategy(normalizedRisk, strictness);
String taskStatus = AuditConstants.RESULT_PASSED.equals(result)
? AuditConstants.TASK_SUCCESS : AuditConstants.TASK_TO_MANUAL;
contentAuditTaskService.updateTaskMachineResult(
task.getId(), payload.toJSONString(), normalizedRisk, result, taskStatus,
label, confidence, description, requestId);
// 重新汇总 audit
List<MiniContentAuditTask> tasks = contentAuditTaskService.listTasksByAuditId(task.getContentAuditId());
boolean hasFailed = false, hasManual = false, allPassed = true;
for (MiniContentAuditTask t : tasks) {
if (AuditConstants.RESULT_FAILED.equals(t.getResult())) { hasFailed = true; break; }
if (AuditConstants.TASK_TO_MANUAL.equals(t.getStatus())) hasManual = true;
if (!AuditConstants.RESULT_PASSED.equals(t.getResult())) allPassed = false;
}
for (MiniContentAuditTask t : tasks) {
if ("video".equals(t.getContentType()) && t.getResult() == null) { allPassed = false; break; }
}
if (hasFailed) {
contentAuditService.updateAuditStatus(audit.getId(), AuditConstants.AUDIT_FAILED, "failed");
} else if (hasManual) {
contentAuditService.updateAuditStatus(audit.getId(), AuditConstants.AUDIT_MANUAL_REVIEW, null);
} else if (allPassed && !tasks.isEmpty()) {
contentAuditService.updateAuditStatus(audit.getId(), AuditConstants.AUDIT_PASSED, "passed");
}
} else {
// mixed / manual:仅记录机审信息
contentAuditTaskService.updateTaskMachineResult(
task.getId(), payload.toJSONString(), normalizedRisk, null, null,
label, confidence, description, requestId);
}
log.info("OSS回调处理完成, taskId={}, riskLevel={}", task.getId(), normalizedRisk);
}
/** 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<MiniContentAuditTask>()
.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;
}
}

118
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<ReportVO> pageReport(ReportQuery query) {
LambdaQueryWrapper<MiniReport> 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<MiniReport> page = new Page<>(query.getPageNum(), query.getPageSize());
Page<MiniReport> 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<String, Object> 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;
}
}
}

140
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<String> 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<String> 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<String> imageUrls) {
if (imageUrls == null || imageUrls.isEmpty()) {
return null;
}
if (imageUrls.size() > BATCH_IMAGE_MAX) {
throw new MsgException("批量图片审核单次上限" + BATCH_IMAGE_MAX + "张,当前" + imageUrls.size() + "张");
}
return executeWithFailover(client -> {
JSONObject params = new JSONObject();
JSONArray arr = new JSONArray();
arr.addAll(imageUrls);
params.put("images", arr);
ImageModerationRequest request = new ImageModerationRequest()
.setService("oss_baselineCheck")
.setServiceParameters(params.toJSONString());
return client.imageModeration(request);
});
}
// ===================== 视频审核(异步) =====================
//视频文件检测:videoDetection ; AI生成视频判定:videoAigcDetector ; 视频文件检测_大模型版:videoDetectionByVL
public VideoModerationResponse videoModeration(String videoUrl) {
@ -170,6 +253,30 @@ public class AliyunContentAuditUtil {
});
}
/**
* 批量视频审核异步上限10个
*/
public VideoModerationResponse batchVideoModeration(List<String> videoUrls) {
if (videoUrls == null || videoUrls.isEmpty()) {
return null;
}
if (videoUrls.size() > BATCH_VIDEO_MAX) {
throw new MsgException("批量视频审核单次上限" + BATCH_VIDEO_MAX + "个,当前" + videoUrls.size() + "个");
}
return executeWithFailover(client -> {
JSONObject params = new JSONObject();
JSONArray arr = new JSONArray();
arr.addAll(videoUrls);
params.put("urls", arr);
VideoModerationRequest request = new VideoModerationRequest()
.setService("videoDetection")
.setServiceParameters(params.toJSONString());
return client.videoModeration(request);
});
}
// ===================== 查询视频审核结果 =====================
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 调用逻辑
*/

34
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<Void> submitAppeal(@Valid @RequestBody AppealSubmitForm form) {
appealService.submitAppeal(form);
return Result.success();
}
}

34
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<Void> submitReport(@Valid @RequestBody ReportSubmitForm form) {
reportService.submitReport(form);
return Result.success();
}
}

25
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;
}

32
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;
}

10
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);
}

10
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);
}

69
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<MiniContentAuditAppeal>()
.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);
}
}

58
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<MiniReport>()
.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);
}
}

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

@ -227,7 +227,7 @@ public class UserPostServiceImpl extends ServiceImpl<MiniUserPostMapper, MiniUse
Long postId = post.getId();
try {
AuditContentDTO auditContent = buildAuditContent(formData);
Map<String, Object> auditResult = auditExecutorService.executeAudit("user_post", postId, auditContent);
Map<String, Object> auditResult = auditExecutorService.executeAudit("user_post", postId, auditContent, AuditConstants.TRIGGER_AUTO);
if (auditResult != null) {
log.info("用户作品审核任务已创建, postId={}, auditResult={}", postId, auditResult);
}

4
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

Loading…
Cancel
Save