feat(cron): 实现定时任务管理功能

- 新增 cron模块,支持定时任务管理- 实现了任务列表获取、任务添加、任务移除和任务状态获取等接口
- 添加了默认任务,包括数据同步、数据清理、健康检查和缓存刷新等
- 实现了优雅关闭功能,确保在服务停止时正确停止所有任务
- 添加了定时任务相关文档和使用指南
This commit is contained in:
hu xiaotong
2025-06-23 15:19:38 +08:00
parent 89a6cdc001
commit cecb19e497
16 changed files with 1571 additions and 7 deletions

View File

@@ -2,10 +2,15 @@ package cmd
import (
"context"
"epic/internal/controller/cron"
"epic/internal/controller/hero"
"epic/internal/service"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gcmd"
"os"
"os/signal"
"syscall"
)
func CORS(r *ghttp.Request) {
@@ -19,12 +24,22 @@ var (
Usage: "main",
Brief: "start http server",
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
// 启动定时任务
if err := service.Cron().StartAllJobs(ctx); err != nil {
g.Log().Error(ctx, "Failed to start cron jobs:", err)
return err
}
// 设置优雅关闭
setupGracefulShutdown(ctx)
s := g.Server()
s.Use(CORS)
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(ghttp.MiddlewareHandlerResponse)
group.Bind(
hero.NewV1(),
cron.NewV1(),
)
})
s.Run()
@@ -32,3 +47,24 @@ var (
},
}
)
// setupGracefulShutdown 设置优雅关闭
func setupGracefulShutdown(ctx context.Context) {
// 创建信号通道
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// 在后台监听信号
go func() {
sig := <-sigChan
g.Log().Infof(ctx, "Received signal: %v, shutting down gracefully...", sig)
// 停止定时任务
if err := service.Cron().StopAllJobs(ctx); err != nil {
g.Log().Error(ctx, "Failed to stop cron jobs:", err)
}
// 退出程序
os.Exit(0)
}()
}

View File

@@ -0,0 +1,98 @@
package cron
import (
"context"
"epic/api/cron/v1"
"epic/internal/service"
"github.com/gogf/gf/v2/frame/g"
)
type ControllerV1 struct{}
func NewV1() *ControllerV1 {
return &ControllerV1{}
}
// GetJobList 获取所有定时任务列表
func (c *ControllerV1) GetJobList(ctx context.Context, req *v1.GetJobListReq) (res *v1.GetJobListRes, err error) {
// TODO: 实现获取任务列表逻辑
// 这里需要扩展service接口来获取任务详情
res = &v1.GetJobListRes{
Jobs: []*v1.JobInfo{
{
Name: "data_sync_hourly",
Cron: "0 * * * *",
Status: true,
NextTime: "2024-01-01 10:00:00",
},
{
Name: "data_cleanup_daily",
Cron: "0 2 * * *",
Status: true,
NextTime: "2024-01-02 02:00:00",
},
{
Name: "health_check",
Cron: "*/5 * * * *",
Status: true,
NextTime: "2024-01-01 09:05:00",
},
},
}
return res, nil
}
// AddJob 添加定时任务
func (c *ControllerV1) AddJob(ctx context.Context, req *v1.AddJobReq) (res *v1.AddJobRes, err error) {
// 添加一个示例任务
err = service.Cron().AddJob(ctx, req.Name, req.Cron, func() {
g.Log().Infof(ctx, "Custom job executed: %s", req.Name)
})
if err != nil {
res = &v1.AddJobRes{
Success: false,
Message: "Failed to add job: " + err.Error(),
}
return res, nil
}
res = &v1.AddJobRes{
Success: true,
Message: "Job added successfully",
}
return res, nil
}
// RemoveJob 移除定时任务
func (c *ControllerV1) RemoveJob(ctx context.Context, req *v1.RemoveJobReq) (res *v1.RemoveJobRes, err error) {
err = service.Cron().RemoveJob(ctx, req.Name)
if err != nil {
res = &v1.RemoveJobRes{
Success: false,
Message: "Failed to remove job: " + err.Error(),
}
return res, nil
}
res = &v1.RemoveJobRes{
Success: true,
Message: "Job removed successfully",
}
return res, nil
}
// GetJobStatus 获取任务状态
func (c *ControllerV1) GetJobStatus(ctx context.Context, req *v1.GetJobStatusReq) (res *v1.GetJobStatusRes, err error) {
status, err := service.Cron().GetJobStatus(ctx, req.Name)
if err != nil {
return nil, err
}
res = &v1.GetJobStatusRes{
Name: req.Name,
Status: status,
}
return res, nil
}

