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
|
#### Windows
|
||||||
```cmd
|
|
||||||
scripts\start.bat
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 手动启动
|
|
||||||
```bash
|
```bash
|
||||||
go run main.go
|
go run main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 访问服务
|
### 访问服务
|
||||||
|
|
||||||
- **API服务**: http://localhost:8283
|
- **API服务**: http://localhost:8283
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 退出程序
|
// 退出程序
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ type EpicHeroInfoColumns struct {
|
|||||||
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.
|
||||||
@@ -59,6 +61,8 @@ var epicHeroInfoColumns = EpicHeroInfoColumns{
|
|||||||
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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("修复英雄/神器中文名成功")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,9 +170,30 @@ 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 {
|
||||||
|
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)
|
util.RedisCache.Set(ctx, cacheKey, fribbleHeroSet.JsonContent, 0)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 解析 JsonContent 字段
|
// 解析 JsonContent 字段
|
||||||
if err := gjson.DecodeTo(fribbleHeroSet.JsonContent, &heroSetData); err != nil {
|
if err := gjson.DecodeTo(fribbleHeroSet.JsonContent, &heroSetData); err != nil {
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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 m, ok := l.cache[lang]; ok {
|
||||||
if v, ok := m[key]; ok {
|
if v, ok := m.ByCode[code[0]]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m, ok := l.cache[lang]; 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,4 +29,6 @@ type EpicHeroInfo struct {
|
|||||||
Attribute interface{} // 配置编号
|
Attribute interface{} // 配置编号
|
||||||
Remark interface{} // 配置编号
|
Remark interface{} // 配置编号
|
||||||
RawJson interface{} // 原始json
|
RawJson interface{} // 原始json
|
||||||
|
SetContentJson interface{} // 配装json数据
|
||||||
|
SetUpdateTime *gtime.Time // 配装更新时间
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,4 +23,5 @@ type EpicI18NMappings struct {
|
|||||||
Updater interface{} // 更新者
|
Updater interface{} // 更新者
|
||||||
UpdateTime *gtime.Time // 更新时间
|
UpdateTime *gtime.Time // 更新时间
|
||||||
Deleted interface{} // 是否删除
|
Deleted interface{} // 是否删除
|
||||||
|
Code interface{} // 编码
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,4 +27,6 @@ type EpicHeroInfo struct {
|
|||||||
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:"配装更新时间"` // 配装更新时间
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:"编码"` // 编码
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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...)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
// 缓存到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:
|
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"
|
||||||
Reference in New Issue
Block a user