Files
2026-01-06 19:36:42 +08:00

352 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package utils
import (
"ai_xhs/config"
"fmt"
"io"
"mime"
"mime/multipart"
"path/filepath"
"strings"
"time"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/google/uuid"
)
// OSSStorage 阿里云OSS存储服务
type OSSStorage struct {
client *oss.Client
bucket *oss.Bucket
config *config.OSSConfig
}
var ossStorage *OSSStorage
// InitOSS 初始化OSS客户端
func InitOSS() error {
cfg := &config.AppConfig.Upload.OSS
// 打印详细配置信息用于调试
fmt.Printf("\n=== OSS初始化配置 ===\n")
fmt.Printf("Endpoint: [%s]\n", cfg.Endpoint)
fmt.Printf("AccessKeyID: [%s]\n", cfg.AccessKeyID)
fmt.Printf("AccessKeySecret: [%s] (长度: %d)\n", cfg.AccessKeySecret, len(cfg.AccessKeySecret))
fmt.Printf("BucketName: [%s]\n", cfg.BucketName)
fmt.Printf("BasePath: [%s]\n", cfg.BasePath)
fmt.Printf("Domain: [%s]\n", cfg.Domain)
fmt.Printf("==================\n\n")
// 创建OSSClient实例
client, err := oss.New(cfg.Endpoint, cfg.AccessKeyID, cfg.AccessKeySecret)
if err != nil {
return fmt.Errorf("创建OSS客户端失败: %w", err)
}
// 获取存储空间
bucket, err := client.Bucket(cfg.BucketName)
if err != nil {
return fmt.Errorf("获取OSS Bucket失败: %w", err)
}
ossStorage = &OSSStorage{
client: client,
bucket: bucket,
config: cfg,
}
return nil
}
// min 辅助函数
func min(a, b int) int {
if a < b {
return a
}
return b
}
// UploadFile 上传文件到OSS
func (s *OSSStorage) UploadFile(file multipart.File, filename string, objectPath string) (string, error) {
// 生成OSS对象路径
objectKey := s.generateObjectKey(objectPath, filename)
// 获取文件MIME类型
contentType := getContentType(filename)
// 上传文件到OSS设置Content-Type和其他元数据
// 使用 ObjectACL 设置为公共读,确保文件可以直接访问
err := s.bucket.PutObject(objectKey, file,
oss.ContentType(contentType),
oss.ObjectACL(oss.ACLPublicRead),
oss.ContentDisposition("inline"), // 关键设置为inline而不是attachment
)
if err != nil {
return "", fmt.Errorf("上传文件到OSS失败: %w", err)
}
// 获取OSS返回的文件URL
fileURL, err := s.GetFileURL(objectKey)
if err != nil {
return "", fmt.Errorf("获取OSS文件URL失败: %w", err)
}
return fileURL, nil
}
// UploadFromBytes 从字节数组上传文件到OSS
func (s *OSSStorage) UploadFromBytes(data []byte, filename string, objectPath string) (string, error) {
// 生成OSS对象路径
objectKey := s.generateObjectKey(objectPath, filename)
// 获取文件MIME类型
contentType := getContentType(filename)
// 上传文件到OSS设置Content-Type和其他元数据
err := s.bucket.PutObject(objectKey, strings.NewReader(string(data)),
oss.ContentType(contentType),
oss.ObjectACL(oss.ACLPublicRead),
oss.ContentDisposition("inline"),
)
if err != nil {
return "", fmt.Errorf("上传文件到OSS失败: %w", err)
}
// 获取OSS返回的文件URL
fileURL, err := s.GetFileURL(objectKey)
if err != nil {
return "", fmt.Errorf("获取OSS文件URL失败: %w", err)
}
return fileURL, nil
}
// UploadFromReader 从Reader上传文件到OSS
func (s *OSSStorage) UploadFromReader(reader io.Reader, filename string, objectPath string) (string, error) {
// 生成OSS对象路径
objectKey := s.generateObjectKey(objectPath, filename)
// 获取文件MIME类型
contentType := getContentType(filename)
// 打印详细上传信息
fmt.Printf("\n=== OSS上传请求 ===\n")
fmt.Printf("ObjectKey: [%s]\n", objectKey)
fmt.Printf("ContentType: [%s]\n", contentType)
fmt.Printf("Endpoint: [%s]\n", s.config.Endpoint)
fmt.Printf("BucketName: [%s]\n", s.config.BucketName)
fmt.Printf("AccessKeyID: [%s]\n", s.config.AccessKeyID)
fmt.Printf("AccessKeySecret: [%s]\n", s.config.AccessKeySecret)
fmt.Printf("==================\n\n")
// 上传文件到OSS设置Content-Type和其他元数据
err := s.bucket.PutObject(objectKey, reader,
oss.ContentType(contentType),
oss.ObjectACL(oss.ACLPublicRead),
oss.ContentDisposition("inline"),
)
if err != nil {
fmt.Printf("\n!!! OSS上传失败 !!!\n")
fmt.Printf("错误详情: %v\n", err)
fmt.Printf("错误类型: %T\n", err)
fmt.Printf("==================\n\n")
return "", fmt.Errorf("上传文件到OSS失败: %w", err)
}
// 获取OSS返回的文件URL
fileURL, err := s.GetFileURL(objectKey)
if err != nil {
return "", fmt.Errorf("获取OSS文件URL失败: %w", err)
}
fmt.Printf("✓ OSS上传成功: %s\n\n", fileURL)
return fileURL, nil
}
// DeleteFile 删除OSS中的文件
func (s *OSSStorage) DeleteFile(objectKey string) error {
err := s.bucket.DeleteObject(objectKey)
if err != nil {
return fmt.Errorf("删除OSS文件失败: %w", err)
}
return nil
}
// IsObjectExist 检查对象是否存在
func (s *OSSStorage) IsObjectExist(objectKey string) (bool, error) {
exist, err := s.bucket.IsObjectExist(objectKey)
if err != nil {
return false, fmt.Errorf("检查OSS对象是否存在失败: %w", err)
}
return exist, nil
}
// GetFileURL 获取文件访问URL使用OSS SDK标准方法
func (s *OSSStorage) GetFileURL(objectKey string) (string, error) {
// 如果配置了自定义域名,使用自定义域名
if s.config.Domain != "" {
// 确保 Domain 以 https:// 开头
domain := s.config.Domain
if !strings.HasPrefix(domain, "http://") && !strings.HasPrefix(domain, "https://") {
domain = "https://" + domain
}
url := fmt.Sprintf("%s/%s", strings.TrimRight(domain, "/"), objectKey)
return url, nil
}
// 使用OSS SDK获取对象的公共URL
// 正确格式https://bucket-name.endpoint/objectKey
// Endpoint 不应该包含 https://
endpoint := s.config.Endpoint
// 移除 endpoint 中可能存在的协议前缀
endpoint = strings.TrimPrefix(endpoint, "https://")
endpoint = strings.TrimPrefix(endpoint, "http://")
endpoint = strings.TrimRight(endpoint, "/")
// 移除 objectKey 开头的斜杠
objectKey = strings.TrimLeft(objectKey, "/")
url := fmt.Sprintf("https://%s.%s/%s", s.config.BucketName, endpoint, objectKey)
return url, nil
}
// GeneratePresignedURL 生成临时访问URL带签名
func (s *OSSStorage) GeneratePresignedURL(objectKey string, expireSeconds int64) (string, error) {
signedURL, err := s.bucket.SignURL(objectKey, oss.HTTPGet, expireSeconds)
if err != nil {
return "", fmt.Errorf("生成预签名URL失败: %w", err)
}
return signedURL, nil
}
// generateObjectKey 生成OSS对象键名
func (s *OSSStorage) generateObjectKey(objectPath string, filename string) string {
// 组合基础路径和对象路径
basePath := strings.TrimRight(s.config.BasePath, "/")
objectPath = strings.TrimLeft(objectPath, "/")
// OSS 路径统一使用斜杠,避免 Windows 下使用反斜杠
if basePath == "" {
return strings.ReplaceAll(filepath.ToSlash(filepath.Join(objectPath, filename)), "\\", "/")
}
return strings.ReplaceAll(filepath.ToSlash(filepath.Join(basePath, objectPath, filename)), "\\", "/")
}
// UploadToOSS 上传文件到OSS兼容旧接口
func UploadToOSS(reader io.Reader, originalFilename string) (string, error) {
if ossStorage == nil {
return "", fmt.Errorf("OSS未初始化请先调用InitOSS()")
}
// 生成唯一文件名
filename := GenerateFilename(originalFilename)
// 使用日期目录作为路径
objectPath := time.Now().Format("20060102")
return ossStorage.UploadFromReader(reader, filename, objectPath)
}
// DeleteFromOSS 从OSS删除文件兼容旧接口
func DeleteFromOSS(fileURL string) error {
if ossStorage == nil {
return fmt.Errorf("OSS未初始化")
}
cfg := ossStorage.config
// 从URL中提取ObjectKey
var objectKey string
if cfg.Domain != "" {
// 自定义域名格式: https://domain.com/path/file.jpg
domain := cfg.Domain
if !strings.HasPrefix(domain, "http://") && !strings.HasPrefix(domain, "https://") {
domain = "https://" + domain
}
objectKey = strings.TrimPrefix(fileURL, fmt.Sprintf("%s/", strings.TrimRight(domain, "/")))
} else {
// 默认域名格式: https://bucket.endpoint/path/file.jpg
endpoint := strings.TrimPrefix(cfg.Endpoint, "https://")
endpoint = strings.TrimPrefix(endpoint, "http://")
objectKey = strings.TrimPrefix(fileURL, fmt.Sprintf("https://%s.%s/", cfg.BucketName, endpoint))
}
return ossStorage.DeleteFile(objectKey)
}
// GenerateFilename 生成唯一文件名
func GenerateFilename(originalFilename string) string {
ext := filepath.Ext(originalFilename)
name := strings.TrimSuffix(originalFilename, ext)
// 生成时间戳和UUID
timestamp := time.Now().Format("20060102150405")
uuidStr := uuid.New().String()[:8]
// 清理文件名,移除特殊字符
name = strings.ReplaceAll(name, " ", "_")
name = strings.ReplaceAll(name, "(", "")
name = strings.ReplaceAll(name, ")", "")
return fmt.Sprintf("%s_%s_%s%s", name, timestamp, uuidStr, ext)
}
// IsValidImageType 验证图片文件类型
func IsValidImageType(filename string) bool {
ext := strings.ToLower(filepath.Ext(filename))
validExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"}
for _, validExt := range validExts {
if ext == validExt {
return true
}
}
return false
}
// getContentType 根据文件名获取MIME类型
func getContentType(filename string) string {
ext := strings.ToLower(filepath.Ext(filename))
// 常见图片类型
switch ext {
case ".jpg", ".jpeg":
return "image/jpeg"
case ".png":
return "image/png"
case ".gif":
return "image/gif"
case ".webp":
return "image/webp"
case ".svg":
return "image/svg+xml"
case ".bmp":
return "image/bmp"
case ".ico":
return "image/x-icon"
// 常见文档类型
case ".pdf":
return "application/pdf"
case ".doc":
return "application/msword"
case ".docx":
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
case ".xls":
return "application/vnd.ms-excel"
case ".xlsx":
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
case ".txt":
return "text/plain; charset=utf-8"
case ".json":
return "application/json"
case ".xml":
return "application/xml"
default:
// 使用Go标准库自动检测
if mimeType := mime.TypeByExtension(ext); mimeType != "" {
return mimeType
}
// 默认类型
return "application/octet-stream"
}
}