chore: snapshot production code before Apple Design UI refinement

This commit is contained in:
lingniu
2026-09-17 18:05:57 +08:00
parent 9f0a87f49e
commit f7e7176f88
90 changed files with 8990 additions and 328 deletions
@@ -0,0 +1,133 @@
package main
import (
"context"
"database/sql"
"encoding/json"
"flag"
"fmt"
_ "github.com/go-sql-driver/mysql"
_ "github.com/taosdata/driver-go/v3/taosWS"
"lingniu/vehicle-data-platform/apps/api/internal/config"
"lingniu/vehicle-data-platform/apps/api/internal/dailygeo"
"log"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
if e := run(); e != nil {
log.Fatal(e)
}
}
func run() error {
mode := flag.String("mode", "status", "status, backfill, resolve, or live")
from := flag.String("from", "", "first historical date, defaults to first hydrogen day")
to := flag.String("to", "", "last historical date, defaults to yesterday")
concurrency := flag.Int("concurrency", 3, "maximum concurrent geocoding requests (1-8)")
flag.Parse()
cfg := config.Load()
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
db, e := sql.Open("mysql", cfg.MySQLDSN)
if e != nil {
return e
}
defer db.Close()
db.SetMaxOpenConns(12)
if *from == "" {
if e = db.QueryRowContext(ctx, `SELECT COALESCE(DATE_FORMAT(MIN(stat_date),'%Y-%m-%d'),'') FROM vehicle_open_daily_energy WHERE energy_type='HYDROGEN'`).Scan(from); e != nil {
return e
}
}
if *to == "" {
*to = time.Now().In(dailygeo.Shanghai).AddDate(0, 0, -1).Format("2006-01-02")
}
first, e := time.ParseInLocation("2006-01-02", *from, dailygeo.Shanghai)
if e != nil {
return e
}
last, e := time.ParseInLocation("2006-01-02", *to, dailygeo.Shanghai)
if e != nil {
return e
}
if first.After(last) {
return fmt.Errorf("invalid date range")
}
if *mode == "status" {
return printStatus(ctx, db, *from, *to)
}
if cfg.AMapAPIKey == "" {
return fmt.Errorf("AMAP_API_KEY required")
}
td, e := sql.Open(cfg.TDengineDriver, cfg.TDengineDSN)
if e != nil {
return e
}
defer td.Close()
td.SetMaxOpenConns(2)
w := dailygeo.New(db, td, cfg.TDengineDatabase, &dailygeo.AMap{Key: cfg.AMapAPIKey})
w.Concurrency = *concurrency
if *mode == "live" {
w.Run(ctx)
return ctx.Err()
}
if *mode != "backfill" && *mode != "resolve" {
return fmt.Errorf("invalid mode")
}
return w.WithLock(ctx, "vehicle_daily_geography_backfill", func() error {
if *mode == "backfill" {
for d := first; !d.After(last); d = d.AddDate(0, 0, 1) {
rctx, c := context.WithTimeout(ctx, 90*time.Second)
n, e := w.RefreshDay(rctx, d.Format("2006-01-02"))
c()
if e != nil {
return e
}
log.Printf("daily_geography_seed date=%s points=%d", d.Format("2006-01-02"), n)
}
}
for {
n, failed, e := w.ResolvePending(ctx, *from, *to, 1000)
log.Printf("daily_geography_batch resolved=%d failed=%d error=%v", n, failed, e)
if ctx.Err() != nil {
return ctx.Err()
}
if e != nil && failed == 0 {
return e
}
if n+failed == 0 {
break
}
}
return printStatus(ctx, db, *from, *to)
})
}
func printStatus(ctx context.Context, db *sql.DB, from, to string) error {
rows, e := db.QueryContext(ctx, `SELECT status,COUNT(*) FROM vehicle_daily_geography WHERE stat_date BETWEEN ? AND ? GROUP BY status`, from, to)
if e != nil {
return e
}
defer rows.Close()
counts := map[string]int{}
for rows.Next() {
var s string
var n int
if e = rows.Scan(&s, &n); e != nil {
return e
}
counts[s] = n
}
if e = rows.Err(); e != nil {
return e
}
rows.Close()
var missing int
e = db.QueryRowContext(ctx, `SELECT COUNT(*) FROM vehicle_open_daily_energy h LEFT JOIN vehicle_daily_geography g ON g.vin=h.vin COLLATE utf8mb4_unicode_ci AND g.stat_date=h.stat_date WHERE h.energy_type='HYDROGEN' AND h.stat_date BETWEEN ? AND ? AND g.vin IS NULL`, from, to).Scan(&missing)
if e != nil {
return e
}
return json.NewEncoder(os.Stdout).Encode(map[string]any{"from": from, "to": to, "counts": counts, "hydrogenDaysMissing": missing})
}
@@ -0,0 +1,93 @@
// Read-only production verification of the same repository used by HTTP APIs.
package main
import (
"context"
"database/sql"
"encoding/json"
"flag"
"fmt"
_ "github.com/go-sql-driver/mysql"
"lingniu/vehicle-data-platform/apps/api/internal/openplatform"
"math"
"os"
"reflect"
"strings"
"time"
)
func main() {
start := flag.String("start", "2026-09-11", "first date")
end := flag.String("end", "2026-09-15", "last date")
vin := flag.String("vins", "", "VINs, empty = all current vehicles")
flag.Parse()
db, err := sql.Open("mysql", os.Getenv("MYSQL_DSN"))
must(err)
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
vins := strings.Split(*vin, ",")
if *vin == "" {
vins = nil
rows, e := db.QueryContext(ctx, "SELECT DISTINCT vin FROM vehicle_identity_binding WHERE vin<>''")
must(e)
for rows.Next() {
var v string
must(rows.Scan(&v))
vins = append(vins, v)
}
must(rows.Err())
rows.Close()
}
r := openplatform.NewMySQLRepository(db)
began := time.Now()
priorities := []string{"GB32960", "YUTONG_MQTT"}
all, err := r.ReconciledMileageRange(ctx, vins, *start, *end, priorities)
must(err)
first, e := time.Parse("2006-01-02", *start)
must(e)
last, e := time.Parse("2006-01-02", *end)
must(e)
mismatches := 0
differences := 0
normal := 0
qualities := map[string]int{}
samples := map[string]openplatform.DailyMileage{}
for date := first; !date.After(last); date = date.AddDate(0, 0, 1) {
d := date.Format("2006-01-02")
single, e := r.ReconciledMileageRange(ctx, vins, d, d, priorities)
must(e)
for _, v := range vins {
key := v + "\x00" + d
value, ok := all[key]
one, exists := single[key]
if ok != exists || !reflect.DeepEqual(value, one) {
differences++
}
if !ok {
continue
}
qualities[value.DataQuality]++
prev, hasPrev := all[v+"\x00"+date.AddDate(0, 0, -1).Format("2006-01-02")]
if value.DataQuality == "" || value.DataQuality == "CARRIED_FORWARD" {
normal++
if hasPrev && math.Abs(value.TotalMileageKm-prev.TotalMileageKm-value.MileageKm) > 0.001 {
mismatches++
}
}
if len(vins) < 10 {
samples[key] = value
}
}
}
must(json.NewEncoder(os.Stdout).Encode(map[string]any{"vehicles": len(vins), "rows": len(all), "normal": normal, "qualities": qualities, "reconciliationMismatches": mismatches, "singleRangeDifferences": differences, "samples": samples, "elapsed": time.Since(began).String()}))
if mismatches > 0 || differences > 0 {
os.Exit(2)
}
}
func must(e error) {
if e != nil {
fmt.Fprintln(os.Stderr, e)
os.Exit(1)
}
}
@@ -0,0 +1,174 @@
package main
import (
"context"
"database/sql"
"encoding/json"
"flag"
"fmt"
"os"
"time"
_ "github.com/go-sql-driver/mysql"
"lingniu/vehicle-data-platform/apps/api/internal/platform"
)
func main() {
apply := flag.Bool("apply", false, "apply corrections after writing a durable backup")
backup := flag.String("backup", "", "new backup JSON path, required with --apply")
flag.Parse()
if *apply && *backup == "" {
panic("--backup is required with --apply")
}
db, err := sql.Open("mysql", os.Getenv("MYSQL_DSN"))
if err != nil {
panic("invalid database configuration")
}
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
if err = run(ctx, db, *apply, *backup); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func rows(ctx context.Context, tx *sql.Tx, query string) ([]map[string]any, error) {
r, err := tx.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer r.Close()
columns, err := r.Columns()
if err != nil {
return nil, err
}
result := []map[string]any{}
for r.Next() {
values := make([]any, len(columns))
targets := make([]any, len(columns))
for i := range values {
targets[i] = &values[i]
}
if err = r.Scan(targets...); err != nil {
return nil, err
}
item := map[string]any{}
for i, k := range columns {
if b, ok := values[i].([]byte); ok {
item[k] = string(b)
} else {
item[k] = values[i]
}
}
result = append(result, item)
}
return result, r.Err()
}
type correction struct {
id, title, status string
remove bool
}
func run(ctx context.Context, db *sql.DB, apply bool, backup string) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
archive := map[string]any{"createdAt": time.Now().UTC().Format(time.RFC3339), "reason": "decode GB32960 table 18; remove reserved-only false alarms"}
data := map[string][]map[string]any{}
// Consumer must be stopped for --apply. Lock event rows as well so a concurrent
// operator's action is serialized with the correction and fully backed up.
for _, q := range []struct{ key, sql string }{
{"events", `SELECT * FROM vehicle_alert_event WHERE rule_id='native-gb32960-alarm' ORDER BY id FOR UPDATE`},
{"evidence", `SELECT n.* FROM vehicle_native_alarm_evidence n JOIN vehicle_alert_event e ON e.id=n.event_id WHERE e.rule_id='native-gb32960-alarm'`},
{"actions", `SELECT a.* FROM vehicle_alert_event_action a JOIN vehicle_alert_event e ON e.id=a.event_id WHERE e.rule_id='native-gb32960-alarm'`},
{"state", `SELECT s.* FROM vehicle_native_alarm_state s JOIN vehicle_alert_event e ON e.id=s.active_event_id WHERE e.rule_id='native-gb32960-alarm'`},
{"notifications", `SELECT n.id,n.event_id FROM vehicle_alert_notification n JOIN vehicle_alert_event e ON e.id=n.event_id WHERE e.rule_id='native-gb32960-alarm'`},
} {
items, e := rows(ctx, tx, q.sql)
if e != nil {
return e
}
data[q.key] = items
archive[q.key] = items
}
evidence := map[string]string{}
for _, row := range data["evidence"] {
evidence[fmt.Sprint(row["event_id"])] = fmt.Sprint(row["fields_json"])
}
notified := map[string]bool{}
for _, row := range data["notifications"] {
notified[fmt.Sprint(row["event_id"])] = true
}
changes := []correction{}
renamed, removed := 0, 0
names := map[string]int{}
for _, row := range data["events"] {
id := fmt.Sprint(row["id"])
var fields map[string]json.RawMessage
if err = json.Unmarshal([]byte(evidence[id]), &fields); err != nil {
return fmt.Errorf("event %s has no valid evidence; no changes applied", id)
}
description, valid := platform.DescribeNativeAlarm(fields)
if !valid {
return fmt.Errorf("event %s has unsupported evidence; no changes applied", id)
}
names[description.Title]++
if !description.Active {
if notified[id] {
return fmt.Errorf("event %s has notifications; refusing automatic deletion", id)
}
changes = append(changes, correction{id: id, remove: true})
removed++
} else if fmt.Sprint(row["rule_name"]) != description.Title {
changes = append(changes, correction{id: id, title: description.Title, status: fmt.Sprint(row["status"])})
renamed++
}
}
if apply && len(changes) > 0 {
file, e := os.OpenFile(backup, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if e != nil {
return e
}
e = json.NewEncoder(file).Encode(archive)
if e == nil {
e = file.Sync()
}
closeErr := file.Close()
if e != nil {
return e
}
if closeErr != nil {
return closeErr
}
for _, change := range changes {
if change.remove {
if _, err = tx.ExecContext(ctx, `UPDATE vehicle_native_alarm_state SET active_event_id='' WHERE active_event_id=?`, change.id); err != nil {
return err
}
for _, table := range []string{"vehicle_alert_event_action", "vehicle_native_alarm_evidence"} {
if _, err = tx.ExecContext(ctx, "DELETE FROM "+table+" WHERE event_id=?", change.id); err != nil {
return err
}
}
if _, err = tx.ExecContext(ctx, `DELETE FROM vehicle_alert_event WHERE id=? AND rule_id='native-gb32960-alarm'`, change.id); err != nil {
return err
}
} else {
if _, err = tx.ExecContext(ctx, `UPDATE vehicle_alert_event SET rule_name=?,version=version+1 WHERE id=? AND rule_id='native-gb32960-alarm'`, change.title, change.id); err != nil {
return err
}
if _, err = tx.ExecContext(ctx, `INSERT INTO vehicle_alert_event_action(event_id,action,from_status,to_status,actor,note) VALUES(?,'repair',?,?,'native-alarm-repair','按GB/T 32960.3-2016表18修正具体告警名称,保留原始证据')`, change.id, change.status, change.status); err != nil {
return err
}
}
}
if err = tx.Commit(); err != nil {
return err
}
}
return json.NewEncoder(os.Stdout).Encode(map[string]any{"applied": apply, "scanned": len(data["events"]), "renamed": renamed, "removed": removed, "descriptions": names})
}
@@ -0,0 +1,108 @@
package main
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/DATA-DOG/go-sqlmock"
)
func expectSnapshot(mock sqlmock.Sqlmock, notified bool) {
mock.ExpectBegin()
mock.ExpectQuery(`SELECT \* FROM vehicle_alert_event`).WillReturnRows(sqlmock.NewRows([]string{"id", "rule_name", "status"}).AddRow("false-event", "车辆原生告警", "unprocessed").AddRow("real-event", "车辆原生告警", "processing"))
payload := func(flag string) string {
data := map[string]any{"gb32960.alarm.max_alarm_level": "0", "gb32960.alarm.general_alarm_flag": flag}
for _, key := range []string{"battery_faults", "motor_faults", "engine_faults", "other_faults"} {
data["gb32960.alarm."+key] = []string{}
}
b, _ := json.Marshal(data)
return string(b)
}
mock.ExpectQuery(`SELECT n.\* FROM vehicle_native_alarm_evidence`).WillReturnRows(sqlmock.NewRows([]string{"event_id", "fields_json"}).AddRow("false-event", payload("0x00300000")).AddRow("real-event", payload("0x00380800")))
mock.ExpectQuery(`SELECT a.\* FROM vehicle_alert_event_action`).WillReturnRows(sqlmock.NewRows([]string{"event_id", "action"}).AddRow("false-event", "trigger"))
mock.ExpectQuery(`SELECT s.\* FROM vehicle_native_alarm_state`).WillReturnRows(sqlmock.NewRows([]string{"vin", "active_event_id"}).AddRow("VIN1", "false-event"))
notifications := sqlmock.NewRows([]string{"id", "event_id"})
if notified {
notifications.AddRow(1, "false-event")
}
mock.ExpectQuery(`SELECT n.id,n.event_id`).WillReturnRows(notifications)
}
func TestRepairDryRunNeverWrites(t *testing.T) {
db, mock, _ := sqlmock.New()
defer db.Close()
expectSnapshot(mock, false)
mock.ExpectRollback()
if err := run(t.Context(), db, false, ""); err != nil {
t.Fatal(err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestRepairBacksUpAndCorrectsOnlyNativeEvents(t *testing.T) {
db, mock, _ := sqlmock.New()
defer db.Close()
expectSnapshot(mock, false)
mock.ExpectExec(`UPDATE vehicle_native_alarm_state`).WithArgs("false-event").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`DELETE FROM vehicle_alert_event_action`).WithArgs("false-event").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`DELETE FROM vehicle_native_alarm_evidence`).WithArgs("false-event").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`DELETE FROM vehicle_alert_event WHERE id=\? AND rule_id='native-gb32960-alarm'`).WithArgs("false-event").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`UPDATE vehicle_alert_event SET rule_name=\?,version=version\+1 WHERE id=\? AND rule_id='native-gb32960-alarm'`).WithArgs("绝缘报警", "real-event").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`INSERT INTO vehicle_alert_event_action`).WithArgs("real-event", "processing", "processing").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
path := filepath.Join(t.TempDir(), "backup.json")
if err := run(t.Context(), db, true, path); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var archive map[string]json.RawMessage
if err = json.Unmarshal(data, &archive); err != nil {
t.Fatal(err)
}
for _, key := range []string{"events", "evidence", "actions", "state"} {
if len(archive[key]) == 0 {
t.Fatalf("missing backup %s", key)
}
}
info, _ := os.Stat(path)
if info.Mode().Perm() != 0600 {
t.Fatal("backup permissions")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestRepairRefusesToOverwriteBackup(t *testing.T) {
db, mock, _ := sqlmock.New()
defer db.Close()
expectSnapshot(mock, false)
mock.ExpectRollback()
path := filepath.Join(t.TempDir(), "backup.json")
if err := os.WriteFile(path, []byte("previous backup"), 0600); err != nil {
t.Fatal(err)
}
if err := run(t.Context(), db, true, path); err == nil {
t.Fatal("overwrote backup")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestRepairRefusesToOrphanNotifications(t *testing.T) {
db, mock, _ := sqlmock.New()
defer db.Close()
expectSnapshot(mock, true)
mock.ExpectRollback()
if err := run(t.Context(), db, true, filepath.Join(t.TempDir(), "backup.json")); err == nil {
t.Fatal("deleted notified event")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}