feat: build vehicle data platform and production pipeline

This commit is contained in:
lingniu
2026-07-14 12:35:33 +08:00
parent b452be3b94
commit bb59303a4b
270 changed files with 88016 additions and 1975 deletions

View File

@@ -3,53 +3,121 @@ package config
import (
"os"
"strconv"
"strings"
"time"
)
type Config struct {
HTTPAddr string
StaticDir string
MySQLDSN string
RedisAddr string
RedisUsername string
RedisPassword string
RedisDB int
TDengineDriver string
TDengineDSN string
TDengineDatabase string
CapacityCheckBin string
AuthToken string
RequestTimeout time.Duration
AMapWebJSKey string
AMapAPIKey string
AMapSecurityCode string
AMapServiceHost string
PlatformRelease string
HTTPAddr string
StaticDir string
MySQLDSN string
RedisAddr string
RedisUsername string
RedisPassword string
RedisDB int
TDengineDriver string
TDengineDSN string
TDengineDatabase string
CapacityCheckBin string
AuthToken string
AuthMode string
AuthTokensJSON string
DataMode string
ExportDir string
RequestTimeout time.Duration
AMapWebJSKey string
AMapAPIKey string
AMapSecurityCode string
AMapServiceHost string
PlatformRelease string
AlertEvaluationInterval time.Duration
AlertStreamMode string
AlertStreamKafkaBrokers []string
AlertStreamKafkaTopics []string
AlertStreamKafkaGroup string
AlertStreamBatchSize int
AlertStreamBatchWait time.Duration
AlertStreamLateness time.Duration
}
func Load() Config {
return Config{
HTTPAddr: env("HTTP_ADDR", ":20300"),
StaticDir: env("STATIC_DIR", ""),
MySQLDSN: os.Getenv("MYSQL_DSN"),
RedisAddr: os.Getenv("REDIS_ADDR"),
RedisUsername: os.Getenv("REDIS_USERNAME"),
RedisPassword: os.Getenv("REDIS_PASSWORD"),
RedisDB: envInt("REDIS_DB", 50),
TDengineDriver: env("TDENGINE_DRIVER", "taosWS"),
TDengineDSN: os.Getenv("TDENGINE_DSN"),
TDengineDatabase: env("TDENGINE_DATABASE", "lingniu_vehicle_ts"),
CapacityCheckBin: os.Getenv("CAPACITY_CHECK_BIN"),
AuthToken: os.Getenv("AUTH_TOKEN"),
RequestTimeout: time.Duration(envInt("REQUEST_TIMEOUT_MS", 5000)) * time.Millisecond,
AMapWebJSKey: os.Getenv("AMAP_WEB_JS_KEY"),
AMapAPIKey: os.Getenv("AMAP_API_KEY"),
AMapSecurityCode: os.Getenv("AMAP_SECURITY_JS_CODE"),
AMapServiceHost: os.Getenv("AMAP_SECURITY_SERVICE_HOST"),
PlatformRelease: os.Getenv("PLATFORM_RELEASE"),
HTTPAddr: env("HTTP_ADDR", ":20300"),
StaticDir: env("STATIC_DIR", ""),
MySQLDSN: os.Getenv("MYSQL_DSN"),
RedisAddr: os.Getenv("REDIS_ADDR"),
RedisUsername: os.Getenv("REDIS_USERNAME"),
RedisPassword: os.Getenv("REDIS_PASSWORD"),
RedisDB: envInt("REDIS_DB", 50),
TDengineDriver: env("TDENGINE_DRIVER", "taosWS"),
TDengineDSN: os.Getenv("TDENGINE_DSN"),
TDengineDatabase: env("TDENGINE_DATABASE", "lingniu_vehicle_ts"),
CapacityCheckBin: os.Getenv("CAPACITY_CHECK_BIN"),
AuthToken: os.Getenv("AUTH_TOKEN"),
AuthMode: authMode(),
AuthTokensJSON: os.Getenv("AUTH_TOKENS_JSON"),
DataMode: dataMode(),
ExportDir: os.Getenv("EXPORT_DIR"),
RequestTimeout: time.Duration(envInt("REQUEST_TIMEOUT_MS", 5000)) * time.Millisecond,
AMapWebJSKey: os.Getenv("AMAP_WEB_JS_KEY"),
AMapAPIKey: os.Getenv("AMAP_API_KEY"),
AMapSecurityCode: os.Getenv("AMAP_SECURITY_JS_CODE"),
AMapServiceHost: os.Getenv("AMAP_SECURITY_SERVICE_HOST"),
PlatformRelease: os.Getenv("PLATFORM_RELEASE"),
AlertEvaluationInterval: time.Duration(envInt("ALERT_EVALUATION_INTERVAL_SEC", 10)) * time.Second,
AlertStreamMode: strings.ToLower(env("ALERT_STREAM_MODE", "disabled")),
AlertStreamKafkaBrokers: splitCSV(firstEnv("ALERT_STREAM_KAFKA_BROKERS", "KAFKA_BROKERS")),
AlertStreamKafkaTopics: splitCSV(env("ALERT_STREAM_KAFKA_TOPICS", "vehicle.fields.go.gb32960.v1,vehicle.fields.go.jt808.v1,vehicle.fields.go.yutong-mqtt.v1")),
AlertStreamKafkaGroup: env("ALERT_STREAM_KAFKA_GROUP", "vehicle-alert-stream-v1"),
AlertStreamBatchSize: envInt("ALERT_STREAM_BATCH_SIZE", 200),
AlertStreamBatchWait: time.Duration(envInt("ALERT_STREAM_BATCH_WAIT_MS", 100)) * time.Millisecond,
AlertStreamLateness: time.Duration(envInt("ALERT_STREAM_LATENESS_SEC", 120)) * time.Second,
}
}
func firstEnv(keys ...string) string {
for _, key := range keys {
if value := os.Getenv(key); value != "" {
return value
}
}
return ""
}
func splitCSV(raw string) []string {
parts := strings.Split(raw, ",")
values := make([]string, 0, len(parts))
seen := map[string]bool{}
for _, part := range parts {
part = strings.TrimSpace(part)
if part != "" && !seen[part] {
seen[part] = true
values = append(values, part)
}
}
return values
}
func dataMode() string {
if value := os.Getenv("DATA_MODE"); value != "" {
return value
}
if os.Getenv("MYSQL_DSN") != "" {
return "production"
}
return "mock"
}
func authMode() string {
if value := os.Getenv("AUTH_MODE"); value != "" {
return value
}
if os.Getenv("AUTH_TOKEN") != "" || os.Getenv("AUTH_TOKENS_JSON") != "" {
return "enforce"
}
return "disabled"
}
func env(key, fallback string) string {
if value := os.Getenv(key); value != "" {
return value

View File

@@ -34,3 +34,19 @@ func TestLoadReadsAMapServerAPIKey(t *testing.T) {
t.Fatalf("AMapAPIKey = %q", cfg.AMapAPIKey)
}
}
func TestLoadSelectsProductionDataModeWhenMySQLIsConfigured(t *testing.T) {
t.Setenv("DATA_MODE", "")
t.Setenv("MYSQL_DSN", "user:password@tcp(db:3306)/platform")
if cfg := Load(); cfg.DataMode != "production" {
t.Fatalf("DataMode = %q, want production", cfg.DataMode)
}
}
func TestLoadKeepsExplicitMockDataMode(t *testing.T) {
t.Setenv("DATA_MODE", "mock")
t.Setenv("MYSQL_DSN", "user:password@tcp(db:3306)/platform")
if cfg := Load(); cfg.DataMode != "mock" {
t.Fatalf("DataMode = %q, want mock", cfg.DataMode)
}
}