# 采集数据历史快照化改造方案 ## 背景 当前系统的 `collected_data` 更接近“当前结果表”: - 同一个 `source + source_id` 会被更新覆盖 - 前端列表页默认读取这张表 - `collection_tasks` 只记录任务执行状态,不直接承载数据版本语义 这套方式适合管理后台,但不利于后续做态势感知、时间回放、趋势分析和版本对比。 如果后面需要回答下面这类问题,当前模型会比较吃力: - 某条实体在过去 7 天如何变化 - 某次采集相比上次新增了什么、删除了什么、值变了什么 - 某个时刻地图上“当时的世界状态”是什么 - 告警是在第几次采集后触发的 因此建议把采集数据改造成“历史快照 + 当前视图”模型。 ## 目标 1. 每次触发采集都保留一份独立快照,历史可追溯。 2. 管理后台默认仍然只看“当前最新状态”,不增加使用复杂度。 3. 后续支持: - 时间线回放 - 两次采集差异对比 - 趋势分析 - 按快照回溯告警和地图状态 4. 尽量兼容现有接口,降低改造成本。 ## 结论 不建议继续用以下两种单一模式: - 直接覆盖旧数据 问题:没有历史,无法回溯。 - 软删除旧数据再全量新增 问题:语义不清,历史和“当前无效”混在一起,后续统计复杂。 推荐方案: - 保留历史事实表 - 维护当前视图 - 每次采集对应一个明确的快照批次 ## 推荐数据模型 ### 方案概览 建议拆成三层: 1. `collection_tasks` 继续作为采集任务表,表示“这次采集任务”。 2. `data_snapshots` 新增快照表,表示“某个数据源在某次任务中产出的一个快照批次”。 3. `collected_data` 从“当前结果表”升级为“历史事实表”,每一行归属于一个快照。 同时再提供一个“当前视图”: - SQL View / 物化视图 / API 查询层封装均可 - 语义是“每个 `source + source_id` 的最新有效记录” ### 新增表:`data_snapshots` 建议字段: | 字段 | 类型 | 含义 | |---|---|---| | `id` | bigint PK | 快照主键 | | `datasource_id` | int | 对应数据源 | | `task_id` | int | 对应采集任务 | | `source` | varchar(100) | 数据源名,如 `top500` | | `snapshot_key` | varchar(100) | 可选,业务快照标识 | | `reference_date` | timestamptz nullable | 这批数据的参考时间 | | `started_at` | timestamptz | 快照开始时间 | | `completed_at` | timestamptz | 快照完成时间 | | `record_count` | int | 快照总记录数 | | `status` | varchar(20) | `running/success/failed/partial` | | `is_current` | bool | 当前是否是该数据源最新快照 | | `parent_snapshot_id` | bigint nullable | 上一版快照,可用于 diff | | `summary` | jsonb | 本次快照统计摘要 | 说明: - `collection_tasks` 偏“执行过程” - `data_snapshots` 偏“数据版本” - 一个任务通常对应一个快照,但保留分层更清晰 ### 升级表:`collected_data` 建议新增字段: | 字段 | 类型 | 含义 | |---|---|---| | `snapshot_id` | bigint not null | 归属快照 | | `task_id` | int nullable | 归属任务,便于追查 | | `entity_key` | varchar(255) | 实体稳定键,通常可由 `source + source_id` 派生 | | `is_current` | bool | 当前是否为该实体最新记录 | | `previous_record_id` | bigint nullable | 上一个版本的记录 | | `change_type` | varchar(20) | `created/updated/unchanged/deleted` | | `change_summary` | jsonb | 字段变化摘要 | | `deleted_at` | timestamptz nullable | 对应“本次快照中消失”的实体 | 保留现有字段: - `source` - `source_id` - `data_type` - `name` - `title` - `description` - `country` - `city` - `latitude` - `longitude` - `value` - `unit` - `metadata` - `collected_at` - `reference_date` - `is_valid` ### 当前视图 建议新增一个只读视图: `current_collected_data` 语义: - 对每个 `source + source_id` 只保留最新一条 `is_current = true` 且 `deleted_at is null` 的记录 这样: - 管理后台继续像现在一样查“当前数据” - 历史分析查 `collected_data` ## 写入策略 ### 触发按钮语义 “触发”不再理解为“覆盖旧表”,而是: - 启动一次新的采集任务 - 生成一个新的快照 - 将本次结果写入历史事实表 - 再更新当前视图标记 ### 写入流程 1. 创建 `collection_tasks` 记录,状态 `running` 2. 创建 `data_snapshots` 记录,状态 `running` 3. 采集器拉取原始数据并标准化 4. 为每条记录生成 `entity_key` - 推荐:`{source}:{source_id}` 5. 将本次记录批量写入 `collected_data` 6. 与上一个快照做比对,计算: - 新增 - 更新 - 未变 - 删除 7. 更新本批记录的: - `change_type` - `previous_record_id` - `is_current` 8. 将上一批同实体记录的 `is_current` 置为 `false` 9. 将本次快照未出现但上一版存在的实体标记为 `deleted` 10. 更新 `data_snapshots.status = success` 11. 更新 `collection_tasks.status = success` ### 删除语义 这里不建议真的删记录。 建议采用“逻辑消失”模型: - 历史行永远保留 - 如果某实体在新快照里消失: - 上一条历史记录补一条“删除状态记录”或标记 `change_type = deleted` - 同时该实体不再出现在当前视图 这样最适合态势感知。 ## API 改造建议 ### 保持现有接口默认行为 现有接口: - `GET /api/v1/collected` - `GET /api/v1/collected/{id}` - `GET /api/v1/collected/summary` 建议默认仍返回“当前视图”,避免前端全面重写。 ### 新增历史查询能力 建议新增参数或新接口: #### 1. 当前/历史切换 `GET /api/v1/collected?mode=current|history` - `current`:默认,查当前视图 - `history`:查历史事实表 #### 2. 按快照查询 `GET /api/v1/collected?snapshot_id=123` #### 3. 快照列表 `GET /api/v1/snapshots` 支持筛选: - `datasource_id` - `source` - `status` - `date_from/date_to` #### 4. 快照详情 `GET /api/v1/snapshots/{id}` 返回: - 快照基础信息 - 统计摘要 - 与上一版的 diff 摘要 #### 5. 快照 diff `GET /api/v1/snapshots/{id}/diff?base_snapshot_id=122` 返回: - `created` - `updated` - `deleted` - `unchanged` ## 前端改造建议 ### 1. 数据列表页 默认仍看当前数据,不改用户使用习惯。 建议新增: - “视图模式” - 当前数据 - 历史数据 - “快照时间”筛选 - “只看变化项”筛选 ### 2. 数据详情页 详情页建议展示: - 当前记录基础信息 - 元数据动态字段 - 所属快照 - 上一版本对比入口 - 历史版本时间线 ### 3. 数据源管理页 “触发”按钮文案建议改成更准确的: - `立即采集` 并在详情里补: - 最近一次快照时间 - 最近一次快照记录数 - 最近一次变化数 ## 迁移方案 ### Phase 1:兼容式落地 目标:先保留当前页面可用。 改动: 1. 新增 `data_snapshots` 2. 给 `collected_data` 增加: - `snapshot_id` - `task_id` - `entity_key` - `is_current` - `previous_record_id` - `change_type` - `change_summary` - `deleted_at` 3. 现有数据全部补成一个“初始化快照” 4. 现有 `/collected` 默认改查当前视图 优点: - 前端几乎无感 - 风险最小 ### Phase 2:启用差异计算 目标:采集后可知道本次改了什么。 改动: 1. 写入时做新旧快照比对 2. 写 `change_type` 3. 生成快照摘要 ### Phase 3:前端态势感知能力 目标:支持历史回放和趋势分析。 改动: 1. 快照时间线 2. 版本 diff 页面 3. 地图时间回放 4. 告警和快照关联 ## 唯一性与索引建议 ### 建议保留的业务唯一性 在“同一个快照内部”,建议唯一: - `(snapshot_id, source, source_id)` 不要在整张历史表上强加: - `(source, source_id)` 唯一 因为历史表本来就应该允许同一实体跨快照存在多条版本。 ### 建议索引 - `idx_collected_data_snapshot_id` - `idx_collected_data_source_source_id` - `idx_collected_data_entity_key` - `idx_collected_data_is_current` - `idx_collected_data_reference_date` - `idx_snapshots_source_completed_at` ## 风险点 1. 存储量会明显增加 - 需要评估保留周期 - 可以考虑冷热分层 2. 写入复杂度上升 - 需要批量 upsert / diff 逻辑 3. 当前接口语义会从“表”变成“视图” - 文档必须同步 4. 某些采集器缺稳定 `source_id` - 需要补齐实体稳定键策略 ## 对当前项目的具体建议 结合当前代码,推荐这样落地: ### 短期 1. 先设计并落表: - `data_snapshots` - `collected_data` 新字段 2. 采集完成后每次新增快照 3. `/api/v1/collected` 默认查 `is_current = true` ### 中期 1. 在 `BaseCollector._save_data()` 中改成: - 生成快照 - 批量写历史 - 标记当前 2. 将 `CollectionTask.id` 关联到 `snapshot.task_id` ### 长期 1. 地图接口支持按 `snapshot_id` 查询 2. 仪表盘支持“最近一次快照变化量” 3. 告警支持绑定到快照版本 ## 最终建议 最终建议采用: - 历史事实表:保存每次采集结果 - 当前视图:服务管理后台默认查询 - 快照表:承载版本批次和 diff 语义 这样既能保留历史,又不会把当前页面全部推翻重做,是最适合后续做态势感知的一条路径。