feat(content): 短剧类型与内容类型支持多选
This commit is contained in:
1 parent
030df5c5dc
commit
4c614a5ece
13 files changed
+665
-50
No files matched your search
@@ -22,6 +22,8 @@ public class Drama {
|
||||
private String title;
|
||||
private String subtitle;
|
||||
private Long categoryId;
|
||||
/** 分类多选,逗号分隔的 category_id,category_id 保留第一个 */
|
||||
private String categoryIds;
|
||||
private String genre;
|
||||
private String synopsis;
|
||||
private String coverUrl;
|
||||
@@ -29,6 +31,8 @@ public class Drama {
|
||||
private String language;
|
||||
/** 内容类型:ANIME=漫剧,LIVE=真人剧 */
|
||||
private String dramaType;
|
||||
/** 内容类型多选,逗号分隔(ANIME/LIVE),drama_type 保留第一个 */
|
||||
private String dramaTypes;
|
||||
private Integer totalEpisodes;
|
||||
private Integer freeEpisodes;
|
||||
private Integer unlockCoin;
|
||||
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.content.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.fruitshorts.common.exception.ErrorCode;
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
import com.fruitshorts.content.dal.entity.Drama;
|
||||
import com.fruitshorts.content.vo.DramaSaveReqVO;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** 短剧分类与内容类型多选值的存储、解析和整词过滤。 */
|
||||
final class DramaMultiTagSupport {
|
||||
|
||||
static final String DEFAULT_DRAMA_TYPE = "LIVE";
|
||||
private static final Set<String> DRAMA_TYPES = Set.of("ANIME", "LIVE");
|
||||
|
||||
private DramaMultiTagSupport() {
|
||||
}
|
||||
|
||||
static void applyDramaTypes(Drama drama, DramaSaveReqVO req) {
|
||||
List<String> values = normalizeDramaTypes(req.getDramaTypes());
|
||||
if (values.isEmpty() && StringUtils.hasText(req.getDramaType())) {
|
||||
values = List.of(normalizeDramaType(req.getDramaType()));
|
||||
}
|
||||
if (!values.isEmpty()) {
|
||||
drama.setDramaType(values.get(0));
|
||||
drama.setDramaTypes(String.join(",", values));
|
||||
return;
|
||||
}
|
||||
if (!StringUtils.hasText(drama.getDramaTypes())
|
||||
&& !StringUtils.hasText(drama.getDramaType())) {
|
||||
drama.setDramaType(DEFAULT_DRAMA_TYPE);
|
||||
drama.setDramaTypes(DEFAULT_DRAMA_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
static void applyCategoryIds(Drama drama, DramaSaveReqVO req) {
|
||||
List<Long> values = normalizeCategoryIds(req.getCategoryIds());
|
||||
if (values.isEmpty() && req.getCategoryId() != null && req.getCategoryId() > 0L) {
|
||||
values = List.of(req.getCategoryId());
|
||||
}
|
||||
if (!values.isEmpty()) {
|
||||
drama.setCategoryId(values.get(0));
|
||||
drama.setCategoryIds(String.join(",", values.stream().map(String::valueOf).toList()));
|
||||
return;
|
||||
}
|
||||
if (drama.getCategoryId() == null) {
|
||||
drama.setCategoryId(0L);
|
||||
}
|
||||
if (drama.getCategoryIds() == null) {
|
||||
drama.setCategoryIds("");
|
||||
}
|
||||
}
|
||||
|
||||
static void applyDramaTypeTokens(LambdaQueryWrapper<Drama> wrapper,
|
||||
List<String> dramaTypes) {
|
||||
wrapper.and(nested -> {
|
||||
for (int index = 0; index < dramaTypes.size(); index++) {
|
||||
if (index > 0) {
|
||||
nested.or();
|
||||
}
|
||||
nested.apply("(',' || drama_types || ',') LIKE {0}",
|
||||
tokenPattern(dramaTypes.get(index)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void applyCategoryIdTokens(LambdaQueryWrapper<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 void applyDramaTypeTokens(QueryWrapper<Drama> wrapper,
|
||||
List<String> dramaTypes) {
|
||||
wrapper.and(nested -> {
|
||||
for (int index = 0; index < dramaTypes.size(); index++) {
|
||||
if (index > 0) {
|
||||
nested.or();
|
||||
}
|
||||
nested.apply("(',' || drama_types || ',') LIKE {0}",
|
||||
tokenPattern(dramaTypes.get(index)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static List<String> parseDramaTypes(Drama drama) {
|
||||
List<String> values = parseStringTokens(drama.getDramaTypes()).stream()
|
||||
.map(DramaMultiTagSupport::normalizeDramaType)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (!values.isEmpty()) {
|
||||
return values;
|
||||
}
|
||||
if (StringUtils.hasText(drama.getDramaType())) {
|
||||
return List.of(normalizeDramaType(drama.getDramaType()));
|
||||
}
|
||||
return List.of(DEFAULT_DRAMA_TYPE);
|
||||
}
|
||||
|
||||
static List<Long> parseCategoryIds(Drama drama) {
|
||||
List<Long> values = parseLongTokens(drama.getCategoryIds());
|
||||
if (values.isEmpty() && drama.getCategoryId() != null && drama.getCategoryId() > 0L) {
|
||||
return List.of(drama.getCategoryId());
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
static List<String> categoryNames(Drama drama, List<Long> categoryIds,
|
||||
Map<Long, String> names) {
|
||||
List<String> values = categoryIds.stream()
|
||||
.map(names::get)
|
||||
.filter(StringUtils::hasText)
|
||||
.toList();
|
||||
if (values.isEmpty() && StringUtils.hasText(drama.getGenre())) {
|
||||
return List.of(drama.getGenre());
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
static List<String> normalizeDramaTypes(List<String> dramaTypes) {
|
||||
if (dramaTypes == null || dramaTypes.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return dramaTypes.stream()
|
||||
.filter(StringUtils::hasText)
|
||||
.map(DramaMultiTagSupport::normalizeDramaType)
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
static List<Long> normalizeCategoryIds(List<Long> categoryIds) {
|
||||
if (categoryIds == null || categoryIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return categoryIds.stream()
|
||||
.filter(id -> id != null && id > 0L)
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static 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 static List<String> parseStringTokens(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> result = new ArrayList<>();
|
||||
for (String token : value.split(",")) {
|
||||
String trimmed = token.trim();
|
||||
if (StringUtils.hasText(trimmed) && !result.contains(trimmed)) {
|
||||
result.add(trimmed);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<Long> parseLongTokens(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return List.of();
|
||||
}
|
||||
List<Long> result = new ArrayList<>();
|
||||
for (String token : value.split(",")) {
|
||||
Long parsed = parseLongValue(token);
|
||||
if (parsed != null && parsed > 0L && !result.contains(parsed)) {
|
||||
result.add(parsed);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Long parseLongValue(String value) {
|
||||
try {
|
||||
return Long.parseLong(value.trim());
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String tokenPattern(Object value) {
|
||||
return "%," + value + ",%";
|
||||
}
|
||||
}
|
||||
+42
-46
@@ -3,6 +3,7 @@
|
||||
package com.fruitshorts.content.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
@@ -28,7 +29,6 @@ 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;
|
||||
|
||||
@@ -37,8 +37,6 @@ 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;
|
||||
@@ -46,16 +44,23 @@ public class DramaService {
|
||||
private final DramaCategoryService dramaCategoryService;
|
||||
|
||||
public PageResult<DramaRespVO> page(DramaPageReqVO req) {
|
||||
List<String> dramaTypes = normalizeDramaTypes(req.getDramaTypes());
|
||||
List<String> dramaTypes = DramaMultiTagSupport.normalizeDramaTypes(req.getDramaTypes());
|
||||
List<Long> categoryIds = DramaMultiTagSupport.normalizeCategoryIds(req.getCategoryIds());
|
||||
Page<Drama> page = new Page<>(req.getSafePageIndex(), req.getSafePageSize());
|
||||
IPage<Drama> result = dramaMapper.selectPage(page, Wrappers.<Drama>lambdaQuery()
|
||||
LambdaQueryWrapper<Drama> wrapper = 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));
|
||||
.eq(req.getIsHot() != null, Drama::getIsHot, Boolean.TRUE.equals(req.getIsHot()) ? 1 : 0);
|
||||
if (!dramaTypes.isEmpty()) {
|
||||
DramaMultiTagSupport.applyDramaTypeTokens(wrapper, dramaTypes);
|
||||
}
|
||||
if (!categoryIds.isEmpty()) {
|
||||
DramaMultiTagSupport.applyCategoryIdTokens(wrapper, categoryIds);
|
||||
} else {
|
||||
wrapper.eq(req.getCategoryId() != null, Drama::getCategoryId, req.getCategoryId());
|
||||
}
|
||||
wrapper.orderByDesc(Drama::getSort).orderByDesc(Drama::getId);
|
||||
IPage<Drama> result = dramaMapper.selectPage(page, wrapper);
|
||||
return new PageResult<>(result.getTotal(), toRespList(result.getRecords()));
|
||||
}
|
||||
|
||||
@@ -210,11 +215,13 @@ public class DramaService {
|
||||
/** 已上线短剧,按运营排序 + 播放量 */
|
||||
public List<Drama> listPublished(Long categoryId, long limit) {
|
||||
Page<Drama> page = new Page<>(1, Math.max(1, limit));
|
||||
return dramaMapper.selectPage(page, Wrappers.<Drama>lambdaQuery()
|
||||
.eq(Drama::getStatus, ContentStatus.PUBLISHED.getCode())
|
||||
.eq(categoryId != null && categoryId > 0, Drama::getCategoryId, categoryId)
|
||||
.orderByDesc(Drama::getSort)
|
||||
.orderByDesc(Drama::getViewCount)).getRecords();
|
||||
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);
|
||||
return dramaMapper.selectPage(page, wrapper).getRecords();
|
||||
}
|
||||
|
||||
/** 已上线短剧,按上线时间倒序 */
|
||||
@@ -231,17 +238,19 @@ public class DramaService {
|
||||
* cursor 使用上一页最后一条 ID,服务端反查排序键后做 keyset 查询。
|
||||
*/
|
||||
public List<Drama> listPublishedFeed(List<String> dramaTypes, Long cursor, int limit) {
|
||||
List<String> normalizedTypes = normalizeDramaTypes(dramaTypes);
|
||||
List<String> normalizedTypes = DramaMultiTagSupport.normalizeDramaTypes(dramaTypes);
|
||||
QueryWrapper<Drama> query = new QueryWrapper<>();
|
||||
query.eq("status", ContentStatus.PUBLISHED.getCode());
|
||||
if (!normalizedTypes.isEmpty()) {
|
||||
query.in("drama_type", normalizedTypes);
|
||||
DramaMultiTagSupport.applyDramaTypeTokens(query, normalizedTypes);
|
||||
}
|
||||
if (cursor != null) {
|
||||
Drama marker = dramaMapper.selectById(cursor);
|
||||
if (marker == null
|
||||
|| !ContentStatus.PUBLISHED.getCode().equals(marker.getStatus())
|
||||
|| (!normalizedTypes.isEmpty() && !normalizedTypes.contains(marker.getDramaType()))) {
|
||||
|| (!normalizedTypes.isEmpty()
|
||||
&& DramaMultiTagSupport.parseDramaTypes(marker).stream()
|
||||
.noneMatch(normalizedTypes::contains))) {
|
||||
return List.of();
|
||||
}
|
||||
applyFeedCursor(query, marker);
|
||||
@@ -333,18 +342,14 @@ public class DramaService {
|
||||
private void apply(Drama drama, DramaSaveReqVO req) {
|
||||
drama.setTitle(req.getTitle());
|
||||
drama.setSubtitle(req.getSubtitle() == null ? "" : req.getSubtitle());
|
||||
drama.setCategoryId(req.getCategoryId() == null ? 0L : req.getCategoryId());
|
||||
DramaMultiTagSupport.applyCategoryIds(drama, req);
|
||||
drama.setGenre(req.getGenre() == null ? "" : req.getGenre());
|
||||
drama.setSynopsis(req.getDescription() == null ? "" : req.getDescription());
|
||||
drama.setCoverUrl(req.getCover() == null ? "" : req.getCover());
|
||||
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);
|
||||
}
|
||||
DramaMultiTagSupport.applyDramaTypes(drama, req);
|
||||
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);
|
||||
@@ -363,19 +368,29 @@ public class DramaService {
|
||||
}
|
||||
Map<Long, String> categories = dramaCategoryService.nameMap();
|
||||
return dramas.stream().map(drama -> {
|
||||
List<Long> categoryIds = DramaMultiTagSupport.parseCategoryIds(drama);
|
||||
Long primaryCategoryId = categoryIds.isEmpty()
|
||||
? (drama.getCategoryId() == null ? 0L : drama.getCategoryId())
|
||||
: categoryIds.get(0);
|
||||
List<String> categoryNames =
|
||||
DramaMultiTagSupport.categoryNames(drama, categoryIds, categories);
|
||||
List<String> dramaTypes = DramaMultiTagSupport.parseDramaTypes(drama);
|
||||
DramaRespVO vo = new DramaRespVO();
|
||||
vo.setId(drama.getId());
|
||||
vo.setCode(drama.getCode());
|
||||
vo.setTitle(drama.getTitle());
|
||||
vo.setSubtitle(drama.getSubtitle());
|
||||
vo.setCategoryId(drama.getCategoryId());
|
||||
vo.setCategory(categories.getOrDefault(drama.getCategoryId(), ""));
|
||||
vo.setCategoryId(primaryCategoryId);
|
||||
vo.setCategoryIds(categoryIds);
|
||||
vo.setCategoryNames(categoryNames);
|
||||
vo.setCategory(categories.getOrDefault(primaryCategoryId, ""));
|
||||
vo.setGenre(drama.getGenre());
|
||||
vo.setDescription(drama.getSynopsis());
|
||||
vo.setCover(drama.getCoverUrl());
|
||||
vo.setTrailerUrl(drama.getTrailerUrl());
|
||||
vo.setLanguage(drama.getLanguage());
|
||||
vo.setDramaType(drama.getDramaType());
|
||||
vo.setDramaType(dramaTypes.get(0));
|
||||
vo.setDramaTypes(dramaTypes);
|
||||
vo.setEpisodes(drama.getTotalEpisodes());
|
||||
vo.setFreeEpisodes(drama.getFreeEpisodes());
|
||||
vo.setUnlockCoin(drama.getUnlockCoin());
|
||||
@@ -412,25 +427,6 @@ public class DramaService {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
+2
@@ -16,6 +16,8 @@ public class DramaPageReqVO extends PageParam {
|
||||
private String keyword;
|
||||
private String status;
|
||||
private Long categoryId;
|
||||
/** 分类多选,空时回退 categoryId */
|
||||
private List<Long> categoryIds;
|
||||
/** 是否只看热门 */
|
||||
private Boolean isHot;
|
||||
/** 内容类型多选:ANIME/LIVE,空=全部 */
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 短剧列表/详情响应,字段名与后台页面表格列一致。
|
||||
@@ -18,6 +19,8 @@ public class DramaRespVO {
|
||||
private String title;
|
||||
private String subtitle;
|
||||
private Long categoryId;
|
||||
private List<Long> categoryIds;
|
||||
private List<String> categoryNames;
|
||||
private String category;
|
||||
private String genre;
|
||||
private String description;
|
||||
@@ -26,6 +29,7 @@ public class DramaRespVO {
|
||||
private String trailerUrl;
|
||||
private String language;
|
||||
private String dramaType;
|
||||
private List<String> dramaTypes;
|
||||
private Integer episodes;
|
||||
private Integer freeEpisodes;
|
||||
private Integer unlockCoin;
|
||||
|
||||
+6
@@ -7,6 +7,8 @@ import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class DramaSaveReqVO {
|
||||
|
||||
@@ -15,6 +17,8 @@ public class DramaSaveReqVO {
|
||||
|
||||
private String subtitle;
|
||||
private Long categoryId;
|
||||
/** 后台多选优先,创作者中心可继续只传 categoryId */
|
||||
private List<Long> categoryIds;
|
||||
private String genre;
|
||||
private String description;
|
||||
private String cover;
|
||||
@@ -23,6 +27,8 @@ public class DramaSaveReqVO {
|
||||
private String language;
|
||||
@Pattern(regexp = "ANIME|LIVE", message = "内容类型仅支持 ANIME 或 LIVE")
|
||||
private String dramaType;
|
||||
/** 后台多选优先,创作者中心可继续只传 dramaType */
|
||||
private List<String> dramaTypes;
|
||||
private Integer freeEpisodes;
|
||||
private Integer unlockCoin;
|
||||
@PositiveOrZero(message = "收藏数不能小于 0")
|
||||
|
||||
Reference in new issue
Block a user