feat(platform): consolidate production vehicle data workflows
This commit is contained in:
139
vehicle-data-platform/apps/api/cmd/oneos-scope-sync/main.go
Normal file
139
vehicle-data-platform/apps/api/cmd/oneos-scope-sync/main.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
|
||||
"lingniu/vehicle-data-platform/apps/api/internal/businessscope"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
log.Printf("OneOS scope sync failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
sourceDSN, err := normalizedDSN("ONEOS_MYSQL_DSN", os.Getenv("ONEOS_MYSQL_DSN"), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetDSN, err := normalizedDSN("MYSQL_DSN", os.Getenv("MYSQL_DSN"), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
timeout := time.Duration(envInt("ONEOS_SCOPE_SYNC_TIMEOUT_SEC", 60)) * time.Second
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
source, err := openDB(ctx, sourceDSN, 2)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open OneOS read-only database: %w", err)
|
||||
}
|
||||
defer source.Close()
|
||||
target, err := openDB(ctx, targetDSN, 4)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open vehicle platform database: %w", err)
|
||||
}
|
||||
defer target.Close()
|
||||
candidates, err := businessscope.ReadCandidates(ctx, source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snapshot, err := businessscope.BuildSnapshot(candidates, time.Now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
maxRejected := envInt("ONEOS_SCOPE_MAX_REJECTED", 100)
|
||||
maxRejectedRatio, err := envFloat("ONEOS_SCOPE_MAX_REJECT_RATIO", 0.10)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := businessscope.ValidateSnapshot(snapshot, maxRejected, maxRejectedRatio); err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := businessscope.Publish(ctx, target, snapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("OneOS scope sync complete changed=%t candidates=%d accepted=%d rejected=%d version=%s",
|
||||
result.Changed, snapshot.Candidates, len(snapshot.Items), len(snapshot.Rejections), snapshot.SourceVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizedDSN(name, raw string, requireOneOSDatabase bool) (string, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return "", fmt.Errorf("%s is required", name)
|
||||
}
|
||||
config, err := mysql.ParseDSN(raw)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse %s: %w", name, err)
|
||||
}
|
||||
if requireOneOSDatabase && config.DBName != "ln_asset_management" {
|
||||
return "", fmt.Errorf("%s must select ln_asset_management", name)
|
||||
}
|
||||
location, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("load Asia/Shanghai timezone: %w", err)
|
||||
}
|
||||
config.ParseTime = true
|
||||
config.Loc = location
|
||||
if config.Timeout <= 0 {
|
||||
config.Timeout = 10 * time.Second
|
||||
}
|
||||
if config.ReadTimeout <= 0 {
|
||||
config.ReadTimeout = 30 * time.Second
|
||||
}
|
||||
if config.WriteTimeout <= 0 {
|
||||
config.WriteTimeout = 10 * time.Second
|
||||
}
|
||||
return config.FormatDSN(), nil
|
||||
}
|
||||
|
||||
func openDB(ctx context.Context, dsn string, maxConnections int) (*sql.DB, error) {
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db.SetMaxOpenConns(maxConnections)
|
||||
db.SetMaxIdleConns(1)
|
||||
db.SetConnMaxLifetime(5 * time.Minute)
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func envInt(key string, fallback int) int {
|
||||
value := strings.TrimSpace(os.Getenv(key))
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func envFloat(key string, fallback float64) (float64, error) {
|
||||
value := strings.TrimSpace(os.Getenv(key))
|
||||
if value == "" {
|
||||
return fallback, nil
|
||||
}
|
||||
parsed, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil || parsed < 0 || parsed > 1 {
|
||||
return 0, fmt.Errorf("%s must be a number between 0 and 1", key)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
Reference in New Issue
Block a user