Files
planet/docs/plans/earth-vessel-ais-aggregation-plan.md
2026-04-30 14:30:12 +08:00

9.4 KiB
Raw Blame History

AIS 多源采集、冲突记录与聚合接口计划

状态:规划中
创建日期2026-04-30
核心原则:采集器只写原始观测;去重、合并、冲突解释放在聚合接口中完成

已确认决策

项目 决策
AISStream 接入方式 单独实现 WebSocket 采集器,不塞进现有 BarentsWatch HTTP collector
采集器职责 连接上游、标准化字段、写入原始观测,不直接决定最终展示值
去重合并位置 放在聚合服务和聚合 API 中,而不是散落在每个 collector 的保存逻辑里
冲突处理 先记录冲突事实和当前选择原因,后续再开放用户规则配置
默认可信度 同类 AIS 数据源优先按 delivery_mode 评估:realtime_stream 优于 batch_stream,再优于 pollingsnapshot
过期保护 实时流源断流超过 freshness 窗口后,不能仅凭“实时源”身份压过更新的轮询数据

背景

当前 AIS 链路以 BarentsWatch 为主。它是 HTTP polling 模式,覆盖挪威附近海域,适合作为稳定的免费起点,但不适合承担全球实时船只数据的全部职责。后续接入 AISStream 后,会出现同一个 MMSI 被多个来源同时上报的情况:

  • 位置、航速、航向可能在多个来源之间存在秒级差异。
  • 船名、IMO、呼号、船型、尺寸等静态字段可能不完整甚至互相冲突。
  • WebSocket 或其他实时流通常更接近实时,但也可能断流或批量延迟。
  • 如果每个 collector 自己做去重合并,规则会分散、不可审计,也很难让用户后续配置“某个字段信任哪个来源”。

因此 v1 不应让采集器直接覆盖最终船只表。更稳的方式是先保留观测事实,再由聚合接口统一给出当前展示视图。

目标架构

flowchart LR
    A[BarentsWatch HTTP collector] --> D[AIS raw observations]
    B[AISStream WebSocket collector] --> D
    C[Custom mapped vessel_ais sources] --> D
    D --> E[AIS aggregation service]
    E --> F[Conflict records]
    E --> G[GeoJSON vessels API]
    E --> H[Vessel detail API]
    I[Aggregation strategy config] --> E

原始观测层

原始观测层保存每个来源看到的事实。建议模型包含:

字段 用途
target_schema 例如 vessel_ais
source 例如 barentswatch_vesselsaisstream_vessels
entity_key AIS 使用 MMSI
delivery_mode realtime_streambatch_streampollingsnapshot
transport websocketssehttpfile
observed_at 上游数据时间,优先使用 AIS 消息时间
collected_at 本系统接收或采集时间
normalized_payload 标准化后的 AIS JSON
raw_payload 可选,保存原始或裁剪后的上游记录

delivery_modetransport 不应混为一谈。WebSocket 是传输方式streaming 是交付模式。聚合可信度主要看 delivery_modetransport 只作为辅助信息。

冲突记录层

聚合服务发现同一个实体、同一个字段存在多个非空不同值时,写入冲突记录。冲突记录不代表错误,只代表“有多个可用候选值”。

{
  "target_schema": "vessel_ais",
  "entity_key": "257123000",
  "field": "name",
  "candidates": {
    "barentswatch_vessels": "OSLO TRADER",
    "aisstream_vessels": "OSLO TRADER II"
  },
  "selected_source": "aisstream_vessels",
  "selected_value": "OSLO TRADER II",
  "selected_reason": "delivery_mode_priority",
  "resolved_by": "system",
  "status": "open"
}

第一阶段只需要记录冲突和当前选择原因,不需要做人工逐条确认。后续 UI 的目标也不是让用户处理每条冲突,而是把冲突沉淀成字段级规则。

聚合规则

字段分类

类型 字段 默认策略
动态位置 latlonsogcogheadingnav_status 优先最新 observed_at,同时间再按来源优先级
静态身份 namecallsignimoflag 非空优先,再按字段策略或来源优先级
静态规格 vessel_typevessel_type_namelengthwidthdraught 非空优先;冲突时记录候选值
元信息 field_sourcesconflict_countselected_reasons 聚合接口生成,便于调试和后续 UI 展示

默认优先级

默认优先级应使用两个维度:

delivery_mode_priority:
  - realtime_stream
  - batch_stream
  - polling
  - snapshot

transport_priority:
  - websocket
  - sse
  - http
  - file

delivery_mode_priority 是主判断。比如 AISStream 如果提供实时推送,应标记为 realtime_stream + websocketBarentsWatch 当前是 polling + http

