feat(cron): 实现定时任务管理功能
- 新增 cron模块,支持定时任务管理- 实现了任务列表获取、任务添加、任务移除和任务状态获取等接口 - 添加了默认任务,包括数据同步、数据清理、健康检查和缓存刷新等 - 实现了优雅关闭功能,确保在服务停止时正确停止所有任务 - 添加了定时任务相关文档和使用指南
This commit is contained in:
221
README.MD
221
README.MD
@@ -1,4 +1,219 @@
|
||||
# GoFrame Template For SingleRepo
|
||||
# Epic Game Web Service
|
||||
|
||||
Quick Start:
|
||||
- https://goframe.org/quick
|
||||
基于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
|
||||
60
api/cron/v1/cron.go
Normal file
60
api/cron/v1/cron.go
Normal file
@@ -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:"任务状态"`
|
||||
}
|
||||
207
docs/cron.md
Normal file
207
docs/cron.md
Normal file
@@ -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界面管理
|
||||
- 邮件/短信告警
|
||||
@@ -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)
|
||||
}()
|
||||
}
|
||||
|
||||
98
internal/controller/cron/cron.go
Normal file
98
internal/controller/cron/cron.go
Normal 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
246
internal/logic/cron/cron.go
Normal 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")
|
||||
}
|
||||
114
internal/logic/cron/cron_test.go
Normal file
114
internal/logic/cron/cron_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
238
internal/logic/cron/third_party_sync.go
Normal file
238
internal/logic/cron/third_party_sync.go
Normal 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
|
||||
}
|
||||
145
internal/logic/cron/third_party_sync_test.go
Normal file
145
internal/logic/cron/third_party_sync_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -6,4 +6,5 @@ package logic
|
||||
|
||||
import (
|
||||
_ "epic/internal/logic/hero"
|
||||
_ "epic/internal/logic/cron"
|
||||
)
|
||||
|
||||
26
internal/model/dto/third_party.go
Normal file
26
internal/model/dto/third_party.go
Normal 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
38
internal/service/cron.go
Normal 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
|
||||
}
|
||||
44
manifest/config/cron.yaml
Normal file
44
manifest/config/cron.yaml
Normal file
@@ -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
|
||||
49
scripts/start.bat
Normal file
49
scripts/start.bat
Normal file
@@ -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
|
||||
47
scripts/start.sh
Normal file
47
scripts/start.sh
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user