feat(go): add realtime kv projection

This commit is contained in:
lingniu
2026-07-03 11:34:52 +08:00
parent b948a46e64
commit 6fb6262d0d
10 changed files with 653 additions and 17 deletions

View File

@@ -0,0 +1,187 @@
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:
rows = add(rows, "vehicle", cloneFields(env.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 != "<nil>" {
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 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
}
}