Files
epic-go/internal/logic/cron/third_party_sync.go
hxt 0ad79c4f27 ci(drone): 添加 Go 模块和构建缓存
- 在 restore cache 和 rebuild cache 步骤中添加了 go-mod-cache 和 go
2025-07-17 20:25:50 +08:00

480 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package cron
import (
"context"
"encoding/json"
"epic/internal/consts"
"epic/internal/dao"
"epic/internal/logic/i18n"
"epic/internal/model/dto"
"epic/internal/model/entity"
"epic/internal/util"
"fmt"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/gclient"
"github.com/gogf/gf/v2/os/gtime"
"strconv"
"time"
)
// ThirdPartyDataSync 第三方数据同步器
type ThirdPartyDataSync struct {
client *gclient.Client
}
// NewThirdPartyDataSync 创建第三方数据同步器
func NewThirdPartyDataSync() *ThirdPartyDataSync {
return &ThirdPartyDataSync{
client: gclient.New().Timeout(30 * time.Second),
}
}
func (t *ThirdPartyDataSync) SyncHeroData(ctx context.Context) error {
g.Log().Info(ctx, "开始同步英雄数据...")
// 示例从第三方API获取英雄数据
heroData, err := t.fetchHeroDataFromAPI(ctx)
if err != nil || heroData == nil {
g.Log().Error(ctx, "获取英雄数据失败:", err)
return err
}
// 处理并保存数据
if err := t.processAndSaveHeroData(ctx, heroData); err != nil {
g.Log().Error(ctx, "处理英雄数据失败:", err)
return err
}
g.Log().Info(ctx, "英雄数据同步完成")
return nil
}
// SyncArtifactData 同步神器数据
func (t *ThirdPartyDataSync) SyncArtifactData(ctx context.Context) error {
g.Log().Info(ctx, "开始同步神器数据...")
//从第三方API获取神器数据
artifactData, err := t.fetchArtifactDataFromAPI(ctx)
if err != nil {
g.Log().Error(ctx, "获取神器数据失败:", err)
return err
}
// 处理并保存数据
if err := t.processAndSaveArtifactData(ctx, artifactData); err != nil {
g.Log().Error(ctx, "处理神器数据失败:", err)
return err
}
g.Log().Info(ctx, "神器数据同步完成")
return nil
}
// fetchHeroDataFromAPI 从API获取英雄数据
func (t *ThirdPartyDataSync) fetchHeroDataFromAPI(ctx context.Context) ([]byte, error) {
// 示例API地址实际使用时需要替换为真实的API
apiURL := consts.HeroListURL
// 添加请求头
headers := map[string]string{
"User-Agent": "EpicGameBot/1.0",
"Accept": "application/json",
}
// 发送GET请求
resp, err := t.client.Header(headers).Get(ctx, apiURL)
if err != nil {
return nil, fmt.Errorf("API请求失败: %v", err)
}
defer resp.Close()
// 检查响应状态
if resp.StatusCode != 200 {
return nil, fmt.Errorf("API响应错误状态码: %d", resp.StatusCode)
}
// 读取响应内容
content := resp.ReadAll()
g.Log().Debug(ctx, "API响应内容长度:", len(content))
return content, nil
}
// fetchHeroBuildsFromAPI 通过角色英文名POST请求获取配装数据
func (t *ThirdPartyDataSync) FetchHeroBuildsFromAPI(ctx context.Context, heroName string) (string, error) {
apiURL := consts.HeroNameURL
headers := map[string]string{
"User-Agent": "EpicGameBot/1.0",
"Accept": "application/json",
"Content-Type": "application/json",
}
heroNameEN := i18n.Zh2En(heroName)
resp, err := t.client.Header(headers).Post(ctx, apiURL, heroNameEN)
if err != nil {
return "", fmt.Errorf("API请求失败: %v", err)
}
defer resp.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("API响应错误状态码: %d", resp.StatusCode)
}
content := resp.ReadAll()
g.Log().Debug(ctx, "配装API响应内容长度:", len(content))
return string(content), nil
}
// 从API获取神器数据
func (t *ThirdPartyDataSync) fetchArtifactDataFromAPI(ctx context.Context) (string, error) {
// 示例API地址
apiURL := "https://static.smilegatemegaport.com/gameRecord/epic7/epic7_artifact.json?_=1729322698936"
headers := map[string]string{
//"User-Agent": "EpicGameBot/1.0",
"Accept": "application/json",
}
resp, err := t.client.Header(headers).Get(ctx, apiURL)
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
}
func (t *ThirdPartyDataSync) processAndSaveHeroData(ctx context.Context, data []byte) error {
// 使用 gjson 解析
j := gjson.New(data)
if j == nil || j.IsNil() {
return fmt.Errorf("英雄数据格式错误期望是一个JSON对象")
}
// 先解析为 map[string]*ThirdPartyHeroDTO
var heroes map[string]*dto.ThirdPartyHeroDTO
if err := j.Scan(&heroes); err != nil {
return fmt.Errorf("解析英雄数据到DTO失败: %v", err)
}
g.Log().Info(ctx, "解析到", len(heroes), "个英雄数据")
// 一次性查出所有数据库英雄构建map
var dbHeroes []*entity.EpicHeroInfo
err := dao.EpicHeroInfo.Ctx(ctx).Scan(&dbHeroes)
if err != nil {
return fmt.Errorf("查询数据库英雄失败: %v", err)
}
heroMap := make(map[string]*entity.EpicHeroInfo, len(dbHeroes))
for _, h := range dbHeroes {
heroMap[h.HeroCode] = h
}
// 遍历 map设置 Name 字段,并保存
for name, hero := range heroes {
hero.Name = name // 将 map 的 key 作为 Name 字段
dbHero, exists := heroMap[hero.Code]
if exists && dbHero.RawJson != "" {
// 已有且rawJson有值跳过
continue
}
if exists && dbHero.RawJson == "" {
// 只更新 rawJson 字段
rawJsonBytes, err := json.Marshal(hero)
if err != nil {
g.Log().Error(ctx, "序列化英雄数据失败:", err)
continue
}
rawJson := string(rawJsonBytes)
_, err = dao.EpicHeroInfo.Ctx(ctx).
Where(dao.EpicHeroInfo.Columns().HeroCode, hero.Code).
Data(g.Map{
dao.EpicHeroInfo.Columns().RawJson: rawJson,
dao.EpicHeroInfo.Columns().UpdateTime: gtime.Now(),
}).
Update()
if err != nil {
g.Log().Error(ctx, "更新英雄rawJson失败:", err)
continue
}
g.Log().Debug(ctx, "更新英雄rawJson:", hero.Code)
continue
}
// 新增逻辑保持原样
status60 := hero.CalculatedStatus.Lv60SixStarFullyAwakened
status60json, _ := gjson.EncodeString(status60)
heroJson, _ := gjson.EncodeString(hero)
zhHeroName := i18n.GetZh(ctx, hero.Name)
//zhRole := i18n.GetZh(ctx, hero.Role)
//zhAttribute := i18n.GetZh(ctx, hero.Attribute)
fmt.Println(hero.Assets.Image)
// 上传图片到 OSS并获取自定义域名的访问路径
ossObjectKey := fmt.Sprintf("epic/hero/images/%s.png", hero.Code)
ossUrl, err := util.DownloadAndUploadToOSS(ctx, hero.Assets.Icon, ossObjectKey)
if err != nil {
g.Log().Error(ctx, "上传英雄图片到OSS失败:", err)
return err // 直接返回,后续数据库不会插入
}
fmt.Println(ossUrl)
// 替换为自定义域名
customImgUrl := ""
if ossUrl != "" {
prefix := consts.S3Endpoint + "/" + consts.S3Bucket
if len(ossUrl) > len(prefix) && ossUrl[:len(prefix)] == prefix {
customImgUrl = consts.S3CustomDomain + ossUrl[len(prefix):]
} else {
customImgUrl = ossUrl // fallback
}
}
newHero := &entity.EpicHeroInfo{
Id: 0,
HeroName: zhHeroName,
HeroCode: hero.Code,
HeroAttrLv60: status60json,
Creator: "",
CreateTime: gtime.Now(),
Updater: "",
UpdateTime: gtime.Now(),
Deleted: false,
//NickName: "",
Rarity: strconv.Itoa(hero.Rarity),
Role: hero.Role,
//Zodiac: "",
HeadImgUrl: customImgUrl,
Attribute: hero.Attribute,
Remark: "",
RawJson: heroJson,
}
_, err = dao.EpicHeroInfo.Ctx(ctx).Data(newHero).Insert()
if err != nil {
g.Log().Error(ctx, "插入新英雄失败:", err)
continue
}
g.Log().Debug(ctx, "插入新英雄:", hero.Code)
// 新增后立即刷新该图片的OSS预签名URL到缓存
if customImgUrl != "" {
key := ossObjectKey
err := util.RefreshOssPresignedUrlCache(ctx, []string{key}, 24*time.Hour)
if err != nil {
g.Log().Error(ctx, "刷新新英雄图片预签名URL失败:", key, err)
}
}
}
return nil
}
// processAndSaveArtifactData 处理并保存神器数据
func (t *ThirdPartyDataSync) processAndSaveArtifactData(ctx context.Context, data string) error {
// 使用 gjson 解析
j := gjson.New(data)
zhcn := j.Get("zh-CN")
// 检查json对象本身和其内部值并使用 .Var().IsSlice() 这种更可靠的方式判断是否为数组
if !zhcn.IsSlice() {
return fmt.Errorf("神器数据格式错误期望是一个JSON数组")
}
var artifacts []*dto.ThirdPartyArtifactDTO
if err := zhcn.Scan(&artifacts); err != nil {
return fmt.Errorf("解析神器数据到DTO失败: %v", err)
}
// 批量处理数据
for _, artifact := range artifacts {
if err := t.saveArtifactData(ctx, artifact); err != nil {
g.Log().Error(ctx, "保存神器数据失败:", err)
continue
}
}
return nil
}
// saveHeroData 保存单个英雄数据
func (t *ThirdPartyDataSync) saveHeroData(ctx context.Context, hero *dto.ThirdPartyHeroDTO) error {
// 查询是否存在
var dbHero *entity.EpicHeroInfo
err := dao.EpicHeroInfo.Ctx(ctx).
Where(dao.EpicHeroInfo.Columns().HeroCode, hero.Code).
Scan(&dbHero)
if err != nil {
return err
}
// 获取原始 JSON 字符串
rawJsonBytes, err := json.Marshal(hero)
if err != nil {
return err
}
rawJson := string(rawJsonBytes)
if dbHero != nil && dbHero.HeroCode != "" {
// 查到记录
if dbHero.RawJson == "" {
// 只更新 rawJson 字段
_, err = dao.EpicHeroInfo.Ctx(ctx).
Where(dao.EpicHeroInfo.Columns().HeroCode, hero.Code).
Data(g.Map{
dao.EpicHeroInfo.Columns().RawJson: rawJson,
dao.EpicHeroInfo.Columns().UpdateTime: gtime.Now(),
}).
Update()
if err != nil {
return err
}
g.Log().Debug(ctx, "更新英雄rawJson:", hero.Code)
}
// 已有 rawJson不做处理
} else {
status60 := hero.CalculatedStatus.Lv60SixStarFullyAwakened
status60json, _ := gjson.EncodeString(status60)
heroJson, _ := gjson.EncodeString(hero)
// 使用i18n服务转换字段
zhHeroName := i18n.GetZh(ctx, hero.Name)
//zhRole := i18n.GetZh(ctx, hero.Role)
//zhAttribute := i18n.GetZh(ctx, hero.Attribute)
newHero := &entity.EpicHeroInfo{
Id: 0,
HeroName: zhHeroName,
HeroCode: hero.Code,
HeroAttrLv60: status60json,
Creator: "",
CreateTime: gtime.Now(),
Updater: "",
UpdateTime: gtime.Now(),
Deleted: false,
//NickName: nil,
Rarity: strconv.Itoa(hero.Rarity),
Role: hero.Role,
Zodiac: "",
HeadImgUrl: "",
Attribute: hero.Attribute,
Remark: "",
RawJson: heroJson,
}
// 没查到,插入新记录,字段按 DO 结构体补全
_, err = dao.EpicHeroInfo.Ctx(ctx).Data(newHero).Insert()
if err != nil {
return err
}
g.Log().Debug(ctx, "插入新英雄:", hero.Code)
}
return nil
}
// 保存单个神器数据
func (t *ThirdPartyDataSync) saveArtifactData(ctx context.Context, artifact *dto.ThirdPartyArtifactDTO) error {
// TODO: 实现具体的数据库保存逻辑
// 现在 artifact 是一个强类型对象, 可以直接使用 artifact.Code, artifact.Name 等
// 示例:记录同步日志
syncLog := map[string]interface{}{
"type": "artifact_sync",
"artifact_code": artifact.Code,
"sync_time": gtime.Now(),
"status": "success",
}
g.Log().Debug(ctx, "保存神器数据:", syncLog)
return nil
}
// SyncAllData 同步所有数据
func (t *ThirdPartyDataSync) SyncAllData(ctx context.Context) error {
g.Log().Info(ctx, "开始同步所有第三方数据...")
// 同步英雄数据
if err := t.SyncHeroData(ctx); err != nil {
g.Log().Error(ctx, "英雄数据同步失败:", err)
// 继续同步其他数据
}
// 同步神器数据
if err := t.SyncArtifactData(ctx); err != nil {
g.Log().Error(ctx, "神器数据同步失败:", err)
// 继续同步其他数据
}
// 可以继续添加其他数据类型的同步
g.Log().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 {
g.Log().Error(ctx, "配装数据无效(长度<=200): %s", heroName)
return fmt.Errorf("配装数据无效(长度<=200)")
}
}
// RefreshAllHeroSetContent 刷新所有角色配装字段
func (t *ThirdPartyDataSync) RefreshAllHeroSetContent(ctx context.Context) error {
g.Log().Info(ctx, "开始批量刷新所有角色配装字段...")
// 1. 查询所有角色按set_update_time正序排列为空的在最前面
var heroList []*entity.EpicHeroInfo
err := dao.EpicHeroInfo.Ctx(ctx).
OrderAsc("set_update_time").
Scan(&heroList)
if err != nil {
g.Log().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
}