feat(creator): 新增一键发布与删除作品接口
This commit is contained in:
1 parent
a36276051d
commit
af0bf7f90e
7 files changed
+185
-4
No files matched your search
+20
@@ -12,6 +12,8 @@ import com.fruitshorts.creator.service.CreatorContentService;
|
||||
import com.fruitshorts.creator.vo.CreatorAttachReqVO;
|
||||
import com.fruitshorts.creator.vo.CreatorDramaEditReqVO;
|
||||
import com.fruitshorts.creator.vo.CreatorEpisodeReqVO;
|
||||
import com.fruitshorts.creator.vo.CreatorPublishReqVO;
|
||||
import com.fruitshorts.creator.vo.CreatorPublishRespVO;
|
||||
import com.fruitshorts.creator.vo.CreatorSubmitReqVO;
|
||||
import com.fruitshorts.framework.log.OperationLog;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -53,6 +55,15 @@ public class CreatorContentController {
|
||||
return Result.ok(creatorContentService.createDrama(req));
|
||||
}
|
||||
|
||||
/** 一键发布:建作品 + 建分集 + 绑定视频(可选同时提交审核),一个事务完成。 */
|
||||
@Operation(summary = "一键发布(建作品 / 分集 / 绑定视频 / 可选提交审核)")
|
||||
@PostMapping("/publish")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
|
||||
@OperationLog(module = "创作者中心", action = "一键发布作品")
|
||||
public Result<CreatorPublishRespVO> publish(@Valid @RequestBody CreatorPublishReqVO req) {
|
||||
return Result.ok(creatorContentService.publish(req));
|
||||
}
|
||||
|
||||
/** 编辑作品信息(仅草稿 / 已下线可改)。 */
|
||||
@Operation(summary = "编辑作品信息(仅草稿 / 已下线可改)")
|
||||
@PutMapping("/dramas/{id}")
|
||||
@@ -101,6 +112,15 @@ public class CreatorContentController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "删除作品")
|
||||
@DeleteMapping("/dramas/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
|
||||
@OperationLog(module = "创作者中心", action = "删除作品")
|
||||
public Result<Void> deleteDrama(@PathVariable Long id) {
|
||||
creatorContentService.deleteDrama(id);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/** 批量排序:body 里按新顺序给出分集 id。 */
|
||||
@Operation(summary = "分集批量排序")
|
||||
@PutMapping("/dramas/{id}/episodes/sort")
|
||||
|
||||
+2
-2
@@ -31,14 +31,14 @@ public class DictController {
|
||||
|
||||
@Operation(summary = "短剧分类字典")
|
||||
@GetMapping("/drama-category")
|
||||
@PreAuthorize("@ss.hasPermission('drama:list')")
|
||||
@PreAuthorize("@ss.hasAnyPermission('drama:list','creator-center:view')")
|
||||
public Result<List<DramaCategoryRespVO>> dramaCategories() {
|
||||
return Result.ok(dramaCategoryService.list());
|
||||
}
|
||||
|
||||
@Operation(summary = "内容状态字典")
|
||||
@GetMapping("/content-status")
|
||||
@PreAuthorize("@ss.hasPermission('drama:list')")
|
||||
@PreAuthorize("@ss.hasAnyPermission('drama:list','creator-center:view')")
|
||||
public Result<List<Map<String, String>>> contentStatus() {
|
||||
return Result.ok(Arrays.stream(ContentStatus.values())
|
||||
.map(status -> Map.of("code", status.getCode(), "text", status.getText()))
|
||||
|
||||
+78
-1
@@ -21,9 +21,12 @@ import com.fruitshorts.creator.dal.entity.ContentSubmission;
|
||||
import com.fruitshorts.creator.dal.entity.Creator;
|
||||
import com.fruitshorts.creator.vo.CreatorDramaEditReqVO;
|
||||
import com.fruitshorts.creator.vo.CreatorEpisodeReqVO;
|
||||
import com.fruitshorts.creator.vo.CreatorPublishReqVO;
|
||||
import com.fruitshorts.creator.vo.CreatorPublishRespVO;
|
||||
import com.fruitshorts.framework.security.SecurityUtils;
|
||||
import com.fruitshorts.media.dal.entity.MediaAsset;
|
||||
import com.fruitshorts.media.dal.mapper.MediaAssetMapper;
|
||||
import com.fruitshorts.media.service.EpisodeTranscodeBindingService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -49,6 +52,7 @@ public class CreatorContentService {
|
||||
private final PriceService priceService;
|
||||
private final DramaEpisodeMapper episodeMapper;
|
||||
private final MediaAssetMapper mediaAssetMapper;
|
||||
private final EpisodeTranscodeBindingService transcodeBindingService;
|
||||
|
||||
/** 新建草稿:归属写自己,状态固定 draft(是否上线由后续提交审核决定)。 */
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -59,6 +63,59 @@ public class CreatorContentService {
|
||||
return dramaService.createForCreator(toSaveReq(req), creator.getId(), creator.getAdminUserId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键发布:建作品 + 建全部分集 + 绑定视频 + (可选)提交审核,一个事务做完。
|
||||
*
|
||||
* <p>抖音式创作流程的落点:创作者先选好视频(前端已按顺序排列并截好封面),
|
||||
* 这里按数组顺序分配集号。任何一集绑定失败都整体回滚,不会留下半成品草稿。
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CreatorPublishRespVO publish(CreatorPublishReqVO req) {
|
||||
Creator creator = centerService.requireCurrentCreator();
|
||||
centerService.requireWritable();
|
||||
requireApprovedCreator(creator);
|
||||
List<CreatorPublishReqVO.EpisodeItem> items = req.getEpisodes();
|
||||
if (items == null || items.isEmpty()) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "请至少上传一集视频");
|
||||
}
|
||||
|
||||
CreatorDramaEditReqVO dramaReq = new CreatorDramaEditReqVO();
|
||||
dramaReq.setTitle(req.getTitle());
|
||||
dramaReq.setSubtitle(req.getSubtitle());
|
||||
dramaReq.setCategoryId(req.getCategoryId());
|
||||
dramaReq.setGenre(req.getGenre());
|
||||
dramaReq.setDescription(req.getDescription());
|
||||
dramaReq.setCover(req.getCover());
|
||||
dramaReq.setLanguage(req.getLanguage());
|
||||
if ((dramaReq.getCover() == null || dramaReq.getCover().isBlank())
|
||||
&& items.get(0).getCover() != null) {
|
||||
// 没单独传作品封面时,用第一集封面兜底:抖音式流程里用户通常只截了视频首帧
|
||||
dramaReq.setCover(items.get(0).getCover());
|
||||
}
|
||||
Long dramaId = createDrama(dramaReq);
|
||||
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
CreatorPublishReqVO.EpisodeItem item = items.get(i);
|
||||
CreatorEpisodeReqVO ep = new CreatorEpisodeReqVO();
|
||||
ep.setEpNo(i + 1);
|
||||
ep.setTitle(item.getTitle() == null || item.getTitle().isBlank()
|
||||
? "第 " + (i + 1) + " 集" : item.getTitle().trim());
|
||||
ep.setDurationSec(item.getDurationSec());
|
||||
ep.setCover(item.getCover());
|
||||
Long episodeId = addEpisode(dramaId, ep);
|
||||
attachAsset(dramaId, episodeId, item.getAssetId());
|
||||
}
|
||||
|
||||
Long submissionId = null;
|
||||
if (Boolean.TRUE.equals(req.getSubmit())) {
|
||||
ContentSubmission submission = submissionService.submit(
|
||||
creator, dramaId, ContentSubmissionService.TYPE_CREATE);
|
||||
submissionId = submission.getId();
|
||||
}
|
||||
Drama created = dramaService.getExisting(dramaId);
|
||||
return new CreatorPublishRespVO(dramaId, submissionId, created.getStatus());
|
||||
}
|
||||
|
||||
/** 编辑作品信息。定价、热门、排序不在本接口的可改范围,原样回填,避免被默认值冲掉。 */
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateDrama(Long dramaId, CreatorDramaEditReqVO req) {
|
||||
@@ -195,7 +252,10 @@ public class CreatorContentService {
|
||||
"只能把视频产物(bizType=EPISODE)挂到分集,当前是 " + asset.getBizType());
|
||||
}
|
||||
String assetStatus = asset.getStatus() == null ? "" : asset.getStatus();
|
||||
if (!"ready".equals(assetStatus) && !"uploaded".equals(assetStatus)) {
|
||||
// transcoding 也要放行:转码任务在 upload complete 时就已入队,此时任务还没绑定分集,
|
||||
// 必须先把分集挂上再让 bind 绑任务,否则 worker 一直跳过(episode_id 未绑定)。
|
||||
if (!"ready".equals(assetStatus) && !"uploaded".equals(assetStatus)
|
||||
&& !"transcoding".equals(assetStatus)) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "产物尚未就绪,当前状态:" + assetStatus);
|
||||
}
|
||||
|
||||
@@ -206,6 +266,8 @@ public class CreatorContentService {
|
||||
episode.setDurationSec(asset.getDurationSec());
|
||||
}
|
||||
episodeMapper.updateById(episode);
|
||||
// 绑定转码任务:把排队中的任务挂到这部剧 / 这一集,worker 才会开始转码并回写 HLS
|
||||
transcodeBindingService.bind(episode.getId(), dramaId, episode.getVideoUrl());
|
||||
}
|
||||
|
||||
public PriceRespVO priceDetail(Long dramaId) {
|
||||
@@ -260,6 +322,21 @@ public class CreatorContentService {
|
||||
"「" + ContentStatus.textOf(drama.getStatus()) + "」" + reason);
|
||||
}
|
||||
|
||||
/** 草稿 / 已下线 / 审核中(允许撤掉)均可直接删;已上线的走 DramaService 自己拦。 */
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteDrama(Long dramaId) {
|
||||
Creator creator = centerService.requireCurrentCreator();
|
||||
centerService.requireWritable();
|
||||
Drama drama = dramaService.getExisting(dramaId);
|
||||
if (!Objects.equals(creator.getId(), drama.getCreatorId())) {
|
||||
throw new ServiceException(ErrorCode.FORBIDDEN, "无权删除该作品");
|
||||
}
|
||||
// 清 submissions(审核中的、撤回的、已审核的),否则残留 submission 挂在不存在的 drama 上
|
||||
submissionService.deleteByDrama(dramaId);
|
||||
// 再调 DramaService.delete(禁删 PUBLISHED、级联删 drama_episode)
|
||||
dramaService.delete(dramaId);
|
||||
}
|
||||
|
||||
private void requireApprovedCreator(Creator creator) {
|
||||
if (!CreatorService.STATUS_APPROVED.equals(creator.getStatus())) {
|
||||
throw new ServiceException(ErrorCode.FORBIDDEN,
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.creator.vo;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 创作者中心 · 一键发布(抖音式:选视频 → 填标题简介 → 发布)。
|
||||
*
|
||||
* 一次请求把「建作品 + 建分集 + 绑定视频」全部做完,避免前端分多次调用时
|
||||
* 中途失败留下半成品草稿。视频必须先经分片上传拿到 assetId,再由这里绑定。
|
||||
*/
|
||||
@Data
|
||||
public class CreatorPublishReqVO {
|
||||
|
||||
@NotBlank(message = "请填写作品名称")
|
||||
private String title;
|
||||
|
||||
private String subtitle;
|
||||
/** 短剧分类 id(创作者下拉选择,如都市 / 古装 / 甜宠) */
|
||||
private Long categoryId;
|
||||
/** 题材标签(可选,自由文本,如"闪婚"、"霸总") */
|
||||
private String genre;
|
||||
private String description;
|
||||
/** 作品封面;不传时取第一集封面 */
|
||||
private String cover;
|
||||
private String language;
|
||||
|
||||
/** 创建后立即提交审核,默认 true(抖音式一键发布) */
|
||||
private Boolean submit = true;
|
||||
|
||||
@Valid
|
||||
@NotEmpty(message = "请至少上传一集视频")
|
||||
private List<EpisodeItem> episodes;
|
||||
|
||||
@Data
|
||||
public static class EpisodeItem {
|
||||
|
||||
@NotNull(message = "缺少媒资 id")
|
||||
private Long assetId;
|
||||
|
||||
private String title;
|
||||
private Integer durationSec;
|
||||
private String cover;
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.creator.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
/** 一键发布结果:作品 id 与本次提交的审核单 id(不提交审核时为空)。 */
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class CreatorPublishRespVO {
|
||||
|
||||
private Long dramaId;
|
||||
private Long submissionId;
|
||||
/** 作品状态:提交审核后为 reviewing */
|
||||
private String status;
|
||||
}
|
||||
+3
-1
@@ -261,7 +261,9 @@ INSERT INTO sys_menu (id, parent_id, title, route_path, route_name, component, r
|
||||
(27, 26, '我的作品', 'workspace', 'creator-workspace', 'views/creator-center/WorkspaceView', '', '', 'creator-center:view', 'MENU', 0, 0, 1, 1),
|
||||
(28, 26, '作品编辑', 'dramas/:id', 'creator-drama-edit', 'views/creator-center/DramaEditView', '', '', 'creator-center:drama:edit', 'MENU', 0, 0, 2, 0),
|
||||
(29, 26, '收费配置', 'prices', 'creator-prices', 'views/creator-center/PriceConfigView', '', '', 'creator-center:price:update', 'MENU', 0, 0, 3, 1),
|
||||
(30, 26, '数据看板', 'dashboard', 'creator-dashboard', 'views/creator-center/DashboardView', '', '', 'creator-center:stat:view', 'MENU', 0, 0, 4, 1)
|
||||
(30, 26, '数据看板', 'dashboard', 'creator-dashboard', 'views/creator-center/DashboardView', '', '', 'creator-center:stat:view', 'MENU', 0, 0, 4, 1),
|
||||
-- 31 是隐藏路由(一键发布页,从「我的作品」页的发布按钮进入,不进侧边栏)
|
||||
(31, 26, '发布作品', 'publish', 'creator-publish', 'views/creator-center/PublishView', '', '', 'creator-center:upload', 'MENU', 0, 0, 0, 0)
|
||||
ON CONFLICT (id) DO UPDATE SET parent_id = EXCLUDED.parent_id, title = EXCLUDED.title,
|
||||
route_path = EXCLUDED.route_path, route_name = EXCLUDED.route_name, component = EXCLUDED.component,
|
||||
redirect = EXCLUDED.redirect, icon = EXCLUDED.icon, permission_code = EXCLUDED.permission_code,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Author: fruit.com.cn
|
||||
-- 创作者中心「一键发布」隐藏路由(幂等,由发布流程执行)
|
||||
-- visible=0:只作为路由存在,从「我的作品」页的发布按钮进入,不进侧边栏。
|
||||
|
||||
INSERT INTO sys_menu (id, parent_id, title, route_path, route_name, component, redirect, icon,
|
||||
permission_code, type, always_show, affix, sort, visible) VALUES
|
||||
(31, 26, '发布作品', 'publish', 'creator-publish', 'views/creator-center/PublishView',
|
||||
'', '', 'creator-center:upload', 'MENU', 0, 0, 0, 0)
|
||||
ON CONFLICT (id) DO UPDATE SET parent_id = EXCLUDED.parent_id, title = EXCLUDED.title,
|
||||
route_path = EXCLUDED.route_path, route_name = EXCLUDED.route_name, component = EXCLUDED.component,
|
||||
redirect = EXCLUDED.redirect, icon = EXCLUDED.icon, permission_code = EXCLUDED.permission_code,
|
||||
type = EXCLUDED.type, always_show = EXCLUDED.always_show, affix = EXCLUDED.affix,
|
||||
sort = EXCLUDED.sort, visible = EXCLUDED.visible;
|
||||
Reference in new issue
Block a user