- 重构 TimelinePanel.vue 组件,使用 Tailwind CSS 替代 Less,简化样式代码 - 改进视觉设计:更新颜色方案、间距和图标,提升用户体验 - 移除音频结束位置标记,优化刻度尺和轨道显示逻辑 - 统一时长差异提示的样式和状态显示 feat(infra): 扩展文件预签名接口支持 Content-Type 参数 - 在 FileApi、FileClient、FileService 接口中新增带 Content-Type 参数的 presignGetUrl 方法 - 实现 S3FileClient 对 Content-Type 参数的支持,确保浏览器正确渲染媒体文件 - 在 TikUserFileServiceImpl 中为音视频文件生成预签名 URL 时自动推断 Content-Type - 支持公开访问和私有访问两种模式下的 Content-Type 参数传递
This commit is contained in:
@@ -1,94 +1,94 @@
|
||||
<template>
|
||||
<div class="timeline-panel">
|
||||
<div class="timeline-header">
|
||||
<span class="timeline-title">时间轴对比</span>
|
||||
<span v-if="showDurations" class="duration-badge">
|
||||
人脸 {{ formatDuration(faceDurationMs) }}
|
||||
<div class="bg-[#fafbfc] border border-[#e5e7eb] rounded-md px-4 py-3 mt-2">
|
||||
<div class="flex justify-between items-center mb-2.5">
|
||||
<span class="text-[13px] font-semibold text-gray-800">时间轴对比</span>
|
||||
<span v-if="showDurations" class="flex items-center gap-1.5 text-[13px] text-gray-600 tabular-nums">
|
||||
<span class="flex items-center gap-[5px]">
|
||||
<span class="w-[7px] h-[7px] rounded-sm bg-blue-500"></span>
|
||||
视频 {{ formatDuration(faceDurationMs) }}
|
||||
</span>
|
||||
<template v-if="audioDurationMs > 0">
|
||||
<span class="divider"></span>
|
||||
音频 {{ formatDuration(audioDurationMs) }}
|
||||
<span class="text-gray-400 text-xs">/</span>
|
||||
<span class="flex items-center gap-[5px]">
|
||||
<span class="w-[7px] h-[7px] rounded-sm bg-emerald-500"></span>
|
||||
音频 {{ formatDuration(audioDurationMs) }}
|
||||
</span>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 刻度尺 -->
|
||||
<div class="timeline-ruler">
|
||||
<div class="relative h-5 mb-1.5 ml-12">
|
||||
<div
|
||||
v-for="mark in rulerMarks"
|
||||
:key="mark.time"
|
||||
class="ruler-mark"
|
||||
class="absolute -translate-x-1/2 flex flex-col items-center"
|
||||
:style="{ left: mark.position + '%' }"
|
||||
>
|
||||
<span class="ruler-label">{{ mark.label }}</span>
|
||||
<span class="ruler-tick"></span>
|
||||
</div>
|
||||
|
||||
<!-- 音频结束位置标记 -->
|
||||
<div
|
||||
v-if="audioDurationMs > 0 && isExceed"
|
||||
class="audio-end-marker"
|
||||
:style="{ left: audioEndPosition + '%' }"
|
||||
>
|
||||
<span class="audio-marker-label">{{ (audioDurationMs / 1000).toFixed(1) }}s</span>
|
||||
<span class="audio-marker-line"></span>
|
||||
<span class="text-[11px] text-gray-400 leading-none mb-1">{{ mark.label }}</span>
|
||||
<span class="block w-px h-1 bg-[#e5e7eb]"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 轨道区域 -->
|
||||
<div class="timeline-tracks">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<!-- 视频轨道 -->
|
||||
<div class="track">
|
||||
<div class="track-info">
|
||||
<span class="track-icon">📹</span>
|
||||
<span class="track-label">视频</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-9 shrink-0">
|
||||
<span class="text-xs text-gray-600 font-medium">视频</span>
|
||||
</div>
|
||||
<div class="track-bar">
|
||||
<div class="flex-1 h-6 bg-[#f1f3f5] rounded relative overflow-hidden flex items-center">
|
||||
<div
|
||||
class="track-fill video-fill"
|
||||
class="h-full rounded flex items-center justify-center transition-[width] duration-300 min-w-[2px] bg-blue-500"
|
||||
:style="{ width: videoBarWidth + '%' }"
|
||||
>
|
||||
<span v-if="videoBarWidth > 15" class="track-time">{{ formatDuration(faceDurationMs) }}</span>
|
||||
<span v-if="videoBarWidth > 20" class="text-xs text-white font-medium tracking-wide">{{ formatDuration(faceDurationMs) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 音频轨道 -->
|
||||
<div class="track">
|
||||
<div class="track-info">
|
||||
<span class="track-icon">🎙️</span>
|
||||
<span class="track-label">音频</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-9 shrink-0">
|
||||
<span class="text-xs text-gray-600 font-medium">音频</span>
|
||||
</div>
|
||||
<div class="track-bar">
|
||||
<div class="flex-1 h-6 bg-[#f1f3f5] rounded relative overflow-hidden flex items-center">
|
||||
<div
|
||||
v-if="audioDurationMs > 0"
|
||||
class="track-fill audio-fill"
|
||||
:class="{ 'audio-exceed': isExceed }"
|
||||
class="h-full rounded flex items-center justify-center transition-[width] duration-300 min-w-[2px]"
|
||||
:class="isExceed ? 'bg-red-500' : 'bg-emerald-500'"
|
||||
:style="{ width: audioBarWidth + '%' }"
|
||||
>
|
||||
<span v-if="audioBarWidth > 15" class="track-time">{{ formatDuration(audioDurationMs) }}</span>
|
||||
<span v-if="audioBarWidth > 20" class="text-xs text-white font-medium tracking-wide">{{ formatDuration(audioDurationMs) }}</span>
|
||||
</div>
|
||||
<span v-else class="track-placeholder">等待生成音频</span>
|
||||
<span v-else class="text-xs text-gray-400 pl-2.5">等待生成音频</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 校验错误提示 -->
|
||||
<div v-if="validationError" class="timeline-diff error">
|
||||
<Icon icon="lucide:x-circle" class="diff-icon" />
|
||||
<div v-if="validationError" class="flex items-center gap-1.5 mt-2.5 px-2.5 py-2 rounded text-xs font-medium text-red-500 bg-red-50 border border-red-200">
|
||||
<Icon icon="lucide:x-circle" class="text-sm shrink-0" />
|
||||
<span>{{ validationError }}</span>
|
||||
</div>
|
||||
<!-- 时长差异提示 -->
|
||||
<div v-else-if="audioDurationMs > 0" class="timeline-diff" :class="diffStatus">
|
||||
<div v-else-if="audioDurationMs > 0" class="flex items-center gap-1.5 mt-2.5 px-2.5 py-2 rounded text-xs font-medium border border-transparent"
|
||||
:class="{
|
||||
'bg-[#f0fdf4] border-[#bbf7d0] text-emerald-600': diffStatus === 'match',
|
||||
'bg-red-50 border-red-200 text-red-500': diffStatus === 'exceed',
|
||||
'bg-amber-50 border-amber-200 text-amber-500': diffStatus === 'short',
|
||||
}"
|
||||
>
|
||||
<template v-if="diffStatus === 'match'">
|
||||
<Icon icon="lucide:check-circle" class="diff-icon" />
|
||||
<Icon icon="lucide:check-circle" class="text-sm shrink-0" />
|
||||
<span>时长匹配良好,可以生成</span>
|
||||
</template>
|
||||
<template v-else-if="diffStatus === 'exceed'">
|
||||
<Icon icon="lucide:alert-circle" class="diff-icon" />
|
||||
<Icon icon="lucide:alert-circle" class="text-sm shrink-0" />
|
||||
<span>音频超出 {{ formatDuration(diffMs) }},建议缩短文案</span>
|
||||
</template>
|
||||
<template v-else-if="diffStatus === 'short'">
|
||||
<Icon icon="lucide:info" class="diff-icon" />
|
||||
<Icon icon="lucide:info" class="text-sm shrink-0" />
|
||||
<span>音频较短,可适当增加文案</span>
|
||||
</template>
|
||||
</div>
|
||||
@@ -127,10 +127,6 @@ const audioBarWidth = computed(() =>
|
||||
Math.min(100, (props.audioDurationMs / maxDuration.value) * 100)
|
||||
)
|
||||
|
||||
const audioEndPosition = computed(() =>
|
||||
Math.min(100, (props.audioDurationMs / maxDuration.value) * 100)
|
||||
)
|
||||
|
||||
const isExceed = computed(() => props.audioDurationMs > props.faceDurationMs)
|
||||
|
||||
const diffMs = computed(() => Math.abs(props.audioDurationMs - props.faceDurationMs))
|
||||
@@ -178,235 +174,3 @@ function calculateInterval(duration: number): number {
|
||||
|
||||
const formatDuration = formatDurationMs
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
// 蓝紫主题配色 - 与主页面协调
|
||||
@text-primary: #1e293b;
|
||||
@text-secondary: #64748b;
|
||||
@text-tertiary: #94a3b8;
|
||||
@bg-subtle: rgba(59, 130, 246, 0.04);
|
||||
@bg-hover: rgba(59, 130, 246, 0.08);
|
||||
@border-light: rgba(59, 130, 246, 0.1);
|
||||
@border-medium: rgba(59, 130, 246, 0.15);
|
||||
@accent-blue: #3b82f6;
|
||||
@accent-purple: #8b5cf6;
|
||||
@accent-green: #10b981;
|
||||
@accent-red: #ef4444;
|
||||
@accent-orange: #f59e0b;
|
||||
|
||||
.timeline-panel {
|
||||
background: @bg-subtle;
|
||||
border-radius: 10px;
|
||||
padding: 14px 18px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: @text-secondary;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.duration-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: @text-tertiary;
|
||||
|
||||
.divider {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: @border-medium;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
// 刻度尺
|
||||
.timeline-ruler {
|
||||
position: relative;
|
||||
height: 18px;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 80px;
|
||||
}
|
||||
|
||||
.ruler-mark {
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ruler-label {
|
||||
font-size: 10px;
|
||||
color: @text-tertiary;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.ruler-tick {
|
||||
display: block;
|
||||
width: 1px;
|
||||
height: 4px;
|
||||
background: @border-medium;
|
||||
}
|
||||
|
||||
// 音频结束位置标记
|
||||
.audio-end-marker {
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.audio-marker-label {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
color: @accent-red;
|
||||
background: rgba(235, 87, 87, 0.1);
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.audio-marker-line {
|
||||
width: 1px;
|
||||
height: 6px;
|
||||
background: @accent-red;
|
||||
}
|
||||
|
||||
// 轨道区域
|
||||
.timeline-tracks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.track-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 68px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.track-icon {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.track-label {
|
||||
font-size: 12px;
|
||||
color: @text-secondary;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.track-bar {
|
||||
flex: 1;
|
||||
height: 22px;
|
||||
background: rgba(55, 53, 47, 0.06);
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.track-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.track-time {
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-weight: 600;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.track-placeholder {
|
||||
font-size: 11px;
|
||||
color: @text-tertiary;
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.video-fill {
|
||||
background: linear-gradient(90deg, @accent-blue 0%, @accent-purple 100%);
|
||||
}
|
||||
|
||||
.audio-fill {
|
||||
background: linear-gradient(90deg, @accent-green 0%, #059669 100%);
|
||||
|
||||
&.audio-exceed {
|
||||
background: linear-gradient(90deg, @accent-red 0%, #dc2626 100%);
|
||||
animation: pulse-warning 2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-warning {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.75; }
|
||||
}
|
||||
|
||||
// 差异提示
|
||||
.timeline-diff {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
.diff-icon {
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&.match {
|
||||
background: rgba(16, 185, 129, 0.08);
|
||||
color: @accent-green;
|
||||
}
|
||||
|
||||
&.exceed {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
color: @accent-red;
|
||||
}
|
||||
|
||||
&.short {
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
color: @accent-orange;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: @accent-red;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -52,6 +52,18 @@ public interface FileApi {
|
||||
String presignGetUrl(@NotEmpty(message = "URL 不能为空") String url,
|
||||
Integer expirationSeconds);
|
||||
|
||||
/**
|
||||
* 生成文件预签名地址(带 Content-Type),用于读取
|
||||
*
|
||||
* @param url 完整的文件访问地址
|
||||
* @param expirationSeconds 访问有效期,单位秒
|
||||
* @param contentType 响应的 Content-Type,为 null 时不设置
|
||||
* @return 文件预签名地址
|
||||
*/
|
||||
String presignGetUrl(@NotEmpty(message = "URL 不能为空") String url,
|
||||
Integer expirationSeconds,
|
||||
String contentType);
|
||||
|
||||
/**
|
||||
* 生成文件预签名地址(带 OSS 处理参数),用于读取
|
||||
* 用于阿里云 OSS 视频截帧等图片处理场景
|
||||
|
||||
@@ -31,7 +31,12 @@ public class FileApiImpl implements FileApi {
|
||||
|
||||
@Override
|
||||
public String presignGetUrl(String url, Integer expirationSeconds) {
|
||||
return fileService.presignGetUrl(url, expirationSeconds);
|
||||
return presignGetUrl(url, expirationSeconds, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String presignGetUrl(String url, Integer expirationSeconds, String contentType) {
|
||||
return fileService.presignGetUrl(url, expirationSeconds, contentType);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -71,6 +71,18 @@ public interface FileClient {
|
||||
* @return 文件预签名地址
|
||||
*/
|
||||
default String presignGetUrl(String url, Integer expirationSeconds) {
|
||||
return presignGetUrl(url, expirationSeconds, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件预签名地址(带 Content-Type),用于读取
|
||||
*
|
||||
* @param url 完整的文件访问地址
|
||||
* @param expirationSeconds 访问有效期,单位秒
|
||||
* @param contentType 响应的 Content-Type,为 null 时不设置
|
||||
* @return 文件预签名地址
|
||||
*/
|
||||
default String presignGetUrl(String url, Integer expirationSeconds, String contentType) {
|
||||
throw new UnsupportedOperationException("不支持的操作");
|
||||
}
|
||||
|
||||
|
||||
@@ -200,12 +200,19 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String presignGetUrl(String url, Integer expirationSeconds) {
|
||||
return presignGetUrlWithProcess(url, expirationSeconds, null);
|
||||
public String presignGetUrl(String url, Integer expirationSeconds, String contentType) {
|
||||
return presignGetUrlWithProcess(url, expirationSeconds, null, contentType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String presignGetUrlWithProcess(String url, Integer expirationSeconds, String processParam) {
|
||||
return presignGetUrlWithProcess(url, expirationSeconds, processParam, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件预签名地址(带 OSS 处理参数和 Content-Type),用于读取
|
||||
*/
|
||||
private String presignGetUrlWithProcess(String url, Integer expirationSeconds, String processParam, String contentType) {
|
||||
// 1. 将 url 转换为 path(支持 CDN 域名和 OSS 原始域名)
|
||||
String path = extractPathFromUrl(url);
|
||||
String decodedPath = URLUtil.decode(path, StandardCharsets.UTF_8);
|
||||
@@ -213,11 +220,17 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
||||
// 2. 公开访问:无需签名,直接拼接参数
|
||||
if (!BooleanUtil.isFalse(config.getEnablePublicAccess())) {
|
||||
String encodedPath = UriUtils.encodePath(decodedPath, StandardCharsets.UTF_8);
|
||||
String resultUrl = config.getDomain() + "/" + encodedPath;
|
||||
StringBuilder resultUrl = new StringBuilder(config.getDomain()).append("/").append(encodedPath);
|
||||
char separator = '?';
|
||||
if (StrUtil.isNotBlank(processParam)) {
|
||||
resultUrl = resultUrl + "?x-oss-process=" + processParam;
|
||||
resultUrl.append(separator).append("x-oss-process=").append(processParam);
|
||||
separator = '&';
|
||||
}
|
||||
return resultUrl;
|
||||
if (StrUtil.isNotBlank(contentType)) {
|
||||
resultUrl.append(separator).append("response-content-type=").append(
|
||||
URLUtil.encode(contentType, StandardCharsets.UTF_8));
|
||||
}
|
||||
return resultUrl.toString();
|
||||
}
|
||||
|
||||
// 3. 私有访问:生成预签名 URL(需要将处理参数包含在签名中)
|
||||
@@ -229,10 +242,14 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
||||
com.aliyun.oss.model.GeneratePresignedUrlRequest request =
|
||||
new com.aliyun.oss.model.GeneratePresignedUrlRequest(config.getBucket(), decodedPath, HttpMethod.GET);
|
||||
request.setExpiration(expirationDate);
|
||||
// 关键:将 x-oss-process 参数包含在签名中
|
||||
// 将 x-oss-process 参数包含在签名中
|
||||
if (StrUtil.isNotBlank(processParam)) {
|
||||
request.addQueryParameter("x-oss-process", processParam);
|
||||
}
|
||||
// 设置 response-content-type,确保浏览器能正确渲染
|
||||
if (StrUtil.isNotBlank(contentType)) {
|
||||
request.addQueryParameter("response-content-type", contentType);
|
||||
}
|
||||
signedUrl = aliyunOssClient.generatePresignedUrl(request).toString();
|
||||
} else {
|
||||
// 非阿里云不支持 OSS 处理参数,直接返回普通预签名 URL
|
||||
|
||||
@@ -54,6 +54,16 @@ public interface FileService {
|
||||
*/
|
||||
String presignGetUrl(String url, Integer expirationSeconds);
|
||||
|
||||
/**
|
||||
* 生成文件预签名地址(带 Content-Type),用于读取
|
||||
*
|
||||
* @param url 完整的文件访问地址
|
||||
* @param expirationSeconds 访问有效期,单位秒
|
||||
* @param contentType 响应的 Content-Type,为 null 时不设置
|
||||
* @return 文件预签名地址
|
||||
*/
|
||||
String presignGetUrl(String url, Integer expirationSeconds, String contentType);
|
||||
|
||||
/**
|
||||
* 生成文件预签名地址(带 OSS 处理参数),用于读取
|
||||
* 用于阿里云 OSS 视频截帧等图片处理场景
|
||||
|
||||
@@ -142,8 +142,13 @@ public class FileServiceImpl implements FileService {
|
||||
|
||||
@Override
|
||||
public String presignGetUrl(String url, Integer expirationSeconds) {
|
||||
return presignGetUrl(url, expirationSeconds, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String presignGetUrl(String url, Integer expirationSeconds, String contentType) {
|
||||
FileClient fileClient = fileConfigService.getMasterFileClient();
|
||||
return fileClient.presignGetUrl(url, expirationSeconds);
|
||||
return fileClient.presignGetUrl(url, expirationSeconds, contentType);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -339,7 +339,9 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
||||
}
|
||||
|
||||
// 视频播放URL不缓存,每次都生成新的签名URL(使用 filePath 而非 fileUrl,避免域名匹配问题)
|
||||
return fileApi.presignGetUrl(file.getFilePath(), PRESIGN_URL_EXPIRATION_SECONDS);
|
||||
// 根据文件扩展名推断 Content-Type,确保浏览器 <video> 元素能正确渲染
|
||||
String contentType = java.net.URLConnection.guessContentTypeFromName(file.getFilePath());
|
||||
return fileApi.presignGetUrl(file.getFilePath(), PRESIGN_URL_EXPIRATION_SECONDS, contentType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -354,7 +356,8 @@ public class TikUserFileServiceImpl implements TikUserFileService {
|
||||
}
|
||||
|
||||
// 音频播放URL不缓存,每次都生成新的签名URL(使用 filePath 而非 fileUrl,避免域名匹配问题)
|
||||
return fileApi.presignGetUrl(file.getFilePath(), PRESIGN_URL_EXPIRATION_SECONDS);
|
||||
String contentType = java.net.URLConnection.guessContentTypeFromName(file.getFilePath());
|
||||
return fileApi.presignGetUrl(file.getFilePath(), PRESIGN_URL_EXPIRATION_SECONDS, contentType);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user