38 changed files with 1568 additions and 150 deletions
@ -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(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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); |
||||
|
|
||||
|
} |
||||
@ -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); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
package com.youlai.boot.admin.service; |
||||
|
|
||||
|
public interface OssCallbackService { |
||||
|
|
||||
|
/** 处理OSS图片审核增量回调 */ |
||||
|
void handleImageCallback(String checksum, String content); |
||||
|
|
||||
|
} |
||||
@ -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); |
||||
|
|
||||
|
} |
||||
@ -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()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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); |
||||
|
|
||||
|
} |
||||
@ -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); |
||||
|
|
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue