Files
lingniu-vehicle-ingest/go/vehicle-gateway/cmd/mileage-baseline-repair/main.go
T
lingniu d6b957dfc8 feat: sync vehicle monitoring and accumulated platform updates
Add monitoring metrics, numeric sorting, report-time display, vehicle number labels, and map layout fixes. Include accumulated mileage, hydrogen, OneOS authentication, open platform, ingestion, deployment, and UI updates.

Exclude temporary files, generated reports, and credentials. Validate frontend 632 tests and production build, API packages, vehicle-map tests, and release installer tests. Gateway suite has one pre-existing date-sensitive identity throttling test failure reproduced on the parent commit.
2026-10-26 18:05:59 +08:00

168 lines
5.6 KiB
Go

// Apply a reviewed, backed-up stale-baseline plan and rebuild its projections atomically.
package main
import (
"context"
"database/sql"
"encoding/json"
"flag"
"fmt"
_ "github.com/go-sql-driver/mysql"
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/stats"
"math"
"os"
"sort"
"strconv"
"strings"
"time"
)
type change struct {
Before struct {
VIN string `json:"vin"`
Protocol string `json:"protocol"`
Date string `json:"stat_date"`
Source string `json:"source_key"`
First string `json:"first_total_mileage_km"`
FirstTime string `json:"first_event_time"`
Total string `json:"latest_total_mileage_km"`
LastTime string `json:"latest_event_time"`
Daily string `json:"daily_mileage_km"`
Status string `json:"quality_status"`
Selected int `json:"is_selected"`
} `json:"before"`
Baseline struct {
Source string `json:"source_key"`
Total string `json:"total"`
Time string `json:"time"`
} `json:"baseline"`
Daily string `json:"daily"`
Status string `json:"status"`
Reason string `json:"reason"`
}
func main() {
input := flag.String("input", "", "reviewed JSON plan")
apply := flag.Bool("apply", false, "apply plan and projections in one transaction")
manifest := flag.String("backup-manifest", "", "verified backup manifest required for apply")
selectedOnly := flag.Bool("selected-projections-only", false, "preserve final results when only unselected candidates change")
flag.Parse()
data, err := os.ReadFile(*input)
must(err)
var changes []change
must(json.Unmarshal(data, &changes))
if !*apply {
fmt.Printf("planned=%d dry_run=true\n", len(changes))
return
}
backup, err := os.ReadFile(*manifest)
must(err)
if !json.Valid(backup) {
panic("invalid backup manifest")
}
db, err := sql.Open("mysql", os.Getenv("MYSQL_DSN"))
must(err)
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
tx, err := db.BeginTx(ctx, nil)
must(err)
defer tx.Rollback()
affected := map[string]change{}
for _, c := range changes {
b := c.Before
baselineSource := b.Source
if c.Baseline.Source != "" && c.Baseline.Source != b.Source {
// Historical IP keys and current platform keys may describe the
// same terminal. Verify that identity against the registry before
// accepting an explicitly reviewed alias boundary.
currentCanonical, err := canonicalRepairSource(ctx, tx, b.Protocol, b.Source)
must(err)
baselineCanonical, err := canonicalRepairSource(ctx, tx, b.Protocol, c.Baseline.Source)
must(err)
if currentCanonical != baselineCanonical {
panic("repair cannot mix independent source counters")
}
baselineSource = c.Baseline.Source
}
total, stamp, found, err := stats.LookupLatestSourceBaselineBefore(ctx, tx, b.VIN, b.Date, envelope.Protocol(b.Protocol), baselineSource)
must(err)
expected, err := strconv.ParseFloat(c.Baseline.Total, 64)
must(err)
if !found || math.Abs(total-expected) > 0.0001 || stamp.Format("2006-01-02 15:04:05") != c.Baseline.Time {
panic(fmt.Sprintf("baseline changed: %s %s", b.VIN, b.Date))
}
oldStatus := b.Status
if oldStatus == "" {
oldStatus = stats.QualityOK
}
result, err := tx.ExecContext(ctx, `UPDATE vehicle_daily_mileage_source SET first_total_mileage_km=?,first_event_time=?,daily_mileage_km=?,quality_status=?,quality_reason=?
WHERE vin=? AND stat_date=? AND protocol=? AND source_key=?
AND first_total_mileage_km <=> ? AND first_event_time <=> ? AND latest_total_mileage_km=?
AND latest_event_time=? AND daily_mileage_km=? AND quality_status=?`, c.Baseline.Total, c.Baseline.Time, c.Daily, c.Status, c.Reason, b.VIN, b.Date, b.Protocol, b.Source, b.First, b.FirstTime, b.Total, b.LastTime, b.Daily, oldStatus)
must(err)
n, err := result.RowsAffected()
must(err)
if n != 1 {
panic(fmt.Sprintf("concurrent change: %s %s %s", b.VIN, b.Date, b.Source))
}
if !*selectedOnly || b.Selected == 1 {
affected[b.Date+"|"+b.VIN+"|"+b.Protocol] = c
}
}
keys := make([]string, 0, len(affected))
for key := range affected {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
b := affected[key].Before
must(stats.ProjectDailyMileage(ctx, tx, b.VIN, b.Date, envelope.Protocol(b.Protocol)))
}
must(tx.Commit())
must(json.NewEncoder(os.Stdout).Encode(map[string]any{"updatedSources": len(changes), "reprojectedDays": len(keys), "committed": true}))
}
func canonicalRepairSource(ctx context.Context, db stats.Queryer, protocol, key string) (string, error) {
if protocol != "JT808" {
return "", fmt.Errorf("source alias repair is only supported for JT808")
}
if strings.Contains(key, "@PLATFORM:") || strings.HasSuffix(key, "@DIRECT") {
return key, nil
}
at := strings.LastIndex(key, "@")
if at < 0 || !strings.HasPrefix(key, protocol+":") {
return "", fmt.Errorf("unsupported legacy source key")
}
rows, err := db.QueryContext(ctx, `SELECT COALESCE(source_code,''),source_kind
FROM vehicle_data_source WHERE protocol=? AND source_ip=?`, protocol, key[at+1:])
if err != nil {
return "", err
}
defer rows.Close()
if !rows.Next() {
return "", fmt.Errorf("source registry is missing the legacy IP")
}
var code, kind string
if err := rows.Scan(&code, &kind); err != nil {
return "", err
}
if err := rows.Err(); err != nil {
return "", err
}
if kind == "PLATFORM" && strings.TrimSpace(code) != "" {
return key[:at] + "@PLATFORM:" + strings.TrimSpace(code), nil
}
if kind == "DIRECT" {
return key[:at] + "@DIRECT", nil
}
return "", fmt.Errorf("legacy source has no verified platform or direct-terminal identity")
}
func must(err error) {
if err != nil {
panic(err)
}
}