← 返回目录

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 做自然语言查轨迹、区域分析

不建议的做法:


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  │
                                        └─────────────┘

增量触发(三选一,可并存):

  1. Cron 轮询(与 TLA 同频 15min):查 analyze_status=done 且 PG 未 ingest 或 fingerprint 变化
  2. Webhook(TLA 分析完成后 POST /internal/ingest/{trajectory_id})
  3. 手动: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 部署

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

渲染建议:


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. 下一步行动(建议顺序)

  1. Codeup 新建仓库 jetwave-traj-store,从本方案 sql/ 初始化
  2. 服务器起 PG 容器(5434 端口),执行 DDL
  3. ingest MVP:MySQL 只读 + OSS 下载 + 写 PG(1 个 trajectory_id 跑通)
  4. 历史 backfill 脚本 + cron
  5. FastAPI /sessions + /sessions/{id}/track
  6. MCP 3 个核心 tool
  7. (可选)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(若启用进度跟踪)。