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)
This commit is contained in:
544
platform/as_platform/fleet/service.py
Normal file
544
platform/as_platform/fleet/service.py
Normal file
@@ -0,0 +1,544 @@
|
||||
"""Fleet map business logic."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from as_platform.db.models import (
|
||||
FleetCollectionRun,
|
||||
FleetRunMilestone,
|
||||
FleetTrackPoint,
|
||||
FleetVehicle,
|
||||
)
|
||||
from as_platform.fleet.geo import haversine_km, simplify_coords
|
||||
from as_platform.fleet.mock_seed import ROUTES
|
||||
from as_platform.redis.bus import publish
|
||||
|
||||
RUN_IDLE_TIMEOUT_MIN = 10
|
||||
|
||||
|
||||
def _parse_ts(ts: str | datetime | None) -> datetime:
|
||||
if ts is None:
|
||||
return datetime.now(timezone.utc)
|
||||
if isinstance(ts, datetime):
|
||||
return ts if ts.tzinfo else ts.replace(tzinfo=timezone.utc)
|
||||
s = str(ts).strip()
|
||||
if s.endswith("Z"):
|
||||
s = s[:-1] + "+00:00"
|
||||
return datetime.fromisoformat(s)
|
||||
|
||||
|
||||
def get_vehicle_by_device(db: Session, device_id: str) -> FleetVehicle | None:
|
||||
return db.query(FleetVehicle).filter_by(tbox_device_id=device_id).first()
|
||||
|
||||
|
||||
def get_active_run(db: Session, vehicle_id: int) -> FleetCollectionRun | None:
|
||||
return (
|
||||
db.query(FleetCollectionRun)
|
||||
.filter_by(vehicle_id=vehicle_id, status="active")
|
||||
.order_by(desc(FleetCollectionRun.id))
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
def recalc_run_mileage(db: Session, run_id: int) -> float:
|
||||
pts = (
|
||||
db.query(FleetTrackPoint)
|
||||
.filter_by(run_id=run_id)
|
||||
.order_by(FleetTrackPoint.ts.asc())
|
||||
.all()
|
||||
)
|
||||
total = 0.0
|
||||
prev = None
|
||||
for p in pts:
|
||||
if prev:
|
||||
total += haversine_km(prev[0], prev[1], p.lat, p.lng)
|
||||
prev = (p.lat, p.lng)
|
||||
run = db.get(FleetCollectionRun, run_id)
|
||||
if run:
|
||||
run.mileage_km = round(total, 3)
|
||||
return total
|
||||
|
||||
|
||||
def _ensure_start_milestone(db: Session, run: FleetCollectionRun, lat: float, lng: float, ts: datetime) -> None:
|
||||
exists = db.query(FleetRunMilestone).filter_by(run_id=run.id, type="start").first()
|
||||
if exists:
|
||||
return
|
||||
db.add(FleetRunMilestone(run_id=run.id, type="start", name="出发", lat=lat, lng=lng, mileage_km=0.0, occurred_at=ts))
|
||||
|
||||
|
||||
def _close_run(db: Session, run: FleetCollectionRun, lat: float, lng: float, ts: datetime) -> None:
|
||||
run.status = "ended"
|
||||
run.ended_at = ts
|
||||
recalc_run_mileage(db, run.id)
|
||||
end_ms = db.query(FleetRunMilestone).filter_by(run_id=run.id, type="end").first()
|
||||
if not end_ms:
|
||||
db.add(
|
||||
FleetRunMilestone(
|
||||
run_id=run.id,
|
||||
type="end",
|
||||
name="结束",
|
||||
lat=lat,
|
||||
lng=lng,
|
||||
mileage_km=run.mileage_km,
|
||||
occurred_at=ts,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def ingest_tbox_gps(db: Session, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
device_id = str(payload.get("device_id") or "").strip()
|
||||
if not device_id:
|
||||
raise ValueError("device_id 必填")
|
||||
lat = float(payload["lat"])
|
||||
lng = float(payload["lng"])
|
||||
ts = _parse_ts(payload.get("ts"))
|
||||
speed = float(payload["speed_kmh"]) if payload.get("speed_kmh") is not None else None
|
||||
heading = float(payload["heading"]) if payload.get("heading") is not None else None
|
||||
run_signal = str(payload.get("run_signal") or "active").lower()
|
||||
plate_no = str(payload.get("plate_no") or "").strip()
|
||||
|
||||
vehicle = get_vehicle_by_device(db, device_id)
|
||||
if not vehicle:
|
||||
vehicle = FleetVehicle(
|
||||
plate_no=plate_no or device_id,
|
||||
tbox_device_id=device_id,
|
||||
name=plate_no or device_id,
|
||||
team="T-Box",
|
||||
status="active",
|
||||
online=True,
|
||||
)
|
||||
db.add(vehicle)
|
||||
db.flush()
|
||||
|
||||
vehicle.last_lat = lat
|
||||
vehicle.last_lng = lng
|
||||
vehicle.last_speed_kmh = speed
|
||||
vehicle.last_ts = ts
|
||||
vehicle.online = True
|
||||
|
||||
run = get_active_run(db, vehicle.id)
|
||||
if run_signal in ("idle", "off", "end", "stopped"):
|
||||
if run:
|
||||
_close_run(db, run, lat, lng, ts)
|
||||
vehicle.online = False
|
||||
db.flush()
|
||||
publish("fleet.gps", {"vehicle_id": vehicle.id, "device_id": device_id, "lat": lat, "lng": lng})
|
||||
return {"ok": True, "vehicle": vehicle.to_dict(), "run": run.to_dict() if run else None, "closed": True}
|
||||
|
||||
if not run:
|
||||
run_no = f"{device_id}-{ts.strftime('%Y%m%d%H%M')}"
|
||||
run = FleetCollectionRun(
|
||||
vehicle_id=vehicle.id,
|
||||
run_no=run_no,
|
||||
engineer=payload.get("engineer"),
|
||||
project=payload.get("project"),
|
||||
batch=payload.get("batch"),
|
||||
started_at=ts,
|
||||
status="active",
|
||||
source="tbox",
|
||||
)
|
||||
db.add(run)
|
||||
db.flush()
|
||||
_ensure_start_milestone(db, run, lat, lng, ts)
|
||||
|
||||
prev = (
|
||||
db.query(FleetTrackPoint)
|
||||
.filter_by(run_id=run.id)
|
||||
.order_by(desc(FleetTrackPoint.ts))
|
||||
.first()
|
||||
)
|
||||
seg = 0.0
|
||||
if prev:
|
||||
seg = haversine_km(prev.lat, prev.lng, lat, lng)
|
||||
db.add(FleetTrackPoint(run_id=run.id, ts=ts, lat=lat, lng=lng, speed_kmh=speed, heading=heading))
|
||||
run.mileage_km = round((run.mileage_km or 0.0) + seg, 3)
|
||||
db.flush()
|
||||
publish("fleet.gps", {"vehicle_id": vehicle.id, "run_id": run.id, "lat": lat, "lng": lng})
|
||||
return {"ok": True, "vehicle": vehicle.to_dict(), "run": run.to_dict()}
|
||||
|
||||
|
||||
def list_vehicles(db: Session) -> list[dict]:
|
||||
return [v.to_dict() for v in db.query(FleetVehicle).order_by(FleetVehicle.id).all()]
|
||||
|
||||
|
||||
def get_vehicle(db: Session, vehicle_id: int) -> dict | None:
|
||||
v = db.get(FleetVehicle, vehicle_id)
|
||||
return v.to_dict() if v else None
|
||||
|
||||
|
||||
def create_vehicle(db: Session, data: dict[str, Any]) -> dict:
|
||||
device_id = str(data.get("tbox_device_id") or "").strip()
|
||||
plate_no = str(data.get("plate_no") or "").strip()
|
||||
if not device_id:
|
||||
raise ValueError("tbox_device_id 必填")
|
||||
if not plate_no:
|
||||
raise ValueError("plate_no 必填")
|
||||
if get_vehicle_by_device(db, device_id):
|
||||
raise ValueError(f"设备 ID 已存在: {device_id}")
|
||||
v = FleetVehicle(
|
||||
plate_no=plate_no,
|
||||
tbox_device_id=device_id,
|
||||
name=str(data.get("name") or plate_no).strip(),
|
||||
team=str(data.get("team") or "").strip() or None,
|
||||
status=str(data.get("status") or "active").strip() or "active",
|
||||
online=False,
|
||||
)
|
||||
db.add(v)
|
||||
db.flush()
|
||||
return v.to_dict()
|
||||
|
||||
|
||||
def update_vehicle(db: Session, vehicle_id: int, data: dict[str, Any]) -> dict:
|
||||
v = db.get(FleetVehicle, vehicle_id)
|
||||
if not v:
|
||||
raise ValueError("车辆不存在")
|
||||
if "tbox_device_id" in data and data["tbox_device_id"]:
|
||||
new_dev = str(data["tbox_device_id"]).strip()
|
||||
other = get_vehicle_by_device(db, new_dev)
|
||||
if other and other.id != vehicle_id:
|
||||
raise ValueError(f"设备 ID 已被占用: {new_dev}")
|
||||
v.tbox_device_id = new_dev
|
||||
if "plate_no" in data and data["plate_no"]:
|
||||
v.plate_no = str(data["plate_no"]).strip()
|
||||
if "name" in data:
|
||||
v.name = str(data["name"] or v.plate_no).strip()
|
||||
if "team" in data:
|
||||
v.team = str(data["team"]).strip() or None
|
||||
if "status" in data and data["status"]:
|
||||
v.status = str(data["status"]).strip()
|
||||
db.flush()
|
||||
return v.to_dict()
|
||||
|
||||
|
||||
def delete_vehicle(db: Session, vehicle_id: int) -> None:
|
||||
v = db.get(FleetVehicle, vehicle_id)
|
||||
if not v:
|
||||
raise ValueError("车辆不存在")
|
||||
if get_active_run(db, vehicle_id):
|
||||
raise ValueError("该车有进行中的趟次,请先结束趟次后再删除")
|
||||
db.delete(v)
|
||||
|
||||
|
||||
def _run_duration_sec(run: FleetCollectionRun) -> int:
|
||||
if not run.started_at:
|
||||
return 0
|
||||
end = run.ended_at
|
||||
if end is None and run.status == "active":
|
||||
end = datetime.now(timezone.utc)
|
||||
if end is None:
|
||||
return 0
|
||||
return max(0, int((end - run.started_at).total_seconds()))
|
||||
|
||||
|
||||
def list_runs(
|
||||
db: Session,
|
||||
vehicle_id: int | None = None,
|
||||
status: str | None = None,
|
||||
*,
|
||||
offset: int = 0,
|
||||
limit: int = 20,
|
||||
) -> dict[str, Any]:
|
||||
q = db.query(FleetCollectionRun).order_by(desc(FleetCollectionRun.started_at))
|
||||
if vehicle_id:
|
||||
q = q.filter_by(vehicle_id=vehicle_id)
|
||||
if status:
|
||||
q = q.filter_by(status=status)
|
||||
total = q.count()
|
||||
rows = q.offset(max(0, offset)).limit(max(1, limit)).all()
|
||||
out: list[dict] = []
|
||||
for r in rows:
|
||||
d = r.to_dict()
|
||||
d["point_count"] = db.query(FleetTrackPoint).filter_by(run_id=r.id).count()
|
||||
d["milestone_count"] = db.query(FleetRunMilestone).filter_by(run_id=r.id).count()
|
||||
d["duration_sec"] = _run_duration_sec(r)
|
||||
out.append(d)
|
||||
return {"items": out, "total": total, "offset": offset, "limit": limit}
|
||||
|
||||
|
||||
def get_run_detail(db: Session, run_id: int) -> dict | None:
|
||||
run = db.get(FleetCollectionRun, run_id)
|
||||
if not run:
|
||||
return None
|
||||
vehicle = db.get(FleetVehicle, run.vehicle_id)
|
||||
milestones = (
|
||||
db.query(FleetRunMilestone).filter_by(run_id=run_id).order_by(FleetRunMilestone.occurred_at.asc()).all()
|
||||
)
|
||||
return {
|
||||
"run": run.to_dict(),
|
||||
"vehicle": vehicle.to_dict() if vehicle else None,
|
||||
"milestones": [m.to_dict() for m in milestones],
|
||||
}
|
||||
|
||||
|
||||
def close_idle_runs(db: Session) -> int:
|
||||
"""无 GPS 超过 RUN_IDLE_TIMEOUT_MIN 的 active 趟次自动结束。"""
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(minutes=RUN_IDLE_TIMEOUT_MIN)
|
||||
closed = 0
|
||||
for run in db.query(FleetCollectionRun).filter_by(status="active").all():
|
||||
vehicle = db.get(FleetVehicle, run.vehicle_id)
|
||||
last_ts = vehicle.last_ts if vehicle else None
|
||||
if not last_ts or last_ts >= cutoff:
|
||||
continue
|
||||
lat = float(vehicle.last_lat or 0) if vehicle else 0.0
|
||||
lng = float(vehicle.last_lng or 0) if vehicle else 0.0
|
||||
_close_run(db, run, lat, lng, datetime.now(timezone.utc))
|
||||
if vehicle:
|
||||
vehicle.online = False
|
||||
closed += 1
|
||||
return closed
|
||||
|
||||
|
||||
def ingest_tbox_gps_batch(db: Session, points: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
if not points:
|
||||
raise ValueError("points 不能为空")
|
||||
if len(points) > 100:
|
||||
raise ValueError("单次最多 100 个点")
|
||||
results: list[dict[str, Any]] = []
|
||||
for payload in points:
|
||||
results.append(ingest_tbox_gps(db, payload))
|
||||
return {"ok": True, "count": len(results), "last": results[-1] if results else None}
|
||||
|
||||
|
||||
def _parse_csv_track(content: str) -> list[tuple[float, float, datetime | None]]:
|
||||
import csv
|
||||
from io import StringIO
|
||||
|
||||
rows = list(csv.reader(StringIO(content.strip())))
|
||||
if not rows:
|
||||
return []
|
||||
start = 0
|
||||
header = [c.strip().lower() for c in rows[0]]
|
||||
if any(h in header for h in ("lat", "lng", "lon", "latitude", "longitude")):
|
||||
start = 1
|
||||
points: list[tuple[float, float, datetime | None]] = []
|
||||
for row in rows[start:]:
|
||||
if len(row) < 2:
|
||||
continue
|
||||
try:
|
||||
if start and len(header) >= 2:
|
||||
idx = {h: i for i, h in enumerate(header)}
|
||||
lat_i = idx.get("lat", idx.get("latitude", 0))
|
||||
lng_i = idx.get("lng", idx.get("lon", idx.get("longitude", 1)))
|
||||
lat = float(row[lat_i])
|
||||
lng = float(row[lng_i])
|
||||
ts_i = idx.get("ts") or idx.get("time")
|
||||
ts = _parse_ts(row[ts_i]) if ts_i is not None and ts_i < len(row) else None
|
||||
else:
|
||||
lat = float(row[0])
|
||||
lng = float(row[1])
|
||||
ts = _parse_ts(row[2]) if len(row) > 2 and row[2].strip() else None
|
||||
points.append((lat, lng, ts))
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
return points
|
||||
|
||||
|
||||
def import_csv_run(db: Session, *, vehicle_id: int, csv_content: str, note: str | None = None, project: str | None = None, batch: str | None = None) -> dict[str, Any]:
|
||||
vehicle = db.get(FleetVehicle, vehicle_id)
|
||||
if not vehicle:
|
||||
raise ValueError("车辆不存在")
|
||||
pts = _parse_csv_track(csv_content)
|
||||
if len(pts) < 2:
|
||||
raise ValueError("CSV 轨迹点不足")
|
||||
active = get_active_run(db, vehicle_id)
|
||||
if active:
|
||||
_close_run(db, active, pts[0][0], pts[0][1], pts[0][2] or datetime.now(timezone.utc))
|
||||
start_ts = pts[0][2] or datetime.now(timezone.utc)
|
||||
run_no = f"csv-{vehicle_id}-{int(start_ts.timestamp())}"
|
||||
run = FleetCollectionRun(
|
||||
vehicle_id=vehicle_id,
|
||||
run_no=run_no,
|
||||
status="ended",
|
||||
source="csv",
|
||||
note=note,
|
||||
project=project,
|
||||
batch=batch,
|
||||
started_at=start_ts,
|
||||
ended_at=pts[-1][2] or datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(run)
|
||||
db.flush()
|
||||
for lat, lng, ts in pts:
|
||||
t = ts or start_ts
|
||||
db.add(FleetTrackPoint(run_id=run.id, lat=lat, lng=lng, ts=t, speed_kmh=0.0))
|
||||
_ensure_start_milestone(db, run, pts[0][0], pts[0][1], start_ts)
|
||||
_close_run(db, run, pts[-1][0], pts[-1][1], pts[-1][2] or datetime.now(timezone.utc))
|
||||
recalc_run_mileage(db, run.id)
|
||||
return {"ok": True, "run_id": run.id, "point_count": len(pts), "mileage_km": run.mileage_km}
|
||||
|
||||
|
||||
def get_live_fleet(db: Session) -> dict[str, Any]:
|
||||
close_idle_runs(db)
|
||||
vehicles = db.query(FleetVehicle).order_by(FleetVehicle.id).all()
|
||||
items = []
|
||||
for v in vehicles:
|
||||
active = get_active_run(db, v.id)
|
||||
items.append(
|
||||
{
|
||||
**v.to_dict(),
|
||||
"active_run_id": active.id if active else None,
|
||||
"active_mileage_km": active.mileage_km if active else None,
|
||||
"active_run_no": active.run_no if active else None,
|
||||
}
|
||||
)
|
||||
active_runs = db.query(FleetCollectionRun).filter_by(status="active").count()
|
||||
total_km = sum(r.mileage_km or 0.0 for r in db.query(FleetCollectionRun).all())
|
||||
return {
|
||||
"vehicles": items,
|
||||
"stats": {
|
||||
"vehicle_count": len(vehicles),
|
||||
"online_count": sum(1 for v in vehicles if v.online),
|
||||
"active_runs": active_runs,
|
||||
"total_mileage_km": round(total_km, 2),
|
||||
},
|
||||
"mock": True,
|
||||
}
|
||||
|
||||
|
||||
def get_run_track_geojson(db: Session, run_id: int, max_points: int = 5000) -> dict:
|
||||
pts = (
|
||||
db.query(FleetTrackPoint)
|
||||
.filter_by(run_id=run_id)
|
||||
.order_by(FleetTrackPoint.ts.asc())
|
||||
.all()
|
||||
)
|
||||
coords = [[p.lng, p.lat] for p in pts]
|
||||
coords = simplify_coords(coords, max_points)
|
||||
milestones = db.query(FleetRunMilestone).filter_by(run_id=run_id).all()
|
||||
return {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {"kind": "track", "run_id": run_id, "point_count": len(pts)},
|
||||
"geometry": {"type": "LineString", "coordinates": coords},
|
||||
},
|
||||
*[
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {"kind": "milestone", **m.to_dict()},
|
||||
"geometry": {"type": "Point", "coordinates": [m.lng, m.lat]},
|
||||
}
|
||||
for m in milestones
|
||||
],
|
||||
],
|
||||
"points": [p.to_dict() for p in pts[-200:]],
|
||||
}
|
||||
|
||||
|
||||
def fleet_summary(db: Session) -> dict:
|
||||
runs = db.query(FleetCollectionRun).all()
|
||||
by_vehicle: dict[int, float] = {}
|
||||
for r in runs:
|
||||
by_vehicle[r.vehicle_id] = by_vehicle.get(r.vehicle_id, 0.0) + (r.mileage_km or 0.0)
|
||||
vehicles = {v.id: v for v in db.query(FleetVehicle).all()}
|
||||
per_vehicle = [
|
||||
{
|
||||
"vehicle_id": vid,
|
||||
"plate_no": vehicles[vid].plate_no if vid in vehicles else str(vid),
|
||||
"mileage_km": round(km, 2),
|
||||
}
|
||||
for vid, km in by_vehicle.items()
|
||||
]
|
||||
return {
|
||||
"total_runs": len(runs),
|
||||
"active_runs": sum(1 for r in runs if r.status == "active"),
|
||||
"total_mileage_km": round(sum(r.mileage_km or 0.0 for r in runs), 2),
|
||||
"per_vehicle": per_vehicle,
|
||||
}
|
||||
|
||||
|
||||
def simulate_tick(db: Session) -> int:
|
||||
"""Advance mock vehicles along ROUTES (demo)."""
|
||||
n = 0
|
||||
now = datetime.now(timezone.utc)
|
||||
for v in db.query(FleetVehicle).all():
|
||||
meta = v.meta()
|
||||
route_key = meta.get("sim_route") or v.tbox_device_id
|
||||
coords = ROUTES.get(route_key)
|
||||
if not coords:
|
||||
continue
|
||||
run = get_active_run(db, v.id)
|
||||
if not run:
|
||||
continue
|
||||
idx = int(meta.get("sim_index", 0))
|
||||
direction = int(meta.get("sim_dir", 1))
|
||||
next_idx = idx + direction
|
||||
if next_idx >= len(coords):
|
||||
next_idx = len(coords) - 2
|
||||
direction = -1
|
||||
elif next_idx < 0:
|
||||
next_idx = 1
|
||||
direction = 1
|
||||
lat, lng = coords[next_idx]
|
||||
payload = {
|
||||
"device_id": v.tbox_device_id,
|
||||
"lat": lat,
|
||||
"lng": lng,
|
||||
"ts": now.isoformat(),
|
||||
"speed_kmh": 38.0 + (next_idx % 4),
|
||||
"run_signal": "active",
|
||||
}
|
||||
ingest_tbox_gps(db, payload)
|
||||
v.set_meta({**meta, "sim_index": next_idx, "sim_route": route_key, "sim_dir": direction})
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def _parse_gpx_points(content: str) -> list[tuple[float, float, datetime | None]]:
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
root = ET.fromstring(content)
|
||||
ns = ""
|
||||
if root.tag.startswith("{"):
|
||||
ns = root.tag.split("}")[0] + "}"
|
||||
points: list[tuple[float, float, datetime | None]] = []
|
||||
for trkpt in root.iter(f"{ns}trkpt"):
|
||||
lat_s = trkpt.get("lat")
|
||||
lon_s = trkpt.get("lon")
|
||||
if lat_s is None or lon_s is None:
|
||||
continue
|
||||
ts_el = trkpt.find(f"{ns}time")
|
||||
ts = _parse_ts(ts_el.text) if ts_el is not None and ts_el.text else None
|
||||
points.append((float(lat_s), float(lon_s), ts))
|
||||
return points
|
||||
|
||||
|
||||
def import_gpx_run(db: Session, *, vehicle_id: int, gpx_content: str, note: str | None = None) -> dict[str, Any]:
|
||||
vehicle = db.get(FleetVehicle, vehicle_id)
|
||||
if not vehicle:
|
||||
raise ValueError("车辆不存在")
|
||||
pts = _parse_gpx_points(gpx_content)
|
||||
if len(pts) < 2:
|
||||
raise ValueError("GPX 轨迹点不足")
|
||||
active = get_active_run(db, vehicle_id)
|
||||
if active:
|
||||
_close_run(db, active, pts[0][0], pts[0][1], pts[0][2] or datetime.now(timezone.utc))
|
||||
start_ts = pts[0][2] or datetime.now(timezone.utc)
|
||||
run_no = f"gpx-{vehicle_id}-{int(start_ts.timestamp())}"
|
||||
run = FleetCollectionRun(
|
||||
vehicle_id=vehicle_id,
|
||||
run_no=run_no,
|
||||
status="ended",
|
||||
source="gpx",
|
||||
note=note,
|
||||
started_at=start_ts,
|
||||
ended_at=pts[-1][2] or datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(run)
|
||||
db.flush()
|
||||
prev = None
|
||||
for lat, lng, ts in pts:
|
||||
t = ts or start_ts
|
||||
db.add(FleetTrackPoint(run_id=run.id, lat=lat, lng=lng, ts=t, speed_kmh=0.0))
|
||||
if prev is None:
|
||||
_ensure_start_milestone(db, run, lat, lng, t)
|
||||
prev = (lat, lng)
|
||||
end_ts = pts[-1][2] or datetime.now(timezone.utc)
|
||||
_close_run(db, run, pts[-1][0], pts[-1][1], end_ts)
|
||||
recalc_run_mileage(db, run.id)
|
||||
return {"ok": True, "run_id": run.id, "point_count": len(pts), "mileage_km": run.mileage_km}
|
||||
Reference in New Issue
Block a user