i18n翻译
This commit is contained in:
@@ -24,4 +24,6 @@ const (
|
|||||||
S3Bucket = "epic"
|
S3Bucket = "epic"
|
||||||
S3Region = "cn-east-1"
|
S3Region = "cn-east-1"
|
||||||
S3Endpoint = "https://s3.bitiful.net"
|
S3Endpoint = "https://s3.bitiful.net"
|
||||||
|
// 自定义域名常量
|
||||||
|
S3CustomDomain = "https://bfoss.htoop.cn"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,12 +2,17 @@ package cron
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"epic/internal/dao"
|
||||||
|
"epic/internal/model/entity"
|
||||||
"epic/internal/service"
|
"epic/internal/service"
|
||||||
|
"epic/internal/util"
|
||||||
"github.com/gogf/gf/v2/errors/gerror"
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
"github.com/gogf/gf/v2/os/gcron"
|
"github.com/gogf/gf/v2/os/gcron"
|
||||||
"github.com/gogf/gf/v2/os/gtime"
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Logic struct {
|
type Logic struct {
|
||||||
@@ -168,6 +173,13 @@ func (l *Logic) registerDefaultJobs(ctx context.Context) error {
|
|||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,3 +256,43 @@ func (l *Logic) refreshCache(ctx context.Context) {
|
|||||||
|
|
||||||
g.Log().Info(ctx, "Cache refresh completed")
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -195,6 +195,25 @@ func (t *ThirdPartyDataSync) processAndSaveHeroData(ctx context.Context, data []
|
|||||||
//zhAttribute := i18n.GetZh(ctx, hero.Attribute)
|
//zhAttribute := i18n.GetZh(ctx, hero.Attribute)
|
||||||
fmt.Println(hero.Assets.Image)
|
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{
|
newHero := &entity.EpicHeroInfo{
|
||||||
Id: 0,
|
Id: 0,
|
||||||
HeroName: zhHeroName,
|
HeroName: zhHeroName,
|
||||||
@@ -209,7 +228,7 @@ func (t *ThirdPartyDataSync) processAndSaveHeroData(ctx context.Context, data []
|
|||||||
Rarity: strconv.Itoa(hero.Rarity),
|
Rarity: strconv.Itoa(hero.Rarity),
|
||||||
Role: hero.Role,
|
Role: hero.Role,
|
||||||
//Zodiac: "",
|
//Zodiac: "",
|
||||||
HeadImgUrl: "",
|
HeadImgUrl: customImgUrl,
|
||||||
Attribute: hero.Attribute,
|
Attribute: hero.Attribute,
|
||||||
Remark: "",
|
Remark: "",
|
||||||
RawJson: heroJson,
|
RawJson: heroJson,
|
||||||
@@ -221,6 +240,15 @@ func (t *ThirdPartyDataSync) processAndSaveHeroData(ctx context.Context, data []
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
g.Log().Debug(ctx, "插入新英雄:", hero.Code)
|
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
|
return nil
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func TestMain(m *testing.M) {
|
|||||||
* 测试同步英雄数据
|
* 测试同步英雄数据
|
||||||
*/
|
*/
|
||||||
func TestSyncHeroData(t *testing.T) {
|
func TestSyncHeroData(t *testing.T) {
|
||||||
|
_ = i18n.RefreshI18n(context.Background())
|
||||||
thirdPartyDataSync := NewThirdPartyDataSync()
|
thirdPartyDataSync := NewThirdPartyDataSync()
|
||||||
|
|
||||||
err := thirdPartyDataSync.SyncHeroData(context.Background())
|
err := thirdPartyDataSync.SyncHeroData(context.Background())
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
@@ -16,22 +16,68 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
"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
|
// imageUrl: 网络图片完整URL
|
||||||
|
// fileName: 上传到OSS的完整objectKey(如 epic/image/hero/herocode.jpg)
|
||||||
// 返回: OSS上的图片URL,或错误
|
// 返回: OSS上的图片URL,或错误
|
||||||
func DownloadAndUploadToOSS(ctx context.Context, imageUrl string) (string, error) {
|
func DownloadAndUploadToOSS(ctx context.Context, imageUrl string, fileName string) (string, error) {
|
||||||
// 1. 下载 imageUrl 到本地临时文件
|
fmt.Printf("开始下载图片: %s\n", imageUrl)
|
||||||
resp, err := http.Get(imageUrl)
|
resp, err := http.Get(imageUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Error(ctx, "下载图片失败", imageUrl, err)
|
||||||
return "", fmt.Errorf("failed to download image: %w", err)
|
return "", fmt.Errorf("failed to download image: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
Error(ctx, "下载图片状态码异常", imageUrl, resp.StatusCode)
|
||||||
return "", fmt.Errorf("failed to download image: status %d", resp.StatusCode)
|
return "", fmt.Errorf("failed to download image: status %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpFile, err := os.CreateTemp("", "ossimg-*.tmp")
|
tmpFile, err := os.CreateTemp("", "ossimg-*.tmp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Error(ctx, "创建临时文件失败", err)
|
||||||
return "", fmt.Errorf("failed to create temp file: %w", err)
|
return "", fmt.Errorf("failed to create temp file: %w", err)
|
||||||
}
|
}
|
||||||
tmpFilePath := tmpFile.Name()
|
tmpFilePath := tmpFile.Name()
|
||||||
@@ -42,57 +88,70 @@ func DownloadAndUploadToOSS(ctx context.Context, imageUrl string) (string, error
|
|||||||
|
|
||||||
_, err = io.Copy(tmpFile, resp.Body)
|
_, err = io.Copy(tmpFile, resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Error(ctx, "保存图片到临时文件失败", tmpFilePath, err)
|
||||||
return "", fmt.Errorf("failed to save image to temp file: %w", err)
|
return "", fmt.Errorf("failed to save image to temp file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 上传临时文件到OSS,获取OSS路径
|
s3Client, err := getS3Client()
|
||||||
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),
|
|
||||||
)
|
|
||||||
if err != nil {
|
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
|
bucket := consts.S3Bucket
|
||||||
fileName := filepath.Base(tmpFilePath)
|
if bucket == "" {
|
||||||
objectKey := fmt.Sprintf("images/%d_%s", time.Now().UnixNano(), fileName)
|
Error(ctx, "S3 bucket未配置")
|
||||||
|
return "", fmt.Errorf("missing S3 bucket info in consts")
|
||||||
|
}
|
||||||
|
|
||||||
tmpFile.Seek(0, io.SeekStart)
|
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{
|
_, err = s3Client.PutObject(ctx, &s3.PutObjectInput{
|
||||||
Bucket: aws.String(bucket),
|
Bucket: aws.String(bucket),
|
||||||
Key: aws.String(objectKey),
|
Key: aws.String(fileName),
|
||||||
Body: tmpFile,
|
Body: tmpFile,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Error(ctx, "上传到S3失败", fileName, err)
|
||||||
return "", fmt.Errorf("failed to upload to S3: %w", err)
|
return "", fmt.Errorf("failed to upload to S3: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 返回OSS路径(拼接URL)
|
ossUrl := fmt.Sprintf("%s/%s", consts.S3Endpoint, fileName)
|
||||||
ossUrl := fmt.Sprintf("%s/%s/%s", endpoint, bucket, objectKey)
|
fmt.Printf("上传成功,OSS图片URL: %s\n", ossUrl)
|
||||||
return ossUrl, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user