断流保护

实时流不能永久凭身份占优。聚合时需要 freshness 窗口:

freshness:
  realtime_stream_seconds: 900
  polling_seconds: 3600

如果 aisstream_vessels 最近 15 分钟没有该 MMSI 的新观测,而 BarentsWatch 轮询源有更新位置,则位置类字段应采用 BarentsWatch 的更新观测,并记录选择原因 newest_observationfreshness_fallback

聚合接口

现有展示接口应逐步改为消费聚合服务,而不是自己直接拼 VesselPosition + VesselStatic

GET /api/v1/visualization/geo/vessels
GET /api/v1/visualization/vessels/{mmsi}
GET /api/v1/visualization/vessels/{mmsi}/track
GET /api/v1/visualization/vessels/{mmsi}/conflicts

GeoJSON properties 建议增加:

{
  "mmsi": 257123000,
  "name": "OSLO TRADER",
  "lat": 59.91,
  "lon": 10.73,
  "received_at": "2026-04-30T10:00:00Z",
  "field_sources": {
    "name": "aisstream_vessels",
    "lat": "aisstream_vessels",
    "lon": "aisstream_vessels",
    "vessel_type": "barentswatch_vessels"
  },
  "selected_reasons": {
    "name": "delivery_mode_priority",
    "lat": "newest_observation",
    "vessel_type": "non_empty_priority"
  },
  "conflict_count": 2
}

开放配置计划

Phase 1 — 内置默认策略和只读解释

  • 实现后端默认策略。
  • 聚合接口返回 field_sourcesselected_reasonsconflict_count
  • 冲突记录可查询,但不允许用户修改。
  • 保持现有前端船只图层接口形状基本兼容,新增字段只作为调试和后续 UI 输入。

Phase 2 — 系统设置中的 JSON/YAML 策略配置

新增系统设置项,例如:

collector_aggregation:
  vessel_ais:
    source_priority:
      - aisstream_vessels
      - barentswatch_vessels
    field_rules:
      name:
        mode: source_priority
      vessel_type:
        mode: source_priority
        source_priority:
          - barentswatch_vessels
          - aisstream_vessels
      lat:
        mode: newest
      lon:
        mode: newest

配置校验要求:

  • 未知 source 只警告,不阻断保存,便于先配置后启用。
  • 未知 field 必须拒绝,避免拼写错误悄悄失效。
  • 动态位置字段默认不允许被固定来源永久锁死,除非显式开启高级选项。
  • 空值不覆盖非空值是全局保护,不建议开放关闭。

Phase 3 — 冲突治理 UI

基于冲突记录提供页面或 drawer

  • 查看某个 MMSI 的冲突字段。
  • 查看每个字段的候选来源和值。
  • 查看当前选择原因。
  • 将一次人工选择保存成字段规则,而不是只处理单条冲突。
  • 支持恢复默认策略。

AISStream 采集器计划

AISStream 采集器单独实现,建议命名为 aisstream_vessels。它的职责是:

  • 维护 WebSocket 连接、订阅范围和重连。
  • 将上游 AIS 消息标准化为 vessel_ais payload。
  • 标记 delivery_mode = realtime_streamtransport = websocket
  • 写入原始观测层。
  • 不直接 upsert 最终展示数据。

配置应放入采集器设置,而不是硬编码:

aisstream_vessels:
  api_key: "${AISSTREAM_API_KEY}"
  bounding_boxes:
    - [[-180, -90], [180, 90]]
  message_types:
    - PositionReport
    - ShipStaticData

实施顺序

  1. 新增原始观测模型和冲突记录模型。
  2. 实现 AIS 聚合服务,先从现有 vessel_position / vessel_static 兼容读取,再逐步切换到原始观测层。
  3. /geo/vessels/vessels/{mmsi} 改为走聚合服务。
  4. 改造 BarentsWatch 保存逻辑,让它写入原始观测,同时保留现有表作为兼容缓存。
  5. 实现 AISStream WebSocket collector。
  6. 接入系统设置中的聚合策略配置。
  7. 做冲突治理 UI。

测试计划

  • 同一来源同一 mmsi + observed_at + lat + lon 重复记录只聚合一次。
  • 多来源同一 MMSI 的位置字段优先选择最新观测。
  • 实时流和轮询源同时间冲突时,实时流优先。
  • 实时流过期后,更新的轮询源可以接管动态字段。
  • 静态字段不会被空值覆盖。
  • 静态字段冲突会写入冲突记录。
  • 字段级配置可以覆盖默认来源优先级。
  • 聚合接口在没有冲突表时仍可返回兼容 GeoJSON。

相关文件