feat(history): align fields quality and exports
This commit is contained in:
@@ -2127,7 +2127,7 @@ func (s *Service) HistoryData(ctx context.Context, query url.Values) (HistoryDat
|
||||
pageRows := append([]HistoryDataRow(nil), rows[offset:end]...)
|
||||
columns := historyMetricsForCategory(category)
|
||||
if category == "raw" {
|
||||
columns = mergeDiscoveredRawMetrics(columns, pageRows)
|
||||
columns = mergeDiscoveredRawMetrics(columns, rows)
|
||||
}
|
||||
vehicles := map[string]struct{}{}
|
||||
sources := map[string]struct{}{}
|
||||
@@ -2396,11 +2396,8 @@ func (s *Service) historyDataForVehicle(ctx context.Context, category, keyword s
|
||||
}
|
||||
rows := make([]HistoryDataRow, 0, len(page.Items))
|
||||
for index, item := range page.Items {
|
||||
quality := "normal"
|
||||
if !validTrackCoordinate(item) {
|
||||
quality = "warning"
|
||||
}
|
||||
rows = append(rows, HistoryDataRow{ID: "location-" + item.VIN + "-" + strconv.Itoa(index) + "-" + item.DeviceTime, VIN: item.VIN, Plate: item.Plate, Protocol: item.Protocol, DeviceTime: item.DeviceTime, ServerTime: item.ServerTime, Quality: quality, Values: map[string]any{"speedKmh": item.SpeedKmh, "totalMileageKm": item.TotalMileageKm, "longitude": item.Longitude, "latitude": item.Latitude}})
|
||||
quality, reason := historyLocationQuality(item)
|
||||
rows = append(rows, HistoryDataRow{ID: "location-" + item.VIN + "-" + strconv.Itoa(index) + "-" + item.DeviceTime, VIN: item.VIN, Plate: item.Plate, Protocol: item.Protocol, DeviceTime: item.DeviceTime, ServerTime: item.ServerTime, Quality: quality, QualityReason: reason, Values: map[string]any{"speedKmh": item.SpeedKmh, "totalMileageKm": item.TotalMileageKm, "longitude": item.Longitude, "latitude": item.Latitude}})
|
||||
}
|
||||
return rows, page.Total, nil
|
||||
case "raw":
|
||||
@@ -2418,7 +2415,8 @@ func (s *Service) historyDataForVehicle(ctx context.Context, category, keyword s
|
||||
for key, value := range item.ParsedFields {
|
||||
values[key] = value
|
||||
}
|
||||
rows = append(rows, HistoryDataRow{ID: item.ID, VIN: item.VIN, Plate: item.Plate, Protocol: item.Protocol, DeviceTime: item.DeviceTime, ServerTime: item.ServerTime, Quality: "normal", EvidenceID: item.ID, Values: values})
|
||||
quality, reason := historyRawQuality(item)
|
||||
rows = append(rows, HistoryDataRow{ID: item.ID, VIN: item.VIN, Plate: item.Plate, Protocol: item.Protocol, DeviceTime: item.DeviceTime, ServerTime: item.ServerTime, Quality: quality, QualityReason: reason, EvidenceID: item.ID, Values: values})
|
||||
}
|
||||
return rows, page.Total, nil
|
||||
case "mileage":
|
||||
@@ -2437,17 +2435,83 @@ func (s *Service) historyDataForVehicle(ctx context.Context, category, keyword s
|
||||
}
|
||||
rows := make([]HistoryDataRow, 0, len(page.Items))
|
||||
for index, item := range page.Items {
|
||||
quality := "normal"
|
||||
if item.AnomalySeverity != "" {
|
||||
quality = item.AnomalySeverity
|
||||
}
|
||||
rows = append(rows, HistoryDataRow{ID: "mileage-" + item.VIN + "-" + strconv.Itoa(index) + "-" + item.Date, VIN: item.VIN, Plate: item.Plate, Protocol: item.Source, DeviceTime: item.Date, ServerTime: item.Date, Quality: quality, Values: map[string]any{"startMileageKm": item.StartMileageKm, "endMileageKm": item.EndMileageKm, "dailyMileageKm": item.DailyMileageKm}})
|
||||
quality, reason := historyMileageQuality(item)
|
||||
rows = append(rows, HistoryDataRow{ID: "mileage-" + item.VIN + "-" + strconv.Itoa(index) + "-" + item.Date, VIN: item.VIN, Plate: item.Plate, Protocol: item.Source, DeviceTime: item.Date, ServerTime: item.Date, Quality: quality, QualityReason: reason, Values: map[string]any{"startMileageKm": item.StartMileageKm, "endMileageKm": item.EndMileageKm, "dailyMileageKm": item.DailyMileageKm}})
|
||||
}
|
||||
return rows, page.Total, nil
|
||||
}
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
func historyLocationQuality(item HistoryLocationRow) (string, string) {
|
||||
if !validTrackCoordinate(item) {
|
||||
return "warning", "坐标超出有效范围或为无效零点"
|
||||
}
|
||||
if item.SpeedKmh < 0 || item.SpeedKmh > 200 {
|
||||
return "warning", fmt.Sprintf("速度 %.2f km/h 超出 0–200 km/h 基础校验范围", item.SpeedKmh)
|
||||
}
|
||||
if item.TotalMileageKm < 0 {
|
||||
return "warning", "总里程为负值"
|
||||
}
|
||||
deviceTime, deviceOK := parseVehicleServiceTime(item.DeviceTime)
|
||||
serverTime, serverOK := parseVehicleServiceTime(item.ServerTime)
|
||||
if !deviceOK || !serverOK {
|
||||
return "warning", "设备时间或服务接收时间无法解析"
|
||||
}
|
||||
delaySeconds := int64(math.Abs(serverTime.Sub(deviceTime).Seconds()))
|
||||
if delaySeconds > int64(latestTelemetryStaleAfter/time.Second) {
|
||||
return "warning", fmt.Sprintf("设备时间与服务接收时间相差 %d 秒,超过 5 分钟", delaySeconds)
|
||||
}
|
||||
return "normal", "坐标、速度、里程及设备/服务时间通过基础校验"
|
||||
}
|
||||
|
||||
func historyRawQuality(item RawFrameRow) (string, string) {
|
||||
status := strings.TrimSpace(item.ParseStatus)
|
||||
if status != "" && !strings.EqualFold(status, "ok") && !strings.EqualFold(status, "success") && !strings.EqualFold(status, "parsed") {
|
||||
reason := "原始报文解析状态为 " + status
|
||||
if detail := strings.TrimSpace(item.ParseError); detail != "" {
|
||||
reason += ":" + detail
|
||||
}
|
||||
return "warning", reason
|
||||
}
|
||||
deviceTime, deviceOK := parseVehicleServiceTime(item.DeviceTime)
|
||||
serverTime, serverOK := parseVehicleServiceTime(item.ServerTime)
|
||||
if !deviceOK || !serverOK {
|
||||
return "warning", "设备时间或服务接收时间无法解析"
|
||||
}
|
||||
delaySeconds := int64(math.Abs(serverTime.Sub(deviceTime).Seconds()))
|
||||
if delaySeconds > int64(latestTelemetryStaleAfter/time.Second) {
|
||||
return "warning", fmt.Sprintf("设备时间与服务接收时间相差 %d 秒,超过 5 分钟", delaySeconds)
|
||||
}
|
||||
return "normal", "报文解析成功,设备时间与服务接收时间关系正常"
|
||||
}
|
||||
|
||||
func historyMileageQuality(item DailyMileageRow) (string, string) {
|
||||
severity := strings.ToLower(strings.TrimSpace(item.AnomalySeverity))
|
||||
if severity != "" && severity != "normal" && severity != "good" && severity != "ok" {
|
||||
quality := "warning"
|
||||
if strings.Contains(severity, "error") || strings.Contains(severity, "critical") || strings.Contains(severity, "严重") {
|
||||
quality = "error"
|
||||
}
|
||||
return quality, "里程聚合记录标记异常:" + item.AnomalySeverity
|
||||
}
|
||||
if item.StartMileageKm < 0 || item.EndMileageKm < 0 || item.DailyMileageKm < 0 {
|
||||
return "warning", "起始、结束或日里程存在负值"
|
||||
}
|
||||
if item.EndMileageKm < item.StartMileageKm {
|
||||
return "warning", "结束里程小于起始里程"
|
||||
}
|
||||
calculated := item.EndMileageKm - item.StartMileageKm
|
||||
tolerance := math.Max(1, calculated*0.05)
|
||||
if math.Abs(calculated-item.DailyMileageKm) > tolerance {
|
||||
return "warning", fmt.Sprintf("日里程 %.2f km 与起止里程差 %.2f km 不一致", item.DailyMileageKm, calculated)
|
||||
}
|
||||
if item.DailyMileageKm > 1500 {
|
||||
return "warning", fmt.Sprintf("单日里程 %.2f km 超过 1500 km 基础校验范围", item.DailyMileageKm)
|
||||
}
|
||||
return "normal", "起止里程与日里程关系通过基础校验"
|
||||
}
|
||||
|
||||
func copyHistoryFilters(target, source url.Values, protocol string) {
|
||||
if protocol != "" {
|
||||
target.Set("protocol", protocol)
|
||||
@@ -2931,7 +2995,7 @@ func writeHistoryExportMetadata(writer *csv.Writer, request HistoryExportRequest
|
||||
rows := [][]string{
|
||||
{"导出元数据", "查询开始", request.DateFrom, "查询结束", request.DateTo, "车辆", strings.Join(request.Keywords, "、"), "数据类型", request.Category, "协议", firstNonEmpty(request.Protocol, "全部")},
|
||||
{"指标与单位", strings.Join(exportMetricLabels(columns), ";")},
|
||||
append([]string{"设备时间", "服务时间", "车牌", "VIN", "协议", "数据质量", "证据ID"}, exportMetricLabels(columns)...),
|
||||
append([]string{"设备时间", "服务时间", "车牌", "VIN", "协议", "数据质量", "质量原因", "证据ID"}, exportMetricLabels(columns)...),
|
||||
}
|
||||
for _, row := range rows {
|
||||
if err := writer.Write(row); err != nil {
|
||||
@@ -2942,7 +3006,7 @@ func writeHistoryExportMetadata(writer *csv.Writer, request HistoryExportRequest
|
||||
}
|
||||
|
||||
func historyExportRecord(row HistoryDataRow, columns []HistoryMetricDefinition) []string {
|
||||
record := []string{row.DeviceTime, row.ServerTime, row.Plate, row.VIN, row.Protocol, row.Quality, row.EvidenceID}
|
||||
record := []string{row.DeviceTime, row.ServerTime, row.Plate, row.VIN, row.Protocol, row.Quality, row.QualityReason, row.EvidenceID}
|
||||
for _, column := range columns {
|
||||
value, ok := row.Values[column.Key]
|
||||
if !ok || value == nil {
|
||||
|
||||
Reference in New Issue
Block a user