Compare commits
10 Commits
8657bc4eea
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dd95f787a | ||
|
|
707b3cb347 | ||
|
|
fc41c5ca73 | ||
|
|
ce0fa7f2ed | ||
|
|
f8001aef5b | ||
|
|
c36a2cb8b0 | ||
|
|
0ad79c4f27 | ||
|
|
9ef6ac9cdb | ||
|
|
8ab1379cae | ||
|
|
7b7f8c31d7 |
13
README.MD
13
README.MD
@@ -52,22 +52,13 @@ redis:
|
||||
|
||||
### 启动服务
|
||||
|
||||
#### Linux/Mac
|
||||
```bash
|
||||
chmod +x scripts/start.sh
|
||||
./scripts/start.sh
|
||||
```
|
||||
|
||||
#### Windows
|
||||
```cmd
|
||||
scripts\start.bat
|
||||
```
|
||||
|
||||
#### 手动启动
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 访问服务
|
||||
|
||||
- **API服务**: http://localhost:8283
|
||||
|
||||
@@ -7,12 +7,13 @@ import (
|
||||
"epic/internal/logic/i18n"
|
||||
"epic/internal/service"
|
||||
"epic/internal/util"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gcmd"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gcmd"
|
||||
)
|
||||
|
||||
func CORS(r *ghttp.Request) {
|
||||
@@ -31,7 +32,7 @@ var (
|
||||
|
||||
// 启动定时任务
|
||||
if err := service.Cron().StartAllJobs(ctx); err != nil {
|
||||
g.Log().Error(ctx, "Failed to start cron jobs:", err)
|
||||
util.Error(ctx, "Failed to start cron jobs:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -66,7 +67,7 @@ func setupGracefulShutdown(ctx context.Context) {
|
||||
|
||||
// 停止定时任务
|
||||
if err := service.Cron().StopAllJobs(ctx); err != nil {
|
||||
g.Log().Error(ctx, "Failed to stop cron jobs:", err)
|
||||
util.Error(ctx, "Failed to stop cron jobs:", err)
|
||||
}
|
||||
|
||||
// 退出程序
|
||||
|
||||
@@ -2,8 +2,8 @@ package consts
|
||||
|
||||
const (
|
||||
// 笑门官网查询英雄名称和神器名称,用作中文翻译
|
||||
SimileHeroName = "https://static.smilegatemegaport.com/gameRecord/epic7/epic7_hero.json?_=1729322698936"
|
||||
SimileArtifactName = "https://static.smilegatemegaport.com/gameRecord/epic7/epic7_artifact.json?_=1729322698936"
|
||||
SimileHeroName = "https://static.smilegatemegaport.com/gameRecord/epic7/epic7_hero.json"
|
||||
SimileArtifactName = "https://static.smilegatemegaport.com/gameRecord/epic7/epic7_artifact.json"
|
||||
|
||||
// 获取角色信息
|
||||
HeroListURL = "https://e7-optimizer-game-data.s3-accelerate.amazonaws.com/herodata.json"
|
||||
@@ -18,10 +18,14 @@ const (
|
||||
// 角色图片基础 URL
|
||||
GfHeroPngURL = "https://static.smilegatemegaport.com/event/live/epic7/guide/images/hero/"
|
||||
|
||||
EPIC_DB_URL = "https://epic7db.com/"
|
||||
|
||||
// S3/OSS 配置
|
||||
S3AccessKey = "s5iWm6wXVvhCNN9nJlXwgWRf"
|
||||
S3SecretKey = "91sTurpFtugXijPg0uSof3JcJma0HED"
|
||||
S3Bucket = "epic"
|
||||
S3Region = "cn-east-1"
|
||||
S3Endpoint = "https://s3.bitiful.net"
|
||||
// 自定义域名常量
|
||||
S3CustomDomain = "https://bfoss.htoop.cn"
|
||||
)
|
||||
|
||||
@@ -21,44 +21,48 @@ type EpicHeroInfoDao struct {
|
||||
|
||||
// EpicHeroInfoColumns defines and stores column names for the table epic_hero_info.
|
||||
type EpicHeroInfoColumns struct {
|
||||
Id string // 文件编号
|
||||
HeroName string // 配置编号
|
||||
HeroCode string // 文件名
|
||||
HeroAttrLv60 string // 文件路径
|
||||
Creator string // 创建者
|
||||
CreateTime string // 创建时间
|
||||
Updater string // 更新者
|
||||
UpdateTime string // 更新时间
|
||||
Deleted string // 是否删除
|
||||
NickName string // 配置编号
|
||||
Rarity string // 配置编号
|
||||
Role string // 配置编号
|
||||
Zodiac string // 配置编号
|
||||
HeadImgUrl string // 配置编号
|
||||
Attribute string // 配置编号
|
||||
Remark string // 配置编号
|
||||
RawJson string // 原始json
|
||||
Id string // 文件编号
|
||||
HeroName string // 配置编号
|
||||
HeroCode string // 文件名
|
||||
HeroAttrLv60 string // 文件路径
|
||||
Creator string // 创建者
|
||||
CreateTime string // 创建时间
|
||||
Updater string // 更新者
|
||||
UpdateTime string // 更新时间
|
||||
Deleted string // 是否删除
|
||||
NickName string // 配置编号
|
||||
Rarity string // 配置编号
|
||||
Role string // 配置编号
|
||||
Zodiac string // 配置编号
|
||||
HeadImgUrl string // 配置编号
|
||||
Attribute string // 配置编号
|
||||
Remark string // 配置编号
|
||||
RawJson string // 原始json
|
||||
SetContentJson string // 配装json数据
|
||||
SetUpdateTime string // 配装更新时间
|
||||
}
|
||||
|
||||
// epicHeroInfoColumns holds the columns for the table epic_hero_info.
|
||||
var epicHeroInfoColumns = EpicHeroInfoColumns{
|
||||
Id: "id",
|
||||
HeroName: "hero_name",
|
||||
HeroCode: "hero_code",
|
||||
HeroAttrLv60: "hero_attr_lv60",
|
||||
Creator: "creator",
|
||||
CreateTime: "create_time",
|
||||
Updater: "updater",
|
||||
UpdateTime: "update_time",
|
||||
Deleted: "deleted",
|
||||
NickName: "nick_name",
|
||||
Rarity: "rarity",
|
||||
Role: "role",
|
||||
Zodiac: "zodiac",
|
||||
HeadImgUrl: "head_img_url",
|
||||
Attribute: "attribute",
|
||||
Remark: "remark",
|
||||
RawJson: "raw_json",
|
||||
Id: "id",
|
||||
HeroName: "hero_name",
|
||||
HeroCode: "hero_code",
|
||||
HeroAttrLv60: "hero_attr_lv60",
|
||||
Creator: "creator",
|
||||
CreateTime: "create_time",
|
||||
Updater: "updater",
|
||||
UpdateTime: "update_time",
|
||||
Deleted: "deleted",
|
||||
NickName: "nick_name",
|
||||
Rarity: "rarity",
|
||||
Role: "role",
|
||||
Zodiac: "zodiac",
|
||||
HeadImgUrl: "head_img_url",
|
||||
Attribute: "attribute",
|
||||
Remark: "remark",
|
||||
RawJson: "raw_json",
|
||||
SetContentJson: "set_content_json",
|
||||
SetUpdateTime: "set_update_time",
|
||||
}
|
||||
|
||||
// NewEpicHeroInfoDao creates and returns a new DAO object for table data access.
|
||||
|
||||
@@ -32,6 +32,7 @@ type EpicI18NMappingsColumns struct {
|
||||
Updater string // 更新者
|
||||
UpdateTime string // 更新时间
|
||||
Deleted string // 是否删除
|
||||
Code string // 编码
|
||||
}
|
||||
|
||||
// epicI18NMappingsColumns holds the columns for the table epic_i18n_mappings.
|
||||
@@ -47,6 +48,7 @@ var epicI18NMappingsColumns = EpicI18NMappingsColumns{
|
||||
Updater: "updater",
|
||||
UpdateTime: "update_time",
|
||||
Deleted: "deleted",
|
||||
Code: "code",
|
||||
}
|
||||
|
||||
// NewEpicI18NMappingsDao creates and returns a new DAO object for table data access.
|
||||
|
||||
@@ -2,12 +2,17 @@ package cron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"epic/internal/dao"
|
||||
"epic/internal/logic/i18n"
|
||||
"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 {
|
||||
@@ -31,7 +36,7 @@ func init() {
|
||||
|
||||
// StartAllJobs 启动所有定时任务
|
||||
func (l *Logic) StartAllJobs(ctx context.Context) error {
|
||||
g.Log().Info(ctx, "Starting all cron jobs...")
|
||||
util.Info(ctx, "Starting all cron jobs...")
|
||||
|
||||
// 启动定时任务调度器
|
||||
l.cron.Start()
|
||||
@@ -41,13 +46,13 @@ func (l *Logic) StartAllJobs(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
g.Log().Info(ctx, "All cron jobs started successfully")
|
||||
util.Info(ctx, "All cron jobs started successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopAllJobs 停止所有定时任务
|
||||
func (l *Logic) StopAllJobs(ctx context.Context) error {
|
||||
g.Log().Info(ctx, "Stopping all cron jobs...")
|
||||
util.Info(ctx, "Stopping all cron jobs...")
|
||||
|
||||
l.jobsMux.Lock()
|
||||
defer l.jobsMux.Unlock()
|
||||
@@ -56,13 +61,13 @@ func (l *Logic) StopAllJobs(ctx context.Context) error {
|
||||
for name, entry := range l.jobs {
|
||||
l.cron.Remove(entry.Name)
|
||||
delete(l.jobs, name)
|
||||
g.Log().Infof(ctx, "Stopped job: %s", name)
|
||||
util.Infof(ctx, "Stopped job: %s", name)
|
||||
}
|
||||
|
||||
// 停止调度器
|
||||
l.cron.Stop()
|
||||
|
||||
g.Log().Info(ctx, "All cron jobs stopped successfully")
|
||||
util.Info(ctx, "All cron jobs stopped successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -79,11 +84,11 @@ func (l *Logic) AddJob(ctx context.Context, name, cron string, job func()) error
|
||||
// 添加任务到调度器
|
||||
entry, err := l.cron.Add(ctx, cron, func(ctx context.Context) {
|
||||
startTime := gtime.Now()
|
||||
g.Log().Infof(ctx, "Starting job: %s at %s", name, startTime.String())
|
||||
util.Infof(ctx, "Starting job: %s at %s", name, startTime.String())
|
||||
job()
|
||||
endTime := gtime.Now()
|
||||
duration := endTime.Sub(startTime)
|
||||
g.Log().Infof(ctx, "Completed job: %s, duration: %v", name, duration)
|
||||
util.Infof(ctx, "Completed job: %s, duration: %v", name, duration)
|
||||
}, name)
|
||||
|
||||
if err != nil {
|
||||
@@ -92,7 +97,7 @@ func (l *Logic) AddJob(ctx context.Context, name, cron string, job func()) error
|
||||
|
||||
// 保存任务引用
|
||||
l.jobs[name] = entry
|
||||
g.Log().Infof(ctx, "Added job: %s with cron: %s", name, cron)
|
||||
util.Infof(ctx, "Added job: %s with cron: %s", name, cron)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -111,7 +116,7 @@ func (l *Logic) RemoveJob(ctx context.Context, name string) error {
|
||||
l.cron.Remove(entry.Name)
|
||||
delete(l.jobs, name)
|
||||
|
||||
g.Log().Infof(ctx, "Removed job: %s", name)
|
||||
util.Infof(ctx, "Removed job: %s", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -126,19 +131,19 @@ func (l *Logic) GetJobStatus(ctx context.Context, name string) (bool, error) {
|
||||
|
||||
// registerDefaultJobs 注册默认的定时任务
|
||||
func (l *Logic) registerDefaultJobs(ctx context.Context) error {
|
||||
// 每小时执行一次数据同步任务
|
||||
if err := l.AddJob(ctx, "data_sync_hourly", "0 0 * * * *", func() {
|
||||
l.syncDataFromThirdParty(ctx)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 每天凌晨2点执行数据清理任务
|
||||
if err := l.AddJob(ctx, "data_cleanup_daily", "0 0 2 * * *", func() {
|
||||
l.cleanupOldData(ctx)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
//// 每小时执行一次数据同步任务
|
||||
//if err := l.AddJob(ctx, "data_sync_hourly", "0 0 * * * *", func() {
|
||||
// l.syncDataFromThirdParty(ctx)
|
||||
//}); err != nil {
|
||||
// return err
|
||||
//}
|
||||
//
|
||||
//// 每天凌晨2点执行数据清理任务
|
||||
//if err := l.AddJob(ctx, "data_cleanup_daily", "0 0 2 * * *", func() {
|
||||
// l.cleanupOldData(ctx)
|
||||
//}); err != nil {
|
||||
// return err
|
||||
//}
|
||||
|
||||
// 每5分钟执行一次健康检查任务
|
||||
if err := l.AddJob(ctx, "health_check", "0 0/5 * * * *", func() {
|
||||
@@ -168,61 +173,68 @@ func (l *Logic) registerDefaultJobs(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 每5天执行一次角色配装信息刷新任务
|
||||
if err := l.AddJob(ctx, "hero_set_refresh_5days", "0 0 0 */5 * *", func() {
|
||||
l.refreshHeroSetContent(ctx)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 每天凌晨2点同步i18n远程翻译数据
|
||||
if err := l.AddJob(ctx, "i18n_remote_sync", "0 0 2 * * *", func() {
|
||||
util.Info(ctx, "开始同步i18n远程翻译数据...")
|
||||
if err := i18n.GetI18nLogic().SyncI18nFromRemote(ctx); err != nil {
|
||||
util.Error(ctx, "i18n远程翻译数据同步失败:", err)
|
||||
} else {
|
||||
util.Info(ctx, "i18n远程翻译数据同步完成")
|
||||
}
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncDataFromThirdParty 从第三方网站同步数据
|
||||
func (l *Logic) syncDataFromThirdParty(ctx context.Context) {
|
||||
g.Log().Info(ctx, "Starting data sync from third party...")
|
||||
util.Info(ctx, "Starting data sync from third party...")
|
||||
|
||||
// 使用第三方数据同步器
|
||||
if err := l.sync.SyncAllData(ctx); err != nil {
|
||||
g.Log().Error(ctx, "Data sync failed:", err)
|
||||
util.Error(ctx, "Data sync failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
g.Log().Info(ctx, "Data sync completed")
|
||||
util.Info(ctx, "Data sync completed")
|
||||
}
|
||||
|
||||
// syncHeroData 同步英雄数据
|
||||
// 同步英雄数据
|
||||
func (l *Logic) syncHeroData(ctx context.Context) {
|
||||
g.Log().Info(ctx, "Starting hero data sync...")
|
||||
util.Info(ctx, "Starting hero data sync...")
|
||||
|
||||
if err := l.sync.SyncHeroData(ctx); err != nil {
|
||||
g.Log().Error(ctx, "Hero data sync failed:", err)
|
||||
util.Error(ctx, "Hero data sync failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
g.Log().Info(ctx, "Hero data sync completed")
|
||||
util.Info(ctx, "Hero data sync completed")
|
||||
}
|
||||
|
||||
// syncArtifactData 同步神器数据
|
||||
// 同步神器数据
|
||||
func (l *Logic) syncArtifactData(ctx context.Context) {
|
||||
g.Log().Info(ctx, "Starting artifact data sync...")
|
||||
util.Info(ctx, "Starting artifact data sync...")
|
||||
|
||||
if err := l.sync.SyncArtifactData(ctx); err != nil {
|
||||
g.Log().Error(ctx, "Artifact data sync failed:", err)
|
||||
util.Error(ctx, "Artifact data sync failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
g.Log().Info(ctx, "Artifact data sync completed")
|
||||
}
|
||||
|
||||
// cleanupOldData 清理旧数据
|
||||
func (l *Logic) cleanupOldData(ctx context.Context) {
|
||||
g.Log().Info(ctx, "Starting data cleanup...")
|
||||
|
||||
// TODO: 实现数据清理逻辑
|
||||
// 1. 删除过期的缓存数据
|
||||
// 2. 清理过期的日志记录
|
||||
// 3. 归档历史数据
|
||||
|
||||
g.Log().Info(ctx, "Data cleanup completed")
|
||||
util.Info(ctx, "Artifact data sync completed")
|
||||
}
|
||||
|
||||
// healthCheck 健康检查
|
||||
func (l *Logic) healthCheck(ctx context.Context) {
|
||||
g.Log().Debug(ctx, "Performing health check...")
|
||||
util.Debug(ctx, "Performing health check...")
|
||||
|
||||
// TODO: 实现健康检查逻辑
|
||||
// 1. 检查数据库连接
|
||||
@@ -230,17 +242,67 @@ func (l *Logic) healthCheck(ctx context.Context) {
|
||||
// 3. 检查第三方API可用性
|
||||
// 4. 记录系统状态
|
||||
|
||||
g.Log().Debug(ctx, "Health check completed")
|
||||
util.Debug(ctx, "Health check completed")
|
||||
}
|
||||
|
||||
// refreshCache 刷新缓存
|
||||
func (l *Logic) refreshCache(ctx context.Context) {
|
||||
g.Log().Info(ctx, "Starting cache refresh...")
|
||||
util.Info(ctx, "Starting cache refresh...")
|
||||
|
||||
// TODO: 实现缓存刷新逻辑
|
||||
// 1. 刷新英雄数据缓存
|
||||
// 2. 刷新神器数据缓存
|
||||
// 3. 刷新其他业务缓存
|
||||
|
||||
g.Log().Info(ctx, "Cache refresh completed")
|
||||
util.Info(ctx, "Cache refresh completed")
|
||||
}
|
||||
|
||||
// 刷新OSS图片预签名URL缓存的定时任务
|
||||
func (l *Logic) refreshOssPresignUrlCacheJob(ctx context.Context) {
|
||||
util.Info(ctx, "Starting OSS presigned URL cache refresh...")
|
||||
|
||||
// 1. 从数据库读取所有英雄图片地址
|
||||
var dbHeroes []*entity.EpicHeroInfo
|
||||
err := dao.EpicHeroInfo.Ctx(ctx).Scan(&dbHeroes)
|
||||
if err != nil {
|
||||
util.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 {
|
||||
util.Error(ctx, "OSS presigned URL cache refresh failed:", err)
|
||||
} else {
|
||||
util.Info(ctx, "OSS presigned URL cache refresh completed")
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:定时刷新角色配装信息
|
||||
func (l *Logic) refreshHeroSetContent(ctx context.Context) {
|
||||
util.Info(ctx, "Starting hero set content refresh...")
|
||||
if err := l.sync.RefreshAllHeroSetContent(ctx); err != nil {
|
||||
util.Error(ctx, "Hero set content refresh failed:", err)
|
||||
return
|
||||
}
|
||||
util.Info(ctx, "Hero set content refresh completed")
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/gogf/gf/v2/net/gclient"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -31,45 +32,43 @@ func NewThirdPartyDataSync() *ThirdPartyDataSync {
|
||||
}
|
||||
|
||||
func (t *ThirdPartyDataSync) SyncHeroData(ctx context.Context) error {
|
||||
g.Log().Info(ctx, "开始同步英雄数据...")
|
||||
util.Info(ctx, "开始同步英雄数据...")
|
||||
|
||||
// 示例:从第三方API获取英雄数据
|
||||
heroData, err := t.fetchHeroDataFromAPI(ctx)
|
||||
if err != nil || heroData == nil {
|
||||
g.Log().Error(ctx, "获取英雄数据失败:", err)
|
||||
util.Error(ctx, "获取英雄数据失败:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 处理并保存数据
|
||||
if err := t.processAndSaveHeroData(ctx, heroData); err != nil {
|
||||
g.Log().Error(ctx, "处理英雄数据失败:", err)
|
||||
util.Error(ctx, "处理英雄数据失败:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
g.Log().Info(ctx, "英雄数据同步完成")
|
||||
util.Info(ctx, "英雄数据同步完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncArtifactData 同步神器数据
|
||||
// 同步神器数据
|
||||
func (t *ThirdPartyDataSync) SyncArtifactData(ctx context.Context) error {
|
||||
g.Log().Info(ctx, "开始同步神器数据...")
|
||||
util.Info(ctx, "开始同步神器数据...")
|
||||
|
||||
util.RedisCache.Set(ctx, "artifacts_all11", "asd", 0)
|
||||
//从第三方API获取神器数据
|
||||
artifactData, err := t.fetchArtifactDataFromAPI(ctx)
|
||||
if err != nil {
|
||||
util.Error(ctx, "获取神器数据失败:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 示例:从第三方API获取神器数据
|
||||
//artifactData, err := t.fetchArtifactDataFromAPI(ctx)
|
||||
//if err != nil {
|
||||
// g.Log().Error(ctx, "获取神器数据失败:", err)
|
||||
// return err
|
||||
//}
|
||||
//
|
||||
//// 处理并保存数据
|
||||
//if err := t.processAndSaveArtifactData(ctx, artifactData); err != nil {
|
||||
// g.Log().Error(ctx, "处理神器数据失败:", err)
|
||||
// return err
|
||||
//}
|
||||
// 处理并保存数据
|
||||
if err := t.processAndSaveArtifactData(ctx, artifactData); err != nil {
|
||||
util.Error(ctx, "处理神器数据失败:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
g.Log().Info(ctx, "神器数据同步完成")
|
||||
util.Info(ctx, "神器数据同步完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -103,13 +102,40 @@ func (t *ThirdPartyDataSync) fetchHeroDataFromAPI(ctx context.Context) ([]byte,
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// fetchHeroBuildsFromAPI 通过角色英文名POST请求获取配装数据
|
||||
func (t *ThirdPartyDataSync) FetchHeroBuildsFromAPI(ctx context.Context, heroName string) (string, error) {
|
||||
apiURL := consts.HeroNameURL
|
||||
|
||||
headers := map[string]string{
|
||||
"User-Agent": "EpicGameBot/1.0",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
heroNameEN := i18n.Zh2En(heroName)
|
||||
|
||||
resp, err := t.client.Header(headers).Post(ctx, apiURL, heroNameEN)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("API请求失败: %v", err)
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("API响应错误,状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
content := resp.ReadAll()
|
||||
g.Log().Debug(ctx, "配装API响应内容长度:", len(content))
|
||||
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
// 从API获取神器数据
|
||||
func (t *ThirdPartyDataSync) fetchArtifactDataFromAPI(ctx context.Context) (string, error) {
|
||||
// 示例API地址
|
||||
apiURL := "https://static.smilegatemegaport.com/gameRecord/epic7/epic7_artifact.json?_=1729322698936"
|
||||
apiURL := consts.ArtifactDataURL
|
||||
|
||||
headers := map[string]string{
|
||||
//"User-Agent": "EpicGameBot/1.0",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
@@ -143,11 +169,16 @@ func (t *ThirdPartyDataSync) processAndSaveHeroData(ctx context.Context, data []
|
||||
return fmt.Errorf("解析英雄数据到DTO失败: %v", err)
|
||||
}
|
||||
|
||||
g.Log().Info(ctx, "解析到", len(heroes), "个英雄数据")
|
||||
util.Info(ctx, "解析到", len(heroes), "个英雄数据")
|
||||
|
||||
// 一次性查出所有数据库英雄,构建map
|
||||
var dbHeroes []*entity.EpicHeroInfo
|
||||
err := dao.EpicHeroInfo.Ctx(ctx).Scan(&dbHeroes)
|
||||
err := dao.EpicHeroInfo.Ctx(ctx).
|
||||
Fields(
|
||||
dao.EpicHeroInfo.Columns().HeroCode,
|
||||
dao.EpicHeroInfo.Columns().RawJson,
|
||||
).
|
||||
Scan(&dbHeroes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询数据库英雄失败: %v", err)
|
||||
}
|
||||
@@ -168,7 +199,7 @@ func (t *ThirdPartyDataSync) processAndSaveHeroData(ctx context.Context, data []
|
||||
// 只更新 rawJson 字段
|
||||
rawJsonBytes, err := json.Marshal(hero)
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, "序列化英雄数据失败:", err)
|
||||
util.Error(ctx, "序列化英雄数据失败:", err)
|
||||
continue
|
||||
}
|
||||
rawJson := string(rawJsonBytes)
|
||||
@@ -180,7 +211,7 @@ func (t *ThirdPartyDataSync) processAndSaveHeroData(ctx context.Context, data []
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, "更新英雄rawJson失败:", err)
|
||||
util.Error(ctx, "更新英雄rawJson失败:", err)
|
||||
continue
|
||||
}
|
||||
g.Log().Debug(ctx, "更新英雄rawJson:", hero.Code)
|
||||
@@ -195,6 +226,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 {
|
||||
util.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 +259,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,
|
||||
@@ -217,38 +267,128 @@ func (t *ThirdPartyDataSync) processAndSaveHeroData(ctx context.Context, data []
|
||||
|
||||
_, err = dao.EpicHeroInfo.Ctx(ctx).Data(newHero).Insert()
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, "插入新英雄失败:", err)
|
||||
util.Error(ctx, "插入新英雄失败:", err)
|
||||
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 {
|
||||
util.Error(ctx, "刷新新英雄图片预签名URL失败:", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processAndSaveArtifactData 处理并保存神器数据
|
||||
// 处理并保存神器数据
|
||||
func (t *ThirdPartyDataSync) processAndSaveArtifactData(ctx context.Context, data string) error {
|
||||
// 使用 gjson 解析
|
||||
j := gjson.New(data)
|
||||
zhcn := j.Get("zh-CN")
|
||||
// 检查json对象本身和其内部值,并使用 .Var().IsSlice() 这种更可靠的方式判断是否为数组
|
||||
if !zhcn.IsSlice() {
|
||||
return fmt.Errorf("神器数据格式错误,期望是一个JSON数组")
|
||||
// 1. 解析json为map
|
||||
var artifactMap map[string]struct {
|
||||
Name string `json:"name"`
|
||||
Rarity int `json:"rarity"`
|
||||
Role string `json:"role"`
|
||||
Stats struct {
|
||||
Attack int `json:"attack"`
|
||||
Health int `json:"health"`
|
||||
Defense int `json:"defense"`
|
||||
} `json:"stats"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(data), &artifactMap); err != nil {
|
||||
return fmt.Errorf("解析神器数据失败: %v", err)
|
||||
}
|
||||
util.Info(ctx, "解析到", len(artifactMap), "个神器数据")
|
||||
|
||||
// 2. 查询数据库所有神器,构建map
|
||||
var dbArtifacts []*entity.EpicArtifactInfo
|
||||
err := dao.EpicArtifactInfo.Ctx(ctx).Scan(&dbArtifacts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询数据库神器失败: %v", err)
|
||||
}
|
||||
artifactDbMap := make(map[string]*entity.EpicArtifactInfo, len(dbArtifacts))
|
||||
for _, a := range dbArtifacts {
|
||||
artifactDbMap[a.ArtifactNameEn] = a
|
||||
}
|
||||
|
||||
var artifacts []*dto.ThirdPartyArtifactDTO
|
||||
if err := zhcn.Scan(&artifacts); err != nil {
|
||||
return fmt.Errorf("解析神器数据到DTO失败: %v", err)
|
||||
}
|
||||
for _, art := range artifactMap {
|
||||
var dbArt *entity.EpicArtifactInfo
|
||||
if v, ok := artifactDbMap[art.Name]; ok {
|
||||
dbArt = v
|
||||
}
|
||||
|
||||
// 批量处理数据
|
||||
for _, artifact := range artifacts {
|
||||
if err := t.saveArtifactData(ctx, artifact); err != nil {
|
||||
g.Log().Error(ctx, "保存神器数据失败:", err)
|
||||
// 如果数据库中已有自定义CDN图片,跳过处理
|
||||
if dbArt != nil && dbArt.ImageUrl != "" && strings.HasPrefix(dbArt.ImageUrl, consts.S3CustomDomain) {
|
||||
//g.Log().Debug(ctx, "跳过已有CDN图片的神器:", art.Name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
customImgUrl := getArtifactImageUrl(ctx, art.Code, dbArt)
|
||||
artifact := &entity.EpicArtifactInfo{
|
||||
ArtifactName: i18n.GetZh(ctx, art.Name),
|
||||
ArtifactNameEn: art.Name,
|
||||
ArtifactCode: art.Code,
|
||||
Rarity: strconv.Itoa(art.Rarity),
|
||||
Role: art.Role,
|
||||
StatsAttack: art.Stats.Attack,
|
||||
StatsHealth: art.Stats.Health,
|
||||
StatsDefense: art.Stats.Defense,
|
||||
ImageUrl: customImgUrl,
|
||||
Updater: "sync",
|
||||
UpdateTime: gtime.Now(),
|
||||
Deleted: false,
|
||||
}
|
||||
if dbArt != nil {
|
||||
artifact.Id = dbArt.Id
|
||||
_, err := dao.EpicArtifactInfo.Ctx(ctx).
|
||||
Where(dao.EpicArtifactInfo.Columns().ArtifactCode, art.Code).
|
||||
Data(g.Map{
|
||||
dao.EpicArtifactInfo.Columns().ArtifactName: artifact.ArtifactName,
|
||||
dao.EpicArtifactInfo.Columns().ArtifactNameEn: artifact.ArtifactNameEn,
|
||||
dao.EpicArtifactInfo.Columns().Rarity: artifact.Rarity,
|
||||
dao.EpicArtifactInfo.Columns().Role: artifact.Role,
|
||||
dao.EpicArtifactInfo.Columns().StatsAttack: artifact.StatsAttack,
|
||||
dao.EpicArtifactInfo.Columns().StatsHealth: artifact.StatsHealth,
|
||||
dao.EpicArtifactInfo.Columns().StatsDefense: artifact.StatsDefense,
|
||||
dao.EpicArtifactInfo.Columns().ImageUrl: artifact.ImageUrl,
|
||||
dao.EpicArtifactInfo.Columns().Updater: artifact.Updater,
|
||||
dao.EpicArtifactInfo.Columns().UpdateTime: gtime.Now(),
|
||||
dao.EpicArtifactInfo.Columns().Deleted: artifact.Deleted,
|
||||
}).Update()
|
||||
if err != nil {
|
||||
util.Error(ctx, "更新神器失败:", art.Name, err)
|
||||
continue
|
||||
}
|
||||
util.Info(ctx, "更新神器:", art.Name)
|
||||
} else {
|
||||
artifact.Creator = "sync"
|
||||
_, err := dao.EpicArtifactInfo.Ctx(ctx).Data(g.Map{
|
||||
dao.EpicArtifactInfo.Columns().ArtifactName: artifact.ArtifactName,
|
||||
dao.EpicArtifactInfo.Columns().ArtifactNameEn: artifact.ArtifactNameEn,
|
||||
dao.EpicArtifactInfo.Columns().ArtifactCode: artifact.ArtifactCode,
|
||||
dao.EpicArtifactInfo.Columns().Rarity: artifact.Rarity,
|
||||
dao.EpicArtifactInfo.Columns().Role: artifact.Role,
|
||||
dao.EpicArtifactInfo.Columns().StatsAttack: artifact.StatsAttack,
|
||||
dao.EpicArtifactInfo.Columns().StatsHealth: artifact.StatsHealth,
|
||||
dao.EpicArtifactInfo.Columns().StatsDefense: artifact.StatsDefense,
|
||||
dao.EpicArtifactInfo.Columns().ImageUrl: artifact.ImageUrl,
|
||||
dao.EpicArtifactInfo.Columns().Creator: "sync",
|
||||
dao.EpicArtifactInfo.Columns().CreateTime: gtime.Now(),
|
||||
dao.EpicArtifactInfo.Columns().Updater: artifact.Updater,
|
||||
dao.EpicArtifactInfo.Columns().UpdateTime: artifact.UpdateTime,
|
||||
dao.EpicArtifactInfo.Columns().Deleted: artifact.Deleted,
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
util.Error(ctx, "插入神器失败:", art.Name, err)
|
||||
continue
|
||||
}
|
||||
util.Info(ctx, "插入神器:", art.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -257,6 +397,10 @@ func (t *ThirdPartyDataSync) saveHeroData(ctx context.Context, hero *dto.ThirdPa
|
||||
// 查询是否存在
|
||||
var dbHero *entity.EpicHeroInfo
|
||||
err := dao.EpicHeroInfo.Ctx(ctx).
|
||||
Fields(
|
||||
dao.EpicHeroInfo.Columns().HeroCode,
|
||||
dao.EpicHeroInfo.Columns().RawJson,
|
||||
).
|
||||
Where(dao.EpicHeroInfo.Columns().HeroCode, hero.Code).
|
||||
Scan(&dbHero)
|
||||
if err != nil {
|
||||
@@ -346,22 +490,333 @@ func (t *ThirdPartyDataSync) saveArtifactData(ctx context.Context, artifact *dto
|
||||
|
||||
// SyncAllData 同步所有数据
|
||||
func (t *ThirdPartyDataSync) SyncAllData(ctx context.Context) error {
|
||||
g.Log().Info(ctx, "开始同步所有第三方数据...")
|
||||
util.Info(ctx, "开始同步所有第三方数据...")
|
||||
|
||||
// 同步英雄数据
|
||||
if err := t.SyncHeroData(ctx); err != nil {
|
||||
g.Log().Error(ctx, "英雄数据同步失败:", err)
|
||||
util.Error(ctx, "英雄数据同步失败:", err)
|
||||
// 继续同步其他数据
|
||||
}
|
||||
|
||||
// 同步神器数据
|
||||
if err := t.SyncArtifactData(ctx); err != nil {
|
||||
g.Log().Error(ctx, "神器数据同步失败:", err)
|
||||
util.Error(ctx, "神器数据同步失败:", err)
|
||||
// 继续同步其他数据
|
||||
}
|
||||
|
||||
// 可以继续添加其他数据类型的同步
|
||||
|
||||
g.Log().Info(ctx, "所有第三方数据同步完成")
|
||||
util.Info(ctx, "所有第三方数据同步完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshHeroSetContentByHeroInfo 刷新单个角色配装字段
|
||||
func (t *ThirdPartyDataSync) RefreshHeroSetContentByHeroInfo(ctx context.Context, heroName, heroCode, jsonStr string) error {
|
||||
g.Log().Infof(ctx, "刷新角色配装: %s", heroName)
|
||||
if len(jsonStr) > 200 {
|
||||
_, err := dao.EpicHeroInfo.Ctx(ctx).
|
||||
Where(dao.EpicHeroInfo.Columns().HeroCode, heroCode).
|
||||
Data(g.Map{
|
||||
dao.EpicHeroInfo.Columns().SetContentJson: jsonStr,
|
||||
dao.EpicHeroInfo.Columns().SetUpdateTime: gtime.Now(),
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "更新数据库失败: %s, err: %v", heroName, err)
|
||||
return err
|
||||
}
|
||||
g.Log().Infof(ctx, "已更新: %s", heroName)
|
||||
return nil
|
||||
} else {
|
||||
util.Error(ctx, "配装数据无效(长度<=200): %s", heroName)
|
||||
return fmt.Errorf("配装数据无效(长度<=200)")
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新所有角色配装字段
|
||||
func (t *ThirdPartyDataSync) RefreshAllHeroSetContent(ctx context.Context) error {
|
||||
util.Info(ctx, "开始批量刷新所有角色配装字段...")
|
||||
|
||||
// 1. 查询所有角色,按set_update_time正序排列,为空的在最前面
|
||||
var heroList []*entity.EpicHeroInfo
|
||||
err := dao.EpicHeroInfo.Ctx(ctx).
|
||||
OrderAsc("set_update_time").
|
||||
Scan(&heroList)
|
||||
if err != nil {
|
||||
util.Error(ctx, "查询epic_hero_info失败:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for i, epicHeroInfo := range heroList {
|
||||
g.Log().Infof(ctx, "[%d/%d] 处理角色: %s", i+1, len(heroList), epicHeroInfo.HeroName)
|
||||
jsonStr, err := t.FetchHeroBuildsFromAPI(ctx, epicHeroInfo.HeroName)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取配装数据失败: %s, err: %v", epicHeroInfo.HeroName, err)
|
||||
} else {
|
||||
err = t.RefreshHeroSetContentByHeroInfo(ctx, epicHeroInfo.HeroName, epicHeroInfo.HeroCode, jsonStr)
|
||||
}
|
||||
// 远程接口有频率限制,停顿10分钟
|
||||
if i < len(heroList)-1 {
|
||||
g.Log().Info(ctx, "等待10分钟...")
|
||||
time.Sleep(10 * time.Minute)
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
g.Log().Info(ctx, "所有角色配装字段刷新完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取神器图片URL,已上传则复用,否则从epic7db.com匹配并上传到OSS
|
||||
func getArtifactImageUrl(ctx context.Context, artCode string, dbArt *entity.EpicArtifactInfo) string {
|
||||
if dbArt != nil && dbArt.ImageUrl != "" && strings.HasPrefix(dbArt.ImageUrl, consts.S3CustomDomain) {
|
||||
g.Log().Debug(ctx, "神器已有CDN图片,直接返回:", artCode, dbArt.ImageUrl)
|
||||
return dbArt.ImageUrl
|
||||
}
|
||||
|
||||
// 取神器英文名,优先用dbArt.ArtifactNameEn,否则artCode
|
||||
var artifactNameEn string
|
||||
if dbArt != nil && dbArt.ArtifactNameEn != "" {
|
||||
artifactNameEn = dbArt.ArtifactNameEn
|
||||
} else {
|
||||
artifactNameEn = artCode
|
||||
}
|
||||
g.Log().Debug(ctx, "开始匹配神器图片:", artCode, "英文名:", artifactNameEn)
|
||||
|
||||
// 从Redis获取神器爬虫数据
|
||||
redisVal, err := util.RedisCache.Get(ctx, "epic7:artifacts")
|
||||
if err != nil || redisVal == nil {
|
||||
util.Error(ctx, "获取Redis神器数据失败:", err)
|
||||
return ""
|
||||
}
|
||||
var artifactArr []map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(redisVal.String()), &artifactArr); err != nil {
|
||||
util.Error(ctx, "解析Redis神器数据失败:", err)
|
||||
return ""
|
||||
}
|
||||
g.Log().Debug(ctx, "Redis中共有", len(artifactArr), "个神器数据")
|
||||
|
||||
normalize := func(s string) string {
|
||||
r := []rune{}
|
||||
for _, c := range s {
|
||||
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') {
|
||||
r = append(r, c)
|
||||
}
|
||||
}
|
||||
return strings.ToLower(string(r))
|
||||
}
|
||||
|
||||
// 计算字符串相似度(Levenshtein距离)
|
||||
similarity := func(s1, s2 string) float64 {
|
||||
if s1 == s2 {
|
||||
return 1.0
|
||||
}
|
||||
if len(s1) == 0 || len(s2) == 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// 计算编辑距离
|
||||
d := make([][]int, len(s1)+1)
|
||||
for i := range d {
|
||||
d[i] = make([]int, len(s2)+1)
|
||||
}
|
||||
|
||||
for i := 0; i <= len(s1); i++ {
|
||||
d[i][0] = i
|
||||
}
|
||||
for j := 0; j <= len(s2); j++ {
|
||||
d[0][j] = j
|
||||
}
|
||||
|
||||
for i := 1; i <= len(s1); i++ {
|
||||
for j := 1; j <= len(s2); j++ {
|
||||
if s1[i-1] == s2[j-1] {
|
||||
d[i][j] = d[i-1][j-1]
|
||||
} else {
|
||||
d[i][j] = min(d[i-1][j]+1, min(d[i][j-1]+1, d[i-1][j-1]+1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maxLen := max(len(s1), len(s2))
|
||||
if maxLen == 0 {
|
||||
return 1.0
|
||||
}
|
||||
return 1.0 - float64(d[len(s1)][len(s2)])/float64(maxLen)
|
||||
}
|
||||
|
||||
localNorm := normalize(artifactNameEn)
|
||||
g.Log().Debug(ctx, "本地神器名标准化后:", localNorm)
|
||||
|
||||
var bestMatch map[string]interface{}
|
||||
var bestSimilarity float64
|
||||
var bestMatchName string
|
||||
|
||||
// 先尝试精确匹配
|
||||
g.Log().Debug(ctx, "开始精确匹配...")
|
||||
for _, item := range artifactArr {
|
||||
archName, ok := item["arch_name"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
archNorm := normalize(archName)
|
||||
if archNorm == localNorm {
|
||||
bestMatch = item
|
||||
bestMatchName = archName
|
||||
g.Log().Debug(ctx, "精确匹配成功:", archName, "->", archNorm)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果精确匹配失败,尝试模糊匹配
|
||||
if bestMatch == nil {
|
||||
g.Log().Debug(ctx, "精确匹配失败,开始模糊匹配...")
|
||||
for _, item := range artifactArr {
|
||||
archName, ok := item["arch_name"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
archNorm := normalize(archName)
|
||||
sim := similarity(localNorm, archNorm)
|
||||
if sim > bestSimilarity {
|
||||
bestSimilarity = sim
|
||||
bestMatch = item
|
||||
bestMatchName = archName
|
||||
g.Log().Debug(ctx, "发现更高相似度匹配:", archName, "相似度:", sim)
|
||||
}
|
||||
}
|
||||
|
||||
// 只有相似度达到90%才使用模糊匹配结果
|
||||
if bestSimilarity < 0.9 {
|
||||
g.Log().Debug(ctx, "模糊匹配相似度不足90%,最高相似度:", bestSimilarity, "最佳匹配:", bestMatchName)
|
||||
bestMatch = nil
|
||||
} else {
|
||||
g.Log().Debug(ctx, "模糊匹配成功,相似度:", bestSimilarity, "匹配名称:", bestMatchName)
|
||||
}
|
||||
}
|
||||
|
||||
if bestMatch != nil {
|
||||
archSrc, ok := bestMatch["arch_src"].(string)
|
||||
if !ok || archSrc == "" {
|
||||
util.Error(ctx, "匹配成功但arch_src为空:", bestMatchName)
|
||||
return ""
|
||||
}
|
||||
g.Log().Debug(ctx, "开始下载并上传图片:", bestMatchName, "图片路径:", archSrc)
|
||||
|
||||
// 拼接第三方图片完整URL
|
||||
imgUrl := consts.EPIC_DB_URL + strings.TrimPrefix(archSrc, "/")
|
||||
// 上传到OSS
|
||||
ossObjectKey := fmt.Sprintf("epic/artifact/images/%s.png", artCode)
|
||||
ossUrl, err := util.DownloadAndUploadToOSS(ctx, imgUrl, ossObjectKey)
|
||||
if err != nil || ossUrl == "" {
|
||||
util.Error(ctx, "下载上传图片失败:", bestMatchName, "错误:", err)
|
||||
return ""
|
||||
}
|
||||
prefix := consts.S3Endpoint + "/" + consts.S3Bucket
|
||||
if strings.HasPrefix(ossUrl, prefix) {
|
||||
finalUrl := consts.S3CustomDomain + ossUrl[len(prefix):]
|
||||
g.Log().Debug(ctx, "图片处理成功:", bestMatchName, "最终URL:", finalUrl)
|
||||
return finalUrl
|
||||
}
|
||||
g.Log().Debug(ctx, "图片处理成功:", bestMatchName, "OSS URL:", ossUrl)
|
||||
return ossUrl
|
||||
}
|
||||
|
||||
g.Log().Debug(ctx, "神器匹配失败:", artifactNameEn, "标准化后:", localNorm)
|
||||
return ""
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// 修复历史数据,将epic_hero_info.hero_name和epic_artifact_info.artifact_name翻译为中文(基于code匹配)
|
||||
func FixHeroAndArtifactNameToChinese(ctx context.Context) error {
|
||||
// 1. 查询i18n hero映射(code->zh)
|
||||
var heroMappings []*entity.EpicI18NMappings
|
||||
err := dao.EpicI18NMappings.Ctx(ctx).
|
||||
Where(dao.EpicI18NMappings.Columns().Language, "zh").
|
||||
Where(dao.EpicI18NMappings.Columns().Category, "hero").
|
||||
Where(dao.EpicI18NMappings.Columns().Status, 1).
|
||||
Where(dao.EpicI18NMappings.Columns().Deleted, 0).
|
||||
Scan(&heroMappings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询i18n hero映射失败: %v", err)
|
||||
}
|
||||
heroCodeToZh := make(map[string]string)
|
||||
for _, m := range heroMappings {
|
||||
heroCodeToZh[m.Code] = m.Value
|
||||
}
|
||||
|
||||
// 2. 修复英雄表
|
||||
var heroes []*entity.EpicHeroInfo
|
||||
err = dao.EpicHeroInfo.Ctx(ctx).
|
||||
Fields(
|
||||
dao.EpicHeroInfo.Columns().HeroCode,
|
||||
dao.EpicHeroInfo.Columns().HeroName,
|
||||
).
|
||||
Scan(&heroes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询epic_hero_info失败: %v", err)
|
||||
}
|
||||
for _, h := range heroes {
|
||||
if zh, ok := heroCodeToZh[h.HeroCode]; ok && zh != "" && zh != h.HeroName {
|
||||
_, err := dao.EpicHeroInfo.Ctx(ctx).
|
||||
Where(dao.EpicHeroInfo.Columns().HeroCode, h.HeroCode).
|
||||
Data(g.Map{dao.EpicHeroInfo.Columns().HeroName: zh}).
|
||||
Update()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "更新英雄中文名失败: %s(%s) -> %s, err: %v", h.HeroName, h.HeroCode, zh, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
g.Log().Info(ctx, "epic_hero_info英雄名修复完成")
|
||||
|
||||
// 3. 查询i18n artifact映射(code->zh)
|
||||
var artifactMappings []*entity.EpicI18NMappings
|
||||
err = dao.EpicI18NMappings.Ctx(ctx).
|
||||
Where(dao.EpicI18NMappings.Columns().Language, "zh").
|
||||
Where(dao.EpicI18NMappings.Columns().Category, "artifact").
|
||||
Where(dao.EpicI18NMappings.Columns().Status, 1).
|
||||
Where(dao.EpicI18NMappings.Columns().Deleted, 0).
|
||||
Scan(&artifactMappings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询i18n artifact映射失败: %v", err)
|
||||
}
|
||||
artifactCodeToZh := make(map[string]string)
|
||||
for _, m := range artifactMappings {
|
||||
artifactCodeToZh[m.Code] = m.Value
|
||||
}
|
||||
|
||||
// 4. 修复神器表
|
||||
var artifacts []*entity.EpicArtifactInfo
|
||||
err = dao.EpicArtifactInfo.Ctx(ctx).Scan(&artifacts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询epic_artifact_info失败: %v", err)
|
||||
}
|
||||
for _, a := range artifacts {
|
||||
if zh, ok := artifactCodeToZh[a.ArtifactCode]; ok && zh != "" && zh != a.ArtifactName {
|
||||
_, err := dao.EpicArtifactInfo.Ctx(ctx).
|
||||
Where(dao.EpicArtifactInfo.Columns().ArtifactCode, a.ArtifactCode).
|
||||
Data(g.Map{dao.EpicArtifactInfo.Columns().ArtifactName: zh}).
|
||||
Update()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "更新神器中文名失败: %s(%s) -> %s, err: %v", a.ArtifactName, a.ArtifactCode, zh, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
g.Log().Info(ctx, "epic_artifact_info神器名修复完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -23,10 +23,20 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试同步英雄数据
|
||||
*/
|
||||
func TestI18n(t *testing.T) {
|
||||
_ = i18n.RefreshI18n(context.Background())
|
||||
fmt.Println(i18n.Zh2En("湖畔魔女泰妮布里雅"))
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试同步英雄数据
|
||||
*/
|
||||
func TestSyncHeroData(t *testing.T) {
|
||||
_ = i18n.RefreshI18n(context.Background())
|
||||
thirdPartyDataSync := NewThirdPartyDataSync()
|
||||
|
||||
err := thirdPartyDataSync.SyncHeroData(context.Background())
|
||||
@@ -39,8 +49,8 @@ func TestSyncHeroData(t *testing.T) {
|
||||
* 测试同步神器数据
|
||||
*/
|
||||
func TestSyncArtifactData_Success(t *testing.T) {
|
||||
_ = i18n.RefreshI18n(context.Background())
|
||||
sync := NewThirdPartyDataSync()
|
||||
|
||||
err := sync.SyncArtifactData(context.Background())
|
||||
if err != nil {
|
||||
t.Errorf("expected success, but got error: %v", err)
|
||||
@@ -57,3 +67,22 @@ func TestInitI18nStaticToDB(t *testing.T) {
|
||||
}
|
||||
t.Logf("静态i18n数据导入成功,共%d条", len(i18n.I18nEnToZh))
|
||||
}
|
||||
|
||||
func TestSyncI18nFromRemote(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
logic := i18n.GetI18nLogic()
|
||||
err := logic.SyncI18nFromRemote(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("远程i18n数据同步失败: %v", err)
|
||||
}
|
||||
t.Logf("远程i18n数据同步成功")
|
||||
}
|
||||
|
||||
func TestFixHeroAndArtifactNameToChinese(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := FixHeroAndArtifactNameToChinese(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("修复英雄/神器中文名失败: %v", err)
|
||||
}
|
||||
t.Logf("修复英雄/神器中文名成功")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
v1 "epic/api/hero/v1"
|
||||
"epic/internal/dao"
|
||||
"epic/internal/logic/cron"
|
||||
"epic/internal/model/dto"
|
||||
"epic/internal/model/entity"
|
||||
"epic/internal/service"
|
||||
@@ -34,6 +35,19 @@ func (l *Logic) GetHeroByCode(ctx context.Context, code string) (*entity.EpicHer
|
||||
// 2. 缓存未命中,查数据库
|
||||
var hero *entity.EpicHeroInfo
|
||||
err := dao.EpicHeroInfo.Ctx(ctx).
|
||||
Fields(
|
||||
dao.EpicHeroInfo.Columns().Id,
|
||||
dao.EpicHeroInfo.Columns().HeroName,
|
||||
dao.EpicHeroInfo.Columns().HeroCode,
|
||||
dao.EpicHeroInfo.Columns().HeroAttrLv60,
|
||||
dao.EpicHeroInfo.Columns().NickName,
|
||||
dao.EpicHeroInfo.Columns().Rarity,
|
||||
dao.EpicHeroInfo.Columns().Role,
|
||||
dao.EpicHeroInfo.Columns().Zodiac,
|
||||
dao.EpicHeroInfo.Columns().HeadImgUrl,
|
||||
dao.EpicHeroInfo.Columns().Attribute,
|
||||
dao.EpicHeroInfo.Columns().Remark,
|
||||
).
|
||||
Where(dao.EpicHeroInfo.Columns().HeroCode, code).
|
||||
Scan(&hero)
|
||||
if err != nil {
|
||||
@@ -57,7 +71,19 @@ func (l *Logic) GetHeroList(ctx context.Context) ([]*v1.EpicHeroVO, error) {
|
||||
|
||||
// 2. 缓存未命中,查数据库
|
||||
err := dao.EpicHeroInfo.Ctx(ctx).
|
||||
OrderDesc(dao.EpicHeroInfo.Columns().CreateTime). // 按创建时间倒序
|
||||
Fields(
|
||||
dao.EpicHeroInfo.Columns().Id,
|
||||
dao.EpicHeroInfo.Columns().HeroName,
|
||||
dao.EpicHeroInfo.Columns().HeroCode,
|
||||
dao.EpicHeroInfo.Columns().HeroAttrLv60,
|
||||
dao.EpicHeroInfo.Columns().NickName,
|
||||
dao.EpicHeroInfo.Columns().Rarity,
|
||||
dao.EpicHeroInfo.Columns().Role,
|
||||
dao.EpicHeroInfo.Columns().Zodiac,
|
||||
dao.EpicHeroInfo.Columns().HeadImgUrl,
|
||||
dao.EpicHeroInfo.Columns().Attribute,
|
||||
dao.EpicHeroInfo.Columns().Remark,
|
||||
).OrderDesc(dao.EpicHeroInfo.Columns().CreateTime).
|
||||
Scan(&doList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -101,6 +127,19 @@ func (l *Logic) GetHeroDetailByCode(ctx context.Context, code string) (*v1.HeroD
|
||||
)
|
||||
|
||||
err = dao.EpicHeroInfo.Ctx(ctx).
|
||||
Fields(
|
||||
dao.EpicHeroInfo.Columns().Id,
|
||||
dao.EpicHeroInfo.Columns().HeroName,
|
||||
dao.EpicHeroInfo.Columns().HeroCode,
|
||||
dao.EpicHeroInfo.Columns().HeroAttrLv60,
|
||||
dao.EpicHeroInfo.Columns().NickName,
|
||||
dao.EpicHeroInfo.Columns().Rarity,
|
||||
dao.EpicHeroInfo.Columns().Role,
|
||||
dao.EpicHeroInfo.Columns().Zodiac,
|
||||
dao.EpicHeroInfo.Columns().HeadImgUrl,
|
||||
dao.EpicHeroInfo.Columns().Attribute,
|
||||
dao.EpicHeroInfo.Columns().Remark,
|
||||
).
|
||||
Where(dao.EpicHeroInfo.Columns().HeroCode, code).
|
||||
Scan(&epicHeroInfo)
|
||||
|
||||
@@ -115,6 +154,7 @@ func (l *Logic) GetHeroDetailByCode(ctx context.Context, code string) (*v1.HeroD
|
||||
return nil, err
|
||||
}
|
||||
|
||||
thirdPartySync := cron.NewThirdPartyDataSync()
|
||||
// 优化:先查 RedisCache,再查数据库
|
||||
cacheKey := "epic_hero_set:" + code
|
||||
jsonContent, err := util.RedisCache.Get(ctx, cacheKey)
|
||||
@@ -130,8 +170,29 @@ func (l *Logic) GetHeroDetailByCode(ctx context.Context, code string) (*v1.HeroD
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 写入 Redis 缓存,1小时
|
||||
util.RedisCache.Set(ctx, cacheKey, fribbleHeroSet.JsonContent, 0)
|
||||
if util.RedisCache == nil {
|
||||
panic("util.RedisCache is nil")
|
||||
}
|
||||
if fribbleHeroSet == nil {
|
||||
// 新增:如果fribbleHeroSet为nil,调用第三方接口获取配装json
|
||||
//thirdPartySync := cron.NewThirdPartyDataSync()
|
||||
jsonStr, err := thirdPartySync.FetchHeroBuildsFromAPI(ctx, epicHeroInfo.HeroName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fribbleHeroSet = &entity.FribbleHeroSet{
|
||||
HeroCode: code,
|
||||
JsonContent: jsonStr,
|
||||
}
|
||||
}
|
||||
if fribbleHeroSet.JsonContent != "" {
|
||||
err := thirdPartySync.RefreshHeroSetContentByHeroInfo(ctx, epicHeroInfo.HeroName, code, fribbleHeroSet.JsonContent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 写入 Redis 缓存,永久
|
||||
util.RedisCache.Set(ctx, cacheKey, fribbleHeroSet.JsonContent, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 JsonContent 字段
|
||||
@@ -397,7 +458,7 @@ func (l *Logic) GetHeroDetailByCode(ctx context.Context, code string) (*v1.HeroD
|
||||
return heroDetailVO, nil
|
||||
}
|
||||
|
||||
// ClearHeroCache 清理英雄相关缓存
|
||||
// 清理英雄相关缓存
|
||||
func (l *Logic) ClearHeroCache(ctx context.Context, code string) error {
|
||||
redis := g.Redis()
|
||||
|
||||
|
||||
@@ -2,17 +2,20 @@ package i18n
|
||||
|
||||
import (
|
||||
"context"
|
||||
"epic/internal/consts"
|
||||
"epic/internal/dao"
|
||||
"epic/internal/model/entity"
|
||||
"epic/internal/util"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/gclient"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// I18nEnToZh 英文->中文映射表(示例)
|
||||
// I18nEnToZh 英文->中文映射表(仅做备份,不再作为主数据源)
|
||||
var I18nEnToZh = map[string]string{
|
||||
"A Little Queen's Huge Crown": "小小女王的巨大王冠",
|
||||
"A Song for Everybody": "献给你和这个世界的歌曲",
|
||||
@@ -603,14 +606,22 @@ var I18nZhToEn = func() map[string]string {
|
||||
return m
|
||||
}()
|
||||
|
||||
// Logic i18n主逻辑结构体
|
||||
// cache结构调整:lang -> { code -> value, key -> value }
|
||||
type Logic struct {
|
||||
cache map[string]map[string]string // lang -> key -> value
|
||||
cache map[string]struct {
|
||||
ByCode map[string]string // code->value
|
||||
ByKey map[string]string // key->value
|
||||
}
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func New() *Logic {
|
||||
return &Logic{
|
||||
cache: make(map[string]map[string]string),
|
||||
cache: make(map[string]struct {
|
||||
ByCode map[string]string
|
||||
ByKey map[string]string
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,25 +656,44 @@ func (l *Logic) LoadFromDB(ctx context.Context) error {
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
// 重新构建缓存
|
||||
l.cache = make(map[string]map[string]string)
|
||||
l.cache = make(map[string]struct {
|
||||
ByCode map[string]string
|
||||
ByKey map[string]string
|
||||
})
|
||||
for _, m := range mappings {
|
||||
if l.cache[m.Language] == nil {
|
||||
l.cache[m.Language] = make(map[string]string)
|
||||
if l.cache[m.Language].ByCode == nil {
|
||||
l.cache[m.Language] = struct {
|
||||
ByCode map[string]string
|
||||
ByKey map[string]string
|
||||
}{ByCode: make(map[string]string), ByKey: make(map[string]string)}
|
||||
}
|
||||
if m.Code != "" {
|
||||
l.cache[m.Language].ByCode[m.Code] = m.Value
|
||||
}
|
||||
l.cache[m.Language].ByKey[m.KeyName] = m.Value
|
||||
if m.Language == "zh" {
|
||||
I18nEnToZh[m.KeyName] = m.Value
|
||||
I18nZhToEn[m.Value] = m.KeyName
|
||||
}
|
||||
l.cache[m.Language][m.KeyName] = m.Value
|
||||
}
|
||||
|
||||
util.Info(ctx, "i18n缓存加载完成,共", len(mappings), "条记录")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get 获取指定key的指定语言翻译
|
||||
func (l *Logic) Get(ctx context.Context, lang, key string) string {
|
||||
// Get 获取指定key/code的指定语言翻译
|
||||
func (l *Logic) Get(ctx context.Context, lang, key string, code ...string) string {
|
||||
l.mutex.RLock()
|
||||
defer l.mutex.RUnlock()
|
||||
|
||||
if len(code) > 0 && code[0] != "" {
|
||||
if m, ok := l.cache[lang]; ok {
|
||||
if v, ok := m.ByCode[code[0]]; ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
if m, ok := l.cache[lang]; ok {
|
||||
if v, ok := m[key]; ok {
|
||||
if v, ok := m.ByKey[key]; ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
@@ -678,7 +708,7 @@ func (l *Logic) GetBatch(ctx context.Context, lang string, keys []string) map[st
|
||||
result := make(map[string]string)
|
||||
if m, ok := l.cache[lang]; ok {
|
||||
for _, key := range keys {
|
||||
if v, ok := m[key]; ok {
|
||||
if v, ok := m.ByKey[key]; ok {
|
||||
result[key] = v
|
||||
} else {
|
||||
result[key] = key // 找不到返回原文
|
||||
@@ -719,10 +749,16 @@ func (l *Logic) Add(ctx context.Context, key, lang, value, category string) erro
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if l.cache[lang] == nil {
|
||||
l.cache[lang] = make(map[string]string)
|
||||
if _, exists := l.cache[lang]; !exists {
|
||||
l.cache[lang] = struct {
|
||||
ByCode map[string]string
|
||||
ByKey map[string]string
|
||||
}{
|
||||
ByCode: make(map[string]string),
|
||||
ByKey: make(map[string]string),
|
||||
}
|
||||
}
|
||||
l.cache[lang][key] = value
|
||||
l.cache[lang].ByKey[key] = value
|
||||
|
||||
util.Info(ctx, "添加翻译:", lang, key, "->", value)
|
||||
return nil
|
||||
@@ -747,10 +783,16 @@ func (l *Logic) Update(ctx context.Context, key, lang, value string) error {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if l.cache[lang] == nil {
|
||||
l.cache[lang] = make(map[string]string)
|
||||
if _, exists := l.cache[lang]; !exists {
|
||||
l.cache[lang] = struct {
|
||||
ByCode map[string]string
|
||||
ByKey map[string]string
|
||||
}{
|
||||
ByCode: make(map[string]string),
|
||||
ByKey: make(map[string]string),
|
||||
}
|
||||
}
|
||||
l.cache[lang][key] = value
|
||||
l.cache[lang].ByKey[key] = value
|
||||
|
||||
util.Info(ctx, "更新翻译:", lang, key, "->", value)
|
||||
return nil
|
||||
@@ -776,7 +818,7 @@ func (l *Logic) Delete(ctx context.Context, key, lang string) error {
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if m, ok := l.cache[lang]; ok {
|
||||
delete(m, key)
|
||||
delete(m.ByKey, key)
|
||||
}
|
||||
|
||||
util.Info(ctx, "删除翻译:", lang, key)
|
||||
@@ -806,7 +848,7 @@ func (l *Logic) GetByCategory(ctx context.Context, lang, category string) (map[s
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ImportFromMap 从map批量导入翻译
|
||||
// 从map批量导入翻译
|
||||
func (l *Logic) ImportFromMap(ctx context.Context, lang string, mappings map[string]string, category string) error {
|
||||
if len(mappings) == 0 {
|
||||
return nil
|
||||
@@ -835,11 +877,17 @@ func (l *Logic) ImportFromMap(ctx context.Context, lang string, mappings map[str
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if l.cache[lang] == nil {
|
||||
l.cache[lang] = make(map[string]string)
|
||||
if _, exists := l.cache[lang]; !exists {
|
||||
l.cache[lang] = struct {
|
||||
ByCode map[string]string
|
||||
ByKey map[string]string
|
||||
}{
|
||||
ByCode: make(map[string]string),
|
||||
ByKey: make(map[string]string),
|
||||
}
|
||||
}
|
||||
for key, value := range mappings {
|
||||
l.cache[lang][key] = value
|
||||
l.cache[lang].ByKey[key] = value
|
||||
}
|
||||
|
||||
util.Info(ctx, "批量导入翻译完成:", lang, category, "共", len(mappings), "条")
|
||||
@@ -871,16 +919,24 @@ func (l *Logic) StartAutoRefresh(ctx context.Context) {
|
||||
}()
|
||||
}
|
||||
|
||||
// En2Zh 英文转中文(静态映射)
|
||||
// En2Zh 英文转中文(静态映射,仅降级时使用)
|
||||
func En2Zh(s string) string {
|
||||
if v, ok := GetI18nLogic().cache["zh"].ByKey[s]; ok {
|
||||
return v
|
||||
}
|
||||
// 仅降级时才用备份
|
||||
if v, ok := I18nEnToZh[s]; ok {
|
||||
return v
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Zh2En 中文转英文(静态映射)
|
||||
// Zh2En 中文转英文(静态映射,仅降级时使用)
|
||||
func Zh2En(s string) string {
|
||||
if v, ok := GetI18nLogic().cache["en"].ByKey[s]; ok {
|
||||
return v
|
||||
}
|
||||
// 仅降级时才用备份
|
||||
if v, ok := I18nZhToEn[s]; ok {
|
||||
return v
|
||||
}
|
||||
@@ -962,3 +1018,158 @@ func ImportI18nFromMap(ctx context.Context, lang string, mappings map[string]str
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncI18nFromRemote 从远程接口拉取英雄和神器的中英文名称,写入数据库和内存缓存
|
||||
func (l *Logic) SyncI18nFromRemote(ctx context.Context) error {
|
||||
client := gclient.New()
|
||||
|
||||
types := []struct {
|
||||
url string
|
||||
category string
|
||||
}{
|
||||
{consts.SimileHeroName, "hero"},
|
||||
{consts.SimileArtifactName, "artifact"},
|
||||
}
|
||||
|
||||
for _, t := range types {
|
||||
g.Log().Infof(ctx, "拉取i18n远程数据: %s", t.url)
|
||||
resp, err := client.Get(ctx, t.url)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "拉取远程i18n数据失败: %s, err: %v", t.url, err)
|
||||
continue
|
||||
}
|
||||
content := resp.ReadAll()
|
||||
j := gjson.New(content)
|
||||
if j == nil || j.IsNil() {
|
||||
g.Log().Errorf(ctx, "解析远程i18n数据失败: %s, 内容为空", t.url)
|
||||
continue
|
||||
}
|
||||
var data map[string][]map[string]interface{}
|
||||
if err := j.Var().Scan(&data); err != nil {
|
||||
g.Log().Errorf(ctx, "解析远程i18n数据失败: %s, err: %v", t.url, err)
|
||||
continue
|
||||
}
|
||||
enArr, enOk := data["en"]
|
||||
zhArr, zhOk := data["zh-CN"]
|
||||
if !enOk || !zhOk {
|
||||
g.Log().Warningf(ctx, "远程i18n数据缺少en或zh-CN: %s", t.url)
|
||||
continue
|
||||
}
|
||||
// code->en/zh
|
||||
enMap := make(map[string]string)
|
||||
zhMap := make(map[string]string)
|
||||
for _, item := range enArr {
|
||||
if code, ok := item["code"].(string); ok {
|
||||
if name, ok := item["name"].(string); ok {
|
||||
enMap[code] = name
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, item := range zhArr {
|
||||
if code, ok := item["code"].(string); ok {
|
||||
if name, ok := item["name"].(string); ok {
|
||||
zhMap[code] = name
|
||||
}
|
||||
}
|
||||
}
|
||||
// code->en/zh三元组
|
||||
type row struct{ code, en, zh string }
|
||||
var rows []row
|
||||
for code, enName := range enMap {
|
||||
if zhName, ok := zhMap[code]; ok {
|
||||
rows = append(rows, row{code, enName, zhName})
|
||||
}
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
g.Log().Warningf(ctx, "未生成任何en->zh映射: %s", t.url)
|
||||
continue
|
||||
}
|
||||
|
||||
// 读取数据库现有映射,优先用code查找
|
||||
var dbMappings []*entity.EpicI18NMappings
|
||||
err = dao.EpicI18NMappings.Ctx(ctx).
|
||||
Where(dao.EpicI18NMappings.Columns().Language, "zh").
|
||||
Where(dao.EpicI18NMappings.Columns().Category, t.category).
|
||||
Where(dao.EpicI18NMappings.Columns().Status, 1).
|
||||
Where(dao.EpicI18NMappings.Columns().Deleted, 0).
|
||||
Scan(&dbMappings)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "查询i18n数据库失败: %v", err)
|
||||
continue
|
||||
}
|
||||
dbMapByCode := make(map[string]*entity.EpicI18NMappings)
|
||||
dbMapByKey := make(map[string]*entity.EpicI18NMappings)
|
||||
for _, m := range dbMappings {
|
||||
if m.Code != "" {
|
||||
dbMapByCode[m.Code] = m
|
||||
}
|
||||
dbMapByKey[m.KeyName] = m
|
||||
}
|
||||
|
||||
var toInsert []g.Map
|
||||
var toUpdate []struct{ code, en, zh string }
|
||||
for _, r := range rows {
|
||||
var exist *entity.EpicI18NMappings
|
||||
if r.code != "" {
|
||||
exist = dbMapByCode[r.code]
|
||||
}
|
||||
if exist == nil {
|
||||
exist = dbMapByKey[r.en]
|
||||
}
|
||||
if exist == nil {
|
||||
toInsert = append(toInsert, g.Map{
|
||||
dao.EpicI18NMappings.Columns().KeyName: r.en,
|
||||
dao.EpicI18NMappings.Columns().Language: "zh",
|
||||
dao.EpicI18NMappings.Columns().Value: r.zh,
|
||||
dao.EpicI18NMappings.Columns().Category: t.category,
|
||||
dao.EpicI18NMappings.Columns().Code: r.code,
|
||||
dao.EpicI18NMappings.Columns().Status: 1,
|
||||
dao.EpicI18NMappings.Columns().CreateTime: gtime.Now(),
|
||||
dao.EpicI18NMappings.Columns().UpdateTime: gtime.Now(),
|
||||
})
|
||||
} else if exist.Value != r.zh {
|
||||
toUpdate = append(toUpdate, struct{ code, en, zh string }{r.code, r.en, r.zh})
|
||||
}
|
||||
}
|
||||
|
||||
if len(toInsert) > 0 {
|
||||
_, err := dao.EpicI18NMappings.Ctx(ctx).Data(toInsert).Insert()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "批量导入i18n数据失败: %s, err: %v", t.url, err)
|
||||
} else {
|
||||
g.Log().Infof(ctx, "远程i18n新增导入成功: %s, 共%d条", t.url, len(toInsert))
|
||||
// 更新缓存
|
||||
l.mutex.Lock()
|
||||
if _, exists := l.cache["zh"]; !exists {
|
||||
l.cache["zh"] = struct {
|
||||
ByCode map[string]string
|
||||
ByKey map[string]string
|
||||
}{
|
||||
ByCode: make(map[string]string),
|
||||
ByKey: make(map[string]string),
|
||||
}
|
||||
}
|
||||
for _, item := range toInsert {
|
||||
if keyName, ok := item[dao.EpicI18NMappings.Columns().KeyName].(string); ok {
|
||||
if value, ok := item[dao.EpicI18NMappings.Columns().Value].(string); ok {
|
||||
l.cache["zh"].ByKey[keyName] = value
|
||||
if code, ok := item[dao.EpicI18NMappings.Columns().Code].(string); ok && code != "" {
|
||||
l.cache["zh"].ByCode[code] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
l.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
if len(toUpdate) > 0 {
|
||||
for _, r := range toUpdate {
|
||||
if err := l.Update(ctx, r.en, "zh", r.zh); err != nil {
|
||||
g.Log().Errorf(ctx, "更新i18n数据失败: %s %s -> %s, err: %v", t.category, r.en, r.zh, err)
|
||||
}
|
||||
}
|
||||
g.Log().Infof(ctx, "远程i18n更新成功: %s, 共%d条", t.url, len(toUpdate))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,22 +11,24 @@ import (
|
||||
|
||||
// EpicHeroInfo is the golang structure of table epic_hero_info for DAO operations like Where/Data.
|
||||
type EpicHeroInfo struct {
|
||||
g.Meta `orm:"table:epic_hero_info, do:true"`
|
||||
Id interface{} // 文件编号
|
||||
HeroName interface{} // 配置编号
|
||||
HeroCode interface{} // 文件名
|
||||
HeroAttrLv60 interface{} // 文件路径
|
||||
Creator interface{} // 创建者
|
||||
CreateTime *gtime.Time // 创建时间
|
||||
Updater interface{} // 更新者
|
||||
UpdateTime *gtime.Time // 更新时间
|
||||
Deleted interface{} // 是否删除
|
||||
NickName interface{} // 配置编号
|
||||
Rarity interface{} // 配置编号
|
||||
Role interface{} // 配置编号
|
||||
Zodiac interface{} // 配置编号
|
||||
HeadImgUrl interface{} // 配置编号
|
||||
Attribute interface{} // 配置编号
|
||||
Remark interface{} // 配置编号
|
||||
RawJson interface{} // 原始json
|
||||
g.Meta `orm:"table:epic_hero_info, do:true"`
|
||||
Id interface{} // 文件编号
|
||||
HeroName interface{} // 配置编号
|
||||
HeroCode interface{} // 文件名
|
||||
HeroAttrLv60 interface{} // 文件路径
|
||||
Creator interface{} // 创建者
|
||||
CreateTime *gtime.Time // 创建时间
|
||||
Updater interface{} // 更新者
|
||||
UpdateTime *gtime.Time // 更新时间
|
||||
Deleted interface{} // 是否删除
|
||||
NickName interface{} // 配置编号
|
||||
Rarity interface{} // 配置编号
|
||||
Role interface{} // 配置编号
|
||||
Zodiac interface{} // 配置编号
|
||||
HeadImgUrl interface{} // 配置编号
|
||||
Attribute interface{} // 配置编号
|
||||
Remark interface{} // 配置编号
|
||||
RawJson interface{} // 原始json
|
||||
SetContentJson interface{} // 配装json数据
|
||||
SetUpdateTime *gtime.Time // 配装更新时间
|
||||
}
|
||||
|
||||
@@ -23,4 +23,5 @@ type EpicI18NMappings struct {
|
||||
Updater interface{} // 更新者
|
||||
UpdateTime *gtime.Time // 更新时间
|
||||
Deleted interface{} // 是否删除
|
||||
Code interface{} // 编码
|
||||
}
|
||||
|
||||
@@ -10,21 +10,23 @@ import (
|
||||
|
||||
// EpicHeroInfo is the golang structure for table epic_hero_info.
|
||||
type EpicHeroInfo struct {
|
||||
Id int64 `json:"id" orm:"id" description:"文件编号"` // 文件编号
|
||||
HeroName string `json:"heroName" orm:"hero_name" description:"配置编号"` // 配置编号
|
||||
HeroCode string `json:"heroCode" orm:"hero_code" description:"文件名"` // 文件名
|
||||
HeroAttrLv60 string `json:"heroAttrLv60" orm:"hero_attr_lv60" description:"文件路径"` // 文件路径
|
||||
Creator string `json:"creator" orm:"creator" description:"创建者"` // 创建者
|
||||
CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间
|
||||
Updater string `json:"updater" orm:"updater" description:"更新者"` // 更新者
|
||||
UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间
|
||||
Deleted bool `json:"deleted" orm:"deleted" description:"是否删除"` // 是否删除
|
||||
NickName string `json:"nickName" orm:"nick_name" description:"配置编号"` // 配置编号
|
||||
Rarity string `json:"rarity" orm:"rarity" description:"配置编号"` // 配置编号
|
||||
Role string `json:"role" orm:"role" description:"配置编号"` // 配置编号
|
||||
Zodiac string `json:"zodiac" orm:"zodiac" description:"配置编号"` // 配置编号
|
||||
HeadImgUrl string `json:"headImgUrl" orm:"head_img_url" description:"配置编号"` // 配置编号
|
||||
Attribute string `json:"attribute" orm:"attribute" description:"配置编号"` // 配置编号
|
||||
Remark string `json:"remark" orm:"remark" description:"配置编号"` // 配置编号
|
||||
RawJson string `json:"rawJson" orm:"raw_json" description:"原始json"` // 原始json
|
||||
Id int64 `json:"id" orm:"id" description:"文件编号"` // 文件编号
|
||||
HeroName string `json:"heroName" orm:"hero_name" description:"配置编号"` // 配置编号
|
||||
HeroCode string `json:"heroCode" orm:"hero_code" description:"文件名"` // 文件名
|
||||
HeroAttrLv60 string `json:"heroAttrLv60" orm:"hero_attr_lv60" description:"文件路径"` // 文件路径
|
||||
Creator string `json:"creator" orm:"creator" description:"创建者"` // 创建者
|
||||
CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间
|
||||
Updater string `json:"updater" orm:"updater" description:"更新者"` // 更新者
|
||||
UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间
|
||||
Deleted bool `json:"deleted" orm:"deleted" description:"是否删除"` // 是否删除
|
||||
NickName string `json:"nickName" orm:"nick_name" description:"配置编号"` // 配置编号
|
||||
Rarity string `json:"rarity" orm:"rarity" description:"配置编号"` // 配置编号
|
||||
Role string `json:"role" orm:"role" description:"配置编号"` // 配置编号
|
||||
Zodiac string `json:"zodiac" orm:"zodiac" description:"配置编号"` // 配置编号
|
||||
HeadImgUrl string `json:"headImgUrl" orm:"head_img_url" description:"配置编号"` // 配置编号
|
||||
Attribute string `json:"attribute" orm:"attribute" description:"配置编号"` // 配置编号
|
||||
Remark string `json:"remark" orm:"remark" description:"配置编号"` // 配置编号
|
||||
RawJson string `json:"rawJson" orm:"raw_json" description:"原始json"` // 原始json
|
||||
SetContentJson string `json:"setContentJson" orm:"set_content_json" description:"配装json数据"` // 配装json数据
|
||||
SetUpdateTime *gtime.Time `json:"setUpdateTime" orm:"set_update_time" description:"配装更新时间"` // 配装更新时间
|
||||
}
|
||||
|
||||
@@ -21,4 +21,5 @@ type EpicI18NMappings struct {
|
||||
Updater string `json:"updater" orm:"updater" description:"更新者"` // 更新者
|
||||
UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间
|
||||
Deleted int `json:"deleted" orm:"deleted" description:"是否删除"` // 是否删除
|
||||
Code string `json:"code" orm:"code" description:"编码"` // 编码
|
||||
}
|
||||
|
||||
@@ -17,12 +17,37 @@ func Info(ctx context.Context, args ...interface{}) {
|
||||
g.Log().Info(ctx, args...)
|
||||
}
|
||||
|
||||
// Infof 全局Info格式化日志
|
||||
func Infof(ctx context.Context, format string, args ...interface{}) {
|
||||
g.Log().Infof(ctx, format, args...)
|
||||
}
|
||||
|
||||
// Debug 全局Debug日志
|
||||
func Debug(ctx context.Context, args ...interface{}) {
|
||||
g.Log().Debug(ctx, args...)
|
||||
}
|
||||
|
||||
// Debugf 全局Debug格式化日志
|
||||
func Debugf(ctx context.Context, format string, args ...interface{}) {
|
||||
g.Log().Debugf(ctx, format, args...)
|
||||
}
|
||||
|
||||
// Error 全局Error日志
|
||||
func Error(ctx context.Context, args ...interface{}) {
|
||||
g.Log().Error(ctx, args...)
|
||||
}
|
||||
|
||||
// Errorf 全局Error格式化日志
|
||||
func Errorf(ctx context.Context, format string, args ...interface{}) {
|
||||
g.Log().Errorf(ctx, format, args...)
|
||||
}
|
||||
|
||||
// Warn 全局Warn日志
|
||||
func Warn(ctx context.Context, args ...interface{}) {
|
||||
g.Log().Warning(ctx, args...)
|
||||
}
|
||||
|
||||
// Warnf 全局Warn格式化日志
|
||||
func Warnf(ctx context.Context, format string, args ...interface{}) {
|
||||
g.Log().Warningf(ctx, format, args...)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
@@ -16,22 +17,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 +89,71 @@ 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
|
||||
// 自动生成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
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ logger:
|
||||
database:
|
||||
default:
|
||||
link: "mysql:root:hu123456@tcp(193.112.151.199:3306)/wow"
|
||||
debug: true
|
||||
debug: false
|
||||
|
||||
# https://goframe.org/docs/core/gredis-config-file
|
||||
redis:
|
||||
default:
|
||||
address: "193.112.151.199:6379"
|
||||
db: 1
|
||||
db: 0
|
||||
pass: "hu123"
|
||||
Reference in New Issue
Block a user