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 map[string]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 switch env.Protocol { case envelope.ProtocolGB32960: units, ok := asAnySlice(parsed["data_units"]) if !ok { return rows } for _, unit := range units { unitMap, ok := unit.(map[string]any) if !ok { continue } domain := strings.TrimSpace(strconvAny(unitMap["name"])) if domain == "" { domain = strings.TrimSpace(strconvAny(unitMap["type"])) } if domain == "" { continue } value, ok := unitMap["value"].(map[string]any) if !ok { continue } rows = add(rows, domain, domainKVFields(domain, value)) } case envelope.ProtocolJT808: fields := cloneFields(env.Fields) if location, ok := parsed["location"].(map[string]any); ok { for key, value := range location { fields[key] = value } } rows = add(rows, "location", fields) case envelope.ProtocolYutongMQTT: fields := cloneFields(env.Fields) if data, ok := parsed["data"].(map[string]any); ok { for key, value := range data { fields[key] = value } } rows = add(rows, "vehicle", fields) } return rows } func domainKVFields(domain string, value map[string]any) map[string]any { out := cloneMap(value) if domain != "gd_fc_stack" { return out } summaries, ok := asAnySlice(out["summaries"]) if !ok || len(summaries) != 1 { return out } summary, ok := summaries[0].(map[string]any) if !ok { return out } delete(out, "summaries") for key, value := range summary { out[key] = value } return out } 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) if serial := strings.TrimSpace(strconvAny(itemMap["serial_no"])); serial != "" && serial != "" { itemKey = serial } 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 } }