添加视频帧提取功能和阿里云OSS存储支持

- 新增从视频素材提取首帧/尾帧的功能,支持画面连续性编辑
- 添加阿里云OSS存储支持,可配置本地或OSS存储方式
- 导入视频素材时自动探测并更新视频时长信息
- 前端添加从素材提取尾帧的UI界面
- 添加FramePrompt模型的数据库迁移

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
empty
2026-01-18 21:44:05 +08:00
parent f2b8bfaa25
commit fe595db96e

View File

@@ -0,0 +1,122 @@
package storage
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/drama-generator/backend/pkg/config"
)
// OssStorage 阿里云 OSS 存储实现
type OssStorage struct {
client *oss.Client
bucket *oss.Bucket
bucketName string
customDomain string
endpoint string
}
// NewOssStorage 创建 OSS 存储实例
func NewOssStorage(cfg *config.OssConfig) (*OssStorage, error) {
if cfg.Endpoint == "" || cfg.AccessKeyID == "" || cfg.AccessKeySecret == "" || cfg.BucketName == "" {
return nil, fmt.Errorf("OSS configuration is incomplete")
}
client, err := oss.New(cfg.Endpoint, cfg.AccessKeyID, cfg.AccessKeySecret)
if err != nil {
return nil, fmt.Errorf("failed to create OSS client: %w", err)
}
bucket, err := client.Bucket(cfg.BucketName)
if err != nil {
return nil, fmt.Errorf("failed to get OSS bucket: %w", err)
}
return &OssStorage{
client: client,
bucket: bucket,
bucketName: cfg.BucketName,
customDomain: cfg.CustomDomain,
endpoint: cfg.Endpoint,
}, nil
}
// Upload 上传文件到 OSS
func (s *OssStorage) Upload(localPath, category string) (string, error) {
// 读取本地文件
file, err := os.Open(localPath)
if err != nil {
return "", fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
// 生成 OSS 对象键
ext := filepath.Ext(localPath)
timestamp := time.Now().Format("20060102_150405_000")
objectKey := fmt.Sprintf("%s/%s%s", category, timestamp, ext)
// 上传到 OSS
err = s.bucket.PutObject(objectKey, file)
if err != nil {
return "", fmt.Errorf("failed to upload to OSS: %w", err)
}
// 构建访问 URL
url := s.GetURL(objectKey)
return url, nil
}
// UploadWithFilename 使用指定文件名上传文件到 OSS
func (s *OssStorage) UploadWithFilename(localPath, category, filename string) (string, error) {
file, err := os.Open(localPath)
if err != nil {
return "", fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
objectKey := fmt.Sprintf("%s/%s", category, filename)
err = s.bucket.PutObject(objectKey, file)
if err != nil {
return "", fmt.Errorf("failed to upload to OSS: %w", err)
}
return s.GetURL(objectKey), nil
}
// GetURL 获取 OSS 对象的访问 URL
func (s *OssStorage) GetURL(objectKey string) string {
if s.customDomain != "" {
// 使用自定义域名CDN
domain := strings.TrimSuffix(s.customDomain, "/")
if !strings.HasPrefix(domain, "http") {
domain = "https://" + domain
}
return fmt.Sprintf("%s/%s", domain, objectKey)
}
// 使用默认 OSS 域名
return fmt.Sprintf("https://%s.%s/%s", s.bucketName, s.endpoint, objectKey)
}
// Delete 删除 OSS 对象
func (s *OssStorage) Delete(objectKey string) error {
err := s.bucket.DeleteObject(objectKey)
if err != nil {
return fmt.Errorf("failed to delete OSS object: %w", err)
}
return nil
}
// IsConfigured 检查 OSS 是否已配置
func IsOssConfigured(cfg *config.OssConfig) bool {
return cfg != nil &&
cfg.Endpoint != "" &&
cfg.AccessKeyID != "" &&
cfg.AccessKeySecret != "" &&
cfg.BucketName != ""
}