feat(interaction): 新增分集计数统计接口
This commit is contained in:
1 parent
15b540cdd6
commit
2fe24b9db7
10 files changed
+299
-2
No files matched your search
+10
@@ -3,7 +3,9 @@
|
||||
package com.fruitshorts.app.controller;
|
||||
|
||||
import com.fruitshorts.app.service.AppCatalogService;
|
||||
import com.fruitshorts.app.service.AppEpisodeStatsService;
|
||||
import com.fruitshorts.app.vo.AppDramaDetailVO;
|
||||
import com.fruitshorts.app.vo.AppEpisodeStatsVO;
|
||||
import com.fruitshorts.common.pojo.Result;
|
||||
import com.fruitshorts.content.dal.entity.DramaEpisode;
|
||||
import com.fruitshorts.content.service.EpisodeService;
|
||||
@@ -34,6 +36,7 @@ public class AppDramaController {
|
||||
private final PlaybackService playbackService;
|
||||
private final EpisodeService episodeService;
|
||||
private final CommentService commentService;
|
||||
private final AppEpisodeStatsService appEpisodeStatsService;
|
||||
|
||||
/** 详情:包含剧集列表,客户端进详情页一次请求即可渲染 */
|
||||
@Operation(summary = "短剧详情(含剧集列表,进详情页一次请求即可渲染)")
|
||||
@@ -59,4 +62,11 @@ public class AppDramaController {
|
||||
vo.setCommentCount(commentService.countByEpisodes(List.of(id)).getOrDefault(id, 0L));
|
||||
return Result.ok(vo);
|
||||
}
|
||||
|
||||
/** 轻量计数快照:只读互动状态,不签发播放地址、不写票据缓存。 */
|
||||
@Operation(summary = "播放页轻量计数与当前用户互动状态")
|
||||
@GetMapping("/episodes/{id}/stats")
|
||||
public Result<AppEpisodeStatsVO> stats(@PathVariable Long id) {
|
||||
return Result.ok(appEpisodeStatsService.get(id, SecurityUtils.getUserIdOrNull()));
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.app.service;
|
||||
|
||||
import com.fruitshorts.app.vo.AppEpisodeStatsVO;
|
||||
import com.fruitshorts.common.exception.ErrorCode;
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
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.service.CommentService;
|
||||
import com.fruitshorts.interaction.service.LikeService;
|
||||
import com.fruitshorts.playback.service.FavoriteService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** 播放页只读计数快照;固定维度批量查询,不签发播放地址或票据。 */
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AppEpisodeStatsService {
|
||||
|
||||
private final EpisodeService episodeService;
|
||||
private final DramaService dramaService;
|
||||
private final CommentService commentService;
|
||||
private final LikeService likeService;
|
||||
private final FavoriteService favoriteService;
|
||||
|
||||
public AppEpisodeStatsVO get(Long episodeId, Long userId) {
|
||||
Map<Long, AppEpisodeStatsVO> stats = batch(List.of(episodeId), userId);
|
||||
AppEpisodeStatsVO result = stats.get(episodeId);
|
||||
if (result == null) {
|
||||
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "剧集不存在或未上线");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 每类数据只查一次,供后续多集统计复用。 */
|
||||
public Map<Long, AppEpisodeStatsVO> batch(Collection<Long> episodeIds, Long userId) {
|
||||
if (episodeIds == null || episodeIds.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
List<DramaEpisode> episodes = episodeService.listPublishedByIds(episodeIds);
|
||||
if (episodes.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
List<Long> normalizedIds = episodes.stream().map(DramaEpisode::getId).toList();
|
||||
List<Long> dramaIds = episodes.stream().map(DramaEpisode::getDramaId).distinct().toList();
|
||||
Map<Long, Long> commentCounts = commentService.countByEpisodes(normalizedIds);
|
||||
Map<Long, Drama> dramas = new HashMap<>();
|
||||
for (Drama drama : dramaService.listPublishedByIds(dramaIds)) {
|
||||
dramas.put(drama.getId(), drama);
|
||||
}
|
||||
Set<Long> likedIds = likeService.likedTargetIds(
|
||||
userId, LikeService.TYPE_EPISODE, normalizedIds);
|
||||
Set<Long> favoriteDramaIds = favoriteService.favoriteDramaIds(userId, dramaIds);
|
||||
|
||||
Map<Long, AppEpisodeStatsVO> result = new HashMap<>();
|
||||
for (DramaEpisode episode : episodes) {
|
||||
Drama drama = dramas.get(episode.getDramaId());
|
||||
if (drama == null) {
|
||||
continue;
|
||||
}
|
||||
AppEpisodeStatsVO vo = new AppEpisodeStatsVO();
|
||||
vo.setEpisodeId(episode.getId());
|
||||
vo.setDramaId(episode.getDramaId());
|
||||
vo.setLikeCount(episode.getLikeCount() == null ? 0L : episode.getLikeCount());
|
||||
vo.setCommentCount(commentCounts.getOrDefault(episode.getId(), 0L));
|
||||
vo.setFavoriteCount(drama.getFavoriteCount() == null ? 0L : drama.getFavoriteCount());
|
||||
vo.setLiked(likedIds.contains(episode.getId()));
|
||||
vo.setFavorited(favoriteDramaIds.contains(episode.getDramaId()));
|
||||
result.put(episode.getId(), vo);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.app.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** 播放页轻量计数快照,不触发票据签发。 */
|
||||
@Data
|
||||
public class AppEpisodeStatsVO {
|
||||
|
||||
private Long episodeId;
|
||||
private Long dramaId;
|
||||
private Long likeCount;
|
||||
private Long commentCount;
|
||||
private Long favoriteCount;
|
||||
private Boolean liked;
|
||||
private Boolean favorited;
|
||||
}
|
||||
+3
-1
@@ -2,6 +2,7 @@
|
||||
package com.fruitshorts.app.controller;
|
||||
|
||||
import com.fruitshorts.app.service.AppCatalogService;
|
||||
import com.fruitshorts.app.service.AppEpisodeStatsService;
|
||||
import com.fruitshorts.app.support.ControllerContractSupport;
|
||||
import com.fruitshorts.app.vo.AppBannerVO;
|
||||
import com.fruitshorts.app.vo.AppCategoryVO;
|
||||
@@ -49,7 +50,8 @@ class AppCatalogControllerContractTest {
|
||||
catalogMvc = ControllerContractSupport.mockMvcOf(new AppCatalogController(catalogService));
|
||||
dramaMvc = ControllerContractSupport.mockMvcOf(
|
||||
new AppDramaController(catalogService, mock(PlaybackService.class),
|
||||
mock(EpisodeService.class), mock(CommentService.class)));
|
||||
mock(EpisodeService.class), mock(CommentService.class),
|
||||
mock(AppEpisodeStatsService.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+31
-1
@@ -2,6 +2,8 @@
|
||||
package com.fruitshorts.app.controller;
|
||||
|
||||
import com.fruitshorts.app.service.AppCatalogService;
|
||||
import com.fruitshorts.app.service.AppEpisodeStatsService;
|
||||
import com.fruitshorts.app.vo.AppEpisodeStatsVO;
|
||||
import com.fruitshorts.app.support.ControllerContractSupport;
|
||||
import com.fruitshorts.content.dal.entity.DramaEpisode;
|
||||
import com.fruitshorts.content.service.EpisodeService;
|
||||
@@ -37,6 +39,7 @@ class AppPlaybackContractTest {
|
||||
private PlaybackService playbackService;
|
||||
private EpisodeService episodeService;
|
||||
private CommentService commentService;
|
||||
private AppEpisodeStatsService episodeStatsService;
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@BeforeEach
|
||||
@@ -44,13 +47,14 @@ class AppPlaybackContractTest {
|
||||
playbackService = mock(PlaybackService.class);
|
||||
episodeService = mock(EpisodeService.class);
|
||||
commentService = mock(CommentService.class);
|
||||
episodeStatsService = mock(AppEpisodeStatsService.class);
|
||||
DramaEpisode episode = new DramaEpisode();
|
||||
episode.setLikeCount(88L);
|
||||
given(episodeService.getExisting(any())).willReturn(episode);
|
||||
given(commentService.countByEpisodes(any())).willReturn(Map.of());
|
||||
mockMvc = ControllerContractSupport.mockMvcOf(
|
||||
new AppDramaController(mock(AppCatalogService.class), playbackService,
|
||||
episodeService, commentService));
|
||||
episodeService, commentService, episodeStatsService));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -102,6 +106,32 @@ class AppPlaybackContractTest {
|
||||
verify(playbackService).play(isNull(), eq(13L), eq("device-9"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_episode_stats_without_play_ticket() throws Exception {
|
||||
AppEpisodeStatsVO stats = new AppEpisodeStatsVO();
|
||||
stats.setEpisodeId(11L);
|
||||
stats.setDramaId(7L);
|
||||
stats.setLikeCount(88L);
|
||||
stats.setCommentCount(8L);
|
||||
stats.setFavoriteCount(1800L);
|
||||
stats.setLiked(true);
|
||||
stats.setFavorited(false);
|
||||
given(episodeStatsService.get(11L, null)).willReturn(stats);
|
||||
|
||||
mockMvc.perform(get("/app/episodes/11/stats"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.episodeId").value("11"))
|
||||
.andExpect(jsonPath("$.data.dramaId").value("7"))
|
||||
.andExpect(jsonPath("$.data.likeCount").value("88"))
|
||||
.andExpect(jsonPath("$.data.commentCount").value("8"))
|
||||
.andExpect(jsonPath("$.data.favoriteCount").value("1800"))
|
||||
.andExpect(jsonPath("$.data.liked").value(true))
|
||||
.andExpect(jsonPath("$.data.favorited").value(false));
|
||||
|
||||
verify(episodeStatsService).get(11L, null);
|
||||
verify(playbackService, org.mockito.Mockito.never()).play(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
private PlayRespVO ticket(Long episodeId, boolean accessible, String accessType,
|
||||
String playUrl, String thumbnailsUrl) {
|
||||
PlayRespVO vo = new PlayRespVO();
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.app.service;
|
||||
|
||||
import com.fruitshorts.app.vo.AppEpisodeStatsVO;
|
||||
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.service.CommentService;
|
||||
import com.fruitshorts.interaction.service.LikeService;
|
||||
import com.fruitshorts.playback.service.FavoriteService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
class AppEpisodeStatsServiceTest {
|
||||
|
||||
@Test
|
||||
void should_batch_single_episode_stats_without_ticket_side_effects() {
|
||||
EpisodeService episodeService = mock(EpisodeService.class);
|
||||
DramaService dramaService = mock(DramaService.class);
|
||||
CommentService commentService = mock(CommentService.class);
|
||||
LikeService likeService = mock(LikeService.class);
|
||||
FavoriteService favoriteService = mock(FavoriteService.class);
|
||||
|
||||
DramaEpisode episode = new DramaEpisode();
|
||||
episode.setId(11L);
|
||||
episode.setDramaId(7L);
|
||||
episode.setLikeCount(88L);
|
||||
Drama drama = new Drama();
|
||||
drama.setId(7L);
|
||||
drama.setFavoriteCount(1800L);
|
||||
given(episodeService.listPublishedByIds(List.of(11L))).willReturn(List.of(episode));
|
||||
given(dramaService.listPublishedByIds(List.of(7L))).willReturn(List.of(drama));
|
||||
given(commentService.countByEpisodes(List.of(11L))).willReturn(Map.of(11L, 8L));
|
||||
given(likeService.likedTargetIds(
|
||||
100L, LikeService.TYPE_EPISODE, List.of(11L))).willReturn(Set.of(11L));
|
||||
given(favoriteService.favoriteDramaIds(100L, List.of(7L))).willReturn(Set.of());
|
||||
|
||||
AppEpisodeStatsService service = new AppEpisodeStatsService(
|
||||
episodeService, dramaService, commentService, likeService, favoriteService);
|
||||
AppEpisodeStatsVO result = service.get(11L, 100L);
|
||||
|
||||
assertThat(result.getEpisodeId()).isEqualTo(11L);
|
||||
assertThat(result.getDramaId()).isEqualTo(7L);
|
||||
assertThat(result.getLikeCount()).isEqualTo(88L);
|
||||
assertThat(result.getCommentCount()).isEqualTo(8L);
|
||||
assertThat(result.getFavoriteCount()).isEqualTo(1800L);
|
||||
assertThat(result.getLiked()).isTrue();
|
||||
assertThat(result.getFavorited()).isFalse();
|
||||
verify(episodeService).listPublishedByIds(List.of(11L));
|
||||
verify(commentService).countByEpisodes(List.of(11L));
|
||||
verify(likeService).likedTargetIds(
|
||||
100L, LikeService.TYPE_EPISODE, List.of(11L));
|
||||
verify(favoriteService).favoriteDramaIds(100L, List.of(7L));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user