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

@@ -23,13 +23,27 @@ import (
)
func NewServer(cfg config.Config) http.Handler {
dataMode := strings.ToLower(strings.TrimSpace(cfg.DataMode))
if dataMode == "" {
dataMode = "mock"
}
var store platform.Store = platform.NewMockStore()
if cfg.MySQLDSN != "" {
var storeErr error
if dataMode != "mock" && dataMode != "production" {
storeErr = fmt.Errorf("DATA_MODE must be mock or production")
}
if dataMode == "production" && strings.TrimSpace(cfg.MySQLDSN) == "" {
storeErr = fmt.Errorf("production data mode requires MYSQL_DSN")
}
if cfg.MySQLDSN != "" && dataMode == "production" {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
db, err := platform.OpenSQL(ctx, "mysql", cfg.MySQLDSN)
if err != nil {
log.Printf("production mysql store disabled: %v", err)
if dataMode == "production" {
storeErr = fmt.Errorf("connect production mysql: %w", err)
}
} else {
var tdengine *sql.DB
if cfg.TDengineDSN != "" {
@@ -49,11 +63,15 @@ func NewServer(cfg config.Config) http.Handler {
productionStore.WithCapacityChecker(platform.NewCapacityCheckCommand(cfg.CapacityCheckBin))
log.Printf("production capacity-check probe enabled")
}
productionStore.WithAlertStreamConfig(cfg.AlertStreamMode, cfg.AlertStreamKafkaGroup)
store = productionStore
storeErr = nil
log.Printf("production mysql store enabled")
}
}
api := platform.NewHandler(platform.NewServiceWithRuntime(store, platform.RuntimeInfo{
var api http.Handler = platform.NewHandler(platform.NewServiceWithRuntime(store, platform.RuntimeInfo{
DataMode: dataMode,
ExportDir: strings.TrimSpace(cfg.ExportDir),
RequestTimeoutMs: int(cfg.RequestTimeout / time.Millisecond),
AMapWebJSConfigured: strings.TrimSpace(cfg.AMapWebJSKey) != "",
AMapAPIConfigured: strings.TrimSpace(cfg.AMapAPIKey) != "",
@@ -61,8 +79,16 @@ func NewServer(cfg config.Config) http.Handler {
AMapSecurityCodeExposed: exposedAMapSecurityCode(cfg) != "",
AMapSecurityServiceHost: strings.TrimSpace(cfg.AMapServiceHost),
PlatformRelease: strings.TrimSpace(cfg.PlatformRelease),
AlertStreamMode: strings.TrimSpace(cfg.AlertStreamMode),
AlertStreamConsumerGroup: strings.TrimSpace(cfg.AlertStreamKafkaGroup),
}))
handler := static.Handler(cfg.StaticDir, api)
if storeErr != nil {
log.Printf("platform data store unavailable: %v", storeErr)
api = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpx.WriteError(w, http.StatusServiceUnavailable, "DATA_STORE_UNAVAILABLE", "生产数据源不可用", storeErr.Error(), requestTraceID(r))
})
}
handler := static.Handler(cfg.StaticDir, withAPIAuth(api, cfg))
handler = withAMapReverseGeocodeAPI(handler, cfg, "https://restapi.amap.com", http.DefaultClient)
handler = withAppConfig(handler, cfg)
handler = withAMapSecurityProxy(handler, cfg, defaultAMapProxyUpstreams(), http.DefaultClient)
@@ -244,7 +270,8 @@ func withAMapReverseGeocodeAPI(next http.Handler, cfg config.Config, upstream st
}
query := target.Query()
query.Set("key", apiKey)
query.Set("location", fmt.Sprintf("%.6f,%.6f", longitude, latitude))
mapLongitude, mapLatitude := wgs84ToGCJ02(longitude, latitude)
query.Set("location", fmt.Sprintf("%.6f,%.6f", mapLongitude, mapLatitude))
query.Set("extensions", "base")
query.Set("radius", "1000")
query.Set("output", "JSON")
@@ -321,6 +348,38 @@ func parseReverseGeocodeCoordinate(query url.Values) (float64, float64, error) {
return longitude, latitude, nil
}
func wgs84ToGCJ02(longitude float64, latitude float64) (float64, float64) {
if longitude < 72.004 || longitude > 137.8347 || latitude < 0.8293 || latitude > 55.8271 {
return longitude, latitude
}
const semiMajorAxis = 6378245.0
const eccentricitySquared = 0.006693421622965943
longitudeOffset := transformGCJLongitude(longitude-105, latitude-35)
latitudeOffset := transformGCJLatitude(longitude-105, latitude-35)
radianLatitude := latitude / 180 * math.Pi
magic := 1 - eccentricitySquared*math.Pow(math.Sin(radianLatitude), 2)
squareRootMagic := math.Sqrt(magic)
convertedLatitude := latitude + latitudeOffset*180/((semiMajorAxis*(1-eccentricitySquared))/(magic*squareRootMagic)*math.Pi)
convertedLongitude := longitude + longitudeOffset*180/(semiMajorAxis/squareRootMagic*math.Cos(radianLatitude)*math.Pi)
return convertedLongitude, convertedLatitude
}
func transformGCJLatitude(longitude float64, latitude float64) float64 {
value := -100 + 2*longitude + 3*latitude + 0.2*latitude*latitude + 0.1*longitude*latitude + 0.2*math.Sqrt(math.Abs(longitude))
value += (20*math.Sin(6*longitude*math.Pi) + 20*math.Sin(2*longitude*math.Pi)) * 2 / 3
value += (20*math.Sin(latitude*math.Pi) + 40*math.Sin(latitude/3*math.Pi)) * 2 / 3
value += (160*math.Sin(latitude/12*math.Pi) + 320*math.Sin(latitude*math.Pi/30)) * 2 / 3
return value
}
func transformGCJLongitude(longitude float64, latitude float64) float64 {
value := 300 + longitude + 2*latitude + 0.1*longitude*longitude + 0.1*longitude*latitude + 0.1*math.Sqrt(math.Abs(longitude))
value += (20*math.Sin(6*longitude*math.Pi) + 20*math.Sin(2*longitude*math.Pi)) * 2 / 3
value += (20*math.Sin(longitude*math.Pi) + 40*math.Sin(longitude/3*math.Pi)) * 2 / 3
value += (150*math.Sin(longitude/12*math.Pi) + 300*math.Sin(longitude/30*math.Pi)) * 2 / 3
return value
}
func isCoordinate(value float64, min float64, max float64) bool {
return !math.IsNaN(value) && !math.IsInf(value, 0) && value >= min && value <= max
}