From 2367a5d12ba60a9dfc5582e993ede4de8b3cf74c Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Sat, 24 Oct 2026 16:53:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(app):=20=E5=85=A8=E9=83=A8=E7=9F=AD?= =?UTF-8?q?=E5=89=A7=E6=94=B9=E4=B8=BA=E8=AF=B7=E6=B1=82=E4=BD=93=E5=88=86?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/controller/AppCatalogController.java | 49 ++++- .../app/service/AppCatalogService.java | 205 +++++++++++++++--- .../com/fruitshorts/app/vo/AppCategoryVO.java | 12 +- .../fruitshorts/app/vo/AppDramaCardVO.java | 16 +- .../fruitshorts/app/vo/AppDramaDetailVO.java | 10 +- .../fruitshorts/app/vo/AppDramaPageReqVO.java | 21 ++ .../app/vo/AppPlaylistDetailVO.java | 9 +- .../com/fruitshorts/app/vo/AppSectionVO.java | 16 +- .../AppCatalogControllerContractTest.java | 32 ++- .../app/service/AppCatalogServiceTest.java | 3 +- .../DramaServiceFavoriteCountTest.java | 2 + .../app/service/DramaServiceFeedTest.java | 2 + .../app/service/DramaServiceK88Test.java | 2 + .../app/service/DramaServiceMultiTagTest.java | 2 + .../framework/security/SecurityConfig.java | 2 + 15 files changed, 322 insertions(+), 61 deletions(-) create mode 100644 fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaPageReqVO.java diff --git a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/controller/AppCatalogController.java b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/controller/AppCatalogController.java index 11f9b6a..ce23d55 100644 --- a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/controller/AppCatalogController.java +++ b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/controller/AppCatalogController.java @@ -4,6 +4,7 @@ package com.fruitshorts.app.controller; import com.fruitshorts.app.service.AppCatalogService; import com.fruitshorts.app.vo.AppCategoryVO; +import com.fruitshorts.app.vo.AppDramaPageReqVO; import com.fruitshorts.app.vo.AppFeedVO; import com.fruitshorts.app.vo.AppDramaCardVO; import com.fruitshorts.app.vo.AppPlaylistDetailVO; @@ -16,6 +17,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -30,6 +33,9 @@ import java.util.List; @RequiredArgsConstructor public class AppCatalogController { + private static final int DRAMA_PAGE_INDEX = 1; + private static final int DRAMA_PAGE_SIZE = 20; + private final AppCatalogService appCatalogService; @Operation(summary = "首页聚合:轮播 + 分区,一次请求渲染整页") @@ -69,9 +75,46 @@ public class AppCatalogController { return Result.ok(appCatalogService.categoryDramas(id, pageIndex, pageSize)); } - @Operation(summary = "剧单详情:剧单基本信息 + 该剧单下已上线短剧卡片") + @Operation(summary = "已启用类型") + @GetMapping("/drama-categories") + public Result> dramaCategories() { + return Result.ok(appCatalogService.dramaCategories()); + } + + @Operation(summary = "已启用标签") + @GetMapping("/tags") + public Result> tags() { + return Result.ok(appCatalogService.tags()); + } + + /** + * 全部短剧分页。类型、分类、标签放在请求体,避免长 id 进入查询串。 + * 同一组多选为或,不同组同时传入为且。 + */ + @Operation(summary = "全部短剧:类型、分类、标签多选,分页") + @PostMapping("/dramas/page") + public Result> dramas(@RequestBody(required = false) AppDramaPageReqVO req) { + AppDramaPageReqVO body = req == null ? new AppDramaPageReqVO() : req; + int pageIndex = body.getPageIndex() == null ? DRAMA_PAGE_INDEX : body.getPageIndex(); + int pageSize = body.getPageSize() == null ? DRAMA_PAGE_SIZE : body.getPageSize(); + return Result.ok(appCatalogService.libraryDramas( + body.getCategoryIds(), body.getClassificationIds(), body.getTagIds(), pageIndex, pageSize)); + } + + @Operation(summary = "剧单列表:已启用剧单及其已上线短剧卡片,分页") + @GetMapping("/playlists") + public Result> playlists( + @RequestParam(defaultValue = "1") int pageIndex, + @RequestParam(defaultValue = "10") int pageSize) { + return Result.ok(appCatalogService.playlists(pageIndex, pageSize)); + } + + @Operation(summary = "剧单详情:剧单基本信息 + 该页已上线短剧卡片") @GetMapping("/playlists/{id}") - public Result playlistDetail(@PathVariable Long id) { - return Result.ok(appCatalogService.playlistDetail(id)); + public Result playlistDetail( + @PathVariable Long id, + @RequestParam(defaultValue = "1") int pageIndex, + @RequestParam(defaultValue = "20") int pageSize) { + return Result.ok(appCatalogService.playlistDetail(id, pageIndex, pageSize)); } } diff --git a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/service/AppCatalogService.java b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/service/AppCatalogService.java index 580747e..9249237 100644 --- a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/service/AppCatalogService.java +++ b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/service/AppCatalogService.java @@ -13,10 +13,13 @@ import com.fruitshorts.app.vo.AppSectionVO; import com.fruitshorts.common.pojo.CursorResult; import com.fruitshorts.common.pojo.PageResult; import com.fruitshorts.content.dal.entity.ContentBanner; +import com.fruitshorts.content.dal.entity.ContentTag; import com.fruitshorts.content.dal.entity.Drama; import com.fruitshorts.content.dal.entity.DramaEpisode; import com.fruitshorts.content.enums.BannerPosition; import com.fruitshorts.content.service.BannerService; +import com.fruitshorts.content.service.ContentLocaleTexts; +import com.fruitshorts.content.service.ContentTagService; import com.fruitshorts.content.service.DramaCategoryService; import com.fruitshorts.content.service.DramaClassificationService; import com.fruitshorts.content.service.DramaPlaylistCategoryService; @@ -31,6 +34,7 @@ import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Arrays; @@ -48,11 +52,17 @@ public class AppCatalogService { private static final int MAX_CATEGORY_SECTIONS = 4; private static final int DEFAULT_FEED_SIZE = 20; private static final int MAX_FEED_SIZE = 50; + private static final int DEFAULT_PLAYLIST_PAGE_SIZE = 10; + private static final int MAX_PLAYLIST_PAGE_SIZE = 20; + private static final int DEFAULT_PLAYLIST_DRAMA_PAGE_SIZE = 20; + private static final int MAX_PLAYLIST_DRAMA_PAGE_SIZE = 50; + private static final int PLAYLIST_DRAMA_PREVIEW_SIZE = 10; private final DramaService dramaService; private final EpisodeService episodeService; private final BannerService bannerService; private final DramaCategoryService dramaCategoryService; + private final ContentTagService contentTagService; private final DramaClassificationService dramaClassificationService; private final DramaPlaylistCategoryService dramaPlaylistCategoryService; private final PlaybackSigner playbackSigner; @@ -60,28 +70,65 @@ public class AppCatalogService { private final ThumbnailUrlResolver thumbnailUrlResolver; public AppFeedVO homeFeed() { - Map categoryNames = categoryNameMap(); + Map> categoryLocales = categoryLocaleMap(); List sections = new ArrayList<>(); addIfNotEmpty(sections, "hot", "热门推荐", - dramaService.listPublishedHot(HOME_SECTION_SIZE), categoryNames); + dramaService.listPublishedHot(HOME_SECTION_SIZE), categoryLocales); addIfNotEmpty(sections, "new", "新剧上线", - dramaService.listPublishedLatest(HOME_SECTION_SIZE), categoryNames); - sections.addAll(categorySections(HOME_SECTION_SIZE, categoryNames)); + dramaService.listPublishedLatest(HOME_SECTION_SIZE), categoryLocales); + sections.addAll(categorySections(HOME_SECTION_SIZE, categoryLocales)); return new AppFeedVO(banners(BannerPosition.HOME_TOP.name()), sections); } public AppFeedVO discoverFeed() { return new AppFeedVO(banners(BannerPosition.DISCOVER.name()), - categorySections(DISCOVER_SECTION_SIZE, categoryNameMap())); + categorySections(DISCOVER_SECTION_SIZE, categoryLocaleMap())); } /** 分类频道:运营自定义的精选/新剧/榜单等(drama_classification)。 */ public List categories() { return dramaClassificationService.listEnabled().stream() - .map(c -> new AppCategoryVO(c.getId(), c.getName())) + .map(c -> { + Map locales = ContentLocaleTexts.read( + c.getNameLocales(), c.getName(), c.getNameEn(), c.getNameId()); + AppCategoryVO vo = new AppCategoryVO(c.getId(), ContentLocaleTexts.value(locales, ContentLocaleTexts.ZH)); + vo.setLocales(locales); + return vo; + }) .toList(); } + /** 已启用类型,名称只放 locales。 */ + public List dramaCategories() { + return dramaCategoryService.list().stream() + .filter(item -> item.getStatus() != null && item.getStatus() == 1) + .map(item -> option(item.getId(), item.getName(), item.getLocales())) + .toList(); + } + + /** 已启用标签,名称只放 locales。 */ + public List tags() { + return contentTagService.listEnabled().stream() + .map(item -> option(item.getId(), item.getName(), item.getLocales())) + .toList(); + } + + /** 全部短剧分页。同一行多选是或,不同类型之间是且。 */ + public PageResult libraryDramas(List categoryIds, List classificationIds, + List tagIds, int pageIndex, int pageSize) { + PageResult page = dramaService.pagePublishedLibrary( + categoryIds, classificationIds, tagIds, pageIndex, pageSize); + return new PageResult<>(page.getTotal(), toCards(page.getList())); + } + + private AppCategoryVO option(Long id, String name, Map locales) { + AppCategoryVO vo = new AppCategoryVO(id, name); + if (locales != null && !locales.isEmpty()) { + vo.setLocales(locales); + } + return vo; + } + /** 分类频道:按分类分页查已上线短剧卡片(通过分类关联表过滤)。 */ public PageResult categoryDramas(Long classificationId, int pageIndex, int pageSize) { @@ -108,7 +155,7 @@ public class AppCatalogService { } int toIndex = Math.min(fromIndex + safeSize, ordered.size()); List pageRows = ordered.subList(fromIndex, toIndex); - return new PageResult<>(total, cards(pageRows, categoryNameMap())); + return new PageResult<>(total, cards(pageRows, categoryLocaleMap())); } public CursorResult feed( @@ -128,7 +175,7 @@ public class AppCatalogService { boolean hasMore = rows.size() > pageSize; List pageRows = hasMore ? rows.subList(0, pageSize) : rows; Long nextCursor = hasMore ? pageRows.get(pageRows.size() - 1).getId() : null; - return new CursorResult<>(cards(pageRows, categoryNameMap()), nextCursor, hasMore); + return new CursorResult<>(cards(pageRows, categoryLocaleMap()), nextCursor, hasMore); } /** @@ -139,7 +186,7 @@ public class AppCatalogService { if (dramas == null || dramas.isEmpty()) { return List.of(); } - return cards(dramas, categoryNameMap()); + return cards(dramas, categoryLocaleMap()); } public AppDramaDetailVO dramaDetail(Long dramaId) { @@ -149,15 +196,18 @@ public class AppCatalogService { episodes.stream().map(DramaEpisode::getId).sorted().toList()); Map thumbnailUrls = thumbnailUrlResolver.resolveAll( episodes.stream().map(DramaEpisode::getId).sorted().toList()); - String categoryName = categoryNameMap().getOrDefault(drama.getCategoryId(), ""); + List> types = dramaCategoryService.localesForDrama(drama, categoryLocaleMap()); AppDramaDetailVO detail = new AppDramaDetailVO(); detail.setId(drama.getId()); - detail.setTitle(drama.getTitle()); - detail.setSynopsis(drama.getSynopsis()); + Map titles = ContentLocaleTexts.read(drama.getTitleLocales(), drama.getTitle(), null, null); + Map synopses = ContentLocaleTexts.read( + drama.getSynopsisLocales(), drama.getSynopsis(), null, null); + detail.setTitleLocales(titles); + detail.setSynopsisLocales(synopses); detail.setCover(drama.getCoverUrl()); - detail.setGenre(drama.getGenre()); - detail.setCategory(categoryName); + detail.setTypes(types); + fillDetailTag(detail, drama); detail.setTotalEpisodes(drama.getTotalEpisodes()); detail.setFreeEpisodes(drama.getFreeEpisodes()); detail.setUnlockCoin(drama.getUnlockCoin()); @@ -172,8 +222,27 @@ public class AppCatalogService { return detail; } - /** C 端剧单详情:剧单基本信息 + 该剧单下所有已上线短剧卡片。 */ - public AppPlaylistDetailVO playlistDetail(Long playlistId) { + /** C 端剧单列表:按页返回已启用剧单,每条只含该页剧单的已上线短剧。 */ + public PageResult playlists(int pageIndex, int pageSize) { + var all = dramaPlaylistCategoryService.listEnabled(); + int safeIndex = Math.max(1, pageIndex); + int safeSize = pageSize <= 0 + ? DEFAULT_PLAYLIST_PAGE_SIZE + : Math.min(pageSize, MAX_PLAYLIST_PAGE_SIZE); + int fromIndex = (safeIndex - 1) * safeSize; + if (fromIndex >= all.size()) { + return new PageResult<>(all.size(), List.of()); + } + int toIndex = Math.min(fromIndex + safeSize, all.size()); + List page = new ArrayList<>(); + for (var category : all.subList(fromIndex, toIndex)) { + page.add(playlistDetail(category.getId(), 1, PLAYLIST_DRAMA_PREVIEW_SIZE)); + } + return new PageResult<>(all.size(), page); + } + + /** C 端剧单详情:剧单基本信息 + 该页已上线短剧卡片。 */ + public AppPlaylistDetailVO playlistDetail(Long playlistId, int pageIndex, int pageSize) { var category = dramaPlaylistCategoryService.getEnabledById(playlistId); List allIds = dramaPlaylistCategoryService.listDramaIdsByCategory(playlistId); List published = dramaService.listPublishedByIds(allIds); @@ -190,12 +259,27 @@ public class AppCatalogService { }) .toList(); + int safeIndex = Math.max(1, pageIndex); + int safeSize = pageSize <= 0 + ? DEFAULT_PLAYLIST_DRAMA_PAGE_SIZE + : Math.min(pageSize, MAX_PLAYLIST_DRAMA_PAGE_SIZE); + int from = (safeIndex - 1) * safeSize; + List pageRows = from >= ordered.size() + ? List.of() + : ordered.subList(from, Math.min(from + safeSize, ordered.size())); + Map locales = ContentLocaleTexts.read( + category.getNameLocales(), category.getName(), category.getNameEn(), category.getNameId()); + Map descriptions = ContentLocaleTexts.read( + category.getDescriptionLocales(), category.getDescription(), + category.getDescriptionEn(), category.getDescriptionId()); AppPlaylistDetailVO vo = new AppPlaylistDetailVO(); vo.setId(category.getId()); - vo.setName(category.getName()); - vo.setDescription(category.getDescription()); + vo.setLocales(locales); + vo.setDescriptionLocales(descriptions); vo.setDramaCount(ordered.size()); - vo.setList(cards(ordered, categoryNameMap())); + vo.setPageIndex(safeIndex); + vo.setPageSize(safeSize); + vo.setList(cards(pageRows, categoryLocaleMap())); return vo; } @@ -203,7 +287,7 @@ public class AppCatalogService { return bannerService.listActive(position).stream().map(this::toBanner).toList(); } - private List categorySections(int size, Map categoryNames) { + private List categorySections(int size, Map> categoryLocales) { List sections = new ArrayList<>(); for (DramaCategoryRespVO category : dramaCategoryService.listBasic()) { if (sections.size() >= MAX_CATEGORY_SECTIONS) { @@ -213,17 +297,17 @@ public class AppCatalogService { if (!dramas.isEmpty()) { sections.add(new AppSectionVO( "category-" + category.getId(), - category.getName(), - cards(dramas, categoryNames))); + category.getLocales(), + cards(dramas, categoryLocales))); } } return sections; } private void addIfNotEmpty(List sections, String code, String title, - List dramas, Map categoryNames) { + List dramas, Map> categoryLocales) { if (!dramas.isEmpty()) { - sections.add(new AppSectionVO(code, title, cards(dramas, categoryNames))); + sections.add(new AppSectionVO(code, title, cards(dramas, categoryLocales))); } } @@ -237,30 +321,36 @@ public class AppCatalogService { } /** 整屏卡片共用一次剧集查询,取每部剧的首个可播免费集作为预览。 */ - private List cards(List dramas, Map categoryNames) { + private List cards(List dramas, Map> categoryLocales) { List ids = dramas.stream().map(Drama::getId).toList(); Map previews = episodeService.firstPlayableByDramas(ids); Map commentCounts = commentService.countByEpisodes( previews.values().stream().map(DramaEpisode::getId).sorted().toList()); Map thumbnailUrls = thumbnailUrlResolver.resolveAll( previews.values().stream().map(DramaEpisode::getId).sorted().toList()); + Map tags = loadTags(dramas); return dramas.stream() .map(drama -> toCard( - drama, categoryNames, previews.get(drama.getId()), commentCounts, + drama, categoryLocales, tags, previews.get(drama.getId()), commentCounts, thumbnailUrls)) .toList(); } - private AppDramaCardVO toCard(Drama drama, Map categoryNames, + private AppDramaCardVO toCard(Drama drama, Map> categoryLocales, + Map tags, DramaEpisode preview, Map commentCounts, Map thumbnailUrls) { + List> types = dramaCategoryService.localesForDrama(drama, categoryLocales); AppDramaCardVO card = new AppDramaCardVO(); card.setId(drama.getId()); - card.setTitle(drama.getTitle()); + Map titles = ContentLocaleTexts.read(drama.getTitleLocales(), drama.getTitle(), null, null); + Map synopses = ContentLocaleTexts.read( + drama.getSynopsisLocales(), drama.getSynopsis(), null, null); + card.setTitleLocales(titles); card.setCover(drama.getCoverUrl()); - card.setSynopsis(shortSynopsis(drama.getSynopsis())); - card.setGenre(drama.getGenre()); - card.setCategory(categoryNames.getOrDefault(drama.getCategoryId(), "")); + card.setSynopsisLocales(shortLocales(synopses)); + card.setTypes(types); + fillTag(card, drama, tags); card.setTotalEpisodes(drama.getTotalEpisodes()); card.setIsHot(drama.getIsHot() != null && drama.getIsHot() == 1); card.setViewCount(drama.getViewCount()); @@ -307,13 +397,58 @@ public class AppCatalogService { return episode.getVideoUrl() == null ? "" : episode.getVideoUrl(); } - private Map categoryNameMap() { - Map names = new HashMap<>(); - dramaCategoryService.listBasic().forEach(category -> names.put(category.getId(), category.getName())); - return names; + private Map loadTags(List dramas) { + List ids = dramas.stream() + .map(Drama::getTagId) + .filter(id -> id != null && id > 0L) + .distinct() + .toList(); + Map tags = contentTagService.findByIds(ids); + return tags == null ? Map.of() : tags; + } + + private void fillTag(AppDramaCardVO card, Drama drama, Map tags) { + ContentTag tag = tagOf(drama, tags); + Map locales = tagLocales(tag); + card.setTagLocales(locales); + card.setTagColor(tag == null ? "" : tag.getBgColor()); + } + + private void fillDetailTag(AppDramaDetailVO detail, Drama drama) { + ContentTag tag = tagOf(drama, loadTags(List.of(drama))); + Map locales = tagLocales(tag); + detail.setTagLocales(locales); + detail.setTagColor(tag == null ? "" : tag.getBgColor()); + } + + private ContentTag tagOf(Drama drama, Map tags) { + if (drama.getTagId() == null || drama.getTagId() <= 0L) { + return null; + } + return tags.get(drama.getTagId()); + } + + private Map tagLocales(ContentTag tag) { + if (tag == null) { + return Map.of(); + } + return ContentLocaleTexts.read(tag.getNameLocales(), tag.getName(), tag.getNameEn(), tag.getNameId()); + } + + private Map> categoryLocaleMap() { + return dramaCategoryService.localeMap(); } /** 卡片只带一句简介,避免列表响应体过大 */ + private Map shortLocales(Map locales) { + Map result = new LinkedHashMap<>(); + if (locales == null) { + return result; + } + locales.forEach((code, text) -> result.put(code, shortSynopsis(text))); + return result; + } + private String shortSynopsis(String synopsis) { if (synopsis == null) { return ""; diff --git a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppCategoryVO.java b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppCategoryVO.java index 62c1c50..d27d99c 100644 --- a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppCategoryVO.java +++ b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppCategoryVO.java @@ -2,13 +2,19 @@ package com.fruitshorts.app.vo; -import lombok.AllArgsConstructor; import lombok.Data; +import java.util.Map; + +/** 首页分类。名称只放在 locales。 */ @Data -@AllArgsConstructor public class AppCategoryVO { private Long id; - private String name; + private Map locales; + + public AppCategoryVO(Long id, String name) { + this.id = id; + this.locales = name == null || name.isBlank() ? Map.of() : Map.of("zh", name); + } } diff --git a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaCardVO.java b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaCardVO.java index 4a9d080..819e888 100644 --- a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaCardVO.java +++ b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaCardVO.java @@ -5,20 +5,26 @@ package com.fruitshorts.app.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.util.List; +import java.util.Map; + /** 列表卡片,用于首页/发现页/分类分区 */ @Data public class AppDramaCardVO { private Long id; - private String title; + private Map titleLocales; private String cover; - private String synopsis; - private String genre; - private String category; + private Map synopsisLocales; + /** 短剧选中的全部类型,每项是一份语言表。 */ + private List> types; + /** 标签语言表,key 为 zh / en / id。 */ + private Map tagLocales; + /** 标签角标背景色,#RRGGBB。 */ + private String tagColor; private Integer totalEpisodes; private Boolean isHot; private Long viewCount; - /** 短剧累计点赞数:drama.like_count */ private Long likeCount; /** 短剧累计收藏数:drama.favorite_count */ private Long favoriteCount; diff --git a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaDetailVO.java b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaDetailVO.java index 9992c04..f4503e5 100644 --- a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaDetailVO.java +++ b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaDetailVO.java @@ -5,16 +5,18 @@ package com.fruitshorts.app.vo; import lombok.Data; import java.util.List; +import java.util.Map; @Data public class AppDramaDetailVO { private Long id; - private String title; - private String synopsis; + private Map titleLocales; + private Map synopsisLocales; private String cover; - private String genre; - private String category; + private List> types; + private Map tagLocales; + private String tagColor; private Integer totalEpisodes; private Integer freeEpisodes; private Integer unlockCoin; diff --git a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaPageReqVO.java b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaPageReqVO.java new file mode 100644 index 0000000..f9be8d5 --- /dev/null +++ b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaPageReqVO.java @@ -0,0 +1,21 @@ +// Author: fruit.com.cn + +package com.fruitshorts.app.vo; + +import lombok.Data; + +import java.util.List; + +/** 全部短剧分页筛选。多选 id 放在请求体里,避免查询串过长。 */ +@Data +public class AppDramaPageReqVO { + + private Integer pageIndex; + private Integer pageSize; + /** 类型 id,多选为或。 */ + private List categoryIds; + /** 分类 id,多选为或。 */ + private List classificationIds; + /** 标签 id,多选为或。 */ + private List tagIds; +} diff --git a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppPlaylistDetailVO.java b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppPlaylistDetailVO.java index acc89b6..ef3f6e3 100644 --- a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppPlaylistDetailVO.java +++ b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppPlaylistDetailVO.java @@ -5,15 +5,20 @@ package com.fruitshorts.app.vo; import lombok.Data; import java.util.List; +import java.util.Map; /** C 端剧单详情:基本信息 + 该剧单下已上线的短剧卡片。 */ @Data public class AppPlaylistDetailVO { private Long id; - private String name; - private String description; + private Map locales; + private Map descriptionLocales; /** 该剧单下已上线短剧的实际条数(被下架/下线的不计入) */ private Integer dramaCount; + /** 当前短剧页码,从 1 开始 */ + private Integer pageIndex; + /** 当前短剧页大小 */ + private Integer pageSize; private List list; } diff --git a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppSectionVO.java b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppSectionVO.java index 86bc787..ca1653b 100644 --- a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppSectionVO.java +++ b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppSectionVO.java @@ -2,18 +2,28 @@ package com.fruitshorts.app.vo; -import lombok.AllArgsConstructor; +import com.fruitshorts.content.service.ContentLocaleTexts; import lombok.Data; import java.util.List; +import java.util.Map; /** 首页/发现页的一个横滑分区 */ @Data -@AllArgsConstructor public class AppSectionVO { /** 分区标识:hot / new / category-{id},客户端可据此做埋点与样式 */ private String code; - private String title; + private Map titleLocales; private List dramas; + + public AppSectionVO(String code, String title, List dramas) { + this(code, Map.of(ContentLocaleTexts.ZH, title == null ? "" : title), dramas); + } + + public AppSectionVO(String code, Map titleLocales, List dramas) { + this.code = code; + this.titleLocales = titleLocales; + this.dramas = dramas; + } } diff --git a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/controller/AppCatalogControllerContractTest.java b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/controller/AppCatalogControllerContractTest.java index 4d8d844..b1793ee 100644 --- a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/controller/AppCatalogControllerContractTest.java +++ b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/controller/AppCatalogControllerContractTest.java @@ -17,15 +17,18 @@ import com.fruitshorts.playback.service.PlaybackService; import com.fruitshorts.interaction.service.CommentService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import java.util.List; +import java.util.Map; import static org.hamcrest.Matchers.nullValue; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; 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.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -58,7 +61,7 @@ class AppCatalogControllerContractTest { banner.setLinkValue("12"); AppDramaCardVO card = new AppDramaCardVO(); card.setId(12L); - card.setTitle("命运的相遇"); + card.setTitleLocales(Map.of("zh", "命运的相遇")); card.setViewCount(12400L); card.setLikeCount(3200L); card.setFavoriteCount(1800L); @@ -94,7 +97,7 @@ class AppCatalogControllerContractTest { void should_return_typed_cursor_feed() throws Exception { AppDramaCardVO card = new AppDramaCardVO(); card.setId(12L); - card.setTitle("漫剧卡片"); + card.setTitleLocales(Map.of("zh", "漫剧卡片")); card.setDramaType("ANIME"); given(catalogService.feed("ANIME", 7L, null, 20)) .willReturn(new CursorResult<>(List.of(card), 12L, true)); @@ -122,14 +125,14 @@ class AppCatalogControllerContractTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(0)) .andExpect(jsonPath("$.data[0].id").value(3)) - .andExpect(jsonPath("$.data[0].name").value("都市")); + .andExpect(jsonPath("$.data[0].locales.zh").value("都市")); } @Test void should_bind_path_variable_when_drama_detail_is_requested() throws Exception { AppDramaDetailVO detail = new AppDramaDetailVO(); detail.setId(42L); - detail.setTitle("命运的相遇"); + detail.setTitleLocales(Map.of("zh", "命运的相遇")); detail.setViewCount(12400L); detail.setLikeCount(3200L); detail.setFavoriteCount(1800L); @@ -142,7 +145,7 @@ class AppCatalogControllerContractTest { .andExpect(jsonPath("$.code").value(0)) .andExpect(jsonPath("$.data.id").value("42")) .andExpect(jsonPath("$.data.id").isString()) - .andExpect(jsonPath("$.data.title").value("命运的相遇")) + .andExpect(jsonPath("$.data.titleLocales.zh").value("命运的相遇")) .andExpect(jsonPath("$.data.viewCount").value("12400")) .andExpect(jsonPath("$.data.likeCount").value("3200")) .andExpect(jsonPath("$.data.favoriteCount").value("1800")) @@ -161,4 +164,23 @@ class AppCatalogControllerContractTest { .andExpect(jsonPath("$.code").value(ErrorCode.DATA_NOT_FOUND.getCode())) .andExpect(jsonPath("$.data").value(nullValue())); } + + @Test + void should_bind_filter_ids_from_body_when_drama_page_is_posted() throws Exception { + AppDramaCardVO card = new AppDramaCardVO(); + card.setId(9L); + card.setTitleLocales(Map.of("zh", "重生")); + given(catalogService.libraryDramas(List.of(2L), List.of(3L), List.of(4L), 1, 20)) + .willReturn(new com.fruitshorts.common.pojo.PageResult<>(1L, List.of(card))); + + catalogMvc.perform(post("/app/dramas/page") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"pageIndex\":1,\"pageSize\":20," + + "\"categoryIds\":[\"2\"],\"classificationIds\":[\"3\"],\"tagIds\":[\"4\"]}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.total").value("1")) + .andExpect(jsonPath("$.data.list[0].titleLocales.zh").value("重生")); + verify(catalogService).libraryDramas(List.of(2L), List.of(3L), List.of(4L), 1, 20); + } } diff --git a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/AppCatalogServiceTest.java b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/AppCatalogServiceTest.java index 745bc2d..a734b20 100644 --- a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/AppCatalogServiceTest.java +++ b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/AppCatalogServiceTest.java @@ -7,6 +7,7 @@ import com.fruitshorts.common.pojo.CursorResult; import com.fruitshorts.content.dal.entity.Drama; import com.fruitshorts.content.dal.entity.DramaEpisode; import com.fruitshorts.content.service.BannerService; +import com.fruitshorts.content.service.ContentTagService; import com.fruitshorts.content.service.DramaCategoryService; import com.fruitshorts.content.service.DramaClassificationService; import com.fruitshorts.content.service.DramaPlaylistCategoryService; @@ -54,7 +55,7 @@ class AppCatalogServiceTest { given(thumbnailUrlResolver.resolveAll(any())).willReturn(Map.of()); commentService = mock(CommentService.class); catalogService = new AppCatalogService(dramaService, episodeService, bannerService, - dramaCategoryService, dramaClassificationService, dramaPlaylistCategoryService, + dramaCategoryService, mock(ContentTagService.class), dramaClassificationService, dramaPlaylistCategoryService, playbackSigner, commentService, thumbnailUrlResolver); } diff --git a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/DramaServiceFavoriteCountTest.java b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/DramaServiceFavoriteCountTest.java index d1c1fb0..681227e 100644 --- a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/DramaServiceFavoriteCountTest.java +++ b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/DramaServiceFavoriteCountTest.java @@ -43,6 +43,8 @@ class DramaServiceFavoriteCountTest { mock(DramaEpisodeMapper.class), mock(DramaReviewRecordMapper.class), mock(DramaCategoryService.class), + mock(com.fruitshorts.content.dal.mapper.ContentTagMapper.class), + mock(com.fruitshorts.content.service.ContentTagService.class), mock(EpisodePriceSync.class)); given(dramaMapper.update(isNull(), any())).willReturn(1); ArgumentCaptor> captor = ArgumentCaptor.forClass(Wrapper.class); diff --git a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/DramaServiceFeedTest.java b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/DramaServiceFeedTest.java index 5e4111b..f2f975a 100644 --- a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/DramaServiceFeedTest.java +++ b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/DramaServiceFeedTest.java @@ -47,6 +47,8 @@ class DramaServiceFeedTest { mock(DramaEpisodeMapper.class), mock(DramaReviewRecordMapper.class), mock(DramaCategoryService.class), + mock(com.fruitshorts.content.dal.mapper.ContentTagMapper.class), + mock(com.fruitshorts.content.service.ContentTagService.class), mock(EpisodePriceSync.class)); } diff --git a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/DramaServiceK88Test.java b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/DramaServiceK88Test.java index 6287c06..1829714 100644 --- a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/DramaServiceK88Test.java +++ b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/DramaServiceK88Test.java @@ -48,6 +48,8 @@ class DramaServiceK88Test { mock(DramaEpisodeMapper.class), mock(DramaReviewRecordMapper.class), mock(DramaCategoryService.class), + mock(com.fruitshorts.content.dal.mapper.ContentTagMapper.class), + mock(com.fruitshorts.content.service.ContentTagService.class), mock(EpisodePriceSync.class)); } diff --git a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/DramaServiceMultiTagTest.java b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/DramaServiceMultiTagTest.java index 7d6c712..4ed2ef0 100644 --- a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/DramaServiceMultiTagTest.java +++ b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/DramaServiceMultiTagTest.java @@ -56,6 +56,8 @@ class DramaServiceMultiTagTest { mock(DramaEpisodeMapper.class), mock(DramaReviewRecordMapper.class), categoryService, + mock(com.fruitshorts.content.dal.mapper.ContentTagMapper.class), + mock(com.fruitshorts.content.service.ContentTagService.class), mock(EpisodePriceSync.class)); } diff --git a/fruitshorts-framework/src/main/java/com/fruitshorts/framework/security/SecurityConfig.java b/fruitshorts-framework/src/main/java/com/fruitshorts/framework/security/SecurityConfig.java index 6422ebe..3c8f04f 100644 --- a/fruitshorts-framework/src/main/java/com/fruitshorts/framework/security/SecurityConfig.java +++ b/fruitshorts-framework/src/main/java/com/fruitshorts/framework/security/SecurityConfig.java @@ -38,6 +38,8 @@ public class SecurityConfig { "/app/discover/**", "/app/feed", "/app/categories/**", + "/app/drama-categories", + "/app/tags", "/app/playlists/**", "/app/banners/**", "/app/dramas/**",