feat: 添加异步数据入库任务系统,支持全量/增量更新与实时进度展示

新增完整的异步任务管理体系:
1. 新增Redis缓存层实现任务状态实时存储与降级机制
2. 实现并发拉取+批量写入的高性能入库流程
3. 添加任务管理HTTP接口与路由
4. 重构行情控制器使用共享SQLite存储
5. 配套添加Docker Compose Redis启动配置
6. 前端新增任务管理面板支持操作与进度查看
This commit is contained in:
2026-08-18 10:09:52 +08:00
parent e28bfec957
commit 273fc90a95
18 changed files with 1682 additions and 80 deletions

107
internal/cache/redis.go vendored Normal file
View 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()
}