feat(watch): 支持设备观看与登录合并

This commit is contained in:
Zhou Weidong committed 2026-10-27 09:54:47 +08:00
1 parent 4b0fbd73d3
commit 848c746505
14 files changed
+613 -70

No files matched your search

@@ -8,9 +8,13 @@ import com.fruitshorts.app.service.AppCatalogService;
import com.fruitshorts.app.vo.AppEpisodeStatsVO;
import com.fruitshorts.common.exception.ErrorCode;
import com.fruitshorts.common.pojo.CursorResult;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.interaction.InteractionIdentity;
import com.fruitshorts.interaction.service.LikeService;
import com.fruitshorts.playback.service.FavoriteService;
import com.fruitshorts.playback.service.PlaybackService;
import com.fruitshorts.playback.service.WatchProgressService;
import com.fruitshorts.playback.vo.ProgressRespVO;
import com.fruitshorts.content.service.EpisodeService;
import com.fruitshorts.framework.interaction.InteractionIdentityResolver;
import com.fruitshorts.app.service.AnonymousInteractionRateLimiter;
@@ -42,9 +46,11 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import java.util.List;
import static org.mockito.BDDMockito.given;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -63,6 +69,10 @@ class AppCommentCountAnonymousSecurityTest {
private AppCatalogService catalogService;
@Autowired
private AppEpisodeStatsService episodeStatsService;
@Autowired
private FavoriteService favoriteService;
@Autowired
private WatchProgressService watchProgressService;
@BeforeEach
void setUp() {
@@ -78,6 +88,16 @@ class AppCommentCountAnonymousSecurityTest {
stats.setLiked(true);
stats.setFavorited(false);
given(episodeStatsService.get(11L, null, "device-a")).willReturn(stats);
given(favoriteService.page(any(InteractionIdentity.class), any(Integer.class), any(Integer.class)))
.willReturn(new PageResult<>(0L, List.of()));
given(catalogService.toCards(List.of())).willReturn(List.of());
given(watchProgressService.recentPage(
any(InteractionIdentity.class), any(Integer.class), any(Integer.class)))
.willReturn(new PageResult<>(0L, List.of()));
given(watchProgressService.ofDrama(any(InteractionIdentity.class), any(Long.class)))
.willReturn(null);
given(watchProgressService.save(any(InteractionIdentity.class), any()))
.willReturn(new ProgressRespVO());
}
@Test
@@ -128,6 +148,35 @@ class AppCommentCountAnonymousSecurityTest {
.andExpect(jsonPath("$.data.favoriteCount").value(8));
}
@Test
void should_allow_anonymous_library_and_watch_operations() throws Exception {
mockMvc.perform(get("/app/favorites").header("X-Device-Id", "device-a"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
mockMvc.perform(get("/app/watch/recent").header("X-Device-Id", "device-a"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
mockMvc.perform(get("/app/watch/progress")
.header("X-Device-Id", "device-a")
.param("dramaId", "7"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
mockMvc.perform(post("/app/watch/progress")
.header("X-Device-Id", "device-a")
.contentType("application/json")
.content("{\"episodeId\":11,\"positionMs\":1000}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
mockMvc.perform(delete("/app/watch/progress/7")
.header("X-Device-Id", "device-a"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
mockMvc.perform(delete("/app/watch/progress")
.header("X-Device-Id", "device-a"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
}
@Test
void should_keep_paid_and_entitlement_operations_login_required() throws Exception {
mockMvc.perform(post("/app/episodes/11/unlock"))
@@ -183,6 +232,11 @@ class AppCommentCountAnonymousSecurityTest {
return mock(FavoriteService.class);
}
@Bean
WatchProgressService watchProgressService() {
return mock(WatchProgressService.class);
}
@Bean
AppInteractionController appInteractionController(
LikeService likeService,
@@ -207,6 +261,17 @@ class AppCommentCountAnonymousSecurityTest {
rateLimiter);
}
@Bean
AppWatchController appWatchController(
WatchProgressService watchProgressService,
InteractionIdentityResolver identityResolver,
AnonymousInteractionRateLimiter rateLimiter) {
return new AppWatchController(
watchProgressService,
identityResolver,
rateLimiter);
}
@Bean
AppEpisodeStatsService appEpisodeStatsService() {
return mock(AppEpisodeStatsService.class);
@@ -6,6 +6,7 @@ import com.fruitshorts.app.service.AppCatalogService;
import com.fruitshorts.app.support.ControllerContractSupport;
import com.fruitshorts.app.vo.AppDramaCardVO;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.interaction.InteractionIdentity;
import com.fruitshorts.content.dal.entity.Drama;
import com.fruitshorts.framework.security.LoginUser;
import com.fruitshorts.framework.interaction.InteractionIdentityResolver;
@@ -24,6 +25,8 @@ import java.time.LocalDateTime;
import java.util.List;
import static org.mockito.BDDMockito.given;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -46,14 +49,17 @@ class AppLibraryPagingContractTest {
favoriteService = mock(FavoriteService.class);
catalogService = mock(AppCatalogService.class);
progressService = mock(WatchProgressService.class);
InteractionIdentityResolver identityResolver = new InteractionIdentityResolver();
AnonymousInteractionRateLimiter rateLimiter =
mock(AnonymousInteractionRateLimiter.class);
favoriteMvc = ControllerContractSupport.mockMvcOf(
new AppFavoriteController(
favoriteService,
catalogService,
mock(InteractionIdentityResolver.class),
mock(AnonymousInteractionRateLimiter.class)));
identityResolver,
rateLimiter));
watchMvc = ControllerContractSupport.mockMvcOf(
new AppWatchController(progressService));
new AppWatchController(progressService, identityResolver, rateLimiter));
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
new LoginUser(USER_ID, "app-user", "APP_USER", List.of()),
@@ -72,9 +78,9 @@ class AppLibraryPagingContractTest {
Drama first = drama(101L, "收藏一");
Drama second = drama(102L, "收藏二");
Drama third = drama(103L, "收藏三");
given(favoriteService.page(USER_ID, 1, 20))
given(favoriteService.page(any(InteractionIdentity.class), eq(1), eq(20)))
.willReturn(new PageResult<>(21L, List.of(first, second)));
given(favoriteService.page(USER_ID, 2, 20))
given(favoriteService.page(any(InteractionIdentity.class), eq(2), eq(20)))
.willReturn(new PageResult<>(21L, List.of(third)));
given(catalogService.toCards(List.of(first, second)))
.willReturn(List.of(card(101L), card(102L)));
@@ -95,8 +101,8 @@ class AppLibraryPagingContractTest {
.andExpect(jsonPath("$.data.list.length()").value(1))
.andExpect(jsonPath("$.data.list[0].id").value("103"));
verify(favoriteService).page(USER_ID, 1, 20);
verify(favoriteService).page(USER_ID, 2, 20);
verify(favoriteService).page(any(InteractionIdentity.class), eq(1), eq(20));
verify(favoriteService).page(any(InteractionIdentity.class), eq(2), eq(20));
}
@Test
@@ -104,9 +110,9 @@ class AppLibraryPagingContractTest {
ProgressRespVO first = progress(12L, 88L, 6, "剧名一", 68);
ProgressRespVO second = progress(13L, 99L, 2, "剧名二", 20);
ProgressRespVO third = progress(14L, 100L, 1, "剧名三", 16);
given(progressService.recentPage(USER_ID, 1, 20))
given(progressService.recentPage(any(InteractionIdentity.class), eq(1), eq(20)))
.willReturn(new PageResult<>(23L, List.of(first, second)));
given(progressService.recentPage(USER_ID, 2, 20))
given(progressService.recentPage(any(InteractionIdentity.class), eq(2), eq(20)))
.willReturn(new PageResult<>(23L, List.of(third)));
watchMvc.perform(get("/app/watch/recent"))
@@ -126,14 +132,14 @@ class AppLibraryPagingContractTest {
.andExpect(jsonPath("$.data.list[0].dramaId").value("14"))
.andExpect(jsonPath("$.data.list[0].dramaTitle").value("剧名三"));
verify(progressService).recentPage(USER_ID, 1, 20);
verify(progressService).recentPage(USER_ID, 2, 20);
verify(progressService).recentPage(any(InteractionIdentity.class), eq(1), eq(20));
verify(progressService).recentPage(any(InteractionIdentity.class), eq(2), eq(20));
}
@Test
void should_keep_total_and_return_empty_list_when_watch_page_is_out_of_range()
throws Exception {
given(progressService.recentPage(USER_ID, 9, 20))
given(progressService.recentPage(any(InteractionIdentity.class), eq(9), eq(20)))
.willReturn(new PageResult<>(23L, List.of()));
watchMvc.perform(get("/app/watch/recent").param("pageIndex", "9"))
@@ -146,7 +152,7 @@ class AppLibraryPagingContractTest {
@Test
void should_ignore_removed_limit_and_still_return_page_result() throws Exception {
// limit 已从契约移除:即使客户端误传,也应走分页并返回 PageResult。
given(progressService.recentPage(USER_ID, 1, 20))
given(progressService.recentPage(any(InteractionIdentity.class), eq(1), eq(20)))
.willReturn(new PageResult<>(3L, List.of(progress(12L, 88L, 6, "剧名一", 68))));
watchMvc.perform(get("/app/watch/recent").param("limit", "5"))
@@ -156,7 +162,7 @@ class AppLibraryPagingContractTest {
.andExpect(jsonPath("$.data.list.length()").value(1))
.andExpect(jsonPath("$.data.list[0].dramaTitle").value("剧名一"));
verify(progressService).recentPage(USER_ID, 1, 20);
verify(progressService).recentPage(any(InteractionIdentity.class), eq(1), eq(20));
}
private Drama drama(Long id, String title) {
@@ -0,0 +1,41 @@
// Author: fruit.com.cn
package com.fruitshorts.app.service;
import com.fruitshorts.interaction.service.CommentService;
import com.fruitshorts.interaction.service.LikeService;
import com.fruitshorts.playback.service.FavoriteService;
import com.fruitshorts.playback.service.WatchProgressService;
import org.junit.jupiter.api.Test;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
class InteractionAccountMergeServiceTest {
@Test
void should_merge_all_device_interactions_and_watch_data() {
LikeService likeService = mock(LikeService.class);
FavoriteService favoriteService = mock(FavoriteService.class);
CommentService commentService = mock(CommentService.class);
WatchProgressService watchService = mock(WatchProgressService.class);
given(likeService.mergeDeviceIntoUser(9L, "device-a"))
.willReturn(new LikeService.MergeResult(1, 0));
given(favoriteService.mergeDeviceIntoUser(9L, "device-a"))
.willReturn(new FavoriteService.MergeResult(1, 0));
given(commentService.mergeDeviceIntoUser(9L, "device-a"))
.willReturn(new CommentService.MergeResult(1, 0));
given(watchService.mergeDeviceIntoUser(9L, "device-a"))
.willReturn(new WatchProgressService.MergeResult(2, 1, 3, 2));
InteractionAccountMergeService service = new InteractionAccountMergeService(
likeService, favoriteService, commentService, watchService);
service.merge(9L, "device-a");
verify(likeService).mergeDeviceIntoUser(9L, "device-a");
verify(favoriteService).mergeDeviceIntoUser(9L, "device-a");
verify(commentService).mergeDeviceIntoUser(9L, "device-a");
verify(watchService).mergeDeviceIntoUser(9L, "device-a");
}
}