package services import ( "context" "fmt" "io" "net/url" "strings" "time" "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss" "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials" "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 } request := &oss.PutObjectRequest{ Bucket: oss.Ptr(s.cfg.Bucket), Key: oss.Ptr(objectKey), Body: reader, } if contentType != "" { request.ContentType = oss.Ptr(contentType) } if _, err := s.newClient().PutObject(context.TODO(), request); 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 } expires := s.cfg.SignedURLExpire if expires <= 0 { expires = 3600 } result, err := s.newClient().Presign( context.TODO(), &oss.GetObjectRequest{ Bucket: oss.Ptr(s.cfg.Bucket), Key: oss.Ptr(objectKey), }, oss.PresignExpires(time.Duration(expires)*time.Second), ) if err != nil { return "", fmt.Errorf("生成 OSS 签名 URL 失败: %w", err) } return result.URL, nil } 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 } func (s OSSService) newClient() *oss.Client { cfg := oss.LoadDefaultConfig(). WithCredentialsProvider(credentials.NewStaticCredentialsProvider( s.cfg.AccessKeyID, s.cfg.AccessKeySecret, )). WithRegion(strings.TrimSpace(s.cfg.Region)). WithEndpoint(strings.TrimPrefix(strings.TrimPrefix(normalizeOSSEndpoint(s.cfg.Endpoint), "https://"), "http://")) return oss.NewClient(cfg) }