feat(admin): 新增观看记录分页与评论分集信息
This commit is contained in:
1 parent
d52b805adf
commit
1d316791b1
14 files changed
+953
-7
No files matched your search
+2
-2
@@ -35,8 +35,8 @@ public class CommentAdminController {
|
||||
|
||||
private final CommentService commentService;
|
||||
|
||||
/** 检索评论:默认按状态过滤,`status=pending` 即审核队列。 */
|
||||
@Operation(summary = "评论检索(默认按状态过滤,待审队列)")
|
||||
/** 检索评论:status 为空时返回全部状态,传 pending 即审核队列。 */
|
||||
@Operation(summary = "评论检索(status 为空返回全部状态)")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('comment:list')")
|
||||
public Result<PageResult<CommentRespVO>> page(CommentQueryReqVO req) {
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.controller;
|
||||
|
||||
import com.fruitshorts.admin.service.WatchHistoryAdminService;
|
||||
import com.fruitshorts.admin.vo.AdminWatchHistoryVO;
|
||||
import com.fruitshorts.admin.vo.WatchHistoryQueryReqVO;
|
||||
import com.fruitshorts.common.pojo.PageResult;
|
||||
import com.fruitshorts.common.pojo.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 后台跨用户观看记录检索。 */
|
||||
@Tag(name = "后台-观看记录")
|
||||
@RestController
|
||||
@RequestMapping("/admin/watch-history")
|
||||
@RequiredArgsConstructor
|
||||
public class WatchHistoryAdminController {
|
||||
|
||||
private final WatchHistoryAdminService watchHistoryAdminService;
|
||||
|
||||
@Operation(summary = "观看记录分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('watch-history:list')")
|
||||
public Result<PageResult<AdminWatchHistoryVO>> page(WatchHistoryQueryReqVO req) {
|
||||
return Result.ok(watchHistoryAdminService.page(req));
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
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.AdminWatchHistoryVO;
|
||||
import com.fruitshorts.admin.vo.WatchHistoryQueryReqVO;
|
||||
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.content.enums.ContentStatus;
|
||||
import com.fruitshorts.playback.dal.entity.WatchHistory;
|
||||
import com.fruitshorts.playback.dal.mapper.WatchHistoryMapper;
|
||||
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.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 后台观看记录分页与跨域装配。
|
||||
*
|
||||
* <p>观看流水是播放域的原始事件,用户、短剧与分集资料分别位于用户和内容域。
|
||||
* 该服务位于 admin-api 聚合层,直接注入只读 Mapper,避免反向扩展业务模块依赖。
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WatchHistoryAdminService {
|
||||
|
||||
private static final int DEFAULT_PAGE_INDEX = 1;
|
||||
private static final int DEFAULT_PAGE_SIZE = 10;
|
||||
private static final int MAX_PAGE_SIZE = 100;
|
||||
private static final long FINISHED_TOLERANCE_MS = 1000L;
|
||||
|
||||
private final WatchHistoryMapper watchHistoryMapper;
|
||||
private final AppUserMapper appUserMapper;
|
||||
private final DramaMapper dramaMapper;
|
||||
private final DramaEpisodeMapper dramaEpisodeMapper;
|
||||
|
||||
/** 按后台筛选条件分页查询观看记录。 */
|
||||
public PageResult<AdminWatchHistoryVO> page(WatchHistoryQueryReqVO req) {
|
||||
int pageIndex = req.getPageIndex() == null || req.getPageIndex() < 1
|
||||
? DEFAULT_PAGE_INDEX : req.getPageIndex();
|
||||
int pageSize = req.getPageSize() == null || req.getPageSize() < 1
|
||||
? DEFAULT_PAGE_SIZE : Math.min(req.getPageSize(), MAX_PAGE_SIZE);
|
||||
List<Long> matchedUserIds = StringUtils.hasText(req.getKeyword())
|
||||
? searchUserIds(req.getKeyword()) : null;
|
||||
if (matchedUserIds != null && matchedUserIds.isEmpty()) {
|
||||
return emptyPage();
|
||||
}
|
||||
List<Long> matchedDramaIds = StringUtils.hasText(req.getDramaTitle())
|
||||
? searchDramaIds(req.getDramaTitle()) : null;
|
||||
if (matchedDramaIds != null && matchedDramaIds.isEmpty()) {
|
||||
return emptyPage();
|
||||
}
|
||||
IPage<WatchHistory> result = watchHistoryMapper.selectPage(new Page<>(pageIndex, pageSize),
|
||||
buildQuery(req, matchedUserIds, matchedDramaIds));
|
||||
return new PageResult<>(result.getTotal(), toVoList(result.getRecords()));
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<WatchHistory> buildQuery(WatchHistoryQueryReqVO req,
|
||||
List<Long> matchedUserIds,
|
||||
List<Long> matchedDramaIds) {
|
||||
LambdaQueryWrapper<WatchHistory> wrapper = Wrappers.<WatchHistory>lambdaQuery()
|
||||
.eq(req.getUserId() != null, WatchHistory::getUserId, req.getUserId())
|
||||
.in(matchedUserIds != null, WatchHistory::getUserId, matchedUserIds)
|
||||
.eq(req.getDramaId() != null, WatchHistory::getDramaId, req.getDramaId())
|
||||
.in(matchedDramaIds != null, WatchHistory::getDramaId, matchedDramaIds)
|
||||
.eq(req.getEpisodeId() != null, WatchHistory::getEpisodeId, req.getEpisodeId())
|
||||
.eq(StringUtils.hasText(req.getSource()), WatchHistory::getSource, req.getSource())
|
||||
.ge(req.getStartTime() != null, WatchHistory::getWatchedAt, req.getStartTime())
|
||||
.le(req.getEndTime() != null, WatchHistory::getWatchedAt, req.getEndTime());
|
||||
applyFinished(wrapper, req.getFinished());
|
||||
return wrapper.orderByDesc(WatchHistory::getWatchedAt)
|
||||
.orderByDesc(WatchHistory::getId);
|
||||
}
|
||||
|
||||
private void applyFinished(LambdaQueryWrapper<WatchHistory> wrapper, Boolean finished) {
|
||||
if (Boolean.TRUE.equals(finished)) {
|
||||
wrapper.gt(WatchHistory::getDurationMs, 0L)
|
||||
.apply("position_ms + {0} >= duration_ms", FINISHED_TOLERANCE_MS);
|
||||
} else if (Boolean.FALSE.equals(finished)) {
|
||||
wrapper.and(query -> query.eq(WatchHistory::getDurationMs, 0L)
|
||||
.or()
|
||||
.apply("position_ms + {0} < duration_ms", FINISHED_TOLERANCE_MS));
|
||||
}
|
||||
}
|
||||
|
||||
private List<Long> searchUserIds(String keyword) {
|
||||
String value = keyword.trim();
|
||||
return appUserMapper.selectList(Wrappers.<AppUser>lambdaQuery()
|
||||
.select(AppUser::getId)
|
||||
.and(query -> query.like(AppUser::getUid, value)
|
||||
.or()
|
||||
.like(AppUser::getNickname, value)))
|
||||
.stream()
|
||||
.map(AppUser::getId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<Long> searchDramaIds(String dramaTitle) {
|
||||
String value = dramaTitle.trim();
|
||||
return dramaMapper.selectList(Wrappers.<Drama>lambdaQuery()
|
||||
.select(Drama::getId)
|
||||
.like(Drama::getTitle, value))
|
||||
.stream()
|
||||
.map(Drama::getId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<AdminWatchHistoryVO> toVoList(List<WatchHistory> rows) {
|
||||
if (rows.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<Long, AppUser> users = loadUsers(rows);
|
||||
Map<Long, Drama> dramas = loadDramas(rows);
|
||||
Map<Long, DramaEpisode> episodes = loadEpisodes(rows);
|
||||
List<AdminWatchHistoryVO> list = new ArrayList<>(rows.size());
|
||||
for (WatchHistory row : rows) {
|
||||
list.add(toVo(row, users.get(row.getUserId()), dramas.get(row.getDramaId()),
|
||||
episodes.get(row.getEpisodeId())));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private Map<Long, AppUser> loadUsers(List<WatchHistory> rows) {
|
||||
Map<Long, AppUser> result = new HashMap<>();
|
||||
List<Long> ids = rows.stream().map(WatchHistory::getUserId).distinct().toList();
|
||||
for (AppUser user : appUserMapper.selectBatchIds(ids)) {
|
||||
result.put(user.getId(), user);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<Long, Drama> loadDramas(List<WatchHistory> rows) {
|
||||
Map<Long, Drama> result = new HashMap<>();
|
||||
List<Long> ids = rows.stream().map(WatchHistory::getDramaId).distinct().toList();
|
||||
for (Drama drama : dramaMapper.selectBatchIds(ids)) {
|
||||
result.put(drama.getId(), drama);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<Long, DramaEpisode> loadEpisodes(List<WatchHistory> rows) {
|
||||
Map<Long, DramaEpisode> result = new HashMap<>();
|
||||
List<Long> ids = rows.stream().map(WatchHistory::getEpisodeId).distinct().toList();
|
||||
for (DramaEpisode episode : dramaEpisodeMapper.selectBatchIds(ids)) {
|
||||
result.put(episode.getId(), episode);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private AdminWatchHistoryVO toVo(WatchHistory row, AppUser user, Drama drama,
|
||||
DramaEpisode episode) {
|
||||
AdminWatchHistoryVO vo = new AdminWatchHistoryVO();
|
||||
vo.setId(row.getId());
|
||||
vo.setUserId(row.getUserId());
|
||||
vo.setDramaId(row.getDramaId());
|
||||
vo.setEpisodeId(row.getEpisodeId());
|
||||
vo.setPositionMs(row.getPositionMs());
|
||||
vo.setDurationMs(row.getDurationMs());
|
||||
vo.setSource(row.getSource());
|
||||
vo.setDeviceId(row.getDeviceId());
|
||||
vo.setWatchedAt(row.getWatchedAt());
|
||||
long position = row.getPositionMs() == null ? 0L : row.getPositionMs();
|
||||
long duration = row.getDurationMs() == null ? 0L : row.getDurationMs();
|
||||
vo.setFinished(duration > 0L && position + FINISHED_TOLERANCE_MS >= duration);
|
||||
if (user != null) {
|
||||
vo.setUid(user.getUid());
|
||||
vo.setNickname(user.getNickname());
|
||||
}
|
||||
if (drama != null) {
|
||||
vo.setDramaTitle(drama.getTitle());
|
||||
vo.setDramaPublished(ContentStatus.PUBLISHED.getCode().equals(drama.getStatus()));
|
||||
} else {
|
||||
vo.setDramaPublished(false);
|
||||
}
|
||||
if (episode != null) {
|
||||
vo.setEpisodeNo(episode.getEpNo());
|
||||
vo.setEpisodeTitle(episode.getTitle());
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
private PageResult<AdminWatchHistoryVO> emptyPage() {
|
||||
return new PageResult<>(0L, List.of());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 后台观看记录行。
|
||||
*
|
||||
* <p>页面需要识别用户、短剧、分集与完播状态,因此由 admin-api 聚合流水与基础资料。
|
||||
*/
|
||||
@Data
|
||||
public class AdminWatchHistoryVO {
|
||||
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private String uid;
|
||||
private String nickname;
|
||||
private Long dramaId;
|
||||
private String dramaTitle;
|
||||
private Boolean dramaPublished;
|
||||
private Long episodeId;
|
||||
private Integer episodeNo;
|
||||
private String episodeTitle;
|
||||
private Long positionMs;
|
||||
private Long durationMs;
|
||||
private Boolean finished;
|
||||
private String source;
|
||||
private String deviceId;
|
||||
private LocalDateTime watchedAt;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 后台观看记录分页检索条件。 */
|
||||
@Data
|
||||
public class WatchHistoryQueryReqVO {
|
||||
|
||||
private Integer pageIndex = 1;
|
||||
private Integer pageSize = 10;
|
||||
/** uid / 昵称模糊检索 */
|
||||
private String keyword;
|
||||
private Long userId;
|
||||
/** 短剧名称模糊检索 */
|
||||
private String dramaTitle;
|
||||
private Long dramaId;
|
||||
private Long episodeId;
|
||||
private String source;
|
||||
private Boolean finished;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fruitshorts.common.pojo.PageResult;
|
||||
import com.fruitshorts.content.dal.entity.Drama;
|
||||
import com.fruitshorts.content.dal.entity.DramaEpisode;
|
||||
import com.fruitshorts.content.service.DramaService;
|
||||
import com.fruitshorts.content.service.EpisodeService;
|
||||
import com.fruitshorts.interaction.config.InteractionProperties;
|
||||
import com.fruitshorts.interaction.dal.entity.ContentComment;
|
||||
import com.fruitshorts.interaction.dal.mapper.ContentAuditRecordMapper;
|
||||
import com.fruitshorts.interaction.dal.mapper.ContentCommentMapper;
|
||||
import com.fruitshorts.interaction.dal.mapper.ContentLikeMapper;
|
||||
import com.fruitshorts.interaction.service.AuditRecorder;
|
||||
import com.fruitshorts.interaction.service.CommentService;
|
||||
import com.fruitshorts.interaction.service.SensitiveWordService;
|
||||
import com.fruitshorts.interaction.vo.CommentQueryReqVO;
|
||||
import com.fruitshorts.interaction.vo.CommentRespVO;
|
||||
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 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.verify;
|
||||
|
||||
/** 后台评论页批量补充短剧与分集展示字段。 */
|
||||
class CommentServiceTest {
|
||||
|
||||
private ContentCommentMapper commentMapper;
|
||||
private AppUserMapper appUserMapper;
|
||||
private DramaService dramaService;
|
||||
private EpisodeService episodeService;
|
||||
private CommentService commentService;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfoCache() {
|
||||
TableInfoHelper.initTableInfo(
|
||||
new MapperBuilderAssistant(new MybatisConfiguration(), ""),
|
||||
ContentComment.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
commentMapper = mock(ContentCommentMapper.class);
|
||||
appUserMapper = mock(AppUserMapper.class);
|
||||
dramaService = mock(DramaService.class);
|
||||
episodeService = mock(EpisodeService.class);
|
||||
commentService = new CommentService(
|
||||
commentMapper,
|
||||
mock(ContentLikeMapper.class),
|
||||
mock(ContentAuditRecordMapper.class),
|
||||
appUserMapper,
|
||||
dramaService,
|
||||
episodeService,
|
||||
mock(SensitiveWordService.class),
|
||||
mock(AuditRecorder.class),
|
||||
mock(InteractionProperties.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_fill_drama_and_episode_metadata_when_admin_page_is_loaded() {
|
||||
ContentComment withEpisode = comment(1L, 5L, 11L);
|
||||
ContentComment legacy = comment(2L, 5L, 0L);
|
||||
Page<ContentComment> page = new Page<>(1, 10);
|
||||
page.setTotal(2L);
|
||||
page.setRecords(List.of(withEpisode, legacy));
|
||||
given(commentMapper.selectPage(any(), any())).willReturn(page);
|
||||
AppUser user = new AppUser();
|
||||
user.setId(7L);
|
||||
user.setUid("uid-7");
|
||||
user.setNickname("Viewer");
|
||||
given(appUserMapper.selectBatchIds(any())).willReturn(List.of(user));
|
||||
Drama drama = new Drama();
|
||||
drama.setId(5L);
|
||||
drama.setTitle("Signal");
|
||||
given(dramaService.listByIds(any())).willReturn(List.of(drama));
|
||||
DramaEpisode episode = new DramaEpisode();
|
||||
episode.setId(11L);
|
||||
episode.setEpNo(3);
|
||||
episode.setTitle("The Call");
|
||||
given(episodeService.listByIds(any())).willReturn(List.of(episode));
|
||||
|
||||
PageResult<CommentRespVO> result = commentService.adminPage(new CommentQueryReqVO());
|
||||
|
||||
assertThat(result.getList()).hasSize(2);
|
||||
assertThat(result.getList().get(0).getDramaTitle()).isEqualTo("Signal");
|
||||
assertThat(result.getList().get(0).getEpisodeNo()).isEqualTo(3);
|
||||
assertThat(result.getList().get(0).getEpisodeTitle()).isEqualTo("The Call");
|
||||
assertThat(result.getList().get(1).getDramaTitle()).isEqualTo("Signal");
|
||||
assertThat(result.getList().get(1).getEpisodeNo()).isNull();
|
||||
verify(dramaService).listByIds(List.of(5L));
|
||||
verify(episodeService).listByIds(List.of(11L));
|
||||
}
|
||||
|
||||
private ContentComment comment(Long id, Long dramaId, Long episodeId) {
|
||||
ContentComment comment = new ContentComment();
|
||||
comment.setId(id);
|
||||
comment.setDramaId(dramaId);
|
||||
comment.setEpisodeId(episodeId);
|
||||
comment.setParentId(0L);
|
||||
comment.setRootId(0L);
|
||||
comment.setUserId(7L);
|
||||
comment.setContent("comment");
|
||||
comment.setStatus("published");
|
||||
comment.setAuditReason("");
|
||||
comment.setLikeCount(0);
|
||||
comment.setReplyCount(0);
|
||||
comment.setReportCount(0);
|
||||
return comment;
|
||||
}
|
||||
}
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
// 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.AdminWatchHistoryVO;
|
||||
import com.fruitshorts.admin.vo.WatchHistoryQueryReqVO;
|
||||
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.playback.dal.entity.WatchHistory;
|
||||
import com.fruitshorts.playback.dal.mapper.WatchHistoryMapper;
|
||||
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.verify;
|
||||
|
||||
/** 后台观看记录分页条件与批量装配。 */
|
||||
class WatchHistoryAdminServiceTest {
|
||||
|
||||
private WatchHistoryMapper watchHistoryMapper;
|
||||
private AppUserMapper appUserMapper;
|
||||
private DramaMapper dramaMapper;
|
||||
private DramaEpisodeMapper dramaEpisodeMapper;
|
||||
private WatchHistoryAdminService service;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfoCache() {
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
for (Class<?> type : List.of(AppUser.class, Drama.class, DramaEpisode.class,
|
||||
WatchHistory.class)) {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), type);
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
watchHistoryMapper = mock(WatchHistoryMapper.class);
|
||||
appUserMapper = mock(AppUserMapper.class);
|
||||
dramaMapper = mock(DramaMapper.class);
|
||||
dramaEpisodeMapper = mock(DramaEpisodeMapper.class);
|
||||
service = new WatchHistoryAdminService(watchHistoryMapper, appUserMapper, dramaMapper,
|
||||
dramaEpisodeMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_use_default_page_and_newest_first_when_no_filters_are_given() {
|
||||
WatchHistory row = history(100L, 7L, 5L, 11L);
|
||||
stubPage(row);
|
||||
stubBatchData();
|
||||
|
||||
PageResult<AdminWatchHistoryVO> result = service.page(new WatchHistoryQueryReqVO());
|
||||
|
||||
ArgumentCaptor<Page<WatchHistory>> pageCaptor = pageCaptor();
|
||||
ArgumentCaptor<LambdaQueryWrapper<WatchHistory>> queryCaptor = queryCaptor();
|
||||
verify(watchHistoryMapper).selectPage(pageCaptor.capture(), queryCaptor.capture());
|
||||
assertThat(pageCaptor.getValue().getCurrent()).isEqualTo(1);
|
||||
assertThat(pageCaptor.getValue().getSize()).isEqualTo(10);
|
||||
assertThat(queryCaptor.getValue().getSqlSegment())
|
||||
.contains("watched_at DESC")
|
||||
.contains("id DESC");
|
||||
assertThat(result.getTotal()).isEqualTo(1L);
|
||||
assertThat(result.getList().get(0).getUid()).isEqualTo("user-7");
|
||||
assertThat(result.getList().get(0).getDramaTitle()).isEqualTo("Signal");
|
||||
assertThat(result.getList().get(0).getEpisodeNo()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_filter_by_matched_user_ids_when_keyword_is_present() {
|
||||
WatchHistory row = history(100L, 7L, 5L, 11L);
|
||||
given(appUserMapper.selectList(any())).willReturn(List.of(user(7L, "Alice")));
|
||||
stubPage(row);
|
||||
stubBatchData();
|
||||
WatchHistoryQueryReqVO req = new WatchHistoryQueryReqVO();
|
||||
req.setKeyword("Ali");
|
||||
|
||||
service.page(req);
|
||||
|
||||
LambdaQueryWrapper<WatchHistory> query = capturedQuery();
|
||||
assertThat(query.getSqlSegment()).contains("user_id IN");
|
||||
assertThat(query.getParamNameValuePairs().containsValue(7L)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_filter_by_matched_drama_ids_when_title_is_present() {
|
||||
WatchHistory row = history(100L, 7L, 5L, 11L);
|
||||
given(dramaMapper.selectList(any())).willReturn(List.of(drama(5L, "Signal")));
|
||||
stubPage(row);
|
||||
stubBatchData();
|
||||
WatchHistoryQueryReqVO req = new WatchHistoryQueryReqVO();
|
||||
req.setDramaTitle("Sig");
|
||||
|
||||
service.page(req);
|
||||
|
||||
LambdaQueryWrapper<WatchHistory> query = capturedQuery();
|
||||
assertThat(query.getSqlSegment()).contains("drama_id IN");
|
||||
assertThat(query.getParamNameValuePairs().containsValue(5L)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_empty_page_without_querying_history_when_keyword_has_no_user() {
|
||||
given(appUserMapper.selectList(any())).willReturn(List.of());
|
||||
WatchHistoryQueryReqVO req = new WatchHistoryQueryReqVO();
|
||||
req.setKeyword("missing");
|
||||
|
||||
PageResult<AdminWatchHistoryVO> result = service.page(req);
|
||||
|
||||
assertThat(result.getTotal()).isZero();
|
||||
assertThat(result.getList()).isEmpty();
|
||||
verify(watchHistoryMapper, never()).selectPage(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_apply_true_finished_condition_in_sql() {
|
||||
WatchHistory row = history(100L, 7L, 5L, 11L);
|
||||
row.setPositionMs(59000L);
|
||||
row.setDurationMs(60000L);
|
||||
stubPage(row);
|
||||
stubBatchData();
|
||||
WatchHistoryQueryReqVO req = new WatchHistoryQueryReqVO();
|
||||
req.setFinished(true);
|
||||
|
||||
PageResult<AdminWatchHistoryVO> result = service.page(req);
|
||||
|
||||
LambdaQueryWrapper<WatchHistory> query = capturedQuery();
|
||||
assertThat(query.getSqlSegment())
|
||||
.contains("duration_ms >")
|
||||
.contains("position_ms +");
|
||||
assertThat(query.getParamNameValuePairs().containsValue(1000L)).isTrue();
|
||||
assertThat(result.getList().get(0).getFinished()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_apply_false_finished_condition_in_sql() {
|
||||
WatchHistory row = history(100L, 7L, 5L, 11L);
|
||||
row.setPositionMs(1000L);
|
||||
row.setDurationMs(60000L);
|
||||
stubPage(row);
|
||||
stubBatchData();
|
||||
WatchHistoryQueryReqVO req = new WatchHistoryQueryReqVO();
|
||||
req.setFinished(false);
|
||||
|
||||
PageResult<AdminWatchHistoryVO> result = service.page(req);
|
||||
|
||||
LambdaQueryWrapper<WatchHistory> query = capturedQuery();
|
||||
assertThat(query.getSqlSegment())
|
||||
.contains("duration_ms =")
|
||||
.contains("OR")
|
||||
.contains("position_ms +");
|
||||
assertThat(result.getList().get(0).getFinished()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_apply_closed_time_range_and_clamp_page_size() {
|
||||
WatchHistory row = history(100L, 7L, 5L, 11L);
|
||||
stubPage(row);
|
||||
stubBatchData();
|
||||
LocalDateTime start = LocalDateTime.of(2026, 10, 1, 0, 0);
|
||||
LocalDateTime end = LocalDateTime.of(2026, 10, 21, 23, 59, 59);
|
||||
WatchHistoryQueryReqVO req = new WatchHistoryQueryReqVO();
|
||||
req.setPageSize(500);
|
||||
req.setStartTime(start);
|
||||
req.setEndTime(end);
|
||||
|
||||
service.page(req);
|
||||
|
||||
ArgumentCaptor<Page<WatchHistory>> pageCaptor = pageCaptor();
|
||||
ArgumentCaptor<LambdaQueryWrapper<WatchHistory>> queryCaptor = queryCaptor();
|
||||
verify(watchHistoryMapper).selectPage(pageCaptor.capture(), queryCaptor.capture());
|
||||
assertThat(pageCaptor.getValue().getSize()).isEqualTo(100);
|
||||
assertThat(queryCaptor.getValue().getSqlSegment())
|
||||
.contains("watched_at >=")
|
||||
.contains("watched_at <=");
|
||||
}
|
||||
|
||||
private void stubPage(WatchHistory row) {
|
||||
Page<WatchHistory> page = new Page<>(1, 10);
|
||||
page.setTotal(1L);
|
||||
page.setRecords(List.of(row));
|
||||
given(watchHistoryMapper.selectPage(any(Page.class), any())).willReturn(page);
|
||||
}
|
||||
|
||||
private void stubBatchData() {
|
||||
given(appUserMapper.selectBatchIds(any())).willReturn(List.of(user(7L, "Alice")));
|
||||
given(dramaMapper.selectBatchIds(any())).willReturn(List.of(drama(5L, "Signal")));
|
||||
given(dramaEpisodeMapper.selectBatchIds(any())).willReturn(List.of(episode(11L)));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ArgumentCaptor<Page<WatchHistory>> pageCaptor() {
|
||||
return ArgumentCaptor.forClass(Page.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ArgumentCaptor<LambdaQueryWrapper<WatchHistory>> queryCaptor() {
|
||||
return ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<WatchHistory> capturedQuery() {
|
||||
ArgumentCaptor<LambdaQueryWrapper<WatchHistory>> captor = queryCaptor();
|
||||
verify(watchHistoryMapper).selectPage(any(), captor.capture());
|
||||
return captor.getValue();
|
||||
}
|
||||
|
||||
private WatchHistory history(Long id, Long userId, Long dramaId, Long episodeId) {
|
||||
WatchHistory row = new WatchHistory();
|
||||
row.setId(id);
|
||||
row.setUserId(userId);
|
||||
row.setDramaId(dramaId);
|
||||
row.setEpisodeId(episodeId);
|
||||
row.setPositionMs(1000L);
|
||||
row.setDurationMs(60000L);
|
||||
row.setSource("HOME");
|
||||
row.setDeviceId("device-1");
|
||||
row.setWatchedAt(LocalDateTime.of(2026, 10, 21, 12, 0));
|
||||
return row;
|
||||
}
|
||||
|
||||
private AppUser user(Long id, String nickname) {
|
||||
AppUser user = new AppUser();
|
||||
user.setId(id);
|
||||
user.setUid("user-" + id);
|
||||
user.setNickname(nickname);
|
||||
return user;
|
||||
}
|
||||
|
||||
private Drama drama(Long id, String title) {
|
||||
Drama drama = new Drama();
|
||||
drama.setId(id);
|
||||
drama.setTitle(title);
|
||||
drama.setStatus("published");
|
||||
return drama;
|
||||
}
|
||||
|
||||
private DramaEpisode episode(Long id) {
|
||||
DramaEpisode episode = new DramaEpisode();
|
||||
episode.setId(id);
|
||||
episode.setEpNo(1);
|
||||
episode.setTitle("Episode 1");
|
||||
return episode;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user