Compare commits

..

10 Commits

Author SHA1 Message Date
hu xiaotong
1dd95f787a refactor(internal): 优化 OSS 预签名 URL 缓存刷新任务和英雄数据缓存逻辑
- 注释掉 OSS预签名 URL 缓存刷新任务的定时执行代码
- 在 hero/hero.go 中增加对 Redis缓存和英雄数据集的非空校验
- 修改 OSS预签名 URL 生成逻辑,自动替换为 CDN 域名
2025-10-27 17:16:35 +08:00
hxt
707b3cb347 feat(cron): 添加角色配装信息刷新任务并优化神器数据同步功能
- 新增每5天执行一次的角色配装信息刷新任务
- 重构神器数据同步功能,优化数据处理和保存逻辑- 添加神器图片URL获取和上传逻辑
- 更新相关测试用例
2025-07-26 16:22:12 +08:00
hu xiaotong
fc41c5ca73 refactor(internal): 优化 OSS 预签名 URL 缓存刷新任务和英雄数据缓存逻辑
- 注释掉 OSS预签名 URL 缓存刷新任务的定时执行代码
- 在 hero/hero.go 中增加对 Redis缓存和英雄数据集的非空校验
- 修改 OSS预签名 URL 生成逻辑,自动替换为 CDN 域名
2025-07-25 17:08:39 +08:00
hu xiaotong
ce0fa7f2ed refactor(internal): 优化 OSS 预签名 URL 缓存刷新任务和英雄数据缓存逻辑
- 注释掉 OSS预签名 URL 缓存刷新任务的定时执行代码
- 在 hero/hero.go 中增加对 Redis缓存和英雄数据集的非空校验
- 修改 OSS预签名 URL 生成逻辑,自动替换为 CDN 域名
2025-07-18 16:56:36 +08:00
hxt
f8001aef5b feat(cron): 添加角色配装信息刷新任务并优化神器数据同步功能
- 新增每5天执行一次的角色配装信息刷新任务
- 重构神器数据同步功能,优化数据处理和保存逻辑- 添加神器图片URL获取和上传逻辑
- 更新相关测试用例
2025-07-17 22:05:27 +08:00
hxt
c36a2cb8b0 ci(drone): 添加 Go 模块和构建缓存
- 在 restore cache 和 rebuild cache 步骤中添加了 go-mod-cache 和 go
2025-07-17 20:26:11 +08:00
hxt
0ad79c4f27 ci(drone): 添加 Go 模块和构建缓存
- 在 restore cache 和 rebuild cache 步骤中添加了 go-mod-cache 和 go
2025-07-17 20:25:50 +08:00
hu xiaotong
9ef6ac9cdb refactor(internal): 优化 OSS 预签名 URL 缓存刷新任务和英雄数据缓存逻辑
- 注释掉 OSS预签名 URL 缓存刷新任务的定时执行代码
- 在 hero/hero.go 中增加对 Redis缓存和英雄数据集的非空校验
- 修改 OSS预签名 URL 生成逻辑,自动替换为 CDN 域名
2025-07-17 17:37:13 +08:00
hu xiaotong
8ab1379cae refactor(internal): 优化 OSS 预签名 URL 缓存刷新任务和英雄数据缓存逻辑
- 注释掉 OSS预签名 URL 缓存刷新任务的定时执行代码
- 在 hero/hero.go 中增加对 Redis缓存和英雄数据集的非空校验
- 修改 OSS预签名 URL 生成逻辑,自动替换为 CDN 域名
2025-07-17 16:11:36 +08:00
hu xiaotong
7b7f8c31d7 i18n翻译 2025-07-17 15:36:24 +08:00
17 changed files with 1175 additions and 263 deletions

View File

@@ -52,22 +52,13 @@ redis:
### 启动服务 ### 启动服务
#### Linux/Mac
```bash
chmod +x scripts/start.sh
./scripts/start.sh
```
#### Windows #### Windows
```cmd
scripts\start.bat
```
#### 手动启动
```bash ```bash
go run main.go go run main.go
``` ```
### 访问服务 ### 访问服务
- **API服务**: http://localhost:8283 - **API服务**: http://localhost:8283

View File

@@ -7,12 +7,13 @@ import (
"epic/internal/logic/i18n" "epic/internal/logic/i18n"
"epic/internal/service" "epic/internal/service"
"epic/internal/util" "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"
"os/signal" "os/signal"
"syscall" "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) { func CORS(r *ghttp.Request) {
@@ -31,7 +32,7 @@ var (
// 启动定时任务 // 启动定时任务
if err := service.Cron().StartAllJobs(ctx); err != nil { 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 return err
} }
@@ -66,7 +67,7 @@ func setupGracefulShutdown(ctx context.Context) {
// 停止定时任务 // 停止定时任务
if err := service.Cron().StopAllJobs(ctx); err != nil { 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)
} }
// 退出程序 // 退出程序

View File