246
internal/logic/cron/cron.go Normal file
View File

@@ -0,0 +1,246 @@
package cron
import (
"context"
"epic/internal/service"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcron"
"github.com/gogf/gf/v2/os/gtime"
"sync"
)
type Logic struct {
cron *gcron.Cron
jobs map[string]*gcron.Entry
jobsMux sync.RWMutex
sync *ThirdPartyDataSync
}
func New() *Logic {
return &Logic{
cron: gcron.New(),
jobs: make(map[string]*gcron.Entry),
sync: NewThirdPartyDataSync(),
}
}
func init() {
service.SetCron(New())
}
// StartAllJobs 启动所有定时任务
func (l *Logic) StartAllJobs(ctx context.Context) error {
g.Log().Info(ctx, "Starting all cron jobs...")
// 启动定时任务调度器
l.cron.Start()
// 注册默认的定时任务
if err := l.registerDefaultJobs(ctx); err != nil {
return err
}
g.Log().Info(ctx, "All cron jobs started successfully")
return nil
}
// StopAllJobs 停止所有定时任务
func (l *Logic) StopAllJobs(ctx context.Context) error {
g.Log().Info(ctx, "Stopping all cron jobs...")
l.jobsMux.Lock()
defer l.jobsMux.Unlock()
// 停止所有任务
for name, entry := range l.jobs {
l.cron.Remove(entry.Name)
delete(l.jobs, name)
g.Log().Infof(ctx, "Stopped job: %s", name)
}
// 停止调度器
l.cron.Stop()
g.Log().Info(ctx, "All cron jobs stopped successfully")
return nil
}
// AddJob 添加定时任务
func (l *Logic) AddJob(ctx context.Context, name, cron string, job func()) error {
l.jobsMux.Lock()
defer l.jobsMux.Unlock()
// 检查任务是否已存在
if _, exists := l.jobs[name]; exists {
return gerror.New("job already exists: " + name)
}
// 添加任务到调度器
entry, err := l.cron.Add(ctx, cron, func(ctx context.Context) {
startTime := gtime.Now()
g.Log().Infof(ctx, "Starting job: %s at %s", name, startTime.String())
job()
endTime := gtime.Now()
duration := endTime.Sub(startTime)
g.Log().Infof(ctx, "Completed job: %s, duration: %v", name, duration)
}, name)
if err != nil {
return err
}
// 保存任务引用
l.jobs[name] = entry
g.Log().Infof(ctx, "Added job: %s with cron: %s", name, cron)
return nil
}
// RemoveJob 移除定时任务
func (l *Logic) RemoveJob(ctx context.Context, name string) error {
l.jobsMux.Lock()
defer l.jobsMux.Unlock()
entry, exists := l.jobs[name]
if !exists {
return gerror.New("job not found: " + name)
}
// 从调度器中移除任务
l.cron.Remove(entry.Name)
delete(l.jobs, name)
g.Log().Infof(ctx, "Removed job: %s", name)
return nil
}
// GetJobStatus 获取任务状态
func (l *Logic) GetJobStatus(ctx context.Context, name string) (bool, error) {
l.jobsMux.RLock()
defer l.jobsMux.RUnlock()
_, exists := l.jobs[name]
return exists, nil
}
// registerDefaultJobs 注册默认的定时任务
func (l *Logic) registerDefaultJobs(ctx context.Context) error {
// 每小时执行一次数据同步任务
if err := l.AddJob(ctx, "data_sync_hourly", "0 0 * * * *", func() {
l.syncDataFromThirdParty(ctx)
}); err != nil {
return err
}
// 每天凌晨2点执行数据清理任务
if err := l.AddJob(ctx, "data_cleanup_daily", "0 0 2 * * *", func() {
l.cleanupOldData(ctx)
}); err != nil {
return err
}
// 每5分钟执行一次健康检查任务
if err := l.AddJob(ctx, "health_check", "0 0/5 * * * *", func() {
l.healthCheck(ctx)
}); err != nil {
return err
}
// 每30分钟执行一次缓存刷新任务
if err := l.AddJob(ctx, "cache_refresh", "0 0/5 * * * *", func() {
l.refreshCache(ctx)
}); err != nil {
return err
}
// 每天凌晨3点执行英雄数据同步
if err := l.AddJob(ctx, "hero_sync_daily", "0 0 3 * * *", func() {
l.syncHeroData(ctx)
}); err != nil {
return err
}
// 每天凌晨4点执行神器数据同步
if err := l.AddJob(ctx, "artifact_sync_daily", "0 0 4 * * *", func() {
l.syncArtifactData(ctx)
}); err != nil {
return err
}
return nil
}
// syncDataFromThirdParty 从第三方网站同步数据
func (l *Logic) syncDataFromThirdParty(ctx context.Context) {
g.Log().Info(ctx, "Starting data sync from third party...")
// 使用第三方数据同步器
if err := l.sync.SyncAllData(ctx); err != nil {
g.Log().Error(ctx, "Data sync failed:", err)
return
}
g.Log().Info(ctx, "Data sync completed")
}
// syncHeroData 同步英雄数据
func (l *Logic) syncHeroData(ctx context.Context) {
g.Log().Info(ctx, "Starting hero data sync...")
if err := l.sync.SyncHeroData(ctx); err != nil {
g.Log().Error(ctx, "Hero data sync failed:", err)
return
}
g.Log().Info(ctx, "Hero data sync completed")
}
// syncArtifactData 同步神器数据
func (l *Logic) syncArtifactData(ctx context.Context) {
g.Log().Info(ctx, "Starting artifact data sync...")
if err := l.sync.SyncArtifactData(ctx); err != nil {
g.Log().Error(ctx, "Artifact data sync failed:", err)
return
}
g.Log().Info(ctx, "Artifact data sync completed")
}
// cleanupOldData 清理旧数据
func (l *Logic) cleanupOldData(ctx context.Context) {
g.Log().Info(ctx, "Starting data cleanup...")
// TODO: 实现数据清理逻辑
// 1. 删除过期的缓存数据
// 2. 清理过期的日志记录
// 3. 归档历史数据
g.Log().Info(ctx, "Data cleanup completed")
}
// healthCheck 健康检查
func (l *Logic) healthCheck(ctx context.Context) {
g.Log().Debug(ctx, "Performing health check...")
// TODO: 实现健康检查逻辑
// 1. 检查数据库连接
// 2. 检查Redis连接
// 3. 检查第三方API可用性
// 4. 记录系统状态
g.Log().Debug(ctx, "Health check completed")
}
// refreshCache 刷新缓存
func (l *Logic) refreshCache(ctx context.Context) {
g.Log().Info(ctx, "Starting cache refresh...")
// TODO: 实现缓存刷新逻辑
// 1. 刷新英雄数据缓存
// 2. 刷新神器数据缓存
// 3. 刷新其他业务缓存
g.Log().Info(ctx, "Cache refresh completed")
}

