diff --git a/fruitshorts-app-api/src/test/java/com/fruitshorts/playback/service/WatchProgressServiceTest.java b/fruitshorts-app-api/src/test/java/com/fruitshorts/playback/service/WatchProgressServiceTest.java index 5d9094e..35c2f26 100644 --- a/fruitshorts-app-api/src/test/java/com/fruitshorts/playback/service/WatchProgressServiceTest.java +++ b/fruitshorts-app-api/src/test/java/com/fruitshorts/playback/service/WatchProgressServiceTest.java @@ -32,9 +32,11 @@ import org.springframework.transaction.support.TransactionTemplate; import java.time.LocalDateTime; import java.lang.reflect.Method; import java.util.List; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -330,6 +332,72 @@ class WatchProgressServiceTest { verify(progressMapper).deleteById(1L); } + @Test + void should_delete_buffer_state_when_removing_or_clearing_history() { + WatchProgressBuffer buffer = mock(WatchProgressBuffer.class); + WatchProgressService service = serviceWithBuffer(buffer); + + service.remove(new InteractionIdentity(9L, ""), 5L); + service.clear(new InteractionIdentity(9L, "")); + + verify(buffer).remove(any(InteractionIdentity.class), eq(5L)); + verify(buffer).clear(any(InteractionIdentity.class)); + verify(progressMapper, times(2)).delete(any()); + } + + @Test + void should_not_bring_back_deleted_drama_from_buffer_in_recent_page() { + WatchProgressBuffer buffer = mock(WatchProgressBuffer.class); + WatchProgressService service = serviceWithBuffer(buffer); + Page emptyPage = new Page<>(1, 20, 0); + emptyPage.setRecords(List.of()); + given(progressMapper.selectPage(any(Page.class), any())).willReturn(emptyPage); + given(buffer.getAll(any(InteractionIdentity.class))).willReturn(Map.of( + 5L, new WatchProgressBuffer.BufferedProgress( + 5L, 88L, 1_000L, 2_000L, false, "feed", "", 1_700_000_000_000L))); + // 库里该剧只剩逻辑删除行:缓冲状态不得再把它读回列表 + WatchProgress hidden = progress(5L, 88L); + hidden.setDeleted(1); + given(progressMapper.selectAnyForIdentityIn(9L, "", List.of(5L))) + .willReturn(List.of(hidden)); + + PageResult result = + service.recentPage(new InteractionIdentity(9L, ""), 1, 20); + + assertThat(result.getList()).isEmpty(); + assertThat(result.getTotal()).isZero(); + } + + @Test + void should_drop_buffered_refs_whose_state_is_gone_instead_of_requeue() { + WatchProgressBuffer buffer = mock(WatchProgressBuffer.class); + WatchProgressService service = serviceWithBuffer(buffer); + given(buffer.drain(10)).willReturn( + List.of(new WatchProgressBuffer.IdentityRef("u:9", 9L, "", 5L))); + given(buffer.getAllBatch(List.of("u:9"))).willReturn(Map.of()); + + int flushed = service.flushBuffered(10); + + // 状态已消失(用户删除/清空历史或 TTL 过期):必须丢弃,退回队列会变成永不消失的僵尸条目 + assertThat(flushed).isZero(); + verify(buffer, never()).requeue(any()); + verify(progressMapper, never()).selectBatchForPairs(any()); + } + + private WatchProgressService serviceWithBuffer(WatchProgressBuffer buffer) { + @SuppressWarnings("unchecked") + ObjectProvider provider = mock(ObjectProvider.class); + given(provider.getIfAvailable()).willReturn(buffer); + return new WatchProgressService( + progressMapper, + historyMapper, + dramaService, + episodeService, + provider, + mock(SqlSessionFactory.class), + new TransactionTemplate(mock(PlatformTransactionManager.class))); + } + private Drama drama(Long id, String title, int totalEpisodes) { Drama drama = new Drama(); drama.setId(id);