fix(access): report only real vehicle sources
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user