348 lines
9.2 KiB
Markdown
348 lines
9.2 KiB
Markdown
# 数据请求策略优化总结
|
||
|
||
## 📋 问题发现
|
||
|
||
### 原始问题
|
||
根据最初的规划,为了减少数据量:
|
||
1. **时间轴**: 只显示每天 **00:00:00** 的位置数据
|
||
2. **首页**: 只显示用户打开时**当前小时**的位置数据
|
||
|
||
但实际实现中存在以下问题:
|
||
- ❌ 时间轴请求了**范围数据**(从 day_start 到 day_end),导致返回多个时间点
|
||
- ❌ 首页请求了**最近 24 小时**的所有数据,而不是单个时间点
|
||
- ❌ positions 表中存储了大量冗余数据(每天多个时间点)
|
||
|
||
---
|
||
|
||
## ✅ 解决方案
|
||
|
||
### 核心策略
|
||
**所有请求都改为单个时间点查询**:`start_time = end_time`
|
||
|
||
这样 NASA Horizons API 只返回单个时间点的数据,而不是时间范围内的多个点。
|
||
|
||
---
|
||
|
||
## 🔧 具体修改
|
||
|
||
### 1. 前端修改
|
||
|
||
#### 1.1 首页数据请求 (`frontend/src/hooks/useSpaceData.ts`)
|
||
|
||
**修改前**:
|
||
```tsx
|
||
// 无参数请求,后端返回最近 24 小时的数据
|
||
const data = await fetchCelestialPositions();
|
||
```
|
||
|
||
**修改后**:
|
||
```tsx
|
||
// 请求当前小时的单个时间点
|
||
const now = new Date();
|
||
now.setMinutes(0, 0, 0); // 圆整到小时
|
||
|
||
const data = await fetchCelestialPositions(
|
||
now.toISOString(),
|
||
now.toISOString(), // start = end,单个时间点
|
||
'1h'
|
||
);
|
||
```
|
||
|
||
**API 请求示例**:
|
||
```http
|
||
GET /api/celestial/positions?start_time=2025-11-29T12:00:00Z&end_time=2025-11-29T12:00:00Z&step=1h
|
||
```
|
||
|
||
**返回数据**: 每个天体 1 个位置点(当前小时)
|
||
|
||
---
|
||
|
||
#### 1.2 时间轴数据请求 (`frontend/src/hooks/useHistoricalData.ts`)
|
||
|
||
**修改前**:
|
||
```tsx
|
||
const startDate = new Date(date);
|
||
const endDate = new Date(date);
|
||
endDate.setDate(endDate.getDate() + 1); // +1 天
|
||
|
||
const data = await fetchCelestialPositions(
|
||
startDate.toISOString(), // 2025-01-15T00:00:00Z
|
||
endDate.toISOString(), // 2025-01-16T00:00:00Z ❌ 多了1天
|
||
'1d'
|
||
);
|
||
```
|
||
|
||
**修改后**:
|
||
```tsx
|
||
// 圆整到 UTC 午夜
|
||
const targetDate = new Date(date);
|
||
targetDate.setUTCHours(0, 0, 0, 0);
|
||
|
||
// start = end,只请求午夜这个时间点
|
||
const data = await fetchCelestialPositions(
|
||
targetDate.toISOString(), // 2025-01-15T00:00:00Z
|
||
targetDate.toISOString(), // 2025-01-15T00:00:00Z ✅ 单个时间点
|
||
'1d'
|
||
);
|
||
```
|
||
|
||
**API 请求示例**:
|
||
```http
|
||
GET /api/celestial/positions?start_time=2025-01-15T00:00:00Z&end_time=2025-01-15T00:00:00Z&step=1d
|
||
```
|
||
|
||
**返回数据**: 每个天体 1 个位置点(2025-01-15 00:00:00)
|
||
|
||
---
|
||
|
||
### 2. 后端缓存预热修改
|
||
|
||
#### 2.1 当前位置预热 (`backend/app/services/cache_preheat.py`)
|
||
|
||
**策略变更**:
|
||
- **修改前**: 加载最近 24 小时的所有数据
|
||
- **修改后**: 只加载当前小时最接近的单个时间点
|
||
|
||
**实现逻辑**:
|
||
```python
|
||
# 当前小时
|
||
now = datetime.utcnow()
|
||
current_hour = now.replace(minute=0, second=0, microsecond=0)
|
||
|
||
# 搜索窗口: 当前小时 ± 1 小时
|
||
start_window = current_hour - timedelta(hours=1)
|
||
end_window = current_hour + timedelta(hours=1)
|
||
|
||
# 找到最接近当前小时的位置
|
||
closest_pos = min(
|
||
recent_positions,
|
||
key=lambda p: abs((p.time - current_hour).total_seconds())
|
||
)
|
||
```
|
||
|
||
**Redis Key**:
|
||
```
|
||
positions:2025-11-29T12:00:00+00:00:2025-11-29T12:00:00+00:00:1h
|
||
```
|
||
|
||
---
|
||
|
||
#### 2.2 历史位置预热
|
||
|
||
**策略变更**:
|
||
- **修改前**: 每天加载所有时间点的数据
|
||
- **修改后**: 每天只加载 00:00:00 这个时间点
|
||
|
||
**实现逻辑**:
|
||
```python
|
||
# 目标时间: 当天的午夜 (00:00:00)
|
||
target_midnight = target_day.replace(hour=0, minute=0, second=0, microsecond=0)
|
||
|
||
# 搜索窗口: 午夜 ± 30 分钟
|
||
search_start = target_midnight - timedelta(minutes=30)
|
||
search_end = target_midnight + timedelta(minutes=30)
|
||
|
||
# 找到最接近午夜的位置
|
||
closest_pos = min(
|
||
positions,
|
||
key=lambda p: abs((p.time - target_midnight).total_seconds())
|
||
)
|
||
```
|
||
|
||
**Redis Key**:
|
||
```
|
||
positions:2025-11-26T00:00:00+00:00:2025-11-26T00:00:00+00:00:1d
|
||
positions:2025-11-27T00:00:00+00:00:2025-11-27T00:00:00+00:00:1d
|
||
positions:2025-11-28T00:00:00+00:00:2025-11-28T00:00:00+00:00:1d
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 数据量对比
|
||
|
||
### 首页数据
|
||
|
||
| 指标 | 修改前 | 修改后 | 减少 |
|
||
|------|--------|--------|------|
|
||
| 时间范围 | 最近 24 小时 | 当前 1 小时 | - |
|
||
| 每个天体位置点数 | 可能 1-24 个 | 1 个 | **96%** ⬇️ |
|
||
| 总数据量(20 天体) | 20-480 个点 | 20 个点 | **96%** ⬇️ |
|
||
|
||
### 时间轴数据(3 天)
|
||
|
||
| 指标 | 修改前 | 修改后 | 减少 |
|
||
|------|--------|--------|------|
|
||
| 每天每个天体位置点数 | 2 个(00:00 和 24:00) | 1 个(00:00) | **50%** ⬇️ |
|
||
| 3 天总数据量(20 天体) | 120 个点 | 60 个点 | **50%** ⬇️ |
|
||
|
||
### positions 表数据量(假设每小时更新一次)
|
||
|
||
| 场景 | 每个天体每天记录数 | 20 个天体每天总记录数 | 一年总记录数 |
|
||
|------|-------------------|---------------------|-------------|
|
||
| **首页**(每小时 1 条) | 24 | 480 | 175,200 |
|
||
| **时间轴**(每天 1 条) | 1 | 20 | 7,300 |
|
||
|
||
---
|
||
|
||
## 🎯 建议的数据管理策略
|
||
|
||
### 策略 1: 清空并重建 positions 表(推荐)
|
||
|
||
**原因**:
|
||
- 当前表中可能有大量冗余数据
|
||
- 重新开始可以确保数据质量
|
||
|
||
**步骤**:
|
||
```sql
|
||
-- 1. 清空 positions 表
|
||
TRUNCATE TABLE positions;
|
||
|
||
-- 2. 清空 nasa_cache 表(可选)
|
||
TRUNCATE TABLE nasa_cache;
|
||
```
|
||
|
||
**重新获取数据**:
|
||
```bash
|
||
# 1. 清空 Redis 缓存
|
||
curl -X POST "http://localhost:8000/api/celestial/cache/clear"
|
||
|
||
# 2. 访问首页触发数据获取(当前小时)
|
||
# 打开 http://localhost:5173
|
||
|
||
# 3. 访问时间轴触发数据获取(过去 3 天的午夜数据)
|
||
# 点击"时间轴"按钮
|
||
```
|
||
|
||
---
|
||
|
||
### 策略 2: 定期更新数据(管理后台实现)
|
||
|
||
#### 2.1 每小时更新当前位置
|
||
```python
|
||
@scheduler.scheduled_job('cron', minute=0) # 每小时整点
|
||
async def update_current_positions():
|
||
"""每小时更新一次所有天体的位置"""
|
||
now = datetime.utcnow()
|
||
current_hour = now.replace(minute=0, second=0, microsecond=0)
|
||
|
||
for body in all_bodies:
|
||
# 查询 NASA API(单个时间点)
|
||
positions = horizons_service.get_body_positions(
|
||
body.id,
|
||
current_hour,
|
||
current_hour, # start = end
|
||
"1h"
|
||
)
|
||
|
||
# 保存到数据库
|
||
await position_service.save_positions(
|
||
body.id, positions, "nasa_horizons", db
|
||
)
|
||
|
||
# 预热缓存
|
||
await preheat_current_positions()
|
||
```
|
||
|
||
**数据量**: 20 天体 × 1 条/小时 × 24 小时 = **480 条/天**
|
||
|
||
---
|
||
|
||
#### 2.2 每天凌晨更新历史数据(时间轴)
|
||
```python
|
||
@scheduler.scheduled_job('cron', hour=0, minute=0) # 每天凌晨
|
||
async def update_midnight_positions():
|
||
"""每天凌晨更新所有天体的午夜位置"""
|
||
now = datetime.utcnow()
|
||
midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||
|
||
for body in all_bodies:
|
||
# 查询 NASA API(单个时间点)
|
||
positions = horizons_service.get_body_positions(
|
||
body.id,
|
||
midnight,
|
||
midnight, # start = end
|
||
"1d"
|
||
)
|
||
|
||
# 保存到数据库
|
||
await position_service.save_positions(
|
||
body.id, positions, "nasa_horizons", db
|
||
)
|
||
|
||
# 预热历史缓存(3天)
|
||
await preheat_historical_positions(days=3)
|
||
```
|
||
|
||
**数据量**: 20 天体 × 1 条/天 = **20 条/天**
|
||
|
||
---
|
||
|
||
### 策略 3: 数据清理(可选)
|
||
|
||
定期清理旧数据以节省存储空间:
|
||
|
||
```python
|
||
@scheduler.scheduled_job('cron', hour=3, minute=0) # 每天凌晨 3 点
|
||
async def cleanup_old_positions():
|
||
"""清理 30 天前的位置数据"""
|
||
cutoff_date = datetime.utcnow() - timedelta(days=30)
|
||
|
||
# 删除旧数据
|
||
await db.execute(
|
||
"DELETE FROM positions WHERE time < :cutoff",
|
||
{"cutoff": cutoff_date}
|
||
)
|
||
|
||
logger.info(f"Cleaned up positions older than {cutoff_date.date()}")
|
||
```
|
||
|
||
---
|
||
|
||
## 📈 性能优化效果
|
||
|
||
### 数据传输量
|
||
|
||
| 场景 | 修改前 | 修改后 | 优化 |
|
||
|------|--------|--------|------|
|
||
| 首页加载(20 天体) | ~5-50KB | ~2KB | **75-95%** ⬇️ |
|
||
| 时间轴加载(3 天,20 天体) | ~10KB | ~6KB | **40%** ⬇️ |
|
||
|
||
### 数据库存储
|
||
|
||
| 周期 | 修改前 | 修改后 | 减少 |
|
||
|------|--------|--------|------|
|
||
| 每天新增记录 | 不确定(混乱) | 500 条(首页 480 + 时间轴 20) | - |
|
||
| 每年总记录数 | 不确定 | ~18万 | - |
|
||
|
||
---
|
||
|
||
## ✅ 总结
|
||
|
||
### 关键改进
|
||
|
||
1. ✅ **单点查询策略**: 所有请求都改为 `start_time = end_time`
|
||
2. ✅ **首页优化**: 只请求当前小时的单个时间点
|
||
3. ✅ **时间轴优化**: 只请求每天 00:00:00 的单个时间点
|
||
4. ✅ **缓存预热优化**: 预热逻辑匹配单点查询策略
|
||
5. ✅ **数据量减少**: 减少 50-96% 的数据传输和存储
|
||
|
||
### 数据规范
|
||
|
||
| 场景 | 时间点 | 频率 | 用途 |
|
||
|------|--------|------|------|
|
||
| **首页** | 每小时整点(XX:00:00) | 每小时更新 1 次 | 显示当前位置 |
|
||
| **时间轴** | 每天午夜(00:00:00) | 每天更新 1 次 | 显示历史轨迹 |
|
||
|
||
### 下一步操作
|
||
|
||
1. ⚠️ **重启前端和后端**,应用新的请求逻辑
|
||
2. ⚠️ **清空 positions 表**(可选但推荐),确保数据干净
|
||
3. ✅ **测试首页和时间轴**,验证数据正确性
|
||
4. ✅ **在管理后台实现定时任务**,每小时/每天更新数据
|
||
|
||
---
|
||
|
||
**文档版本**: v1.0
|
||
**最后更新**: 2025-11-29
|
||
**作者**: Cosmo Team
|