feat: 添加异步数据入库任务系统,支持全量/增量更新与实时进度展示
新增完整的异步任务管理体系: 1. 新增Redis缓存层实现任务状态实时存储与降级机制 2. 实现并发拉取+批量写入的高性能入库流程 3. 添加任务管理HTTP接口与路由 4. 重构行情控制器使用共享SQLite存储 5. 配套添加Docker Compose Redis启动配置 6. 前端新增任务管理面板支持操作与进度查看
This commit is contained in:
74
internal/cache/cache.go
vendored
Normal file
74
internal/cache/cache.go
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
// Package cache 定义任务状态缓存接口,支持 Redis 实时状态 + 内存降级。
|
||||
// Redis 不可用时降级到 NoopCache,任务系统仍可运行(仅失去实时状态能力,回退到 SQLite)。
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TaskType 任务类型。
|
||||
type TaskType string
|
||||
|
||||
const (
|
||||
TaskTypeFull TaskType = "full" // 全量入库
|
||||
TaskTypeIncremental TaskType = "incremental" // 增量同步
|
||||
)
|
||||
|
||||
// TaskStatus 任务状态。
|
||||
type TaskStatus string
|
||||
|
||||
const (
|
||||
TaskStatusPending TaskStatus = "pending" // 已创建未启动
|
||||
TaskStatusRunning TaskStatus = "running" // 执行中
|
||||
TaskStatusSuccess TaskStatus = "success" // 成功
|
||||
TaskStatusFailed TaskStatus = "failed" // 失败
|
||||
TaskStatusCancelled TaskStatus = "cancelled" // 已取消
|
||||
)
|
||||
|
||||
// Task 任务运行态快照,Redis 实时状态与 SQLite 持久化共用同一结构。
|
||||
type Task struct {
|
||||
ID string `json:"id"`
|
||||
Type TaskType `json:"type"`
|
||||
Status TaskStatus `json:"status"`
|
||||
Total int `json:"total"`
|
||||
Done int `json:"done"`
|
||||
Success int `json:"success"`
|
||||
Failed int `json:"failed"`
|
||||
FailedCodes []string `json:"failedCodes,omitempty"`
|
||||
Params string `json:"params"`
|
||||
Error string `json:"error,omitempty"`
|
||||
StartedAt string `json:"startedAt"`
|
||||
FinishedAt string `json:"finishedAt,omitempty"`
|
||||
}
|
||||
|
||||
// Cache 任务状态缓存接口。
|
||||
// Redis 实现用于实时状态查询;NoopCache 用于 Redis 不可用时降级。
|
||||
type Cache interface {
|
||||
// GetTask 读取任务状态,不存在返回 nil,nil。
|
||||
GetTask(ctx context.Context, id string) (*Task, error)
|
||||
// SetTask 写入任务状态,ttl 为过期时间。
|
||||
SetTask(ctx context.Context, t *Task, ttl time.Duration) error
|
||||
// DeleteTask 删除任务状态。
|
||||
DeleteTask(ctx context.Context, id string) error
|
||||
// ListActiveTaskIDs 返回运行中任务的 ID 列表。
|
||||
ListActiveTaskIDs(ctx context.Context) ([]string, error)
|
||||
// AddActiveTask 将任务 ID 加入活跃集合。
|
||||
AddActiveTask(ctx context.Context, id string) error
|
||||
// RemoveActiveTask 从活跃集合移除任务 ID。
|
||||
RemoveActiveTask(ctx context.Context, id string) error
|
||||
// Ping 探测缓存可用性。
|
||||
Ping(ctx context.Context) error
|
||||
}
|
||||
|
||||
// NoopCache Cache 的空实现,Redis 不可用时降级使用。
|
||||
// 所有操作均为空操作,任务状态仅依赖 SQLite 持久化。
|
||||
type NoopCache struct{}
|
||||
|
||||
func (NoopCache) GetTask(ctx context.Context, id string) (*Task, error) { return nil, nil }
|
||||
func (NoopCache) SetTask(ctx context.Context, t *Task, ttl time.Duration) error { return nil }
|
||||
func (NoopCache) DeleteTask(ctx context.Context, id string) error { return nil }
|
||||
func (NoopCache) ListActiveTaskIDs(ctx context.Context) ([]string, error) { return nil, nil }
|
||||
func (NoopCache) AddActiveTask(ctx context.Context, id string) error { return nil }
|
||||
func (NoopCache) RemoveActiveTask(ctx context.Context, id string) error { return nil }
|
||||
func (NoopCache) Ping(ctx context.Context) error { return nil }
|
||||
107
internal/cache/redis.go
vendored
Normal file
107
internal/cache/redis.go
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// keyPrefix Redis 键前缀,避免与其他应用冲突。
|
||||
const keyPrefix = "stock-analysis:"
|
||||
|
||||
// taskKey 构造任务状态键。
|
||||
func taskKey(id string) string { return keyPrefix + "task:" + id }
|
||||
|
||||
// activeSetKey 构造活跃任务集合键。
|
||||
const activeSetKey = keyPrefix + "tasks:active"
|
||||
|
||||
// RedisCache 基于 go-redis/v9 的 Cache 实现。
|
||||
type RedisCache struct {
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
// NewRedisCache 创建 Redis 缓存实例。
|
||||
// addr 格式 "host:port",password 为空表示无密码,db 为 Redis 数据库编号。
|
||||
// 配置较短的超时和重试次数,Redis 不可用时快速降级到 NoopCache。
|
||||
func NewRedisCache(addr, password string, db int) (*RedisCache, error) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: addr,
|
||||
Password: password,
|
||||
DB: db,
|
||||
DialTimeout: 2 * time.Second,
|
||||
ReadTimeout: 2 * time.Second,
|
||||
WriteTimeout: 2 * time.Second,
|
||||
MaxRetries: 1,
|
||||
PoolSize: 10,
|
||||
})
|
||||
return &RedisCache{client: client}, nil
|
||||
}
|
||||
|
||||
// GetTask 从 Redis 读取任务状态(JSON 反序列化)。
|
||||
func (c *RedisCache) GetTask(ctx context.Context, id string) (*Task, error) {
|
||||
data, err := c.client.Get(ctx, taskKey(id)).Bytes()
|
||||
if err != nil {
|
||||
if err == redis.Nil {
|
||||
return nil, nil // 不存在
|
||||
}
|
||||
return nil, fmt.Errorf("redis get task %s: %w", id, err)
|
||||
}
|
||||
var t Task
|
||||
if err := json.Unmarshal(data, &t); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal task %s: %w", id, err)
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
// SetTask 写入任务状态(JSON 序列化),设置 TTL 防止过期数据残留。
|
||||
func (c *RedisCache) SetTask(ctx context.Context, t *Task, ttl time.Duration) error {
|
||||
data, err := json.Marshal(t)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal task %s: %w", t.ID, err)
|
||||
}
|
||||
if err := c.client.Set(ctx, taskKey(t.ID), data, ttl).Err(); err != nil {
|
||||
return fmt.Errorf("redis set task %s: %w", t.ID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteTask 删除任务状态。
|
||||
func (c *RedisCache) DeleteTask(ctx context.Context, id string) error {
|
||||
if err := c.client.Del(ctx, taskKey(id)).Err(); err != nil {
|
||||
return fmt.Errorf("redis del task %s: %w", id, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListActiveTaskIDs 返回活跃任务集合中的所有 ID。
|
||||
func (c *RedisCache) ListActiveTaskIDs(ctx context.Context) ([]string, error) {
|
||||
ids, err := c.client.SMembers(ctx, activeSetKey).Result()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("redis smembers: %w", err)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// AddActiveTask 将任务 ID 加入活跃集合。
|
||||
func (c *RedisCache) AddActiveTask(ctx context.Context, id string) error {
|
||||
if err := c.client.SAdd(ctx, activeSetKey, id).Err(); err != nil {
|
||||
return fmt.Errorf("redis sadd %s: %w", id, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveActiveTask 从活跃集合移除任务 ID。
|
||||
func (c *RedisCache) RemoveActiveTask(ctx context.Context, id string) error {
|
||||
if err := c.client.SRem(ctx, activeSetKey, id).Err(); err != nil {
|
||||
return fmt.Errorf("redis srem %s: %w", id, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ping 探测 Redis 连通性。
|
||||
func (c *RedisCache) Ping(ctx context.Context) error {
|
||||
return c.client.Ping(ctx).Err()
|
||||
}
|
||||
Reference in New Issue
Block a user