JetWave 轨迹时空服务 · 独立项目技术方案
| 项 | 内容 |
|---|---|
| 版本 | v1.0 |
| 日期 | 2026-07-17 |
| 状态 | 方案设计(待评审) |
| 关联 | trajectory-log-analyzer(TLA)、LOG_ANALYTICS_PLATFORM_PLAN.md |
| 建议项目名 | jetwave-traj-store(独立仓库 + 独立 Docker) |
0. 结论与建议(是否采纳你的思路)
建议采纳:构建一个与 TLA 完全隔离的独立项目,通过 HTTP API 对接 TLA/MySQL 元数据,自建 PostgreSQL + PostGIS + TimescaleDB 存轨迹点,对外提供 REST API + MCP,供后续 App / AI Agent 做地图渲染与时空分析。
| 你的思路 | 评估 |
|---|---|
| 独立项目,与日志分析隔离 | ✅ 正确。TLA 专注会话级 KPI 与故障判定;轨迹点量级、查询模式完全不同,混在一个项目里会互相拖累 |
| 接口对接当前日志分析 | ✅ 正确。不共享写库、不复制 fault_engine;只读 TLA 的会话摘要,避免双份业务逻辑 |
| 独立 Docker | ✅ 正确。PG/PostGIS/Timescale 与 MySQL TLA 生命周期、资源、扩容策略不同 |
| 历史 + 增量日志都构建过来 | ✅ 必须。App 需要全量回放,仅增量不够 |
| MCP + API 给 App | ✅ 正确。API 给 App/前端;MCP 给 Cursor/Agent 做自然语言查轨迹、区域分析 |
不建议的做法:
- ❌ 在 TLA 里直接写 PG(耦合过紧,TLA cron 失败会拖垮轨迹库)
- ❌ App 直连 MySQL 分析表 + OSS 拼轨迹(延迟高、无空间索引、难做框选)
- ❌ 上 pgstac/STAC 标准(过重,JetWave 日志字段固定,自研 Schema 更轻)
1. 系统全景
┌──────────────────────────────────────┐
│ 业务源(不变) │
│ MySQL: surf_motion_trajectory_history│
│ OSS: surf-oss/.../*.txt │
└───────────────┬──────────────────────┘
│
┌──────────────────────────┼──────────────────────────┐
▼ ▼ │
┌─────────────────────┐ ┌─────────────────────┐ │
│ trajectory-log- │ │ jetwave-traj-store │ ★ 新项目 │
│ analyzer (TLA) │ │ (独立 Docker) │ │
│ │ │ │ │
│ MySQL 会话聚合 │ │ PG + PostGIS + │ │
│ 故障 / KPI / 版本 │ │ TimescaleDB │ │
│ cron 每 15min │ │ 轨迹点 + 空间索引 │ │
└──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │
│ HTTP 只读对接 │ │
│◄─────────────────────────┤ │
│ GET /api/v1/sessions │ │
│ (摘要、故障、版本) │ │
│ │ │
│ ├── REST API ──► App / Web │
│ ├── MCP Server ► Cursor/Agent│
│ └── GeoJSON ──► 地图渲染 │
└──────────────────────────┴──────────────────────────┘
1.1 职责边界
| 系统 | 职责 | 存储 | 消费者 |
|---|---|---|---|
| TLA | 故障判定、has_fault、max_current、版本矩阵、日聚合 | MySQL | 后台管理、运营报表 |
| jetwave-traj-store | 轨迹点入库、时空查询、回放、区域筛选、GeoJSON | PostgreSQL | 轨迹 App、地图、MCP Agent |
黄金规则:故障怎么算、等级怎么定 —— 只在 TLA;轨迹库只 同步结果字段,不重新实现 fault_engine。
2. 新项目结构建议
jetwave-traj-store/ # 独立 Git 仓库(Codeup 新建)
├── README.md
├── docker-compose.yml # pg + api + ingest + mcp
├── .env.example
├── Dockerfile.api
├── Dockerfile.ingest
├── Dockerfile.mcp
├── requirements.txt
├── sql/
│ ├── 001_extensions.sql # postgis + timescaledb
│ ├── 002_schema.sql # traj_session + traj_point
│ └── 003_indexes_policies.sql # GiST、压缩、continuous agg
├── jts/ # Jetwave Trajectory Store
│ ├── config.py
│ ├── db/
│ │ ├── connection.py # asyncpg → PG
│ │ └── repository.py
│ ├── ingest/
│ │ ├── worker.py # 增量 ingest 主流程
│ │ ├── oss_client.py # 可复用 TLA 逻辑或独立实现
│ │ ├── tla_client.py # ★ 调 TLA/MySQL 只读 API
│ │ └── point_writer.py # COPY 批量写 traj_point
│ ├── api/
│ │ ├── main.py # FastAPI 入口
│ │ ├── routes/
│ │ │ ├── sessions.py
│ │ │ ├── points.py
│ │ │ ├── search.py # 时空搜索
│ │ │ └── geojson.py
│ │ └── schemas.py # Pydantic 模型
│ └── mcp/
│ ├── server.py # MCP stdio / SSE
│ └── tools.py # search_sessions, get_track, ...
├── run_ingest.py
├── run_api.py
└── tests/
与 monorepo jetwave-fly-logs 的关系:并列目录,不放入 trajectory-log-analyzer/。
jetwave-fly-logs/
├── trajectory-log-analyzer/ # 现有,不动
├── analysis/ # 探索性分析
└── (本方案仅文档,代码在新仓库)
3. 数据模型(PostgreSQL)
3.1 会话表 traj_session
与 TLA 主表 1:1(trajectory_id),摘要字段从 TLA API 同步,几何字段本地计算。
CREATE TABLE traj_session (
trajectory_id BIGINT PRIMARY KEY,
object_key TEXT NOT NULL UNIQUE,
session_ts TIMESTAMPTZ NOT NULL,
session_date DATE,
-- 设备 / 用户
remote_sn TEXT,
board_sn TEXT,
battery_sn TEXT,
play_user_id BIGINT,
user_nickname TEXT,
-- 版本(App 筛选)
bms_version TEXT,
ecu_version TEXT,
esc_version TEXT,
rc_version TEXT,
-- ★ 从 TLA 同步(只读镜像,不做二次计算)
has_fault BOOLEAN DEFAULT FALSE,
has_protocol_fault BOOLEAN DEFAULT FALSE,
has_board_out_of_control BOOLEAN DEFAULT FALSE,
has_high_current BOOLEAN DEFAULT FALSE,
has_warning BOOLEAN DEFAULT FALSE,
fault_severity_display TEXT, -- fault|warning|notice|none
max_current NUMERIC(8,2),
max_speed_kmh NUMERIC(8,2),
stall_points INT DEFAULT 0,
total_points INT,
duration_sec INT,
analyzer_version TEXT,
tla_synced_at TIMESTAMPTZ,
-- 本地计算(PostGIS)
bbox GEOMETRY(Polygon, 4326),
track_line GEOMETRY(LineString, 4326),
track_line_simplified GEOMETRY(LineString, 4326), -- ST_Simplify 供地图
-- 幂等
content_fingerprint CHAR(64),
ingest_status TEXT DEFAULT 'pending', -- pending|done|failed
ingested_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
3.2 轨迹点表 traj_point(Timescale hypertable)
CREATE TABLE traj_point (
trajectory_id BIGINT NOT NULL,
ts TIMESTAMPTZ NOT NULL,
seq INT NOT NULL,
geom GEOMETRY(Point, 4326) NOT NULL,
longitude DOUBLE PRECISION NOT NULL,
latitude DOUBLE PRECISION NOT NULL,
esc_event_code BIGINT DEFAULT 0,
ecu_event_code BIGINT DEFAULT 0,
bms_event_code BIGINT DEFAULT 0,
motor_speed INT,
electric_current NUMERIC(8,2),
throttle INT,
hour_speed NUMERIC(8,2),
battery_voltage NUMERIC(8,2),
battery_temperature NUMERIC(8,2),
motor_temperature NUMERIC(8,2),
mode SMALLINT,
gear SMALLINT,
is_corrupt_speed BOOLEAN DEFAULT FALSE,
PRIMARY KEY (trajectory_id, ts, seq)
);
SELECT create_hypertable('traj_point', 'ts',
chunk_time_interval => INTERVAL '7 days');
CREATE INDEX idx_traj_point_geom ON traj_point USING GIST (geom);
CREATE INDEX idx_traj_point_traj_ts ON traj_point (trajectory_id, ts);
3.3 同步水位表 traj_ingest_job
CREATE TABLE traj_ingest_job (
id BIGSERIAL PRIMARY KEY,
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
finished_at TIMESTAMPTZ,
status TEXT, -- running|success|failed
scanned INT DEFAULT 0,
ingested INT DEFAULT 0,
skipped INT DEFAULT 0,
failed INT DEFAULT 0,
watermark_add_time TIMESTAMPTZ, -- 对齐 TLA 水位思路
error_summary TEXT
);
4. 与 TLA 的对接方式
4.1 推荐:TLA 只读 HTTP API(轻量扩展)
在 trajectory-log-analyzer 增加 可选 薄 API 层(或独立 tla-api 容器,只读 MySQL),不修改分析逻辑:
GET /api/v1/sessions/pending-ingest?since=&limit=50
→ 返回:trajectory_id, object_key, content_fingerprint,
analyzer_version, analyze_status, add_time
GET /api/v1/sessions/{trajectory_id}
→ 返回:surf_motion_trajectory_analysis 全字段(摘要)
GET /api/v1/sessions/{trajectory_id}/fault-details
→ 返回:fault_detail 列表
鉴权:内网 API Key / JWT,仅 jetwave-traj-store ingest 服务可访问。
4.2 备选:MySQL 只读直连(更快落地)
ingest 服务配置 MYSQL_READ_ONLY_DSN,直接查 surf_motion_trajectory_analysis + history。
| 方式 | 优点 | 缺点 |
|---|---|---|
| HTTP API | 解耦、可版本化、TLA 换库不影响 traj-store | 需开发小 API |
| MySQL 只读 | 零 API 开发、立刻能跑 | 耦合 MySQL Schema,字段变更需同步 |
阶段建议:P0 用 MySQL 只读快速验证;P1 补 HTTP API 正式解耦。
4.3 数据流(历史 + 增量)
┌─────────────┐ ① 查待同步列表 ┌─────────────┐
│ TLA / MySQL │ ◄──────────────────────│ ingest │
│ (摘要已有) │ trajectory_id 等 │ worker │
└─────────────┘ └──────┬──────┘
│
② 下载 OSS JSON │
③ 解析 trajectoryList │
④ COPY → traj_point │
⑤ ST_MakeLine → track │
⑥ UPSERT traj_session │
(摘要字段从 TLA 复制) ▼
┌─────────────┐
│ PostgreSQL │
└─────────────┘
增量触发(三选一,可并存):
- Cron 轮询(与 TLA 同频 15min):查
analyze_status=done且 PG 未 ingest 或 fingerprint 变化 - Webhook(TLA 分析完成后 POST
/internal/ingest/{trajectory_id}) - 手动:
POST /admin/backfill?from=2026-01-01
历史回填:BATCH_SIZE=100,按 add_time ASC 扫全表,预计 8000 条约数小时(取决于 OSS 带宽)。
5. REST API 设计(App / Web)
Base URL: https://traj-api.yourdomain.com/api/v1
格式:JSON;轨迹几何:GeoJSON(RFC 7946)
5.1 会话列表
GET /sessions
Query:
from, to ISO8601 时间
remote_sn, board_sn
has_high_current bool
has_board_out_of_control bool
fault_severity fault|warning|notice|none
bbox west,south,east,north (地图视野)
page, page_size
sort -max_current | -session_ts
Response:
{
"total": 120,
"items": [{
"trajectory_id": 467,
"session_ts": "2026-05-17T10:05:13Z",
"remote_sn": "231AAAA5G001585",
"max_current": 185.2,
"has_high_current": true,
"fault_severity_display": "fault",
"total_points": 3384,
"duration_sec": 1800,
"bbox": [120.64, 31.43, 120.65, 31.44]
}]
}
5.2 轨迹线(地图渲染)
GET /sessions/{id}/track
Query:
simplified=true 使用 track_line_simplified
tolerance=0.0001 Douglas-Peucker 容差
Response: GeoJSON Feature
{
"type": "Feature",
"geometry": { "type": "LineString", "coordinates": [[lon,lat],...] },
"properties": { "trajectory_id": 467, "max_current": 185.2 }
}
5.3 轨迹点(回放 / 曲线)
GET /sessions/{id}/points
Query:
from_ts, to_ts 时间窗口(可选)
fields 逗号分隔,默认全量
downsample 每 N 点取 1(大轨迹加速)
Response:
{
"trajectory_id": 467,
"points": [
{ "ts": "...", "lon": 120.645, "lat": 31.434,
"electric_current": 69.2, "motor_speed": 11175, "esc_event_code": 131072 }
]
}
5.4 时空搜索(核心)
POST /search/area
Body:
{
"time_from": "2026-05-01T00:00:00Z",
"time_to": "2026-05-31T23:59:59Z",
"geometry": {
"type": "Polygon",
"coordinates": [[[120.6,31.4],[120.7,31.4],[120.7,31.5],[120.6,31.5],[120.6,31.4]]]
},
"filters": { "has_high_current": true, "min_max_current": 150 }
}
Response: 命中的 session 列表 + 可选命中点统计
POST /search/radius
Body:
{
"center": [120.645, 31.435],
"radius_m": 500,
"time_from": "...",
"time_to": "..."
}
5.5 健康与元数据
GET /health
GET /stats # 总会话数、总点数、最新 ingest 时间
6. MCP Server 设计
供 Cursor / Agent / 内部 AI 以工具方式查轨迹,与 REST 共用 jts/db 层。
6.1 部署
- 容器:
jetwave-traj-store-mcp - 传输:stdio(Cursor MCP)或 SSE(远程,类似现有 jetwave-mcp)
- 配置:
.cursor/mcp.json指向该服务
6.2 工具列表(建议 v1)
| 工具名 | 说明 | 参数 |
|---|---|---|
search_trajectory_sessions |
按时间/设备/高电流/故障等级查会话 | from, to, remote_sn, has_high_current, fault_severity, limit |
get_session_summary |
单会话摘要(含 TLA 同步字段) | trajectory_id |
get_session_track_geojson |
返回 GeoJSON 轨迹线 | trajectory_id, simplified |
get_session_points |
轨迹点序列(回放/分析) | trajectory_id, from_ts, to_ts, max_points |
search_sessions_in_area |
多边形/半径时空搜索 | geometry 或 center+radius_m, time_range, filters |
get_ingest_status |
同步进度、水位 | — |
6.3 示例 Agent 用法
「查一下 5 月份 max_current 超过 150A 且在苏州附近的航行记录」
Agent 调用链:
1. search_sessions_in_area(polygon=..., time_from=..., filters={has_high_current:true})
2. get_session_track_geojson 取 top N 画地图
3. get_session_points 下钻电流曲线
7. Docker 部署
7.1 docker-compose.yml 骨架
services:
traj-postgres:
image: timescale/timescaledb-ha:pg16 # 含 PostGIS
environment:
POSTGRES_DB: jetwave_traj
POSTGRES_USER: jts
POSTGRES_PASSWORD: ${PG_PASSWORD}
volumes:
- traj-pg-data:/var/lib/postgresql/data
- ./sql:/docker-entrypoint-initdb.d:ro
ports:
- "5434:5432" # 避免与现有 5432/5433 冲突
healthcheck:
test: ["CMD-SHELL", "pg_isready -U jts -d jetwave_traj"]
interval: 10s
retries: 5
traj-api:
build:
context: .
dockerfile: Dockerfile.api
env_file: .env
environment:
DATABASE_URL: postgresql://jts:${PG_PASSWORD}@traj-postgres:5432/jetwave_traj
TLA_API_BASE: ${TLA_API_BASE:-} # 可选
MYSQL_READ_DSN: ${MYSQL_READ_DSN:-} # P0 备选
ports:
- "8095:8095"
depends_on:
traj-postgres:
condition: service_healthy
traj-ingest:
build:
context: .
dockerfile: Dockerfile.ingest
env_file: .env
environment:
DATABASE_URL: postgresql://jts:${PG_PASSWORD}@traj-postgres:5432/jetwave_traj
OSS_BASE_URL: ${OSS_BASE_URL}
MYSQL_READ_DSN: ${MYSQL_READ_DSN}
BATCH_SIZE: 50
volumes:
- traj-oss-cache:/data
depends_on:
traj-postgres:
condition: service_healthy
# 不常驻:cron 用 docker compose run --rm traj-ingest
traj-mcp:
build:
context: .
dockerfile: Dockerfile.mcp
env_file: .env
environment:
DATABASE_URL: postgresql://jts:${PG_PASSWORD}@traj-postgres:5432/jetwave_traj
TRAJ_API_BASE: http://traj-api:8095
ports:
- "8096:8096" # SSE 模式
depends_on:
- traj-api
volumes:
traj-pg-data:
traj-oss-cache:
7.2 服务器 cron(与 TLA 并列)
# TLA(现有,不动)
*/15 * * * * cd /home/trajectory-log-analyzer && docker compose run --rm trajectory-log-analyzer >> /var/log/tla.log 2>&1
# 轨迹库 ingest(新增,晚 TLA 2 分钟跑,确保摘要已写好)
2-59/15 * * * * cd /home/jetwave-traj-store && docker compose run --rm traj-ingest >> /var/log/traj-ingest.log 2>&1
7.3 环境变量 (.env.example)
PG_PASSWORD=...
DATABASE_URL=postgresql://jts:...@traj-postgres:5432/jetwave_traj
# TLA 对接(二选一)
TLA_API_BASE=http://host.docker.internal:8094/api/v1
TLA_API_KEY=...
MYSQL_READ_DSN=mysql+pymysql://readonly:...@host:3306/surf-dev?charset=utf8mb4
OSS_BASE_URL=https://surf-oss.oss-accelerate.aliyuncs.com/
BATCH_SIZE=50
FORCE_REINGEST=0
WATERMARK_ADD_TIME=2026-01-01 00:00:00
API_PORT=8095
MCP_PORT=8096
CORS_ORIGINS=https://app.example.com
8. App 对接指南(后续)
App
├── 列表页 → GET /sessions?bbox=&has_high_current=
├── 地图页 → GET /sessions/{id}/track → Mapbox/MapLibre 画 LineString
├── 回放页 → GET /sessions/{id}/points?downsample=5
├── 曲线图 → points[].electric_current / motor_speed → ECharts
└── 区域分析 → POST /search/area
渲染建议:
- 线色按
fault_severity_display:fault 红 / warning 黄 / notice 蓝 / none 绿 - 高电流点:
electric_current > 150在回放时间轴上标红点 - 大数据:
simplified track+ 点downsample,详情页再拉全量
9. 分阶段路线图
| 阶段 | 周期 | 交付 | 验收 |
|---|---|---|---|
| P0 基础 | 1~2 周 | 新仓库 + PG Schema + ingest 单条 OSS + docker-compose | 1 条日志可在 psql 时空查询 |
| P1 回填 | 1~2 周 | 历史 backfill + cron 增量 + TLA 摘要同步 | 8000 会话可 API 列表 |
| P2 API | 2 周 | FastAPI 全路由 + GeoJSON + 区域搜索 | Postman/前端可调通地图 |
| P3 MCP | 1 周 | MCP tools + Cursor 配置 | Agent 能「查高电流轨迹」 |
| P4 App | 按需 | 移动端/Web 轨迹 App | 地图回放 + 筛选 |
10. 风险与对策
| 风险 | 对策 |
|---|---|
| 双写不一致(TLA 有、PG 无) | ingest 以 analyze_status=done 为准;/stats 暴露 lag |
| 存储膨胀 | Timescale 压缩 + 可选只保留 simplified line + 原始点 2 年 |
| OSS 下载慢 | 本地 /data 缓存;ingest 与 TLA 可共享 cache 卷 |
| MySQL Schema 变更 | P1 切 HTTP API;traj_session 只存必要镜像字段 |
| 脏转速点 | ingest 打 is_corrupt_speed,App 默认过滤 |
11. 与现有文档关系
| 文档 | 关系 |
|---|---|
TLA_IMPROVEMENT_PLAN_V2.md |
TLA 继续演进故障/KPI;本方案 只读同步 其输出字段 |
LOG_ANALYTICS_PLATFORM_PLAN.md |
平台 v2 偏 MySQL 报表;本方案是 轨迹 App 专用层 |
FAULT_CODE_GRADED_REFERENCE.md |
故障等级语义;同步到 fault_severity_display,不在 PG 重算 |
12. 下一步行动(建议顺序)
- Codeup 新建仓库
jetwave-traj-store,从本方案sql/初始化 - 服务器起 PG 容器(5434 端口),执行 DDL
- ingest MVP:MySQL 只读 + OSS 下载 + 写 PG(1 个 trajectory_id 跑通)
- 历史 backfill 脚本 + cron
- FastAPI
/sessions+/sessions/{id}/track - MCP 3 个核心 tool
- (可选)TLA 侧加只读 HTTP API,替换 MySQL 直连
附录 A:核心 SQL 示例
-- 时间 + 矩形范围
SELECT trajectory_id, ts, electric_current
FROM traj_point
WHERE ts BETWEEN '2026-05-01' AND '2026-05-31'
AND geom && ST_MakeEnvelope(120.5, 31.3, 121.0, 31.8, 4326);
-- 半径 500m + 高电流会话
SELECT DISTINCT p.trajectory_id, s.max_current
FROM traj_point p
JOIN traj_session s USING (trajectory_id)
WHERE s.has_high_current
AND p.ts >= NOW() - INTERVAL '30 days'
AND ST_DWithin(
p.geom::geography,
ST_SetSRID(ST_MakePoint(120.645, 31.435), 4326)::geography,
500);
附录 B:TLA 需同步字段清单(traj_session 镜像)
从 surf_motion_trajectory_analysis 只读复制:
trajectory_id, object_key, session_date, run_time, remote_sn, board_sn, battery_sn, play_user_id, user_nickname, bms_version, ecu_version, esc_version, rc_version, has_fault, has_protocol_fault, has_board_out_of_control, has_high_current, has_warning, fault_severity_display, max_current, max_speed, stall_points, total_points, duration_sec, content_fingerprint, analyzer_version, analyze_status
不在 PG 存:symptoms, fault_codes JSON(明细走 API 查 MySQL fault_detail 或后续 PG 冗余表)
文档维护:方案变更请更新本文件并同步 analysis/reports/analysis_state.json(若启用进度跟踪)。