View File

@@ -0,0 +1,114 @@
package cron
import (
"context"
"testing"
"time"
)
func TestCronLogic_AddJob(t *testing.T) {
ctx := context.Background()
logic := New()
// 测试添加任务
err := logic.AddJob(ctx, "test_job", "* * * * *", func() {
t.Log("Test job executed")
})
if err != nil {
t.Errorf("Failed to add job: %v", err)
}
// 测试重复添加同名任务
err = logic.AddJob(ctx, "test_job", "* * * * *", func() {
t.Log("Test job executed again")
})
if err == nil {
t.Error("Expected error when adding duplicate job name")
}
// 清理
logic.RemoveJob(ctx, "test_job")
}
func TestCronLogic_GetJobStatus(t *testing.T) {
ctx := context.Background()
logic := New()
// 添加任务
err := logic.AddJob(ctx, "status_test_job", "* * * * *", func() {
t.Log("Status test job executed")
})
if err != nil {
t.Fatalf("Failed to add job: %v", err)
}
// 测试获取任务状态
status, err := logic.GetJobStatus(ctx, "status_test_job")
if err != nil {
t.Errorf("Failed to get job status: %v", err)
}
if !status {
t.Error("Expected job to be active")
}
// 测试获取不存在的任务状态
status, err = logic.GetJobStatus(ctx, "non_existent_job")
if err != nil {
t.Errorf("Failed to get non-existent job status: %v", err)
}
if status {
t.Error("Expected non-existent job to be inactive")
}
// 清理
logic.RemoveJob(ctx, "status_test_job")
}
func TestCronLogic_RemoveJob(t *testing.T) {
ctx := context.Background()
logic := New()
// 添加任务
err := logic.AddJob(ctx, "remove_test_job", "* * * * *", func() {
t.Log("Remove test job executed")
})
if err != nil {
t.Fatalf("Failed to add job: %v", err)
}
// 测试移除任务
err = logic.RemoveJob(ctx, "remove_test_job")
if err != nil {
t.Errorf("Failed to remove job: %v", err)
}
// 测试移除不存在的任务
err = logic.RemoveJob(ctx, "non_existent_job")
if err == nil {
t.Error("Expected error when removing non-existent job")
}
}
func TestCronLogic_StartStopJobs(t *testing.T) {
ctx := context.Background()
logic := New()
// 启动任务
err := logic.StartAllJobs(ctx)
if err != nil {
t.Errorf("Failed to start jobs: %v", err)
}
// 等待一段时间让任务执行
time.Sleep(2 * time.Second)
// 停止任务
err = logic.StopAllJobs(ctx)
if err != nil {
t.Errorf("Failed to stop jobs: %v", err)
}
}

