feat: 07 创作者中心批 3(分片上传挂载 + 分集收费与试看)
为什么:创作者传完片子得把产物挂到分集上才能播,而这条链路之前有两个洞:
uploadId 是"凭证",谁拿到都能往别人的会话里塞分片 / 合并;产物挂载也没有归属校验,
拿到别人的 assetId 就能把自己的剧指向别人的素材。定价这边则缺创作者入口与试看秒数配置。
- 上传复用现有 /admin/media/upload/{init,chunk,complete},不重写;新增 GET /admin/media/upload/{uploadId}
做断点续传回显(receivedChunks/totalChunks/percent/status,完成后带 url)
- 上传会话归属校验:chunk / complete / 进度查询都要求会话发起人,别人拿到 uploadId 也操作不了(403)
- 新增 attach 接口:产物必须① 是本次登录的人上传的(media_asset.create_by)② bizType=EPISODE 且状态 ready/uploaded
③ 分集属于自己这部剧且作品可编辑;挂载时把产物时长补到分集上
- 收费配置复用 PriceService:新增创作者侧 GET/PUT /admin/creator-center/dramas/{id}/prices;
PriceSaveReqVO/PriceRespVO 补 previewSeconds(落在已有的 drama_episode.preview_seconds,不新造一套)
- 定价沿用"可编辑状态"口径:draft/offline 可改,已上线要先申请下线;dramaId 以路径为准,别人的 episodeId 被忽略
- 权限点 creator-center:upload / creator-center:price:update;创作者角色同时获得 cover:upload / episode:upload(复用上传接口)
实测:未登录请求付费集返回 {"accessType":"COIN_REQUIRED","unlockCoin":50,"previewSeconds":15},
免费集直接拿到签名播放地址 —— 创作者改的价格确实走到了 C 端付费判定。
This commit is contained in:
1 parent
255bcbf02b
commit
8b8ebf944f
12 files changed
+269
-10
No files matched your search
+51
-2
@@ -12,6 +12,7 @@ import com.fruitshorts.media.vo.MediaAssetRespVO;
|
||||
import com.fruitshorts.media.vo.UploadCompleteReqVO;
|
||||
import com.fruitshorts.media.vo.UploadInitReqVO;
|
||||
import com.fruitshorts.media.vo.UploadInitRespVO;
|
||||
import com.fruitshorts.media.vo.UploadProgressRespVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -63,8 +64,15 @@ public class MediaService {
|
||||
return new UploadInitRespVO(task.getUploadId(), chunkSize, totalChunks, task.getObjectKey());
|
||||
}
|
||||
|
||||
public void saveChunk(String uploadId, int index, MultipartFile file) {
|
||||
/**
|
||||
* 续传分片。
|
||||
*
|
||||
* uploadId 相当于一张"上传凭证",谁拿到都能往里塞分片,所以这里必须校验会话归属:
|
||||
* 只有发起 init 的那个人能继续传自己的会话。
|
||||
*/
|
||||
public void saveChunk(String uploadId, int index, MultipartFile file, Long userId) {
|
||||
MediaUploadTask task = getTask(uploadId);
|
||||
requireTaskOwner(task, userId);
|
||||
if (index < 0 || index >= task.getTotalChunks()) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "分片序号越界: " + index);
|
||||
}
|
||||
@@ -77,8 +85,9 @@ public class MediaService {
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public MediaAssetRespVO complete(UploadCompleteReqVO req) {
|
||||
public MediaAssetRespVO complete(UploadCompleteReqVO req, Long userId) {
|
||||
MediaUploadTask task = getTask(req.getUploadId());
|
||||
requireTaskOwner(task, userId);
|
||||
if (TASK_COMPLETED.equals(task.getStatus()) && task.getAssetId() > 0) {
|
||||
return get(task.getAssetId());
|
||||
}
|
||||
@@ -128,6 +137,46 @@ public class MediaService {
|
||||
return toResp(asset);
|
||||
}
|
||||
|
||||
/** 断点续传用:回显传到第几片、还剩多少;会话归属同样校验。 */
|
||||
public UploadProgressRespVO progress(String uploadId, Long userId) {
|
||||
MediaUploadTask task = getTask(uploadId);
|
||||
requireTaskOwner(task, userId);
|
||||
|
||||
UploadProgressRespVO vo = new UploadProgressRespVO();
|
||||
vo.setUploadId(task.getUploadId());
|
||||
vo.setFileName(task.getFileName());
|
||||
vo.setBizType(task.getBizType());
|
||||
vo.setFileSize(task.getFileSize());
|
||||
vo.setChunkSize(task.getChunkSize());
|
||||
vo.setTotalChunks(task.getTotalChunks());
|
||||
int received = task.getReceivedChunks() == null ? 0 : task.getReceivedChunks();
|
||||
vo.setReceivedChunks(received);
|
||||
int total = task.getTotalChunks() == null || task.getTotalChunks() == 0 ? 1 : task.getTotalChunks();
|
||||
vo.setPercent(Math.min(100, received * 100 / total));
|
||||
vo.setStatus(task.getStatus());
|
||||
vo.setAssetId(task.getAssetId() == null ? 0L : task.getAssetId());
|
||||
if (task.getAssetId() != null && task.getAssetId() > 0
|
||||
&& TASK_COMPLETED.equals(task.getStatus())) {
|
||||
MediaAsset asset = mediaAssetMapper.selectById(task.getAssetId());
|
||||
vo.setUrl(asset == null ? "" : asset.getMp4Url());
|
||||
} else {
|
||||
vo.setUrl("");
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传会话归属:只有发起人自己能续传 / 合并 / 查进度。
|
||||
* 不区分是后台管理员还是创作者 —— 凭证归谁就是谁的。
|
||||
*/
|
||||
private void requireTaskOwner(MediaUploadTask task, Long userId) {
|
||||
long owner = task.getCreateBy() == null ? 0L : task.getCreateBy();
|
||||
long current = userId == null ? 0L : userId;
|
||||
if (owner != current) {
|
||||
throw new ServiceException(ErrorCode.FORBIDDEN, "无权操作该上传会话");
|
||||
}
|
||||
}
|
||||
|
||||
private MediaUploadTask getTask(String uploadId) {
|
||||
MediaUploadTask task = mediaUploadTaskMapper.selectOne(
|
||||
Wrappers.<MediaUploadTask>lambdaQuery().eq(MediaUploadTask::getUploadId, uploadId));
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.fruitshorts.media.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 分片上传进度:断点续传时客户端拿它恢复"传到第几片、还剩多少"。
|
||||
*
|
||||
* percent 由服务端算好,避免两端各算一遍口径不一致。
|
||||
*/
|
||||
@Data
|
||||
public class UploadProgressRespVO {
|
||||
|
||||
private String uploadId;
|
||||
private String fileName;
|
||||
private String bizType;
|
||||
private Long fileSize;
|
||||
private Integer chunkSize;
|
||||
private Integer totalChunks;
|
||||
private Integer receivedChunks;
|
||||
/** 0-100,按已收分片 / 总分片计算 */
|
||||
private Integer percent;
|
||||
/** uploading / completed */
|
||||
private String status;
|
||||
/** 合并完成后才有 */
|
||||
private Long assetId;
|
||||
private String url;
|
||||
}
|
||||
Reference in new issue
Block a user