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