fix(access): report only real vehicle sources

This commit is contained in:
lingniu
2026-07-16 18:07:45 +08:00
parent 85de053962
commit ff6df370cb
10 changed files with 106 additions and 54 deletions

View File

@@ -223,11 +223,12 @@ func buildAccessVehicleGroup(items []AccessEvidenceRow, config AccessThresholdCo
OEM: strings.TrimSpace(base.OEM),
Model: strings.TrimSpace(base.Model),
Company: strings.TrimSpace(base.Company),
ExpectedProtocols: append([]string(nil), canonicalVehicleProtocols...),
ExpectedProtocols: []string{},
ActualProtocols: []string{},
MissingProtocols: []string{},
ProtocolStatuses: []AccessProtocolStatus{},
ExpectationEvidence: "平台标准接入基线GB32960 / JT808 / YUTONG_MQTT",
MasterDataIssues: []string{},
ExpectationEvidence: "尚未接入业务系统的应接协议口径;当前只展示真实接入来源,不推断车辆必须具备三协议",
ConnectionState: "not_connected",
OnlineState: "never_reported",
FirstSeenEvidence: "尚未形成任何协议接入快照",
@@ -254,29 +255,29 @@ func buildAccessVehicleGroup(items []AccessEvidenceRow, config AccessThresholdCo
}
onlineCount := 0
unknownCount := 0
connectedCount := 0
for _, protocol := range canonicalVehicleProtocols {
item, connected := byProtocol[protocol]
if !connected {
threshold := accessProtocolThreshold(config, protocol)
row.MissingProtocols = append(row.MissingProtocols, protocol)
row.ProtocolStatuses = append(row.ProtocolStatuses, AccessProtocolStatus{
Protocol: protocol, Expected: true, Connected: false, OnlineState: "never_reported", ThresholdSec: threshold,
FirstSeenEvidence: "应接协议尚未形成实时快照", ReportIntervalEvidence: "尚无接收样本",
Protocol: protocol, Expected: false, Connected: false, OnlineState: "never_reported", ThresholdSec: threshold,
FirstSeenEvidence: "当前未发现该协议来源;在业务应接口径接入前不判定为缺失", ReportIntervalEvidence: "尚无接收样本",
})
continue
}
source := buildAccessVehicleRow(item, config, now)
connectedCount++
row.ActualProtocols = append(row.ActualProtocols, protocol)
if source.OnlineState == "online" {
onlineCount++
}
if source.OnlineState == "unknown" {
unknownCount++
}
if source.DelayAbnormal {
row.DelayAbnormal = true
}
row.ProtocolStatuses = append(row.ProtocolStatuses, AccessProtocolStatus{
Protocol: protocol, Expected: true, Connected: true, Provider: source.Provider,
Protocol: protocol, Expected: false, Connected: true, Provider: source.Provider,
FirstSeenAt: source.FirstSeenAt, LatestEventAt: source.LatestEventAt, LatestReceivedAt: source.LatestReceivedAt,
ReportIntervalSec: source.ReportIntervalSec, DataDelaySec: source.DataDelaySec, FreshnessSec: source.FreshnessSec,
OnlineState: source.OnlineState, ThresholdSec: source.ThresholdSec, DelayAbnormal: source.DelayAbnormal,
@@ -292,10 +293,16 @@ func buildAccessVehicleGroup(items []AccessEvidenceRow, config AccessThresholdCo
copyAccessPrimaryFields(&row, source)
}
}
for protocol, item := range byProtocol {
extraProtocols := make([]string, 0)
for protocol := range byProtocol {
if containsString(canonicalVehicleProtocols, protocol) {
continue
}
extraProtocols = append(extraProtocols, protocol)
}
sort.Strings(extraProtocols)
for _, protocol := range extraProtocols {
item := byProtocol[protocol]
source := buildAccessVehicleRow(item, config, now)
row.ActualProtocols = append(row.ActualProtocols, protocol)
if source.OnlineState == "online" {
@@ -316,29 +323,47 @@ func buildAccessVehicleGroup(items []AccessEvidenceRow, config AccessThresholdCo
copyAccessPrimaryFields(&row, source)
}
}
if accessMasterDataMissing(row.OEM) {
row.MasterDataIssues = append(row.MasterDataIssues, "车辆品牌未维护")
}
for _, status := range row.ProtocolStatuses {
if status.Connected && accessMasterDataMissing(status.Provider) {
row.MasterDataIssues = append(row.MasterDataIssues, status.Protocol+" 接入方未维护")
}
}
sourceCount := len(row.ActualProtocols)
switch {
case connectedCount == 0 && len(row.ActualProtocols) == 0:
case sourceCount == 0:
row.ConnectionState = "not_connected"
row.OnlineState = "never_reported"
case connectedCount == len(canonicalVehicleProtocols) && onlineCount == connectedCount:
row.ConnectionState = "healthy"
row.OnlineState = "online"
case onlineCount == 0 && unknownCount > 0:
row.ConnectionState = "incomplete"
case onlineCount == 0 && unknownCount == sourceCount:
row.ConnectionState = "degraded"
row.OnlineState = "unknown"
case onlineCount == 0:
row.ConnectionState = "offline"
row.OnlineState = "offline"
case connectedCount < len(canonicalVehicleProtocols):
case onlineCount < sourceCount || row.DelayAbnormal:
row.ConnectionState = "degraded"
row.OnlineState = "online"
case len(row.MasterDataIssues) > 0:
row.ConnectionState = "incomplete"
row.OnlineState = "online"
default:
row.ConnectionState = "degraded"
row.ConnectionState = "healthy"
row.OnlineState = "online"
}
return row
}
func accessMasterDataMissing(value string) bool {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", "未维护", "待维护", "unknown", "未知":
return true
default:
return false
}
}
func accessProtocolThreshold(config AccessThresholdConfig, protocol string) int {
for _, override := range config.Protocols {
if strings.EqualFold(override.Protocol, protocol) {
@@ -446,8 +471,14 @@ func keepAccessRow(row AccessVehicleRow, query AccessQuery) bool {
if value := strings.TrimSpace(query.Protocol); value != "" && !containsString(row.ActualProtocols, value) {
return false
}
if value := strings.TrimSpace(query.OEM); value != "" && !strings.EqualFold(value, row.OEM) {
return false
if value := strings.TrimSpace(query.OEM); value != "" {
if value == "未维护" {
if !accessMasterDataMissing(row.OEM) {
return false
}
} else if !strings.EqualFold(value, row.OEM) {
return false
}
}
if value := strings.ToLower(strings.TrimSpace(query.Model)); value != "" && !strings.Contains(strings.ToLower(row.Model), value) {
return false

View File

@@ -69,7 +69,7 @@ COALESCE(NULLIF(b.oem, ''), '') AS oem,
COALESCE(p.model_name, '') AS model_name,
COALESCE(p.company_name, '') AS company_name,
COALESCE(s.protocol, '') AS protocol,
COALESCE(s.platform_name, '') AS provider,
COALESCE(NULLIF(TRIM(s.platform_name), ''), NULLIF(TRIM(ls.source_code), ''), '') AS provider,
COALESCE(DATE_FORMAT(s.access_first_seen_at, '%Y-%m-%d %H:%i:%s.%f'), '') AS first_seen_at,
COALESCE(s.access_first_seen_source, '') AS first_seen_source,
COALESCE(DATE_FORMAT(s.event_time, '%Y-%m-%d %H:%i:%s.%f'), '') AS event_time,
@@ -91,6 +91,7 @@ LEFT JOIN vehicle_identity_binding b ON b.vin = v.vin
LEFT JOIN vehicle_profile p ON p.vin = v.vin
LEFT JOIN vehicle_realtime_snapshot s ON s.vin = v.vin
LEFT JOIN vehicle_realtime_location l ON l.vin = s.vin AND l.protocol = s.protocol
LEFT JOIN vehicle_realtime_location_source ls ON ls.vin = l.vin AND ls.protocol = l.protocol AND ls.source_key = l.source_key
LEFT JOIN (
SELECT vin, protocol, MAX(daily_mileage_km) AS daily_mileage_km
FROM vehicle_daily_mileage

View File

@@ -23,7 +23,7 @@ func TestAccessSummaryUsesDynamicFreshnessAndDelay(t *testing.T) {
if summary.DelayAbnormal != 1 || summary.ThresholdVersion != 1 {
t.Fatalf("summary must expose dynamic delay and threshold version: %+v", summary)
}
if summary.HealthyVehicles != 0 || summary.IncompleteVehicles != 3 || summary.DegradedVehicles != 0 {
if summary.HealthyVehicles != 1 || summary.IncompleteVehicles != 0 || summary.DegradedVehicles != 2 {
t.Fatalf("connection-state counters must be mutually exclusive: %+v", summary)
}
}
@@ -47,8 +47,8 @@ func TestAccessVehiclesFiltersAndKeepsEvidenceGapsExplicit(t *testing.T) {
}
attention, err := service.AccessVehicles(context.Background(), AccessQuery{ConnectionState: "attention", Limit: 20})
if err != nil || attention.Total != 6 {
t.Fatalf("attention filter should return every vehicle with an access difference: page=%+v err=%v", attention, err)
if err != nil || attention.Total != 5 {
t.Fatalf("attention filter should return every vehicle requiring attention: page=%+v err=%v", attention, err)
}
}
@@ -116,7 +116,7 @@ func TestAccessVehiclesKeywordMatchesFleetAndModel(t *testing.T) {
}
}
func TestAccessVehicleGroupsCanonicalProtocolsByMasterVehicle(t *testing.T) {
func TestAccessVehicleShowsCanonicalSlotsWithoutInventingExpectedSources(t *testing.T) {
store := NewMockStore()
service := NewService(store)
page, err := service.AccessVehicles(t.Context(), AccessQuery{Keyword: "LB9A32A24R0LS1426", Limit: 10})
@@ -124,19 +124,37 @@ func TestAccessVehicleGroupsCanonicalProtocolsByMasterVehicle(t *testing.T) {
t.Fatalf("access vehicle query failed: page=%+v err=%v", page, err)
}
row := page.Items[0]
if len(row.ExpectedProtocols) != 3 || len(row.ProtocolStatuses) != 3 {
t.Fatalf("vehicle must expose three canonical protocol slots: %+v", row)
if len(row.ExpectedProtocols) != 0 || len(row.ProtocolStatuses) != 3 {
t.Fatalf("vehicle must expose comparison slots without inventing an expected-source baseline: %+v", row)
}
if len(row.ActualProtocols) != 1 || row.ActualProtocols[0] != "JT808" || len(row.MissingProtocols) != 2 {
t.Fatalf("actual and missing protocols are incorrect: %+v", row)
if len(row.ActualProtocols) != 1 || row.ActualProtocols[0] != "JT808" || len(row.MissingProtocols) != 0 {
t.Fatalf("actual source must remain truthful and absent protocols must not be reported as missing: %+v", row)
}
if row.ConnectionState != "incomplete" || row.OnlineState != "online" {
t.Fatalf("single online source should be an online but incomplete vehicle: %+v", row)
if row.ConnectionState != "healthy" || row.OnlineState != "online" {
t.Fatalf("a maintained single online source should be healthy: %+v", row)
}
for _, status := range row.ProtocolStatuses {
if status.Protocol == "JT808" && (!status.Connected || status.LatestReceivedAt == "") {
t.Fatalf("connected protocol lost evidence: %+v", status)
}
if status.Expected {
t.Fatalf("business expectation must remain unknown until OneOS provides it: %+v", status)
}
}
}
func TestAccessVehicleSeparatesMasterDataMaintenanceFromSourceAbsence(t *testing.T) {
now := time.Now()
row := buildAccessVehicleGroup([]AccessEvidenceRow{{
VIN: "VIN-MASTER-DATA", Protocol: "JT808", OEM: "", Provider: "",
LatestEventAt: now.Add(-2 * time.Second).Format(time.RFC3339),
LatestReceivedAt: now.Add(-time.Second).Format(time.RFC3339),
}}, defaultAccessThresholds(now), now)
if row.ConnectionState != "incomplete" || len(row.MasterDataIssues) != 2 {
t.Fatalf("online source with missing brand/provider should enter the maintenance queue: %+v", row)
}
if len(row.MissingProtocols) != 0 || len(row.ExpectedProtocols) != 0 {
t.Fatalf("master-data maintenance must not invent absent protocol alarms: %+v", row)
}
}

View File

@@ -460,6 +460,7 @@ type AccessVehicleRow struct {
ActualProtocols []string `json:"actualProtocols"`
MissingProtocols []string `json:"missingProtocols"`
ProtocolStatuses []AccessProtocolStatus `json:"protocolStatuses"`
MasterDataIssues []string `json:"masterDataIssues"`
ConnectionState string `json:"connectionState"`
ExpectationEvidence string `json:"expectationEvidence"`
}