# 架构修订：基于会话的数据快照与分层存储 (Session-Based Data Snapshotting)

## 1. 核心理念修订 (Core Philosophy Refinement)

基于您的反馈，我们修正了架构的核心逻辑，将数据明确划分为两类，并采取不同的存储策略。

### 1.1 数据分类 (Data Classification)

1.  **客观历史数据 (Objective History / Time-Series)**
    *   **定义**: 股价、成交量、K线图等交易数据。
    *   **特性**: "出现即历史"，不可篡改，全球唯一。
    *   **存储策略**: **全局共享存储**。不需要按 Session 隔离，不需要存多份。
    *   **表**: 现有的 `daily_market_data` (TimescaleDB) 保持不变。

2.  **观测型数据 (Observational Data / Fundamentals)**
    *   **定义**: 财务报表、公司简介、以及 Provider 返回的原始非结构化或半结构化信息。
    *   **特性**: 不同来源（Providers）说法不一；可能随时间修正（Restatement）；分析依赖于“当时”获取的版本。
    *   **存储策略**: **基于 Session 的快照存储**。每一次 Session 都必须保存一份当时获取的原始数据的完整副本。
    *   **表**: 新增 `session_raw_data` 表。

### 1.2 解决的问题
*   **会话隔离**: 新的 Session 拥有自己独立的一套基础面数据，不受历史 Session 干扰，也不污染未来 Session。
*   **历史回溯**: 即使 Provider 变了，查看历史 Report 时，依然能看到当时是基于什么数据得出的结论。
*   **数据清洗解耦**: 我们现在只负责“收集并快照”，不负责“清洗和聚合”。复杂的清洗逻辑（WASM/AI）留待后续模块处理。

---

## 2. 数据库架构设计 (Schema Design)

### 2.1 新增：会话原始数据表 (`session_raw_data`)

这是本次架构调整的核心。我们不再试图把财务数据强行塞进一个全局唯一的标准表，而是忠实记录每个 Provider 在该 Session 中返回的内容。

```sql
CREATE TABLE session_raw_data (
    id BIGSERIAL PRIMARY KEY,
    request_id UUID NOT NULL,              -- 关联的 Session ID
    symbol VARCHAR(32) NOT NULL,
    provider VARCHAR(64) NOT NULL,         -- e.g., 'tushare', 'alphavantage'
    data_type VARCHAR(32) NOT NULL,        -- e.g., 'financial_statements', 'company_profile'
    
    -- 核心：直接存储 Provider 返回的（或稍微标准化的）完整 JSON
    data_payload JSONB NOT NULL,           
    
    created_at TIMESTAMPTZ DEFAULT NOW(),
    
    -- 索引：为了快速查询某次 Session 的数据
    CONSTRAINT fk_request_id FOREIGN KEY (request_id) REFERENCES requests(id) ON DELETE CASCADE
);

CREATE INDEX idx_session_data_req ON session_raw_data(request_id);
```

### 2.2 新增：供应商缓存表 (`provider_response_cache`)

为了优化性能和节省 API 调用次数，我们在全局层引入缓存。但请注意：**缓存仅作为读取源，不作为 Session 的存储地。**

```sql
CREATE TABLE provider_response_cache (
    cache_key VARCHAR(255) PRIMARY KEY,    -- e.g., "tushare:AAPL:financials"
    data_payload JSONB NOT NULL,
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    expires_at TIMESTAMPTZ NOT NULL
);
```

### 2.3 保持不变：市场数据表 (`daily_market_data`)
*   继续使用 TimescaleDB 存储 `open`, `high`, `low`, `close`, `volume`。
*   所有 Session 共享读取此表。

---

## 3. 数据流转逻辑 (Data Lifecycle)

### Phase 1: Session 启动与数据获取 (Acquisition)

1.  **Start**: API Gateway 生成 `request_id`。
2.  **Fetch & Cache Logic (在 Provider Service 中执行)**:
    *   Provider 收到任务 (Symbol: AAPL)。
    *   **Check Cache**: 查询 `provider_response_cache`。
        *   *Hit*: 拿出现成的 JSON。
        *   *Miss*: 调用外部 API，获得 JSON，写入 Cache (设置过期时间如 24h)。
3.  **Snapshot (关键步骤)**:
    *   Provider 将拿到的 JSON (无论来自 Cache 还是 API)，作为一条**新记录**写入 `session_raw_data`。
    *   字段: `request_id=UUID`, `provider=tushare`, `data=JSON`。

### Phase 2: 展示与分析 (Consumption)

1.  **Frontend Raw View (UI)**:
    *   前端调用 `GET /api/v1/session/{request_id}/raw-data`。
    *   后端 `SELECT * FROM session_raw_data WHERE request_id = ...`。
    *   UI 依然可以使用之前的 Accordion 结构，展示 "Tushare: Financials", "AlphaVantage: Profile"。这就是用户看到的“本次调查的原始底稿”。

2.  **Analysis (LLM)**:
    *   Report Generator 获取 `request_id` 对应的所有 raw data。
    *   将这些 Raw Data 作为 Context 喂给 LLM。
    *   (未来扩展): 在这一步之前，插入一个 "Data Cleaning Agent/Wasm"，读取 raw data，输出 clean data，再喂给 LLM。