@@ -2,8 +2,8 @@ package consts
const ( const (
// 笑门官网查询英雄名称和神器名称,用作中文翻译 // 笑门官网查询英雄名称和神器名称,用作中文翻译
SimileHeroName = "https://static.smilegatemegaport.com/gameRecord/epic7/epic7_hero.json?_=1729322698936" SimileHeroName = "https://static.smilegatemegaport.com/gameRecord/epic7/epic7_hero.json"
SimileArtifactName = "https://static.smilegatemegaport.com/gameRecord/epic7/epic7_artifact.json?_=1729322698936" SimileArtifactName = "https://static.smilegatemegaport.com/gameRecord/epic7/epic7_artifact.json"
// 获取角色信息 // 获取角色信息
HeroListURL = "https://e7-optimizer-game-data.s3-accelerate.amazonaws.com/herodata.json" HeroListURL = "https://e7-optimizer-game-data.s3-accelerate.amazonaws.com/herodata.json"
@@ -18,10 +18,14 @@ const (
// 角色图片基础 URL // 角色图片基础 URL
GfHeroPngURL = "https://static.smilegatemegaport.com/event/live/epic7/guide/images/hero/" GfHeroPngURL = "https://static.smilegatemegaport.com/event/live/epic7/guide/images/hero/"
EPIC_DB_URL = "https://epic7db.com/"
// S3/OSS 配置 // S3/OSS 配置
S3AccessKey = "s5iWm6wXVvhCNN9nJlXwgWRf" S3AccessKey = "s5iWm6wXVvhCNN9nJlXwgWRf"
S3SecretKey = "91sTurpFtugXijPg0uSof3JcJma0HED" S3SecretKey = "91sTurpFtugXijPg0uSof3JcJma0HED"
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"
) )

View File

@@ -21,44 +21,48 @@ type EpicHeroInfoDao struct {
// EpicHeroInfoColumns defines and stores column names for the table epic_hero_info. // EpicHeroInfoColumns defines and stores column names for the table epic_hero_info.
type EpicHeroInfoColumns struct { type EpicHeroInfoColumns struct {
Id string // 文件编号 Id string // 文件编号
HeroName string // 配置编号 HeroName string // 配置编号
HeroCode string // 文件名 HeroCode string // 文件名
HeroAttrLv60 string // 文件路径 HeroAttrLv60 string // 文件路径
Creator string // 创建者 Creator string // 创建者
CreateTime string // 创建时间 CreateTime string // 创建时间
Updater string // 更新者 Updater string // 更新者
UpdateTime string // 更新时间 UpdateTime string // 更新时间
Deleted string // 是否删除 Deleted string // 是否删除
NickName string // 配置编号 NickName string // 配置编号
Rarity string // 配置编号 Rarity string // 配置编号
Role string // 配置编号 Role string // 配置编号
Zodiac string // 配置编号 Zodiac string // 配置编号
HeadImgUrl string // 配置编号 HeadImgUrl string // 配置编号
Attribute string // 配置编号 Attribute string // 配置编号
Remark string // 配置编号 Remark string // 配置编号
RawJson string // 原始json RawJson string // 原始json
SetContentJson string // 配装json数据
SetUpdateTime string // 配装更新时间
} }
// epicHeroInfoColumns holds the columns for the table epic_hero_info. // epicHeroInfoColumns holds the columns for the table epic_hero_info.
var epicHeroInfoColumns = EpicHeroInfoColumns{ var epicHeroInfoColumns = EpicHeroInfoColumns{
Id: "id", Id: "id",
HeroName: "hero_name", HeroName: "hero_name",
HeroCode: "hero_code", HeroCode: "hero_code",
HeroAttrLv60: "hero_attr_lv60", HeroAttrLv60: "hero_attr_lv60",
Creator: "creator", Creator: "creator",
CreateTime: "create_time", CreateTime: "create_time",
Updater: "updater", Updater: "updater",
UpdateTime: "update_time", UpdateTime: "update_time",
Deleted: "deleted", Deleted: "deleted",
NickName: "nick_name", NickName: "nick_name",
Rarity: "rarity", Rarity: "rarity",
Role: "role", Role: "role",
Zodiac: "zodiac", Zodiac: "zodiac",
HeadImgUrl: "head_img_url", HeadImgUrl: "head_img_url",
Attribute: "attribute", Attribute: "attribute",
Remark: "remark", Remark: "remark",
RawJson: "raw_json", RawJson: "raw_json",
SetContentJson: "set_content_json",
SetUpdateTime: "set_update_time",
} }
// NewEpicHeroInfoDao creates and returns a new DAO object for table data access. // NewEpicHeroInfoDao creates and returns a new DAO object for table data access.

View File

@@ -32,6 +32,7 @@ type EpicI18NMappingsColumns struct {
Updater string // 更新者 Updater string // 更新者
UpdateTime string // 更新时间 UpdateTime string // 更新时间
Deleted string // 是否删除 Deleted string // 是否删除
Code string // 编码
} }
// epicI18NMappingsColumns holds the columns for the table epic_i18n_mappings. // epicI18NMappingsColumns holds the columns for the table epic_i18n_mappings.
@@ -47,6 +48,7 @@ var epicI18NMappingsColumns = EpicI18NMappingsColumns{
Updater: "updater", Updater: "updater",
UpdateTime: "update_time", UpdateTime: "update_time",
Deleted: "deleted", Deleted: "deleted",
Code: "code",
} }
// NewEpicI18NMappingsDao creates and returns a new DAO object for table data access. // NewEpicI18NMappingsDao creates and returns a new DAO object for table data access.

View File

