Files
HSAP/platform/as_platform/api/fleet_routes.py
Chengfang Lu e72bc061c5 feat: HSAP platform v2 — modular navigation, quality review, audit log, world model simulation
Major changes:
- New frontend (platform/web/): Vite + React 18 + TypeScript + Tailwind
- 4-module navigation: 数据送标 / 模型管理 / 车队管理 / 系统管理
- Data catalog with charts (DMS/ADAS/Lane 3-tab view)
- Quality review workflow (标注质检): Good/Fine/Bad scoring with auto-advance
- Audit enhancements: batch operations, rejection categories, Feishu notifications
- Operation audit log (操作日志)
- World model simulation studio (仿真工坊)
- Dataset version management with snapshots and diff
- ADAS 7-class dataset integration (138K images organized + compressed)
- User management with Feishu integration and pagination
- CRUD/search/filter on all pages, card layout redesign
- PIL-optimized image overlay rendering
- Auto-snapshot on build, in_review workflow stage
- Removed embedded algorithm code (now in workspace)
2026-06-03 11:40:21 +08:00

345 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Fleet map API routes."""
from __future__ import annotations
import json
from typing import Annotated, Any
from fastapi import APIRouter, Depends, File, Form, Header, HTTPException, Query, UploadFile
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from as_platform.auth.deps import get_current_user, require_permission
from as_platform.config import AMAP_KEY, FLEET_MAP_ENABLED, MAP_TILE_PROVIDER, TBOX_INGEST_TOKEN
from as_platform.db.engine import get_db
from as_platform.db.models import User
from as_platform.fleet import service as fleet_svc
from as_platform.fleet.mock_seed import reseed_demo_fleet, seed_demo_fleet
router = APIRouter(tags=["fleet"])
class VehicleBody(BaseModel):
plate_no: str
tbox_device_id: str
name: str | None = None
team: str | None = None
status: str | None = "active"
class VehiclePatchBody(BaseModel):
plate_no: str | None = None
tbox_device_id: str | None = None
name: str | None = None
team: str | None = None
status: str | None = None
class TboxGpsBody(BaseModel):
device_id: str
lat: float
lng: float
ts: str | None = None
speed_kmh: float | None = None
heading: float | None = None
plate_no: str | None = None
run_signal: str | None = "active"
engineer: str | None = None
project: str | None = None
batch: str | None = None
class TboxGpsBatchBody(BaseModel):
points: list[TboxGpsBody] = Field(..., min_length=1, max_length=100)
def _check_fleet_enabled() -> None:
if not FLEET_MAP_ENABLED:
raise HTTPException(503, "车队地图功能未启用 (AS_FLEET_MAP_ENABLED)")
def _verify_tbox_token(x_tbox_token: str | None = None, authorization: str | None = None) -> None:
token = (x_tbox_token or "").strip()
if not token and authorization:
parts = authorization.split(None, 1)
if len(parts) == 2 and parts[0].lower() == "bearer":
token = parts[1].strip()
if not TBOX_INGEST_TOKEN:
return
if token != TBOX_INGEST_TOKEN:
raise HTTPException(401, "T-Box Token 无效")
@router.get("/api/v1/fleet/map-config")
def fleet_map_config(_user: Annotated[User, Depends(require_permission("read:fleet"))]) -> dict[str, Any]:
_check_fleet_enabled()
tile_provider = MAP_TILE_PROVIDER if MAP_TILE_PROVIDER in ("gaode", "osm") else "gaode"
return {
"enabled": True,
"provider": tile_provider,
"tileProvider": tile_provider,
"amapKey": AMAP_KEY or "",
"pollIntervalSec": 5,
"note": "GPS 为 WGS84高德底图为 GCJ-02演示轨迹可能有轻微偏移",
}
@router.get("/api/v1/fleet/live")
def fleet_live(_user: Annotated[User, Depends(require_permission("read:fleet"))], db: Annotated[Session, Depends(get_db)]) -> dict[str, Any]:
_check_fleet_enabled()
return fleet_svc.get_live_fleet(db)
@router.get("/api/v1/fleet/vehicles")
def fleet_vehicles(_user: Annotated[User, Depends(require_permission("read:fleet"))], db: Annotated[Session, Depends(get_db)]) -> dict[str, Any]:
_check_fleet_enabled()
return {"items": fleet_svc.list_vehicles(db)}
@router.get("/api/v1/fleet/vehicles/{vehicle_id}")
def fleet_vehicle_get(
vehicle_id: int,
_user: Annotated[User, Depends(require_permission("read:fleet"))],
db: Annotated[Session, Depends(get_db)],
) -> dict[str, Any]:
_check_fleet_enabled()
item = fleet_svc.get_vehicle(db, vehicle_id)
if not item:
raise HTTPException(404, "车辆不存在")
return item
@router.post("/api/v1/fleet/vehicles")
def fleet_vehicle_create(
body: VehicleBody,
_user: Annotated[User, Depends(require_permission("write:fleet"))],
db: Annotated[Session, Depends(get_db)],
) -> dict[str, Any]:
_check_fleet_enabled()
try:
item = fleet_svc.create_vehicle(db, body.model_dump())
db.commit()
return {"ok": True, "vehicle": item}
except ValueError as e:
raise HTTPException(400, str(e)) from e
@router.patch("/api/v1/fleet/vehicles/{vehicle_id}")
def fleet_vehicle_update(
vehicle_id: int,
body: VehiclePatchBody,
_user: Annotated[User, Depends(require_permission("write:fleet"))],
db: Annotated[Session, Depends(get_db)],
) -> dict[str, Any]:
_check_fleet_enabled()
try:
item = fleet_svc.update_vehicle(db, vehicle_id, body.model_dump(exclude_unset=True))
db.commit()
return {"ok": True, "vehicle": item}
except ValueError as e:
raise HTTPException(400, str(e)) from e
@router.delete("/api/v1/fleet/vehicles/{vehicle_id}")
def fleet_vehicle_delete(
vehicle_id: int,
_user: Annotated[User, Depends(require_permission("write:fleet"))],
db: Annotated[Session, Depends(get_db)],
) -> dict[str, Any]:
_check_fleet_enabled()
try:
fleet_svc.delete_vehicle(db, vehicle_id)
db.commit()
return {"ok": True}
except ValueError as e:
raise HTTPException(400, str(e)) from e
@router.get("/api/v1/fleet/runs")
def fleet_runs(
_user: Annotated[User, Depends(require_permission("read:fleet"))],
db: Annotated[Session, Depends(get_db)],
vehicle_id: int | None = None,
status: str | None = None,
offset: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
) -> dict[str, Any]:
_check_fleet_enabled()
return fleet_svc.list_runs(db, vehicle_id=vehicle_id, status=status, offset=offset, limit=limit)
@router.get("/api/v1/fleet/runs/{run_id}")
def fleet_run_detail(run_id: int, _user: Annotated[User, Depends(require_permission("read:fleet"))], db: Annotated[Session, Depends(get_db)]) -> dict[str, Any]:
_check_fleet_enabled()
detail = fleet_svc.get_run_detail(db, run_id)
if not detail:
raise HTTPException(404, "趟次不存在")
return detail
@router.get("/api/v1/fleet/runs/{run_id}/track")
def fleet_run_track(run_id: int, _user: Annotated[User, Depends(require_permission("read:fleet"))], db: Annotated[Session, Depends(get_db)]) -> dict[str, Any]:
_check_fleet_enabled()
return fleet_svc.get_run_track_geojson(db, run_id)
@router.get("/api/v1/fleet/summary")
def fleet_summary(_user: Annotated[User, Depends(require_permission("read:fleet"))], db: Annotated[Session, Depends(get_db)]) -> dict[str, Any]:
_check_fleet_enabled()
return fleet_svc.fleet_summary(db)
@router.post("/api/v1/fleet/mock/seed")
def fleet_mock_seed(
_user: Annotated[User, Depends(require_permission("write:fleet"))],
db: Annotated[Session, Depends(get_db)],
) -> dict[str, Any]:
_check_fleet_enabled()
created = seed_demo_fleet(db)
db.commit()
return {"ok": True, "created": created}
@router.post("/api/v1/fleet/mock/reseed")
def fleet_mock_reseed(
_user: Annotated[User, Depends(require_permission("write:fleet"))],
db: Annotated[Session, Depends(get_db)],
) -> dict[str, Any]:
"""清空车队演示数据并重新注入(切换区域后使用)。"""
_check_fleet_enabled()
created = reseed_demo_fleet(db)
db.commit()
return {"ok": True, "created": created, "region": "changsha"}
@router.post("/api/v1/fleet/mock/tick")
def fleet_mock_tick(
_user: Annotated[User, Depends(require_permission("write:fleet"))],
db: Annotated[Session, Depends(get_db)],
) -> dict[str, Any]:
_check_fleet_enabled()
n = fleet_svc.simulate_tick(db)
db.commit()
return {"ok": True, "advanced": n}
@router.post("/api/v1/fleet/runs/import-gpx")
async def fleet_import_gpx(
_user: Annotated[User, Depends(require_permission("write:fleet"))],
db: Annotated[Session, Depends(get_db)],
vehicle_id: int = Form(...),
file: UploadFile = File(...),
note: str | None = Form(None),
) -> dict[str, Any]:
_check_fleet_enabled()
raw = await file.read()
try:
text = raw.decode("utf-8")
except UnicodeDecodeError:
text = raw.decode("utf-8", errors="ignore")
try:
result = fleet_svc.import_gpx_run(db, vehicle_id=vehicle_id, gpx_content=text, note=note)
db.commit()
return result
except ValueError as e:
raise HTTPException(400, str(e)) from e
@router.get("/api/v1/fleet/stream")
def fleet_stream(
_user: Annotated[User, Depends(require_permission("read:fleet"))],
) -> StreamingResponse:
"""SSE推送 fleet.gps 事件(需 Redis"""
_check_fleet_enabled()
from as_platform.redis.bus import get_redis
def event_generator():
r = get_redis()
if not r:
yield f"data: {json.dumps({'event': 'error', 'message': 'redis unavailable'}, ensure_ascii=False)}\n\n"
return
pubsub = r.pubsub()
pubsub.subscribe("as:events")
yield f"data: {json.dumps({'event': 'fleet.connected'}, ensure_ascii=False)}\n\n"
while True:
msg = pubsub.get_message(timeout=25)
if msg is None:
yield ": ping\n\n"
continue
if msg.get("type") != "message":
continue
raw = msg.get("data")
if isinstance(raw, bytes):
raw = raw.decode("utf-8", errors="ignore")
if isinstance(raw, str) and "fleet." in raw:
yield f"data: {raw}\n\n"
return StreamingResponse(event_generator(), media_type="text/event-stream")
@router.post("/api/v1/tbox/gps")
def tbox_gps(
body: TboxGpsBody,
db: Annotated[Session, Depends(get_db)],
x_tbox_token: Annotated[str | None, Header(alias="X-Tbox-Token")] = None,
authorization: Annotated[str | None, Header()] = None,
) -> dict[str, Any]:
_check_fleet_enabled()
_verify_tbox_token(x_tbox_token, authorization)
try:
fleet_svc.close_idle_runs(db)
result = fleet_svc.ingest_tbox_gps(db, body.model_dump())
db.commit()
return result
except ValueError as e:
raise HTTPException(400, str(e)) from e
@router.post("/api/v1/tbox/gps/batch")
def tbox_gps_batch(
body: TboxGpsBatchBody,
db: Annotated[Session, Depends(get_db)],
x_tbox_token: Annotated[str | None, Header(alias="X-Tbox-Token")] = None,
authorization: Annotated[str | None, Header()] = None,
) -> dict[str, Any]:
_check_fleet_enabled()
_verify_tbox_token(x_tbox_token, authorization)
try:
fleet_svc.close_idle_runs(db)
result = fleet_svc.ingest_tbox_gps_batch(db, [p.model_dump() for p in body.points])
db.commit()
return result
except ValueError as e:
raise HTTPException(400, str(e)) from e
@router.post("/api/v1/fleet/runs/import-csv")
async def fleet_import_csv(
_user: Annotated[User, Depends(require_permission("write:fleet"))],
db: Annotated[Session, Depends(get_db)],
vehicle_id: int = Form(...),
file: UploadFile = File(...),
note: str | None = Form(None),
project: str | None = Form(None),
batch: str | None = Form(None),
) -> dict[str, Any]:
_check_fleet_enabled()
raw = await file.read()
try:
text = raw.decode("utf-8")
except UnicodeDecodeError:
text = raw.decode("utf-8", errors="ignore")
try:
result = fleet_svc.import_csv_run(
db,
vehicle_id=vehicle_id,
csv_content=text,
note=note,
project=project,
batch=batch,
)
db.commit()
return result
except ValueError as e:
raise HTTPException(400, str(e)) from e