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

@@ -19,6 +19,8 @@ type RealtimeTableQuery struct {
Protocol string
VIN string
Plate string
Domain string
Field string
IncludeTotal bool
Limit int
Offset int
@@ -56,6 +58,19 @@ type LocationRow struct {
UpdatedAt string `json:"updated_at"`
}
type KVRow struct {
Protocol string `json:"protocol"`
VIN string `json:"vin"`
Domain string `json:"domain"`
Field string `json:"field"`
Value string `json:"value"`
ValueType string `json:"value_type"`
EventTime string `json:"event_time,omitempty"`
ReceivedAt string `json:"received_at,omitempty"`
EventID string `json:"event_id"`
UpdatedAt string `json:"updated_at"`
}
type SnapshotQueryRepository struct {
db mysqlQueryer
}
@@ -108,6 +123,10 @@ type LocationQueryRepository struct {
db mysqlQueryer
}
type KVQueryRepository struct {
db mysqlQueryer
}
func NewLocationQueryRepository(db mysqlQueryer) *LocationQueryRepository {
if db == nil {
panic("location query db must not be nil")
@@ -115,6 +134,13 @@ func NewLocationQueryRepository(db mysqlQueryer) *LocationQueryRepository {
return &LocationQueryRepository{db: db}
}
func NewKVQueryRepository(db mysqlQueryer) *KVQueryRepository {
if db == nil {
panic("kv query db must not be nil")
}
return &KVQueryRepository{db: db}
}
func (r *LocationQueryRepository) Count(ctx context.Context, query RealtimeTableQuery) (int64, error) {
sqlText, args := buildRealtimeCountSQL("vehicle_realtime_location", normalizeRealtimeTableQuery(query))
return queryCount(ctx, r.db, sqlText, args)
@@ -174,6 +200,49 @@ func (r *LocationQueryRepository) Query(ctx context.Context, query RealtimeTable
return out, rows.Err()
}
func (r *KVQueryRepository) Count(ctx context.Context, query RealtimeTableQuery) (int64, error) {
where, args := buildRealtimeKVWhere(normalizeRealtimeTableQuery(query))
sqlText := "SELECT COUNT(*) FROM vehicle_realtime_kv"
if len(where) > 0 {
sqlText += " WHERE " + strings.Join(where, " AND ")
}
return queryCount(ctx, r.db, sqlText, args)
}
func (r *KVQueryRepository) Query(ctx context.Context, query RealtimeTableQuery) ([]KVRow, error) {
query = normalizeRealtimeTableQuery(query)
where, args := buildRealtimeKVWhere(query)
sqlText := "SELECT protocol, vin, domain_name, field_name, field_value, value_type, event_time, received_at, event_id, updated_at FROM vehicle_realtime_kv"
if len(where) > 0 {
sqlText += " WHERE " + strings.Join(where, " AND ")
}
sqlText += " ORDER BY updated_at DESC LIMIT ? OFFSET ?"
args = append(args, query.Limit, query.Offset)
rows, err := r.db.QueryContext(ctx, sqlText, args...)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]KVRow, 0)
for rows.Next() {
var row KVRow
var eventTime, receivedAt, updatedAt scanSQLDateTime
var value sql.NullString
if err := rows.Scan(&row.Protocol, &row.VIN, &row.Domain, &row.Field, &value, &row.ValueType, &eventTime, &receivedAt, &row.EventID, &updatedAt); err != nil {
return nil, err
}
if value.Valid {
row.Value = value.String
}
row.EventTime = eventTime.String
row.ReceivedAt = receivedAt.String
row.UpdatedAt = updatedAt.String
out = append(out, row)
}
return out, rows.Err()
}
type SnapshotQueryHandler struct {
repository *SnapshotQueryRepository
}
@@ -222,6 +291,10 @@ type LocationQueryHandler struct {
repository *LocationQueryRepository
}
type KVQueryHandler struct {
repository *KVQueryRepository
}
func NewLocationQueryHandler(repository *LocationQueryRepository) *LocationQueryHandler {
if repository == nil {
panic("location query repository must not be nil")
@@ -229,6 +302,13 @@ func NewLocationQueryHandler(repository *LocationQueryRepository) *LocationQuery
return &LocationQueryHandler{repository: repository}
}
func NewKVQueryHandler(repository *KVQueryRepository) *KVQueryHandler {
if repository == nil {
panic("kv query repository must not be nil")
}
return &KVQueryHandler{repository: repository}
}
func (h *LocationQueryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
@@ -262,6 +342,39 @@ func (h *LocationQueryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
writePage(w, rows, total, query)
}
func (h *KVQueryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
if strings.Trim(r.URL.Path, "/") != "api/realtime/kv" {
writeError(w, http.StatusNotFound, "route not found")
return
}
query, err := parseRealtimeTableQuery(r)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
var total int64
if query.IncludeTotal {
total, err = h.repository.Count(r.Context(), query)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
}
rows, err := h.repository.Query(r.Context(), query)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if !query.IncludeTotal {
total = int64(len(rows))
}
writePage(w, rows, total, query)
}
func parseRealtimeTableQuery(r *http.Request) (RealtimeTableQuery, error) {
values := r.URL.Query()
limit, err := parseRealtimeBoundedInt(values.Get("limit"), 50, 1, 1000, "limit")
@@ -276,6 +389,8 @@ func parseRealtimeTableQuery(r *http.Request) (RealtimeTableQuery, error) {
Protocol: values.Get("protocol"),
VIN: values.Get("vin"),
Plate: values.Get("plate"),
Domain: values.Get("domain"),
Field: values.Get("field"),
IncludeTotal: strings.EqualFold(strings.TrimSpace(values.Get("includeTotal")), "true"),
Limit: limit,
Offset: offset,
@@ -286,12 +401,36 @@ func normalizeRealtimeTableQuery(query RealtimeTableQuery) RealtimeTableQuery {
query.Protocol = strings.ToUpper(strings.TrimSpace(query.Protocol))
query.VIN = strings.TrimSpace(query.VIN)
query.Plate = strings.TrimSpace(query.Plate)
query.Domain = strings.TrimSpace(query.Domain)
query.Field = strings.TrimSpace(query.Field)
if query.Limit <= 0 {
query.Limit = 50
}
return query
}
func buildRealtimeKVWhere(query RealtimeTableQuery) ([]string, []any) {
var where []string
var args []any
if query.Protocol != "" {
where = append(where, "protocol = ?")
args = append(args, query.Protocol)
}
if query.VIN != "" {
where = append(where, "vin = ?")
args = append(args, query.VIN)
}
if query.Domain != "" {
where = append(where, "domain_name = ?")
args = append(args, query.Domain)
}
if query.Field != "" {
where = append(where, "field_name = ?")
args = append(args, query.Field)
}
return where, args
}
func buildRealtimeCountSQL(table string, query RealtimeTableQuery) (string, []any) {
where, args := buildRealtimeWhere(query)
sqlText := "SELECT COUNT(*) FROM " + table