feat(content): 新增内容类型与首页分类 feed
This commit is contained in:
1 parent
d9cdbc8e15
commit
1052b97306
19 files changed
+557
-4
No files matched your search
@@ -27,6 +27,8 @@ public class Drama {
|
||||
private String coverUrl;
|
||||
private String horizontalCoverUrl;
|
||||
private String language;
|
||||
/** 内容类型:ANIME=漫剧,LIVE=真人剧 */
|
||||
private String dramaType;
|
||||
private Integer totalEpisodes;
|
||||
private Integer freeEpisodes;
|
||||
private Integer unlockCoin;
|
||||
|
||||
+98
-3
@@ -3,6 +3,7 @@
|
||||
package com.fruitshorts.content.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fruitshorts.common.exception.ErrorCode;
|
||||
@@ -27,6 +28,7 @@ import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -35,6 +37,8 @@ import java.util.Set;
|
||||
public class DramaService {
|
||||
|
||||
private static final DateTimeFormatter CODE_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
private static final String DEFAULT_DRAMA_TYPE = "LIVE";
|
||||
private static final Set<String> DRAMA_TYPES = Set.of("ANIME", "LIVE");
|
||||
|
||||
private final DramaMapper dramaMapper;
|
||||
private final DramaEpisodeMapper dramaEpisodeMapper;
|
||||
@@ -42,12 +46,14 @@ public class DramaService {
|
||||
private final DramaCategoryService dramaCategoryService;
|
||||
|
||||
public PageResult<DramaRespVO> page(DramaPageReqVO req) {
|
||||
List<String> dramaTypes = normalizeDramaTypes(req.getDramaTypes());
|
||||
Page<Drama> page = new Page<>(req.getSafePageIndex(), req.getSafePageSize());
|
||||
IPage<Drama> result = dramaMapper.selectPage(page, Wrappers.<Drama>lambdaQuery()
|
||||
.like(StringUtils.hasText(req.getKeyword()), Drama::getTitle, req.getKeyword())
|
||||
.eq(StringUtils.hasText(req.getStatus()), Drama::getStatus, req.getStatus())
|
||||
.eq(req.getCategoryId() != null, Drama::getCategoryId, req.getCategoryId())
|
||||
.eq(req.getIsHot() != null, Drama::getIsHot, Boolean.TRUE.equals(req.getIsHot()) ? 1 : 0)
|
||||
.in(!dramaTypes.isEmpty(), Drama::getDramaType, dramaTypes)
|
||||
.orderByDesc(Drama::getSort)
|
||||
.orderByDesc(Drama::getId));
|
||||
return new PageResult<>(result.getTotal(), toRespList(result.getRecords()));
|
||||
@@ -86,10 +92,10 @@ public class DramaService {
|
||||
drama.setStatus(ContentStatus.DRAFT.getCode());
|
||||
drama.setCreatorId(creatorId == null ? 0L : creatorId);
|
||||
drama.setTotalEpisodes(0);
|
||||
drama.setViewCount(0L);
|
||||
drama.setViewCount(nonNegativeOrZero(req.getHeat(), "热度"));
|
||||
drama.setPlayCount(0L);
|
||||
drama.setFavoriteCount(0L);
|
||||
drama.setLikeCount(0L);
|
||||
drama.setFavoriteCount(nonNegativeOrZero(req.getFavoriteCount(), "收藏数"));
|
||||
drama.setLikeCount(nonNegativeOrZero(req.getLikeCount(), "点赞数"));
|
||||
drama.setCreateBy(operatorId == null ? 0L : operatorId);
|
||||
dramaMapper.insert(drama);
|
||||
return drama.getId();
|
||||
@@ -99,6 +105,15 @@ public class DramaService {
|
||||
public void update(Long id, DramaSaveReqVO req) {
|
||||
Drama drama = getExisting(id);
|
||||
apply(drama, req);
|
||||
if (req.getFavoriteCount() != null) {
|
||||
drama.setFavoriteCount(nonNegative(req.getFavoriteCount(), "收藏数"));
|
||||
}
|
||||
if (req.getLikeCount() != null) {
|
||||
drama.setLikeCount(nonNegative(req.getLikeCount(), "点赞数"));
|
||||
}
|
||||
if (req.getHeat() != null) {
|
||||
drama.setViewCount(nonNegative(req.getHeat(), "热度"));
|
||||
}
|
||||
dramaMapper.updateById(drama);
|
||||
}
|
||||
|
||||
@@ -211,6 +226,31 @@ public class DramaService {
|
||||
.orderByDesc(Drama::getId)).getRecords();
|
||||
}
|
||||
|
||||
/**
|
||||
* App 推荐流:按推荐权重、热度、ID 倒序,并支持内容类型多选。
|
||||
* cursor 使用上一页最后一条 ID,服务端反查排序键后做 keyset 查询。
|
||||
*/
|
||||
public List<Drama> listPublishedFeed(List<String> dramaTypes, Long cursor, int limit) {
|
||||
List<String> normalizedTypes = normalizeDramaTypes(dramaTypes);
|
||||
QueryWrapper<Drama> query = new QueryWrapper<>();
|
||||
query.eq("status", ContentStatus.PUBLISHED.getCode());
|
||||
if (!normalizedTypes.isEmpty()) {
|
||||
query.in("drama_type", normalizedTypes);
|
||||
}
|
||||
if (cursor != null) {
|
||||
Drama marker = dramaMapper.selectById(cursor);
|
||||
if (marker == null
|
||||
|| !ContentStatus.PUBLISHED.getCode().equals(marker.getStatus())
|
||||
|| (!normalizedTypes.isEmpty() && !normalizedTypes.contains(marker.getDramaType()))) {
|
||||
return List.of();
|
||||
}
|
||||
applyFeedCursor(query, marker);
|
||||
}
|
||||
int safeLimit = Math.max(1, limit);
|
||||
query.last("ORDER BY recommend_weight DESC NULLS LAST, view_count DESC, id DESC LIMIT " + safeLimit);
|
||||
return dramaMapper.selectList(query);
|
||||
}
|
||||
|
||||
/** 已上线的热门短剧 */
|
||||
public List<Drama> listPublishedHot(long limit) {
|
||||
Page<Drama> page = new Page<>(1, Math.max(1, limit));
|
||||
@@ -300,6 +340,11 @@ public class DramaService {
|
||||
drama.setTrailerUrl(req.getTrailerUrl() == null ? "" : req.getTrailerUrl());
|
||||
drama.setHorizontalCoverUrl(drama.getHorizontalCoverUrl() == null ? "" : drama.getHorizontalCoverUrl());
|
||||
drama.setLanguage(req.getLanguage() == null ? "zh-CN" : req.getLanguage());
|
||||
if (StringUtils.hasText(req.getDramaType())) {
|
||||
drama.setDramaType(normalizeDramaType(req.getDramaType()));
|
||||
} else if (!StringUtils.hasText(drama.getDramaType())) {
|
||||
drama.setDramaType(DEFAULT_DRAMA_TYPE);
|
||||
}
|
||||
drama.setFreeEpisodes(req.getFreeEpisodes() == null ? 3 : req.getFreeEpisodes());
|
||||
drama.setUnlockCoin(req.getUnlockCoin() == null ? 0 : req.getUnlockCoin());
|
||||
drama.setIsHot(Boolean.TRUE.equals(req.getIsHot()) ? 1 : 0);
|
||||
@@ -330,6 +375,7 @@ public class DramaService {
|
||||
vo.setCover(drama.getCoverUrl());
|
||||
vo.setTrailerUrl(drama.getTrailerUrl());
|
||||
vo.setLanguage(drama.getLanguage());
|
||||
vo.setDramaType(drama.getDramaType());
|
||||
vo.setEpisodes(drama.getTotalEpisodes());
|
||||
vo.setFreeEpisodes(drama.getFreeEpisodes());
|
||||
vo.setUnlockCoin(drama.getUnlockCoin());
|
||||
@@ -346,4 +392,53 @@ public class DramaService {
|
||||
return vo;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private void applyFeedCursor(QueryWrapper<Drama> query, Drama marker) {
|
||||
Long viewCount = marker.getViewCount() == null ? 0L : marker.getViewCount();
|
||||
Long id = marker.getId();
|
||||
if (marker.getRecommendWeight() != null) {
|
||||
Integer weight = marker.getRecommendWeight();
|
||||
query.and(wrapper -> wrapper
|
||||
.lt("recommend_weight", weight)
|
||||
.or(or -> or.eq("recommend_weight", weight)
|
||||
.and(inner -> inner
|
||||
.lt("view_count", viewCount)
|
||||
.or(tail -> tail.eq("view_count", viewCount).lt("id", id)))));
|
||||
} else {
|
||||
query.and(wrapper -> wrapper.isNull("recommend_weight")
|
||||
.and(inner -> inner
|
||||
.lt("view_count", viewCount)
|
||||
.or(tail -> tail.eq("view_count", viewCount).lt("id", id))));
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> normalizeDramaTypes(List<String> dramaTypes) {
|
||||
if (dramaTypes == null || dramaTypes.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return dramaTypes.stream()
|
||||
.filter(StringUtils::hasText)
|
||||
.map(this::normalizeDramaType)
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String normalizeDramaType(String dramaType) {
|
||||
String normalized = dramaType == null ? "" : dramaType.trim().toUpperCase(Locale.ROOT);
|
||||
if (!DRAMA_TYPES.contains(normalized)) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "内容类型仅支持 ANIME 或 LIVE");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private Long nonNegativeOrZero(Long value, String fieldName) {
|
||||
return value == null ? 0L : nonNegative(value, fieldName);
|
||||
}
|
||||
|
||||
private Long nonNegative(Long value, String fieldName) {
|
||||
if (value < 0) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, fieldName + "不能小于 0");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
+4
@@ -6,6 +6,8 @@ import com.fruitshorts.common.pojo.PageParam;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class DramaPageReqVO extends PageParam {
|
||||
@@ -16,4 +18,6 @@ public class DramaPageReqVO extends PageParam {
|
||||
private Long categoryId;
|
||||
/** 是否只看热门 */
|
||||
private Boolean isHot;
|
||||
/** 内容类型多选:ANIME/LIVE,空=全部 */
|
||||
private List<String> dramaTypes;
|
||||
}
|
||||
@@ -25,6 +25,7 @@ public class DramaRespVO {
|
||||
/** 整剧预告片地址 */
|
||||
private String trailerUrl;
|
||||
private String language;
|
||||
private String dramaType;
|
||||
private Integer episodes;
|
||||
private Integer freeEpisodes;
|
||||
private Integer unlockCoin;
|
||||
|
||||
+10
@@ -3,6 +3,8 @@
|
||||
package com.fruitshorts.content.vo;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@@ -19,8 +21,16 @@ public class DramaSaveReqVO {
|
||||
/** 整剧预告片地址(复用已有的 drama.trailer_url 列) */
|
||||
private String trailerUrl;
|
||||
private String language;
|
||||
@Pattern(regexp = "ANIME|LIVE", message = "内容类型仅支持 ANIME 或 LIVE")
|
||||
private String dramaType;
|
||||
private Integer freeEpisodes;
|
||||
private Integer unlockCoin;
|
||||
@PositiveOrZero(message = "收藏数不能小于 0")
|
||||
private Long favoriteCount;
|
||||
@PositiveOrZero(message = "点赞数不能小于 0")
|
||||
private Long likeCount;
|
||||
@PositiveOrZero(message = "热度不能小于 0")
|
||||
private Long heat;
|
||||
/** 是否热门 */
|
||||
private Boolean isHot;
|
||||
private Integer sort;
|
||||
|
||||
Reference in new issue
Block a user