Compare commits

...

3 Commits

Author SHA1 Message Date
668515a451 feat: 优化 2026-03-02 01:32:02 +08:00
ce3d529f80 feat: 优化 2026-03-02 01:28:46 +08:00
b2e5bb85f4 feat: 功能优化 2026-03-01 23:57:19 +08:00
11 changed files with 154 additions and 88 deletions

View File

@@ -47,7 +47,7 @@
<script setup> <script setup>
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue' import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
import { Empty } from 'ant-design-vue' import { Empty, message } from 'ant-design-vue'
import { SoundOutlined } from '@ant-design/icons-vue' import { SoundOutlined } from '@ant-design/icons-vue'
import { useVoiceCopyStore } from '@/stores/voiceCopy' import { useVoiceCopyStore } from '@/stores/voiceCopy'
import { useTTS, TTS_PROVIDERS } from '@/composables/useTTS' import { useTTS, TTS_PROVIDERS } from '@/composables/useTTS'
@@ -205,6 +205,14 @@ const initPlayer = (url) => {
nextTick(() => { nextTick(() => {
try { try {
// 检查容器是否存在
if (!playerContainer.value) {
message.error('播放器容器未就绪')
isPlayerInitializing.value = false
audioUrl.value = ''
return
}
player = new APlayer({ player = new APlayer({
container: playerContainer.value, container: playerContainer.value,
autoplay: true, autoplay: true,
@@ -225,6 +233,7 @@ const initPlayer = (url) => {
player.on('error', (e) => { player.on('error', (e) => {
console.error('APlayer 播放错误:', e) console.error('APlayer 播放错误:', e)
message.error('音频播放失败,请重试')
}) })
player.on('canplay', () => { player.on('canplay', () => {
@@ -232,7 +241,9 @@ const initPlayer = (url) => {
}) })
} catch (e) { } catch (e) {
console.error('APlayer 初始化失败:', e) console.error('APlayer 初始化失败:', e)
message.error('播放器初始化失败,请重试')
isPlayerInitializing.value = false isPlayerInitializing.value = false
audioUrl.value = ''
} }
}) })
} }

View File

@@ -7,6 +7,7 @@ import { ref, reactive } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { MaterialService } from '@/api/material' import { MaterialService } from '@/api/material'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { OSS_ORIGINAL, isDev } from '@gold/config/api'
// GB转字节常量 // GB转字节常量
const GB_TO_BYTES = 1073741824 const GB_TO_BYTES = 1073741824
@@ -106,11 +107,12 @@ export function useUpload() {
reject(new Error('网络错误,上传失败')) reject(new Error('网络错误,上传失败'))
}) })
// 发起PUT请求 - 使用代理路径 // 发起PUT请求
const uploadUrl = presignedData.presignedUrl.replace( // 开发环境:使用 /oss 代理避免CORS问题
'https://muye-ai-chat.oss-cn-hangzhou.aliyuncs.com', // 生产环境直接使用OSS原始域名需要OSS配置CORS
'/oss' const uploadUrl = isDev()
) ? presignedData.presignedUrl.replace(OSS_ORIGINAL, '/oss')
: presignedData.presignedUrl
xhr.open('PUT', uploadUrl) xhr.open('PUT', uploadUrl)
if (presignedData.headers && presignedData.headers['Content-Type']) { if (presignedData.headers && presignedData.headers['Content-Type']) {
xhr.setRequestHeader('Content-Type', presignedData.headers['Content-Type']) xhr.setRequestHeader('Content-Type', presignedData.headers['Content-Type'])

View File

@@ -101,7 +101,12 @@
<SearchOutlined /> <SearchOutlined />
</template> </template>
</a-input> </a-input>
<a-button type="primary" class="upload-btn" @click="handleOpenUploadModal"> <a-button
type="primary"
class="upload-btn"
:disabled="activeCategory === 'MIX' && (!selectedGroupId || groupList.length === 0)"
@click="handleOpenUploadModal"
>
<UploadOutlined /> <UploadOutlined />
上传 上传
</a-button> </a-button>
@@ -401,10 +406,16 @@ const handleSaveGroupName = async (group) => {
const handleDeleteGroup = async (group, event) => { const handleDeleteGroup = async (group, event) => {
event?.stopPropagation?.() event?.stopPropagation?.()
// 校验:分组内还有文件时不允许删除
if (group.fileCount > 0) {
message.warning(`分组「${group.name}」内还有 ${group.fileCount} 个文件,请先删除文件后再删除分组`)
return
}
const confirmed = await new Promise((resolve) => { const confirmed = await new Promise((resolve) => {
Modal.confirm({ Modal.confirm({
title: '删除分组', title: '删除分组',
content: `确定要删除分组「${group.name}」吗?删除后该分组下的所有文件将被移动到默认分组。此操作不可恢复。`, content: `确定要删除分组「${group.name}」吗?此操作不可恢复。`,
okText: '确认删除', okText: '确认删除',
cancelText: '取消', cancelText: '取消',
okType: 'danger', okType: 'danger',
@@ -657,10 +668,13 @@ const handleBatchDelete = async () => {
} }
const formatFileSize = (size) => { const formatFileSize = (size) => {
if (size < 1024) return size + ' B' const units = ['B', 'KB', 'MB', 'GB']
if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB' let unitIndex = 0
if (size < 1024 * 1024 * 1024) return (size / (1024 * 1024)).toFixed(2) + ' MB' while (size >= 1024 && unitIndex < units.length - 1) {
return (size / (1024 * 1024 * 1024)).toFixed(2) + ' GB' size /= 1024
unitIndex++
}
return `${size.toFixed(2)} ${units[unitIndex]}`
} }
const formatDate = (date) => { const formatDate = (date) => {

View File

@@ -70,7 +70,7 @@ export default defineConfig(({ mode }) => {
}, },
}, },
'/oss': { '/oss': {
target: 'https://muye-ai-chat.oss-cn-hangzhou.aliyuncs.com', target: 'https://oss.muyetools.cn',
changeOrigin: true, changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/oss/, ''), rewrite: (path: string) => path.replace(/^\/oss/, ''),
headers: { headers: {

View File

@@ -19,7 +19,7 @@ function getBaseUrl() {
} }
return '' return ''
} }
//
const BASE_URL = getBaseUrl() const BASE_URL = getBaseUrl()
/** /**
@@ -38,6 +38,21 @@ export const API_BASE = {
AI_APP: `${BASE_URL}/api/ai`, AI_APP: `${BASE_URL}/api/ai`,
} }
/**
* OSS 原始域名用于上传预签名URL代理
*/
export const OSS_ORIGINAL = 'https://muye-ai-chat.oss-cn-hangzhou.aliyuncs.com'
/**
* 判断是否为开发环境
*/
export const isDev = () => {
if (typeof import.meta !== 'undefined' && import.meta.env) {
return import.meta.env.DEV
}
return false
}
/** /**
* 获取完整的 API 路径 * 获取完整的 API 路径
* @param {string} module - 模块名称 (如 'ADMIN_AI', 'APP_MEMBER') * @param {string} module - 模块名称 (如 'ADMIN_AI', 'APP_MEMBER')
@@ -46,7 +61,7 @@ export const API_BASE = {
*/ */
export function getApiUrl(module, path) { export function getApiUrl(module, path) {
const base = API_BASE[module] || API_BASE.APP const base = API_BASE[module] || API_BASE.APP
return `${base}${path.startsWith('/') ? path : '/' + path}` return `${base}${path.startsWith('/') ? '' : '/'}${path}`
} }
export default API_BASE export default API_BASE

View File

@@ -116,8 +116,8 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
@Override @Override
public String presignGetUrl(String url, Integer expirationSeconds) { public String presignGetUrl(String url, Integer expirationSeconds) {
// 1. 将 url 转换为 path // 1. 将 url 转换为 path支持多种URL格式
String path = StrUtil.removePrefix(url, config.getDomain() + "/"); String path = extractPathFromUrl(url);
path = HttpUtils.removeUrlQuery(path); path = HttpUtils.removeUrlQuery(path);
String decodedPath = URLUtil.decode(path, StandardCharsets.UTF_8); String decodedPath = URLUtil.decode(path, StandardCharsets.UTF_8);
@@ -138,6 +138,37 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
return signedUrl.toString(); return signedUrl.toString();
} }
/**
* 从URL中提取相对路径
* 支持多种URL格式
* 1. CDN域名https://oss.muyetools.cn/path/to/file
* 2. OSS原始域名https://bucket.oss-region.aliyuncs.com/path/to/file
* 3. 相对路径path/to/file
*/
private String extractPathFromUrl(String url) {
if (StrUtil.isEmpty(url)) {
return url;
}
// 1. 尝试移除配置的域名前缀
String path = StrUtil.removePrefix(url, config.getDomain() + "/");
if (!StrUtil.equals(url, path)) {
return path;
}
// 2. 如果不是以配置域名开头检查是否是完整URL
if (url.contains("://")) {
// 提取域名后的路径
int pathStart = url.indexOf("/", url.indexOf("://") + 3);
if (pathStart > 0) {
return url.substring(pathStart + 1);
}
}
// 3. 已经是相对路径,直接返回
return url;
}
/** /**
* 基于 bucket + endpoint 构建访问的 Domain 地址 * 基于 bucket + endpoint 构建访问的 Domain 地址
* *

View File

@@ -151,9 +151,8 @@ public class BatchProduceAlignment {
String dateDir = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd")); String dateDir = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"));
String outputMediaPath = mixDirectory + "/" + dateDir + "/" + targetFileName + ".mp4"; String outputMediaPath = mixDirectory + "/" + dateDir + "/" + targetFileName + ".mp4";
// 使用默认的阿里云OSS endpoint格式 // ICE写入必须使用OSS原始域名不能是CDN域名因为ICE需要写权限
String bucketEndpoint = "https://" + properties.getBucket() + ".oss-" + properties.getRegionId() + ".aliyuncs.com"; String outputMediaUrl = properties.getOssWriteUrl(outputMediaPath);
String outputMediaUrl = bucketEndpoint + "/" + outputMediaPath;
// ICE需要将处理结果写入到该URL签名URL会导致写入失败 // ICE需要将处理结果写入到该URL签名URL会导致写入失败
int width = 720; int width = 720;
@@ -217,39 +216,32 @@ public class BatchProduceAlignment {
*/ */
private Map<String, Integer> calculateCropParams(int sourceWidth, int sourceHeight, String cropMode) { private Map<String, Integer> calculateCropParams(int sourceWidth, int sourceHeight, String cropMode) {
Map<String, Integer> cropParams = new HashMap<>(); Map<String, Integer> cropParams = new HashMap<>();
double targetRatio = 9.0 / 16.0; // 9:16竖屏比例
// 填充模式:不裁剪,保持原尺寸
if ("fill".equals(cropMode)) { if ("fill".equals(cropMode)) {
// 填充模式:不裁剪,保持原尺寸
cropParams.put("X", 0); cropParams.put("X", 0);
cropParams.put("Y", 0); cropParams.put("Y", 0);
cropParams.put("Width", sourceWidth); cropParams.put("Width", sourceWidth);
cropParams.put("Height", sourceHeight); cropParams.put("Height", sourceHeight);
} else if ("smart".equals(cropMode)) { log.debug("[裁剪计算] 源尺寸={}x{}, 模式=fill, 裁剪参数={}", sourceWidth, sourceHeight, cropParams);
// 智能裁剪功能暂未开放,自动降级为居中裁剪 return cropParams;
log.info("[裁剪模式] smart模式暂未开放自动降级为center模式");
double cropHeight = sourceHeight;
double cropWidth = cropHeight * targetRatio;
int cropX = (int) Math.round((sourceWidth - cropWidth) / 2);
int cropY = 0;
cropParams.put("X", cropX);
cropParams.put("Y", cropY);
cropParams.put("Width", (int) Math.round(cropWidth));
cropParams.put("Height", (int) Math.round(cropHeight));
} else {
// center模式居中裁剪默认
double cropHeight = sourceHeight;
double cropWidth = cropHeight * targetRatio;
int cropX = (int) Math.round((sourceWidth - cropWidth) / 2);
int cropY = 0;
cropParams.put("X", cropX);
cropParams.put("Y", cropY);
cropParams.put("Width", (int) Math.round(cropWidth));
cropParams.put("Height", (int) Math.round(cropHeight));
} }
// center/smart模式居中裁剪smart暂未开放降级为center
if ("smart".equals(cropMode)) {
log.info("[裁剪模式] smart模式暂未开放自动降级为center模式");
}
double targetRatio = 9.0 / 16.0; // 9:16竖屏比例
double cropHeight = sourceHeight;
double cropWidth = cropHeight * targetRatio;
int cropX = (int) Math.round((sourceWidth - cropWidth) / 2);
cropParams.put("X", cropX);
cropParams.put("Y", 0);
cropParams.put("Width", (int) Math.round(cropWidth));
cropParams.put("Height", (int) Math.round(cropHeight));
log.debug("[裁剪计算] 源尺寸={}x{}, 模式={}, 裁剪参数={}", sourceWidth, sourceHeight, cropMode, cropParams); log.debug("[裁剪计算] 源尺寸={}x{}, 模式={}, 裁剪参数={}", sourceWidth, sourceHeight, cropMode, cropParams);
return cropParams; return cropParams;
} }
@@ -374,8 +366,8 @@ public class BatchProduceAlignment {
String dateDir = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd")); String dateDir = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"));
String outputMediaPath = mixDirectory + "/" + dateDir + "/" + targetFileName + ".mp4"; String outputMediaPath = mixDirectory + "/" + dateDir + "/" + targetFileName + ".mp4";
String bucketEndpoint = "https://" + properties.getBucket() + ".oss-" + properties.getRegionId() + ".aliyuncs.com"; // ICE写入必须使用OSS原始域名不能是CDN域名因为ICE需要写权限
String outputMediaUrl = bucketEndpoint + "/" + outputMediaPath; String outputMediaUrl = properties.getOssWriteUrl(outputMediaPath);
int width = 720; int width = 720;
int height = 1280; int height = 1280;

View File

@@ -53,4 +53,11 @@ public class IceProperties {
public boolean isEnabled() { public boolean isEnabled() {
return enabled && StrUtil.isNotBlank(accessKeyId) && StrUtil.isNotBlank(accessKeySecret); return enabled && StrUtil.isNotBlank(accessKeyId) && StrUtil.isNotBlank(accessKeySecret);
} }
/**
* 获取ICE写入用的OSS URL必须使用原始域名
*/
public String getOssWriteUrl(String path) {
return "https://" + bucket + ".oss-" + regionId + ".aliyuncs.com/" + path;
}
} }

View File

@@ -551,24 +551,6 @@ public class MixTaskServiceImpl implements MixTaskService {
} }
} }
/**
* 从OSS URL中提取相对路径
*/
private String extractPathFromUrl(String ossUrl) {
if (StrUtil.isEmpty(ossUrl)) {
return null;
}
// 查找 ".aliyuncs.com/" 的位置
int index = ossUrl.indexOf(".aliyuncs.com/");
if (index == -1) {
return null;
}
// 提取 "/" 后的路径
return ossUrl.substring(index + ".aliyuncs.com/".length());
}
/** /**
* 将DO分页结果转换为VO分页结果消除冗余代码 * 将DO分页结果转换为VO分页结果消除冗余代码
* 优化点: * 优化点:
@@ -607,6 +589,11 @@ public class MixTaskServiceImpl implements MixTaskService {
} }
} }
private static final int MIN_TOTAL_DURATION = 15;
private static final int MAX_TOTAL_DURATION = 30;
private static final int MIN_SEGMENT_DURATION = 3;
private static final int MAX_SEGMENT_DURATION = 5;
/** /**
* 验证新格式场景配置 * 验证新格式场景配置
*/ */
@@ -636,9 +623,7 @@ public class MixTaskServiceImpl implements MixTaskService {
} }
// 场景时长验证 // 场景时长验证
if (scene.getDuration() < 3 || scene.getDuration() > 5) { validateSegmentDuration(scene.getDuration(), "场景" + i);
throw new IllegalArgumentException("场景" + i + "时长需在3-5秒之间当前" + scene.getDuration() + "");
}
// 候选素材验证 // 候选素材验证
for (int j = 0; j < scene.getCandidates().size(); j++) { for (int j = 0; j < scene.getCandidates().size(); j++) {
@@ -652,18 +637,11 @@ public class MixTaskServiceImpl implements MixTaskService {
} }
} }
// 3. 计算总时长 // 3. 计算并校验总时长
int totalDuration = req.getScenes().stream() int totalDuration = req.getScenes().stream()
.mapToInt(MixTaskSaveReqVO.SceneConfig::getDuration) .mapToInt(MixTaskSaveReqVO.SceneConfig::getDuration)
.sum(); .sum();
validateTotalDuration(totalDuration);
// 4. 总时长校验15s-30s
if (totalDuration < 15) {
throw new IllegalArgumentException("总时长不能小于15秒当前" + totalDuration + "");
}
if (totalDuration > 30) {
throw new IllegalArgumentException("总时长不能超过30秒当前" + totalDuration + "");
}
log.info("[MixTask][新格式场景校验通过] totalDuration={}s, sceneCount={}", totalDuration, req.getScenes().size()); log.info("[MixTask][新格式场景校验通过] totalDuration={}s, sceneCount={}", totalDuration, req.getScenes().size());
} }
@@ -682,21 +660,35 @@ public class MixTaskServiceImpl implements MixTaskService {
.mapToInt(MixTaskSaveReqVO.MaterialItem::getDuration) .mapToInt(MixTaskSaveReqVO.MaterialItem::getDuration)
.sum(); .sum();
// 3. 总时长校验15s-30s // 3. 校验总时长
if (totalDuration < 15) { validateTotalDuration(totalDuration);
throw new IllegalArgumentException("总时长不能小于15秒当前" + totalDuration + "");
}
if (totalDuration > 30) {
throw new IllegalArgumentException("总时长不能超过30秒当前" + totalDuration + "");
}
// 4. 单个素材时长校验3s-5s // 4. 单个素材时长校验
for (MixTaskSaveReqVO.MaterialItem item : req.getMaterials()) { for (MixTaskSaveReqVO.MaterialItem item : req.getMaterials()) {
if (item.getDuration() < 3 || item.getDuration() > 5) { validateSegmentDuration(item.getDuration(), "素材");
throw new IllegalArgumentException("单个素材时长需在3-5秒之间当前" + item.getDuration() + "");
}
} }
log.info("[MixTask][旧格式素材校验通过] totalDuration={}s, materialCount={}", totalDuration, req.getMaterials().size()); log.info("[MixTask][旧格式素材校验通过] totalDuration={}s, materialCount={}", totalDuration, req.getMaterials().size());
} }
/**
* 校验总时长
*/
private void validateTotalDuration(int totalDuration) {
if (totalDuration < MIN_TOTAL_DURATION) {
throw new IllegalArgumentException("总时长不能小于" + MIN_TOTAL_DURATION + "秒,当前:" + totalDuration + "");
}
if (totalDuration > MAX_TOTAL_DURATION) {
throw new IllegalArgumentException("总时长不能超过" + MAX_TOTAL_DURATION + "秒,当前:" + totalDuration + "");
}
}
/**
* 校验单个片段时长
*/
private void validateSegmentDuration(int duration, String context) {
if (duration < MIN_SEGMENT_DURATION || duration > MAX_SEGMENT_DURATION) {
throw new IllegalArgumentException(context + "时长需在" + MIN_SEGMENT_DURATION + "-" + MAX_SEGMENT_DURATION + "秒之间,当前:" + duration + "");
}
}
} }

View File

@@ -206,6 +206,7 @@ yudao:
region-id: cn-hangzhou region-id: cn-hangzhou
bucket: muye-ai-chat bucket: muye-ai-chat
enabled: true enabled: true
oss-domain: https://oss.muyetools.cn # CDN加速域名用于文件访问
captcha: captcha:
enable: false # 关闭图片验证码,方便登录等接口的测试 enable: false # 关闭图片验证码,方便登录等接口的测试
security: security:

View File

@@ -268,6 +268,7 @@ yudao:
region-id: cn-hangzhou region-id: cn-hangzhou
bucket: muye-ai-chat bucket: muye-ai-chat
enabled: true enabled: true
oss-domain: https://oss.muyetools.cn # CDN加速域名用于文件访问
dify: dify:
api-url: http://127.0.0.1:8088 # Dify API 地址,请根据实际情况修改 api-url: http://127.0.0.1:8088 # Dify API 地址,请根据实际情况修改
timeout: 240 # 请求超时时间(秒) timeout: 240 # 请求超时时间(秒)