89 lines
3.1 KiB
Go
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)
|
|
}
|