feat(admin): 举报工单返回短剧与分集信息

This commit is contained in:
Zhou Weidong committed 2026-10-21 16:44:46 +08:00
1 parent 6ddddef5ec
commit 9a74876b3f
8 files changed
+727 -4

No files matched your search

@@ -2,11 +2,12 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.admin.service.ReportAdminService;
import com.fruitshorts.admin.vo.AdminReportRespVO;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.interaction.dal.entity.ContentReport;
import com.fruitshorts.interaction.service.ReportService;
import com.fruitshorts.interaction.vo.ReportHandleReqVO;
import com.fruitshorts.interaction.vo.ReportQueryReqVO;
@@ -28,13 +29,14 @@ import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor
public class ReportAdminController {
private final ReportAdminService reportAdminService;
private final ReportService reportService;
@Operation(summary = "举报工单分页查询")
@GetMapping("/page")
@PreAuthorize("@ss.hasPermission('report:list')")
public Result<PageResult<ContentReport>> page(ReportQueryReqVO req) {
return Result.ok(reportService.page(req));
public Result<PageResult<AdminReportRespVO>> page(ReportQueryReqVO req) {
return Result.ok(reportAdminService.page(req));
}
/** 处理工单:结论成立会联动驳回被举报评论,可选择同时封禁被举报用户。 */
@@ -0,0 +1,283 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fruitshorts.admin.vo.AdminReportRespVO;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.content.dal.entity.Drama;
import com.fruitshorts.content.dal.entity.DramaEpisode;
import com.fruitshorts.content.dal.mapper.DramaEpisodeMapper;
import com.fruitshorts.content.dal.mapper.DramaMapper;
import com.fruitshorts.interaction.dal.entity.ContentComment;
import com.fruitshorts.interaction.dal.entity.ContentReport;
import com.fruitshorts.interaction.dal.mapper.ContentCommentMapper;
import com.fruitshorts.interaction.dal.mapper.ContentReportMapper;
import com.fruitshorts.interaction.service.CommentService;
import com.fruitshorts.interaction.service.ReportService;
import com.fruitshorts.interaction.vo.ReportQueryReqVO;
import com.fruitshorts.user.dal.entity.AppUser;
import com.fruitshorts.user.dal.mapper.AppUserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 后台举报工单分页与目标展示信息装配。
*
* <p>举报只保存目标类型和 id;该服务在当前页一次性加载评论、剧、集和用户,
* 避免列表逐行查询,同时保持原举报处理流程不变。
*/
@Service
@RequiredArgsConstructor
public class ReportAdminService {
private static final int DEFAULT_PAGE_INDEX = 1;
private static final int DEFAULT_PAGE_SIZE = 10;
private static final int DELETED_FLAG = 1;
private final ContentReportMapper reportMapper;
private final ContentCommentMapper commentMapper;
private final DramaMapper dramaMapper;
private final DramaEpisodeMapper episodeMapper;
private final AppUserMapper appUserMapper;
/** 按举报工单筛选条件分页查询,并补齐展示字段。 */
public PageResult<AdminReportRespVO> page(ReportQueryReqVO req) {
Page<ContentReport> page = new Page<>(
req.getPageIndex() == null || req.getPageIndex() < 1
? DEFAULT_PAGE_INDEX : req.getPageIndex(),
req.getPageSize() == null || req.getPageSize() < 1
? DEFAULT_PAGE_SIZE : req.getPageSize());
IPage<ContentReport> result = reportMapper.selectPage(page,
Wrappers.<ContentReport>lambdaQuery()
.eq(StringUtils.hasText(req.getStatus()),
ContentReport::getStatus, req.getStatus())
.eq(StringUtils.hasText(req.getTargetType()),
ContentReport::getTargetType, req.getTargetType())
.eq(StringUtils.hasText(req.getReasonCode()),
ContentReport::getReasonCode, req.getReasonCode())
.eq(req.getReporterId() != null,
ContentReport::getReporterId, req.getReporterId())
.orderByDesc(ContentReport::getId));
return new PageResult<>(result.getTotal(), toVoList(result.getRecords()));
}
private List<AdminReportRespVO> toVoList(List<ContentReport> rows) {
if (rows.isEmpty()) {
return List.of();
}
Map<Long, ContentComment> comments = loadComments(rows);
Map<Long, Drama> dramas = loadDramas(rows, comments);
Map<Long, DramaEpisode> episodes = loadEpisodes(comments);
Map<Long, AppUser> users = loadUsers(rows);
List<AdminReportRespVO> list = new ArrayList<>(rows.size());
for (ContentReport row : rows) {
list.add(toVo(row, comments, dramas, episodes, users));
}
return list;
}
private Map<Long, ContentComment> loadComments(List<ContentReport> rows) {
List<Long> ids = rows.stream()
.filter(row -> ReportService.TYPE_COMMENT.equals(row.getTargetType()))
.map(ContentReport::getTargetId)
.filter(id -> id != null && id > 0L)
.distinct()
.toList();
Map<Long, ContentComment> result = new HashMap<>();
if (ids.isEmpty()) {
return result;
}
for (ContentComment comment : commentMapper.selectByIdsIncludingDeleted(ids)) {
result.put(comment.getId(), comment);
}
return result;
}
private Map<Long, Drama> loadDramas(List<ContentReport> rows,
Map<Long, ContentComment> comments) {
Set<Long> ids = new LinkedHashSet<>();
for (ContentReport row : rows) {
if (ReportService.TYPE_DRAMA.equals(row.getTargetType())) {
addPositive(ids, row.getTargetId());
continue;
}
if (ReportService.TYPE_COMMENT.equals(row.getTargetType())) {
ContentComment comment = comments.get(row.getTargetId());
if (!isDeleted(comment)) {
addPositive(ids, comment.getDramaId());
}
}
}
Map<Long, Drama> result = new HashMap<>();
if (ids.isEmpty()) {
return result;
}
for (Drama drama : dramaMapper.selectBatchIds(ids)) {
result.put(drama.getId(), drama);
}
return result;
}
private Map<Long, DramaEpisode> loadEpisodes(Map<Long, ContentComment> comments) {
Set<Long> ids = new LinkedHashSet<>();
for (ContentComment comment : comments.values()) {
if (!isDeleted(comment)) {
addPositive(ids, comment.getEpisodeId());
}
}
Map<Long, DramaEpisode> result = new HashMap<>();
if (ids.isEmpty()) {
return result;
}
for (DramaEpisode episode : episodeMapper.selectBatchIds(ids)) {
result.put(episode.getId(), episode);
}
return result;
}
private Map<Long, AppUser> loadUsers(List<ContentReport> rows) {
Set<Long> ids = new LinkedHashSet<>();
for (ContentReport row : rows) {
addPositive(ids, row.getReporterId());
if (ReportService.TYPE_USER.equals(row.getTargetType())) {
addPositive(ids, row.getTargetId());
}
}
Map<Long, AppUser> result = new HashMap<>();
if (ids.isEmpty()) {
return result;
}
for (AppUser user : appUserMapper.selectBatchIds(ids)) {
result.put(user.getId(), user);
}
return result;
}
private AdminReportRespVO toVo(ContentReport report,
Map<Long, ContentComment> comments,
Map<Long, Drama> dramas,
Map<Long, DramaEpisode> episodes,
Map<Long, AppUser> users) {
AdminReportRespVO vo = new AdminReportRespVO();
copyReport(report, vo);
fillReporter(report, users.get(report.getReporterId()), vo);
if (ReportService.TYPE_COMMENT.equals(report.getTargetType())) {
fillCommentTarget(report, comments.get(report.getTargetId()), dramas, episodes, vo);
} else if (ReportService.TYPE_DRAMA.equals(report.getTargetType())) {
fillDramaTarget(report, dramas.get(report.getTargetId()), vo);
} else if (ReportService.TYPE_USER.equals(report.getTargetType())) {
fillUserTarget(report, users.get(report.getTargetId()), vo);
}
return vo;
}
private void copyReport(ContentReport report, AdminReportRespVO vo) {
vo.setId(report.getId());
vo.setReporterId(report.getReporterId());
vo.setTargetType(report.getTargetType());
vo.setTargetId(report.getTargetId());
vo.setReasonCode(report.getReasonCode());
vo.setDescription(report.getDescription());
vo.setStatus(report.getStatus());
vo.setHandleBy(report.getHandleBy());
vo.setHandleAt(report.getHandleAt());
vo.setHandleNote(report.getHandleNote());
vo.setCreatedAt(report.getCreatedAt());
}
private void fillReporter(ContentReport report, AppUser reporter, AdminReportRespVO vo) {
if (reporter != null) {
vo.setReporterNickname(reporter.getNickname());
vo.setReporterUid(reporter.getUid());
}
}
private void fillCommentTarget(ContentReport report, ContentComment comment,
Map<Long, Drama> dramas,
Map<Long, DramaEpisode> episodes,
AdminReportRespVO vo) {
if (isDeleted(comment)) {
vo.setTargetSummary("评论 #" + report.getTargetId());
return;
}
Drama drama = dramas.get(comment.getDramaId());
if (drama == null) {
vo.setTargetSummary("评论 #" + report.getTargetId());
return;
}
DramaEpisode episode = episodes.get(comment.getEpisodeId());
vo.setDramaId(comment.getDramaId());
vo.setDramaTitle(drama.getTitle());
vo.setEpisodeId(comment.getEpisodeId());
vo.setCommentContent(comment.getContent());
if (episode == null) {
vo.setTargetSummary(drama.getTitle());
return;
}
vo.setEpisodeNo(episode.getEpNo());
vo.setEpisodeTitle(episode.getTitle());
String summary = drama.getTitle() + " · 第 " + episode.getEpNo() + " 集";
if (StringUtils.hasText(episode.getTitle())) {
summary += " · " + episode.getTitle();
}
vo.setTargetSummary(summary);
}
private void fillDramaTarget(ContentReport report, Drama drama, AdminReportRespVO vo) {
vo.setDramaId(report.getTargetId());
if (drama == null) {
vo.setTargetSummary("短剧 #" + report.getTargetId());
return;
}
vo.setDramaTitle(drama.getTitle());
vo.setTargetSummary(drama.getTitle());
}
private void fillUserTarget(ContentReport report, AppUser user, AdminReportRespVO vo) {
if (user == null) {
vo.setTargetSummary("用户 #" + report.getTargetId());
return;
}
vo.setTargetNickname(user.getNickname());
vo.setTargetUid(user.getUid());
vo.setTargetSummary(formatUser(user, report.getTargetId()));
}
private String formatUser(AppUser user, Long fallbackId) {
boolean hasNickname = StringUtils.hasText(user.getNickname());
boolean hasUid = StringUtils.hasText(user.getUid());
if (hasNickname && hasUid) {
return user.getNickname() + "(" + user.getUid() + ")";
}
if (hasNickname) {
return user.getNickname();
}
if (hasUid) {
return user.getUid();
}
return "用户 #" + fallbackId;
}
private boolean isDeleted(ContentComment comment) {
return comment == null
|| CommentService.STATUS_DELETED.equals(comment.getStatus())
|| Integer.valueOf(DELETED_FLAG).equals(comment.getDeleted());
}
private void addPositive(Set<Long> ids, Long id) {
if (id != null && id > 0L) {
ids.add(id);
}
}
}
@@ -0,0 +1,35 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.vo;
import lombok.Data;
import java.time.LocalDateTime;
/** 后台举报工单视图,附带举报人与被举报目标的展示信息。 */
@Data
public class AdminReportRespVO {
private Long id;
private Long reporterId;
private String reporterNickname;
private String reporterUid;
private String targetType;
private Long targetId;
private String targetSummary;
private Long dramaId;
private String dramaTitle;
private Long episodeId;
private Integer episodeNo;
private String episodeTitle;
private String commentContent;
private String targetNickname;
private String targetUid;
private String reasonCode;
private String description;
private String status;
private Long handleBy;
private LocalDateTime handleAt;
private String handleNote;
private LocalDateTime createdAt;
}
@@ -0,0 +1,209 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.service;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fruitshorts.admin.vo.AdminReportRespVO;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.content.dal.entity.Drama;
import com.fruitshorts.content.dal.entity.DramaEpisode;
import com.fruitshorts.content.dal.mapper.DramaEpisodeMapper;
import com.fruitshorts.content.dal.mapper.DramaMapper;
import com.fruitshorts.interaction.dal.entity.ContentComment;
import com.fruitshorts.interaction.dal.entity.ContentReport;
import com.fruitshorts.interaction.dal.mapper.ContentCommentMapper;
import com.fruitshorts.interaction.dal.mapper.ContentReportMapper;
import com.fruitshorts.interaction.service.ReportService;
import com.fruitshorts.interaction.vo.ReportQueryReqVO;
import com.fruitshorts.user.dal.entity.AppUser;
import com.fruitshorts.user.dal.mapper.AppUserMapper;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.time.LocalDateTime;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/** 后台举报工单目标摘要与批量装配。 */
class ReportAdminServiceTest {
private ContentReportMapper reportMapper;
private ContentCommentMapper commentMapper;
private DramaMapper dramaMapper;
private DramaEpisodeMapper episodeMapper;
private AppUserMapper appUserMapper;
private ReportAdminService service;
@BeforeAll
static void initTableInfoCache() {
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), ""),
ContentReport.class);
}
@BeforeEach
void setUp() {
reportMapper = mock(ContentReportMapper.class);
commentMapper = mock(ContentCommentMapper.class);
dramaMapper = mock(DramaMapper.class);
episodeMapper = mock(DramaEpisodeMapper.class);
appUserMapper = mock(AppUserMapper.class);
service = new ReportAdminService(reportMapper, commentMapper, dramaMapper,
episodeMapper, appUserMapper);
}
@Test
void should_build_summary_and_batch_relations_for_all_report_types() {
List<ContentReport> rows = List.of(
report(1L, 7L, ReportService.TYPE_COMMENT, 101L),
report(2L, 8L, ReportService.TYPE_DRAMA, 5L),
report(3L, 7L, ReportService.TYPE_USER, 9L));
stubPage(rows);
given(commentMapper.selectByIdsIncludingDeleted(any()))
.willReturn(List.of(comment(101L, 5L, 11L, "bad comment", 0)));
given(dramaMapper.selectBatchIds(any())).willReturn(List.of(drama(5L, "Signal")));
given(episodeMapper.selectBatchIds(any())).willReturn(List.of(episode(11L, 3, "The Call")));
given(appUserMapper.selectBatchIds(any())).willReturn(List.of(
user(7L, "Alice", "u-7"),
user(8L, "Bob", "u-8"),
user(9L, "Target", "u-9")));
PageResult<AdminReportRespVO> result = service.page(new ReportQueryReqVO());
AdminReportRespVO commentReport = result.getList().get(0);
assertThat(commentReport.getTargetSummary())
.isEqualTo("Signal · 第 3 集 · The Call");
assertThat(commentReport.getDramaTitle()).isEqualTo("Signal");
assertThat(commentReport.getEpisodeNo()).isEqualTo(3);
assertThat(commentReport.getCommentContent()).isEqualTo("bad comment");
assertThat(commentReport.getReporterNickname()).isEqualTo("Alice");
assertThat(commentReport.getReporterUid()).isEqualTo("u-7");
AdminReportRespVO dramaReport = result.getList().get(1);
assertThat(dramaReport.getTargetSummary()).isEqualTo("Signal");
assertThat(dramaReport.getDramaId()).isEqualTo(5L);
AdminReportRespVO userReport = result.getList().get(2);
assertThat(userReport.getTargetSummary()).isEqualTo("Target(u-9)");
assertThat(userReport.getTargetNickname()).isEqualTo("Target");
assertThat(userReport.getTargetUid()).isEqualTo("u-9");
verify(commentMapper, times(1)).selectByIdsIncludingDeleted(List.of(101L));
verify(dramaMapper, times(1)).selectBatchIds(any());
verify(episodeMapper, times(1)).selectBatchIds(any());
verify(appUserMapper, times(1)).selectBatchIds(any());
}
@Test
void should_leave_all_filters_unset_when_status_and_filters_are_empty() {
stubPage(List.of());
PageResult<AdminReportRespVO> result = service.page(new ReportQueryReqVO());
assertThat(result.getTotal()).isZero();
assertThat(result.getList()).isEmpty();
ArgumentCaptor<LambdaQueryWrapper<ContentReport>> captor = queryCaptor();
verify(reportMapper).selectPage(any(), captor.capture());
assertThat(captor.getValue().getSqlSegment())
.doesNotContain("status")
.doesNotContain("target_type")
.doesNotContain("reason_code")
.doesNotContain("reporter_id");
}
@Test
void should_fallback_to_ids_when_targets_are_deleted_or_missing() {
List<ContentReport> rows = List.of(
report(1L, 7L, ReportService.TYPE_COMMENT, 101L),
report(2L, 7L, ReportService.TYPE_DRAMA, 5L),
report(3L, 7L, ReportService.TYPE_USER, 9L));
stubPage(rows);
given(commentMapper.selectByIdsIncludingDeleted(any()))
.willReturn(List.of(comment(101L, 5L, 11L, "removed", 1)));
given(dramaMapper.selectBatchIds(any())).willReturn(List.of());
given(appUserMapper.selectBatchIds(any()))
.willReturn(List.of(user(7L, "Alice", "u-7")));
PageResult<AdminReportRespVO> result = service.page(new ReportQueryReqVO());
assertThat(result.getList()).extracting(AdminReportRespVO::getTargetSummary)
.containsExactly("评论 #101", "短剧 #5", "用户 #9");
assertThat(result.getList().get(0).getCommentContent()).isNull();
verify(episodeMapper, never()).selectBatchIds(any());
}
private void stubPage(List<ContentReport> rows) {
Page<ContentReport> page = new Page<>(1, 10);
page.setTotal(rows.size());
page.setRecords(rows);
given(reportMapper.selectPage(any(Page.class), any())).willReturn(page);
}
@SuppressWarnings("unchecked")
private ArgumentCaptor<LambdaQueryWrapper<ContentReport>> queryCaptor() {
return ArgumentCaptor.forClass(LambdaQueryWrapper.class);
}
private ContentReport report(Long id, Long reporterId, String type, Long targetId) {
ContentReport report = new ContentReport();
report.setId(id);
report.setReporterId(reporterId);
report.setTargetType(type);
report.setTargetId(targetId);
report.setReasonCode("VIOLATION");
report.setDescription("description");
report.setStatus("pending");
report.setHandleBy(0L);
report.setHandleNote("");
report.setCreatedAt(LocalDateTime.of(2026, 10, 21, 12, 0));
return report;
}
private ContentComment comment(Long id, Long dramaId, Long episodeId, String content,
int deleted) {
ContentComment comment = new ContentComment();
comment.setId(id);
comment.setDramaId(dramaId);
comment.setEpisodeId(episodeId);
comment.setUserId(9L);
comment.setContent(content);
comment.setStatus(deleted == 1 ? "deleted" : "published");
comment.setDeleted(deleted);
return comment;
}
private Drama drama(Long id, String title) {
Drama drama = new Drama();
drama.setId(id);
drama.setTitle(title);
return drama;
}
private DramaEpisode episode(Long id, int no, String title) {
DramaEpisode episode = new DramaEpisode();
episode.setId(id);
episode.setEpNo(no);
episode.setTitle(title);
return episode;
}
private AppUser user(Long id, String nickname, String uid) {
AppUser user = new AppUser();
user.setId(id);
user.setNickname(nickname);
user.setUid(uid);
return user;
}
}