From 682570970487149ca33a1ef8859ec3e62d249941 Mon Sep 17 00:00:00 2001 From: ydajiang Date: Sat, 1 Nov 2025 20:48:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E7=BA=BF=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/api.go | 5 ++++ api/api_device.go | 23 ++++++++++++++++++ dao/channel.go | 2 +- dao/log.go | 3 +-- dao/sqlite.go | 3 +++ dao/status_log.go | 54 +++++++++++++++++++++++++++++++++++++++++ stack/online_devices.go | 17 +++++++++++-- stack/recover.go | 8 +++++- stack/sip_handler.go | 22 ++++++++++++++--- 9 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 dao/status_log.go diff --git a/api/api.go b/api/api.go index a56d419..b5080ae 100644 --- a/api/api.go +++ b/api/api.go @@ -302,6 +302,8 @@ func StartApiServer(addr string) { apiServer.router.HandleFunc("/api/v1/log/list", withVerify(common.WithQueryStringParams(apiServer.OnLogList, QueryDeviceChannel{}))) // 操作日志 apiServer.router.HandleFunc("/api/v1/log/clear", withVerify(common.WithQueryStringParams(apiServer.OnLogClear, Empty{}))) // 操作日志 + apiServer.router.HandleFunc("/api/v1/device/statuslog", withVerify(common.WithQueryStringParams(apiServer.OnStatusLogList, QueryDeviceChannel{}))) // 设备上下线统计 + // 暂未开发 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) {})) // 云端录像 @@ -310,6 +312,9 @@ func StartApiServer(addr string) { apiServer.router.HandleFunc("/api/v1/setbaseconfig", withVerify(common.WithFormDataParams(apiServer.OnSetBaseConfig, Empty{}))) apiServer.router.HandleFunc("/api/v1/gm/cert/list", withVerify(func(w http.ResponseWriter, req *http.Request) {})) apiServer.router.HandleFunc("/api/v1/getrequestkey", withVerify(func(w http.ResponseWriter, req *http.Request) {})) + apiServer.router.HandleFunc("/api/v1/getrequestkey", withVerify(func(w http.ResponseWriter, req *http.Request) {})) + apiServer.router.HandleFunc("/api/v1/device/positionlog", withVerify(func(w http.ResponseWriter, req *http.Request) {})) + apiServer.router.HandleFunc("/api/v1/device/streamlog", withVerify(func(w http.ResponseWriter, req *http.Request) {})) apiServer.registerStatisticsHandler("开始录制", "/api/v1/record/start", withVerify(apiServer.OnRecordStart)) // 开启录制 apiServer.registerStatisticsHandler("结束录制", "/api/v1/record/stop", withVerify(apiServer.OnRecordStop)) // 关闭录制 diff --git a/api/api_device.go b/api/api_device.go index 3d0ee82..9cc93d8 100644 --- a/api/api_device.go +++ b/api/api_device.go @@ -444,3 +444,26 @@ func (api *ApiServer) OnPTZControl(v *QueryRecordParams, _ http.ResponseWriter, return "OK", nil } + +func (api *ApiServer) OnStatusLogList(q *QueryDeviceChannel, _ http.ResponseWriter, _ *http.Request) (interface{}, error) { + if q.Limit < 1 { + q.Limit = 10 + } + + v := struct { + LogCount int + LogList interface{} + LogReserveDays int + }{ + LogReserveDays: common.Config.LogReserveDays, + } + + logList, count, err := dao.StatusLog.QueryBySerial(q.DeviceID, q.Limit) + if err != nil { + return nil, err + } + + v.LogCount = count + v.LogList = logList + return &v, nil +} diff --git a/dao/channel.go b/dao/channel.go index 782e7a4..cff09a7 100644 --- a/dao/channel.go +++ b/dao/channel.go @@ -11,7 +11,7 @@ import ( // GBModel 解决`Model`变量名与gorm.Model冲突 type GBModel struct { ID uint `gorm:"primarykey" xml:"-"` - CreatedAt time.Time `json:"created_at" xml:"-"` + CreatedAt time.Time `json:"-" xml:"-"` UpdatedAt time.Time `json:"-" xml:"-"` } diff --git a/dao/log.go b/dao/log.go index cc0364f..a25cac5 100644 --- a/dao/log.go +++ b/dao/log.go @@ -164,7 +164,6 @@ func (l *daoLog) Clear() error { func (l *daoLog) DeleteExpired(expireTime time.Time) error { return DBTransaction(func(tx *gorm.DB) error { - tx.Delete(&LogModel{}, "created_at < ?", expireTime.Format("2006-01-02 15:04:05")) - return nil + return tx.Where("created_at < ?", expireTime).Delete(&LogModel{}).Unscoped().Error }) } diff --git a/dao/sqlite.go b/dao/sqlite.go index f09de52..7137d96 100644 --- a/dao/sqlite.go +++ b/dao/sqlite.go @@ -29,6 +29,7 @@ var ( Position = &daoPosition{} Alarm = &daoAlarm{} Log = &daoLog{} + StatusLog = &daoStatusLog{} ) func init() { @@ -90,6 +91,8 @@ func init() { panic(err) } else if err = db.AutoMigrate(&LogModel{}); err != nil { panic(err) + } else if err = db.AutoMigrate(&StatusLogModel{}); err != nil { + panic(err) } StartSaveTask() diff --git a/dao/status_log.go b/dao/status_log.go new file mode 100644 index 0000000..904b66e --- /dev/null +++ b/dao/status_log.go @@ -0,0 +1,54 @@ +package dao + +import ( + "gorm.io/gorm" + "time" +) + +// StatusLogModel 设备上下线记录 +type StatusLogModel struct { + GBModel + Serial string `json:"Serial"` + Code string `json:"Code"` + Status string `json:"Status"` + Description string `json:"Description"` + CreatedAt_ string `json:"CreatedAt" gorm:"-"` +} + +func (s *StatusLogModel) TableName() string { + return "lkm_status_log" +} + +type daoStatusLog struct { +} + +func (s *daoStatusLog) Save(model *StatusLogModel) error { + return db.Create(model).Error +} + +func (s *daoStatusLog) QueryBySerial(serial string, limit int) ([]*StatusLogModel, int, error) { + // 统计总数 + var count int64 + err := db.Model(&StatusLogModel{}).Where("serial = ?", serial).Select("id").Count(&count).Error + if err != nil { + return nil, 0, err + } else if count < 1 { + return nil, 0, nil + } + + var logs []*StatusLogModel + err = db.Where("serial = ?", serial).Order("created_at desc").Limit(limit).Find(&logs).Error + + // 格式化时间 + for _, log := range logs { + log.CreatedAt_ = log.CreatedAt.Format("2006-01-02 15:04:05") + } + + return logs, int(count), err +} + +func (s *daoStatusLog) DeleteExpired(time time.Time) error { + return DBTransaction(func(tx *gorm.DB) error { + return tx.Where("created_at < ?", time).Delete(&StatusLogModel{}).Unscoped().Error + }) +} diff --git a/stack/online_devices.go b/stack/online_devices.go index 8d2ea17..24d20f3 100644 --- a/stack/online_devices.go +++ b/stack/online_devices.go @@ -1,6 +1,7 @@ package stack import ( + "gb-cms/common" "gb-cms/dao" "gb-cms/log" "sync" @@ -91,15 +92,27 @@ func NewOnlineDeviceManager() *onlineDeviceManager { // OnExpires Redis设备ID到期回调 func OnExpires(db int, id string) { log.Sugar.Infof("设备心跳过期 device: %s", id) - CloseDevice(id) + CloseDevice(id, "设备超时离线 OFF") } -func CloseDevice(id string) { +func CloseDevice(id string, reason string) { device, _ := dao.Device.QueryDevice(id) if device == nil { log.Sugar.Errorf("设备不存在 device: %s", id) return } + // 保存设备状态日志 + err := dao.StatusLog.Save(&dao.StatusLogModel{ + Serial: id, + Code: "*", + Status: common.OFF.String(), + Description: reason, + }) + + if err != nil { + log.Sugar.Errorf("保存设备状态日志失败 device: %s err: %s", id, err.Error()) + } + (&Device{DeviceModel: device}).Close() } diff --git a/stack/recover.go b/stack/recover.go index 6b6f853..652b3ee 100644 --- a/stack/recover.go +++ b/stack/recover.go @@ -144,7 +144,7 @@ func updateDevicesStatus() { } for _, device := range offlineDevices { - CloseDevice(device) + CloseDevice(device, "服务重启时离线 OFF") } } } @@ -246,6 +246,12 @@ func Start() { log.Sugar.Errorf("删除过期的操作记录失败 err: %s", err.Error()) } + // 删除过期的设备上下线记录 + err = dao.StatusLog.DeleteExpired(logExpireTime) + if err != nil { + log.Sugar.Errorf("删除过期的设备上下线记录失败 err: %s", err.Error()) + } + // 删除过期的报警记录 err = dao.Alarm.DeleteExpired(alarmExpireTime) if err != nil { diff --git a/stack/sip_handler.go b/stack/sip_handler.go index b05531f..f7092fa 100644 --- a/stack/sip_handler.go +++ b/stack/sip_handler.go @@ -35,7 +35,7 @@ type EventHandler struct { } func (e *EventHandler) OnUnregister(id string) { - CloseDevice(id) + CloseDevice(id, "设备注销 OFF") } // OnRegister 处理设备注册请求 @@ -72,8 +72,24 @@ func (e *EventHandler) OnRegister(id, transport, addr, userAgent string) (int, G // 级联通知通道上线 device := &Device{model} - if count > 0 && !alreadyOnline { - go device.PushCatalog() + + // 新注册 + if !alreadyOnline { + err := dao.StatusLog.Save(&dao.StatusLogModel{ + Serial: id, + Code: "*", + Status: common.ON.String(), + Description: "设备注册上线 ON", + }) + + if err != nil { + log.Sugar.Errorf("保存设备状态日志失败 device: %s err: %s", id, err.Error()) + } + + // 通道不为空, 通知上级 + if count > 0 { + go device.PushCatalog() + } } return 3600, device, count < 1 || dao.Device.QueryNeedRefreshCatalog(id, now)