feat(go): add realtime kv projection
This commit is contained in:
187
go/vehicle-gateway/internal/realtime/kv.go
Normal file
187
go/vehicle-gateway/internal/realtime/kv.go
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user