feat(content): 新增内容类型与首页分类 feed
This commit is contained in:
1 parent
d9cdbc8e15
commit
1052b97306
19 files changed
+557
-4
No files matched your search
+23
@@ -9,6 +9,7 @@ import com.fruitshorts.app.vo.AppDramaCardVO;
|
||||
import com.fruitshorts.app.vo.AppDramaDetailVO;
|
||||
import com.fruitshorts.app.vo.AppFeedVO;
|
||||
import com.fruitshorts.app.vo.AppSectionVO;
|
||||
import com.fruitshorts.common.pojo.CursorResult;
|
||||
import com.fruitshorts.common.exception.ErrorCode;
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
import com.fruitshorts.playback.service.PlaybackService;
|
||||
@@ -86,6 +87,28 @@ class AppCatalogControllerContractTest {
|
||||
.andExpect(jsonPath("$.data.sections").isArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_typed_cursor_feed() throws Exception {
|
||||
AppDramaCardVO card = new AppDramaCardVO();
|
||||
card.setId(12L);
|
||||
card.setTitle("漫剧卡片");
|
||||
card.setDramaType("ANIME");
|
||||
given(catalogService.feed("ANIME", null, 20))
|
||||
.willReturn(new CursorResult<>(List.of(card), 12L, true));
|
||||
|
||||
catalogMvc.perform(get("/app/feed")
|
||||
.param("types", "ANIME")
|
||||
.param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.list[0].id").value("12"))
|
||||
.andExpect(jsonPath("$.data.list[0].dramaType").value("ANIME"))
|
||||
.andExpect(jsonPath("$.data.nextCursor").value("12"))
|
||||
.andExpect(jsonPath("$.data.hasMore").value(true));
|
||||
|
||||
verify(catalogService).feed("ANIME", null, 20);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_category_list_when_categories_are_requested() throws Exception {
|
||||
AppCategoryVO category = new AppCategoryVO(3L, "都市");
|
||||
|
||||
+29
-1
@@ -3,6 +3,8 @@
|
||||
package com.fruitshorts.app.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fruitshorts.app.service.AppCatalogService;
|
||||
import com.fruitshorts.common.pojo.CursorResult;
|
||||
import com.fruitshorts.framework.log.AccessDeniedRecorder;
|
||||
import com.fruitshorts.framework.security.JwtAuthenticationFilter;
|
||||
import com.fruitshorts.framework.security.JwtTokenProvider;
|
||||
@@ -28,13 +30,15 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/** 评论总数接口的安全链契约:匿名只读请求必须直接进入控制器。 */
|
||||
/** 客户端公开只读接口的安全链契约:匿名请求必须直接进入控制器。 */
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration
|
||||
@ContextConfiguration(classes = AppCommentCountAnonymousSecurityTest.TestConfig.class)
|
||||
@@ -45,10 +49,14 @@ class AppCommentCountAnonymousSecurityTest {
|
||||
|
||||
@Autowired
|
||||
private CommentService commentService;
|
||||
@Autowired
|
||||
private AppCatalogService catalogService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
given(commentService.countByDrama(7L)).willReturn(9L);
|
||||
given(catalogService.feed("ANIME", null, 20))
|
||||
.willReturn(new CursorResult<>(List.of(), null, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -59,6 +67,16 @@ class AppCommentCountAnonymousSecurityTest {
|
||||
.andExpect(jsonPath("$.data").value(9));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_allow_anonymous_typed_feed_request_through_security_chain() throws Exception {
|
||||
mockMvc.perform(get("/app/feed")
|
||||
.param("types", "ANIME")
|
||||
.param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.list").isArray());
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
@EnableWebSecurity
|
||||
@@ -75,6 +93,16 @@ class AppCommentCountAnonymousSecurityTest {
|
||||
return new AppCommentController(commentService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
AppCatalogService appCatalogService() {
|
||||
return mock(AppCatalogService.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
AppCatalogController appCatalogController(AppCatalogService appCatalogService) {
|
||||
return new AppCatalogController(appCatalogService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtAuthenticationFilter jwtAuthenticationFilter() {
|
||||
return new JwtAuthenticationFilter(
|
||||
|
||||
+61
@@ -3,6 +3,7 @@ package com.fruitshorts.app.service;
|
||||
|
||||
import com.fruitshorts.app.vo.AppDramaCardVO;
|
||||
import com.fruitshorts.app.vo.AppDramaDetailVO;
|
||||
import com.fruitshorts.common.pojo.CursorResult;
|
||||
import com.fruitshorts.content.dal.entity.Drama;
|
||||
import com.fruitshorts.content.service.BannerService;
|
||||
import com.fruitshorts.content.service.DramaCategoryService;
|
||||
@@ -99,4 +100,64 @@ class AppCatalogServiceTest {
|
||||
verify(commentService).countByDramas(List.of(1L, 2L));
|
||||
verify(commentService, never()).countByDrama(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_filter_feed_by_drama_type_and_expose_it_on_card() {
|
||||
Drama anime = drama(1L, "ANIME");
|
||||
given(dramaService.listPublishedFeed(List.of("ANIME"), null, 21))
|
||||
.willReturn(List.of(anime));
|
||||
given(dramaCategoryService.listBasic()).willReturn(List.of());
|
||||
given(episodeService.firstPlayableByDramas(List.of(1L))).willReturn(Map.of());
|
||||
given(commentService.countByDramas(List.of(1L))).willReturn(Map.of(1L, 3L));
|
||||
|
||||
CursorResult<AppDramaCardVO> result = catalogService.feed("ANIME", null, null);
|
||||
|
||||
assertThat(result.getList()).hasSize(1);
|
||||
assertThat(result.getList().get(0).getDramaType()).isEqualTo("ANIME");
|
||||
assertThat(result.getList().get(0).getCommentCount()).isEqualTo(3L);
|
||||
assertThat(result.isHasMore()).isFalse();
|
||||
verify(dramaService).listPublishedFeed(List.of("ANIME"), null, 21);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_parse_multiple_feed_types() {
|
||||
given(dramaService.listPublishedFeed(List.of("ANIME", "LIVE"), null, 11))
|
||||
.willReturn(List.of());
|
||||
|
||||
CursorResult<AppDramaCardVO> result = catalogService.feed("ANIME,LIVE", null, 10);
|
||||
|
||||
assertThat(result.getList()).isEmpty();
|
||||
verify(dramaService).listPublishedFeed(List.of("ANIME", "LIVE"), null, 11);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_page_feed_with_cursor_without_duplicates() {
|
||||
Drama first = drama(1L, "ANIME");
|
||||
Drama second = drama(2L, "LIVE");
|
||||
given(dramaService.listPublishedFeed(List.of(), null, 2))
|
||||
.willReturn(List.of(first, second));
|
||||
given(dramaService.listPublishedFeed(List.of(), 1L, 2))
|
||||
.willReturn(List.of(second));
|
||||
given(dramaCategoryService.listBasic()).willReturn(List.of());
|
||||
given(episodeService.firstPlayableByDramas(any())).willReturn(Map.of());
|
||||
given(commentService.countByDramas(any())).willReturn(Map.of());
|
||||
|
||||
CursorResult<AppDramaCardVO> firstPage = catalogService.feed(null, null, 1);
|
||||
CursorResult<AppDramaCardVO> secondPage = catalogService.feed(null, firstPage.getNextCursor(), 1);
|
||||
|
||||
assertThat(firstPage.getList()).extracting(AppDramaCardVO::getId).containsExactly(1L);
|
||||
assertThat(firstPage.getNextCursor()).isEqualTo(1L);
|
||||
assertThat(firstPage.isHasMore()).isTrue();
|
||||
assertThat(secondPage.getList()).extracting(AppDramaCardVO::getId).containsExactly(2L);
|
||||
assertThat(secondPage.getNextCursor()).isNull();
|
||||
assertThat(secondPage.isHasMore()).isFalse();
|
||||
}
|
||||
|
||||
private Drama drama(Long id, String dramaType) {
|
||||
Drama drama = new Drama();
|
||||
drama.setId(id);
|
||||
drama.setTitle("短剧" + id);
|
||||
drama.setDramaType(dramaType);
|
||||
return drama;
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.app.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.fruitshorts.content.dal.entity.Drama;
|
||||
import com.fruitshorts.content.dal.mapper.DramaEpisodeMapper;
|
||||
import com.fruitshorts.content.dal.mapper.DramaMapper;
|
||||
import com.fruitshorts.content.dal.mapper.DramaReviewRecordMapper;
|
||||
import com.fruitshorts.content.service.DramaCategoryService;
|
||||
import com.fruitshorts.content.service.DramaService;
|
||||
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.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;
|
||||
|
||||
/** K88 App 推荐流的类型过滤与三字段 keyset 游标。 */
|
||||
class DramaServiceFeedTest {
|
||||
|
||||
private DramaMapper dramaMapper;
|
||||
private DramaService dramaService;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfo() {
|
||||
TableInfoHelper.initTableInfo(
|
||||
new MapperBuilderAssistant(new MybatisConfiguration(), ""), Drama.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
dramaMapper = mock(DramaMapper.class);
|
||||
dramaService = new DramaService(
|
||||
dramaMapper,
|
||||
mock(DramaEpisodeMapper.class),
|
||||
mock(DramaReviewRecordMapper.class),
|
||||
mock(DramaCategoryService.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void should_filter_types_and_apply_recommendation_cursor() {
|
||||
Drama marker = new Drama();
|
||||
marker.setId(9L);
|
||||
marker.setStatus("published");
|
||||
marker.setDramaType("ANIME");
|
||||
marker.setRecommendWeight(30);
|
||||
marker.setViewCount(1200L);
|
||||
given(dramaMapper.selectById(9L)).willReturn(marker);
|
||||
given(dramaMapper.selectList(any())).willReturn(List.of());
|
||||
ArgumentCaptor<QueryWrapper<Drama>> captor = ArgumentCaptor.forClass(QueryWrapper.class);
|
||||
|
||||
dramaService.listPublishedFeed(List.of("ANIME"), 9L, 5);
|
||||
|
||||
verify(dramaMapper).selectList(captor.capture());
|
||||
QueryWrapper<Drama> query = captor.getValue();
|
||||
assertThat(query.getSqlSegment())
|
||||
.contains("status")
|
||||
.contains("drama_type")
|
||||
.contains("recommend_weight")
|
||||
.contains("view_count")
|
||||
.contains("id");
|
||||
assertThat(query.getParamNameValuePairs().values())
|
||||
.contains("published", "ANIME", 30, 1200L, 9L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void should_handle_null_recommend_weight_in_cursor() {
|
||||
Drama marker = new Drama();
|
||||
marker.setId(9L);
|
||||
marker.setStatus("published");
|
||||
marker.setDramaType("LIVE");
|
||||
marker.setViewCount(1200L);
|
||||
given(dramaMapper.selectById(9L)).willReturn(marker);
|
||||
given(dramaMapper.selectList(any())).willReturn(List.of());
|
||||
ArgumentCaptor<QueryWrapper<Drama>> captor = ArgumentCaptor.forClass(QueryWrapper.class);
|
||||
|
||||
dramaService.listPublishedFeed(List.of("LIVE"), 9L, 5);
|
||||
|
||||
verify(dramaMapper).selectList(captor.capture());
|
||||
assertThat(captor.getValue().getSqlSegment())
|
||||
.contains("recommend_weight IS NULL")
|
||||
.contains("view_count")
|
||||
.contains("id");
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.app.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.fruitshorts.common.exception.ErrorCode;
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
import com.fruitshorts.content.dal.entity.Drama;
|
||||
import com.fruitshorts.content.dal.mapper.DramaEpisodeMapper;
|
||||
import com.fruitshorts.content.dal.mapper.DramaMapper;
|
||||
import com.fruitshorts.content.dal.mapper.DramaReviewRecordMapper;
|
||||
import com.fruitshorts.content.service.DramaCategoryService;
|
||||
import com.fruitshorts.content.service.DramaService;
|
||||
import com.fruitshorts.content.vo.DramaSaveReqVO;
|
||||
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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
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;
|
||||
|
||||
/** K88 后台短剧内容类型与互动数据编辑规则。 */
|
||||
class DramaServiceK88Test {
|
||||
|
||||
private DramaMapper dramaMapper;
|
||||
private DramaService dramaService;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfo() {
|
||||
TableInfoHelper.initTableInfo(
|
||||
new MapperBuilderAssistant(new MybatisConfiguration(), ""), Drama.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
dramaMapper = mock(DramaMapper.class);
|
||||
dramaService = new DramaService(
|
||||
dramaMapper,
|
||||
mock(DramaEpisodeMapper.class),
|
||||
mock(DramaReviewRecordMapper.class),
|
||||
mock(DramaCategoryService.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_update_only_requested_counts_and_preserve_drama_type() {
|
||||
Drama existing = drama();
|
||||
given(dramaMapper.selectById(7L)).willReturn(existing);
|
||||
DramaSaveReqVO req = new DramaSaveReqVO();
|
||||
req.setTitle("更新后的标题");
|
||||
req.setFavoriteCount(99L);
|
||||
req.setHeat(500L);
|
||||
|
||||
dramaService.update(7L, req);
|
||||
|
||||
ArgumentCaptor<Drama> captor = ArgumentCaptor.forClass(Drama.class);
|
||||
verify(dramaMapper).updateById(captor.capture());
|
||||
Drama updated = captor.getValue();
|
||||
assertThat(updated.getFavoriteCount()).isEqualTo(99L);
|
||||
assertThat(updated.getLikeCount()).isEqualTo(20L);
|
||||
assertThat(updated.getViewCount()).isEqualTo(500L);
|
||||
assertThat(updated.getDramaType()).isEqualTo("ANIME");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_reject_negative_interaction_count() {
|
||||
given(dramaMapper.selectById(7L)).willReturn(drama());
|
||||
DramaSaveReqVO req = new DramaSaveReqVO();
|
||||
req.setTitle("更新后的标题");
|
||||
req.setFavoriteCount(-1L);
|
||||
|
||||
assertThatThrownBy(() -> dramaService.update(7L, req))
|
||||
.isInstanceOf(ServiceException.class)
|
||||
.satisfies(error -> assertThat(((ServiceException) error).getCode())
|
||||
.isEqualTo(ErrorCode.BAD_REQUEST.getCode()));
|
||||
verify(dramaMapper, never()).updateById(any(Drama.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_reject_invalid_drama_type() {
|
||||
given(dramaMapper.selectById(7L)).willReturn(drama());
|
||||
DramaSaveReqVO req = new DramaSaveReqVO();
|
||||
req.setTitle("更新后的标题");
|
||||
req.setDramaType("MOVIE");
|
||||
|
||||
assertThatThrownBy(() -> dramaService.update(7L, req))
|
||||
.isInstanceOf(ServiceException.class)
|
||||
.satisfies(error -> assertThat(((ServiceException) error).getCode())
|
||||
.isEqualTo(ErrorCode.BAD_REQUEST.getCode()));
|
||||
verify(dramaMapper, never()).updateById(any(Drama.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_default_new_drama_to_live_and_zero_counts() {
|
||||
DramaSaveReqVO req = new DramaSaveReqVO();
|
||||
req.setTitle("新短剧");
|
||||
|
||||
dramaService.create(req, 1L);
|
||||
|
||||
ArgumentCaptor<Drama> captor = ArgumentCaptor.forClass(Drama.class);
|
||||
verify(dramaMapper).insert(captor.capture());
|
||||
Drama created = captor.getValue();
|
||||
assertThat(created.getDramaType()).isEqualTo("LIVE");
|
||||
assertThat(created.getFavoriteCount()).isZero();
|
||||
assertThat(created.getLikeCount()).isZero();
|
||||
assertThat(created.getViewCount()).isZero();
|
||||
}
|
||||
|
||||
private Drama drama() {
|
||||
Drama drama = new Drama();
|
||||
drama.setId(7L);
|
||||
drama.setTitle("原标题");
|
||||
drama.setStatus("published");
|
||||
drama.setDramaType("ANIME");
|
||||
drama.setFavoriteCount(10L);
|
||||
drama.setLikeCount(20L);
|
||||
drama.setViewCount(30L);
|
||||
return drama;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user