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}) }