refactor: 后端标识统一为 FruitShorts,并落地 05 离线下载
为什么: 1) 产品已定名 FruitShorts,但后端仍是 reelshort 系列标识 —— Java 包 com.reelshort、 模块名与 jar 名 reelshort-*、spring.application.name、配置前缀 reelshort.*, 这些会出现在启动日志、命令行和 IDE 里,与产品名不一致。本次整体替换为 fruitshorts, 数据库连接名上一轮已改名,保持不变。 2) 第一阶段下一批第 1 步(05 离线下载)要求把下载授权(设备绑定/有效期/吊销)、 下载任务与断点续传、配额与过期清理、仅 Wi-Fi 规则做成完整接口,本提交一并落地。 3) 顺带修复:schema-phase1.sql 里 playback_ticket / order_event 仍按普通表定义唯一索引, 表分区化之后重跑脚本必定报错(分区表的唯一索引必须包含分区键)。改为在本脚本里就 按分区表定义,并验证脚本可重复执行。
This commit is contained in:
1 parent
a4aa749cc6
commit
b3179fc836
251 files changed
+7261
-767
No files matched your search
+36
@@ -0,0 +1,36 @@
|
||||
package com.fruitshorts.content.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("content_banner")
|
||||
public class ContentBanner {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String imageUrl;
|
||||
/** 展示位置,列名为 slot(position 在 PostgreSQL 里是关键字) */
|
||||
@TableField("slot")
|
||||
private String position;
|
||||
private String linkType;
|
||||
private String linkValue;
|
||||
private Integer sort;
|
||||
private Integer status;
|
||||
private Long createBy;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/** updated_at 交给数据库的 ON UPDATE CURRENT_TIMESTAMP 维护 */
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package com.fruitshorts.content.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("drama")
|
||||
public class Drama {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String code;
|
||||
private String title;
|
||||
private String subtitle;
|
||||
private Long categoryId;
|
||||
private String genre;
|
||||
private String synopsis;
|
||||
private String coverUrl;
|
||||
private String horizontalCoverUrl;
|
||||
private String language;
|
||||
private Integer totalEpisodes;
|
||||
private Integer freeEpisodes;
|
||||
private Integer unlockCoin;
|
||||
/** 是否热门:1 是,0 否 */
|
||||
private Integer isHot;
|
||||
/** 创作者归属,0 表示平台自制 */
|
||||
private Long creatorId;
|
||||
private String tags;
|
||||
/** 内容分级:ALL/R13/R17/R21(印尼) */
|
||||
private String rating;
|
||||
/** 可播区域,逗号分隔 ISO 国家码,空=不限 */
|
||||
private String regionCodes;
|
||||
private String copyrightOwner;
|
||||
private LocalDateTime copyrightStartAt;
|
||||
/** 版权到期时间,到期自动下架 */
|
||||
private LocalDateTime copyrightEndAt;
|
||||
/** 预留:多语言字幕(第三阶段) */
|
||||
private String subtitleLangs;
|
||||
private String trailerUrl;
|
||||
/** 预留:推荐权重(第三阶段) */
|
||||
private Integer recommendWeight;
|
||||
/** 预留:会员专属(第二阶段) */
|
||||
private Integer isVipOnly;
|
||||
/** 创作者内容的审核状态 */
|
||||
private String auditStatus;
|
||||
private String auditNote;
|
||||
private String status;
|
||||
private LocalDateTime publishAt;
|
||||
private LocalDateTime publishedAt;
|
||||
private Long viewCount;
|
||||
private Long playCount;
|
||||
private Long favoriteCount;
|
||||
private Long likeCount;
|
||||
private Integer sort;
|
||||
private Long createBy;
|
||||
private LocalDateTime createdAt;
|
||||
/** updated_at 交给数据库的 ON UPDATE CURRENT_TIMESTAMP 维护 */
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private LocalDateTime updatedAt;
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.fruitshorts.content.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("drama_category")
|
||||
public class DramaCategory {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String name;
|
||||
private Integer sort;
|
||||
private Integer status;
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.fruitshorts.content.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("drama_episode")
|
||||
public class DramaEpisode {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long dramaId;
|
||||
private Integer epNo;
|
||||
private String title;
|
||||
private Integer durationSec;
|
||||
private String coverUrl;
|
||||
private Long videoAssetId;
|
||||
private String videoUrl;
|
||||
private Integer isFree;
|
||||
private Integer unlockCoin;
|
||||
/** 转码状态:none/queued/running/success/failed */
|
||||
private String transcodeStatus;
|
||||
/** 多码率 master playlist 地址,转码完成后写入 */
|
||||
private String hlsUrl;
|
||||
/** 试看秒数,0 表示不限制 */
|
||||
private Integer previewSeconds;
|
||||
private String subtitleUrl;
|
||||
private String subtitleLang;
|
||||
/** 预留:会员专属(第二阶段) */
|
||||
private Integer isVipOnly;
|
||||
private String status;
|
||||
private LocalDateTime releaseAt;
|
||||
private Long playCount;
|
||||
private Integer sort;
|
||||
private LocalDateTime createdAt;
|
||||
/** updated_at 交给数据库的 ON UPDATE CURRENT_TIMESTAMP 维护 */
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private LocalDateTime updatedAt;
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.fruitshorts.content.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("drama_review_record")
|
||||
public class DramaReviewRecord {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String bizType;
|
||||
private Long bizId;
|
||||
private String action;
|
||||
private String reason;
|
||||
private Long operatorId;
|
||||
private String operatorName;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.fruitshorts.content.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.fruitshorts.content.dal.entity.ContentBanner;
|
||||
|
||||
public interface ContentBannerMapper extends BaseMapper<ContentBanner> {
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.fruitshorts.content.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.fruitshorts.content.dal.entity.DramaCategory;
|
||||
|
||||
public interface DramaCategoryMapper extends BaseMapper<DramaCategory> {
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.fruitshorts.content.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.fruitshorts.content.dal.entity.DramaEpisode;
|
||||
|
||||
public interface DramaEpisodeMapper extends BaseMapper<DramaEpisode> {
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.fruitshorts.content.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.fruitshorts.content.dal.entity.Drama;
|
||||
|
||||
public interface DramaMapper extends BaseMapper<Drama> {
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.fruitshorts.content.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.fruitshorts.content.dal.entity.DramaReviewRecord;
|
||||
|
||||
public interface DramaReviewRecordMapper extends BaseMapper<DramaReviewRecord> {
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.fruitshorts.content.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/** 轮播图点击后的跳转类型。 */
|
||||
@Getter
|
||||
public enum BannerLinkType {
|
||||
|
||||
NONE("不跳转"),
|
||||
DRAMA("跳转短剧"),
|
||||
URL("打开链接");
|
||||
|
||||
private final String text;
|
||||
|
||||
BannerLinkType(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public static String textOf(String code) {
|
||||
for (BannerLinkType type : values()) {
|
||||
if (type.name().equals(code)) {
|
||||
return type.text;
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
public static boolean isValid(String code) {
|
||||
for (BannerLinkType type : values()) {
|
||||
if (type.name().equals(code)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.fruitshorts.content.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/** 轮播图展示位置。 */
|
||||
@Getter
|
||||
public enum BannerPosition {
|
||||
|
||||
HOME_TOP("首页顶部"),
|
||||
DISCOVER("发现页");
|
||||
|
||||
private final String text;
|
||||
|
||||
BannerPosition(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public static String textOf(String code) {
|
||||
for (BannerPosition position : values()) {
|
||||
if (position.name().equals(code)) {
|
||||
return position.text;
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
public static boolean isValid(String code) {
|
||||
for (BannerPosition position : values()) {
|
||||
if (position.name().equals(code)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.fruitshorts.content.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 短剧与剧集共用一套状态码,与前端展示文案一一对应。
|
||||
*/
|
||||
@Getter
|
||||
public enum ContentStatus {
|
||||
|
||||
DRAFT("draft", "草稿"),
|
||||
REVIEWING("reviewing", "审核中"),
|
||||
APPROVED("approved", "待发布"),
|
||||
PUBLISHED("published", "已上线"),
|
||||
OFFLINE("offline", "已下线");
|
||||
|
||||
private final String code;
|
||||
private final String text;
|
||||
|
||||
ContentStatus(String code, String text) {
|
||||
this.code = code;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public static String textOf(String code) {
|
||||
for (ContentStatus status : values()) {
|
||||
if (status.code.equals(code)) {
|
||||
return status.text;
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
package com.fruitshorts.content.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fruitshorts.common.exception.ErrorCode;
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
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.mapper.ContentBannerMapper;
|
||||
import com.fruitshorts.content.dal.mapper.DramaMapper;
|
||||
import com.fruitshorts.content.enums.BannerLinkType;
|
||||
import com.fruitshorts.content.enums.BannerPosition;
|
||||
import com.fruitshorts.content.vo.BannerRespVO;
|
||||
import com.fruitshorts.content.vo.BannerSaveReqVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BannerService {
|
||||
|
||||
private final ContentBannerMapper contentBannerMapper;
|
||||
private final DramaMapper dramaMapper;
|
||||
|
||||
public PageResult<BannerRespVO> page(String position, Integer status, Integer pageIndex, Integer pageSize) {
|
||||
Page<ContentBanner> page = new Page<>(
|
||||
pageIndex == null || pageIndex < 1 ? 1 : pageIndex,
|
||||
pageSize == null || pageSize < 1 ? 10 : pageSize);
|
||||
IPage<ContentBanner> result = contentBannerMapper.selectPage(page,
|
||||
Wrappers.<ContentBanner>lambdaQuery()
|
||||
.eq(StringUtils.hasText(position), ContentBanner::getPosition, position)
|
||||
.eq(status != null, ContentBanner::getStatus, status)
|
||||
.orderByAsc(ContentBanner::getSort)
|
||||
.orderByDesc(ContentBanner::getId));
|
||||
return new PageResult<>(result.getTotal(), toRespList(result.getRecords()));
|
||||
}
|
||||
|
||||
public BannerRespVO get(Long id) {
|
||||
return toRespList(List.of(getExisting(id))).get(0);
|
||||
}
|
||||
|
||||
/** C 端:启用中的轮播图,按运营排序 */
|
||||
public List<ContentBanner> listActive(String position) {
|
||||
return contentBannerMapper.selectList(Wrappers.<ContentBanner>lambdaQuery()
|
||||
.eq(ContentBanner::getStatus, 1)
|
||||
.eq(StringUtils.hasText(position), ContentBanner::getPosition, position)
|
||||
.orderByAsc(ContentBanner::getSort)
|
||||
.orderByAsc(ContentBanner::getId));
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long create(BannerSaveReqVO req, Long operatorId) {
|
||||
ContentBanner banner = new ContentBanner();
|
||||
apply(banner, req);
|
||||
banner.setCreateBy(operatorId == null ? 0L : operatorId);
|
||||
contentBannerMapper.insert(banner);
|
||||
return banner.getId();
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void update(Long id, BannerSaveReqVO req) {
|
||||
ContentBanner banner = getExisting(id);
|
||||
apply(banner, req);
|
||||
contentBannerMapper.updateById(banner);
|
||||
}
|
||||
|
||||
public void delete(Long id) {
|
||||
getExisting(id);
|
||||
contentBannerMapper.deleteById(id);
|
||||
}
|
||||
|
||||
public void updateStatus(Long id, Integer status) {
|
||||
ContentBanner banner = getExisting(id);
|
||||
banner.setStatus(status == null || status == 0 ? 0 : 1);
|
||||
contentBannerMapper.updateById(banner);
|
||||
}
|
||||
|
||||
/** 按传入顺序重排 sort */
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateSort(List<Long> ids) {
|
||||
for (int index = 0; index < ids.size(); index++) {
|
||||
ContentBanner banner = contentBannerMapper.selectById(ids.get(index));
|
||||
if (banner != null) {
|
||||
banner.setSort(index + 1);
|
||||
contentBannerMapper.updateById(banner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ContentBanner getExisting(Long id) {
|
||||
ContentBanner banner = id == null ? null : contentBannerMapper.selectById(id);
|
||||
if (banner == null) {
|
||||
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "轮播图不存在");
|
||||
}
|
||||
return banner;
|
||||
}
|
||||
|
||||
private void apply(ContentBanner banner, BannerSaveReqVO req) {
|
||||
String position = StringUtils.hasText(req.getPosition()) ? req.getPosition() : BannerPosition.HOME_TOP.name();
|
||||
if (!BannerPosition.isValid(position)) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "未知的展示位置: " + position);
|
||||
}
|
||||
String linkType = StringUtils.hasText(req.getLinkType()) ? req.getLinkType() : BannerLinkType.NONE.name();
|
||||
if (!BannerLinkType.isValid(linkType)) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "未知的跳转类型: " + linkType);
|
||||
}
|
||||
String linkValue = req.getLinkValue() == null ? "" : req.getLinkValue().trim();
|
||||
if (BannerLinkType.DRAMA.name().equals(linkType)) {
|
||||
if (!StringUtils.hasText(linkValue)) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "请选择要跳转的短剧");
|
||||
}
|
||||
// link_value 存的是字符串,查询前必须转成 Long,
|
||||
// 否则 PostgreSQL 会直接拒绝 varchar 与 bigint 比较。
|
||||
Long dramaId = parseId(linkValue);
|
||||
if (dramaId == null || dramaMapper.selectById(dramaId) == null) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "跳转的短剧不存在");
|
||||
}
|
||||
linkValue = String.valueOf(dramaId);
|
||||
} else if (BannerLinkType.URL.name().equals(linkType)) {
|
||||
if (!StringUtils.hasText(linkValue)) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "请填写跳转地址");
|
||||
}
|
||||
} else {
|
||||
linkValue = "";
|
||||
}
|
||||
banner.setImageUrl(req.getImageUrl().trim());
|
||||
banner.setPosition(position);
|
||||
banner.setLinkType(linkType);
|
||||
banner.setLinkValue(linkValue);
|
||||
banner.setSort(req.getSort() == null ? 0 : req.getSort());
|
||||
banner.setStatus(req.getStatus() == null ? 1 : req.getStatus());
|
||||
}
|
||||
|
||||
private List<BannerRespVO> toRespList(List<ContentBanner> banners) {
|
||||
if (banners.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
// 批量取跳转短剧名称
|
||||
List<Long> dramaIds = banners.stream()
|
||||
.filter(banner -> BannerLinkType.DRAMA.name().equals(banner.getLinkType()))
|
||||
.map(banner -> parseId(banner.getLinkValue()))
|
||||
.filter(id -> id != null)
|
||||
.distinct()
|
||||
.toList();
|
||||
Map<Long, String> dramaNames = new HashMap<>();
|
||||
if (!dramaIds.isEmpty()) {
|
||||
dramaMapper.selectList(Wrappers.<Drama>lambdaQuery().in(Drama::getId, dramaIds))
|
||||
.forEach(drama -> dramaNames.put(drama.getId(), drama.getTitle()));
|
||||
}
|
||||
|
||||
return banners.stream().map(banner -> {
|
||||
BannerRespVO vo = new BannerRespVO();
|
||||
vo.setId(banner.getId());
|
||||
vo.setImageUrl(banner.getImageUrl());
|
||||
vo.setPosition(banner.getPosition());
|
||||
vo.setPositionText(BannerPosition.textOf(banner.getPosition()));
|
||||
vo.setLinkType(banner.getLinkType());
|
||||
vo.setLinkTypeText(BannerLinkType.textOf(banner.getLinkType()));
|
||||
vo.setLinkValue(banner.getLinkValue());
|
||||
if (BannerLinkType.DRAMA.name().equals(banner.getLinkType())) {
|
||||
Long dramaId = parseId(banner.getLinkValue());
|
||||
vo.setLinkText(dramaId == null ? "" : dramaNames.getOrDefault(dramaId, "已删除的短剧"));
|
||||
} else if (BannerLinkType.URL.name().equals(banner.getLinkType())) {
|
||||
vo.setLinkText(banner.getLinkValue());
|
||||
} else {
|
||||
vo.setLinkText("");
|
||||
}
|
||||
vo.setSort(banner.getSort());
|
||||
vo.setStatus(banner.getStatus());
|
||||
vo.setStatusText(banner.getStatus() != null && banner.getStatus() == 1 ? "启用" : "停用");
|
||||
vo.setUpdatedAt(banner.getUpdatedAt());
|
||||
return vo;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private Long parseId(String value) {
|
||||
try {
|
||||
return Long.valueOf(value);
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package com.fruitshorts.content.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.fruitshorts.common.exception.ErrorCode;
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
import com.fruitshorts.content.dal.entity.Drama;
|
||||
import com.fruitshorts.content.dal.entity.DramaCategory;
|
||||
import com.fruitshorts.content.dal.mapper.DramaMapper;
|
||||
import com.fruitshorts.content.dal.mapper.DramaCategoryMapper;
|
||||
import com.fruitshorts.content.vo.DramaCategoryRespVO;
|
||||
import com.fruitshorts.content.vo.DramaCategorySaveReqVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DramaCategoryService {
|
||||
|
||||
private final DramaCategoryMapper dramaCategoryMapper;
|
||||
private final DramaMapper dramaMapper;
|
||||
|
||||
public List<DramaCategoryRespVO> list() {
|
||||
Map<Long, Integer> counts = countDramasByCategory();
|
||||
return dramaCategoryMapper.selectList(
|
||||
Wrappers.<DramaCategory>lambdaQuery().orderByAsc(DramaCategory::getSort))
|
||||
.stream().map(category -> {
|
||||
DramaCategoryRespVO vo = new DramaCategoryRespVO();
|
||||
vo.setId(category.getId());
|
||||
vo.setName(category.getName());
|
||||
vo.setSort(category.getSort());
|
||||
vo.setStatus(category.getStatus());
|
||||
vo.setDramaCount(counts.getOrDefault(category.getId(), 0));
|
||||
return vo;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
public Long create(DramaCategorySaveReqVO req) {
|
||||
ensureNameAvailable(req.getName(), null);
|
||||
DramaCategory category = new DramaCategory();
|
||||
category.setName(req.getName().trim());
|
||||
Long count = dramaCategoryMapper.selectCount(null);
|
||||
category.setSort(req.getSort() == null
|
||||
? (count == null ? 0 : count.intValue()) + 1
|
||||
: req.getSort());
|
||||
category.setStatus(req.getStatus() == null ? 1 : req.getStatus());
|
||||
dramaCategoryMapper.insert(category);
|
||||
return category.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 轻量列表:只查分类本身,不统计短剧数量。
|
||||
* C 端展示和名称映射用它,避免为了一个名称把整张短剧表扫一遍。
|
||||
*/
|
||||
public List<DramaCategoryRespVO> listBasic() {
|
||||
return dramaCategoryMapper.selectList(
|
||||
Wrappers.<DramaCategory>lambdaQuery().orderByAsc(DramaCategory::getSort))
|
||||
.stream().map(category -> {
|
||||
DramaCategoryRespVO vo = new DramaCategoryRespVO();
|
||||
vo.setId(category.getId());
|
||||
vo.setName(category.getName());
|
||||
vo.setSort(category.getSort());
|
||||
vo.setStatus(category.getStatus());
|
||||
return vo;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
public void update(Long id, DramaCategorySaveReqVO req) {
|
||||
DramaCategory category = getExisting(id);
|
||||
ensureNameAvailable(req.getName(), id);
|
||||
category.setName(req.getName().trim());
|
||||
if (req.getSort() != null) {
|
||||
category.setSort(req.getSort());
|
||||
}
|
||||
if (req.getStatus() != null) {
|
||||
category.setStatus(req.getStatus());
|
||||
}
|
||||
dramaCategoryMapper.updateById(category);
|
||||
}
|
||||
|
||||
public void delete(Long id) {
|
||||
DramaCategory category = getExisting(id);
|
||||
Integer used = countDramasByCategory().getOrDefault(id, 0);
|
||||
if (used > 0) {
|
||||
throw new ServiceException(ErrorCode.OPERATION_NOT_ALLOWED,
|
||||
"「" + category.getName() + "」下还有 " + used + " 部短剧,无法删除");
|
||||
}
|
||||
dramaCategoryMapper.deleteById(id);
|
||||
}
|
||||
|
||||
private DramaCategory getExisting(Long id) {
|
||||
DramaCategory category = id == null ? null : dramaCategoryMapper.selectById(id);
|
||||
if (category == null) {
|
||||
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "类型不存在");
|
||||
}
|
||||
return category;
|
||||
}
|
||||
|
||||
private void ensureNameAvailable(String name, Long excludeId) {
|
||||
Long count = dramaCategoryMapper.selectCount(Wrappers.<DramaCategory>lambdaQuery()
|
||||
.eq(DramaCategory::getName, name.trim())
|
||||
.ne(excludeId != null, DramaCategory::getId, excludeId));
|
||||
if (count != null && count > 0) {
|
||||
throw new ServiceException(ErrorCode.DATA_EXISTS, "类型名称已存在");
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Long, Integer> countDramasByCategory() {
|
||||
Map<Long, Integer> counts = new HashMap<>();
|
||||
dramaMapper.selectList(Wrappers.<Drama>lambdaQuery().select(Drama::getCategoryId))
|
||||
.forEach(drama -> {
|
||||
if (drama.getCategoryId() != null && drama.getCategoryId() > 0) {
|
||||
counts.merge(drama.getCategoryId(), 1, Integer::sum);
|
||||
}
|
||||
});
|
||||
return counts;
|
||||
}
|
||||
|
||||
public Map<Long, String> nameMap() {
|
||||
return dramaCategoryMapper.selectList(null).stream()
|
||||
.collect(Collectors.toMap(DramaCategory::getId, DramaCategory::getName, (a, b) -> a));
|
||||
}
|
||||
}
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
package com.fruitshorts.content.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fruitshorts.common.exception.ErrorCode;
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
import com.fruitshorts.common.pojo.PageResult;
|
||||
import com.fruitshorts.content.dal.entity.Drama;
|
||||
import com.fruitshorts.content.dal.entity.DramaEpisode;
|
||||
import com.fruitshorts.content.dal.entity.DramaReviewRecord;
|
||||
import com.fruitshorts.content.dal.mapper.DramaEpisodeMapper;
|
||||
import com.fruitshorts.content.dal.mapper.DramaMapper;
|
||||
import com.fruitshorts.content.dal.mapper.DramaReviewRecordMapper;
|
||||
import com.fruitshorts.content.enums.ContentStatus;
|
||||
import com.fruitshorts.content.vo.DramaPageReqVO;
|
||||
import com.fruitshorts.content.vo.DramaRespVO;
|
||||
import com.fruitshorts.content.vo.DramaSaveReqVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DramaService {
|
||||
|
||||
private static final DateTimeFormatter CODE_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
|
||||
private final DramaMapper dramaMapper;
|
||||
private final DramaEpisodeMapper dramaEpisodeMapper;
|
||||
private final DramaReviewRecordMapper dramaReviewRecordMapper;
|
||||
private final DramaCategoryService dramaCategoryService;
|
||||
|
||||
public PageResult<DramaRespVO> page(DramaPageReqVO req) {
|
||||
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)
|
||||
.orderByDesc(Drama::getSort)
|
||||
.orderByDesc(Drama::getId));
|
||||
return new PageResult<>(result.getTotal(), toRespList(result.getRecords()));
|
||||
}
|
||||
|
||||
public DramaRespVO get(Long id) {
|
||||
return toRespList(List.of(getExisting(id))).get(0);
|
||||
}
|
||||
|
||||
public Drama getExisting(Long id) {
|
||||
Drama drama = id == null ? null : dramaMapper.selectById(id);
|
||||
if (drama == null) {
|
||||
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "短剧不存在");
|
||||
}
|
||||
return drama;
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long create(DramaSaveReqVO req, Long operatorId) {
|
||||
Drama drama = new Drama();
|
||||
apply(drama, req);
|
||||
drama.setCode(generateCode());
|
||||
drama.setStatus(ContentStatus.DRAFT.getCode());
|
||||
drama.setTotalEpisodes(0);
|
||||
drama.setViewCount(0L);
|
||||
drama.setPlayCount(0L);
|
||||
drama.setFavoriteCount(0L);
|
||||
drama.setLikeCount(0L);
|
||||
drama.setCreateBy(operatorId == null ? 0L : operatorId);
|
||||
dramaMapper.insert(drama);
|
||||
return drama.getId();
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void update(Long id, DramaSaveReqVO req) {
|
||||
Drama drama = getExisting(id);
|
||||
apply(drama, req);
|
||||
dramaMapper.updateById(drama);
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long id) {
|
||||
Drama drama = getExisting(id);
|
||||
if (ContentStatus.PUBLISHED.getCode().equals(drama.getStatus())) {
|
||||
throw new ServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "已上线的短剧需先下线再删除");
|
||||
}
|
||||
dramaMapper.deleteById(id);
|
||||
dramaEpisodeMapper.delete(Wrappers.<DramaEpisode>lambdaQuery().eq(DramaEpisode::getDramaId, id));
|
||||
}
|
||||
|
||||
/** 审核与发布的状态流转。 */
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void changeStatus(Long id, String action, String reason, Long operatorId, String operatorName) {
|
||||
Drama drama = getExisting(id);
|
||||
String current = drama.getStatus();
|
||||
String target = switch (action) {
|
||||
case "SUBMIT" -> {
|
||||
requireState(current, "SUBMIT", Set.of(ContentStatus.DRAFT.getCode(), ContentStatus.OFFLINE.getCode()));
|
||||
yield ContentStatus.REVIEWING.getCode();
|
||||
}
|
||||
case "APPROVE" -> {
|
||||
requireState(current, "APPROVE", Set.of(ContentStatus.REVIEWING.getCode()));
|
||||
yield ContentStatus.APPROVED.getCode();
|
||||
}
|
||||
case "REJECT" -> {
|
||||
requireState(current, "REJECT", Set.of(ContentStatus.REVIEWING.getCode()));
|
||||
yield ContentStatus.DRAFT.getCode();
|
||||
}
|
||||
case "PUBLISH" -> {
|
||||
requireState(current, "PUBLISH", Set.of(ContentStatus.APPROVED.getCode(),
|
||||
ContentStatus.OFFLINE.getCode()));
|
||||
yield ContentStatus.PUBLISHED.getCode();
|
||||
}
|
||||
case "OFFLINE" -> {
|
||||
requireState(current, "OFFLINE", Set.of(ContentStatus.PUBLISHED.getCode()));
|
||||
yield ContentStatus.OFFLINE.getCode();
|
||||
}
|
||||
default -> throw new ServiceException(ErrorCode.BAD_REQUEST, "未知的审核动作: " + action);
|
||||
};
|
||||
|
||||
drama.setStatus(target);
|
||||
if (ContentStatus.PUBLISHED.getCode().equals(target)) {
|
||||
drama.setPublishedAt(LocalDateTime.now());
|
||||
}
|
||||
dramaMapper.updateById(drama);
|
||||
|
||||
DramaReviewRecord record = new DramaReviewRecord();
|
||||
record.setBizType("DRAMA");
|
||||
record.setBizId(id);
|
||||
record.setAction(action);
|
||||
record.setReason(reason == null ? "" : reason);
|
||||
record.setOperatorId(operatorId == null ? 0L : operatorId);
|
||||
record.setOperatorName(operatorName == null ? "" : operatorName);
|
||||
dramaReviewRecordMapper.insert(record);
|
||||
|
||||
}
|
||||
|
||||
public List<DramaReviewRecord> listReviewRecords(Long dramaId) {
|
||||
return dramaReviewRecordMapper.selectList(Wrappers.<DramaReviewRecord>lambdaQuery()
|
||||
.eq(DramaReviewRecord::getBizType, "DRAMA")
|
||||
.eq(DramaReviewRecord::getBizId, dramaId)
|
||||
.orderByDesc(DramaReviewRecord::getCreatedAt));
|
||||
}
|
||||
|
||||
// ========== C 端只读视图:只暴露已上线内容 ==========
|
||||
|
||||
/** 已上线短剧,按运营排序 + 播放量 */
|
||||
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();
|
||||
}
|
||||
|
||||
/** 已上线短剧,按上线时间倒序 */
|
||||
public List<Drama> listPublishedLatest(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())
|
||||
.orderByDesc(Drama::getPublishedAt)
|
||||
.orderByDesc(Drama::getId)).getRecords();
|
||||
}
|
||||
|
||||
/** 已上线的热门短剧 */
|
||||
public List<Drama> listPublishedHot(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(Drama::getIsHot, 1)
|
||||
.orderByDesc(Drama::getSort)
|
||||
.orderByDesc(Drama::getViewCount)).getRecords();
|
||||
}
|
||||
|
||||
/** 已上线的短剧,未上线/已下线一律按不存在处理 */
|
||||
public Drama getPublished(Long id) {
|
||||
Drama drama = getExisting(id);
|
||||
if (!ContentStatus.PUBLISHED.getCode().equals(drama.getStatus())) {
|
||||
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "短剧不存在或已下线");
|
||||
}
|
||||
return drama;
|
||||
}
|
||||
|
||||
/** C 端:按 ID 批量取已上线短剧(收藏与观看记录用),返回顺序由调用方决定。 */
|
||||
public List<Drama> listPublishedByIds(List<Long> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return dramaMapper.selectList(Wrappers.<Drama>lambdaQuery()
|
||||
.in(Drama::getId, ids)
|
||||
.eq(Drama::getStatus, ContentStatus.PUBLISHED.getCode()));
|
||||
}
|
||||
|
||||
/** 按 ID 批量取短剧,不限状态:后台查观看流水时要能看到已下线短剧的名字。 */
|
||||
public List<Drama> listByIds(List<Long> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return dramaMapper.selectList(Wrappers.<Drama>lambdaQuery().in(Drama::getId, ids));
|
||||
}
|
||||
|
||||
/** 剧集数量变化后同步冗余字段。 */
|
||||
public void syncTotalEpisodes(Long dramaId) {
|
||||
Long count = dramaEpisodeMapper.selectCount(
|
||||
Wrappers.<DramaEpisode>lambdaQuery().eq(DramaEpisode::getDramaId, dramaId));
|
||||
Drama drama = dramaMapper.selectById(dramaId);
|
||||
if (drama != null) {
|
||||
drama.setTotalEpisodes(count == null ? 0 : count.intValue());
|
||||
dramaMapper.updateById(drama);
|
||||
}
|
||||
}
|
||||
|
||||
private void requireState(String current, String action, Set<String> allowed) {
|
||||
if (!allowed.contains(current)) {
|
||||
throw new ServiceException(ErrorCode.OPERATION_NOT_ALLOWED,
|
||||
"当前状态(" + ContentStatus.textOf(current) + ")不允许执行该操作");
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
drama.setGenre(req.getGenre() == null ? "" : req.getGenre());
|
||||
drama.setSynopsis(req.getDescription() == null ? "" : req.getDescription());
|
||||
drama.setCoverUrl(req.getCover() == null ? "" : req.getCover());
|
||||
drama.setHorizontalCoverUrl(drama.getHorizontalCoverUrl() == null ? "" : drama.getHorizontalCoverUrl());
|
||||
drama.setLanguage(req.getLanguage() == null ? "zh-CN" : req.getLanguage());
|
||||
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);
|
||||
drama.setSort(req.getSort() == null ? 0 : req.getSort());
|
||||
}
|
||||
|
||||
private String generateCode() {
|
||||
Long count = dramaMapper.selectCount(null);
|
||||
return "D" + LocalDate.now().format(CODE_DATE)
|
||||
+ String.format("%03d", (count == null ? 0 : count + 1));
|
||||
}
|
||||
|
||||
private List<DramaRespVO> toRespList(List<Drama> dramas) {
|
||||
if (dramas.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<Long, String> categories = dramaCategoryService.nameMap();
|
||||
return dramas.stream().map(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.setGenre(drama.getGenre());
|
||||
vo.setDescription(drama.getSynopsis());
|
||||
vo.setCover(drama.getCoverUrl());
|
||||
vo.setLanguage(drama.getLanguage());
|
||||
vo.setEpisodes(drama.getTotalEpisodes());
|
||||
vo.setFreeEpisodes(drama.getFreeEpisodes());
|
||||
vo.setUnlockCoin(drama.getUnlockCoin());
|
||||
vo.setIsHot(drama.getIsHot() != null && drama.getIsHot() == 1);
|
||||
vo.setStatus(drama.getStatus());
|
||||
vo.setStatusText(ContentStatus.textOf(drama.getStatus()));
|
||||
vo.setViews(drama.getViewCount());
|
||||
vo.setSort(drama.getSort());
|
||||
vo.setPublishAt(drama.getPublishedAt());
|
||||
vo.setCreatedAt(drama.getCreatedAt());
|
||||
vo.setUpdatedAt(drama.getUpdatedAt());
|
||||
return vo;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
package com.fruitshorts.content.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.fruitshorts.common.exception.ErrorCode;
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
import com.fruitshorts.content.dal.entity.DramaEpisode;
|
||||
import com.fruitshorts.content.dal.mapper.DramaEpisodeMapper;
|
||||
import com.fruitshorts.content.enums.ContentStatus;
|
||||
import com.fruitshorts.content.vo.EpisodeRespVO;
|
||||
import com.fruitshorts.content.vo.EpisodeSaveReqVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class EpisodeService {
|
||||
|
||||
private final DramaEpisodeMapper dramaEpisodeMapper;
|
||||
private final DramaService dramaService;
|
||||
|
||||
public List<EpisodeRespVO> listByDrama(Long dramaId, String status) {
|
||||
dramaService.getExisting(dramaId);
|
||||
return dramaEpisodeMapper.selectList(Wrappers.<DramaEpisode>lambdaQuery()
|
||||
.eq(DramaEpisode::getDramaId, dramaId)
|
||||
.eq(status != null && !status.isBlank(), DramaEpisode::getStatus, status)
|
||||
.orderByAsc(DramaEpisode::getSort)
|
||||
.orderByAsc(DramaEpisode::getEpNo))
|
||||
.stream().map(this::toResp).toList();
|
||||
}
|
||||
|
||||
/** C 端:只返回已上线剧集,按播放顺序 */
|
||||
public List<DramaEpisode> listPublishedByDrama(Long dramaId) {
|
||||
return dramaEpisodeMapper.selectList(Wrappers.<DramaEpisode>lambdaQuery()
|
||||
.eq(DramaEpisode::getDramaId, dramaId)
|
||||
.eq(DramaEpisode::getStatus, ContentStatus.PUBLISHED.getCode())
|
||||
.orderByAsc(DramaEpisode::getSort)
|
||||
.orderByAsc(DramaEpisode::getEpNo));
|
||||
}
|
||||
|
||||
/**
|
||||
* C 端:批量取每部剧的首个可播免费集,用于列表卡片直接起播。
|
||||
* 整屏卡片共用一次 IN 查询,避免逐条查库。
|
||||
*/
|
||||
public Map<Long, DramaEpisode> firstPlayableByDramas(List<Long> dramaIds) {
|
||||
if (dramaIds == null || dramaIds.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
List<DramaEpisode> episodes = dramaEpisodeMapper.selectList(
|
||||
Wrappers.<DramaEpisode>lambdaQuery()
|
||||
.in(DramaEpisode::getDramaId, dramaIds)
|
||||
.eq(DramaEpisode::getStatus, ContentStatus.PUBLISHED.getCode())
|
||||
.eq(DramaEpisode::getIsFree, 1)
|
||||
.isNotNull(DramaEpisode::getVideoUrl)
|
||||
.ne(DramaEpisode::getVideoUrl, "")
|
||||
.orderByAsc(DramaEpisode::getDramaId)
|
||||
.orderByAsc(DramaEpisode::getSort)
|
||||
.orderByAsc(DramaEpisode::getEpNo));
|
||||
Map<Long, DramaEpisode> firstPlayable = new LinkedHashMap<>();
|
||||
for (DramaEpisode episode : episodes) {
|
||||
firstPlayable.putIfAbsent(episode.getDramaId(), episode);
|
||||
}
|
||||
return firstPlayable;
|
||||
}
|
||||
|
||||
/** C 端:已上线剧集,否则按不存在处理 */
|
||||
public DramaEpisode getPublished(Long id) {
|
||||
DramaEpisode episode = getExisting(id);
|
||||
if (!ContentStatus.PUBLISHED.getCode().equals(episode.getStatus())) {
|
||||
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "剧集不存在或未上线");
|
||||
}
|
||||
return episode;
|
||||
}
|
||||
|
||||
public DramaEpisode getExisting(Long id) {
|
||||
DramaEpisode episode = id == null ? null : dramaEpisodeMapper.selectById(id);
|
||||
if (episode == null) {
|
||||
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "剧集不存在");
|
||||
}
|
||||
return episode;
|
||||
}
|
||||
|
||||
/** 按 ID 批量取剧集,不限状态:后台查观看流水时用。 */
|
||||
public List<DramaEpisode> listByIds(List<Long> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return dramaEpisodeMapper.selectList(
|
||||
Wrappers.<DramaEpisode>lambdaQuery().in(DramaEpisode::getId, ids));
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long create(EpisodeSaveReqVO req) {
|
||||
dramaService.getExisting(req.getDramaId());
|
||||
ensureEpNoAvailable(req.getDramaId(), req.getNumber(), null);
|
||||
DramaEpisode episode = new DramaEpisode();
|
||||
apply(episode, req);
|
||||
episode.setStatus(req.getStatus() == null ? ContentStatus.DRAFT.getCode() : req.getStatus());
|
||||
episode.setPlayCount(0L);
|
||||
episode.setVideoAssetId(0L);
|
||||
dramaEpisodeMapper.insert(episode);
|
||||
dramaService.syncTotalEpisodes(req.getDramaId());
|
||||
return episode.getId();
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void update(Long id, EpisodeSaveReqVO req) {
|
||||
DramaEpisode episode = getExisting(id);
|
||||
dramaService.getExisting(req.getDramaId());
|
||||
ensureEpNoAvailable(req.getDramaId(), req.getNumber(), id);
|
||||
apply(episode, req);
|
||||
if (req.getStatus() != null) {
|
||||
episode.setStatus(req.getStatus());
|
||||
}
|
||||
dramaEpisodeMapper.updateById(episode);
|
||||
if (!req.getDramaId().equals(episode.getDramaId())) {
|
||||
dramaService.syncTotalEpisodes(req.getDramaId());
|
||||
}
|
||||
dramaService.syncTotalEpisodes(episode.getDramaId());
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long id) {
|
||||
DramaEpisode episode = getExisting(id);
|
||||
dramaEpisodeMapper.deleteById(id);
|
||||
dramaService.syncTotalEpisodes(episode.getDramaId());
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateSort(List<Long> ids) {
|
||||
for (int index = 0; index < ids.size(); index++) {
|
||||
DramaEpisode episode = dramaEpisodeMapper.selectById(ids.get(index));
|
||||
if (episode != null) {
|
||||
episode.setSort(index + 1);
|
||||
dramaEpisodeMapper.updateById(episode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureEpNoAvailable(Long dramaId, Integer epNo, Long excludeId) {
|
||||
Long count = dramaEpisodeMapper.selectCount(Wrappers.<DramaEpisode>lambdaQuery()
|
||||
.eq(DramaEpisode::getDramaId, dramaId)
|
||||
.eq(DramaEpisode::getEpNo, epNo)
|
||||
.ne(excludeId != null, DramaEpisode::getId, excludeId));
|
||||
if (count != null && count > 0) {
|
||||
throw new ServiceException(ErrorCode.DATA_EXISTS, "第 " + epNo + " 集已存在");
|
||||
}
|
||||
}
|
||||
|
||||
private void apply(DramaEpisode episode, EpisodeSaveReqVO req) {
|
||||
episode.setDramaId(req.getDramaId());
|
||||
episode.setEpNo(req.getNumber());
|
||||
episode.setTitle(req.getTitle() == null ? "" : req.getTitle());
|
||||
episode.setDurationSec(req.getDurationSec() == null ? 0 : req.getDurationSec());
|
||||
episode.setCoverUrl(req.getCover() == null ? "" : req.getCover());
|
||||
episode.setVideoUrl(req.getVideoUrl() == null ? "" : req.getVideoUrl());
|
||||
episode.setIsFree(Boolean.TRUE.equals(req.getIsFree()) ? 1 : 0);
|
||||
episode.setUnlockCoin(req.getUnlockCoin() == null ? 0 : req.getUnlockCoin());
|
||||
episode.setSort(req.getSort() == null ? (req.getNumber() == null ? 0 : req.getNumber()) : req.getSort());
|
||||
}
|
||||
|
||||
private EpisodeRespVO toResp(DramaEpisode episode) {
|
||||
EpisodeRespVO vo = new EpisodeRespVO();
|
||||
vo.setId(episode.getId());
|
||||
vo.setDramaId(episode.getDramaId());
|
||||
vo.setNumber(episode.getEpNo());
|
||||
vo.setTitle(episode.getTitle());
|
||||
vo.setDurationSec(episode.getDurationSec());
|
||||
vo.setDuration(formatDuration(episode.getDurationSec()));
|
||||
vo.setCover(episode.getCoverUrl());
|
||||
vo.setVideoUrl(episode.getVideoUrl());
|
||||
vo.setIsFree(episode.getIsFree() != null && episode.getIsFree() == 1);
|
||||
vo.setUnlockCoin(episode.getUnlockCoin());
|
||||
vo.setStatus(episode.getStatus());
|
||||
vo.setStatusText(ContentStatus.textOf(episode.getStatus()));
|
||||
vo.setViews(episode.getPlayCount());
|
||||
vo.setSort(episode.getSort());
|
||||
return vo;
|
||||
}
|
||||
|
||||
private String formatDuration(Integer seconds) {
|
||||
int value = seconds == null ? 0 : seconds;
|
||||
return String.format("%02d:%02d", value / 60, value % 60);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.fruitshorts.content.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class BannerRespVO {
|
||||
|
||||
private Long id;
|
||||
private String imageUrl;
|
||||
private String position;
|
||||
private String positionText;
|
||||
private String linkType;
|
||||
private String linkTypeText;
|
||||
private String linkValue;
|
||||
/** 跳转目标的展示文案,如短剧名称 */
|
||||
private String linkText;
|
||||
private Integer sort;
|
||||
private Integer status;
|
||||
private String statusText;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.fruitshorts.content.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class BannerSaveReqVO {
|
||||
|
||||
@jakarta.validation.constraints.NotBlank(message = "请上传轮播图片")
|
||||
private String imageUrl;
|
||||
|
||||
/** HOME_TOP / DISCOVER */
|
||||
private String position;
|
||||
|
||||
/** NONE / DRAMA / URL */
|
||||
private String linkType;
|
||||
|
||||
/** 剧集 ID 或跳转地址 */
|
||||
private String linkValue;
|
||||
|
||||
private Integer sort;
|
||||
private Integer status;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.fruitshorts.content.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DramaCategoryRespVO {
|
||||
|
||||
private Long id;
|
||||
private String name;
|
||||
private Integer sort;
|
||||
private Integer status;
|
||||
/** 该类型下的短剧数量 */
|
||||
private Integer dramaCount;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.fruitshorts.content.vo;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DramaCategorySaveReqVO {
|
||||
|
||||
@NotBlank(message = "请填写类型名称")
|
||||
private String name;
|
||||
|
||||
private Integer sort;
|
||||
private Integer status;
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.fruitshorts.content.vo;
|
||||
|
||||
import com.fruitshorts.common.pojo.PageParam;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class DramaPageReqVO extends PageParam {
|
||||
|
||||
/** 标题关键字 */
|
||||
private String keyword;
|
||||
private String status;
|
||||
private Long categoryId;
|
||||
/** 是否只看热门 */
|
||||
private Boolean isHot;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.fruitshorts.content.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 短剧列表/详情响应,字段名与后台页面表格列一致。
|
||||
*/
|
||||
@Data
|
||||
public class DramaRespVO {
|
||||
|
||||
private Long id;
|
||||
private String code;
|
||||
private String title;
|
||||
private String subtitle;
|
||||
private Long categoryId;
|
||||
private String category;
|
||||
private String genre;
|
||||
private String description;
|
||||
private String cover;
|
||||
private String language;
|
||||
private Integer episodes;
|
||||
private Integer freeEpisodes;
|
||||
private Integer unlockCoin;
|
||||
private Boolean isHot;
|
||||
private String status;
|
||||
private String statusText;
|
||||
private Long views;
|
||||
private Integer sort;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime publishAt;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.fruitshorts.content.vo;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DramaSaveReqVO {
|
||||
|
||||
@NotBlank(message = "请填写短剧名称")
|
||||
private String title;
|
||||
|
||||
private String subtitle;
|
||||
private Long categoryId;
|
||||
private String genre;
|
||||
private String description;
|
||||
private String cover;
|
||||
private String language;
|
||||
private Integer freeEpisodes;
|
||||
private Integer unlockCoin;
|
||||
/** 是否热门 */
|
||||
private Boolean isHot;
|
||||
private Integer sort;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.fruitshorts.content.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DramaStatusReqVO {
|
||||
|
||||
/** 驳回原因,其他动作可空 */
|
||||
private String reason;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.fruitshorts.content.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class EpisodeRespVO {
|
||||
|
||||
private Long id;
|
||||
private Long dramaId;
|
||||
private Integer number;
|
||||
private String title;
|
||||
/** 展示用时长,mm:ss */
|
||||
private String duration;
|
||||
private Integer durationSec;
|
||||
private String cover;
|
||||
private String videoUrl;
|
||||
private Boolean isFree;
|
||||
private Integer unlockCoin;
|
||||
private String status;
|
||||
private String statusText;
|
||||
private Long views;
|
||||
private Integer sort;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.fruitshorts.content.vo;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class EpisodeSaveReqVO {
|
||||
|
||||
@NotNull(message = "请选择所属短剧")
|
||||
private Long dramaId;
|
||||
|
||||
@NotNull(message = "请填写集号")
|
||||
private Integer number;
|
||||
|
||||
private String title;
|
||||
private Integer durationSec;
|
||||
private String cover;
|
||||
private String videoUrl;
|
||||
private Boolean isFree;
|
||||
private Integer unlockCoin;
|
||||
private String status;
|
||||
private Integer sort;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.fruitshorts.content.vo;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class EpisodeSortReqVO {
|
||||
|
||||
@NotEmpty(message = "请提供排序后的剧集 ID 列表")
|
||||
private List<Long> ids;
|
||||
}
|
||||
Reference in new issue
Block a user