208 lines
5.1 KiB
Markdown
208 lines
5.1 KiB
Markdown
# collected_data 强耦合列拆除计划
|
||
|
||
## 背景
|
||
|
||
当前 `collected_data` 同时承担了两类职责:
|
||
|
||
1. 通用采集事实表
|
||
2. 少数数据源的宽表字段承载
|
||
|
||
典型强耦合列包括:
|
||
|
||
- `country`
|
||
- `city`
|
||
- `latitude`
|
||
- `longitude`
|
||
- `value`
|
||
- `unit`
|
||
|
||
以及 API 层临时平铺出来的:
|
||
|
||
- `cores`
|
||
- `rmax`
|
||
- `rpeak`
|
||
- `power`
|
||
|
||
这些字段并不适合作为统一事实表的长期 schema。
|
||
推荐方向是:
|
||
|
||
- 表内保留通用稳定字段
|
||
- 业务差异字段全部归入 `metadata`
|
||
- API 和前端动态读取 `metadata`
|
||
|
||
## 拆除目标
|
||
|
||
最终希望 `collected_data` 只保留:
|
||
|
||
- `id`
|
||
- `snapshot_id`
|
||
- `task_id`
|
||
- `source`
|
||
- `source_id`
|
||
- `entity_key`
|
||
- `data_type`
|
||
- `name`
|
||
- `title`
|
||
- `description`
|
||
- `metadata`
|
||
- `collected_at`
|
||
- `reference_date`
|
||
- `is_valid`
|
||
- `is_current`
|
||
- `previous_record_id`
|
||
- `change_type`
|
||
- `change_summary`
|
||
- `deleted_at`
|
||
|
||
## 计划阶段
|
||
|
||
### Phase 1:读取层去依赖
|
||
|
||
目标:
|
||
|
||
- API / 可视化 / 前端不再优先依赖宽列表字段
|
||
- 所有动态字段优先从 `metadata` 取
|
||
|
||
当前已完成:
|
||
|
||
- 新写入数据时,将 `country/city/latitude/longitude/value/unit` 自动镜像到 `metadata`
|
||
- `/api/v1/collected` 优先从 `metadata` 取动态字段
|
||
- `visualization` 接口优先从 `metadata` 取动态字段
|
||
- 国家筛选已改成只走 `metadata->>'country'`
|
||
- `CollectedData.to_dict()` 已切到 metadata-first
|
||
- 变更比较逻辑已切到 metadata-first
|
||
- 已新增历史回填脚本:
|
||
[scripts/backfill_collected_data_metadata.py](/home/ray/dev/linkong/planet/scripts/backfill_collected_data_metadata.py)
|
||
- 已新增删列脚本:
|
||
[scripts/drop_collected_data_legacy_columns.py](/home/ray/dev/linkong/planet/scripts/drop_collected_data_legacy_columns.py)
|
||
|
||
涉及文件:
|
||
|
||
- [backend/app/core/collected_data_fields.py](/home/ray/dev/linkong/planet/backend/app/core/collected_data_fields.py)
|
||
- [backend/app/services/collectors/base.py](/home/ray/dev/linkong/planet/backend/app/services/collectors/base.py)
|
||
- [backend/app/api/v1/collected_data.py](/home/ray/dev/linkong/planet/backend/app/api/v1/collected_data.py)
|
||
- [backend/app/api/v1/visualization.py](/home/ray/dev/linkong/planet/backend/app/api/v1/visualization.py)
|
||
|
||
### Phase 2:写入层去依赖
|
||
|
||
目标:
|
||
|
||
- 采集器内部不再把这些字段当作数据库一级列来理解
|
||
- 统一只写:
|
||
- 通用主字段
|
||
- `metadata`
|
||
|
||
建议动作:
|
||
|
||
1. Collector 内部仍可使用 `country/city/value` 这种临时字段作为采集过程变量
|
||
2. 进入 `BaseCollector._save_data()` 后统一归档到 `metadata`
|
||
3. `CollectedData` 模型中的强耦合列已从 ORM 移除,写入统一归档到 `metadata`
|
||
|
||
### Phase 3:数据库删列
|
||
|
||
目标:
|
||
|
||
- 从 `collected_data` 真正移除以下列:
|
||
- `country`
|
||
- `city`
|
||
- `latitude`
|
||
- `longitude`
|
||
- `value`
|
||
- `unit`
|
||
|
||
注意:
|
||
|
||
- `cores / rmax / rpeak / power` 当前本来就在 `metadata` 里,不是表列
|
||
- 这四个主要是 API 平铺字段,不需要数据库删列
|
||
|
||
## 当前阻塞点
|
||
|
||
在正式删列前,还需要确认这些地方已经完全不再直接依赖数据库列:
|
||
|
||
### 1. `CollectedData.to_dict()`
|
||
|
||
文件:
|
||
|
||
- [backend/app/models/collected_data.py](/home/ray/dev/linkong/planet/backend/app/models/collected_data.py)
|
||
|
||
状态:
|
||
|
||
- 已完成
|
||
|
||
### 2. 差异计算逻辑
|
||
|
||
文件:
|
||
|
||
- [backend/app/services/collectors/base.py](/home/ray/dev/linkong/planet/backend/app/services/collectors/base.py)
|
||
|
||
状态:
|
||
|
||
- 已完成
|
||
- 当前已改成比较归一化后的 metadata-first payload
|
||
|
||
### 3. 历史数据回填
|
||
|
||
问题:
|
||
|
||
- 老数据可能只有列值,没有对应 `metadata`
|
||
|
||
当前方案:
|
||
|
||
- 在删列前执行一次回填脚本:
|
||
- [scripts/backfill_collected_data_metadata.py](/home/ray/dev/linkong/planet/scripts/backfill_collected_data_metadata.py)
|
||
|
||
### 4. 导出格式兼容
|
||
|
||
文件:
|
||
|
||
- [backend/app/api/v1/collected_data.py](/home/ray/dev/linkong/planet/backend/app/api/v1/collected_data.py)
|
||
|
||
现状:
|
||
|
||
- CSV/JSON 导出已基本切成 metadata-first
|
||
|
||
建议:
|
||
|
||
- 删列前再回归检查一次导出字段是否一致
|
||
|
||
## 推荐执行顺序
|
||
|
||
1. 保持新数据写入时 `metadata` 完整
|
||
2. 把模型和 diff 逻辑完全切成 metadata-first
|
||
3. 写一条历史回填脚本
|
||
4. 回填后观察一轮
|
||
5. 正式执行删列迁移
|
||
|
||
## 推荐迁移 SQL
|
||
|
||
仅在确认全部读取链路已去依赖后执行:
|
||
|
||
```sql
|
||
ALTER TABLE collected_data
|
||
DROP COLUMN IF EXISTS country,
|
||
DROP COLUMN IF EXISTS city,
|
||
DROP COLUMN IF EXISTS latitude,
|
||
DROP COLUMN IF EXISTS longitude,
|
||
DROP COLUMN IF EXISTS value,
|
||
DROP COLUMN IF EXISTS unit;
|
||
```
|
||
|
||
## 风险提示
|
||
|
||
1. 地图类接口对经纬度最敏感
|
||
必须确保所有地图需要的记录,其 `metadata.latitude/longitude` 已回填完整。
|
||
|
||
2. 历史老数据如果没有回填,删列后会直接丢失这些信息。
|
||
|
||
3. 某些 collector 可能仍隐式依赖这些宽字段做差异比较,删列前必须做一次全量回归。
|
||
|
||
## 当前判断
|
||
|
||
当前项目已经完成“代码去依赖 + 历史回填 + readiness 检查”。
|
||
下一步执行顺序建议固定为:
|
||
|
||
1. 先部署当前代码版本并重启后端
|
||
2. 再做一轮功能回归
|
||
3. 最后执行:
|
||
`uv run python scripts/drop_collected_data_legacy_columns.py`
|