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
+33
-2
@@ -229,6 +229,18 @@ POST /app/auth/register
|
||||
新增字段:短剧 `rating`(内容分级)、`regionCodes`(可播区域)、版权窗口、
|
||||
`creatorId`、`tags`、`trailerUrl`;剧集 `transcodeStatus`、`hlsUrl`、`previewSeconds`、字幕。
|
||||
|
||||
媒资上传补充(批 3 加固,后台与创作者共用同一套接口,未重写):
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| POST | `/admin/media/upload/init` | 建上传会话(返回 `uploadId` / `chunkSize` / `totalChunks` / `objectKey`) |
|
||||
| POST | `/admin/media/upload/chunk` | 传分片(`uploadId` + `index` + `file`),可重复传同一片 |
|
||||
| GET | `/admin/media/upload/{uploadId}` | **新增**:断点续传回显(`receivedChunks` / `totalChunks` / `percent` / `status` / 完成后带 `url`) |
|
||||
| POST | `/admin/media/upload/complete` | 合并分片出媒资(幂等:同一 uploadId 重复调用返回同一产物) |
|
||||
|
||||
**上传会话归属**:`uploadId` 相当于上传凭证,`chunk` / `complete` / 进度查询都校验会话发起人,
|
||||
别人拿到 `uploadId` 也塞不进分片(`{"code":403,"message":"无权操作该上传会话"}`)。
|
||||
|
||||
### 2.3 App 用户管理(已实现)
|
||||
|
||||
| 方法 | 路径 | 权限点 | 说明 |
|
||||
@@ -354,7 +366,7 @@ POST /app/auth/register
|
||||
菜单、权限点与超管授权由 `sql/data-phase1.sql` 幂等落库(内容治理 / 订单与权益 / 数据看板挂菜单 id 20–25,创作者两条为 17–19)。
|
||||
页面只读态与可操作态按权限点区分:没有 `*:update` / `*:audit` 的角色进入页面时按钮隐藏或显示「只读」。
|
||||
|
||||
### 2.10 创作者中心(批 1 + 批 2 已实现:工作台 / 内容编辑与提交)
|
||||
### 2.10 创作者中心(批 1 ~ 批 3 已实现:工作台 / 内容编辑 / 上传挂载 / 收费配置)
|
||||
|
||||
创作者 Web 端做在后台管理里(`ReelShortWeb`),身份复用 `sys_admin_user`:
|
||||
`creator.admin_user_id` 绑定后台账号(部分唯一索引,0 = 未绑定),角色 `creator` 只拿 `creator-center:*` 权限点。
|
||||
@@ -405,7 +417,26 @@ POST /app/auth/register
|
||||
后台审核 `PUBLISH` 通过时,该剧仍处于 `draft` 的分集**一并上线**(否则会出现"剧上线了却一集都播不了");
|
||||
后台**驳回**或创作者**撤回**时,处于 `reviewing` 的剧退回 `draft`,避免创作者卡在只读状态。
|
||||
|
||||
**进度**:批 3(分片上传、分集收费配置)、批 4(创作者数据看板)、批 5(前端页面)未开始。
|
||||
**上传挂载与收费配置(批 3)**
|
||||
|
||||
| 方法 | 路径 | 权限点 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| POST | `/admin/creator-center/dramas/{id}/episodes/{episodeId}/attach` | `creator-center:upload` | 把已上传的媒资挂到自己的分集(写 `video_asset_id` / `video_url`,时长为 0 时用产物时长补齐) |
|
||||
| GET | `/admin/creator-center/dramas/{id}/prices` | `creator-center:view` | 读整剧免费集数 + 逐集免费 / 收费 / 试看秒数 |
|
||||
| PUT | `/admin/creator-center/dramas/{id}/prices` | `creator-center:price:update` | 保存收费与试看秒数,复用后台同一套 `PriceService` |
|
||||
|
||||
**上传走已有的 `/admin/media/upload/{init,chunk,complete}`**(创作者角色直接持 `cover:upload` / `episode:upload`,
|
||||
不新造一套上传接口);挂载时三道校验:分集属于自己这部剧 → 作品可编辑 → **产物是本次登录的人自己上传的**
|
||||
(`media_asset.create_by` 必须等于当前账号,否则 `{"code":403,"message":"无权使用该上传产物"}`)。
|
||||
只接受 `bizType=EPISODE` 且状态为 `ready` / `uploaded` 的产物。
|
||||
|
||||
**收费配置**:`previewSeconds` 落在 `drama_episode.preview_seconds`(0 = 不限制),
|
||||
与 `is_free` / `unlock_coin` 一起就是播放与下载付费判定读的字段,**改完立即生效**(实测:未登录请求付费集返回
|
||||
`{"accessType":"COIN_REQUIRED","unlockCoin":50,"previewSeconds":15}`;免费集直接返回签名播放地址)。
|
||||
定价沿用「可编辑状态」口径:`draft` / `offline` 可改,已上线要改价需先申请下线(避免创作者悄悄改已上线内容的价格);
|
||||
`dramaId` 以路径为准,body 里传别人的剧 id 无效,列表里带别人的 `episodeId` 会被忽略(只作用于自己剧的分集)。
|
||||
|
||||
**进度**:批 4(创作者数据看板)、批 5(前端页面)未开始。
|
||||
|
||||
## 3. 契约与实现状态对照
|
||||
|
||||
|
||||
+28
@@ -3,8 +3,11 @@ package com.fruitshorts.admin.controller;
|
||||
import com.fruitshorts.common.pojo.Result;
|
||||
import com.fruitshorts.content.vo.EpisodeRespVO;
|
||||
import com.fruitshorts.content.vo.EpisodeSortReqVO;
|
||||
import com.fruitshorts.content.vo.PriceRespVO;
|
||||
import com.fruitshorts.content.vo.PriceSaveReqVO;
|
||||
import com.fruitshorts.creator.dal.entity.ContentSubmission;
|
||||
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.CreatorSubmitReqVO;
|
||||
@@ -118,4 +121,29 @@ public class CreatorContentController {
|
||||
public Result<List<ContentSubmission>> listSubmissions(@PathVariable Long id) {
|
||||
return Result.ok(creatorContentService.listSubmissions(id));
|
||||
}
|
||||
|
||||
/** 把上传好的视频产物挂到自己的分集上(产物必须是本人上传的)。 */
|
||||
@PostMapping("/dramas/{id}/episodes/{episodeId}/attach")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:upload')")
|
||||
@OperationLog(module = "创作者中心", action = "挂载分集视频")
|
||||
public Result<Void> attachAsset(@PathVariable Long id, @PathVariable Long episodeId,
|
||||
@Valid @RequestBody CreatorAttachReqVO req) {
|
||||
creatorContentService.attachAsset(id, episodeId, req.getAssetId());
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/** 分集收费与试看秒数:读现状。 */
|
||||
@GetMapping("/dramas/{id}/prices")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:view')")
|
||||
public Result<PriceRespVO> priceDetail(@PathVariable Long id) {
|
||||
return Result.ok(creatorContentService.priceDetail(id));
|
||||
}
|
||||
|
||||
/** 分集收费与试看秒数:保存(复用后台同一套 PriceService,改完立即影响付费判定)。 */
|
||||
@PutMapping("/dramas/{id}/prices")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:price:update')")
|
||||
@OperationLog(module = "创作者中心", action = "配置分集价格")
|
||||
public Result<PriceRespVO> savePrice(@PathVariable Long id, @RequestBody PriceSaveReqVO req) {
|
||||
return Result.ok(creatorContentService.savePrice(id, req));
|
||||
}
|
||||
}
|
||||
+10
-2
@@ -7,6 +7,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 jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -40,14 +41,21 @@ public class MediaController {
|
||||
public Result<Integer> uploadChunk(@RequestParam String uploadId,
|
||||
@RequestParam int index,
|
||||
@RequestParam("file") MultipartFile file) {
|
||||
mediaService.saveChunk(uploadId, index, file);
|
||||
mediaService.saveChunk(uploadId, index, file, SecurityUtils.getUserId());
|
||||
return Result.ok(index);
|
||||
}
|
||||
|
||||
@PostMapping("/upload/complete")
|
||||
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
|
||||
public Result<MediaAssetRespVO> complete(@Valid @RequestBody UploadCompleteReqVO req) {
|
||||
return Result.ok(mediaService.complete(req));
|
||||
return Result.ok(mediaService.complete(req, SecurityUtils.getUserId()));
|
||||
}
|
||||
|
||||
/** 断点续传:查上传会话进度(只能查自己发起的会话)。 */
|
||||
@GetMapping("/upload/{uploadId}")
|
||||
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
|
||||
public Result<UploadProgressRespVO> progress(@PathVariable String uploadId) {
|
||||
return Result.ok(mediaService.progress(uploadId, SecurityUtils.getUserId()));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
|
||||
+14
-1
@@ -50,6 +50,7 @@ public class PriceService {
|
||||
item.setTitle(episode.getTitle());
|
||||
item.setFree(episode.getIsFree() != null && episode.getIsFree() == 1);
|
||||
item.setUnlockCoin(episode.getUnlockCoin());
|
||||
item.setPreviewSeconds(episode.getPreviewSeconds() == null ? 0 : episode.getPreviewSeconds());
|
||||
return item;
|
||||
}).toList());
|
||||
return vo;
|
||||
@@ -89,6 +90,7 @@ public class PriceService {
|
||||
int coin = isFree == 1 ? 0 : unlockCoin;
|
||||
|
||||
PriceSaveReqVO.EpisodePrice override = overrides.get(episode.getId());
|
||||
Integer previewSeconds = null;
|
||||
if (override != null) {
|
||||
if (override.getIsFree() != null) {
|
||||
isFree = override.getIsFree() == 1 ? 1 : 0;
|
||||
@@ -97,11 +99,21 @@ public class PriceService {
|
||||
if (isFree == 0 && override.getUnlockCoin() != null) {
|
||||
coin = Math.max(0, override.getUnlockCoin());
|
||||
}
|
||||
if (override.getPreviewSeconds() != null) {
|
||||
previewSeconds = Math.max(0, override.getPreviewSeconds());
|
||||
}
|
||||
}
|
||||
|
||||
if (!Objects.equals(episode.getIsFree(), isFree) || !Objects.equals(episode.getUnlockCoin(), coin)) {
|
||||
boolean priceChanged = !Objects.equals(episode.getIsFree(), isFree)
|
||||
|| !Objects.equals(episode.getUnlockCoin(), coin);
|
||||
boolean previewChanged = previewSeconds != null
|
||||
&& !Objects.equals(episode.getPreviewSeconds(), previewSeconds);
|
||||
if (priceChanged || previewChanged) {
|
||||
episode.setIsFree(isFree);
|
||||
episode.setUnlockCoin(coin);
|
||||
if (previewSeconds != null) {
|
||||
episode.setPreviewSeconds(previewSeconds);
|
||||
}
|
||||
dramaEpisodeMapper.updateById(episode);
|
||||
changed++;
|
||||
}
|
||||
@@ -120,6 +132,7 @@ public class PriceService {
|
||||
item.setTitle(episode.getTitle());
|
||||
item.setFree(episode.getIsFree() != null && episode.getIsFree() == 1);
|
||||
item.setUnlockCoin(episode.getUnlockCoin());
|
||||
item.setPreviewSeconds(episode.getPreviewSeconds() == null ? 0 : episode.getPreviewSeconds());
|
||||
return item;
|
||||
}).toList());
|
||||
return vo;
|
||||
|
||||
@@ -25,5 +25,7 @@ public class PriceRespVO {
|
||||
private String title;
|
||||
private Boolean free;
|
||||
private Integer unlockCoin;
|
||||
/** 试看秒数,0 = 不限制 */
|
||||
private Integer previewSeconds;
|
||||
}
|
||||
}
|
||||
+2
@@ -22,5 +22,7 @@ public class PriceSaveReqVO {
|
||||
/** 1 免费 / 0 收费 */
|
||||
private Integer isFree;
|
||||
private Integer unlockCoin;
|
||||
/** 试看秒数:0 = 不限制(复用 drama_episode.preview_seconds,播放接口已经在下发这个字段) */
|
||||
private Integer previewSeconds;
|
||||
}
|
||||
}
|
||||
@@ -27,5 +27,10 @@
|
||||
<groupId>com.fruitshorts</groupId>
|
||||
<artifactId>fruitshorts-module-user</artifactId>
|
||||
</dependency>
|
||||
<!-- 创作者把上传产物挂到自己的分集,要读媒资库;media 只依赖 framework,方向不会成环 -->
|
||||
<dependency>
|
||||
<groupId>com.fruitshorts</groupId>
|
||||
<artifactId>fruitshorts-module-media</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+68
@@ -12,10 +12,16 @@ import com.fruitshorts.content.service.EpisodeService;
|
||||
import com.fruitshorts.content.vo.DramaSaveReqVO;
|
||||
import com.fruitshorts.content.vo.EpisodeRespVO;
|
||||
import com.fruitshorts.content.vo.EpisodeSaveReqVO;
|
||||
import com.fruitshorts.content.vo.PriceRespVO;
|
||||
import com.fruitshorts.content.vo.PriceSaveReqVO;
|
||||
import com.fruitshorts.content.service.PriceService;
|
||||
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.framework.security.SecurityUtils;
|
||||
import com.fruitshorts.media.dal.entity.MediaAsset;
|
||||
import com.fruitshorts.media.dal.mapper.MediaAssetMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -38,7 +44,9 @@ public class CreatorContentService {
|
||||
private final ContentSubmissionService submissionService;
|
||||
private final DramaService dramaService;
|
||||
private final EpisodeService episodeService;
|
||||
private final PriceService priceService;
|
||||
private final DramaEpisodeMapper episodeMapper;
|
||||
private final MediaAssetMapper mediaAssetMapper;
|
||||
|
||||
/** 新建草稿:归属写自己,状态固定 draft(是否上线由后续提交审核决定)。 */
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -155,6 +163,66 @@ public class CreatorContentService {
|
||||
return submissionService.listByDrama(creator.getId(), dramaId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把上传好的媒资挂到分集上(视频源)。
|
||||
*
|
||||
* 三道闸:① 分集必须属于自己这部剧(requireMyDrama + requireMyEpisode);
|
||||
* ② 作品得是可编辑状态;③ **产物必须是这次登录的人自己上传的** ——
|
||||
* 否则拿到别人的 assetId 就能把别人的素材挂到自己的剧上(或反过来污染别人的剧)。
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void attachAsset(Long dramaId, Long episodeId, Long assetId) {
|
||||
Drama drama = centerService.requireMyDrama(dramaId);
|
||||
requireEditable(drama);
|
||||
DramaEpisode episode = requireMyEpisode(dramaId, episodeId);
|
||||
|
||||
MediaAsset asset = assetId == null ? null : mediaAssetMapper.selectById(assetId);
|
||||
if (asset == null) {
|
||||
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "媒资不存在");
|
||||
}
|
||||
long owner = asset.getCreateBy() == null ? 0L : asset.getCreateBy();
|
||||
long current = SecurityUtils.getUserId() == null ? -1L : SecurityUtils.getUserId();
|
||||
if (owner != current) {
|
||||
throw new ServiceException(ErrorCode.FORBIDDEN, "无权使用该上传产物");
|
||||
}
|
||||
if (!"EPISODE".equals(asset.getBizType())) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST,
|
||||
"只能把视频产物(bizType=EPISODE)挂到分集,当前是 " + asset.getBizType());
|
||||
}
|
||||
String assetStatus = asset.getStatus() == null ? "" : asset.getStatus();
|
||||
if (!"ready".equals(assetStatus) && !"uploaded".equals(assetStatus)) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "产物尚未就绪,当前状态:" + assetStatus);
|
||||
}
|
||||
|
||||
episode.setVideoAssetId(asset.getId());
|
||||
episode.setVideoUrl(asset.getMp4Url() == null ? "" : asset.getMp4Url());
|
||||
if ((episode.getDurationSec() == null || episode.getDurationSec() == 0)
|
||||
&& asset.getDurationSec() != null) {
|
||||
episode.setDurationSec(asset.getDurationSec());
|
||||
}
|
||||
episodeMapper.updateById(episode);
|
||||
}
|
||||
|
||||
public PriceRespVO priceDetail(Long dramaId) {
|
||||
centerService.requireMyDrama(dramaId);
|
||||
return priceService.detail(dramaId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存分集收费与试看秒数:复用后台同一套 PriceService(改完立即影响播放/下载的付费判定)。
|
||||
*
|
||||
* 两点约束:dramaId 以路径为准(body 里传别人的 id 无效);作品必须处于可编辑状态 ——
|
||||
* 让创作者改已上线内容的定价风险太大,要改价先申请下线。这条口径写进 docs/api-phase1.md,
|
||||
* 需要放开的话改一行就行。
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PriceRespVO savePrice(Long dramaId, PriceSaveReqVO req) {
|
||||
Drama drama = centerService.requireMyDrama(dramaId);
|
||||
requireEditable(drama);
|
||||
req.setDramaId(drama.getId());
|
||||
return priceService.save(req);
|
||||
}
|
||||
|
||||
/** 分集归属:分集必须属于这部剧,否则 403(防止拿自己的剧 id 配别人的分集 id)。 */
|
||||
private DramaEpisode requireMyEpisode(Long dramaId, Long episodeId) {
|
||||
DramaEpisode episode = episodeId == null ? null : episodeMapper.selectById(episodeId);
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.fruitshorts.creator.vo;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/** 创作者中心 · 把已上传的媒资挂到自己的分集上。 */
|
||||
@Data
|
||||
public class CreatorAttachReqVO {
|
||||
|
||||
@NotNull(message = "缺少媒资 ID")
|
||||
private Long assetId;
|
||||
}
|
||||
+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;
|
||||
}
|
||||
+17
-3
@@ -179,7 +179,9 @@ ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, code = EXCLUDED.code,
|
||||
-- 权限点按批补:批 1 只有「进创作者中心」这一个,后面各批用到什么再幂等追加
|
||||
INSERT INTO sys_permission (code, name, group_name, type, sort) VALUES
|
||||
('creator-center:view', '访问创作者中心', '创作者中心', 'BUTTON', 150),
|
||||
('creator-center:drama:edit', '创作者中心-编辑作品', '创作者中心', 'BUTTON', 151)
|
||||
('creator-center:drama:edit', '创作者中心-编辑作品', '创作者中心', 'BUTTON', 151),
|
||||
('creator-center:upload', '创作者中心-上传与挂载', '创作者中心', 'BUTTON', 152),
|
||||
('creator-center:price:update', '创作者中心-配置分集价格', '创作者中心', 'BUTTON', 153)
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
-- 创作者角色只拿创作者中心这几个点,不给任何后台运营权限
|
||||
@@ -188,7 +190,8 @@ SELECT r.id, p.id
|
||||
FROM sys_role r
|
||||
CROSS JOIN sys_permission p
|
||||
WHERE r.code = 'creator'
|
||||
AND p.code IN ('creator-center:view', 'creator-center:drama:edit')
|
||||
AND p.code IN ('creator-center:view', 'creator-center:drama:edit',
|
||||
'creator-center:upload', 'creator-center:price:update')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- 超管也留一份,方便排查创作者视角的问题(超管没有绑定创作者主体,进 creator-center 仍会被挡)
|
||||
@@ -197,5 +200,16 @@ SELECT r.id, p.id
|
||||
FROM sys_role r
|
||||
CROSS JOIN sys_permission p
|
||||
WHERE r.code = 'super_admin'
|
||||
AND p.code IN ('creator-center:view', 'creator-center:drama:edit')
|
||||
AND p.code IN ('creator-center:view', 'creator-center:drama:edit',
|
||||
'creator-center:upload', 'creator-center:price:update')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- 媒资分片上传是复用后台已有的 /admin/media/upload/*,所以创作者角色要用现成的上传权限点,
|
||||
-- 不再造一套 creator-center 的上传接口(上传会话归属与产物归属另在服务层校验)。
|
||||
INSERT INTO sys_role_permission (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM sys_role r
|
||||
CROSS JOIN sys_permission p
|
||||
WHERE r.code = 'creator'
|
||||
AND p.code IN ('cover:upload', 'episode:upload')
|
||||
ON CONFLICT DO NOTHING;
|
||||
Reference in new issue
Block a user