feat: 初始化短剧服务端(管理后台 API + 客户端 API)
Spring Boot 3.2 / MyBatis-Plus / PostgreSQL,7 个 Maven 模块;含认证鉴权、内容与媒资管理、操作日志,以及客户端 /app/** 接口。
This commit is contained in:
commit
a4aa749cc6
152 files changed
+8027
No files matched your search
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.reelshort</groupId>
|
||||
<artifactId>reelshort</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>reelshort-module-media</artifactId>
|
||||
<name>reelshort-module-media</name>
|
||||
<description>媒资域:分片上传、本地存储、访问地址</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.reelshort</groupId>
|
||||
<artifactId>reelshort-framework</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.reelshort.media.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "reelshort.media")
|
||||
public class MediaProperties {
|
||||
|
||||
/** 本地存储根目录,可以是相对路径(相对应用启动目录)。 */
|
||||
private String rootDir = "./data/uploads";
|
||||
|
||||
/** 对外访问前缀,与静态资源映射保持一致。 */
|
||||
private String publicPrefix = "/files";
|
||||
|
||||
/** 分片大小(字节),默认 5MB。 */
|
||||
private long chunkSize = 5L * 1024 * 1024;
|
||||
|
||||
/** 单个文件大小上限(字节),默认 2GB。 */
|
||||
private long maxFileSize = 2L * 1024 * 1024 * 1024;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.reelshort.media.config;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.Files;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 把本地媒资目录映射成静态资源,前端直接用 /files/** 访问。
|
||||
*/
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class MediaWebConfig implements WebMvcConfigurer {
|
||||
|
||||
private final MediaProperties properties;
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
Path root = Paths.get(properties.getRootDir()).toAbsolutePath().normalize();
|
||||
// Spring 6 会跳过启动时还不存在的静态资源目录,这里先确保目录存在
|
||||
try {
|
||||
Files.createDirectories(root);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("无法创建媒资目录: " + root, ex);
|
||||
}
|
||||
String prefix = properties.getPublicPrefix();
|
||||
if (!prefix.endsWith("/")) {
|
||||
prefix = prefix + "/";
|
||||
}
|
||||
registry.addResourceHandler(prefix + "**")
|
||||
.addResourceLocations(root.toUri().toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.reelshort.media.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("media_asset")
|
||||
public class MediaAsset {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String bizType;
|
||||
private String fileName;
|
||||
private String objectKey;
|
||||
private String sourceUrl;
|
||||
private Long sizeBytes;
|
||||
private String md5;
|
||||
private Integer durationSec;
|
||||
private Integer width;
|
||||
private Integer height;
|
||||
private String status;
|
||||
private String hlsUrl;
|
||||
private String mp4Url;
|
||||
private String posterUrl;
|
||||
private Long createBy;
|
||||
private LocalDateTime createdAt;
|
||||
/** updated_at 交给数据库的 ON UPDATE CURRENT_TIMESTAMP 维护 */
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private LocalDateTime updatedAt;
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.reelshort.media.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.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("media_upload_task")
|
||||
public class MediaUploadTask {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String uploadId;
|
||||
private String bizType;
|
||||
private String fileName;
|
||||
private Long fileSize;
|
||||
private Integer chunkSize;
|
||||
private Integer totalChunks;
|
||||
private Integer receivedChunks;
|
||||
private String objectKey;
|
||||
private String status;
|
||||
private Long assetId;
|
||||
private Long createBy;
|
||||
private LocalDateTime createdAt;
|
||||
/** updated_at 交给数据库的 ON UPDATE CURRENT_TIMESTAMP 维护 */
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.reelshort.media.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.reelshort.media.dal.entity.MediaAsset;
|
||||
|
||||
public interface MediaAssetMapper extends BaseMapper<MediaAsset> {
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.reelshort.media.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.reelshort.media.dal.entity.MediaUploadTask;
|
||||
|
||||
public interface MediaUploadTaskMapper extends BaseMapper<MediaUploadTask> {
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package com.reelshort.media.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.reelshort.common.exception.ErrorCode;
|
||||
import com.reelshort.common.exception.ServiceException;
|
||||
import com.reelshort.media.config.MediaProperties;
|
||||
import com.reelshort.media.dal.entity.MediaAsset;
|
||||
import com.reelshort.media.dal.entity.MediaUploadTask;
|
||||
import com.reelshort.media.dal.mapper.MediaAssetMapper;
|
||||
import com.reelshort.media.dal.mapper.MediaUploadTaskMapper;
|
||||
import com.reelshort.media.vo.MediaAssetRespVO;
|
||||
import com.reelshort.media.vo.UploadCompleteReqVO;
|
||||
import com.reelshort.media.vo.UploadInitReqVO;
|
||||
import com.reelshort.media.vo.UploadInitRespVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MediaService {
|
||||
|
||||
private static final String STATUS_READY = "ready";
|
||||
private static final String STATUS_UPLOADED = "uploaded";
|
||||
private static final String TASK_UPLOADING = "uploading";
|
||||
private static final String TASK_COMPLETED = "completed";
|
||||
private static final Set<String> IMAGE_EXTENSIONS =
|
||||
Set.of(".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".avif");
|
||||
|
||||
private final MediaProperties properties;
|
||||
private final MediaStorageService storageService;
|
||||
private final MediaAssetMapper mediaAssetMapper;
|
||||
private final MediaUploadTaskMapper mediaUploadTaskMapper;
|
||||
|
||||
public UploadInitRespVO init(UploadInitReqVO req, Long userId) {
|
||||
if (req.getFileSize() > properties.getMaxFileSize()) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST,
|
||||
"文件超过上限 " + (properties.getMaxFileSize() / 1024 / 1024) + "MB");
|
||||
}
|
||||
long chunkSize = req.getChunkSize() != null && req.getChunkSize() > 0
|
||||
? Math.min(req.getChunkSize(), properties.getChunkSize())
|
||||
: properties.getChunkSize();
|
||||
int totalChunks = (int) Math.ceil((double) req.getFileSize() / chunkSize);
|
||||
|
||||
MediaUploadTask task = new MediaUploadTask();
|
||||
task.setUploadId(UUID.randomUUID().toString().replace("-", ""));
|
||||
task.setBizType(normalizeBizType(req.getBizType()));
|
||||
task.setFileName(req.getFileName());
|
||||
task.setFileSize(req.getFileSize());
|
||||
task.setChunkSize((int) chunkSize);
|
||||
task.setTotalChunks(totalChunks);
|
||||
task.setReceivedChunks(0);
|
||||
task.setObjectKey(storageService.buildObjectKey(task.getBizType(), req.getFileName()));
|
||||
task.setStatus(TASK_UPLOADING);
|
||||
task.setAssetId(0L);
|
||||
task.setCreateBy(userId == null ? 0L : userId);
|
||||
mediaUploadTaskMapper.insert(task);
|
||||
|
||||
return new UploadInitRespVO(task.getUploadId(), chunkSize, totalChunks, task.getObjectKey());
|
||||
}
|
||||
|
||||
public void saveChunk(String uploadId, int index, MultipartFile file) {
|
||||
MediaUploadTask task = getTask(uploadId);
|
||||
if (index < 0 || index >= task.getTotalChunks()) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "分片序号越界: " + index);
|
||||
}
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "分片内容为空");
|
||||
}
|
||||
storageService.saveChunk(uploadId, index, file);
|
||||
task.setReceivedChunks(storageService.countChunks(uploadId));
|
||||
mediaUploadTaskMapper.updateById(task);
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public MediaAssetRespVO complete(UploadCompleteReqVO req) {
|
||||
MediaUploadTask task = getTask(req.getUploadId());
|
||||
if (TASK_COMPLETED.equals(task.getStatus()) && task.getAssetId() > 0) {
|
||||
return get(task.getAssetId());
|
||||
}
|
||||
int chunks = storageService.countChunks(req.getUploadId());
|
||||
if (chunks < task.getTotalChunks()) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST,
|
||||
"分片未传完(" + chunks + "/" + task.getTotalChunks() + ")");
|
||||
}
|
||||
|
||||
MediaStorageService.MergeResult merged =
|
||||
storageService.merge(req.getUploadId(), task.getObjectKey(), task.getTotalChunks());
|
||||
String publicUrl = storageService.toPublicUrl(task.getObjectKey());
|
||||
|
||||
MediaAsset asset = new MediaAsset();
|
||||
asset.setBizType(task.getBizType());
|
||||
asset.setFileName(task.getFileName());
|
||||
asset.setObjectKey(task.getObjectKey());
|
||||
asset.setSourceUrl(publicUrl);
|
||||
asset.setSizeBytes(merged.size());
|
||||
asset.setMd5(merged.md5());
|
||||
asset.setDurationSec(req.getDurationSec() == null ? 0 : req.getDurationSec());
|
||||
asset.setWidth(0);
|
||||
asset.setHeight(0);
|
||||
asset.setStatus(isImage(task.getFileName()) ? STATUS_READY : STATUS_UPLOADED);
|
||||
asset.setHlsUrl("");
|
||||
// 没有转码前,mp4_url 直接指向原文件,播放器可以先跑通
|
||||
asset.setMp4Url(publicUrl);
|
||||
asset.setPosterUrl("");
|
||||
asset.setCreateBy(task.getCreateBy());
|
||||
mediaAssetMapper.insert(asset);
|
||||
|
||||
task.setStatus(TASK_COMPLETED);
|
||||
task.setAssetId(asset.getId());
|
||||
task.setReceivedChunks(chunks);
|
||||
mediaUploadTaskMapper.updateById(task);
|
||||
storageService.cleanChunkDir(req.getUploadId());
|
||||
|
||||
// 重新查一次,拿到数据库生成的 created_at
|
||||
return toResp(mediaAssetMapper.selectById(asset.getId()));
|
||||
}
|
||||
|
||||
public MediaAssetRespVO get(Long id) {
|
||||
MediaAsset asset = id == null ? null : mediaAssetMapper.selectById(id);
|
||||
if (asset == null) {
|
||||
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "媒资不存在");
|
||||
}
|
||||
return toResp(asset);
|
||||
}
|
||||
|
||||
private MediaUploadTask getTask(String uploadId) {
|
||||
MediaUploadTask task = mediaUploadTaskMapper.selectOne(
|
||||
Wrappers.<MediaUploadTask>lambdaQuery().eq(MediaUploadTask::getUploadId, uploadId));
|
||||
if (task == null) {
|
||||
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "上传会话不存在或已过期");
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
private String normalizeBizType(String bizType) {
|
||||
if (bizType == null || bizType.isBlank()) {
|
||||
return "EPISODE";
|
||||
}
|
||||
return bizType.trim().toUpperCase();
|
||||
}
|
||||
|
||||
private boolean isImage(String fileName) {
|
||||
if (fileName == null) {
|
||||
return false;
|
||||
}
|
||||
String lower = fileName.toLowerCase();
|
||||
return IMAGE_EXTENSIONS.stream().anyMatch(lower::endsWith);
|
||||
}
|
||||
|
||||
private MediaAssetRespVO toResp(MediaAsset asset) {
|
||||
MediaAssetRespVO vo = new MediaAssetRespVO();
|
||||
vo.setId(asset.getId());
|
||||
vo.setBizType(asset.getBizType());
|
||||
vo.setFileName(asset.getFileName());
|
||||
vo.setObjectKey(asset.getObjectKey());
|
||||
vo.setUrl(asset.getSourceUrl());
|
||||
vo.setSizeBytes(asset.getSizeBytes());
|
||||
vo.setMd5(asset.getMd5());
|
||||
vo.setDurationSec(asset.getDurationSec());
|
||||
vo.setStatus(asset.getStatus());
|
||||
vo.setCreatedAt(asset.getCreatedAt());
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
package com.reelshort.media.service;
|
||||
|
||||
import com.reelshort.common.exception.ErrorCode;
|
||||
import com.reelshort.common.exception.ServiceException;
|
||||
import com.reelshort.media.config.MediaProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HexFormat;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 本地磁盘媒资存储:分片写临时目录,完成后合并成正式文件。
|
||||
* 之后要换成对象存储时,只需替换本类实现。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MediaStorageService {
|
||||
|
||||
private static final DateTimeFormatter MONTH_DIR = DateTimeFormatter.ofPattern("yyyyMM");
|
||||
|
||||
private final MediaProperties properties;
|
||||
|
||||
public record MergeResult(Path path, long size, String md5) {
|
||||
}
|
||||
|
||||
public Path rootDir() {
|
||||
Path root = Paths.get(properties.getRootDir()).toAbsolutePath().normalize();
|
||||
try {
|
||||
Files.createDirectories(root);
|
||||
Files.createDirectories(root.resolve("tmp"));
|
||||
} catch (IOException ex) {
|
||||
throw new ServiceException("无法创建媒资目录: " + root);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
public Path chunkDir(String uploadId) {
|
||||
return rootDir().resolve("tmp").resolve(uploadId);
|
||||
}
|
||||
|
||||
public void saveChunk(String uploadId, int index, MultipartFile file) {
|
||||
Path dir = chunkDir(uploadId);
|
||||
try {
|
||||
Files.createDirectories(dir);
|
||||
Path target = dir.resolve(index + ".part");
|
||||
try (InputStream in = file.getInputStream()) {
|
||||
Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new ServiceException("写入分片失败: " + index);
|
||||
}
|
||||
}
|
||||
|
||||
public int countChunks(String uploadId) {
|
||||
Path dir = chunkDir(uploadId);
|
||||
if (!Files.isDirectory(dir)) {
|
||||
return 0;
|
||||
}
|
||||
try (var stream = Files.list(dir)) {
|
||||
return (int) stream.filter(path -> path.getFileName().toString().endsWith(".part")).count();
|
||||
} catch (IOException ex) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** 按分片序号顺序合并,同时计算 MD5。 */
|
||||
public MergeResult merge(String uploadId, String objectKey, int totalChunks) {
|
||||
Path target = rootDir().resolve(objectKey).normalize();
|
||||
if (!target.startsWith(rootDir())) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "非法的存储路径");
|
||||
}
|
||||
MessageDigest digest = digest();
|
||||
long size = 0;
|
||||
try {
|
||||
Files.createDirectories(target.getParent());
|
||||
try (OutputStream fileOut = Files.newOutputStream(target,
|
||||
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
DigestOutputStream out = new DigestOutputStream(fileOut, digest)) {
|
||||
for (int index = 0; index < totalChunks; index++) {
|
||||
Path part = chunkDir(uploadId).resolve(index + ".part");
|
||||
if (!Files.exists(part)) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "分片 " + index + " 缺失");
|
||||
}
|
||||
size += Files.copy(part, out);
|
||||
}
|
||||
}
|
||||
} catch (ServiceException ex) {
|
||||
throw ex;
|
||||
} catch (IOException ex) {
|
||||
throw new ServiceException("合并分片失败");
|
||||
}
|
||||
return new MergeResult(target, size, HexFormat.of().formatHex(digest.digest()));
|
||||
}
|
||||
|
||||
public void cleanChunkDir(String uploadId) {
|
||||
Path dir = chunkDir(uploadId);
|
||||
if (!Files.isDirectory(dir)) {
|
||||
return;
|
||||
}
|
||||
try (var stream = Files.walk(dir)) {
|
||||
stream.sorted((a, b) -> b.getNameCount() - a.getNameCount())
|
||||
.forEach(path -> {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (IOException ignored) {
|
||||
// 临时文件清理失败不影响主流程
|
||||
}
|
||||
});
|
||||
} catch (IOException ex) {
|
||||
log.warn("清理分片目录失败: {}", dir, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(String objectKey) {
|
||||
try {
|
||||
Files.deleteIfExists(rootDir().resolve(objectKey).normalize());
|
||||
} catch (IOException ex) {
|
||||
log.warn("删除媒资文件失败: {}", objectKey, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/** 生成对象键:bizType/yyyyMM/uuid.ext */
|
||||
public String buildObjectKey(String bizType, String originalName) {
|
||||
String type = (bizType == null || bizType.isBlank() ? "misc" : bizType).toLowerCase();
|
||||
return type + "/" + LocalDate.now().format(MONTH_DIR) + "/"
|
||||
+ UUID.randomUUID().toString().replace("-", "") + extension(originalName);
|
||||
}
|
||||
|
||||
public String toPublicUrl(String objectKey) {
|
||||
String prefix = properties.getPublicPrefix();
|
||||
if (prefix.endsWith("/")) {
|
||||
prefix = prefix.substring(0, prefix.length() - 1);
|
||||
}
|
||||
return prefix + "/" + objectKey;
|
||||
}
|
||||
|
||||
private String extension(String fileName) {
|
||||
if (fileName == null) {
|
||||
return "";
|
||||
}
|
||||
int dot = fileName.lastIndexOf('.');
|
||||
if (dot < 0 || dot == fileName.length() - 1) {
|
||||
return "";
|
||||
}
|
||||
String ext = fileName.substring(dot).toLowerCase();
|
||||
return ext.length() > 12 ? "" : ext;
|
||||
}
|
||||
|
||||
private MessageDigest digest() {
|
||||
try {
|
||||
return MessageDigest.getInstance("MD5");
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
throw new ServiceException("当前 JVM 不支持 MD5");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.reelshort.media.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class MediaAssetRespVO {
|
||||
|
||||
private Long id;
|
||||
private String bizType;
|
||||
private String fileName;
|
||||
private String objectKey;
|
||||
private String url;
|
||||
private Long sizeBytes;
|
||||
private String md5;
|
||||
private Integer durationSec;
|
||||
private String status;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.reelshort.media.vo;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UploadCompleteReqVO {
|
||||
|
||||
@NotBlank(message = "缺少 uploadId")
|
||||
private String uploadId;
|
||||
|
||||
/** 客户端探测到的时长(视频用) */
|
||||
private Integer durationSec;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.reelshort.media.vo;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UploadInitReqVO {
|
||||
|
||||
@NotBlank(message = "缺少文件名")
|
||||
private String fileName;
|
||||
|
||||
@Positive(message = "文件大小不合法")
|
||||
private long fileSize;
|
||||
|
||||
/** DRAMA_COVER / EPISODE / AVATAR */
|
||||
private String bizType = "EPISODE";
|
||||
|
||||
/** 客户端期望的分片大小,留空则用服务端配置 */
|
||||
private Long chunkSize;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.reelshort.media.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class UploadInitRespVO {
|
||||
|
||||
private String uploadId;
|
||||
private long chunkSize;
|
||||
private int totalChunks;
|
||||
private String objectKey;
|
||||
}
|
||||
Reference in new issue
Block a user