Files
WarrenZhou 273fc90a95 feat: 添加异步数据入库任务系统,支持全量/增量更新与实时进度展示
新增完整的异步任务管理体系:
1. 新增Redis缓存层实现任务状态实时存储与降级机制
2. 实现并发拉取+批量写入的高性能入库流程
3. 添加任务管理HTTP接口与路由
4. 重构行情控制器使用共享SQLite存储
5. 配套添加Docker Compose Redis启动配置
6. 前端新增任务管理面板支持操作与进度查看
2026-08-18 10:09:52 +08:00

108 lines
3.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}