feat(app): 推荐流支持分类过滤
This commit is contained in:
1 parent
0e8fb1582d
commit
8f35160b56
12 files changed
+255
-24
No files matched your search
+4
-1
@@ -167,7 +167,7 @@ app.register.max-per-device-per-day
|
||||
| --- | --- | --- |
|
||||
| GET | `/app/home/feed` | 首页:轮播 + 分区,卡片带 `preview` 可播预览集与 `language` 语种 |
|
||||
| GET | `/app/discover/feed` | 发现页分区 |
|
||||
| GET | `/app/feed?types=ANIME,LIVE&cursor=&size=20` | 推荐流;按内容类型多选过滤,游标翻页 |
|
||||
| GET | `/app/feed?types=ANIME,LIVE&categoryId=7&cursor=&size=20` | 推荐流;支持内容类型与分类叠加过滤,游标翻页 |
|
||||
| GET | `/app/categories` | 分类列表 |
|
||||
| GET | `/app/dramas/{id}` | 详情(含剧集列表,免费集带**已签名**播放地址) |
|
||||
| GET | `/app/search?keyword=&pageIndex=&pageSize=` | 搜索已上线短剧:按标题/简介模糊匹配,分页返回卡片列表 |
|
||||
@@ -188,7 +188,10 @@ app.register.max-per-device-per-day
|
||||
|
||||
- `types` 可选,逗号分隔,支持 `ANIME`、`LIVE` 或多选 `ANIME,LIVE`;不传或空值返回全部已上线内容。
|
||||
过滤按 `drama_types` 整词匹配,多标签剧可被多个类型同时命中。
|
||||
- `categoryId` 可选;不传返回全部分类。传入后按 `category_ids` 整词匹配,
|
||||
可与 `types` 叠加,只返回同时命中内容类型和分类的已上线短剧。
|
||||
- `cursor` 可选,上一页返回的 `nextCursor`;`size` 默认 20,最大 50。
|
||||
游标短剧必须同时满足已上线、内容类型命中和分类命中,否则返回空列表。
|
||||
- 排序:`recommend_weight DESC NULLS LAST, view_count DESC, id DESC`。
|
||||
- 返回 `Result<CursorResult<AppDramaCardVO>>`,卡片字段与首页一致,包含互动计数、`preview`、`language`、`dramaType`。
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# K96 剧场页服务端分类分页
|
||||
|
||||
## 目标
|
||||
|
||||
- `/app/feed` 支持按分类过滤,并与内容类型过滤叠加。
|
||||
- 剧场页「全部」展示全部已上线短剧,任意分类展示该分类全部已上线短剧。
|
||||
- 列表使用游标分页,切分类优先命中缓存,支持静默刷新、加载更多和失败重试。
|
||||
|
||||
## 分支
|
||||
|
||||
- FruitServer:`feat/k96-feed-category`
|
||||
- ReelShort:`feat/k96-theater-feed`
|
||||
|
||||
## A. FruitServer
|
||||
|
||||
1. `AppCatalogController.feed` 增加可选 `Long categoryId` 并传给服务层。
|
||||
2. `AppCatalogService.feed` 增加 `categoryId`,传给
|
||||
`DramaService.listPublishedFeed`。
|
||||
3. `DramaService.listPublishedFeed` 增加 `categoryId`:
|
||||
- 非空时使用 `DramaMultiTagSupport.applyCategoryIdTokens`
|
||||
按 `category_ids` 整词匹配。
|
||||
- 游标 marker 必须同时满足已上线、内容类型命中和分类命中,否则返回空列表。
|
||||
4. 补充服务层、控制器契约测试:
|
||||
- 仅 categoryId。
|
||||
- categoryId + types。
|
||||
- 分类游标翻页。
|
||||
- marker 分类不命中返回空。
|
||||
5. 更新 `docs/api-phase1.md` 的 `/app/feed` 参数和分类过滤说明。
|
||||
|
||||
门禁:`mvn test` 全模块通过。
|
||||
|
||||
## B. ReelShort
|
||||
|
||||
### B1. API 与仓库
|
||||
|
||||
- `CatalogApi.feed` 增加可选 `String? categoryId`,非空才拼 query。
|
||||
- `CatalogRepository.fetchFeed` 同步参数,HTTP 实现透传。
|
||||
|
||||
### B2. TheaterFeedController
|
||||
|
||||
- 新增 `lib/core/data/services/theater_feed_controller.dart`。
|
||||
- 按 `String? categoryId` 独立缓存状态:
|
||||
`dramas / cursor / hasMore / loaded / loading / loadingMore / lastError /
|
||||
requestSeq / inFlight`。
|
||||
- `select(id)` 同步切分类;有数据先渲染并静默刷新,无数据时加载。
|
||||
- `loadMore` 游标追加并按 id 去重;`hasMore=false` 停止。
|
||||
- `refreshCurrent` 失败保留旧数据;切换回失败分类自动重试。
|
||||
- 单请求 10 秒超时,响应写回发起分类,requestSeq 仅处理同分类顺序。
|
||||
|
||||
### B3. 剧场接入
|
||||
|
||||
- `DiscoverPage` 创建并持有 TheaterFeedController,初始请求「全部」。
|
||||
- 「全部」和分类 chips 的网格统一读取 `theater.dramas`,不再使用
|
||||
`categories.feed.dramas` / `dramasOf`。
|
||||
- `_selectCategory` 同时更新 `DiscoverLogic.selectedCategoryId` 和
|
||||
`theater.select(categoryId)`。
|
||||
- 滚动剩余不足 400px 时触发 `loadMore`;搜索模式不触发剧场分页。
|
||||
- 下拉刷新并行执行 `categories.load()` 和 `theater.refreshCurrent()`。
|
||||
- 无缓存首屏显示转圈;空、失败和重试保持既有剧场视觉。
|
||||
|
||||
### B4. 测试
|
||||
|
||||
- TheaterFeedController:
|
||||
- 全部使用空 categoryId,分类使用对应 id。
|
||||
- 缓存切回不闪烁。
|
||||
- 分页去重、hasMore=false 停止。
|
||||
- 失败保留旧数据、失败分类切回自动重试。
|
||||
- 剧场 widget:
|
||||
- 点击分类请求对应 categoryId。
|
||||
- 接近底部触发下一页。
|
||||
- 搜索、下拉刷新与既有布局不回归。
|
||||
|
||||
门禁:`dart analyze lib test` 零告警、`flutter test` 全过、
|
||||
`flutter build apk --debug` 成功。
|
||||
|
||||
## Git
|
||||
|
||||
- FruitServer 提交:`feat(app): 推荐流支持分类过滤`
|
||||
- ReelShort 提交:`feat(theater): 剧场分类列表服务端分页`
|
||||
- 两仓库作者:`Zhou Weidong <502333560@qq.com>`,禁止 `git add -A`。
|
||||
- 各自 `--no-ff` 合回 `develop` 并 push,两仓库 PROGRESS 留痕。
|
||||
|
||||
## 约束
|
||||
|
||||
- 全程中文;不改 specs、ReelShortWeb,不改首页 K94/K95 已交付逻辑。
|
||||
- 不安装或操作真机;完成后停住等待验收。
|
||||
@@ -24253,3 +24253,42 @@ Running Gradle task 'assembleDebug'... 27.6s
|
||||
`838ca2c` 的代码差异为空。
|
||||
- 本地分支 `fix/k95.2-poster-transition` 已删除;远端原本不存在该分支。
|
||||
- K95、K95.1 成果保持不变;未实现其他改动,未安装或操作真机。
|
||||
|
||||
## 2026-10-21 K96 剧场页服务端分类分页
|
||||
|
||||
- 已完整读取 K96 `docs/plans/INBOX.md`。
|
||||
- 已从最新 `origin/develop` 创建 FruitServer `feat/k96-feed-category` 与
|
||||
ReelShort `feat/k96-theater-feed`。
|
||||
- 已先写计划 `docs/plans/2026-10-21-K96-theater-feed.md`,覆盖分类参数、
|
||||
category_ids 整词匹配、游标 marker 三重校验、客户端分类缓存分页和剧场接入。
|
||||
- 已完成 `/app/feed` 可选 `categoryId`:可与 `types` 叠加;游标 marker 必须同时满足
|
||||
已上线、类型命中和分类命中,否则返回空列表。
|
||||
- 已补分类单独过滤、分类 + 类型、分类游标和 marker 不匹配测试,并更新
|
||||
`docs/api-phase1.md`。
|
||||
- 客户端已完成 Catalog API 分类参数、TheaterFeedController、分类缓存/静默刷新、
|
||||
游标追加去重、10 秒超时与滚动加载;剧场「全部 / 分类」已改用 `/app/feed`,
|
||||
搜索与首页 K94/K95 逻辑未改。
|
||||
|
||||
门禁原始输出:
|
||||
|
||||
```text
|
||||
$ mvn test
|
||||
[INFO] fruitshorts-app-api ................................ SUCCESS
|
||||
[INFO] BUILD SUCCESS
|
||||
[INFO] Total time: 15.436 s
|
||||
|
||||
$ dart analyze lib test
|
||||
Analyzing lib, test...
|
||||
No issues found!
|
||||
|
||||
$ flutter test --no-pub
|
||||
00:52 +553: All tests passed!
|
||||
|
||||
$ flutter build apk --debug
|
||||
Running Gradle task 'assembleDebug'... 24.8s
|
||||
✓ Built build/app/outputs/flutter-apk/app-debug.apk
|
||||
```
|
||||
|
||||
解读:FruitServer 全部 14 个 Reactor 模块通过,app-api 99 项测试全过;
|
||||
ReelShort 静态分析零告警、553 项测试全过、debug APK 构建成功。
|
||||
`pubspec.lock` 已还原,无依赖变更;未安装或操作真机。
|
||||
+3
-2
@@ -41,13 +41,14 @@ public class AppCatalogController {
|
||||
return Result.ok(appCatalogService.discoverFeed());
|
||||
}
|
||||
|
||||
@Operation(summary = "推荐流:支持内容类型多选与游标翻页")
|
||||
@Operation(summary = "推荐流:支持内容类型、分类过滤与游标翻页")
|
||||
@GetMapping("/feed")
|
||||
public Result<CursorResult<AppDramaCardVO>> feed(
|
||||
@RequestParam(required = false) String types,
|
||||
@RequestParam(required = false) Long categoryId,
|
||||
@RequestParam(required = false) Long cursor,
|
||||
@RequestParam(required = false) Integer size) {
|
||||
return Result.ok(appCatalogService.feed(types, cursor, size));
|
||||
return Result.ok(appCatalogService.feed(types, categoryId, cursor, size));
|
||||
}
|
||||
|
||||
@Operation(summary = "分类列表")
|
||||
|
||||
+4
-2
@@ -74,7 +74,8 @@ public class AppCatalogService {
|
||||
.toList();
|
||||
}
|
||||
|
||||
public CursorResult<AppDramaCardVO> feed(String types, Long cursor, Integer size) {
|
||||
public CursorResult<AppDramaCardVO> feed(
|
||||
String types, Long categoryId, Long cursor, Integer size) {
|
||||
int pageSize = size == null ? DEFAULT_FEED_SIZE : Math.max(1, Math.min(size, MAX_FEED_SIZE));
|
||||
List<String> dramaTypes = types == null
|
||||
? List.of()
|
||||
@@ -82,7 +83,8 @@ public class AppCatalogService {
|
||||
.map(String::trim)
|
||||
.filter(type -> !type.isEmpty())
|
||||
.toList();
|
||||
List<Drama> rows = dramaService.listPublishedFeed(dramaTypes, cursor, pageSize + 1);
|
||||
List<Drama> rows = dramaService.listPublishedFeed(
|
||||
dramaTypes, categoryId, cursor, pageSize + 1);
|
||||
if (rows.isEmpty()) {
|
||||
return CursorResult.empty();
|
||||
}
|
||||
|
||||
+3
-2
@@ -96,11 +96,12 @@ class AppCatalogControllerContractTest {
|
||||
card.setId(12L);
|
||||
card.setTitle("漫剧卡片");
|
||||
card.setDramaType("ANIME");
|
||||
given(catalogService.feed("ANIME", null, 20))
|
||||
given(catalogService.feed("ANIME", 7L, null, 20))
|
||||
.willReturn(new CursorResult<>(List.of(card), 12L, true));
|
||||
|
||||
catalogMvc.perform(get("/app/feed")
|
||||
.param("types", "ANIME")
|
||||
.param("categoryId", "7")
|
||||
.param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
@@ -109,7 +110,7 @@ class AppCatalogControllerContractTest {
|
||||
.andExpect(jsonPath("$.data.nextCursor").value("12"))
|
||||
.andExpect(jsonPath("$.data.hasMore").value(true));
|
||||
|
||||
verify(catalogService).feed("ANIME", null, 20);
|
||||
verify(catalogService).feed("ANIME", 7L, null, 20);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ class AppCommentCountAnonymousSecurityTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
given(commentService.countByDrama(7L)).willReturn(9L);
|
||||
given(catalogService.feed("ANIME", null, 20))
|
||||
given(catalogService.feed("ANIME", null, null, 20))
|
||||
.willReturn(new CursorResult<>(List.of(), null, false));
|
||||
}
|
||||
|
||||
|
||||
+28
-10
@@ -118,7 +118,7 @@ class AppCatalogServiceTest {
|
||||
@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))
|
||||
given(dramaService.listPublishedFeed(List.of("ANIME"), null, null, 21))
|
||||
.willReturn(List.of(anime));
|
||||
given(dramaCategoryService.listBasic()).willReturn(List.of());
|
||||
DramaEpisode preview = episode(11L, 1L, 45L);
|
||||
@@ -126,41 +126,59 @@ class AppCatalogServiceTest {
|
||||
.willReturn(Map.of(1L, preview));
|
||||
given(commentService.countByEpisodes(List.of(11L))).willReturn(Map.of(11L, 3L));
|
||||
|
||||
CursorResult<AppDramaCardVO> result = catalogService.feed("ANIME", null, null);
|
||||
CursorResult<AppDramaCardVO> result = catalogService.feed(
|
||||
"ANIME", null, 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.getList().get(0).getLikeCount()).isEqualTo(45L);
|
||||
assertThat(result.isHasMore()).isFalse();
|
||||
verify(dramaService).listPublishedFeed(List.of("ANIME"), null, 21);
|
||||
verify(dramaService).listPublishedFeed(List.of("ANIME"), null, null, 21);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_parse_multiple_feed_types() {
|
||||
given(dramaService.listPublishedFeed(List.of("ANIME", "LIVE"), null, 11))
|
||||
given(dramaService.listPublishedFeed(
|
||||
List.of("ANIME", "LIVE"), null, null, 11))
|
||||
.willReturn(List.of());
|
||||
|
||||
CursorResult<AppDramaCardVO> result = catalogService.feed("ANIME,LIVE", null, 10);
|
||||
CursorResult<AppDramaCardVO> result = catalogService.feed(
|
||||
"ANIME,LIVE", null, null, 10);
|
||||
|
||||
assertThat(result.getList()).isEmpty();
|
||||
verify(dramaService).listPublishedFeed(List.of("ANIME", "LIVE"), null, 11);
|
||||
verify(dramaService).listPublishedFeed(
|
||||
List.of("ANIME", "LIVE"), null, null, 11);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_filter_feed_by_category_id_only() {
|
||||
given(dramaService.listPublishedFeed(List.of(), 7L, null, 21))
|
||||
.willReturn(List.of());
|
||||
|
||||
CursorResult<AppDramaCardVO> result = catalogService.feed(
|
||||
null, 7L, null, null);
|
||||
|
||||
assertThat(result.getList()).isEmpty();
|
||||
verify(dramaService).listPublishedFeed(List.of(), 7L, null, 21);
|
||||
}
|
||||
|
||||
@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))
|
||||
given(dramaService.listPublishedFeed(List.of(), null, null, 2))
|
||||
.willReturn(List.of(first, second));
|
||||
given(dramaService.listPublishedFeed(List.of(), 1L, 2))
|
||||
given(dramaService.listPublishedFeed(List.of(), null, 1L, 2))
|
||||
.willReturn(List.of(second));
|
||||
given(dramaCategoryService.listBasic()).willReturn(List.of());
|
||||
given(episodeService.firstPlayableByDramas(any())).willReturn(Map.of());
|
||||
given(commentService.countByEpisodes(any())).willReturn(Map.of());
|
||||
|
||||
CursorResult<AppDramaCardVO> firstPage = catalogService.feed(null, null, 1);
|
||||
CursorResult<AppDramaCardVO> secondPage = catalogService.feed(null, firstPage.getNextCursor(), 1);
|
||||
CursorResult<AppDramaCardVO> firstPage = catalogService.feed(
|
||||
null, null, null, 1);
|
||||
CursorResult<AppDramaCardVO> secondPage = catalogService.feed(
|
||||
null, null, firstPage.getNextCursor(), 1);
|
||||
|
||||
assertThat(firstPage.getList()).extracting(AppDramaCardVO::getId).containsExactly(1L);
|
||||
assertThat(firstPage.getNextCursor()).isEqualTo(1L);
|
||||
|
||||
+64
-2
@@ -23,6 +23,7 @@ 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;
|
||||
|
||||
/** K88 App 推荐流的类型过滤与三字段 keyset 游标。 */
|
||||
@@ -60,7 +61,7 @@ class DramaServiceFeedTest {
|
||||
given(dramaMapper.selectList(any())).willReturn(List.of());
|
||||
ArgumentCaptor<QueryWrapper<Drama>> captor = ArgumentCaptor.forClass(QueryWrapper.class);
|
||||
|
||||
dramaService.listPublishedFeed(List.of("ANIME"), 9L, 5);
|
||||
dramaService.listPublishedFeed(List.of("ANIME"), null, 9L, 5);
|
||||
|
||||
verify(dramaMapper).selectList(captor.capture());
|
||||
QueryWrapper<Drama> query = captor.getValue();
|
||||
@@ -86,7 +87,7 @@ class DramaServiceFeedTest {
|
||||
given(dramaMapper.selectList(any())).willReturn(List.of());
|
||||
ArgumentCaptor<QueryWrapper<Drama>> captor = ArgumentCaptor.forClass(QueryWrapper.class);
|
||||
|
||||
dramaService.listPublishedFeed(List.of("LIVE"), 9L, 5);
|
||||
dramaService.listPublishedFeed(List.of("LIVE"), null, 9L, 5);
|
||||
|
||||
verify(dramaMapper).selectList(captor.capture());
|
||||
assertThat(captor.getValue().getSqlSegment())
|
||||
@@ -94,4 +95,65 @@ class DramaServiceFeedTest {
|
||||
.contains("view_count")
|
||||
.contains("id");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void should_apply_category_and_type_tokens_together() {
|
||||
given(dramaMapper.selectList(any())).willReturn(List.of());
|
||||
ArgumentCaptor<QueryWrapper<Drama>> captor = ArgumentCaptor.forClass(QueryWrapper.class);
|
||||
|
||||
dramaService.listPublishedFeed(List.of("ANIME"), 7L, null, 5);
|
||||
|
||||
verify(dramaMapper).selectList(captor.capture());
|
||||
QueryWrapper<Drama> query = captor.getValue();
|
||||
assertThat(query.getSqlSegment())
|
||||
.contains("drama_types")
|
||||
.contains("category_ids")
|
||||
.contains("recommend_weight");
|
||||
assertThat(query.getParamNameValuePairs().values())
|
||||
.contains("%,ANIME,%", "%,7,%");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void should_apply_category_cursor_for_matching_marker() {
|
||||
Drama marker = new Drama();
|
||||
marker.setId(9L);
|
||||
marker.setStatus("published");
|
||||
marker.setDramaType("ANIME");
|
||||
marker.setCategoryId(7L);
|
||||
marker.setCategoryIds("7,8");
|
||||
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(), 7L, 9L, 5);
|
||||
|
||||
verify(dramaMapper).selectList(captor.capture());
|
||||
QueryWrapper<Drama> query = captor.getValue();
|
||||
assertThat(query.getSqlSegment())
|
||||
.contains("category_ids")
|
||||
.contains("recommend_weight")
|
||||
.contains("id");
|
||||
assertThat(query.getParamNameValuePairs().values())
|
||||
.contains("%,7,%", 30, 1200L, 9L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_empty_when_cursor_marker_does_not_match_category() {
|
||||
Drama marker = new Drama();
|
||||
marker.setId(9L);
|
||||
marker.setStatus("published");
|
||||
marker.setDramaType("ANIME");
|
||||
marker.setCategoryId(8L);
|
||||
marker.setCategoryIds("8");
|
||||
given(dramaMapper.selectById(9L)).willReturn(marker);
|
||||
|
||||
List<Drama> rows = dramaService.listPublishedFeed(List.of("ANIME"), 7L, 9L, 5);
|
||||
|
||||
assertThat(rows).isEmpty();
|
||||
verify(dramaMapper, never()).selectList(any());
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -134,8 +134,8 @@ class DramaServiceMultiTagTest {
|
||||
given(dramaMapper.selectList(any())).willReturn(List.of());
|
||||
ArgumentCaptor<QueryWrapper<Drama>> captor = ArgumentCaptor.forClass(QueryWrapper.class);
|
||||
|
||||
dramaService.listPublishedFeed(List.of("ANIME"), 9L, 5);
|
||||
dramaService.listPublishedFeed(List.of("LIVE"), 9L, 5);
|
||||
dramaService.listPublishedFeed(List.of("ANIME"), null, 9L, 5);
|
||||
dramaService.listPublishedFeed(List.of("LIVE"), null, 9L, 5);
|
||||
|
||||
verify(dramaMapper, times(2)).selectList(captor.capture());
|
||||
assertThat(captor.getAllValues()).allSatisfy(query ->
|
||||
|
||||
+13
@@ -99,6 +99,19 @@ final class DramaMultiTagSupport {
|
||||
});
|
||||
}
|
||||
|
||||
static void applyCategoryIdTokens(QueryWrapper<Drama> wrapper,
|
||||
List<Long> categoryIds) {
|
||||
wrapper.and(nested -> {
|
||||
for (int index = 0; index < categoryIds.size(); index++) {
|
||||
if (index > 0) {
|
||||
nested.or();
|
||||
}
|
||||
nested.apply("(',' || category_ids || ',') LIKE {0}",
|
||||
tokenPattern(categoryIds.get(index)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static List<String> parseDramaTypes(Drama drama) {
|
||||
List<String> values = parseStringTokens(drama.getDramaTypes()).stream()
|
||||
.map(DramaMultiTagSupport::normalizeDramaType)
|
||||
|
||||
+8
-2
@@ -237,20 +237,26 @@ public class DramaService {
|
||||
* App 推荐流:按推荐权重、热度、ID 倒序,并支持内容类型多选。
|
||||
* cursor 使用上一页最后一条 ID,服务端反查排序键后做 keyset 查询。
|
||||
*/
|
||||
public List<Drama> listPublishedFeed(List<String> dramaTypes, Long cursor, int limit) {
|
||||
public List<Drama> listPublishedFeed(
|
||||
List<String> dramaTypes, Long categoryId, Long cursor, int limit) {
|
||||
List<String> normalizedTypes = DramaMultiTagSupport.normalizeDramaTypes(dramaTypes);
|
||||
QueryWrapper<Drama> query = new QueryWrapper<>();
|
||||
query.eq("status", ContentStatus.PUBLISHED.getCode());
|
||||
if (!normalizedTypes.isEmpty()) {
|
||||
DramaMultiTagSupport.applyDramaTypeTokens(query, normalizedTypes);
|
||||
}
|
||||
if (categoryId != null) {
|
||||
DramaMultiTagSupport.applyCategoryIdTokens(query, List.of(categoryId));
|
||||
}
|
||||
if (cursor != null) {
|
||||
Drama marker = dramaMapper.selectById(cursor);
|
||||
if (marker == null
|
||||
|| !ContentStatus.PUBLISHED.getCode().equals(marker.getStatus())
|
||||
|| (!normalizedTypes.isEmpty()
|
||||
&& DramaMultiTagSupport.parseDramaTypes(marker).stream()
|
||||
.noneMatch(normalizedTypes::contains))) {
|
||||
.noneMatch(normalizedTypes::contains))
|
||||
|| (categoryId != null
|
||||
&& !DramaMultiTagSupport.parseCategoryIds(marker).contains(categoryId))) {
|
||||
return List.of();
|
||||
}
|
||||
applyFeedCursor(query, marker);
|
||||
|
||||
Reference in new issue
Block a user