feat(platform): consolidate production vehicle data workflows
This commit is contained in:
@@ -124,6 +124,14 @@ func (s *Service) AccessSummary(ctx context.Context, query AccessQuery) (AccessS
|
||||
protocols := map[string]*AccessDistribution{}
|
||||
oems := map[string]*AccessDistribution{}
|
||||
for _, row := range rows {
|
||||
switch row.ConnectionState {
|
||||
case "healthy":
|
||||
result.HealthyVehicles++
|
||||
case "incomplete":
|
||||
result.IncompleteVehicles++
|
||||
case "degraded":
|
||||
result.DegradedVehicles++
|
||||
}
|
||||
switch row.OnlineState {
|
||||
case "online":
|
||||
result.OnlineVehicles++
|
||||
@@ -143,7 +151,11 @@ func (s *Service) AccessSummary(ctx context.Context, query AccessQuery) (AccessS
|
||||
if reportedOnDay(row.LatestReceivedAt, now) {
|
||||
result.ReportedToday++
|
||||
}
|
||||
addAccessDistribution(protocols, firstNonEmpty(row.Protocol, "未识别"), row.OnlineState == "online")
|
||||
for _, status := range row.ProtocolStatuses {
|
||||
if status.Connected {
|
||||
addAccessDistribution(protocols, status.Protocol, status.OnlineState == "online")
|
||||
}
|
||||
}
|
||||
addAccessDistribution(oems, firstNonEmpty(row.OEM, "未维护"), row.OnlineState == "online")
|
||||
}
|
||||
if result.TotalVehicles > 0 {
|
||||
@@ -171,9 +183,18 @@ func (s *Service) accessRows(ctx context.Context, query AccessQuery) ([]AccessVe
|
||||
return nil, AccessThresholdConfig{}, err
|
||||
}
|
||||
now := time.Now()
|
||||
rows := make([]AccessVehicleRow, 0, len(evidence))
|
||||
byVIN := make(map[string][]AccessEvidenceRow, len(evidence))
|
||||
order := make([]string, 0, len(evidence))
|
||||
for _, item := range evidence {
|
||||
row := buildAccessVehicleRow(item, config, now)
|
||||
vin := strings.TrimSpace(item.VIN)
|
||||
if _, exists := byVIN[vin]; !exists {
|
||||
order = append(order, vin)
|
||||
}
|
||||
byVIN[vin] = append(byVIN[vin], item)
|
||||
}
|
||||
rows := make([]AccessVehicleRow, 0, len(byVIN))
|
||||
for _, vin := range order {
|
||||
row := buildAccessVehicleGroup(byVIN[vin], config, now)
|
||||
if keepAccessRow(row, query) {
|
||||
rows = append(rows, row)
|
||||
}
|
||||
@@ -191,6 +212,159 @@ func (s *Service) accessRows(ctx context.Context, query AccessQuery) ([]AccessVe
|
||||
return rows, config, nil
|
||||
}
|
||||
|
||||
func buildAccessVehicleGroup(items []AccessEvidenceRow, config AccessThresholdConfig, now time.Time) AccessVehicleRow {
|
||||
if len(items) == 0 {
|
||||
return AccessVehicleRow{}
|
||||
}
|
||||
base := items[0]
|
||||
row := AccessVehicleRow{
|
||||
VIN: strings.TrimSpace(base.VIN),
|
||||
Plate: strings.TrimSpace(base.Plate),
|
||||
OEM: strings.TrimSpace(base.OEM),
|
||||
Model: strings.TrimSpace(base.Model),
|
||||
Company: strings.TrimSpace(base.Company),
|
||||
ExpectedProtocols: append([]string(nil), canonicalVehicleProtocols...),
|
||||
ActualProtocols: []string{},
|
||||
MissingProtocols: []string{},
|
||||
ProtocolStatuses: []AccessProtocolStatus{},
|
||||
ExpectationEvidence: "平台标准接入基线:GB32960 / JT808 / YUTONG_MQTT",
|
||||
ConnectionState: "not_connected",
|
||||
OnlineState: "never_reported",
|
||||
FirstSeenEvidence: "尚未形成任何协议接入快照",
|
||||
ReportIntervalProof: "需要连续上报样本后才能计算",
|
||||
}
|
||||
byProtocol := make(map[string]AccessEvidenceRow, len(items))
|
||||
for _, item := range items {
|
||||
protocol := strings.ToUpper(strings.TrimSpace(item.Protocol))
|
||||
if protocol != "" {
|
||||
byProtocol[protocol] = item
|
||||
}
|
||||
if row.Plate == "" {
|
||||
row.Plate = strings.TrimSpace(item.Plate)
|
||||
}
|
||||
if row.OEM == "" {
|
||||
row.OEM = strings.TrimSpace(item.OEM)
|
||||
}
|
||||
if row.Model == "" {
|
||||
row.Model = strings.TrimSpace(item.Model)
|
||||
}
|
||||
if row.Company == "" {
|
||||
row.Company = strings.TrimSpace(item.Company)
|
||||
}
|
||||
}
|
||||
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: "尚无接收样本",
|
||||
})
|
||||
continue
|
||||
}
|
||||
source := buildAccessVehicleRow(item, config, now)
|
||||
connectedCount++
|
||||
row.ActualProtocols = append(row.ActualProtocols, protocol)
|
||||
if source.OnlineState == "online" {
|
||||
onlineCount++
|
||||
}
|
||||
if source.DelayAbnormal {
|
||||
row.DelayAbnormal = true
|
||||
}
|
||||
row.ProtocolStatuses = append(row.ProtocolStatuses, AccessProtocolStatus{
|
||||
Protocol: protocol, Expected: true, 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,
|
||||
FirstSeenEvidence: source.FirstSeenEvidence, ReportIntervalEvidence: source.ReportIntervalProof,
|
||||
Longitude: item.Longitude, Latitude: item.Latitude, SpeedKmh: item.SpeedKmh, SOCPercent: item.SOCPercent, TotalMileageKm: item.TotalMileageKm, DailyMileageKm: item.DailyMileageKm,
|
||||
})
|
||||
if row.FirstSeenAt == "" || source.FirstSeenAt != "" && source.FirstSeenAt < row.FirstSeenAt {
|
||||
row.FirstSeenAt = source.FirstSeenAt
|
||||
row.FirstSeenEvidence = source.FirstSeenEvidence
|
||||
row.FirstSeenSource = source.FirstSeenSource
|
||||
}
|
||||
if row.LatestReceivedAt == "" || source.LatestReceivedAt > row.LatestReceivedAt {
|
||||
copyAccessPrimaryFields(&row, source)
|
||||
}
|
||||
}
|
||||
for protocol, item := range byProtocol {
|
||||
if containsString(canonicalVehicleProtocols, protocol) {
|
||||
continue
|
||||
}
|
||||
source := buildAccessVehicleRow(item, config, now)
|
||||
row.ActualProtocols = append(row.ActualProtocols, protocol)
|
||||
if source.OnlineState == "online" {
|
||||
onlineCount++
|
||||
}
|
||||
if source.OnlineState == "unknown" {
|
||||
unknownCount++
|
||||
}
|
||||
row.ProtocolStatuses = append(row.ProtocolStatuses, AccessProtocolStatus{
|
||||
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,
|
||||
FirstSeenEvidence: source.FirstSeenEvidence, ReportIntervalEvidence: source.ReportIntervalProof,
|
||||
Longitude: item.Longitude, Latitude: item.Latitude, SpeedKmh: item.SpeedKmh, SOCPercent: item.SOCPercent, TotalMileageKm: item.TotalMileageKm, DailyMileageKm: item.DailyMileageKm,
|
||||
})
|
||||
if row.LatestReceivedAt == "" || source.LatestReceivedAt > row.LatestReceivedAt {
|
||||
copyAccessPrimaryFields(&row, source)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case connectedCount == 0 && len(row.ActualProtocols) == 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"
|
||||
row.OnlineState = "unknown"
|
||||
case onlineCount == 0:
|
||||
row.ConnectionState = "offline"
|
||||
row.OnlineState = "offline"
|
||||
case connectedCount < len(canonicalVehicleProtocols):
|
||||
row.ConnectionState = "incomplete"
|
||||
row.OnlineState = "online"
|
||||
default:
|
||||
row.ConnectionState = "degraded"
|
||||
row.OnlineState = "online"
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func accessProtocolThreshold(config AccessThresholdConfig, protocol string) int {
|
||||
for _, override := range config.Protocols {
|
||||
if strings.EqualFold(override.Protocol, protocol) {
|
||||
return override.ThresholdSec
|
||||
}
|
||||
}
|
||||
return config.DefaultThresholdSec
|
||||
}
|
||||
|
||||
func copyAccessPrimaryFields(target *AccessVehicleRow, source AccessVehicleRow) {
|
||||
target.Protocol = source.Protocol
|
||||
target.Provider = source.Provider
|
||||
target.Source = source.Source
|
||||
target.LatestEventAt = source.LatestEventAt
|
||||
target.LatestReceivedAt = source.LatestReceivedAt
|
||||
target.ReportIntervalSec = source.ReportIntervalSec
|
||||
target.DataDelaySec = source.DataDelaySec
|
||||
target.FreshnessSec = source.FreshnessSec
|
||||
target.ThresholdSec = source.ThresholdSec
|
||||
target.LatestMessageType = source.LatestMessageType
|
||||
target.LatestEventID = source.LatestEventID
|
||||
target.LatestError = source.LatestError
|
||||
target.ReportIntervalProof = source.ReportIntervalProof
|
||||
target.ReportSampleCount = source.ReportSampleCount
|
||||
}
|
||||
|
||||
func buildAccessVehicleRow(item AccessEvidenceRow, config AccessThresholdConfig, now time.Time) AccessVehicleRow {
|
||||
threshold := config.DefaultThresholdSec
|
||||
for _, override := range config.Protocols {
|
||||
@@ -266,10 +440,10 @@ func buildAccessVehicleRow(item AccessEvidenceRow, config AccessThresholdConfig,
|
||||
|
||||
func keepAccessRow(row AccessVehicleRow, query AccessQuery) bool {
|
||||
keyword := strings.ToLower(strings.TrimSpace(query.Keyword))
|
||||
if keyword != "" && !strings.Contains(strings.ToLower(row.VIN), keyword) && !strings.Contains(strings.ToLower(row.Plate), keyword) {
|
||||
if keyword != "" && !strings.Contains(strings.ToLower(row.VIN), keyword) && !strings.Contains(strings.ToLower(row.Plate), keyword) && !strings.Contains(strings.ToLower(row.OEM), keyword) && !strings.Contains(strings.ToLower(row.Model), keyword) && !strings.Contains(strings.ToLower(row.Company), keyword) {
|
||||
return false
|
||||
}
|
||||
if value := strings.TrimSpace(query.Protocol); value != "" && !strings.EqualFold(value, row.Protocol) {
|
||||
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) {
|
||||
@@ -278,8 +452,17 @@ func keepAccessRow(row AccessVehicleRow, query AccessQuery) bool {
|
||||
if value := strings.ToLower(strings.TrimSpace(query.Model)); value != "" && !strings.Contains(strings.ToLower(row.Model), value) {
|
||||
return false
|
||||
}
|
||||
if value := strings.ToLower(strings.TrimSpace(query.Provider)); value != "" && !strings.Contains(strings.ToLower(row.Provider), value) {
|
||||
return false
|
||||
if value := strings.ToLower(strings.TrimSpace(query.Provider)); value != "" {
|
||||
matched := false
|
||||
for _, status := range row.ProtocolStatuses {
|
||||
if strings.Contains(strings.ToLower(status.Provider), value) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !accessTimeMatches(row.FirstSeenAt, query.FirstSeenFrom, query.FirstSeenTo) || !accessTimeMatches(row.LatestReceivedAt, query.LatestSeenFrom, query.LatestSeenTo) {
|
||||
return false
|
||||
@@ -287,6 +470,15 @@ func keepAccessRow(row AccessVehicleRow, query AccessQuery) bool {
|
||||
if value := strings.TrimSpace(query.OnlineState); value != "" && value != "all" && value != row.OnlineState {
|
||||
return false
|
||||
}
|
||||
if value := strings.TrimSpace(query.ConnectionState); value != "" && value != "all" {
|
||||
if value == "attention" {
|
||||
if row.ConnectionState == "healthy" {
|
||||
return false
|
||||
}
|
||||
} else if value != row.ConnectionState {
|
||||
return false
|
||||
}
|
||||
}
|
||||
switch strings.TrimSpace(query.DelayState) {
|
||||
case "abnormal":
|
||||
return row.DelayAbnormal
|
||||
|
||||
Reference in New Issue
Block a user