package services import ( "fmt" "io" "net/url" "strings" "github.com/aliyun/aliyun-oss-go-sdk/oss" "git.beifan.cn/trace-system/backend-go/config" ) // OSSService 封装阿里云 OSS 上传。 type OSSService struct { cfg config.OSSConfig } func NewOSSService() OSSService { return OSSService{cfg: config.GetAppConfig().OSS} } func (s OSSService) UploadObject(objectKey string, reader io.Reader, contentType string) error { if err := s.validateConfig(); err != nil { return err } client, err := oss.New(normalizeOSSEndpoint(s.cfg.Endpoint), s.cfg.AccessKeyID, s.cfg.AccessKeySecret) if err != nil { return fmt.Errorf("初始化 OSS 客户端失败: %w", err) } bucket, err := client.Bucket(s.cfg.Bucket) if err != nil { return fmt.Errorf("打开 OSS Bucket 失败: %w", err) } options := []oss.Option{} if contentType != "" { options = append(options, oss.ContentType(contentType)) } if err := bucket.PutObject(objectKey, reader, options...); err != nil { return fmt.Errorf("上传现场图片失败: %w", err) } return nil } func (s OSSService) validateConfig() error { if strings.TrimSpace(s.cfg.Endpoint) == "" || strings.TrimSpace(s.cfg.Bucket) == "" || strings.TrimSpace(s.cfg.AccessKeyID) == "" || strings.TrimSpace(s.cfg.AccessKeySecret) == "" { return fmt.Errorf("OSS 配置未完整,请检查 endpoint、bucket、access_key_id、access_key_secret") } return nil } func (s OSSService) publicURL(objectKey string) string { if base := strings.TrimRight(strings.TrimSpace(s.cfg.PublicBaseURL), "/"); base != "" { return base + "/" + objectKey } endpoint := normalizeOSSEndpoint(s.cfg.Endpoint) u, err := url.Parse(endpoint) if err != nil || u.Host == "" { return fmt.Sprintf("https://%s.%s/%s", s.cfg.Bucket, strings.TrimPrefix(endpoint, "https://"), objectKey) } return fmt.Sprintf("%s://%s.%s/%s", u.Scheme, s.cfg.Bucket, u.Host, objectKey) } func (s OSSService) AccessURL(objectRef string) string { objectKey := s.objectKeyFromRef(objectRef) if objectKey == "" { return objectRef } if strings.TrimSpace(s.cfg.PublicBaseURL) != "" { return s.publicURL(objectKey) } url, err := s.SignedURL(objectKey) if err != nil { return s.publicURL(objectKey) } return url } func (s OSSService) SignedURL(objectKey string) (string, error) { if err := s.validateConfig(); err != nil { return "", err } client, err := oss.New(normalizeOSSEndpoint(s.cfg.Endpoint), s.cfg.AccessKeyID, s.cfg.AccessKeySecret) if err != nil { return "", fmt.Errorf("初始化 OSS 客户端失败: %w", err) } bucket, err := client.Bucket(s.cfg.Bucket) if err != nil { return "", fmt.Errorf("打开 OSS Bucket 失败: %w", err) } expires := s.cfg.SignedURLExpire if expires <= 0 { expires = 3600 } return bucket.SignURL(objectKey, oss.HTTPGet, int64(expires)) } func (s OSSService) objectKeyFromRef(objectRef string) string { objectRef = strings.TrimSpace(objectRef) if objectRef == "" { return "" } if !strings.HasPrefix(objectRef, "http://") && !strings.HasPrefix(objectRef, "https://") { return strings.TrimLeft(objectRef, "/") } u, err := url.Parse(objectRef) if err != nil { return "" } host := strings.ToLower(u.Host) bucketHostPrefix := strings.ToLower(s.cfg.Bucket) + "." if strings.HasPrefix(host, bucketHostPrefix) { return strings.TrimLeft(u.Path, "/") } if base := strings.TrimSpace(s.cfg.PublicBaseURL); base != "" { baseURL, err := url.Parse(base) if err == nil && strings.EqualFold(baseURL.Host, u.Host) { return strings.TrimLeft(strings.TrimPrefix(u.Path, baseURL.Path), "/") } } return "" } func normalizeOSSEndpoint(endpoint string) string { endpoint = strings.TrimSpace(endpoint) if endpoint == "" { return endpoint } if strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://") { return endpoint } return "https://" + endpoint }