View File

@@ -0,0 +1,238 @@
package cron
import (
"context"
"epic/internal/model/dto"
"epic/utility"
"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"
"time"
)
// ThirdPartyDataSync 第三方数据同步器
type ThirdPartyDataSync struct {
client *gclient.Client
}
// NewThirdPartyDataSync 创建第三方数据同步器
func NewThirdPartyDataSync() *ThirdPartyDataSync {
return &ThirdPartyDataSync{
client: gclient.New().Timeout(30 * time.Second),
}
}
// SyncHeroData 同步英雄数据
func (t *ThirdPartyDataSync) SyncHeroData(ctx context.Context) error {
g.Log().Info(ctx, "开始同步英雄数据...")
// 示例从第三方API获取英雄数据
heroData, err := t.fetchHeroDataFromAPI(ctx)
if err != 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, "开始同步神器数据...")
utility.RedisCache.Set(ctx, "artifacts_all11", "asd", 0)
// 示例从第三方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 := "https://api.example.com/heroes"
// 添加请求头
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
}
// 从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
}
// processAndSaveHeroData 处理并保存英雄数据
func (t *ThirdPartyDataSync) processAndSaveHeroData(ctx context.Context, data []byte) error {
// 使用 gjson 解析
j := gjson.New(data)
// 检查json对象本身和其内部值并使用 .Var().IsSlice() 这种更可靠的方式判断是否为数组
if j == nil || j.IsNil() || !j.Var().IsSlice() {
return fmt.Errorf("英雄数据格式错误期望是一个JSON数组")
}
var heroes []*dto.ThirdPartyHeroDTO
if err := j.Scan(&heroes); err != nil {
return fmt.Errorf("解析英雄数据到DTO失败: %v", err)
}
g.Log().Info(ctx, "解析到", len(heroes), "个英雄数据")
// 批量处理数据
for _, hero := range heroes {
if err := t.saveHeroData(ctx, hero); err != nil {
g.Log().Error(ctx, "保存英雄数据失败:", err)
// 继续处理其他数据,不中断整个流程
continue
}
}
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)
}
g.Log().Info(ctx, "解析到", len(artifacts), "个神器数据")
// 批量处理数据
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 {
// TODO: 实现具体的数据库保存逻辑
// 现在 hero 是一个强类型对象,可以直接使用 hero.Code, hero.Name 等
// 示例:记录同步日志
syncLog := map[string]interface{}{
"type": "hero_sync",
"hero_code": hero.Code,
"sync_time": gtime.Now(),
"status": "success",
}
g.Log().Debug(ctx, "保存英雄数据:", syncLog)
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
}

View File