### Phase 3: 归档与清理 (Cleanup)

*   **Session Deletion**: 当我们需要清理某个历史 Session 时，只需 `DELETE FROM session_raw_data WHERE request_id = ...`。
*   **副作用**: 零。因为 `daily_market_data` 是共享的（留着也没事），而 Session 独享的 `raw_data` 被彻底删除了。

---

## 4. 实施路线图 (Implementation Roadmap)

1.  **Database Migration**:
    *   创建 `session_raw_data` 表。
    *   创建 `provider_response_cache` 表。
    *   (清理旧表): 废弃 `time_series_financials` 表（原计划用于存标准化的财务指标，现在确认不需要。我们只存 `session_raw_data` 中的原始基本面数据，财务报表由原始数据动态推导）。
    *   **保留** `daily_market_data` 表（存储股价、K线等客观时间序列数据，保持全局共享）。

2.  **Provider Services**:
    *   引入 Cache 检查逻辑。
    *   修改输出逻辑：不再尝试 Upsert 全局表，而是 Insert `session_raw_data`。

3.  **Frontend Refactor**:
    *   修改 `RawDataViewer` 的数据源，从读取“最后一次更新”改为读取“当前 Session 的 Raw Data”。
    *   这完美解决了“刷新页面看到旧数据”的问题——如果是一个新 Session ID，它的 `session_raw_data` 一开始是空的，UI 就会显示为空/Loading，直到新的 Snapshot 写入。

4.  **Future Extensibility (Aggregation)**:
    *   当前架构下，Frontend 直接展示 Raw Data。
    *   未来：新增 `DataProcessorService`。它监听 "Data Fetched" 事件，读取 `session_raw_data`，执行聚合逻辑，将结果写入 `session_clean_data` (假想表)，供 UI 显示“完美报表”。

---

## 5. Step-by-Step Task List

### Phase 1: Data Persistence Service & Database (Foundation)
- [x] **Task 1.1**: Create new SQL migration file.
    - Define `session_raw_data` table (Columns: `id`, `request_id`, `symbol`, `provider`, `data_type`, `data_payload`, `created_at`).
    - Define `provider_response_cache` table (Columns: `cache_key`, `data_payload`, `updated_at`, `expires_at`).
    - (Optional) Rename `time_series_financials` to `_deprecated_time_series_financials` to prevent accidental usage.
- [x] **Task 1.2**: Run SQL migration (`sqlx migrate run`).
- [x] **Task 1.3**: Implement `db/session_data.rs` in Data Persistence Service.
    - Function: `insert_session_data(pool, request_id, provider, data_type, payload)`.
    - Function: `get_session_data(pool, request_id)`.
- [x] **Task 1.4**: Implement `db/provider_cache.rs` in Data Persistence Service.
    - Function: `get_cache(pool, key) -> Option<Payload>`.
    - Function: `set_cache(pool, key, payload, ttl)`.
- [x] **Task 1.5**: Expose new API endpoints in `api/`.
    - `POST /api/v1/session-data` (Internal use by Providers).
    - `GET /api/v1/session-data/:request_id` (Used by ReportGen & Frontend).
    - `GET/POST /api/v1/provider-cache` (Internal use by Providers).

### Phase 2: Common Contracts & SDK (Glue Code)
- [x] **Task 2.1**: Update `common-contracts`.
    - Add DTOs for `SessionData` and `CacheEntry`.
    - Update `PersistenceClient` struct to include methods for calling new endpoints (`save_session_data`, `get_cache`, `set_cache`).

### Phase 3: Provider Services (Logic Update)
- [x] **Task 3.1**: Refactor `tushare-provider-service`.
    - Update Worker to check Cache first.
    - On Cache Miss: Call Tushare API -> Save to Cache.
    - **Final Step**: Post data to `POST /api/v1/session-data` (instead of old batch insert).
    - Ensure `request_id` is propagated correctly.
- [x] **Task 3.2**: Refactor `alphavantage-provider-service` (same logic).
- [x] **Task 3.3**: Refactor `yfinance-provider-service` (same logic).
- [x] **Task 3.4**: Verify `FinancialsPersistedEvent` is still emitted (or similar event) to trigger Gateway aggregation.

### Phase 4: API Gateway & Report Generator (Consumption)
- [x] **Task 4.1**: Update `api-gateway` routing.
    - Proxy `GET /api/v1/session-data/:request_id` for Frontend.
- [x] **Task 4.2**: Update `report-generator-service`.
    - In `worker.rs`, change data fetching logic.
    - Instead of `get_financials_by_symbol`, call `get_session_data(request_id)`.
    - Pass the raw JSON list to the LLM Context Builder.

### Phase 5: Frontend (UI Update)
- [x] **Task 5.1**: Update `useReportEngine.ts`.
    - Change polling/fetching logic to request `GET /api/v1/session-data/${requestId}`.
- [x] **Task 5.2**: Update `RawDataViewer.tsx`.
    - Adapt to new data structure (List of `{ provider, data_type, payload }`).
    - Ensure the UI correctly groups these raw snapshots by Provider.
