// mileage-correction writes a confirmed daily terminal odometer correction. // It is intentionally explicit: callers must provide the VIN, natural day and // confirmed cumulative mileage; it never derives values from a guess. package main import ( "context" "database/sql" "flag" "fmt" "log" "os" "strings" "time" _ "github.com/go-sql-driver/mysql" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/stats" ) func main() { vin := flag.String("vin", "", "VIN") date := flag.String("date", "", "natural day, yyyy-MM-dd") total := flag.Float64("total-km", 0, "confirmed cumulative mileage, km") flag.Parse() if strings.TrimSpace(*vin) == "" || strings.TrimSpace(*date) == "" || *total <= 0 { log.Fatal("vin, date and positive total-km are required") } dsn := strings.TrimSpace(os.Getenv("MYSQL_DSN")) if dsn == "" { log.Fatal("MYSQL_DSN is required") } loc, err := time.LoadLocation("Asia/Shanghai") if err != nil { log.Fatal(err) } eventTime, err := time.ParseInLocation("2006-01-02 15:04:05", strings.TrimSpace(*date)+" 23:59:59", loc) if err != nil { log.Fatal(err) } db, err := sql.Open("mysql", dsn) if err != nil { log.Fatal(err) } defer db.Close() ctx := context.Background() if err := db.PingContext(ctx); err != nil { log.Fatal(err) } candidate := stats.SourceMileageSample{ VIN: strings.TrimSpace(*vin), StatDate: strings.TrimSpace(*date), Protocol: envelope.ProtocolGB32960, SourceKey: "GB32960:manual-correction@manual", SourceIP: "manual", SourceEndpoint: "manual-correction", FirstTotalKM: *total, LatestTotalKM: *total, DailyKM: 0, SampleCount: 1, FirstEventTime: eventTime, LatestEventTime: eventTime, QualityStatus: stats.QualityOK, QualityReason: "manual_correction_confirmed_total", } if err := stats.UpsertSourceMileage(ctx, db, candidate); err != nil { log.Fatal(err) } if err := stats.ProjectDailyMileage(ctx, db, candidate.VIN, candidate.StatDate, candidate.Protocol); err != nil { log.Fatal(err) } // Keep both cumulative fields aligned. day_end_total_mileage_km is the API // field of record, while latest_total_mileage_km remains useful to internal // readers and must not retain the rejected reset value. if _, err := db.ExecContext(ctx, ` UPDATE vehicle_daily_mileage SET latest_total_mileage_km=?, day_end_total_mileage_km=? WHERE vin=? AND stat_date=? AND protocol=?`, *total, *total, candidate.VIN, candidate.StatDate, string(candidate.Protocol)); err != nil { log.Fatal(err) } var daily, latest, dayEnd float64 if err := db.QueryRowContext(ctx, `SELECT daily_mileage_km,latest_total_mileage_km,day_end_total_mileage_km FROM vehicle_daily_mileage WHERE vin=? AND stat_date=? AND protocol=?`, candidate.VIN, candidate.StatDate, string(candidate.Protocol)).Scan(&daily, &latest, &dayEnd); err != nil { log.Fatal(err) } fmt.Printf("corrected vin=%s date=%s protocol=%s daily_km=%.3f latest_total_km=%.3f day_end_total_km=%.3f\n", candidate.VIN, candidate.StatDate, candidate.Protocol, daily, latest, dayEnd) }