diff --git a/api.go b/api.go index 7d72708..6f06bba 100644 --- a/api.go +++ b/api.go @@ -119,6 +119,11 @@ type QueryDeviceChannel struct { Sort string `json:"sort"` // Channel-根据数据库ID排序/iD-根据通道ID排序 SMS string `json:"sms"` Filter string `json:"filter"` + + Priority int `json:"priority"` // 报警参数 + Method int `json:"method"` + StartTime string `json:"starttime"` + EndTime string `json:"endtime"` } type DeleteDevice struct { @@ -184,6 +189,9 @@ type DeviceInfo struct { Latitude float64 `json:"latitude"` } +type Empty struct { +} + var apiServer *ApiServer func init() { @@ -275,10 +283,12 @@ func startApiServer(addr string) { apiServer.router.HandleFunc("/api/v1/cascade/setshareallchannel", withVerify(common.WithFormDataParams(apiServer.OnShareAllChannel, SetEnable{}))) // 开启或取消级联所有通道 apiServer.router.HandleFunc("/api/v1/cascade/pushcatalog", withVerify(common.WithFormDataParams(apiServer.OnCatalogPush, SetEnable{}))) // 推送目录 apiServer.router.HandleFunc("/api/v1/device/setinfo", withVerify(common.WithFormDataParams(apiServer.OnDeviceInfoSet, DeviceInfo{}))) // 编辑设备信息 + apiServer.router.HandleFunc("/api/v1/alarm/list", withVerify(common.WithQueryStringParams(apiServer.OnAlarmList, QueryDeviceChannel{}))) // 报警查询 + apiServer.router.HandleFunc("/api/v1/alarm/remove", withVerify(common.WithFormDataParams(apiServer.OnAlarmRemove, SetEnable{}))) // 删除报警 + apiServer.router.HandleFunc("/api/v1/alarm/clear", withVerify(common.WithFormDataParams(apiServer.OnAlarmClear, Empty{}))) // 清空报警 // 暂未开发 - apiServer.router.HandleFunc("/api/v1/alarm/list", withVerify(func(w http.ResponseWriter, req *http.Request) {})) // 报警查询 - apiServer.router.HandleFunc("/api/v1/sms/list", withVerify(func(w http.ResponseWriter, req *http.Request) {})) // 报警查询 + apiServer.router.HandleFunc("/api/v1/sms/list", withVerify(func(w http.ResponseWriter, req *http.Request) {})) // 流媒体服务器列表 apiServer.router.HandleFunc("/api/v1/cloudrecord/querychannels", withVerify(func(w http.ResponseWriter, req *http.Request) {})) // 云端录像 apiServer.router.HandleFunc("/api/v1/user/list", withVerify(func(w http.ResponseWriter, req *http.Request) {})) // 用户管理 apiServer.router.HandleFunc("/api/v1/log/list", withVerify(func(w http.ResponseWriter, req *http.Request) {})) // 操作日志 @@ -1667,3 +1677,66 @@ func (api *ApiServer) OnDeviceInfoSet(params *DeviceInfo, w http.ResponseWriter, return "OK", nil } + +func (api *ApiServer) OnAlarmList(q *QueryDeviceChannel, _ http.ResponseWriter, _ *http.Request) (interface{}, error) { + conditions := make(map[string]interface{}, 0) + + // 报警查询参数 + if q.Keyword != "" { + conditions["q"] = q.Keyword + } + + if q.StartTime != "" { + conditions["starttime"] = q.StartTime + } + + if q.EndTime != "" { + conditions["endtime"] = q.EndTime + } + + if q.Priority > 0 { + conditions["alarm_priority"] = q.Priority + } + + if q.Method > 0 { + conditions["alarm_method"] = q.Method + } + + alarms, count, err := dao.Alarm.QueryAlarmList((q.Start/q.Limit)+1, q.Limit, conditions) + + if err != nil { + return nil, err + } + + v := struct { + AlarmCount int + AlarmList []*dao.AlarmModel + AlarmPublishToRedis bool + AlarmReserveDays int + }{ + AlarmCount: count, + AlarmList: alarms, + AlarmPublishToRedis: true, + AlarmReserveDays: common.Config.AlarmReserveDays, + } + + return &v, nil +} + +func (api *ApiServer) OnAlarmRemove(params *SetEnable, _ http.ResponseWriter, _ *http.Request) (interface{}, error) { + // 删除报警 + if err := dao.Alarm.DeleteAlarm(params.ID); err != nil { + return nil, err + } + + return "OK", nil +} + +func (api *ApiServer) OnAlarmClear(_ *Empty, _ http.ResponseWriter, req *http.Request) (interface{}, error) { + // 清空报警 + if err := dao.Alarm.ClearAlarm(); err != nil { + return nil, err + } + + return "OK", nil +} diff --git a/common/config.go b/common/config.go index 72267a8..27b5adf 100644 --- a/common/config.go +++ b/common/config.go @@ -3,6 +3,7 @@ package common import ( "encoding/json" "os" + "time" ) var ( @@ -19,16 +20,13 @@ type Config_ struct { Password string `json:"password"` SipContactAddr string - AliveExpires int `json:"alive_expires"` - MobilePositionInterval int `json:"mobile_position_interval"` - SubscribeExpires int `json:"subscribe_expires"` - MediaServer string `json:"media_server"` - AutoCloseOnIdle bool `json:"auto_close_on_idle"` + AliveExpires int `json:"alive_expires"` + MobilePositionInterval int `json:"mobile_position_interval"` + SubscribeExpires int `json:"subscribe_expires"` + PositionReserveDays int `json:"position_reserve_days"` + AlarmReserveDays int `json:"alarm_reserve_days"` - Redis struct { - Addr string `json:"addr"` - Password string `json:"password"` - } + MediaServer string `json:"media_server"` Hooks struct { Online string `json:"online"` @@ -61,3 +59,18 @@ func ParseConfig(path string) (*Config_, error) { return &config, err } + +func ParseGBTime(gbTime string) time.Time { + // 2023-08-10 15:04:05 + if gbTime == "" { + return time.Time{} + } + + // 解析时间字符串 + t, err := time.Parse("2006-01-02T15:04:05", gbTime) + if err != nil { + return time.Time{} + } + + return t +} diff --git a/config.json b/config.json index a2929df..b2b7a33 100644 --- a/config.json +++ b/config.json @@ -10,10 +10,13 @@ "mobile_position_interval": 10, "subscribe_expires": 3600, - "media_server": "http://0.0.0.0:8080", + "?position_reserve_days": "位置记录保留天数, 0-不保存", + "position_reserve_days": 7, - "?auto_close_on_idle": "拉流空闲时, 立即关闭流", - "auto_close_on_idle": true, + "?alarm_reserve_days": "报警记录保留天数, 0-不保存", + "alarm_reserve_days": 3, + + "media_server": "http://0.0.0.0:8080", "hooks": { "?online": "设备上线通知", diff --git a/dao/alarm.go b/dao/alarm.go new file mode 100644 index 0000000..06d0d6f --- /dev/null +++ b/dao/alarm.go @@ -0,0 +1,183 @@ +package dao + +import ( + "gb-cms/common" + "gorm.io/gorm" + "time" +) + +type AlarmModel struct { + GBModel + DeviceID string + DeviceName string // 设备名称, 方便模糊查询 + ChannelID string + ChannelName string // 通道名称, 方便模糊查询 + AlarmPriority int + AlarmPriorityName string + AlarmMethod int + AlarmMethodName string + Time string + Description *string + Longitude *float64 + Latitude *float64 + AlarmType *int + AlarmTypeName string + EventType *int +} + +func (a *AlarmModel) TableName() string { + return "lkm_alarm" +} + +type daoAlarm struct { +} + +func (d *daoAlarm) Save(alarm *AlarmModel) error { + if 1 == alarm.AlarmPriority { + alarm.AlarmPriorityName = "一级警情" + } else if 2 == alarm.AlarmPriority { + alarm.AlarmPriorityName = "二级警情" + } else if 3 == alarm.AlarmPriority { + alarm.AlarmPriorityName = "三级警情" + } else if 4 == alarm.AlarmPriority { + alarm.AlarmPriorityName = "四级警情" + } + + if 1 == alarm.AlarmMethod { + alarm.AlarmMethodName = "电话报警" + } else if 2 == alarm.AlarmMethod { + alarm.AlarmMethodName = "设备报警" + } else if 3 == alarm.AlarmMethod { + alarm.AlarmMethodName = "短信报警" + } else if 4 == alarm.AlarmMethod { + alarm.AlarmMethodName = "GPS报警" + } else if 5 == alarm.AlarmMethod { + alarm.AlarmMethodName = "视频报警" + } else if 6 == alarm.AlarmMethod { + alarm.AlarmMethodName = "设备故障报警" + } else if 7 == alarm.AlarmMethod { + alarm.AlarmMethodName = "其他报警" + } + + // + if 2 == alarm.AlarmMethod { + if alarm.AlarmType == nil { + alarm.AlarmTypeName = "设备报警" + } else if 1 == *alarm.AlarmType { + alarm.AlarmTypeName = "视频丢失报警" + } else if 2 == *alarm.AlarmType { + alarm.AlarmTypeName = "设备防拆报警" + } else if 3 == *alarm.AlarmType { + alarm.AlarmTypeName = "存储设备磁盘满报警" + } else if 4 == *alarm.AlarmType { + alarm.AlarmTypeName = "设备高温报警" + } else if 5 == *alarm.AlarmType { + alarm.AlarmTypeName = "设备低温报警" + } + } else if 5 == alarm.AlarmMethod && alarm.AlarmType != nil { + if 1 == *alarm.AlarmType { + alarm.AlarmTypeName = "人工视频报警" + } else if 2 == *alarm.AlarmType { + alarm.AlarmTypeName = "运动目标检测报警" + } else if 3 == *alarm.AlarmType { + alarm.AlarmTypeName = "遗留物检测报警" + } else if 4 == *alarm.AlarmType { + alarm.AlarmTypeName = "物体移除检测报警" + } else if 5 == *alarm.AlarmType { + alarm.AlarmTypeName = "绊线检测报警" + } else if 6 == *alarm.AlarmType { + alarm.AlarmTypeName = "入侵检测报警" + } else if 7 == *alarm.AlarmType { + alarm.AlarmTypeName = "逆行检测报警" + } else if 8 == *alarm.AlarmType { + alarm.AlarmTypeName = "徘徊检测报警" + } else if 9 == *alarm.AlarmType { + alarm.AlarmTypeName = "流量统计报警" + } else if 10 == *alarm.AlarmType { + alarm.AlarmTypeName = "密度检测报警" + } else if 11 == *alarm.AlarmType { + alarm.AlarmTypeName = "视频异常检测报警" + } else if 12 == *alarm.AlarmType { + alarm.AlarmTypeName = "快速移动报警" + } + } else if 6 == alarm.AlarmMethod && alarm.AlarmType != nil { + if 1 == *alarm.AlarmType { + alarm.AlarmTypeName = "存储设备磁盘故障报警" + } else if 2 == *alarm.AlarmType { + alarm.AlarmTypeName = "存储设备风扇故障报警" + } + } + + deviceName, _ := Device.QueryDeviceName(alarm.DeviceID) + channelName, _ := Channel.QueryChannelName(alarm.DeviceID, alarm.ChannelID) + alarm.DeviceName = deviceName + alarm.ChannelName = channelName + return DBTransaction(func(tx *gorm.DB) error { + return tx.Save(alarm).Error + }) +} + +// QueryAlarmList 分页查询报警列表 +func (d *daoAlarm) QueryAlarmList(page int, size int, conditions map[string]interface{}) ([]*AlarmModel, int, error) { + tx := db.Limit(size).Offset((page - 1) * size) + + if v, ok := conditions["order"]; ok && v != "desc" { + tx.Order("id asc") + } else { + tx.Order("id desc") + } + + if v, ok := conditions["q"]; ok && v != "" { + tx.Where("description like ? or device_id like ? or channel_id like ? or device_name like ? or channel_name like ? or alarm_type_name like ? or alarm_method_name like ? or alarm_priority_name like ?", "%"+v.(string)+"%", "%"+v.(string)+"%", "%"+v.(string)+"%", "%"+v.(string)+"%", "%"+v.(string)+"%", "%"+v.(string)+"%", "%"+v.(string)+"%", "%"+v.(string)+"%") + } + + if v, ok := conditions["starttime"]; ok && v != "" { + tx.Where("created_at >= ?", common.ParseGBTime(v.(string))) + } + + if v, ok := conditions["endtime"]; ok && v != "" { + tx.Where("created_at <= ?", common.ParseGBTime(v.(string))) + } + + if v, ok := conditions["alarm_priority"]; ok && v.(int) > 0 { + tx.Where("alarm_priority = ?", v.(int)) + } + + if v, ok := conditions["alarm_method"]; ok && v.(int) > 0 { + tx.Where("alarm_method = ?", v.(int)) + } + + var alarms []*AlarmModel + if tx := tx.Find(&alarms); tx.Error != nil { + return nil, 0, tx.Error + } + + var count int64 + tx.Count(&count) + + return alarms, int(count), nil +} + +// DeleteAlarm 删除报警 +func (d *daoAlarm) DeleteAlarm(id int) error { + return DBTransaction(func(tx *gorm.DB) error { + return tx.Delete(&AlarmModel{}, id).Unscoped().Error + }) +} + +func (d *daoAlarm) ClearAlarm() error { + // 清空报警 + return DBTransaction(func(tx *gorm.DB) error { + return tx.Exec("DELETE FROM lkm_alarm;").Error + }) +} + +func (d *daoAlarm) DeleteExpired(time time.Time) error { + // 删除过期的报警记录 + return DBTransaction(func(tx *gorm.DB) error { + return tx.Where("created_at < ?", time).Delete(&AlarmModel{}).Unscoped().Error + }) +} diff --git a/dao/channel.go b/dao/channel.go index 79b25d6..7586fa1 100644 --- a/dao/channel.go +++ b/dao/channel.go @@ -319,3 +319,13 @@ func (d *daoChannel) QueryChannelsByParentID(rootId string, parentId string) ([] } return channels, nil } + +func (d *daoChannel) QueryChannelName(rootId string, channelId string) (string, error) { + var channel ChannelModel + tx := db.Select("name").Where("root_id =? and device_id =?", rootId, channelId).Take(&channel) + if tx.Error != nil { + return "", tx.Error + } + + return channel.Name, nil +} diff --git a/dao/device.go b/dao/device.go index 3d27157..ac82598 100644 --- a/dao/device.go +++ b/dao/device.go @@ -37,6 +37,8 @@ type DeviceModel struct { CatalogSubscribe bool `json:"catalog_subscribe"` // 是否开启目录订阅 AlarmSubscribe bool `json:"alarm_subscribe"` // 是否开启报警订阅 PositionSubscribe bool `json:"position_subscribe"` // 是否开启位置订阅 + Longitude float64 + Latitude float64 } func (d *DeviceModel) TableName() string { @@ -287,3 +289,14 @@ func (d *daoDevice) UpdateDevice(deviceId string, conditions map[string]interfac return tx.Model(&DeviceModel{}).Where("device_id =?", deviceId).Updates(conditions).Error }) } + +// QueryDeviceName 查询设备名 +func (d *daoDevice) QueryDeviceName(deviceId string) (string, error) { + var device DeviceModel + tx := db.Select("name").Where("device_id =?", deviceId).Take(&device) + if tx.Error != nil { + return "", tx.Error + } + + return device.Name, nil +} diff --git a/dao/dialog.go b/dao/dialog.go index 2ba80f2..8b72eea 100644 --- a/dao/dialog.go +++ b/dao/dialog.go @@ -79,7 +79,7 @@ func (m *daoDialog) Save(dialog *SipDialogModel) error { // QueryExpiredDialogs 查找即将过期的订阅会话 func (m *daoDialog) QueryExpiredDialogs(now time.Time) ([]*SipDialogModel, error) { var dialogs []*SipDialogModel - err := db.Where("refresh_time >= ?", now).Find(&dialogs).Error + err := db.Where("refresh_time <= ?", now).Find(&dialogs).Error if err != nil { return nil, err } diff --git a/dao/position.go b/dao/position.go new file mode 100644 index 0000000..98b2eec --- /dev/null +++ b/dao/position.go @@ -0,0 +1,45 @@ +package dao + +import ( + "gorm.io/gorm" + "time" +) + +const ( + PositionSourceSubscribe = iota + 1 + PositionSourceAlarm + PositionSourceChannel +) + +type PositionModel struct { + GBModel + DeviceID string + ChannelID string + Longitude float64 + Latitude float64 + Speed *string + Direction *string + Altitude *string + Time string + Source int // 来源 +} + +func (p *PositionModel) TableName() string { + return "lkm_position" +} + +type daoPosition struct { +} + +func (d *daoPosition) SavePosition(position *PositionModel) error { + return DBTransaction(func(tx *gorm.DB) error { + return tx.Create(position).Error + }) +} + +func (d *daoPosition) DeleteExpired(time time.Time) error { + // 删除过期的位置记录 + return DBTransaction(func(tx *gorm.DB) error { + return tx.Where("created_at < ?", time).Delete(&PositionModel{}).Unscoped().Error + }) +} diff --git a/dao/sqlite.go b/dao/sqlite.go index a279c43..a3e86c4 100644 --- a/dao/sqlite.go +++ b/dao/sqlite.go @@ -26,6 +26,8 @@ var ( JTDevice = &daoJTDevice{} Blacklist = &daoBlacklist{} Dialog = &daoDialog{} + Position = &daoPosition{} + Alarm = &daoAlarm{} ) func init() { @@ -81,6 +83,10 @@ func init() { panic(err) } else if err = db.AutoMigrate(&SipDialogModel{}); err != nil { panic(err) + } else if err = db.AutoMigrate(&PositionModel{}); err != nil { + panic(err) + } else if err = db.AutoMigrate(&AlarmModel{}); err != nil { + panic(err) } StartSaveTask() diff --git a/go.mod b/go.mod index 86dc6be..be3af08 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module gb-cms -go 1.23 +go 1.23.0 + +toolchain go1.23.5 require ( github.com/ghettovoice/gosip v0.0.0-20240401112151-56d750b16008 @@ -17,19 +19,22 @@ require ( github.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/go-co-op/gocron/v2 v2.16.6 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/tevino/abool v1.2.0 // indirect diff --git a/main.go b/main.go index 25e26b5..aee5ea1 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "gb-cms/hook" "gb-cms/log" "gb-cms/stack" + "github.com/go-co-op/gocron/v2" "github.com/pretty66/websocketproxy" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -30,7 +31,7 @@ func init() { logConfig := common.LogConfig{ Level: int(zapcore.DebugLevel), - Name: "./logs/clog", + Name: "./logs/lkm_gb_cms", MaxSize: 10, MaxBackup: 100, MaxAge: 7, @@ -55,15 +56,16 @@ func main() { hook.RegisterEventUrl(hook.EventTypeDeviceOnInvite, config.Hooks.OnInvite) } + // 读取或生成密码MD5值 hash := md5.Sum([]byte("admin")) AdminMD5 = hex.EncodeToString(hash[:]) - plaintext, md5 := ReadTempPwd() + plaintext, md5Hex := ReadTempPwd() if plaintext != "" { log.Sugar.Infof("temp pwd: %s", plaintext) } - PwdMD5 = md5 + PwdMD5 = md5Hex // 加载黑名单 blacklists, err := dao.Blacklist.Load() @@ -109,11 +111,11 @@ func main() { // 在sip启动后, 关闭无效的流 for _, stream := range streams { - (&stack.Stream{stream}).Bye() + (&stack.Stream{StreamModel: stream}).Bye() } for _, sink := range sinks { - (&stack.Sink{sink}).Close(true, false) + (&stack.Sink{SinkModel: sink}).Close(true, false) } // 启动级联设备 @@ -121,6 +123,7 @@ func main() { // 启动1078设备 startJTDevices() + // 启动http服务 httpAddr := net.JoinHostPort(config.ListenIP, strconv.Itoa(config.HttpPort)) log.Sugar.Infof("启动http server. addr: %s", httpAddr) go startApiServer(httpAddr) @@ -130,6 +133,37 @@ func main() { // 启动订阅刷新任务 go stack.AddScheduledTask(time.Minute, true, stack.RefreshSubscribeScheduleTask) + // 启动定时任务, 每天凌晨3点执行 + s, _ := gocron.NewScheduler() + defer func() { _ = s.Shutdown() }() + + _, _ = s.NewJob( + gocron.CronJob( + "0 3 * * *", + false, + ), + gocron.NewTask( + func() { + // 删除过期的位置、报警记录 + now := time.Now() + alarmExpireTime := now.Add(time.Duration(common.Config.AlarmReserveDays) * 24 * time.Hour) + positionExpireTime := now.Add(time.Duration(common.Config.PositionReserveDays) * 24 * time.Hour) + // 删除过期的报警记录 + err := dao.Alarm.DeleteExpired(alarmExpireTime) + if err != nil { + log.Sugar.Errorf("删除过期的报警记录失败 err: %s", err.Error()) + } + // 删除过期的位置记录 + err = dao.Position.DeleteExpired(positionExpireTime) + if err != nil { + log.Sugar.Errorf("删除过期的位置记录失败 err: %s", err.Error()) + } + }, + ), + ) + + s.Start() + err = http.ListenAndServe(":19000", nil) if err != nil { println(err) diff --git a/stack/alarm.go b/stack/alarm.go index ede5558..e0fb6ac 100644 --- a/stack/alarm.go +++ b/stack/alarm.go @@ -33,12 +33,12 @@ const ( type AlarmNotify struct { BaseMessage - AlarmPriority string `xml:"AlarmPriority"` // - AlarmMethod string `xml:"AlarmMethod"` // - AlarmTime string `xml:"AlarmTime"` // - AlarmDescription string `xml:"AlarmDescription"` // - Longitude string `xml:"Longitude"` // - Latitude string `xml:"Latitude"` // + AlarmPriority int `xml:"AlarmPriority"` // + AlarmMethod int `xml:"AlarmMethod"` // + AlarmTime string `xml:"AlarmTime"` // + AlarmDescription *string `xml:"AlarmDescription"` // + Longitude *float64 `xml:"Longitude"` // + Latitude *float64 `xml:"Latitude"` // Info *struct { //