feat(platform): consolidate production vehicle data workflows

This commit is contained in:
lingniu
2026-07-15 23:26:29 +08:00
parent 6cddc0a43d
commit 3fabcf181a
59 changed files with 6849 additions and 3532 deletions

View File

@@ -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