@@ -2,12 +2,17 @@ package cron
import ( import (
"context" "context"
"epic/internal/dao"
"epic/internal/logic/i18n"
"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/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 {
@@ -31,7 +36,7 @@ func init() {
// StartAllJobs 启动所有定时任务 // StartAllJobs 启动所有定时任务
func (l *Logic) StartAllJobs(ctx context.Context) error { 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() l.cron.Start()
@@ -41,13 +46,13 @@ func (l *Logic) StartAllJobs(ctx context.Context) error {
return err return err
} }
g.Log().Info(ctx, "All cron jobs started successfully") util.Info(ctx, "All cron jobs started successfully")
return nil return nil
} }
// StopAllJobs 停止所有定时任务 // StopAllJobs 停止所有定时任务
func (l *Logic) StopAllJobs(ctx context.Context) error { 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() l.jobsMux.Lock()
defer l.jobsMux.Unlock() defer l.jobsMux.Unlock()
@@ -56,13 +61,13 @@ func (l *Logic) StopAllJobs(ctx context.Context) error {
for name, entry := range l.jobs { for name, entry := range l.jobs {
l.cron.Remove(entry.Name) l.cron.Remove(entry.Name)
delete(l.jobs, name) delete(l.jobs, name)
g.Log().Infof(ctx, "Stopped job: %s", name) util.Infof(ctx, "Stopped job: %s", name)
} }
// 停止调度器 // 停止调度器
l.cron.Stop() l.cron.Stop()
g.Log().Info(ctx, "All cron jobs stopped successfully") util.Info(ctx, "All cron jobs stopped successfully")
return nil 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) { entry, err := l.cron.Add(ctx, cron, func(ctx context.Context) {
startTime := gtime.Now() 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() job()
endTime := gtime.Now() endTime := gtime.Now()
duration := endTime.Sub(startTime) 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) }, name)
if err != nil { if err != nil {
@@ -92,7 +97,7 @@ func (l *Logic) AddJob(ctx context.Context, name, cron string, job func()) error
// 保存任务引用 // 保存任务引用
l.jobs[name] = entry 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 return nil
} }
@@ -111,7 +116,7 @@ func (l *Logic) RemoveJob(ctx context.Context, name string) error {
l.cron.Remove(entry.Name) l.cron.Remove(entry.Name)
delete(l.jobs, name) delete(l.jobs, name)
g.Log().Infof(ctx, "Removed job: %s", name) util.Infof(ctx, "Removed job: %s", name)
return nil return nil
} }
@@ -126,19 +131,19 @@ func (l *Logic) GetJobStatus(ctx context.Context, name string) (bool, error) {
// registerDefaultJobs 注册默认的定时任务 // registerDefaultJobs 注册默认的定时任务
func (l *Logic) registerDefaultJobs(ctx context.Context) error { func (l *Logic) registerDefaultJobs(ctx context.Context) error {
// 每小时执行一次数据同步任务 //// 每小时执行一次数据同步任务
if err := l.AddJob(ctx, "data_sync_hourly", "0 0 * * * *", func() { //if err := l.AddJob(ctx, "data_sync_hourly", "0 0 * * * *", func() {
l.syncDataFromThirdParty(ctx) // l.syncDataFromThirdParty(ctx)
}); err != nil { //}); err != nil {
return err // return err
} //}
//
// 每天凌晨2点执行数据清理任务 //// 每天凌晨2点执行数据清理任务
if err := l.AddJob(ctx, "data_cleanup_daily", "0 0 2 * * *", func() { //if err := l.AddJob(ctx, "data_cleanup_daily", "0 0 2 * * *", func() {
l.cleanupOldData(ctx) // l.cleanupOldData(ctx)
}); err != nil { //}); err != nil {
return err // return err
} //}
// 每5分钟执行一次健康检查任务 // 每5分钟执行一次健康检查任务
if err := l.AddJob(ctx, "health_check", "0 0/5 * * * *", func() { 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 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 return nil
} }
// syncDataFromThirdParty 从第三方网站同步数据 // syncDataFromThirdParty 从第三方网站同步数据
func (l *Logic) syncDataFromThirdParty(ctx context.Context) { 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 { if err := l.sync.SyncAllData(ctx); err != nil {
g.Log().Error(ctx, "Data sync failed:", err) util.Error(ctx, "Data sync failed:", err)
return return
} }
g.Log().Info(ctx, "Data sync completed") util.Info(ctx, "Data sync completed")
} }
// syncHeroData 同步英雄数据 // 同步英雄数据
func (l *Logic) syncHeroData(ctx context.Context) { 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 { 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 return
} }
g.Log().Info(ctx, "Hero data sync completed") util.Info(ctx, "Hero data sync completed")
} }
// syncArtifactData 同步神器数据 // 同步神器数据
func (l *Logic) syncArtifactData(ctx context.Context) { 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 { 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 return
} }
g.Log().Info(ctx, "Artifact data sync completed") util.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")
} }
// healthCheck 健康检查 // healthCheck 健康检查
func (l *Logic) healthCheck(ctx context.Context) { func (l *Logic) healthCheck(ctx context.Context) {
g.Log().Debug(ctx, "Performing health check...") util.Debug(ctx, "Performing health check...")
// TODO: 实现健康检查逻辑 // TODO: 实现健康检查逻辑
// 1. 检查数据库连接 // 1. 检查数据库连接
@@ -230,17 +242,67 @@ func (l *Logic) healthCheck(ctx context.Context) {
// 3. 检查第三方API可用性 // 3. 检查第三方API可用性
// 4. 记录系统状态 // 4. 记录系统状态
g.Log().Debug(ctx, "Health check completed") util.Debug(ctx, "Health check completed")
} }
// refreshCache 刷新缓存 // refreshCache 刷新缓存
func (l *Logic) refreshCache(ctx context.Context) { func (l *Logic) refreshCache(ctx context.Context) {
g.Log().Info(ctx, "Starting cache refresh...") util.Info(ctx, "Starting cache refresh...")
// TODO: 实现缓存刷新逻辑 // TODO: 实现缓存刷新逻辑
// 1. 刷新英雄数据缓存 // 1. 刷新英雄数据缓存
// 2. 刷新神器数据缓存 // 2. 刷新神器数据缓存
// 3. 刷新其他业务缓存 // 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")
} }

View File

@@ -15,6 +15,7 @@ import (
"github.com/gogf/gf/v2/net/gclient" "github.com/gogf/gf/v2/net/gclient"
"github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/os/gtime"
"strconv" "strconv"
"strings"
"time" "time"
) )
@@ -31,45 +32,43 @@ func NewThirdPartyDataSync() *ThirdPartyDataSync {
} }
func (t *ThirdPartyDataSync) SyncHeroData(ctx context.Context) error { func (t *ThirdPartyDataSync) SyncHeroData(ctx context.Context) error {
g.Log().Info(ctx, "开始同步英雄数据...") util.Info(ctx, "开始同步英雄数据...")
// 示例从第三方API获取英雄数据 // 示例从第三方API获取英雄数据
heroData, err := t.fetchHeroDataFromAPI(ctx) heroData, err := t.fetchHeroDataFromAPI(ctx)
if err != nil || heroData == nil { if err != nil || heroData == nil {
g.Log().Error(ctx, "获取英雄数据失败:", err) util.Error(ctx, "获取英雄数据失败:", err)
return err return err
} }
// 处理并保存数据 // 处理并保存数据
if err := t.processAndSaveHeroData(ctx, heroData); err != nil { if err := t.processAndSaveHeroData(ctx, heroData); err != nil {
g.Log().Error(ctx, "处理英雄数据失败:", err) util.Error(ctx, "处理英雄数据失败:", err)
return err return err
} }
g.Log().Info(ctx, "英雄数据同步完成") util.Info(ctx, "英雄数据同步完成")
return nil return nil
} }
// SyncArtifactData 同步神器数据 // 同步神器数据
func (t *ThirdPartyDataSync) SyncArtifactData(ctx context.Context) error { 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 := t.processAndSaveArtifactData(ctx, artifactData); err != nil {
//if err != nil { util.Error(ctx, "处理神器数据失败:", err)
// g.Log().Error(ctx, "获取神器数据失败:", err) return err
// return err }
//}
//
//// 处理并保存数据
//if err := t.processAndSaveArtifactData(ctx, artifactData); err != nil {
// g.Log().Error(ctx, "处理神器数据失败:", err)
// return err
//}
g.Log().Info(ctx, "神器数据同步完成") util.Info(ctx, "神器数据同步完成")
return nil return nil
} }
@@ -103,13 +102,40 @@ func (t *ThirdPartyDataSync) fetchHeroDataFromAPI(ctx context.Context) ([]byte,
return content, nil 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获取神器数据 // 从API获取神器数据
func (t *ThirdPartyDataSync) fetchArtifactDataFromAPI(ctx context.Context) (string, error) { func (t *ThirdPartyDataSync) fetchArtifactDataFromAPI(ctx context.Context) (string, error) {
// 示例API地址 // 示例API地址
apiURL := "https://static.smilegatemegaport.com/gameRecord/epic7/epic7_artifact.json?_=1729322698936" apiURL := consts.ArtifactDataURL
headers := map[string]string{ headers := map[string]string{
//"User-Agent": "EpicGameBot/1.0",
"Accept": "application/json", "Accept": "application/json",
} }
@@ -143,11 +169,16 @@ func (t *ThirdPartyDataSync) processAndSaveHeroData(ctx context.Context, data []
return fmt.Errorf("解析英雄数据到DTO失败: %v", err) return fmt.Errorf("解析英雄数据到DTO失败: %v", err)
} }
g.Log().Info(ctx, "解析到", len(heroes), "个英雄数据") util.Info(ctx, "解析到", len(heroes), "个英雄数据")
// 一次性查出所有数据库英雄构建map // 一次性查出所有数据库英雄构建map
var dbHeroes []*entity.EpicHeroInfo 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 { if err != nil {
return fmt.Errorf("查询数据库英雄失败: %v", err) return fmt.Errorf("查询数据库英雄失败: %v", err)
} }
@@ -168,7 +199,7 @@ func (t *ThirdPartyDataSync) processAndSaveHeroData(ctx context.Context, data []
// 只更新 rawJson 字段 // 只更新 rawJson 字段
rawJsonBytes, err := json.Marshal(hero) rawJsonBytes, err := json.Marshal(hero)
if err != nil { if err != nil {
g.Log().Error(ctx, "序列化英雄数据失败:", err) util.Error(ctx, "序列化英雄数据失败:", err)
continue continue
} }
rawJson := string(rawJsonBytes) rawJson := string(rawJsonBytes)
@@ -180,7 +211,7 @@ func (t *ThirdPartyDataSync) processAndSaveHeroData(ctx context.Context, data []
}). }).
Update() Update()
if err != nil { if err != nil {
g.Log().Error(ctx, "更新英雄rawJson失败:", err) util.Error(ctx, "更新英雄rawJson失败:", err)
continue continue
} }
g.Log().Debug(ctx, "更新英雄rawJson:", hero.Code) 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) //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 {
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{ newHero := &entity.EpicHeroInfo{
Id: 0, Id: 0,
HeroName: zhHeroName, HeroName: zhHeroName,
@@ -209,7 +259,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,
@@ -217,38 +267,128 @@ func (t *ThirdPartyDataSync) processAndSaveHeroData(ctx context.Context, data []
_, err = dao.EpicHeroInfo.Ctx(ctx).Data(newHero).Insert() _, err = dao.EpicHeroInfo.Ctx(ctx).Data(newHero).Insert()
if err != nil { if err != nil {
g.Log().Error(ctx, "插入新英雄失败:", err) util.Error(ctx, "插入新英雄失败:", err)
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 {
util.Error(ctx, "刷新新英雄图片预签名URL失败:", key, err)
}
}
} }
return nil return nil
} }
// processAndSaveArtifactData 处理并保存神器数据 // 处理并保存神器数据
func (t *ThirdPartyDataSync) processAndSaveArtifactData(ctx context.Context, data string) error { func (t *ThirdPartyDataSync) processAndSaveArtifactData(ctx context.Context, data string) error {
// 使用 gjson 解析 // 1. 解析json为map
j := gjson.New(data) var artifactMap map[string]struct {
zhcn := j.Get("zh-CN") Name string `json:"name"`
// 检查json对象本身和其内部值并使用 .Var().IsSlice() 这种更可靠的方式判断是否为数组 Rarity int `json:"rarity"`
if !zhcn.IsSlice() { Role string `json:"role"`
return fmt.Errorf("神器数据格式错误期望是一个JSON数组") 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 for _, art := range artifactMap {
if err := zhcn.Scan(&artifacts); err != nil { var dbArt *entity.EpicArtifactInfo
return fmt.Errorf("解析神器数据到DTO失败: %v", err) if v, ok := artifactDbMap[art.Name]; ok {
} dbArt = v
}
// 批量处理数据 // 如果数据库中已有自定义CDN图片跳过处理
for _, artifact := range artifacts { if dbArt != nil && dbArt.ImageUrl != "" && strings.HasPrefix(dbArt.ImageUrl, consts.S3CustomDomain) {
if err := t.saveArtifactData(ctx, artifact); err != nil { //g.Log().Debug(ctx, "跳过已有CDN图片的神器:", art.Name)
g.Log().Error(ctx, "保存神器数据失败:", err)
continue 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 return nil
} }
@@ -257,6 +397,10 @@ func (t *ThirdPartyDataSync) saveHeroData(ctx context.Context, hero *dto.ThirdPa
// 查询是否存在 // 查询是否存在
var dbHero *entity.EpicHeroInfo var dbHero *entity.EpicHeroInfo
err := dao.EpicHeroInfo.Ctx(ctx). err := dao.EpicHeroInfo.Ctx(ctx).
Fields(
dao.EpicHeroInfo.Columns().HeroCode,
dao.EpicHeroInfo.Columns().RawJson,
).
Where(dao.EpicHeroInfo.Columns().HeroCode, hero.Code). Where(dao.EpicHeroInfo.Columns().HeroCode, hero.Code).
Scan(&dbHero) Scan(&dbHero)
if err != nil { if err != nil {
@@ -346,22 +490,333 @@ func (t *ThirdPartyDataSync) saveArtifactData(ctx context.Context, artifact *dto
// SyncAllData 同步所有数据 // SyncAllData 同步所有数据
func (t *ThirdPartyDataSync) SyncAllData(ctx context.Context) error { func (t *ThirdPartyDataSync) SyncAllData(ctx context.Context) error {
g.Log().Info(ctx, "开始同步所有第三方数据...") util.Info(ctx, "开始同步所有第三方数据...")
// 同步英雄数据 // 同步英雄数据
if err := t.SyncHeroData(ctx); err != nil { if err := t.SyncHeroData(ctx); err != nil {
g.Log().Error(ctx, "英雄数据同步失败:", err) util.Error(ctx, "英雄数据同步失败:", err)
// 继续同步其他数据 // 继续同步其他数据
} }
// 同步神器数据 // 同步神器数据
if err := t.SyncArtifactData(ctx); err != nil { 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 return nil
} }

View File

@@ -23,10 +23,20 @@ func TestMain(m *testing.M) {
os.Exit(code) os.Exit(code)
} }
/**
* 测试同步英雄数据
*/
func TestI18n(t *testing.T) {
_ = i18n.RefreshI18n(context.Background())
fmt.Println(i18n.Zh2En("湖畔魔女泰妮布里雅"))
}
/** /**
* 测试同步英雄数据 * 测试同步英雄数据
*/ */
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())
@@ -39,8 +49,8 @@ func TestSyncHeroData(t *testing.T) {
* 测试同步神器数据 * 测试同步神器数据
*/ */
func TestSyncArtifactData_Success(t *testing.T) { func TestSyncArtifactData_Success(t *testing.T) {
_ = i18n.RefreshI18n(context.Background())
sync := NewThirdPartyDataSync() sync := NewThirdPartyDataSync()
err := sync.SyncArtifactData(context.Background()) err := sync.SyncArtifactData(context.Background())
if err != nil { if err != nil {
t.Errorf("expected success, but got error: %v", err) 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)) 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("修复英雄/神器中文名成功")
}

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
v1 "epic/api/hero/v1" v1 "epic/api/hero/v1"
"epic/internal/dao" "epic/internal/dao"
"epic/internal/logic/cron"
"epic/internal/model/dto" "epic/internal/model/dto"
"epic/internal/model/entity" "epic/internal/model/entity"
"epic/internal/service" "epic/internal/service"
@@ -34,6 +35,19 @@ func (l *Logic) GetHeroByCode(ctx context.Context, code string) (*entity.EpicHer
// 2. 缓存未命中,查数据库 // 2. 缓存未命中,查数据库
var hero *entity.EpicHeroInfo var hero *entity.EpicHeroInfo
err := dao.EpicHeroInfo.Ctx(ctx). 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). Where(dao.EpicHeroInfo.Columns().HeroCode, code).
Scan(&hero) Scan(&hero)
if err != nil { if err != nil {
@@ -57,7 +71,19 @@ func (l *Logic) GetHeroList(ctx context.Context) ([]*v1.EpicHeroVO, error) {
// 2. 缓存未命中,查数据库 // 2. 缓存未命中,查数据库
err := dao.EpicHeroInfo.Ctx(ctx). 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) Scan(&doList)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -101,6 +127,19 @@ func (l *Logic) GetHeroDetailByCode(ctx context.Context, code string) (*v1.HeroD
) )
err = dao.EpicHeroInfo.Ctx(ctx). 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). Where(dao.EpicHeroInfo.Columns().HeroCode, code).
Scan(&epicHeroInfo) Scan(&epicHeroInfo)
@@ -115,6 +154,7 @@ func (l *Logic) GetHeroDetailByCode(ctx context.Context, code string) (*v1.HeroD
return nil, err return nil, err
} }
thirdPartySync := cron.NewThirdPartyDataSync()
// 优化:先查 RedisCache再查数据库 // 优化:先查 RedisCache再查数据库
cacheKey := "epic_hero_set:" + code cacheKey := "epic_hero_set:" + code
jsonContent, err := util.RedisCache.Get(ctx, cacheKey) 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 { if err != nil {
return nil, err return nil, err
} }
// 写入 Redis 缓存1小时 if util.RedisCache == nil {
util.RedisCache.Set(ctx, cacheKey, fribbleHeroSet.JsonContent, 0) 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 字段 // 解析 JsonContent 字段
@@ -397,7 +458,7 @@ func (l *Logic) GetHeroDetailByCode(ctx context.Context, code string) (*v1.HeroD
return heroDetailVO, nil return heroDetailVO, nil
} }
// ClearHeroCache 清理英雄相关缓存 // 清理英雄相关缓存
func (l *Logic) ClearHeroCache(ctx context.Context, code string) error { func (l *Logic) ClearHeroCache(ctx context.Context, code string) error {
redis := g.Redis() redis := g.Redis()

View File

@@ -2,17 +2,20 @@ package i18n
import ( import (
"context" "context"
"epic/internal/consts"
"epic/internal/dao" "epic/internal/dao"
"epic/internal/model/entity" "epic/internal/model/entity"
"epic/internal/util" "epic/internal/util"
"fmt" "fmt"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/gclient"
"github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/os/gtime"
"sync" "sync"
"time" "time"
) )
// I18nEnToZh 英文->中文映射表(示例 // I18nEnToZh 英文->中文映射表(仅做备份,不再作为主数据源
var I18nEnToZh = map[string]string{ var I18nEnToZh = map[string]string{
"A Little Queen's Huge Crown": "小小女王的巨大王冠", "A Little Queen's Huge Crown": "小小女王的巨大王冠",
"A Song for Everybody": "献给你和这个世界的歌曲", "A Song for Everybody": "献给你和这个世界的歌曲",
@@ -603,14 +606,22 @@ var I18nZhToEn = func() map[string]string {
return m return m
}() }()
// Logic i18n主逻辑结构体
// cache结构调整lang -> { code -> value, key -> value }
type Logic struct { 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 mutex sync.RWMutex
} }
func New() *Logic { func New() *Logic {
return &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() 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 { for _, m := range mappings {
if l.cache[m.Language] == nil { if l.cache[m.Language].ByCode == nil {
l.cache[m.Language] = make(map[string]string) 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), "条记录") util.Info(ctx, "i18n缓存加载完成共", len(mappings), "条记录")
return nil return nil
} }
// Get 获取指定key的指定语言翻译 // Get 获取指定key/code的指定语言翻译
func (l *Logic) Get(ctx context.Context, lang, key string) string { func (l *Logic) Get(ctx context.Context, lang, key string, code ...string) string {
l.mutex.RLock() l.mutex.RLock()
defer l.mutex.RUnlock() 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 m, ok := l.cache[lang]; ok {
if v, ok := m[key]; ok { if v, ok := m.ByKey[key]; ok {
return v return v
} }
} }
@@ -678,7 +708,7 @@ func (l *Logic) GetBatch(ctx context.Context, lang string, keys []string) map[st
result := make(map[string]string) result := make(map[string]string)
if m, ok := l.cache[lang]; ok { if m, ok := l.cache[lang]; ok {
for _, key := range keys { for _, key := range keys {
if v, ok := m[key]; ok { if v, ok := m.ByKey[key]; ok {
result[key] = v result[key] = v
} else { } else {
result[key] = key // 找不到返回原文 result[key] = key // 找不到返回原文
@@ -719,10 +749,16 @@ func (l *Logic) Add(ctx context.Context, key, lang, value, category string) erro
l.mutex.Lock() l.mutex.Lock()
defer l.mutex.Unlock() defer l.mutex.Unlock()
if l.cache[lang] == nil { if _, exists := l.cache[lang]; !exists {
l.cache[lang] = make(map[string]string) 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) util.Info(ctx, "添加翻译:", lang, key, "->", value)
return nil return nil
@@ -747,10 +783,16 @@ func (l *Logic) Update(ctx context.Context, key, lang, value string) error {
l.mutex.Lock() l.mutex.Lock()
defer l.mutex.Unlock() defer l.mutex.Unlock()
if l.cache[lang] == nil { if _, exists := l.cache[lang]; !exists {
l.cache[lang] = make(map[string]string) 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) util.Info(ctx, "更新翻译:", lang, key, "->", value)
return nil return nil
@@ -776,7 +818,7 @@ func (l *Logic) Delete(ctx context.Context, key, lang string) error {
defer l.mutex.Unlock() defer l.mutex.Unlock()
if m, ok := l.cache[lang]; ok { if m, ok := l.cache[lang]; ok {
delete(m, key) delete(m.ByKey, key)
} }
util.Info(ctx, "删除翻译:", lang, 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 return result, nil
} }
// ImportFromMap 从map批量导入翻译 // 从map批量导入翻译
func (l *Logic) ImportFromMap(ctx context.Context, lang string, mappings map[string]string, category string) error { func (l *Logic) ImportFromMap(ctx context.Context, lang string, mappings map[string]string, category string) error {
if len(mappings) == 0 { if len(mappings) == 0 {
return nil return nil
@@ -835,11 +877,17 @@ func (l *Logic) ImportFromMap(ctx context.Context, lang string, mappings map[str
l.mutex.Lock() l.mutex.Lock()
defer l.mutex.Unlock() defer l.mutex.Unlock()
if l.cache[lang] == nil { if _, exists := l.cache[lang]; !exists {
l.cache[lang] = make(map[string]string) 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 { for key, value := range mappings {
l.cache[lang][key] = value l.cache[lang].ByKey[key] = value
} }
util.Info(ctx, "批量导入翻译完成:", lang, category, "共", len(mappings), "条") 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 { func En2Zh(s string) string {
if v, ok := GetI18nLogic().cache["zh"].ByKey[s]; ok {
return v
}
// 仅降级时才用备份
if v, ok := I18nEnToZh[s]; ok { if v, ok := I18nEnToZh[s]; ok {
return v return v
} }
return s return s
} }
// Zh2En 中文转英文(静态映射) // Zh2En 中文转英文(静态映射,仅降级时使用
func Zh2En(s string) string { func Zh2En(s string) string {
if v, ok := GetI18nLogic().cache["en"].ByKey[s]; ok {
return v
}
// 仅降级时才用备份
if v, ok := I18nZhToEn[s]; ok { if v, ok := I18nZhToEn[s]; ok {
return v return v
} }
@@ -962,3 +1018,158 @@ func ImportI18nFromMap(ctx context.Context, lang string, mappings map[string]str
} }
return nil 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
}

View File

@@ -11,22 +11,24 @@ import (
// EpicHeroInfo is the golang structure of table epic_hero_info for DAO operations like Where/Data. // EpicHeroInfo is the golang structure of table epic_hero_info for DAO operations like Where/Data.
type EpicHeroInfo struct { type EpicHeroInfo struct {
g.Meta `orm:"table:epic_hero_info, do:true"` g.Meta `orm:"table:epic_hero_info, do:true"`
Id interface{} // 文件编号 Id interface{} // 文件编号
HeroName interface{} // 配置编号 HeroName interface{} // 配置编号
HeroCode interface{} // 文件名 HeroCode interface{} // 文件名
HeroAttrLv60 interface{} // 文件路径 HeroAttrLv60 interface{} // 文件路径
Creator interface{} // 创建者 Creator interface{} // 创建者
CreateTime *gtime.Time // 创建时间 CreateTime *gtime.Time // 创建时间
Updater interface{} // 更新者 Updater interface{} // 更新者
UpdateTime *gtime.Time // 更新时间 UpdateTime *gtime.Time // 更新时间
Deleted interface{} // 是否删除 Deleted interface{} // 是否删除
NickName interface{} // 配置编号 NickName interface{} // 配置编号
Rarity interface{} // 配置编号 Rarity interface{} // 配置编号
Role interface{} // 配置编号 Role interface{} // 配置编号
Zodiac interface{} // 配置编号 Zodiac interface{} // 配置编号
HeadImgUrl interface{} // 配置编号 HeadImgUrl interface{} // 配置编号
Attribute interface{} // 配置编号 Attribute interface{} // 配置编号
Remark interface{} // 配置编号 Remark interface{} // 配置编号
RawJson interface{} // 原始json RawJson interface{} // 原始json
SetContentJson interface{} // 配装json数据
SetUpdateTime *gtime.Time // 配装更新时间
} }

View File

@@ -23,4 +23,5 @@ type EpicI18NMappings struct {
Updater interface{} // 更新者 Updater interface{} // 更新者
UpdateTime *gtime.Time // 更新时间 UpdateTime *gtime.Time // 更新时间
Deleted interface{} // 是否删除 Deleted interface{} // 是否删除
Code interface{} // 编码
} }

View File

@@ -10,21 +10,23 @@ import (
// EpicHeroInfo is the golang structure for table epic_hero_info. // EpicHeroInfo is the golang structure for table epic_hero_info.
type EpicHeroInfo struct { type EpicHeroInfo struct {
Id int64 `json:"id" orm:"id" description:"文件编号"` // 文件编号 Id int64 `json:"id" orm:"id" description:"文件编号"` // 文件编号
HeroName string `json:"heroName" orm:"hero_name" description:"配置编号"` // 配置编号 HeroName string `json:"heroName" orm:"hero_name" description:"配置编号"` // 配置编号
HeroCode string `json:"heroCode" orm:"hero_code" description:"文件名"` // 文件名 HeroCode string `json:"heroCode" orm:"hero_code" description:"文件名"` // 文件名
HeroAttrLv60 string `json:"heroAttrLv60" orm:"hero_attr_lv60" description:"文件路径"` // 文件路径 HeroAttrLv60 string `json:"heroAttrLv60" orm:"hero_attr_lv60" description:"文件路径"` // 文件路径
Creator string `json:"creator" orm:"creator" description:"创建者"` // 创建者 Creator string `json:"creator" orm:"creator" description:"创建者"` // 创建者
CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间 CreateTime *gtime.Time `json:"createTime" orm:"create_time" description:"创建时间"` // 创建时间
Updater string `json:"updater" orm:"updater" description:"更新者"` // 更新者 Updater string `json:"updater" orm:"updater" description:"更新者"` // 更新者
UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间
Deleted bool `json:"deleted" orm:"deleted" description:"是否删除"` // 是否删除 Deleted bool `json:"deleted" orm:"deleted" description:"是否删除"` // 是否删除
NickName string `json:"nickName" orm:"nick_name" description:"配置编号"` // 配置编号 NickName string `json:"nickName" orm:"nick_name" description:"配置编号"` // 配置编号
Rarity string `json:"rarity" orm:"rarity" description:"配置编号"` // 配置编号 Rarity string `json:"rarity" orm:"rarity" description:"配置编号"` // 配置编号
Role string `json:"role" orm:"role" description:"配置编号"` // 配置编号 Role string `json:"role" orm:"role" description:"配置编号"` // 配置编号
Zodiac string `json:"zodiac" orm:"zodiac" description:"配置编号"` // 配置编号 Zodiac string `json:"zodiac" orm:"zodiac" description:"配置编号"` // 配置编号
HeadImgUrl string `json:"headImgUrl" orm:"head_img_url" description:"配置编号"` // 配置编号 HeadImgUrl string `json:"headImgUrl" orm:"head_img_url" description:"配置编号"` // 配置编号
Attribute string `json:"attribute" orm:"attribute" description:"配置编号"` // 配置编号 Attribute string `json:"attribute" orm:"attribute" description:"配置编号"` // 配置编号
Remark string `json:"remark" orm:"remark" description:"配置编号"` // 配置编号 Remark string `json:"remark" orm:"remark" description:"配置编号"` // 配置编号
RawJson string `json:"rawJson" orm:"raw_json" description:"原始json"` // 原始json 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:"配装更新时间"` // 配装更新时间
} }

View File

@@ -21,4 +21,5 @@ type EpicI18NMappings struct {
Updater string `json:"updater" orm:"updater" description:"更新者"` // 更新者 Updater string `json:"updater" orm:"updater" description:"更新者"` // 更新者
UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间 UpdateTime *gtime.Time `json:"updateTime" orm:"update_time" description:"更新时间"` // 更新时间
Deleted int `json:"deleted" orm:"deleted" description:"是否删除"` // 是否删除 Deleted int `json:"deleted" orm:"deleted" description:"是否删除"` // 是否删除
Code string `json:"code" orm:"code" description:"编码"` // 编码
} }

View File

@@ -17,12 +17,37 @@ func Info(ctx context.Context, args ...interface{}) {
g.Log().Info(ctx, args...) g.Log().Info(ctx, args...)
} }
// Infof 全局Info格式化日志
func Infof(ctx context.Context, format string, args ...interface{}) {
g.Log().Infof(ctx, format, args...)
}
// Debug 全局Debug日志 // Debug 全局Debug日志
func Debug(ctx context.Context, args ...interface{}) { func Debug(ctx context.Context, args ...interface{}) {
g.Log().Debug(ctx, args...) g.Log().Debug(ctx, args...)
} }
// Debugf 全局Debug格式化日志
func Debugf(ctx context.Context, format string, args ...interface{}) {
g.Log().Debugf(ctx, format, args...)
}
// Error 全局Error日志 // Error 全局Error日志
func Error(ctx context.Context, args ...interface{}) { func Error(ctx context.Context, args ...interface{}) {
g.Log().Error(ctx, args...) 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...)
}

View File

@@ -7,7 +7,8 @@ import (
"io" "io"
"net/http" "net/http"
"os" "os"
"path/filepath" "strings"
"sync"
"time" "time"
"github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws"
@@ -16,22 +17,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 +89,71 @@ 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
}
// 缓存到Rediskey可加前缀区分
cacheKey := "oss:presignurl:" + key
// 自动生成CDN域名的预签名可访问地址
cdnPresignedUrl := strings.Replace(presignResult.URL, "https://"+bucket+".s3.bitiful.net", "https://bfoss.htoop.cn", 1)
fmt.Printf("CDN预签名可访问地址: %s\n", cdnPresignedUrl)
// 写入Redis时也用CDN域名的预签名地址
if err := RedisCache.Set(ctx, cacheKey, cdnPresignedUrl, expire); err != nil {
Error(ctx, "写入Redis失败", cacheKey, err)
}
}
return nil
}

View File

@@ -13,11 +13,11 @@ logger:
database: database:
default: default:
link: "mysql:root:hu123456@tcp(193.112.151.199:3306)/wow" link: "mysql:root:hu123456@tcp(193.112.151.199:3306)/wow"
debug: true debug: false
# https://goframe.org/docs/core/gredis-config-file # https://goframe.org/docs/core/gredis-config-file
redis: redis:
default: default:
address: "193.112.151.199:6379" address: "193.112.151.199:6379"
db: 1 db: 0
pass: "hu123" pass: "hu123"