--- comet_change: analysis-storage-refactor role: technical-design canonical_spec: openspec --- # Analysis Storage Refactor - Technical Design ## 1. 架构概览 ### 1.1 目标架构 ``` ┌─────────────────────────────────────────────────────────────┐ │ 应用层 │ │ app/api/futures_analysis.py ←→ app/services/cache.py │ │ ↓ │ │ StorageManager │ │ (storage_manager.py) │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────┼─────────────────────┐ ↓ ↓ ↓ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Redis │ │ MySQL │ │ SQLite │ │ (缓存) │ │(持久化) │ │ (兜底) │ └─────────┘ └─────────┘ └─────────┘ ``` ### 1.2 核心组件 | 组件 | 职责 | |------|------| | `StorageManager` | 封装三级存储逻辑,提供统一读写接口 | | `analysis_db.py` | Analysis 数据库连接管理,降级逻辑 | | `analysis_models.py` | ORM 模型定义(不变) | | `analysis_migration.py` | SQLite → MySQL 数据迁移 | ### 1.3 表清单 | 表名 | 用途 | 缓存 TTL | |------|------|----------| | `futures_analysis` | 期货分析报告 | 5 分钟 | | `watched_symbols` | 用户关注品种 | 10 分钟 | | `ai_model_configs` | AI 模型配置 | 10 分钟 | | `analysis_settings` | 分析设置 | 10 分钟 | | `ai_analysis_cache` | AI 分析缓存 | 5 分钟 | | `review_dates` | 复盘日期 | 1 小时 | | `symbol_rankings` | 品种排名 | 1 小时 | | `trading_plans` | 交易计划 | 1 小时 | | `symbol_scores_v2` | V2 品种评分 | 1 小时 | | `trading_plans_v2` | V2 交易计划 | 1 小时 | | `sector_heat` | 板块热度 | 1 小时 | | `review_plans_v2` | V2 复盘计划 | 1 小时 | | `trade_records` | 交易记录 | 不缓存 | | `trade_import_batches` | 交易导入批次 | 不缓存 | ## 2. 降级策略 ### 2.1 读取流程 ``` 读取请求 ↓ 检查 Redis 缓存 ↓ 命中 返回数据 ↓ 未命中 检查 MySQL 可用性 ↓ 可用 查询 MySQL → 回填 Redis → 返回 ↓ 不可用 降级到 SQLite → 返回 ``` ### 2.2 写入流程 ``` 写入请求 ↓ 删除 Redis 缓存 ↓ 写入 MySQL(事务) ↓ 成功 回填 Redis → 返回 ↓ 失败 写入 SQLite → 返回 ``` ### 2.3 降级状态检测 - 惰性检测:30 秒间隔 - Redis 不可用时跳过缓存层 - MySQL 不可用时降级到 SQLite ## 3. 数据迁移 ### 3.1 迁移时机 应用启动时(`lifespan`),在 MySQL 表结构初始化后执行。 ### 3.2 迁移逻辑 ```python def migrate_analysis_sqlite_to_mysql(): """将 SQLite analysis 数据迁移到 MySQL""" # 1. 检测 MySQL 表是否已有数据 mysql_count = count_mysql_analysis_tables() if mysql_count > 0: logger.info("MySQL analysis 表已有数据,跳过迁移") return # 2. 从 SQLite 读取数据 sqlite_data = read_sqlite_analysis_data() # 3. 批量写入 MySQL write_mysql_analysis_data(sqlite_data) logger.info("Analysis 数据迁移完成") ``` ### 3.3 幂等性 - 检测 MySQL 表是否已有数据 - 有数据则跳过迁移 - 迁移失败不影响应用启动 ## 4. 缓存策略 ### 4.1 缓存键设计 ``` analysis:{table_name}:{id} analysis:{table_name}:list:{query_hash} ``` ### 4.2 缓存失效 - 写入时删除相关缓存 - TTL 过期自动失效 ### 4.3 缓存回填 - 读取时未命中,查询 MySQL 后回填 - TTL 与表类型对应 ## 5. 兼容性 ### 5.1 ORM 模型 - 保持不变 - 使用 `AnalysisBase` 作为基类 ### 5.2 API 接口 - 保持不变 - 通过 `get_analysis_db()` 依赖注入 ### 5.3 SQLite 文件 - 保留 `data/futures_analysis.db` - 降级时使用 ## 6. 测试策略 ### 6.1 单元测试 | 场景 | 测试方法 | |------|----------| | Redis 缓存命中 | Mock Redis,验证不查询 MySQL | | Redis 缓存未命中 | Mock Redis 返回 None,验证查询 MySQL | | MySQL 降级到 SQLite | Mock MySQL 不可用,验证查询 SQLite | | 数据迁移 | 验证 SQLite 数据完整迁移到 MySQL | ### 6.2 集成测试 - 启动应用,验证 MySQL 表创建 - 执行迁移,验证数据完整性 - 调用 API,验证功能正常 ## 7. 风险与缓解 | 风险 | 缓解措施 | |------|----------| | 数据迁移失败 | 保留 SQLite 作为降级方案,迁移失败时自动降级 | | Redis 缓存不一致 | 写入时先删缓存,再写 MySQL,最后回填缓存 | | 表结构不兼容 | 复用现有 ORM 模型,MySQL 自动创建表结构 | | 性能下降 | Redis 缓存减少 MySQL 查询压力 |