package util import ( "context" "epic/internal/consts" "fmt" "io" "net/http" "os" "sync" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" ) var ( s3Client *s3.Client s3Once sync.Once ) // getS3Client 获取S3客户端(单例模式) func getS3Client() (*s3.Client, error) { var err error s3Once.Do(func() { accessKey := consts.S3AccessKey secretKey := consts.S3SecretKey region := consts.S3Region endpoint := consts.S3Endpoint if accessKey == "" || secretKey == "" || endpoint == "" { err = fmt.Errorf("missing S3 credentials or endpoint info in consts") return } customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { if service == s3.ServiceID { return aws.Endpoint{URL: endpoint}, nil } return aws.Endpoint{}, fmt.Errorf("unknown service requested") }) customProvider := credentials.NewStaticCredentialsProvider(accessKey, secretKey, "") cfg, cfgErr := config.LoadDefaultConfig(context.TODO(), config.WithCredentialsProvider(customProvider), config.WithEndpointResolverWithOptions(customResolver), ) if cfgErr != nil { err = fmt.Errorf("failed to load S3 config: %w", cfgErr) return } cfg.Region = region s3Client = s3.NewFromConfig(cfg) }) return s3Client, err } // 下载网络图片并上传到OSS,返回OSS路径 // imageUrl: 网络图片完整URL // fileName: 上传到OSS的完整objectKey(如 epic/image/hero/herocode.jpg) // 返回: OSS上的图片URL,或错误 func DownloadAndUploadToOSS(ctx context.Context, imageUrl string, fileName string) (string, error) { fmt.Printf("开始下载图片: %s\n", imageUrl) resp, err := http.Get(imageUrl) if err != nil { Error(ctx, "下载图片失败", imageUrl, err) return "", fmt.Errorf("failed to download image: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { Error(ctx, "下载图片状态码异常", imageUrl, resp.StatusCode) return "", fmt.Errorf("failed to download image: status %d", resp.StatusCode) } tmpFile, err := os.CreateTemp("", "ossimg-*.tmp") if err != nil { Error(ctx, "创建临时文件失败", err) return "", fmt.Errorf("failed to create temp file: %w", err) } tmpFilePath := tmpFile.Name() defer func() { tmpFile.Close() os.Remove(tmpFilePath) }() _, err = io.Copy(tmpFile, resp.Body) if err != nil { Error(ctx, "保存图片到临时文件失败", tmpFilePath, err) return "", fmt.Errorf("failed to save image to temp file: %w", err) } s3Client, err := getS3Client() if err != nil { Error(ctx, "获取S3客户端失败", err) return "", fmt.Errorf("failed to get S3 client: %w", err) } bucket := consts.S3Bucket if bucket == "" { Error(ctx, "S3 bucket未配置") return "", fmt.Errorf("missing S3 bucket info in consts") } tmpFile.Seek(0, io.SeekStart) fmt.Printf("开始上传到OSS: bucket=%s, key=%s\n", bucket, fileName) _, err = s3Client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucket), Key: aws.String(fileName), Body: tmpFile, }) if err != nil { Error(ctx, "上传到S3失败", fileName, err) return "", fmt.Errorf("failed to upload to S3: %w", err) } ossUrl := fmt.Sprintf("%s/%s", consts.S3Endpoint, fileName) fmt.Printf("上传成功,OSS图片URL: %s\n", ossUrl) return ossUrl, nil } // RefreshOssPresignedUrlCache 批量刷新OSS图片的预签名URL到Redis缓存 // keys: OSS对象key(如 epic/hero/xxx.png) // expire: 预签名URL有效期(建议30分钟-2小时) func RefreshOssPresignedUrlCache(ctx context.Context, keys []string, expire time.Duration) error { s3Client, err := getS3Client() if err != nil { return err } bucket := consts.S3Bucket presignClient := s3.NewPresignClient(s3Client) for _, key := range keys { presignResult, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{ Bucket: &bucket, Key: &key, }, func(opts *s3.PresignOptions) { opts.Expires = expire }) if err != nil { Error(ctx, "生成预签名URL失败", key, err) continue } // 缓存到Redis,key可加前缀区分 cacheKey := "oss:presignurl:" + key if err := RedisCache.Set(ctx, cacheKey, presignResult.URL, expire); err != nil { Error(ctx, "写入Redis失败", cacheKey, err) } else { // 打印预签名可访问地址 fmt.Printf("预签名可访问地址: %s\n", presignResult.URL) } } return nil }