17 KiB
AIS 多源采集、冲突记录与聚合接口计划
状态:v0-v3 已实现,v4+ 规划中
创建日期:2026-04-30
核心原则:采集器只写原始观测;去重、合并、冲突解释放在聚合接口中完成
已确认决策
| 项目 | 决策 |
|---|---|
| AISStream 接入方式 | 单独实现 WebSocket 采集器,不塞进现有 BarentsWatch HTTP collector |
| 采集器职责 | 连接上游、标准化字段、写入原始观测,不直接决定最终展示值 |
| 去重合并位置 | 放在聚合服务和聚合 API 中,而不是散落在每个 collector 的保存逻辑里 |
| 冲突处理 | 先记录冲突事实和当前选择原因,后续再开放用户规则配置 |
| 默认可信度 | 同类 AIS 数据源优先按 delivery_mode 评估:realtime_stream 优于 batch_stream,再优于 polling 和 snapshot |
| 过期保护 | 实时流源断流超过 freshness 窗口后,不能仅凭“实时源”身份压过更新的轮询数据 |
| 源健康状态 | 聚合时必须参考采集器健康状态,不能只看配置中的理论优先级 |
| 媒体富化 | 船只图片等媒体信息不进入 AIS 实时聚合主链路,后续单独做 enrichment |
背景
当前 AIS 链路以 BarentsWatch 为主。它是 HTTP polling 模式,覆盖挪威附近海域,适合作为稳定的免费起点,但不适合承担全球实时船只数据的全部职责。后续接入 AISStream 后,会出现同一个 MMSI 被多个来源同时上报的情况:
- 位置、航速、航向可能在多个来源之间存在秒级差异。
- 船名、IMO、呼号、船型、尺寸等静态字段可能不完整,甚至互相冲突。
- WebSocket 或其他实时流通常更接近实时,但也可能断流或批量延迟。
- 如果每个 collector 自己做去重合并,规则会分散、不可审计,也很难让用户后续配置“某个字段信任哪个来源”。
因此第一阶段不应让采集器直接覆盖最终船只表。更稳的方式是先保留观测事实,再由聚合接口统一给出当前展示视图。
目标架构
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_vessels、aisstream_vessels |
entity_key |
AIS 使用 MMSI |
delivery_mode |
realtime_stream、batch_stream、polling、snapshot |
transport |
websocket、sse、http、file 等 |
observed_at |
上游数据时间,优先使用 AIS 消息时间 |
collected_at |
本系统接收或采集时间 |
source_message_id |
上游消息 ID 或可推导 ID,没有则为空 |
observation_hash |
幂等去重指纹,用于防止同一来源重复写入同一条观测 |
normalized_payload |
标准化后的 AIS JSON |
raw_payload |
可选,保存原始或裁剪后的上游记录 |
quality_flags |
观测级质量标记,例如 stale、position_jump、future_timestamp |
delivery_mode 和 transport 不应混为一谈。WebSocket 是传输方式;streaming 是交付模式。聚合可信度主要看 delivery_mode,transport 只作为辅助信息。
原始观测层需要做存储级幂等去重,但这里的去重不是业务合并。推荐使用 source + entity_key + message_type + observed_at + payload_hash 或上游稳定消息 ID 作为唯一约束,避免 WebSocket 重连、HTTP 重试或批量回放导致同一事实重复入库。
源健康状态
每个采集器应维护独立的健康状态,供聚合服务读取:
| 字段 | 用途 |
|---|---|
source |
采集器标识 |
connection_state |
connected、reconnecting、disconnected、disabled 等 |
last_seen_at |
最近收到上游消息或响应的时间 |
last_success_at |
最近成功写入观测的时间 |
last_error |
最近错误摘要 |
message_rate |
最近窗口内的消息速率 |
lag_seconds |
上游观测时间与本系统接收时间的延迟 |
聚合优先级不能只看 source_priority。例如 aisstream_vessels 默认优先于 barentswatch_vessels,但如果它处于 disconnected 或 lag_seconds 超过 freshness 窗口,则动态字段应回退到更新的可用来源。
身份键边界
v1 可以继续用 MMSI 作为 entity_key,因为它是 AIS 动态消息里最稳定、最容易获得的主键。但文档和模型都要为后续扩展留出口:MMSI 可能复用、填错或缺少静态信息,后续身份解析应结合 mmsi + imo + callsign + name + dimensions 判断是否需要拆分或合并实体。
冲突记录层
聚合服务发现同一个实体、同一个字段存在多个非空不同值时,写入冲突记录。冲突记录不代表错误,只代表“有多个可用候选值”。
{
"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 的目标也不是让用户处理每条冲突,而是把冲突沉淀成字段级规则。
聚合规则
字段分类
| 类型 | 字段 | 默认策略 |
|---|---|---|
| 动态位置 | lat、lon、sog、cog、heading、nav_status |
优先最新 observed_at,同时间再按来源优先级 |
| 静态身份 | name、callsign、imo、flag |
非空优先,再按字段策略或来源优先级 |
| 静态规格 | vessel_type、vessel_type_name、length、width、draught |
非空优先;冲突时记录候选值 |
| 轨迹点 | track_points |
按时间线合并;同一时间窗口内相近点去重;保留点级 source |
| 元信息 | field_sources、conflict_count、selected_reasons、quality_flags |
聚合接口生成,便于调试和后续 UI 展示 |
默认优先级
默认优先级应使用两个维度:
delivery_mode_priority:
- realtime_stream
- batch_stream
- polling
- snapshot
transport_priority:
- websocket
- sse
- http
- file
delivery_mode_priority 是主判断。比如 AISStream 如果提供实时推送,应标记为 realtime_stream + websocket;BarentsWatch 当前是 polling + http。
断流保护
实时流不能永久凭身份占优。聚合时需要 freshness 窗口:
freshness:
realtime_stream_seconds: 900
polling_seconds: 3600
如果 aisstream_vessels 最近 15 分钟没有该 MMSI 的新观测,而 BarentsWatch 轮询源有更新位置,则位置类字段应采用 BarentsWatch 的更新观测,并记录选择原因 newest_observation 或 freshness_fallback。
异常位置保护
多源 AIS 接入后,聚合服务必须过滤或降权明显异常的位置观测:
- 经纬度必须在合法范围内。
observed_at不能明显来自未来。- 同一 MMSI 短时间内跨越不合理距离时,标记
position_jump,默认不直接采用该点。 - 当异常点来自当前优先源时,应记录
selected_reason = anomaly_rejected,再回退到其他可用来源。
异常保护不应静默丢弃事实。原始观测仍应保留,聚合结果通过 quality_flags 和冲突记录解释为什么没有采用它。
轨迹聚合
轨迹接口不能简单拼接所有来源,否则前端会出现折返、抖动和重复点。默认规则:
- 以
observed_at排序,生成统一时间线。 - 同一来源的完全重复点通过
observation_hash去重。 - 多来源在短时间窗口内上报的相近位置视为同一轨迹点,优先选择 freshness 和 source priority 更高的一条。
- 每个轨迹点保留
source、selected_reason和必要的quality_flags。 - 对被判定为
position_jump的点,默认不进入展示轨迹,但可通过调试参数查看。
聚合接口
现有展示接口应逐步改为消费聚合服务,而不是自己直接拼 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"
},
"quality_flags": [],
"conflict_count": 2
}
开放配置计划
Phase 1 — 内置默认策略和只读解释
- 实现后端默认策略。
- 聚合接口返回
field_sources、selected_reasons、conflict_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_aispayload。 - 标记
delivery_mode = realtime_stream,transport = websocket。 - 写入原始观测层。
- 不直接 upsert 最终展示数据。
配置应放入采集器设置,而不是硬编码:
aisstream_vessels:
api_key: "${AISSTREAM_API_KEY}"
bounding_boxes:
- [[-180, -90], [180, 90]]
message_types:
- PositionReport
- ShipStaticData
默认不建议直接订阅全球范围。AISStream 采集器应支持以下订阅策略:
- 使用配置的固定
bounding_boxes。 - 后续支持按 Earth 当前视口或关注区域动态调整订阅范围。
- 支持限制
message_types,避免静态信息、位置报告和扩展消息全量涌入。 - 断线后使用指数退避重连,并把连接状态写入源健康状态。
- 重连后可能收到重复或回放消息,因此必须依赖原始观测层的幂等去重。
媒体富化边界
VesselFinder 等服务里的船只图片不属于 AIS 实时数据本身。图片、船籍详情、公司信息等后续应作为独立 enrichment 链路:
- 通过 MMSI、IMO、船名等字段异步查询。
- 使用独立缓存和授权配置。
- 不阻塞
vessel_ais实时观测入库。 - 聚合接口只暴露已经缓存好的媒体引用,不在请求链路中现场抓取。
版本拆分
计划按 5 个版本推进:
v0 — 聚合基础设施(已实现)
目标是不改变前端展示行为,先把数据底座铺好。
- 新增原始观测模型、冲突记录模型和源健康状态模型。
- 为现有 BarentsWatch collector 写入原始观测,同时保留现有
vessel_position/vessel_static兼容写入。 - 实现存储级
observation_hash幂等去重。 - 补基础管理命令或调试接口,用于查看某个 MMSI 的原始观测和冲突候选。
v1 — 聚合读接口(已实现)
目标是让展示接口开始消费聚合结果,但前端形状保持兼容。
- 实现 AIS 聚合服务,先兼容读取现有表,再逐步切换到原始观测层。
- 将
/geo/vessels和/vessels/{mmsi}改为走聚合服务。 - 将
/vessels/{mmsi}/track改为走轨迹聚合逻辑。 - 返回
field_sources、selected_reasons、quality_flags、conflict_count。 - 加入 freshness fallback 和异常位置保护。
v2 — AISStream WebSocket collector(已实现)
目标是接入第二个真实 AIS 来源,并验证多源冲突和回退逻辑。
- 实现
aisstream_vesselscollector。 - 支持 API key、订阅范围、消息类型、重连和限流配置。
- 将 AISStream 写入原始观测层,不直接 upsert 最终展示表。
- 接入源健康状态和 message rate 统计。
- 提供 AISStream API Key 获取教程、设置页入口和连接验证支持。
- 为重复消息、断流回退、WS 优先级写集成测试。
v3 — AISStream 可用性与配置体验(已实现)
目标是让 AISStream 从“能采集”变成日常可观察、可调试、可配置的数据源。
- 设置页展示 AISStream 运行状态:连接状态、最近收到、最近成功、本轮消息数、延迟和最近错误。
- AISStream 设置页提供常用采集范围 preset,并保留自定义 Bounding Boxes JSON。
- 聚合结果返回
source_summary,展示每艘船的来源、观测数量、最新观测时间、传输模式和消息类型。 - 保留
field_sources和selected_reasons,用于解释动态字段来自实时流、静态字段来自可用非空来源。 - 船名标准化会读取 AISStream
MetaData.ShipName;船型展示会从vessel_type_name和 AIS 数字vessel_type共同归一化,保证 marker 颜色、详情卡、hover 和搜索结果一致。 /geo/vessels不再默认限制 5000 艘;不传limit或传limit=0表示全量返回,前端默认也不再二次裁剪到 5000。
v4 — 策略配置
目标是开放系统级配置,但仍以安全默认值兜底。
- 接入系统设置中的聚合策略配置。
- 支持 source priority、字段级规则、freshness 窗口和高级保护开关。
- 保存配置时校验未知字段、非法模式和危险动态字段锁定。
- 聚合接口返回当前命中的配置版本,方便排查。
v5 — 船舶资料 enrichment 与冲突治理
目标是把 AIS 实时流里不稳定或低频出现的静态信息,补成可缓存、可审计的船舶资料层,同时把冲突解释变成可操作能力。
- 做冲突治理 UI。
- 支持把人工选择沉淀成字段级规则。
- 支持恢复默认策略。
- 设计
vessel_profile_enrichment,按mmsi + imo + name + callsign异步补充船名、船型细分、AIS 大类、旗国、尺寸、建造年份、运营方等静态资料。 - 设计
vessel_media_enrichment,异步补充船只图片和外部详情缓存。 - enrichment 结果必须带
source、fetched_at、expires_at、confidence和原始引用,不覆盖 AIS 原始观测。 - 聚合接口只读取已缓存 enrichment;请求链路不现场抓取第三方页面,避免慢请求和授权风险。
- 前端船只详情面板展示已缓存资料和媒体,并标注字段来源,不阻塞 AIS 实时链路。
测试计划
- 同一来源同一
mmsi + observed_at + lat + lon重复记录只聚合一次。 - 多来源同一 MMSI 的位置字段优先选择最新观测。
- 实时流和轮询源同时间冲突时,实时流优先。
- 实时流过期后,更新的轮询源可以接管动态字段。
- 实时流源健康状态异常时,动态字段可以回退到更新的可用来源。
- 静态字段不会被空值覆盖。
- 静态字段冲突会写入冲突记录。
- 明显异常位置不会进入默认展示轨迹,并会留下
quality_flags。 - 同一时间窗口内多来源相近轨迹点只展示一个点。
- AISStream 重连或回放导致的重复消息不会重复进入聚合结果。
- 字段级配置可以覆盖默认来源优先级。
- 聚合接口在没有冲突表时仍可返回兼容 GeoJSON。