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.
168 lines
5.6 KiB
Go
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)
|
|
}
|
|
}
|