feat(platform): expose source slots in realtime vehicles
This commit is contained in:
@@ -679,6 +679,18 @@ func TestHandlerVehicleRealtime(t *testing.T) {
|
|||||||
if len(body.Data.Items) == 0 || body.Data.Items[0].ServiceStatus == nil {
|
if len(body.Data.Items) == 0 || body.Data.Items[0].ServiceStatus == nil {
|
||||||
t.Fatalf("realtime vehicle row should include canonical serviceStatus: %s", rec.Body.String())
|
t.Fatalf("realtime vehicle row should include canonical serviceStatus: %s", rec.Body.String())
|
||||||
}
|
}
|
||||||
|
byProtocol := map[string]VehicleSourceStatus{}
|
||||||
|
for _, source := range body.Data.Items[0].SourceStatus {
|
||||||
|
byProtocol[source.Protocol] = source
|
||||||
|
}
|
||||||
|
for _, protocol := range []string{"GB32960", "JT808", "YUTONG_MQTT"} {
|
||||||
|
if _, ok := byProtocol[protocol]; !ok {
|
||||||
|
t.Fatalf("realtime row should expose canonical source slot %s, got %+v body=%s", protocol, body.Data.Items[0].SourceStatus, rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !byProtocol["JT808"].Online || byProtocol["YUTONG_MQTT"].Online || byProtocol["YUTONG_MQTT"].HasRealtime {
|
||||||
|
t.Fatalf("realtime source slots should expose online and missing evidence, got %+v", byProtocol)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandlerVehicleRealtimeFiltersServiceStatus(t *testing.T) {
|
func TestHandlerVehicleRealtimeFiltersServiceStatus(t *testing.T) {
|
||||||
|
|||||||
@@ -321,6 +321,13 @@ func (m *MockStore) VehicleRealtime(_ context.Context, query url.Values) (Page[V
|
|||||||
items := make([]VehicleRealtimeRow, 0, len(byVIN))
|
items := make([]VehicleRealtimeRow, 0, len(byVIN))
|
||||||
for _, row := range byVIN {
|
for _, row := range byVIN {
|
||||||
sort.Strings(row.Protocols)
|
sort.Strings(row.Protocols)
|
||||||
|
onlineProtocols := make([]string, 0, row.OnlineSourceCount)
|
||||||
|
for _, location := range rows {
|
||||||
|
if location.VIN == row.VIN && location.LastSeen >= "2026-07-03 20:11:00" {
|
||||||
|
onlineProtocols = append(onlineProtocols, location.Protocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row.SourceStatus = buildVehicleCoverageSourceStatus(row.Protocols, onlineProtocols, row.LastSeen)
|
||||||
row.ServiceStatus = buildRealtimeServiceStatus(*row)
|
row.ServiceStatus = buildRealtimeServiceStatus(*row)
|
||||||
switch strings.TrimSpace(query.Get("online")) {
|
switch strings.TrimSpace(query.Get("online")) {
|
||||||
case "online":
|
case "online":
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ type VehicleRealtimeRow struct {
|
|||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
OEM string `json:"oem"`
|
OEM string `json:"oem"`
|
||||||
Protocols []string `json:"protocols"`
|
Protocols []string `json:"protocols"`
|
||||||
|
SourceStatus []VehicleSourceStatus `json:"sourceStatus"`
|
||||||
SourceCount int `json:"sourceCount"`
|
SourceCount int `json:"sourceCount"`
|
||||||
OnlineSourceCount int `json:"onlineSourceCount"`
|
OnlineSourceCount int `json:"onlineSourceCount"`
|
||||||
Online bool `json:"online"`
|
Online bool `json:"online"`
|
||||||
|
|||||||
@@ -328,6 +328,7 @@ func buildVehicleRealtimeSQL(query url.Values) SQLQuery {
|
|||||||
`COALESCE(NULLIF(MAX(NULLIF(l.plate, '')), ''), b.plate, '') AS plate, ` +
|
`COALESCE(NULLIF(MAX(NULLIF(l.plate, '')), ''), b.plate, '') AS plate, ` +
|
||||||
`COALESCE(b.phone, '') AS phone, COALESCE(b.oem, '') AS oem, ` +
|
`COALESCE(b.phone, '') AS phone, COALESCE(b.oem, '') AS oem, ` +
|
||||||
`COALESCE(GROUP_CONCAT(DISTINCT l.protocol ORDER BY l.protocol SEPARATOR ','), '') AS protocols, ` +
|
`COALESCE(GROUP_CONCAT(DISTINCT l.protocol ORDER BY l.protocol SEPARATOR ','), '') AS protocols, ` +
|
||||||
|
`COALESCE(GROUP_CONCAT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END ORDER BY l.protocol SEPARATOR ','), '') AS online_protocols, ` +
|
||||||
`COUNT(DISTINCT l.protocol) AS source_count, ` +
|
`COUNT(DISTINCT l.protocol) AS source_count, ` +
|
||||||
`COUNT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END) AS online_source_count, ` +
|
`COUNT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END) AS online_source_count, ` +
|
||||||
`CASE WHEN COUNT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END) > 0 THEN 1 ELSE 0 END AS online, ` +
|
`CASE WHEN COUNT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END) > 0 THEN 1 ELSE 0 END AS online, ` +
|
||||||
|
|||||||
@@ -339,12 +339,14 @@ func (s *ProductionStore) VehicleRealtime(ctx context.Context, query url.Values)
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var row VehicleRealtimeRow
|
var row VehicleRealtimeRow
|
||||||
var protocols string
|
var protocols string
|
||||||
|
var onlineProtocols string
|
||||||
var online int
|
var online int
|
||||||
var longitude, latitude, speed, soc, mileage string
|
var longitude, latitude, speed, soc, mileage string
|
||||||
if err := rows.Scan(&row.VIN, &row.Plate, &row.Phone, &row.OEM, &protocols, &row.SourceCount, &row.OnlineSourceCount, &online, &row.BindingStatus, &row.PrimaryProtocol, &longitude, &latitude, &speed, &soc, &mileage, &row.LastSeen); err != nil {
|
if err := rows.Scan(&row.VIN, &row.Plate, &row.Phone, &row.OEM, &protocols, &onlineProtocols, &row.SourceCount, &row.OnlineSourceCount, &online, &row.BindingStatus, &row.PrimaryProtocol, &longitude, &latitude, &speed, &soc, &mileage, &row.LastSeen); err != nil {
|
||||||
return Page[VehicleRealtimeRow]{}, err
|
return Page[VehicleRealtimeRow]{}, err
|
||||||
}
|
}
|
||||||
row.Protocols = splitCSV(protocols)
|
row.Protocols = splitCSV(protocols)
|
||||||
|
row.SourceStatus = buildVehicleCoverageSourceStatus(row.Protocols, splitCSV(onlineProtocols), row.LastSeen)
|
||||||
row.Online = online == 1
|
row.Online = online == 1
|
||||||
row.ServiceStatus = buildRealtimeServiceStatus(row)
|
row.ServiceStatus = buildRealtimeServiceStatus(row)
|
||||||
row.Longitude = parseFloatString(longitude)
|
row.Longitude = parseFloatString(longitude)
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ export interface VehicleRealtimeRow {
|
|||||||
phone: string;
|
phone: string;
|
||||||
oem: string;
|
oem: string;
|
||||||
protocols: string[];
|
protocols: string[];
|
||||||
|
sourceStatus: VehicleSourceStatus[];
|
||||||
sourceCount: number;
|
sourceCount: number;
|
||||||
onlineSourceCount: number;
|
onlineSourceCount: number;
|
||||||
online: boolean;
|
online: boolean;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { api } from '../api/client';
|
|||||||
import type { VehicleRealtimeRow } from '../api/types';
|
import type { VehicleRealtimeRow } from '../api/types';
|
||||||
import { DataEmpty } from '../components/DataEmpty';
|
import { DataEmpty } from '../components/DataEmpty';
|
||||||
import { PageHeader } from '../components/PageHeader';
|
import { PageHeader } from '../components/PageHeader';
|
||||||
|
import { SourceStatusTags } from '../components/SourceStatusTags';
|
||||||
import { StatusTag } from '../components/StatusTag';
|
import { StatusTag } from '../components/StatusTag';
|
||||||
|
|
||||||
function canOpenVehicle(vin?: string) {
|
function canOpenVehicle(vin?: string) {
|
||||||
@@ -146,11 +147,7 @@ export function Realtime({
|
|||||||
title: '来源证据',
|
title: '来源证据',
|
||||||
width: 260,
|
width: 260,
|
||||||
render: (_: unknown, row: VehicleRealtimeRow) => (
|
render: (_: unknown, row: VehicleRealtimeRow) => (
|
||||||
<Space spacing={4} wrap>
|
<SourceStatusTags sourceStatus={row.sourceStatus} protocols={row.protocols} lastSeen={row.lastSeen} />
|
||||||
{row.protocols.map((protocol) => (
|
|
||||||
<Tag key={protocol} color={protocol === row.primaryProtocol ? 'blue' : 'grey'}>{protocol}</Tag>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ title: '证据覆盖', width: 120, render: (_: unknown, row: VehicleRealtimeRow) => sourceEvidenceText(row) },
|
{ title: '证据覆盖', width: 120, render: (_: unknown, row: VehicleRealtimeRow) => sourceEvidenceText(row) },
|
||||||
|
|||||||
@@ -2671,6 +2671,11 @@ test('opens vehicle service from realtime vehicles with current source filter',
|
|||||||
phone: '',
|
phone: '',
|
||||||
oem: 'G7s',
|
oem: 'G7s',
|
||||||
protocols: ['GB32960', 'JT808'],
|
protocols: ['GB32960', 'JT808'],
|
||||||
|
sourceStatus: [
|
||||||
|
{ protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false },
|
||||||
|
{ protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:11:10', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false },
|
||||||
|
{ protocol: 'YUTONG_MQTT', online: false, lastSeen: '', hasRealtime: false, hasHistory: false, hasRaw: false, hasMileage: false }
|
||||||
|
],
|
||||||
sourceCount: 2,
|
sourceCount: 2,
|
||||||
onlineSourceCount: 2,
|
onlineSourceCount: 2,
|
||||||
online: true,
|
online: true,
|
||||||
@@ -2809,6 +2814,11 @@ test('frames realtime page as one vehicle service with source evidence', async (
|
|||||||
phone: '',
|
phone: '',
|
||||||
oem: 'G7s',
|
oem: 'G7s',
|
||||||
protocols: ['GB32960', 'JT808'],
|
protocols: ['GB32960', 'JT808'],
|
||||||
|
sourceStatus: [
|
||||||
|
{ protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false },
|
||||||
|
{ protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:11:10', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false },
|
||||||
|
{ protocol: 'YUTONG_MQTT', online: false, lastSeen: '', hasRealtime: false, hasHistory: false, hasRaw: false, hasMileage: false }
|
||||||
|
],
|
||||||
sourceCount: 2,
|
sourceCount: 2,
|
||||||
onlineSourceCount: 2,
|
onlineSourceCount: 2,
|
||||||
online: true,
|
online: true,
|
||||||
@@ -2853,6 +2863,9 @@ test('frames realtime page as one vehicle service with source evidence', async (
|
|||||||
expect(await screen.findByRole('heading', { name: '车辆服务实时态' })).toBeInTheDocument();
|
expect(await screen.findByRole('heading', { name: '车辆服务实时态' })).toBeInTheDocument();
|
||||||
expect(screen.getByText('车辆核心数据')).toBeInTheDocument();
|
expect(screen.getByText('车辆核心数据')).toBeInTheDocument();
|
||||||
expect(screen.getByText('来源证据')).toBeInTheDocument();
|
expect(screen.getByText('来源证据')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('GB32960 在线')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('JT808 在线')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('YUTONG_MQTT 未接入')).toBeInTheDocument();
|
||||||
expect(screen.getByText('2/2 来源在线')).toBeInTheDocument();
|
expect(screen.getByText('2/2 来源在线')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user