package platform import ( "context" "database/sql" "encoding/json" "math" "time" ) const nativeAlarmRuleID = "native-gb32960-alarm" // Require the complete 0x07 unit: absent/invalid data must never clear an alarm. func nativeAlarmFields(record AlertStreamRecord) (map[string]json.RawMessage, int, bool, bool) { if record.Protocol != "GB32960" { return nil, 0, false, false } fields := make(map[string]json.RawMessage, 6) level, levelOK := alertStreamRawNumber(record.Fields["gb32960.alarm.max_alarm_level"]) flag, flagOK := alertStreamRawNumber(record.Fields["gb32960.alarm.general_alarm_flag"]) if !levelOK || !flagOK || level < 0 || level > 3 || math.Trunc(level) != level || flag < 0 || flag > math.MaxUint32 || math.Trunc(flag) != flag { return nil, 0, false, false } mask := nativeAlarm2016Mask if nativeAlarmVersion(record.Fields) == "V2025" { mask = math.MaxUint32 } active := level > 0 || uint32(flag)&mask != 0 if version := record.Fields["gb32960.header.version"]; len(version) > 0 { fields["gb32960.header.version"] = version } for _, name := range []string{"max_alarm_level", "general_alarm_flag", "battery_faults", "motor_faults", "engine_faults", "other_faults"} { key := "gb32960.alarm." + name raw := record.Fields[key] if name != "max_alarm_level" && name != "general_alarm_flag" { // The gateway's FIELDS contract serializes complex KV values as JSON strings. var encoded string if json.Unmarshal(raw, &encoded) == nil { raw = json.RawMessage(encoded) } var codes []string if len(raw) == 0 || string(raw) == "null" || json.Unmarshal(raw, &codes) != nil || codes == nil { return nil, 0, false, false } active = active || len(codes) > 0 } fields[key] = raw } return fields, int(level), active, true } // Runs in the same transaction as the Kafka checkpoint, independently of rules. func recordNativeAlarmsTx(ctx context.Context, tx *sql.Tx, records []AlertStreamRecord, result *AlertEvaluationResult) error { eligible := make([]AlertStreamRecord, 0) for _, record := range records { if _, _, _, ok := nativeAlarmFields(record); ok && !record.Late { eligible = append(eligible, record) } } if len(eligible) == 0 { return nil } metadata, err := loadAlertStreamVehicleMetadata(ctx, tx, eligible) if err != nil { return err } for _, record := range eligible { fields, level, active, _ := nativeAlarmFields(record) // Serialize concurrent consumers for this VIN, including the first observation. if _, err = tx.ExecContext(ctx, `INSERT IGNORE INTO vehicle_native_alarm_state(vin,last_event_at) VALUES(?,'1970-01-01 00:00:00')`, record.VIN); err != nil { return err } var lastAt time.Time var eventID string if err = tx.QueryRowContext(ctx, `SELECT last_event_at,active_event_id FROM vehicle_native_alarm_state WHERE vin=? FOR UPDATE`, record.VIN).Scan(&lastAt, &eventID); err != nil { return err } if !record.EventAt.After(lastAt) { result.LateObservations++ continue } if active && eventID == "" { eventID, err = newAlertID("alert") if err != nil { return err } severity := "minor" if level == 2 { severity = "major" } if level == 3 { severity = "critical" } description, _ := DescribeNativeAlarm(fields) title := description.Title rule := AlertRule{ID: nativeAlarmRuleID, Name: title, Severity: severity, TriggerType: "metric", Metric: "alarm_active", Operator: "eq", Threshold: 1} item := alertStreamEvidence(record, metadata[record.VIN], nil) shanghai := time.FixedZone("Asia/Shanghai", 8*60*60) item.EventAt = record.EventAt.In(shanghai).Format("2006-01-02 15:04:05.999") item.ReceivedAt = record.ReceivedAt.In(shanghai).Format("2006-01-02 15:04:05.999") query, args := buildAlertEventInsert(eventID, nativeAlarmRuleID+"|"+record.VIN+"|GB32960", rule, item, 1) // Missing coordinates must not appear as a real position at (0,0). if !item.HasLocation { args[17], args[18], args[19] = "", nil, nil } if _, err = tx.ExecContext(ctx, query, args...); err != nil { return err } payload, marshalErr := json.Marshal(fields) if marshalErr != nil { return marshalErr } if _, err = tx.ExecContext(ctx, `INSERT INTO vehicle_native_alarm_evidence(event_id,fields_json) VALUES(?,?)`, eventID, string(payload)); 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(?,'trigger','','unprocessed','native-gb32960','车辆上报原生告警')`, eventID); err != nil { return err } result.Opened++ } else if !active && eventID != "" { update, updateErr := tx.ExecContext(ctx, `UPDATE vehicle_alert_event SET status='recovered',recovered_at=?,version=version+1 WHERE id=? AND status IN ('unprocessed','processing')`, record.EventAt, eventID) if updateErr != nil { return updateErr } count, countErr := update.RowsAffected() if countErr != nil { return countErr } if count > 0 { if _, err = tx.ExecContext(ctx, `INSERT INTO vehicle_alert_event_action(event_id,action,from_status,to_status,actor,note) VALUES(?,'recover','','recovered','native-gb32960','车辆上报原生告警解除')`, eventID); err != nil { return err } result.Recovered++ } eventID = "" } if _, err = tx.ExecContext(ctx, `UPDATE vehicle_native_alarm_state SET last_event_at=?,active_event_id=? WHERE vin=?`, record.EventAt, eventID, record.VIN); err != nil { return err } } return nil }