package service import ( "fmt" "io" "strings" "time" "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/aliyun/credentials-go/credentials" "github.com/topfans/backend/gateway/config" ) // OssHelper OSS 上传与预签名(复用网关 RAM 角色 STS 凭证) type OssHelper struct { cfg config.OSSConfig } func NewOssHelper(cfg config.OSSConfig) *OssHelper { return &OssHelper{cfg: cfg} } func (h *OssHelper) bucketClientDirect() (*oss.Bucket, error) { ak := strings.TrimSpace(h.cfg.AccessKeyID) sk := strings.TrimSpace(h.cfg.AccessKeySecret) if ak == "" || sk == "" || ak == "your-access-key-id" { return nil, fmt.Errorf("OSS AccessKey 未配置") } endpoint := fmt.Sprintf("https://oss-%s.aliyuncs.com", h.cfg.Region) client, err := oss.New(endpoint, ak, sk) if err != nil { return nil, fmt.Errorf("创建 OSS 客户端失败(直连): %w", err) } bucket, err := client.Bucket(h.cfg.BucketName) if err != nil { return nil, fmt.Errorf("获取 Bucket 失败(直连): %w", err) } return bucket, nil } func (h *OssHelper) bucketClientSTS() (*oss.Bucket, error) { credConfig := new(credentials.Config). SetType("ram_role_arn"). SetAccessKeyId(h.cfg.AccessKeyID). SetAccessKeySecret(h.cfg.AccessKeySecret). SetRoleArn(h.cfg.RoleArn). SetRoleSessionName("topfans-segment-session"). SetPolicy(""). SetRoleSessionExpiration(3600) provider, err := credentials.NewCredential(credConfig) if err != nil { return nil, fmt.Errorf("创建凭证提供器失败: %w", err) } cred, err := provider.GetCredential() if err != nil { return nil, fmt.Errorf("获取临时凭证失败: %w", err) } endpoint := fmt.Sprintf("https://oss-%s.aliyuncs.com", h.cfg.Region) client, err := oss.New(endpoint, *cred.AccessKeyId, *cred.AccessKeySecret, oss.SecurityToken(*cred.SecurityToken)) if err != nil { return nil, fmt.Errorf("创建 OSS 客户端失败: %w", err) } bucket, err := client.Bucket(h.cfg.BucketName) if err != nil { return nil, fmt.Errorf("获取 Bucket 失败: %w", err) } return bucket, nil } // bucketClient 优先 STS,失败则回退 AK/SK 直连 func (h *OssHelper) bucketClient() (*oss.Bucket, error) { bucket, err := h.bucketClientSTS() if err == nil { return bucket, nil } direct, derr := h.bucketClientDirect() if derr == nil { return direct, nil } return nil, fmt.Errorf("STS: %v; 直连: %w", err, derr) } // PutObject 上传对象 func (h *OssHelper) PutObject(objectKey string, reader io.Reader, contentType string) error { bucket, err := h.bucketClient() if err != nil { return err } opts := []oss.Option{} if strings.TrimSpace(contentType) != "" { opts = append(opts, oss.ContentType(contentType)) } return bucket.PutObject(objectKey, reader, opts...) } // SignGetURL 生成 GET 预签名 URL func (h *OssHelper) SignGetURL(objectKey string, expiresSec int64) (string, error) { bucket, err := h.bucketClient() if err != nil { return "", err } if expiresSec <= 0 { expiresSec = 3600 } signedURL, err := bucket.SignURL(objectKey, oss.HTTPGet, expiresSec) if err != nil { return "", err } if idx := strings.Index(signedURL, "?"); idx >= 0 { signedURL = strings.ReplaceAll(signedURL[:idx], "%2F", "/") + signedURL[idx:] } else { signedURL = strings.ReplaceAll(signedURL, "%2F", "/") } return signedURL, nil } // BuildLaserCardCutoutKey laser-card/{star}/{user}/{date}/{id}_cutout.png func BuildLaserCardCutoutKey(starID, userID int64, fileID string) string { date := time.Now().Format("20060102") return fmt.Sprintf("laser-card/%d/%d/%s/%s_cutout.png", starID, userID, date, fileID) } // BuildSegmentTempInputKey 抠图前临时上传 func BuildSegmentTempInputKey(starID, userID int64, fileID, ext string) string { if ext == "" { ext = "jpg" } return fmt.Sprintf("laser-card-segment/tmp/%d/%d/%s_in.%s", starID, userID, fileID, ext) }