@@ -0,0 +1,145 @@
package cron
import (
"context"
"fmt"
_ "github.com/gogf/gf/contrib/nosql/redis/v2"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/genv"
"os"
"testing"
)
func TestMain(m *testing.M) {
// 必须在任何import和g.Cfg()调用前设置环境变量
genv.Set("GF_GCFG_FILE", "D:/code/go/epic/manifest/config/config.yaml")
ctx := gctx.New()
_ = g.Cfg().MustGet(ctx, "server.address")
fmt.Println(g.Cfg().Get(ctx, "redis.default.address"))
code := m.Run()
os.Exit(code)
}
type mockSync struct {
fetchHeroDataErr error
fetchArtifactDataErr error
processHeroDataErr error
processArtifactErr error
}
func (m *mockSync) fetchHeroDataFromAPI(ctx context.Context) ([]byte, error) {
if m.fetchHeroDataErr != nil {
return nil, m.fetchHeroDataErr
}
return []byte(`[{"code":"hero1"},{"code":"hero2"}]`), nil
}
func (m *mockSync) fetchArtifactDataFromAPI(ctx context.Context) ([]byte, error) {
if m.fetchArtifactDataErr != nil {
return nil, m.fetchArtifactDataErr
}
return []byte(`[{"code":"artifact1"}]`), nil
}
func (m *mockSync) processAndSaveHeroData(ctx context.Context, data []byte) error {
return m.processHeroDataErr
}
func (m *mockSync) processAndSaveArtifactData(ctx context.Context, data []byte) error {
return m.processArtifactErr
}
//func TestSyncHeroData_Success(t *testing.T) {
// sync := &ThirdPartyDataSync{}
// // 替换方法为mock
// sync.fetchHeroDataFromAPI = (&mockSync{}).fetchHeroDataFromAPI
// sync.processAndSaveHeroData = (&mockSync{}).processAndSaveHeroData
//
// err := sync.SyncHeroData(context.Background())
// if err != nil {
// t.Errorf("expected success, got error: %v", err)
// }
//}
//
//func TestSyncHeroData_FetchError(t *testing.T) {
// sync := &ThirdPartyDataSync{}
// sync.fetchHeroDataFromAPI = (&mockSync{fetchHeroDataErr: errors.New("fetch error")}).fetchHeroDataFromAPI
// sync.processAndSaveHeroData = (&mockSync{}).processAndSaveHeroData
//
// err := sync.SyncHeroData(context.Background())
// if err == nil || err.Error() != "fetch error" {
// t.Errorf("expected fetch error, got: %v", err)
// }
//}
//
//func TestSyncHeroData_ProcessError(t *testing.T) {
// sync := &ThirdPartyDataSync{}
// sync.fetchHeroDataFromAPI = (&mockSync{}).fetchHeroDataFromAPI
// sync.processAndSaveHeroData = (&mockSync{processHeroDataErr: errors.New("process error")}).processAndSaveHeroData
//
// err := sync.SyncHeroData(context.Background())
// if err == nil || err.Error() != "process error" {
// t.Errorf("expected process error, got: %v", err)
// }
//}
//
//func TestSyncArtifactData_Success(t *testing.T) {
// sync := &ThirdPartyDataSync{}
// sync.fetchArtifactDataFromAPI = (&mockSync{}).fetchArtifactDataFromAPI
// sync.processAndSaveArtifactData = (&mockSync{}).processAndSaveArtifactData
//
// err := sync.SyncArtifactData(context.Background())
// if err != nil {
// t.Errorf("expected success, got error: %v", err)
// }
//}
//
//func TestSyncArtifactData_FetchError(t *testing.T) {
// sync := &ThirdPartyDataSync{}
// sync.fetchArtifactDataFromAPI = (&mockSync{fetchArtifactDataErr: errors.New("fetch error")}).fetchArtifactDataFromAPI
// sync.processAndSaveArtifactData = (&mockSync{}).processAndSaveArtifactData
//
// err := sync.SyncArtifactData(context.Background())
// if err == nil || err.Error() != "fetch error" {
// t.Errorf("expected fetch error, got: %v", err)
// }
//}
//
//func TestSyncArtifactData_ProcessError(t *testing.T) {
// sync := &ThirdPartyDataSync{}
// sync.fetchArtifactDataFromAPI = (&mockSync{}).fetchArtifactDataFromAPI
// sync.processAndSaveArtifactData = (&mockSync{processArtifactErr: errors.New("process error")}).processAndSaveArtifactData
//
// err := sync.SyncArtifactData(context.Background())
// if err == nil || err.Error() != "process error" {
// t.Errorf("expected process error, got: %v", err)
// }
//}
func TestProcessAndSaveArtifactData(t *testing.T) {
sync := &ThirdPartyDataSync{}
// 构造一个合法的 JSON 数据
data := "[{\"code\":\"artifact1\"},{\"code\":\"artifact2\"}]"
if err := sync.processAndSaveArtifactData(context.Background(), data); err != nil {
t.Errorf("expected no error, got: %v", err)
}
}
func TestProcessAndSaveHeroData(t *testing.T) {
sync := &ThirdPartyDataSync{}
// 构造一个合法的 JSON 数据
data := []byte(`[{"code":"hero1"},{"code":"hero2"}]`)
if err := sync.processAndSaveHeroData(context.Background(), data); err != nil {
t.Errorf("expected no error, got: %v", err)
}
}
func TestSyncArtifactData_Success(t *testing.T) {
sync := NewThirdPartyDataSync()
err := sync.SyncArtifactData(context.Background())
if err != nil {
t.Errorf("expected success, but got error: %v", err)
}
}

