Files
epic-go/internal/util/oss.go
hu xiaotong 8ab1379cae refactor(internal): 优化 OSS 预签名 URL 缓存刷新任务和英雄数据缓存逻辑
- 注释掉 OSS预签名 URL 缓存刷新任务的定时执行代码
- 在 hero/hero.go 中增加对 Redis缓存和英雄数据集的非空校验
- 修改 OSS预签名 URL 生成逻辑,自动替换为 CDN 域名
2025-07-17 16:11:36 +08:00

160 lines
4.6 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 util
import (
"context"
"epic/internal/consts"
"fmt"
"io"
"net/http"
"os"
"strings"
"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
}
// 缓存到Rediskey可加前缀区分
cacheKey := "oss:presignurl:" + key
// 自动生成CDN域名的预签名可访问地址
cdnPresignedUrl := strings.Replace(presignResult.URL, "https://"+bucket+".s3.bitiful.net", "https://bfoss.htoop.cn", 1)
fmt.Printf("CDN预签名可访问地址: %s\n", cdnPresignedUrl)
// 写入Redis时也用CDN域名的预签名地址
if err := RedisCache.Set(ctx, cacheKey, cdnPresignedUrl, expire); err != nil {
Error(ctx, "写入Redis失败", cacheKey, err)
}
}
return nil
}