diff --git a/vehicle-data-platform/apps/api/internal/platform/access.go b/vehicle-data-platform/apps/api/internal/platform/access.go index 1070fc8a..9f243138 100644 --- a/vehicle-data-platform/apps/api/internal/platform/access.go +++ b/vehicle-data-platform/apps/api/internal/platform/access.go @@ -132,6 +132,9 @@ func (s *Service) AccessSummary(ctx context.Context, query AccessQuery) (AccessS case "degraded": result.DegradedVehicles++ } + if len(row.MasterDataIssues) > 0 { + result.MasterDataIncompleteVehicles++ + } switch row.OnlineState { case "online": result.OnlineVehicles++ @@ -502,7 +505,11 @@ func keepAccessRow(row AccessVehicleRow, query AccessQuery) bool { return false } if value := strings.TrimSpace(query.ConnectionState); value != "" && value != "all" { - if value == "attention" { + if value == "master_data" { + if len(row.MasterDataIssues) == 0 { + return false + } + } else if value == "attention" { if row.ConnectionState == "healthy" { return false } diff --git a/vehicle-data-platform/apps/api/internal/platform/access_test.go b/vehicle-data-platform/apps/api/internal/platform/access_test.go index bff195a5..8fc1961c 100644 --- a/vehicle-data-platform/apps/api/internal/platform/access_test.go +++ b/vehicle-data-platform/apps/api/internal/platform/access_test.go @@ -26,6 +26,9 @@ func TestAccessSummaryUsesDynamicFreshnessAndDelay(t *testing.T) { if summary.HealthyVehicles != 1 || summary.IncompleteVehicles != 0 || summary.DegradedVehicles != 2 { t.Fatalf("connection-state counters must be mutually exclusive: %+v", summary) } + if summary.MasterDataIncompleteVehicles != 1 { + t.Fatalf("master-data maintenance must be counted independently from connection health: %+v", summary) + } } func TestAccessVehiclesFiltersAndKeepsEvidenceGapsExplicit(t *testing.T) { @@ -50,6 +53,10 @@ func TestAccessVehiclesFiltersAndKeepsEvidenceGapsExplicit(t *testing.T) { if err != nil || attention.Total != 5 { t.Fatalf("attention filter should return every vehicle requiring attention: page=%+v err=%v", attention, err) } + masterData, err := service.AccessVehicles(context.Background(), AccessQuery{ConnectionState: "master_data", Limit: 20}) + if err != nil || masterData.Total != 1 || len(masterData.Items[0].MasterDataIssues) == 0 { + t.Fatalf("master-data queue should include issues regardless of connection state: page=%+v err=%v", masterData, err) + } } func TestAccessVehiclesSupportsModelProviderAndTimeFilters(t *testing.T) { diff --git a/vehicle-data-platform/apps/api/internal/platform/model.go b/vehicle-data-platform/apps/api/internal/platform/model.go index 98ae82a7..cbefd295 100644 --- a/vehicle-data-platform/apps/api/internal/platform/model.go +++ b/vehicle-data-platform/apps/api/internal/platform/model.go @@ -520,22 +520,23 @@ type AccessDistribution struct { } type AccessSummary struct { - TotalVehicles int `json:"totalVehicles"` - OnlineVehicles int `json:"onlineVehicles"` - OfflineVehicles int `json:"offlineVehicles"` - LongOfflineVehicles int `json:"longOfflineVehicles"` - NeverReported int `json:"neverReported"` - UnknownVehicles int `json:"unknownVehicles"` - DelayAbnormal int `json:"delayAbnormal"` - ReportedToday int `json:"reportedToday"` - HealthyVehicles int `json:"healthyVehicles"` - IncompleteVehicles int `json:"incompleteVehicles"` - DegradedVehicles int `json:"degradedVehicles"` - OnlineRate float64 `json:"onlineRate"` - Protocols []AccessDistribution `json:"protocols"` - OEMs []AccessDistribution `json:"oems"` - AsOf string `json:"asOf"` - ThresholdVersion int `json:"thresholdVersion"` + TotalVehicles int `json:"totalVehicles"` + OnlineVehicles int `json:"onlineVehicles"` + OfflineVehicles int `json:"offlineVehicles"` + LongOfflineVehicles int `json:"longOfflineVehicles"` + NeverReported int `json:"neverReported"` + UnknownVehicles int `json:"unknownVehicles"` + DelayAbnormal int `json:"delayAbnormal"` + ReportedToday int `json:"reportedToday"` + HealthyVehicles int `json:"healthyVehicles"` + IncompleteVehicles int `json:"incompleteVehicles"` + DegradedVehicles int `json:"degradedVehicles"` + MasterDataIncompleteVehicles int `json:"masterDataIncompleteVehicles"` + OnlineRate float64 `json:"onlineRate"` + Protocols []AccessDistribution `json:"protocols"` + OEMs []AccessDistribution `json:"oems"` + AsOf string `json:"asOf"` + ThresholdVersion int `json:"thresholdVersion"` } type AccessProtocolThreshold struct { diff --git a/vehicle-data-platform/apps/web/src/api/types.ts b/vehicle-data-platform/apps/web/src/api/types.ts index 25940cc7..e62ba748 100644 --- a/vehicle-data-platform/apps/web/src/api/types.ts +++ b/vehicle-data-platform/apps/web/src/api/types.ts @@ -300,7 +300,7 @@ export interface AccessDistribution { name: string; total: number; online: numbe export interface AccessSummary { totalVehicles: number; onlineVehicles: number; offlineVehicles: number; longOfflineVehicles: number; neverReported: number; unknownVehicles: number; delayAbnormal: number; reportedToday: number; onlineRate: number; - healthyVehicles: number; incompleteVehicles: number; degradedVehicles: number; + healthyVehicles: number; incompleteVehicles: number; degradedVehicles: number; masterDataIncompleteVehicles: number; protocols: AccessDistribution[]; oems: AccessDistribution[]; asOf: string; thresholdVersion: number; } export interface AccessProtocolThreshold { protocol: string; thresholdSec: number; } diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx index 13d19f9b..2ea42640 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx @@ -32,7 +32,7 @@ function accessRow(vin: string, plate: string): AccessVehicleRow { } function prepareBaseData() { - mocks.accessSummary.mockResolvedValue({ totalVehicles: 2, onlineVehicles: 2, offlineVehicles: 0, longOfflineVehicles: 0, neverReported: 0, unknownVehicles: 0, delayAbnormal: 0, reportedToday: 2, onlineRate: 100, healthyVehicles: 2, incompleteVehicles: 0, degradedVehicles: 0, protocols: [], oems: [{ name: '测试品牌', total: 2, online: 2, onlineRate: 100 }], asOf: '2026-07-16T04:00:00Z', thresholdVersion: 1 }); + mocks.accessSummary.mockResolvedValue({ totalVehicles: 2, onlineVehicles: 2, offlineVehicles: 0, longOfflineVehicles: 0, neverReported: 0, unknownVehicles: 0, delayAbnormal: 0, reportedToday: 2, onlineRate: 100, healthyVehicles: 2, incompleteVehicles: 0, degradedVehicles: 0, masterDataIncompleteVehicles: 0, protocols: [], oems: [{ name: '测试品牌', total: 2, online: 2, onlineRate: 100 }], asOf: '2026-07-16T04:00:00Z', thresholdVersion: 1 }); mocks.accessUnresolvedIdentities.mockResolvedValue({ items: [], total: 0, limit: 20, offset: 0 }); mocks.accessThresholds.mockResolvedValue({ version: 1, defaultThresholdSec: 300, delayThresholdSec: 60, longOfflineSec: 86_400, protocols: [], updatedBy: 'test', updatedAt: '2026-07-16T04:00:00Z', audit: [] }); } diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx index c2026658..73fc51da 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx @@ -120,10 +120,10 @@ export default function AccessPage() { return

车辆接入管理

只展示车辆真实存在的来源、在线健康和待维护资料;未接业务系统前不推断应接协议

数据时间 {summary?.asOf ? formatAccessTime(summary.asOf) : '—'}
-
+
{summaryQuery.isError ? summaryQuery.refetch()} /> : null}
{[ - ['主车辆', summary?.totalVehicles ?? 0, 'all', ''], ['需关注', Math.max(0, (summary?.totalVehicles ?? 0) - (summary?.healthyVehicles ?? 0)), 'attention', 'attention'], ['资料待维护', summary?.incompleteVehicles ?? 0, 'incomplete', 'incomplete'], ['尚无来源', summary?.neverReported ?? 0, 'never', 'not_connected'] + ['主车辆', summary?.totalVehicles ?? 0, 'all', ''], ['需关注', Math.max(0, (summary?.totalVehicles ?? 0) - (summary?.healthyVehicles ?? 0)), 'attention', 'attention'], ['资料待维护', summary?.masterDataIncompleteVehicles ?? 0, 'incomplete', 'master_data'], ['尚无来源', summary?.neverReported ?? 0, 'never', 'not_connected'] ].map(([label, value, tone, connectionState]) => )}
{vehiclesQuery.isError ? vehiclesQuery.refetch()} /> : null}
车辆真实接入来源缺席协议只表示“当前未发现”,不会被推断成应接缺失;时间为各来源最后接收时间
{mobileLayout ?
{rows.map((row) => )}
: {PROTOCOLS.map((item) => )}{rows.map((row) => setSelectedVIN(row.vin)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') setSelectedVIN(row.vin); }}>{PROTOCOLS.map((protocol) => )})}
车辆品牌 / 车型真实来源{item}综合状态
{row.plate || '未绑定车牌'}{row.vin}{row.oem || '品牌未维护'}{row.model || row.company || '车型未维护'}{row.actualProtocols.length} 个{row.actualProtocols.join(' / ') || '尚无来源'}
}{vehiclesQuery.isFetching ?
正在更新车辆接入状态…
: null}{!vehiclesQuery.isFetching && !rows.length ?
当前筛选条件没有车辆
: null}
第 {page} / {totalPages} 页,共 {(vehiclesQuery.data?.total ?? 0).toLocaleString('zh-CN')} 辆主车辆
{selected ? setSelectedVIN('')} /> : null}