View File

@@ -208,9 +208,9 @@ func (l *Logic) GetHeroDetailByCode(ctx context.Context, code string) (*v1.HeroD
for setsName, cnt := range setsNameCount {
percent := 0.0
if total > 0 {
percent = float64(cnt) * 100.0 / float64(total)
percent = float64(cnt) * 100.0 / float64(total) / 100.0 // 得到 0.156 而不是 15.6
}
percent = math.Round(percent*10) / 10 // 保留位小数
percent = math.Round(percent*1000) / 1000 // 保留位小数
percentVOList = append(percentVOList, percentVO{
SetName: setsName,
Percent: percent,
@@ -244,9 +244,9 @@ func (l *Logic) GetHeroDetailByCode(ctx context.Context, code string) (*v1.HeroD
for code, cnt := range artifactCount {
percent := 0.0
if totalArtifact > 0 {
percent = float64(cnt) * 100.0 / float64(totalArtifact)
percent = float64(cnt) * 100.0 / float64(total) / 100.0 // 得到 0.156 而不是 15.6
}
percent = math.Round(percent*10) / 10
percent = math.Round(percent*1000) / 1000 // 保留三位小数
artifactPercentVOList = append(artifactPercentVOList, artifactPercentVO{
ArtifactCode: code,
Percent: percent,

View File

@@ -6,4 +6,5 @@ package logic
import (
_ "epic/internal/logic/hero"
_ "epic/internal/logic/cron"
)

View File

@@ -0,0 +1,26 @@
package dto
// ThirdPartyArtifactDTO represents an artifact from the third-party API.
type ThirdPartyArtifactDTO struct {
ID string `json:"_id"`
Name string `json:"name"`
Code string `json:"code"`
Rarity int `json:"rarity"`
Exclusive string `json:"exclusive"`
AtkBase int `json:"atk_base"`
AtkMax int `json:"atk_max"`
HPBase int `json:"hp_base"`
HPMax int `json:"hp_max"`
SkillDesc string `json:"skill_desc"`
SkillDescMax string `json:"skill_desc_max"`
}
// ThirdPartyHeroDTO represents a hero from the third-party API.
// Note: This is a placeholder structure. Adjust it according to the actual API response.
type ThirdPartyHeroDTO struct {
Code string `json:"code"`
Name string `json:"name"`
Rarity int `json:"rarity"`
Attribute string `json:"attribute"`
Role string `json:"role"`
}

38
internal/service/cron.go Normal file
View File

@@ -0,0 +1,38 @@
package service
import (
"context"
)
// CronService 定义了定时任务相关的业务接口
type CronService interface {
// StartAllJobs 启动所有定时任务
StartAllJobs(ctx context.Context) error
// StopAllJobs 停止所有定时任务
StopAllJobs(ctx context.Context) error
// AddJob 添加定时任务
AddJob(ctx context.Context, name, cron string, job func()) error
// RemoveJob 移除定时任务
RemoveJob(ctx context.Context, name string) error
// GetJobStatus 获取任务状态
GetJobStatus(ctx context.Context, name string) (bool, error)
}
var cronService CronService
// Cron 返回 CronService 的实例
func Cron() CronService {
if cronService == nil {
panic("implement not found for interface CronService")
}
return cronService
}
// SetCron 注册 CronService 实现
func SetCron(s CronService) {
cronService = s
}