Files
lingniu-vehicle-ingest/go/vehicle-gateway/cmd/mileage-correction/main.go
T

89 lines
3.1 KiB
Go

// 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)
}