feat: 添加异步数据入库任务系统,支持全量/增量更新与实时进度展示
新增完整的异步任务管理体系: 1. 新增Redis缓存层实现任务状态实时存储与降级机制 2. 实现并发拉取+批量写入的高性能入库流程 3. 添加任务管理HTTP接口与路由 4. 重构行情控制器使用共享SQLite存储 5. 配套添加Docker Compose Redis启动配置 6. 前端新增任务管理面板支持操作与进度查看
This commit is contained in:
@@ -12,10 +12,12 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"stock-analysis/internal/cache"
|
||||
"stock-analysis/internal/controller"
|
||||
"stock-analysis/internal/datasource/eastmoney"
|
||||
"stock-analysis/internal/datasource/fallback"
|
||||
"stock-analysis/internal/datasource/tencent"
|
||||
"stock-analysis/internal/job"
|
||||
"stock-analysis/internal/router"
|
||||
"stock-analysis/internal/service"
|
||||
"stock-analysis/internal/storage/sqlite"
|
||||
@@ -29,23 +31,43 @@ func main() {
|
||||
webDir := "web"
|
||||
dbPath := defaultDBPath()
|
||||
|
||||
// 东财为主数据源(实时行情+历史K线),腾讯为历史K线备用源(东财失败时自动切换)
|
||||
ds := fallback.New(eastmoney.NewClient(), tencent.NewClient())
|
||||
svc := service.NewMarketService(ds)
|
||||
mc := controller.NewMarketController(svc, dbPath)
|
||||
|
||||
// 1. 共享 SQLite 存储(MarketController / AuthController / job.Manager 共用)。
|
||||
store, err := sqlite.Open(dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
// 2. 行情数据源:东财为主,腾讯为备用(实时行情 + 历史K线,默认 2s 节流)。
|
||||
marketEM := eastmoney.NewClient()
|
||||
marketDS := fallback.New(marketEM, tencent.NewClient())
|
||||
svc := service.NewMarketService(marketDS)
|
||||
mc := controller.NewMarketController(svc, store)
|
||||
|
||||
// 3. 认证服务。
|
||||
authSvc := service.NewAuthService(store)
|
||||
if err := authSvc.EnsureAdmin(context.Background()); err != nil {
|
||||
log.Printf("ensure admin: %v", err)
|
||||
}
|
||||
ac := controller.NewAuthController(authSvc)
|
||||
|
||||
handler := router.New(mc, ac, webDir)
|
||||
// 4. Redis 缓存:任务实时状态存储。Redis 不可用时降级到 NoopCache,任务仍可运行(回退 SQLite)。
|
||||
taskCache := initCache()
|
||||
|
||||
// 5. 任务数据源:批量入库场景禁用 per-client 节流,由 job.Manager 全局限速器统一控制速率。
|
||||
jobEM := eastmoney.NewClientWithThrottle(0)
|
||||
jobDS := fallback.New(jobEM, tencent.NewClient())
|
||||
mgr := job.NewManager(store, taskCache, jobDS, jobEM, job.Config{
|
||||
Workers: envInt("JOB_WORKERS", 8),
|
||||
BatchSize: envInt("JOB_BATCH_SIZE", 20),
|
||||
RateInterval: 400 * time.Millisecond,
|
||||
RateBurst: 4,
|
||||
})
|
||||
defer mgr.StopAll()
|
||||
tc := controller.NewTaskController(mgr)
|
||||
|
||||
// 6. 路由装配。
|
||||
handler := router.New(mc, ac, tc, webDir)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
@@ -73,6 +95,52 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// initCache 初始化 Redis 缓存,连接失败时降级到 NoopCache。
|
||||
// Redis 地址通过 REDIS_ADDR 环境变量配置(默认 localhost:6379),密码通过 REDIS_PASSWORD 配置。
|
||||
func initCache() cache.Cache {
|
||||
addr := os.Getenv("REDIS_ADDR")
|
||||
if addr == "" {
|
||||
addr = "localhost:6379"
|
||||
}
|
||||
password := os.Getenv("REDIS_PASSWORD")
|
||||
|
||||
rc, err := cache.NewRedisCache(addr, password, 0)
|
||||
if err != nil {
|
||||
log.Printf("[cache] redis create failed (%s), using noop cache: %v", addr, err)
|
||||
return cache.NoopCache{}
|
||||
}
|
||||
pingCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := rc.Ping(pingCtx); err != nil {
|
||||
log.Printf("[cache] redis ping failed (%s), using noop cache: %v", addr, err)
|
||||
return cache.NoopCache{}
|
||||
}
|
||||
log.Printf("[cache] redis connected: %s", addr)
|
||||
return rc
|
||||
}
|
||||
|
||||
// envInt 读取环境变量整数值,缺失或无效时返回默认值。
|
||||
func envInt(key string, def int) int {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if n, err := atoi(v); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// atoi 简易字符串转整数(避免引入 strconv 仅为此一处)。
|
||||
func atoi(s string) (int, error) {
|
||||
n := 0
|
||||
for _, c := range s {
|
||||
if c < '0' || c > '9' {
|
||||
return 0, errors.New("invalid int")
|
||||
}
|
||||
n = n*10 + int(c-'0')
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// defaultDBPath 返回默认数据库路径。
|
||||
// 默认使用项目目录下的 stock.db,避免 OneDrive/AppData 的 I/O 限制。
|
||||
func defaultDBPath() string {
|
||||
|
||||
Reference in New Issue
Block a user