diff --git a/README.MD b/README.MD index d36cedd..0c01f98 100644 --- a/README.MD +++ b/README.MD @@ -1,4 +1,219 @@ -# GoFrame Template For SingleRepo +# Epic Game Web Service -Quick Start: -- https://goframe.org/quick \ No newline at end of file +基于GoFrame框架开发的游戏数据管理Web服务,提供英雄信息管理、数据同步等功能。 + +## 功能特性 + +- 🎮 英雄信息管理 +- 🔄 定时任务调度 +- 🌐 第三方数据同步 +- 💾 数据缓存管理 +- 📊 健康检查监控 +- 🔧 RESTful API接口 + +## 技术栈 + +- **框架**: GoFrame v2.9.0 +- **数据库**: MySQL + Redis +- **定时任务**: gcron +- **API文档**: Swagger +- **配置管理**: YAML + +## 快速开始 + +### 环境要求 + +- Go 1.22+ +- MySQL 5.7+ +- Redis 6.0+ + +### 安装依赖 + +```bash +go mod tidy +``` + +### 配置数据库 + +编辑 `manifest/config/config.yaml` 文件,配置数据库连接信息: + +```yaml +database: + default: + link: "mysql:username:password@tcp(host:port)/database" + debug: true + +redis: + default: + address: "host:port" + db: 1 + pass: "password" +``` + +### 启动服务 + +#### Linux/Mac +```bash +chmod +x scripts/start.sh +./scripts/start.sh +``` + +#### Windows +```cmd +scripts\start.bat +``` + +#### 手动启动 +```bash +go run main.go +``` + +### 访问服务 + +- **API服务**: http://localhost:8283 +- **Swagger文档**: http://localhost:8283/swagger +- **API文档**: http://localhost:8283/api.json + +## 定时任务 + +项目集成了完整的定时任务管理功能,支持: + +- 自动启动和停止 +- Cron表达式配置 +- 任务执行日志 +- RESTful API管理 +- 优雅关闭支持 + +### 默认任务 + +| 任务名称 | 执行频率 | 功能描述 | +|---------|---------|---------| +| data_sync_hourly | 每小时 | 从第三方网站同步数据 | +| data_cleanup_daily | 每天凌晨2点 | 清理过期数据 | +| health_check | 每5分钟 | 系统健康检查 | +| cache_refresh | 每30分钟 | 刷新系统缓存 | +| hero_sync_daily | 每天凌晨3点 | 同步英雄数据 | +| artifact_sync_daily | 每天凌晨4点 | 同步神器数据 | + +### 管理API + +```bash +# 获取任务列表 +GET /cron/jobs + +# 添加新任务 +POST /cron/jobs +{ + "name": "custom_job", + "cron": "0 12 * * *" +} + +# 移除任务 +DELETE /cron/jobs/{name} + +# 获取任务状态 +GET /cron/jobs/{name}/status +``` + +详细文档请参考:[定时任务使用指南](docs/cron.md) + +## API接口 + +### 英雄管理 + +- `GET /getOne` - 获取单个英雄信息 +- `GET /app-api/epic/hero/list-all` - 获取所有英雄列表 +- `GET /app-api/epic/hero/hero-detail` - 获取英雄详细信息 + +### 定时任务管理 + +- `GET /cron/jobs` - 获取任务列表 +- `POST /cron/jobs` - 添加新任务 +- `DELETE /cron/jobs/{name}` - 移除任务 +- `GET /cron/jobs/{name}/status` - 获取任务状态 + +## 项目结构 + +``` +epic/ +├── api/ # API接口定义 +│ ├── hero/ # 英雄相关API +│ └── cron/ # 定时任务API +├── internal/ # 内部业务逻辑 +│ ├── cmd/ # 命令行入口 +│ ├── controller/ # 控制器层 +│ ├── service/ # 服务接口层 +│ ├── logic/ # 业务逻辑实现 +│ ├── dao/ # 数据访问层 +│ └── model/ # 数据模型层 +├── manifest/ # 配置文件 +│ └── config/ # 应用配置 +├── utility/ # 工具类 +├── scripts/ # 启动脚本 +├── docs/ # 文档 +└── main.go # 程序入口 +``` + +## 开发指南 + +### 添加新的定时任务 + +1. 在 `internal/logic/cron/cron.go` 的 `registerDefaultJobs` 方法中添加任务 +2. 实现具体的任务逻辑 +3. 配置Cron表达式 +4. 重启服务 + +### 添加新的API接口 + +1. 在 `api/` 目录下定义接口结构 +2. 在 `internal/controller/` 下实现控制器 +3. 在 `internal/service/` 下定义服务接口 +4. 在 `internal/logic/` 下实现业务逻辑 +5. 在 `internal/cmd/cmd.go` 中注册路由 + +## 部署 + +### Docker部署 + +```bash +# 构建镜像 +docker build -t epic-game-service . + +# 运行容器 +docker run -d -p 8283:8283 epic-game-service +``` + +### 系统服务 + +创建systemd服务文件: + +```ini +[Unit] +Description=Epic Game Web Service +After=network.target + +[Service] +Type=simple +User=epic +WorkingDirectory=/opt/epic +ExecStart=/opt/epic/main.exe +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +## 监控和日志 + +- 应用日志:查看控制台输出或日志文件 +- 健康检查:访问 `/health` 接口 +- 任务状态:通过API接口查询 + +## 贡献 + +欢迎提交Issue和Pull Request! + +## 许可证 + +MIT License \ No newline at end of file diff --git a/api/cron/v1/cron.go b/api/cron/v1/cron.go new file mode 100644 index 0000000..c2f64f5 --- /dev/null +++ b/api/cron/v1/cron.go @@ -0,0 +1,60 @@ +package v1 + +import ( + "github.com/gogf/gf/v2/frame/g" +) + +// GetJobListReq 获取任务列表请求 +type GetJobListReq struct { + g.Meta `path:"/cron/jobs" method:"get" tags:"Cron" summary:"Get all cron jobs"` +} + +// GetJobListRes 获取任务列表响应 +type GetJobListRes struct { + Jobs []*JobInfo `json:"jobs" dc:"任务列表"` +} + +// JobInfo 任务信息 +type JobInfo struct { + Name string `json:"name" dc:"任务名称"` + Cron string `json:"cron" dc:"Cron表达式"` + Status bool `json:"status" dc:"任务状态"` + NextTime string `json:"nextTime" dc:"下次执行时间"` +} + +// AddJobReq 添加任务请求 +type AddJobReq struct { + g.Meta `path:"/cron/jobs" method:"post" tags:"Cron" summary:"Add a new cron job"` + Name string `json:"name" v:"required" dc:"任务名称"` + Cron string `json:"cron" v:"required" dc:"Cron表达式"` +} + +// AddJobRes 添加任务响应 +type AddJobRes struct { + Success bool `json:"success" dc:"是否成功"` + Message string `json:"message" dc:"响应消息"` +} + +// RemoveJobReq 移除任务请求 +type RemoveJobReq struct { + g.Meta `path:"/cron/jobs/{name}" method:"delete" tags:"Cron" summary:"Remove a cron job"` + Name string `json:"name" v:"required" dc:"任务名称"` +} + +// RemoveJobRes 移除任务响应 +type RemoveJobRes struct { + Success bool `json:"success" dc:"是否成功"` + Message string `json:"message" dc:"响应消息"` +} + +// GetJobStatusReq 获取任务状态请求 +type GetJobStatusReq struct { + g.Meta `path:"/cron/jobs/{name}/status" method:"get" tags:"Cron" summary:"Get job status"` + Name string `json:"name" v:"required" dc:"任务名称"` +} + +// GetJobStatusRes 获取任务状态响应 +type GetJobStatusRes struct { + Name string `json:"name" dc:"任务名称"` + Status bool `json:"status" dc:"任务状态"` +} \ No newline at end of file diff --git a/docs/cron.md b/docs/cron.md new file mode 100644 index 0000000..20fb292 --- /dev/null +++ b/docs/cron.md @@ -0,0 +1,207 @@ +# 定时任务模块使用指南 + +## 概述 + +本项目集成了GoFrame的gcron模块,提供了完整的定时任务管理功能。定时任务会在项目启动时自动启动,并在项目关闭时优雅停止。 + +## 功能特性 + +- ✅ 自动启动和停止定时任务 +- ✅ 支持Cron表达式配置 +- ✅ 任务执行日志记录 +- ✅ 优雅关闭支持 +- ✅ RESTful API管理接口 +- ✅ 配置文件管理 +- ✅ 任务状态监控 + +## 默认任务 + +项目启动时会自动注册以下默认任务: + +### 1. 数据同步任务 (data_sync_hourly) +- **执行频率**: 每小时执行一次 +- **Cron表达式**: `0 * * * *` +- **功能**: 从第三方网站同步数据到数据库 + +### 2. 数据清理任务 (data_cleanup_daily) +- **执行频率**: 每天凌晨2点执行 +- **Cron表达式**: `0 2 * * *` +- **功能**: 清理过期的缓存数据和日志记录 + +### 3. 健康检查任务 (health_check) +- **执行频率**: 每5分钟执行一次 +- **Cron表达式**: `*/5 * * * *` +- **功能**: 检查系统健康状态 + +### 4. 缓存刷新任务 (cache_refresh) +- **执行频率**: 每30分钟执行一次 +- **Cron表达式**: `*/30 * * * *` +- **功能**: 刷新系统缓存 + +## API接口 + +### 获取任务列表 +```http +GET /cron/jobs +``` + +### 添加新任务 +```http +POST /cron/jobs +Content-Type: application/json + +{ + "name": "custom_job", + "cron": "0 12 * * *" +} +``` + +### 移除任务 +```http +DELETE /cron/jobs/{name} +``` + +### 获取任务状态 +```http +GET /cron/jobs/{name}/status +``` + +## Cron表达式说明 + +Cron表达式格式:`秒 分 时 日 月 周` + +### 常用示例 +- `0 * * * *` - 每小时执行 +- `0 0 * * *` - 每天零点执行 +- `0 0 0 * *` - 每月1号零点执行 +- `0 0 0 * * 0` - 每周日零点执行 +- `*/5 * * * *` - 每5分钟执行 +- `0 0 12 * * 1-5` - 工作日中午12点执行 + +## 配置文件 + +定时任务配置位于 `manifest/config/cron.yaml`: + +```yaml +cron: + enabled: true + jobs: + data_sync_hourly: + enabled: true + cron: "0 * * * *" + description: "每小时从第三方网站同步数据" + execution: + max_concurrent: 10 + timeout: 300 + retry_count: 3 + retry_interval: 60 +``` + +## 添加自定义任务 + +### 1. 在代码中添加任务 + +```go +// 在 internal/logic/cron/cron.go 的 registerDefaultJobs 方法中添加 +if err := l.AddJob(ctx, "my_custom_job", "0 8 * * *", func() { + // 你的任务逻辑 + l.myCustomTask(ctx) +}); err != nil { + return err +} +``` + +### 2. 实现任务逻辑 + +```go +func (l *Logic) myCustomTask(ctx context.Context) { + g.Log().Info(ctx, "执行自定义任务...") + + // 1. 调用第三方API + // 2. 处理数据 + // 3. 更新数据库 + // 4. 记录日志 +} +``` + +### 3. 通过API动态添加 + +```bash +curl -X POST http://localhost:8283/cron/jobs \ + -H "Content-Type: application/json" \ + -d '{ + "name": "dynamic_job", + "cron": "0 9 * * *" + }' +``` + +## 监控和日志 + +### 日志查看 +定时任务的执行日志会记录在应用日志中,包含: +- 任务开始时间 +- 任务执行时长 +- 任务完成状态 +- 错误信息(如果有) + +### 日志示例 +``` +2024-01-01 10:00:00 [INFO] Starting job: data_sync_hourly at 2024-01-01 10:00:00 +2024-01-01 10:00:05 [INFO] Completed job: data_sync_hourly, duration: 5.2s +``` + +## 最佳实践 + +### 1. 任务设计原则 +- 任务应该是幂等的(可重复执行) +- 避免长时间运行的任务 +- 合理设置超时时间 +- 添加适当的错误处理 + +### 2. 性能优化 +- 使用连接池 +- 批量处理数据 +- 合理使用缓存 +- 避免在任务中执行耗时操作 + +### 3. 监控建议 +- 定期检查任务执行状态 +- 监控任务执行时间 +- 设置告警机制 +- 保留执行日志 + +## 故障排除 + +### 常见问题 + +1. **任务未执行** + - 检查Cron表达式是否正确 + - 确认任务是否已注册 + - 查看应用日志 + +2. **任务执行失败** + - 检查网络连接 + - 验证API接口可用性 + - 查看错误日志 + +3. **任务执行超时** + - 优化任务逻辑 + - 增加超时时间 + - 考虑异步处理 + +### 调试方法 + +1. 启用调试日志 +2. 使用API接口检查任务状态 +3. 查看系统资源使用情况 +4. 检查数据库连接状态 + +## 扩展功能 + +后续可以扩展的功能: +- 任务执行历史记录 +- 任务依赖关系管理 +- 分布式任务调度 +- 任务执行统计 +- Web界面管理 +- 邮件/短信告警 \ No newline at end of file diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index ca571e8..de87696 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -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) + }() +} diff --git a/internal/controller/cron/cron.go b/internal/controller/cron/cron.go new file mode 100644 index 0000000..2ccc669 --- /dev/null +++ b/internal/controller/cron/cron.go @@ -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 +} \ No newline at end of file diff --git a/internal/logic/cron/cron.go b/internal/logic/cron/cron.go new file mode 100644 index 0000000..7205e0a --- /dev/null +++ b/internal/logic/cron/cron.go @@ -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") +} diff --git a/internal/logic/cron/cron_test.go b/internal/logic/cron/cron_test.go new file mode 100644 index 0000000..5c14661 --- /dev/null +++ b/internal/logic/cron/cron_test.go @@ -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) + } +} \ No newline at end of file diff --git a/internal/logic/cron/third_party_sync.go b/internal/logic/cron/third_party_sync.go new file mode 100644 index 0000000..2647c13 --- /dev/null +++ b/internal/logic/cron/third_party_sync.go @@ -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 +} diff --git a/internal/logic/cron/third_party_sync_test.go b/internal/logic/cron/third_party_sync_test.go new file mode 100644 index 0000000..4cb2d5e --- /dev/null +++ b/internal/logic/cron/third_party_sync_test.go @@ -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) + } +} diff --git a/internal/logic/hero/hero.go b/internal/logic/hero/hero.go index 8dde182..0da5d87 100644 --- a/internal/logic/hero/hero.go +++ b/internal/logic/hero/hero.go @@ -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, diff --git a/internal/logic/logic.go b/internal/logic/logic.go index 59db98c..372c321 100644 --- a/internal/logic/logic.go +++ b/internal/logic/logic.go @@ -6,4 +6,5 @@ package logic import ( _ "epic/internal/logic/hero" + _ "epic/internal/logic/cron" ) diff --git a/internal/model/dto/third_party.go b/internal/model/dto/third_party.go new file mode 100644 index 0000000..993e881 --- /dev/null +++ b/internal/model/dto/third_party.go @@ -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"` +} \ No newline at end of file diff --git a/internal/service/cron.go b/internal/service/cron.go new file mode 100644 index 0000000..7779a98 --- /dev/null +++ b/internal/service/cron.go @@ -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 +} \ No newline at end of file diff --git a/manifest/config/cron.yaml b/manifest/config/cron.yaml new file mode 100644 index 0000000..2fe8a8d --- /dev/null +++ b/manifest/config/cron.yaml @@ -0,0 +1,44 @@ +# 定时任务配置 +cron: + # 是否启用定时任务 + enabled: true + + # 默认任务配置 + jobs: + # 数据同步任务 - 每小时执行一次 + data_sync_hourly: + enabled: true + cron: "0 * * * *" + description: "每小时从第三方网站同步数据" + + # 数据清理任务 - 每天凌晨2点执行 + data_cleanup_daily: + enabled: true + cron: "0 2 * * *" + description: "每天清理过期数据" + + # 健康检查任务 - 每5分钟执行一次 + health_check: + enabled: true + cron: "*/5 * * * *" + description: "系统健康检查" + + # 缓存刷新任务 - 每30分钟执行一次 + cache_refresh: + enabled: true + cron: "*/30 * * * *" + description: "刷新系统缓存" + + # 任务执行配置 + execution: + # 最大并发任务数 + max_concurrent: 10 + + # 任务超时时间(秒) + timeout: 300 + + # 失败重试次数 + retry_count: 3 + + # 重试间隔(秒) + retry_interval: 60 \ No newline at end of file diff --git a/scripts/start.bat b/scripts/start.bat new file mode 100644 index 0000000..caafeda --- /dev/null +++ b/scripts/start.bat @@ -0,0 +1,49 @@ +@echo off +chcp 65001 >nul + +echo ========================================== +echo Epic Game Web Service +echo Starting with cron jobs enabled... +echo ========================================== + +REM 检查Go环境 +go version >nul 2>&1 +if errorlevel 1 ( + echo Error: Go is not installed or not in PATH + pause + exit /b 1 +) + +REM 检查Go版本 +for /f "tokens=3" %%i in ('go version') do set GO_VERSION=%%i +echo Go version: %GO_VERSION% + +REM 设置环境变量 +set GO_ENV=production +set GF_GCFG_FILE=manifest/config/config.yaml + +REM 清理旧的构建文件 +echo Cleaning old build files... +if exist main.exe del main.exe +if exist main del main + +REM 构建项目 +echo Building project... +go build -o main.exe . + +if errorlevel 1 ( + echo Error: Build failed + pause + exit /b 1 +) + +echo Build successful! + +REM 启动服务 +echo Starting server on port 8283... +echo Cron jobs will be started automatically... +echo Press Ctrl+C to stop the server +echo ========================================== + +REM 运行服务 +main.exe \ No newline at end of file diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100644 index 0000000..25e9d80 --- /dev/null +++ b/scripts/start.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# Epic Game Web Service 启动脚本 + +echo "==========================================" +echo "Epic Game Web Service" +echo "Starting with cron jobs enabled..." +echo "==========================================" + +# 检查Go环境 +if ! command -v go &> /dev/null; then + echo "Error: Go is not installed or not in PATH" + exit 1 +fi + +# 检查Go版本 +GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//') +echo "Go version: $GO_VERSION" + +# 设置环境变量 +export GO_ENV=production +export GF_GCFG_FILE=manifest/config/config.yaml + +# 清理旧的构建文件 +echo "Cleaning old build files..." +rm -f main.exe +rm -f main + +# 构建项目 +echo "Building project..." +go build -o main.exe . + +if [ $? -ne 0 ]; then + echo "Error: Build failed" + exit 1 +fi + +echo "Build successful!" + +# 启动服务 +echo "Starting server on port 8283..." +echo "Cron jobs will be started automatically..." +echo "Press Ctrl+C to stop the server" +echo "==========================================" + +# 运行服务 +./main.exe \ No newline at end of file