feat(platform): expose source consistency

This commit is contained in:
lingniu
2026-07-04 04:01:33 +08:00
parent 353b71ebbb
commit db70f58a7c
6 changed files with 195 additions and 37 deletions

View File

@@ -2,10 +2,12 @@ package platform
import (
"context"
"math"
"net/url"
"sort"
"strconv"
"strings"
"time"
)
type Store interface {
@@ -277,24 +279,120 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string
sourceStatus := vehicleSourceStatus(vehicles.Items, realtime.Items, history.Items, raw.Items, mileage.Items)
sourceStatus = s.enrichVehicleSourceStatus(ctx, queryVIN, sourceStatus)
return VehicleDetail{
VIN: resolvedVIN,
LookupKey: keyword,
LookupResolved: resolvedVIN != "",
Resolution: &resolution,
Identity: identity,
RealtimeSummary: summary,
ServiceStatus: buildVehicleServiceStatus(true, sourceStatus),
ServiceOverview: buildVehicleServiceOverview(resolvedVIN, keyword, &resolution, identity, summary, sourceStatus, history, raw, mileage, quality),
Sources: sourceNames(sourceStatus),
SourceStatus: sourceStatus,
Realtime: realtime.Items,
History: history,
Raw: raw,
Mileage: mileage,
Quality: quality,
VIN: resolvedVIN,
LookupKey: keyword,
LookupResolved: resolvedVIN != "",
Resolution: &resolution,
Identity: identity,
RealtimeSummary: summary,
ServiceStatus: buildVehicleServiceStatus(true, sourceStatus),
ServiceOverview: buildVehicleServiceOverview(resolvedVIN, keyword, &resolution, identity, summary, sourceStatus, history, raw, mileage, quality),
SourceConsistency: buildVehicleSourceConsistency(sourceStatus, realtime.Items, protocol),
Sources: sourceNames(sourceStatus),
SourceStatus: sourceStatus,
Realtime: realtime.Items,
History: history,
Raw: raw,
Mileage: mileage,
Quality: quality,
}, nil
}
func buildVehicleSourceConsistency(statuses []VehicleSourceStatus, realtime []RealtimeLocationRow, protocol string) *VehicleSourceConsistency {
consistency := &VehicleSourceConsistency{
SourceCount: len(statuses),
Scope: "all_sources",
}
if strings.TrimSpace(protocol) != "" {
consistency.Scope = "single_source"
}
if consistency.SourceCount == 0 {
seen := map[string]struct{}{}
for _, row := range realtime {
if strings.TrimSpace(row.Protocol) == "" {
continue
}
seen[row.Protocol] = struct{}{}
}
consistency.SourceCount = len(seen)
}
for _, status := range statuses {
if status.Online {
consistency.OnlineSourceCount++
}
}
var minMileage, maxMileage float64
hasMileage := false
var minTime, maxTime time.Time
hasTime := false
for _, row := range realtime {
if row.Longitude != 0 || row.Latitude != 0 {
consistency.LocatedSourceCount++
}
if row.TotalMileageKm > 0 {
if !hasMileage {
minMileage = row.TotalMileageKm
maxMileage = row.TotalMileageKm
hasMileage = true
} else {
minMileage = math.Min(minMileage, row.TotalMileageKm)
maxMileage = math.Max(maxMileage, row.TotalMileageKm)
}
}
if parsed, ok := parseVehicleServiceTime(row.LastSeen); ok {
if !hasTime {
minTime = parsed
maxTime = parsed
hasTime = true
} else {
if parsed.Before(minTime) {
minTime = parsed
}
if parsed.After(maxTime) {
maxTime = parsed
}
}
}
}
for _, status := range statuses {
if parsed, ok := parseVehicleServiceTime(status.LastSeen); ok {
if !hasTime {
minTime = parsed
maxTime = parsed
hasTime = true
} else {
if parsed.Before(minTime) {
minTime = parsed
}
if parsed.After(maxTime) {
maxTime = parsed
}
}
}
}
if hasMileage {
consistency.MileageDeltaKm = maxMileage - minMileage
}
if hasTime {
consistency.SourceTimeDeltaSeconds = maxTime.Sub(minTime).Seconds()
}
return consistency
}
func parseVehicleServiceTime(value string) (time.Time, bool) {
value = strings.TrimSpace(value)
if value == "" {
return time.Time{}, false
}
layouts := []string{time.RFC3339, "2006-01-02 15:04:05", "2006-01-02T15:04:05-07:00"}
for _, layout := range layouts {
if parsed, err := time.Parse(layout, value); err == nil {
return parsed, true
}
}
return time.Time{}, false
}
func buildVehicleServiceOverview(vin string, lookupKey string, resolution *VehicleIdentityResolution, identity *VehicleRow, summary *VehicleRealtimeRow, statuses []VehicleSourceStatus, history Page[HistoryLocationRow], raw Page[RawFrameRow], mileage Page[DailyMileageRow], quality Page[QualityIssueRow]) *VehicleServiceOverview {
overview := &VehicleServiceOverview{
VIN: strings.TrimSpace(vin),