diff --git a/vehicle-data-platform/apps/api/internal/platform/handler_test.go b/vehicle-data-platform/apps/api/internal/platform/handler_test.go index 530f5a3a..b2d71c2a 100644 --- a/vehicle-data-platform/apps/api/internal/platform/handler_test.go +++ b/vehicle-data-platform/apps/api/internal/platform/handler_test.go @@ -56,7 +56,7 @@ func TestHandlerVehicleDetail(t *testing.T) { if rec.Code != http.StatusOK { t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String()) } - for _, want := range []string{"identity", "realtime", "history", "raw", "mileage", "quality", "sources", "sourceStatus", "VIN_MISSING"} { + for _, want := range []string{"identity", "realtimeSummary", "realtime", "history", "raw", "mileage", "quality", "sources", "sourceStatus", "VIN_MISSING"} { if !strings.Contains(rec.Body.String(), want) { t.Fatalf("response missing %q: %s", want, rec.Body.String()) } diff --git a/vehicle-data-platform/apps/api/internal/platform/model.go b/vehicle-data-platform/apps/api/internal/platform/model.go index b6fa2823..910f50dc 100644 --- a/vehicle-data-platform/apps/api/internal/platform/model.go +++ b/vehicle-data-platform/apps/api/internal/platform/model.go @@ -55,15 +55,16 @@ type VehicleCoverageRow struct { } type VehicleDetail struct { - VIN string `json:"vin"` - Identity *VehicleRow `json:"identity,omitempty"` - Sources []string `json:"sources"` - SourceStatus []VehicleSourceStatus `json:"sourceStatus"` - Realtime []RealtimeLocationRow `json:"realtime"` - History Page[HistoryLocationRow] `json:"history"` - Raw Page[RawFrameRow] `json:"raw"` - Mileage Page[DailyMileageRow] `json:"mileage"` - Quality Page[QualityIssueRow] `json:"quality"` + VIN string `json:"vin"` + Identity *VehicleRow `json:"identity,omitempty"` + RealtimeSummary *VehicleRealtimeRow `json:"realtimeSummary,omitempty"` + Sources []string `json:"sources"` + SourceStatus []VehicleSourceStatus `json:"sourceStatus"` + Realtime []RealtimeLocationRow `json:"realtime"` + History Page[HistoryLocationRow] `json:"history"` + Raw Page[RawFrameRow] `json:"raw"` + Mileage Page[DailyMileageRow] `json:"mileage"` + Quality Page[QualityIssueRow] `json:"quality"` } type VehicleSourceStatus struct { diff --git a/vehicle-data-platform/apps/api/internal/platform/service.go b/vehicle-data-platform/apps/api/internal/platform/service.go index 77083ce4..64d96c57 100644 --- a/vehicle-data-platform/apps/api/internal/platform/service.go +++ b/vehicle-data-platform/apps/api/internal/platform/service.go @@ -81,6 +81,14 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string rawQuery.Protocol = protocol qualityQuery.Set("protocol", protocol) } + realtimeSummary, err := s.store.VehicleRealtime(ctx, url.Values{"vin": {resolvedVIN}, "limit": {"1"}}) + if err != nil { + return VehicleDetail{}, err + } + var summary *VehicleRealtimeRow + if len(realtimeSummary.Items) > 0 { + summary = &realtimeSummary.Items[0] + } realtime, err := s.store.RealtimeLocations(ctx, realtimeQuery) if err != nil { return VehicleDetail{}, err @@ -104,15 +112,16 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string sourceStatus := vehicleSourceStatus(vehicles.Items, realtime.Items, history.Items, raw.Items, mileage.Items) sourceStatus = s.enrichVehicleSourceStatus(ctx, resolvedVIN, sourceStatus) return VehicleDetail{ - VIN: resolvedVIN, - Identity: identity, - Sources: sourceNames(sourceStatus), - SourceStatus: sourceStatus, - Realtime: realtime.Items, - History: history, - Raw: raw, - Mileage: mileage, - Quality: quality, + VIN: resolvedVIN, + Identity: identity, + RealtimeSummary: summary, + Sources: sourceNames(sourceStatus), + SourceStatus: sourceStatus, + Realtime: realtime.Items, + History: history, + Raw: raw, + Mileage: mileage, + Quality: quality, }, nil } diff --git a/vehicle-data-platform/apps/web/src/api/types.ts b/vehicle-data-platform/apps/web/src/api/types.ts index 34dddb0b..2afeefd6 100644 --- a/vehicle-data-platform/apps/web/src/api/types.ts +++ b/vehicle-data-platform/apps/web/src/api/types.ts @@ -54,6 +54,7 @@ export interface VehicleCoverageRow { export interface VehicleDetail { vin: string; identity?: VehicleRow; + realtimeSummary?: VehicleRealtimeRow; sources: string[]; sourceStatus: VehicleSourceStatus[]; realtime: RealtimeLocationRow[]; diff --git a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx index 5b86b5b7..eb1930e1 100644 --- a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx +++ b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx @@ -40,10 +40,13 @@ export function VehicleDetail({ vin }: { vin: string }) { }, [vin]); const identity = detail?.identity; + const summary = detail?.realtimeSummary; const latest = detail?.realtime[0]; const protocols = useMemo(() => detail?.sources ?? [], [detail?.sources]); const latestRaw = detail?.raw.items[0]; const qualityCount = detail?.quality.total ?? 0; + const online = summary?.online || identity?.online || false; + const lastSeen = summary?.lastSeen || latest?.lastSeen || '-'; return (
@@ -72,13 +75,17 @@ export function VehicleDetail({ vin }: { vin: string }) { }, - { key: '来源协议', value: protocols.length > 0 ? {protocols.map((item) => {item})} : '-' }, - { key: '最后位置时间', value: latest?.lastSeen ?? '-' }, + { key: 'VIN', value: summary?.vin ?? identity?.vin ?? latest?.vin ?? query.vin }, + { key: '车牌', value: summary?.plate || identity?.plate || latest?.plate || '-' }, + { key: '手机号', value: summary?.phone || identity?.phone || '-' }, + { key: 'OEM', value: summary?.oem || identity?.oem || '-' }, + { key: '在线', value: }, + { + key: '来源协议', + value: protocols.length > 0 ? {protocols.map((item) => {item})} : '-' + }, + { key: '在线来源', value: summary ? `${summary.onlineSourceCount}/${summary.sourceCount}` : '-' }, + { key: '最后位置时间', value: lastSeen }, { key: '最新 RAW 时间', value: latestRaw?.serverTime ?? '-' }, { key: '质量问题', value: 0 ? 'orange' : 'green'}>{qualityCount > 0 ? `${qualityCount} 项` : '无'} } ]} @@ -107,6 +114,21 @@ export function VehicleDetail({ vin }: { vin: string }) { +
+ {[ + { label: '最新来源', value: summary?.primaryProtocol || '-' }, + { label: '速度 km/h', value: summary ? summary.speedKmh : '-' }, + { label: 'SOC %', value: summary ? summary.socPercent : '-' }, + { label: '总里程 km', value: summary ? summary.totalMileageKm : '-' }, + { label: '经纬度', value: summary ? `${summary.longitude}, ${summary.latitude}` : '-' }, + { label: '最后时间', value: lastSeen } + ].map((item) => ( +
+
{item.label}
+
{item.value}
+
+ ))} +