Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/api/internal/platform/native_alarm.go
T

143 lines
5.4 KiB
Go

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
}