feat(api): 加C端分类频道和剧单详情接口
This commit is contained in:
1 parent
4f80a691d1
commit
45cf405285
8 files changed
+178
-4
No files matched your search
+18
@@ -6,12 +6,15 @@ import com.fruitshorts.app.service.AppCatalogService;
|
||||
import com.fruitshorts.app.vo.AppCategoryVO;
|
||||
import com.fruitshorts.app.vo.AppFeedVO;
|
||||
import com.fruitshorts.app.vo.AppDramaCardVO;
|
||||
import com.fruitshorts.app.vo.AppPlaylistDetailVO;
|
||||
import com.fruitshorts.common.pojo.CursorResult;
|
||||
import com.fruitshorts.common.pojo.PageResult;
|
||||
import com.fruitshorts.common.pojo.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -56,4 +59,19 @@ public class AppCatalogController {
|
||||
public Result<List<AppCategoryVO>> categories() {
|
||||
return Result.ok(appCatalogService.categories());
|
||||
}
|
||||
|
||||
@Operation(summary = "分类频道:按分类 ID 分页查短剧卡片")
|
||||
@GetMapping("/categories/{id}/dramas")
|
||||
public Result<PageResult<AppDramaCardVO>> categoryDramas(
|
||||
@PathVariable Long id,
|
||||
@RequestParam(defaultValue = "1") int pageIndex,
|
||||
@RequestParam(defaultValue = "20") int pageSize) {
|
||||
return Result.ok(appCatalogService.categoryDramas(id, pageIndex, pageSize));
|
||||
}
|
||||
|
||||
@Operation(summary = "剧单详情:剧单基本信息 + 该剧单下已上线短剧卡片")
|
||||
@GetMapping("/playlists/{id}")
|
||||
public Result<AppPlaylistDetailVO> playlistDetail(@PathVariable Long id) {
|
||||
return Result.ok(appCatalogService.playlistDetail(id));
|
||||
}
|
||||
}
|
||||
+65
-3
@@ -8,14 +8,18 @@ import com.fruitshorts.app.vo.AppDramaCardVO;
|
||||
import com.fruitshorts.app.vo.AppDramaDetailVO;
|
||||
import com.fruitshorts.app.vo.AppEpisodeVO;
|
||||
import com.fruitshorts.app.vo.AppFeedVO;
|
||||
import com.fruitshorts.app.vo.AppPlaylistDetailVO;
|
||||
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.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.DramaCategoryService;
|
||||
import com.fruitshorts.content.service.DramaClassificationService;
|
||||
import com.fruitshorts.content.service.DramaPlaylistCategoryService;
|
||||
import com.fruitshorts.content.service.DramaService;
|
||||
import com.fruitshorts.content.service.EpisodeService;
|
||||
import com.fruitshorts.interaction.service.CommentService;
|
||||
@@ -49,6 +53,8 @@ public class AppCatalogService {
|
||||
private final EpisodeService episodeService;
|
||||
private final BannerService bannerService;
|
||||
private final DramaCategoryService dramaCategoryService;
|
||||
private final DramaClassificationService dramaClassificationService;
|
||||
private final DramaPlaylistCategoryService dramaPlaylistCategoryService;
|
||||
private final PlaybackSigner playbackSigner;
|
||||
private final CommentService commentService;
|
||||
private final ThumbnailUrlResolver thumbnailUrlResolver;
|
||||
@@ -69,13 +75,42 @@ public class AppCatalogService {
|
||||
categorySections(DISCOVER_SECTION_SIZE, categoryNameMap()));
|
||||
}
|
||||
|
||||
/** 分类频道:运营自定义的精选/新剧/榜单等(drama_classification)。 */
|
||||
public List<AppCategoryVO> categories() {
|
||||
return dramaCategoryService.listBasic().stream()
|
||||
.filter(category -> category.getStatus() == null || category.getStatus() == 1)
|
||||
.map(category -> new AppCategoryVO(category.getId(), category.getName()))
|
||||
return dramaClassificationService.listEnabled().stream()
|
||||
.map(c -> new AppCategoryVO(c.getId(), c.getName()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/** 分类频道:按分类分页查已上线短剧卡片(通过分类关联表过滤)。 */
|
||||
public PageResult<AppDramaCardVO> categoryDramas(Long classificationId,
|
||||
int pageIndex, int pageSize) {
|
||||
dramaClassificationService.getEnabledById(classificationId); // 校验存在/启用
|
||||
List<Long> allIds = dramaClassificationService.listDramaIdsByClassification(classificationId);
|
||||
if (allIds.isEmpty()) {
|
||||
return PageResult.empty();
|
||||
}
|
||||
// 只取已上线的,再按关联表原始顺序排
|
||||
Map<Long, Integer> orderMap = new HashMap<>();
|
||||
for (int i = 0; i < allIds.size(); i++) orderMap.put(allIds.get(i), i);
|
||||
List<Drama> published = dramaService.listPublishedByIds(allIds);
|
||||
List<Drama> ordered = published.stream()
|
||||
.sorted((a, b) -> Integer.compare(
|
||||
orderMap.getOrDefault(a.getId(), Integer.MAX_VALUE),
|
||||
orderMap.getOrDefault(b.getId(), Integer.MAX_VALUE)))
|
||||
.toList();
|
||||
int safeIndex = Math.max(1, pageIndex);
|
||||
int safeSize = Math.max(1, Math.min(pageSize, 50));
|
||||
long total = ordered.size();
|
||||
int fromIndex = (safeIndex - 1) * safeSize;
|
||||
if (fromIndex >= ordered.size()) {
|
||||
return new PageResult<>(total, List.of());
|
||||
}
|
||||
int toIndex = Math.min(fromIndex + safeSize, ordered.size());
|
||||
List<Drama> pageRows = ordered.subList(fromIndex, toIndex);
|
||||
return new PageResult<>(total, cards(pageRows, categoryNameMap()));
|
||||
}
|
||||
|
||||
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));
|
||||
@@ -137,6 +172,33 @@ public class AppCatalogService {
|
||||
return detail;
|
||||
}
|
||||
|
||||
/** C 端剧单详情:剧单基本信息 + 该剧单下所有已上线短剧卡片。 */
|
||||
public AppPlaylistDetailVO playlistDetail(Long playlistId) {
|
||||
var category = dramaPlaylistCategoryService.getEnabledById(playlistId);
|
||||
List<Long> allIds = dramaPlaylistCategoryService.listDramaIdsByCategory(playlistId);
|
||||
List<Drama> published = dramaService.listPublishedByIds(allIds);
|
||||
// 保留关联表原始顺序:按 allIds 位置重新排序
|
||||
Map<Long, Integer> orderMap = new HashMap<>();
|
||||
for (int i = 0; i < allIds.size(); i++) {
|
||||
orderMap.put(allIds.get(i), i);
|
||||
}
|
||||
List<Drama> ordered = published.stream()
|
||||
.sorted((a, b) -> {
|
||||
Integer ia = orderMap.getOrDefault(a.getId(), Integer.MAX_VALUE);
|
||||
Integer ib = orderMap.getOrDefault(b.getId(), Integer.MAX_VALUE);
|
||||
return Integer.compare(ia, ib);
|
||||
})
|
||||
.toList();
|
||||
|
||||
AppPlaylistDetailVO vo = new AppPlaylistDetailVO();
|
||||
vo.setId(category.getId());
|
||||
vo.setName(category.getName());
|
||||
vo.setDescription(category.getDescription());
|
||||
vo.setDramaCount(ordered.size());
|
||||
vo.setList(cards(ordered, categoryNameMap()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
private List<AppBannerVO> banners(String position) {
|
||||
return bannerService.listActive(position).stream().map(this::toBanner).toList();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.app.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** C 端剧单详情:基本信息 + 该剧单下已上线的短剧卡片。 */
|
||||
@Data
|
||||
public class AppPlaylistDetailVO {
|
||||
|
||||
private Long id;
|
||||
private String name;
|
||||
private String description;
|
||||
/** 该剧单下已上线短剧的实际条数(被下架/下线的不计入) */
|
||||
private Integer dramaCount;
|
||||
private List<AppDramaCardVO> list;
|
||||
}
|
||||
+6
-1
@@ -8,6 +8,8 @@ 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.DramaCategoryService;
|
||||
import com.fruitshorts.content.service.DramaClassificationService;
|
||||
import com.fruitshorts.content.service.DramaPlaylistCategoryService;
|
||||
import com.fruitshorts.content.service.DramaService;
|
||||
import com.fruitshorts.content.service.EpisodeService;
|
||||
import com.fruitshorts.interaction.service.CommentService;
|
||||
@@ -45,12 +47,15 @@ class AppCatalogServiceTest {
|
||||
episodeService = mock(EpisodeService.class);
|
||||
BannerService bannerService = mock(BannerService.class);
|
||||
dramaCategoryService = mock(DramaCategoryService.class);
|
||||
DramaClassificationService dramaClassificationService = mock(DramaClassificationService.class);
|
||||
DramaPlaylistCategoryService dramaPlaylistCategoryService = mock(DramaPlaylistCategoryService.class);
|
||||
PlaybackSigner playbackSigner = mock(PlaybackSigner.class);
|
||||
thumbnailUrlResolver = mock(ThumbnailUrlResolver.class);
|
||||
given(thumbnailUrlResolver.resolveAll(any())).willReturn(Map.of());
|
||||
commentService = mock(CommentService.class);
|
||||
catalogService = new AppCatalogService(dramaService, episodeService, bannerService,
|
||||
dramaCategoryService, playbackSigner, commentService, thumbnailUrlResolver);
|
||||
dramaCategoryService, dramaClassificationService, dramaPlaylistCategoryService,
|
||||
playbackSigner, commentService, thumbnailUrlResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
@@ -38,6 +38,7 @@ public class SecurityConfig {
|
||||
"/app/discover/**",
|
||||
"/app/feed",
|
||||
"/app/categories/**",
|
||||
"/app/playlists/**",
|
||||
"/app/banners/**",
|
||||
"/app/dramas/**",
|
||||
"/app/search",
|
||||
|
||||
+32
@@ -27,6 +27,7 @@ import org.springframework.util.StringUtils;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 运营分类服务。分类是独立于「短剧类型(drama_category)」的一套概念,
|
||||
@@ -190,6 +191,37 @@ public class DramaClassificationService {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== C 端只读视图 ==========
|
||||
|
||||
/** C 端:只返回已启用(status=1)的分类,按 sort 升序。 */
|
||||
public List<DramaClassification> listEnabled() {
|
||||
return classificationMapper.selectList(Wrappers.<DramaClassification>lambdaQuery()
|
||||
.eq(DramaClassification::getStatus, 1)
|
||||
.orderByAsc(DramaClassification::getSort)
|
||||
.orderByAsc(DramaClassification::getId));
|
||||
}
|
||||
|
||||
/** C 端:校验分类存在且启用。 */
|
||||
public DramaClassification getEnabledById(Long id) {
|
||||
DramaClassification entity = getExisting(id);
|
||||
if (entity.getStatus() != 1) {
|
||||
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "分类不存在或已下架");
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
/** C 端:分类下关联的全部 dramaId(按关联创建时间 desc)。 */
|
||||
public List<Long> listDramaIdsByClassification(Long classificationId) {
|
||||
return classificationDramaMapper.selectList(
|
||||
Wrappers.<DramaClassificationDrama>lambdaQuery()
|
||||
.eq(DramaClassificationDrama::getClassificationId, classificationId)
|
||||
.orderByDesc(DramaClassificationDrama::getCreatedAt)
|
||||
.orderByDesc(DramaClassificationDrama::getId))
|
||||
.stream()
|
||||
.map(DramaClassificationDrama::getDramaId)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private DramaBriefVO toBrief(Drama drama) {
|
||||
DramaBriefVO vo = new DramaBriefVO();
|
||||
vo.setId(drama.getId());
|
||||
|
||||
+21
@@ -27,6 +27,7 @@ import org.springframework.util.StringUtils;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 剧单分类服务:分类直接关联短剧,没有「剧单」这一层。
|
||||
@@ -177,6 +178,26 @@ public class DramaPlaylistCategoryService {
|
||||
return counts;
|
||||
}
|
||||
|
||||
/** C 端用:只返回已启用(status=1)的剧单。 */
|
||||
public DramaPlaylistCategory getEnabledById(Long id) {
|
||||
DramaPlaylistCategory entity = getExisting(id);
|
||||
if (entity.getStatus() == null || entity.getStatus() != 1) {
|
||||
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "剧单不存在或已下架");
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
/** C 端用:剧单下关联的全部 dramaId(按 created_at desc)。 */
|
||||
public List<Long> listDramaIdsByCategory(Long categoryId) {
|
||||
return categoryDramaMapper.selectList(Wrappers.<DramaPlaylistCategoryDrama>lambdaQuery()
|
||||
.eq(DramaPlaylistCategoryDrama::getCategoryId, categoryId)
|
||||
.orderByDesc(DramaPlaylistCategoryDrama::getCreatedAt)
|
||||
.orderByDesc(DramaPlaylistCategoryDrama::getId))
|
||||
.stream()
|
||||
.map(DramaPlaylistCategoryDrama::getDramaId)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private DramaPlaylistCategory getExisting(Long id) {
|
||||
DramaPlaylistCategory entity = id == null ? null : categoryMapper.selectById(id);
|
||||
if (entity == null) {
|
||||
|
||||
+16
@@ -226,6 +226,22 @@ public class DramaService {
|
||||
return dramaMapper.selectPage(page, wrapper).getRecords();
|
||||
}
|
||||
|
||||
/** C 端分类频道:按分类分页查已上线短剧,含 total。 */
|
||||
public PageResult<Drama> pagePublishedByCategory(Long categoryId, int pageIndex, int pageSize) {
|
||||
int safeIndex = Math.max(1, pageIndex);
|
||||
int safeSize = Math.max(1, Math.min(pageSize, 50));
|
||||
Page<Drama> page = new Page<>(safeIndex, safeSize);
|
||||
LambdaQueryWrapper<Drama> wrapper = Wrappers.<Drama>lambdaQuery()
|
||||
.eq(Drama::getStatus, ContentStatus.PUBLISHED.getCode());
|
||||
if (categoryId != null && categoryId > 0) {
|
||||
DramaMultiTagSupport.applyCategoryIdTokens(wrapper, List.of(categoryId));
|
||||
}
|
||||
wrapper.orderByDesc(Drama::getSort).orderByDesc(Drama::getViewCount)
|
||||
.orderByDesc(Drama::getId);
|
||||
IPage<Drama> result = dramaMapper.selectPage(page, wrapper);
|
||||
return new PageResult<>(result.getTotal(), result.getRecords());
|
||||
}
|
||||
|
||||
/** 已上线短剧,按上线时间倒序 */
|
||||
public List<Drama> listPublishedLatest(long limit) {
|
||||
Page<Drama> page = new Page<>(1, Math.max(1, limit));
|
||||
|
||||
Reference in new issue
Block a user