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.
181 lines
5.7 KiB
Go
181 lines
5.7 KiB
Go
// Apply reviewed event-time boundary corrections with a verified backup.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "github.com/go-sql-driver/mysql"
|
|
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
|
|
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/stats"
|
|
)
|
|
|
|
type row struct {
|
|
VIN string `json:"vin"`
|
|
Date string `json:"stat_date"`
|
|
Protocol string `json:"protocol"`
|
|
Source string `json:"source_key"`
|
|
First string `json:"first_total_mileage_km"`
|
|
Last string `json:"latest_total_mileage_km"`
|
|
FirstTime string `json:"first_event_time"`
|
|
LastTime string `json:"latest_event_time"`
|
|
Daily string `json:"daily_mileage_km"`
|
|
Status string `json:"quality_status"`
|
|
Reason string `json:"quality_reason"`
|
|
Count int64 `json:"sample_count"`
|
|
}
|
|
type change struct {
|
|
Before row `json:"before"`
|
|
After row `json:"after"`
|
|
}
|
|
|
|
func main() {
|
|
input := flag.String("input", "", "reviewed event boundary plan")
|
|
manifest := flag.String("backup-manifest", "", "verified backup manifest")
|
|
apply := flag.Bool("apply", false, "apply changes atomically")
|
|
flag.Parse()
|
|
data, err := os.ReadFile(*input)
|
|
must(err)
|
|
var plan []change
|
|
must(json.Unmarshal(data, &plan))
|
|
loc := time.FixedZone("Asia/Shanghai", 8*3600)
|
|
for _, c := range plan {
|
|
must(validateChange(c, loc))
|
|
}
|
|
if !*apply {
|
|
fmt.Printf("planned=%d dry_run=true\n", len(plan))
|
|
return
|
|
}
|
|
var backup struct {
|
|
File string `json:"backup_file"`
|
|
SHA string `json:"backup_sha256"`
|
|
PlanSHA string `json:"plan_sha256"`
|
|
Verified bool `json:"gzip_and_json_verified"`
|
|
}
|
|
b, err := os.ReadFile(*manifest)
|
|
must(err)
|
|
must(json.Unmarshal(b, &backup))
|
|
digest := sha256.Sum256(data)
|
|
if !backup.Verified || hex.EncodeToString(digest[:]) != backup.PlanSHA {
|
|
panic("reviewed plan is not the backed-up plan")
|
|
}
|
|
compressed, err := os.ReadFile(backup.File)
|
|
must(err)
|
|
digest = sha256.Sum256(compressed)
|
|
if hex.EncodeToString(digest[:]) != backup.SHA {
|
|
panic("backup checksum mismatch")
|
|
}
|
|
db, err := sql.Open("mysql", os.Getenv("MYSQL_DSN"))
|
|
must(err)
|
|
defer db.Close()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
|
defer cancel()
|
|
tx, err := db.BeginTx(ctx, nil)
|
|
must(err)
|
|
defer tx.Rollback()
|
|
sort.Slice(plan, func(i, j int) bool {
|
|
a, b := plan[i].Before, plan[j].Before
|
|
return a.Date+a.VIN+a.Source < b.Date+b.VIN+b.Source
|
|
})
|
|
targets := map[string]row{}
|
|
for _, c := range plan {
|
|
b, a := c.Before, c.After
|
|
res, err := tx.ExecContext(ctx, `UPDATE vehicle_daily_mileage_source SET first_total_mileage_km=?,first_event_time=?,latest_total_mileage_km=?,latest_event_time=?,daily_mileage_km=?,quality_status=?,quality_reason=?
|
|
WHERE vin=? AND stat_date=? AND protocol='JT808' 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=?`, a.First, a.FirstTime, a.Last, a.LastTime, a.Daily, a.Status, a.Reason,
|
|
b.VIN, b.Date, b.Source, b.First, b.FirstTime, b.Last, b.LastTime, b.Daily, b.Status)
|
|
must(err)
|
|
n, err := res.RowsAffected()
|
|
must(err)
|
|
if n != 1 {
|
|
panic(fmt.Sprintf("concurrent change: %s %s %s", b.VIN, b.Date, b.Source))
|
|
}
|
|
targets[b.Date+"|"+b.VIN] = b
|
|
}
|
|
keys := make([]string, 0, len(targets))
|
|
for k := range targets {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
projected, preserved := 0, 0
|
|
for _, k := range keys {
|
|
b := targets[k]
|
|
rows, err := tx.QueryContext(ctx, `SELECT source_ip,COALESCE(quality_reason,'') FROM vehicle_daily_mileage_source WHERE vin=? AND stat_date=? AND protocol='JT808' AND is_selected=1 FOR UPDATE`, b.VIN, b.Date)
|
|
must(err)
|
|
manual := false
|
|
for rows.Next() {
|
|
var ip, reason string
|
|
must(rows.Scan(&ip, &reason))
|
|
manual = manual || strings.HasPrefix(ip, "manual") || strings.HasPrefix(ip, "legacy-mysql") || strings.HasPrefix(reason, "manual")
|
|
}
|
|
must(rows.Err())
|
|
must(rows.Close())
|
|
if manual {
|
|
preserved++
|
|
continue
|
|
}
|
|
must(stats.ProjectDailyMileage(ctx, tx, b.VIN, b.Date, envelope.ProtocolJT808))
|
|
projected++
|
|
}
|
|
must(tx.Commit())
|
|
must(json.NewEncoder(os.Stdout).Encode(map[string]any{"committed": true, "updatedSources": len(plan), "reprojectedDays": projected, "preservedManualDays": preserved}))
|
|
}
|
|
|
|
func validateChange(c change, loc *time.Location) error {
|
|
b, a := c.Before, c.After
|
|
if b.Protocol != "JT808" || b.VIN == "" || b.Source == "" {
|
|
return fmt.Errorf("invalid GPS identity")
|
|
}
|
|
first, err := strconv.ParseFloat(a.First, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
last, err := strconv.ParseFloat(a.Last, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
daily, err := strconv.ParseFloat(a.Daily, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
firstTime, err := time.ParseInLocation("2006-01-02 15:04:05", a.FirstTime, loc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lastTime, err := time.ParseInLocation("2006-01-02 15:04:05", a.LastTime, loc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
oldEnd, err := time.ParseInLocation("2006-01-02 15:04:05", b.LastTime, loc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if lastTime.Before(oldEnd) || firstTime.After(lastTime) || lastTime.Format("2006-01-02") != b.Date || first <= 0 || last <= 0 {
|
|
return fmt.Errorf("invalid event-time boundary %s %s", b.VIN, b.Date)
|
|
}
|
|
sample := stats.SourceMileageSample{DailyKM: last - first, FirstEventTime: firstTime, LatestEventTime: lastTime, QualityStatus: stats.QualityOK, QualityReason: a.Reason}
|
|
stats.ApplyMileageQualityRules(&sample)
|
|
if math.IsNaN(daily) || math.IsInf(daily, 0) || math.Abs(sample.DailyKM-daily) > 0.0001 || sample.QualityStatus != a.Status {
|
|
return fmt.Errorf("incorrect reviewed mileage %s %s", b.VIN, b.Date)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func must(err error) {
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|