From 7b7f8c31d7f2ff78210b84b86b7993d3db89fb86 Mon Sep 17 00:00:00 2001 From: hu xiaotong <416314413@163.com> Date: Thu, 17 Jul 2025 15:36:24 +0800 Subject: [PATCH] =?UTF-8?q?i18n=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/consts/consts.go | 2 + internal/logic/cron/cron.go | 52 +++++++ internal/logic/cron/third_party_sync.go | 30 +++- internal/logic/cron/third_party_sync_test.go | 1 + internal/util/oss.go | 137 +++++++++++++------ 5 files changed, 182 insertions(+), 40 deletions(-) diff --git a/internal/consts/consts.go b/internal/consts/consts.go index 65f15df..a9dac83 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -24,4 +24,6 @@ const ( S3Bucket = "epic" S3Region = "cn-east-1" S3Endpoint = "https://s3.bitiful.net" + // 自定义域名常量 + S3CustomDomain = "https://bfoss.htoop.cn" ) diff --git a/internal/logic/cron/cron.go b/internal/logic/cron/cron.go index 7205e0a..a523c86 100644 --- a/internal/logic/cron/cron.go +++ b/internal/logic/cron/cron.go @@ -2,12 +2,17 @@ package cron import ( "context" + "epic/internal/dao" + "epic/internal/model/entity" "epic/internal/service" + "epic/internal/util" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcron" "github.com/gogf/gf/v2/os/gtime" + "strings" "sync" + "time" ) type Logic struct { @@ -168,6 +173,13 @@ func (l *Logic) registerDefaultJobs(ctx context.Context) error { return err } + // 每30分钟执行一次OSS预签名URL缓存刷新任务 + if err := l.AddJob(ctx, "oss_presignurl_refresh", "0 0/30 * * * *", func() { + l.refreshOssPresignUrlCacheJob(ctx) + }); err != nil { + return err + } + return nil } @@ -244,3 +256,43 @@ func (l *Logic) refreshCache(ctx context.Context) { g.Log().Info(ctx, "Cache refresh completed") } + +// 刷新OSS图片预签名URL缓存的定时任务 +func (l *Logic) refreshOssPresignUrlCacheJob(ctx context.Context) { + g.Log().Info(ctx, "Starting OSS presigned URL cache refresh...") + + // 1. 从数据库读取所有英雄图片地址 + var dbHeroes []*entity.EpicHeroInfo + err := dao.EpicHeroInfo.Ctx(ctx).Scan(&dbHeroes) + if err != nil { + g.Log().Error(ctx, "Failed to query hero info for OSS presign refresh:", err) + return + } + + // 2. 提取OSS图片key(去掉域名和bucket前缀) + var keys []string + for _, hero := range dbHeroes { + headImgUrl := hero.HeadImgUrl + if headImgUrl == "" { + continue + } + // 只处理以http开头的图片地址 + // 例:https://s3.bitiful.net/bucket/epic/hero/xxx.png 或 https://bfoss.htoop.cn/epic/hero/xxx.png + // 目标key: epic/hero/xxx.png + key := "" + if idx := strings.Index(headImgUrl, "/epic/hero/"); idx != -1 { + key = headImgUrl[idx+1:] + } + if key != "" { + keys = append(keys, key) + } + } + + expire := 1 * time.Hour // 预签名URL有效期 + err = util.RefreshOssPresignedUrlCache(ctx, keys, expire) + if err != nil { + g.Log().Error(ctx, "OSS presigned URL cache refresh failed:", err) + } else { + g.Log().Info(ctx, "OSS presigned URL cache refresh completed") + } +} diff --git a/internal/logic/cron/third_party_sync.go b/internal/logic/cron/third_party_sync.go index 54fe45a..d969530 100644 --- a/internal/logic/cron/third_party_sync.go +++ b/internal/logic/cron/third_party_sync.go @@ -195,6 +195,25 @@ func (t *ThirdPartyDataSync) processAndSaveHeroData(ctx context.Context, data [] //zhAttribute := i18n.GetZh(ctx, hero.Attribute) fmt.Println(hero.Assets.Image) + // 上传图片到 OSS,并获取自定义域名的访问路径 + ossObjectKey := fmt.Sprintf("epic/hero/images/%s.png", hero.Code) + ossUrl, err := util.DownloadAndUploadToOSS(ctx, hero.Assets.Icon, ossObjectKey) + if err != nil { + g.Log().Error(ctx, "上传英雄图片到OSS失败:", err) + return err // 直接返回,后续数据库不会插入 + } + fmt.Println(ossUrl) + // 替换为自定义域名 + customImgUrl := "" + if ossUrl != "" { + prefix := consts.S3Endpoint + "/" + consts.S3Bucket + if len(ossUrl) > len(prefix) && ossUrl[:len(prefix)] == prefix { + customImgUrl = consts.S3CustomDomain + ossUrl[len(prefix):] + } else { + customImgUrl = ossUrl // fallback + } + } + newHero := &entity.EpicHeroInfo{ Id: 0, HeroName: zhHeroName, @@ -209,7 +228,7 @@ func (t *ThirdPartyDataSync) processAndSaveHeroData(ctx context.Context, data [] Rarity: strconv.Itoa(hero.Rarity), Role: hero.Role, //Zodiac: "", - HeadImgUrl: "", + HeadImgUrl: customImgUrl, Attribute: hero.Attribute, Remark: "", RawJson: heroJson, @@ -221,6 +240,15 @@ func (t *ThirdPartyDataSync) processAndSaveHeroData(ctx context.Context, data [] continue } g.Log().Debug(ctx, "插入新英雄:", hero.Code) + + // 新增后立即刷新该图片的OSS预签名URL到缓存 + if customImgUrl != "" { + key := ossObjectKey + err := util.RefreshOssPresignedUrlCache(ctx, []string{key}, 24*time.Hour) + if err != nil { + g.Log().Error(ctx, "刷新新英雄图片预签名URL失败:", key, err) + } + } } return nil diff --git a/internal/logic/cron/third_party_sync_test.go b/internal/logic/cron/third_party_sync_test.go index 7e1e253..7cb9725 100644 --- a/internal/logic/cron/third_party_sync_test.go +++ b/internal/logic/cron/third_party_sync_test.go @@ -27,6 +27,7 @@ func TestMain(m *testing.M) { * 测试同步英雄数据 */ func TestSyncHeroData(t *testing.T) { + _ = i18n.RefreshI18n(context.Background()) thirdPartyDataSync := NewThirdPartyDataSync() err := thirdPartyDataSync.SyncHeroData(context.Background()) diff --git a/internal/util/oss.go b/internal/util/oss.go index be41a35..a76e4f2 100644 --- a/internal/util/oss.go +++ b/internal/util/oss.go @@ -7,7 +7,7 @@ import ( "io" "net/http" "os" - "path/filepath" + "sync" "time" "github.com/aws/aws-sdk-go-v2/aws" @@ -16,22 +16,68 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" ) -// DownloadAndUploadToOSS 下载网络图片并上传到OSS,返回OSS路径 +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) (string, error) { - // 1. 下载 imageUrl 到本地临时文件 +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() @@ -42,57 +88,70 @@ func DownloadAndUploadToOSS(ctx context.Context, imageUrl string) (string, error _, 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) } - // 2. 上传临时文件到OSS,获取OSS路径 - accessKey := consts.S3AccessKey - secretKey := consts.S3SecretKey - bucket := consts.S3Bucket - region := consts.S3Region - endpoint := consts.S3Endpoint - if accessKey == "" || secretKey == "" || bucket == "" || endpoint == "" { - return "", fmt.Errorf("missing S3 credentials or bucket info in consts") - } - - 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, err := config.LoadDefaultConfig(ctx, - config.WithCredentialsProvider(customProvider), - config.WithEndpointResolverWithOptions(customResolver), - ) + s3Client, err := getS3Client() if err != nil { - return "", fmt.Errorf("failed to load S3 config: %w", err) + Error(ctx, "获取S3客户端失败", err) + return "", fmt.Errorf("failed to get S3 client: %w", err) } - cfg.Region = region - s3Client := s3.NewFromConfig(cfg) - // 生成唯一的 object key - fileName := filepath.Base(tmpFilePath) - objectKey := fmt.Sprintf("images/%d_%s", time.Now().UnixNano(), fileName) + bucket := consts.S3Bucket + if bucket == "" { + Error(ctx, "S3 bucket未配置") + return "", fmt.Errorf("missing S3 bucket info in consts") + } tmpFile.Seek(0, io.SeekStart) - _, err = tmpFile.Stat() - if err != nil { - return "", fmt.Errorf("failed to stat temp file: %w", err) - } - // 上传 + fmt.Printf("开始上传到OSS: bucket=%s, key=%s\n", bucket, fileName) _, err = s3Client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucket), - Key: aws.String(objectKey), + Key: aws.String(fileName), Body: tmpFile, }) if err != nil { + Error(ctx, "上传到S3失败", fileName, err) return "", fmt.Errorf("failed to upload to S3: %w", err) } - // 4. 返回OSS路径(拼接URL) - ossUrl := fmt.Sprintf("%s/%s/%s", endpoint, bucket, objectKey) + 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 +}