package realtime import ( "encoding/json" "fmt" "sort" "strconv" "strings" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) type RealtimeKVField struct { Protocol envelope.Protocol VIN string Domain string Field string Value string ValueType string EventTimeMS int64 ReceivedAtMS int64 EventID string } func realtimeKVFields(env envelope.FrameEnvelope, parsed map[string]any) []RealtimeKVField { vin := strings.TrimSpace(env.VIN) if vin == "" { return nil } eventID := env.StableEventID() add := func(rows []RealtimeKVField, domain string, fields any) []RealtimeKVField { flat := map[string]any{} flattenKV("", fields, flat) names := make([]string, 0, len(flat)) for name := range flat { names = append(names, name) } sort.Strings(names) for _, name := range names { value, valueType, ok := stringifyKVValue(flat[name]) if !ok { continue } rows = append(rows, RealtimeKVField{ Protocol: env.Protocol, VIN: vin, Domain: domain, Field: name, Value: value, ValueType: valueType, EventTimeMS: env.EventTimeMS, ReceivedAtMS: env.ReceivedAtMS, EventID: eventID, }) } return rows } var rows []RealtimeKVField keys := make([]string, 0, len(parsed)) for key := range parsed { keys = append(keys, key) } sort.Strings(keys) metadata := map[string]any{} for _, key := range keys { domain := strings.TrimSpace(key) if domain == "" { continue } value := parsed[key] switch typed := value.(type) { case map[string]any: rows = add(rows, domain, typed) case []any: rows = add(rows, domain, typed) case []map[string]any: rows = add(rows, domain, typed) default: metadata[domain] = value } } if len(metadata) > 0 { rows = add(rows, "metadata", metadata) } return rows } func flattenKV(prefix string, value any, out map[string]any) { switch typed := value.(type) { case map[string]any: keys := make([]string, 0, len(typed)) for key := range typed { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { next := key if prefix != "" { next = prefix + "." + key } flattenKV(next, typed[key], out) } case []any: for index, item := range typed { itemMap, ok := item.(map[string]any) if !ok { out[prefix] = typed return } itemKey := strconv.Itoa(index) next := itemKey if prefix != "" { next = prefix + "." + itemKey } flattenKV(next, itemMap, out) } case []map[string]any: items := make([]any, len(typed)) for index, item := range typed { items[index] = item } flattenKV(prefix, items, out) default: if prefix != "" && value != nil { out[prefix] = value } } } func stringifyKVValue(value any) (string, string, bool) { switch typed := value.(type) { case string: text := strings.TrimSpace(typed) return text, "string", text != "" case bool: return strconv.FormatBool(typed), "bool", true case float64: return strconv.FormatFloat(typed, 'f', -1, 64), "number", true case float32: return strconv.FormatFloat(float64(typed), 'f', -1, 64), "number", true case json.Number: return typed.String(), "number", strings.TrimSpace(typed.String()) != "" case int: return strconv.Itoa(typed), "number", true case int8, int16, int32, int64: return fmt.Sprintf("%d", typed), "number", true case uint, uint8, uint16, uint32, uint64: return fmt.Sprintf("%d", typed), "number", true default: data, err := json.Marshal(typed) if err != nil || string(data) == "null" { return "", "", false } return string(data), "json", true } }