package handler import ( "fmt" "log/slog" "net/http" "time" "silk-server-go/internal/middleware" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // MQTTPublisher MQTT 发布接口(后续 MQTT 模块实现后注入) type MQTTPublisher interface { Publish(topic string, payload interface{}) error } // DeviceCommander 设备命令发送接口(向特定设备发送命令) type DeviceCommander interface { PublishToDevice(deviceKey string, payload interface{}) error GetIRResult(deviceKey string) interface{} GetIRLearnedCodes(deviceKey string) []int } // mqttPublisher 全局 MQTT 发布者实例(后续 MQTT 模块初始化后赋值) var mqttPublisher MQTTPublisher // deviceCommander 全局设备命令发送者(用于 GSTMB1 等设备特定命令) var deviceCommander DeviceCommander // SetMQTTPublisher 设置全局 MQTT 发布者(供 main 或 MQTT 模块调用) func SetMQTTPublisher(p MQTTPublisher) { mqttPublisher = p } // SetDeviceCommander 设置全局设备命令发送者 func SetDeviceCommander(d DeviceCommander) { deviceCommander = d } // ControlCommand 控制命令 type ControlCommand struct { DeviceKey string `json:"deviceKey"` Action string `json:"action"` Value interface{} `json:"value"` Payload map[string]interface{} `json:"payload"` } // RegisterControlRoutes 注册控制命令路由 func RegisterControlRoutes(rg *gin.RouterGroup, db *gorm.DB) { perm := middleware.RequirePermission(db, "device:control") rg.POST("/control/send", perm, sendControl(db)) rg.POST("/control/batch", perm, batchControl(db)) rg.POST("/devices/:id/gstmb1/command", perm, sendGSTMB1Command(db)) rg.POST("/devices/:id/gstmb1/info", perm, sendGSTMB1Info(db)) rg.POST("/devices/:id/gstmb1/restart", perm, sendGSTMB1Restart(db)) rg.POST("/devices/:id/gstmb1/interval", perm, sendGSTMB1Interval(db)) rg.POST("/devices/:id/plug/on", perm, sendPlugOn(db)) rg.POST("/devices/:id/plug/off", perm, sendPlugOff(db)) rg.POST("/devices/:id/plug/statistic", perm, sendPlugStatistic(db)) rg.POST("/devices/:id/plug/info", perm, sendGSTMB1Info(db)) // 插座 info 命令与传感器相同 // GSCU1B-4G 红外控制器 rg.POST("/devices/:id/ir/learn", perm, sendIRLearn(db)) rg.POST("/devices/:id/ir/emit", perm, sendIREmit(db)) rg.POST("/devices/:id/ir/cancel", perm, sendIRCancel(db)) rg.POST("/devices/:id/ir/erase", perm, sendIRErase(db)) rg.GET("/devices/:id/ir/status", perm, getIRStatus(db)) } // sendControl 下发单条控制命令(通过 MQTT 发布到 devices/{deviceKey}/cmd 主题) func sendControl(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var cmd ControlCommand if err := c.ShouldBindJSON(&cmd); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if !requireDeviceKeyAccess(c, db, cmd.DeviceKey) { return } result, err := publishControl(cmd) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } } // ===== GSPE1B 智能插座控制 ===== // sendPlugOn 插座通电 {"type":"event","key":1} func sendPlugOn(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { if !requireObjectAccess(c, db, "device", c.Param("id")) { return } result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{ "type": "event", "key": 1, }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } } // sendPlugOff 插座断电 {"type":"event","key":0} func sendPlugOff(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { if !requireObjectAccess(c, db, "device", c.Param("id")) { return } result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{ "type": "event", "key": 0, }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } } // sendPlugStatistic 查询插座电量信息 {"type":"statistic"} func sendPlugStatistic(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { if !requireObjectAccess(c, db, "device", c.Param("id")) { return } result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{ "type": "statistic", "messageId": fmt.Sprintf("%d", time.Now().UnixMilli()), }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } } // ===== GSCU1B-4G 红外控制器 ===== // sendIRLearn 学习红外码 {"type":"infrared","action":"learn","data":{"no":N}} func sendIRLearn(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { if !requireObjectAccess(c, db, "device", c.Param("id")) { return } var body struct { No int `json:"no"` } if err := c.ShouldBindJSON(&body); err != nil || body.No < 1 || body.No > 248 { c.JSON(http.StatusBadRequest, gin.H{"error": "no 必须为 1-248 的整数"}) return } result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{ "type": "infrared", "action": "learn", "data": map[string]int{"no": body.No}, }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } } // sendIREmit 发射红外码 {"type":"infrared","action":"emit","data":{"no":N}} func sendIREmit(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { if !requireObjectAccess(c, db, "device", c.Param("id")) { return } var body struct { No int `json:"no"` } if err := c.ShouldBindJSON(&body); err != nil || body.No < 1 || body.No > 248 { c.JSON(http.StatusBadRequest, gin.H{"error": "no 必须为 1-248 的整数"}) return } result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{ "type": "infrared", "action": "emit", "data": map[string]int{"no": body.No}, }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } } // sendIRCancel 取消学习 {"type":"infrared","action":"learnCancel"} func sendIRCancel(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { if !requireObjectAccess(c, db, "device", c.Param("id")) { return } result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{ "type": "infrared", "action": "learnCancel", }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } } // sendIRErase 擦除全部红外码 {"type":"infrared","action":"erase"} func sendIRErase(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { if !requireObjectAccess(c, db, "device", c.Param("id")) { return } result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{ "type": "infrared", "action": "erase", }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } } // getIRStatus 查询红外操作结果 func getIRStatus(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { if !requireObjectAccess(c, db, "device", c.Param("id")) { return } deviceKey, err := getDeviceKey(db, c.Param("id")) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } if deviceCommander == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "设备命令服务未初始化"}) return } result := deviceCommander.GetIRResult(deviceKey) learnedCodes := deviceCommander.GetIRLearnedCodes(deviceKey) if result == nil { c.JSON(http.StatusOK, gin.H{"hasResult": false, "learnedCodes": learnedCodes}) return } c.JSON(http.StatusOK, gin.H{"hasResult": true, "result": result, "learnedCodes": learnedCodes}) } } // batchControl 批量下发控制命令 func batchControl(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var body struct { Commands []ControlCommand `json:"commands"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } results := make([]gin.H, 0, len(body.Commands)) for _, cmd := range body.Commands { if !requireDeviceKeyAccess(c, db, cmd.DeviceKey) { continue } result, err := publishControl(cmd) if err != nil { results = append(results, gin.H{"error": err.Error(), "deviceKey": cmd.DeviceKey}) continue } results = append(results, result) } c.JSON(http.StatusOK, results) } } // publishControl 发布控制命令到 MQTT 主题 devices/{deviceKey}/cmd func publishControl(cmd ControlCommand) (gin.H, error) { if cmd.DeviceKey == "" || cmd.Action == "" { return nil, &controlError{"deviceKey/action required"} } topic := "devices/" + cmd.DeviceKey + "/cmd" // 构建消息体:action + value + payload 展开 + ts payload := map[string]interface{}{ "action": cmd.Action, "value": cmd.Value, } for k, v := range cmd.Payload { payload[k] = v } payload["ts"] = time.Now().Format(time.RFC3339) // 通过 MQTT 发布(若未注入则仅记录日志) if mqttPublisher != nil { if err := mqttPublisher.Publish(topic, payload); err != nil { slog.Warn("MQTT 发布失败", "topic", topic, "error", err) } } else { slog.Warn("MQTT 发布者未注入,跳过实际发布", "topic", topic) } return gin.H{ "topic": topic, "payload": payload, "ok": true, }, nil } // controlError 控制命令错误 type controlError struct{ msg string } func (e *controlError) Error() string { return e.msg } // ===== GSTMB1 命令处理 ===== // getDeviceKey 从数据库查找设备的 deviceKey func getDeviceKey(db *gorm.DB, deviceID string) (string, error) { var device struct { DeviceKey string `gorm:"column:device_key"` } if err := db.Table("devices").Where("id = ?", deviceID).First(&device).Error; err != nil { return "", fmt.Errorf("设备不存在") } return device.DeviceKey, nil } // publishGSTMB1Command 向 GSTMB1 设备发送命令 func publishGSTMB1Command(db *gorm.DB, deviceID string, payload map[string]interface{}) (gin.H, error) { deviceKey, err := getDeviceKey(db, deviceID) if err != nil { return nil, err } if deviceCommander == nil { return nil, fmt.Errorf("设备命令服务未初始化") } if err := deviceCommander.PublishToDevice(deviceKey, payload); err != nil { return nil, err } slog.Info("GSTMB1 命令已发送", "deviceKey", deviceKey, "payload", payload) return gin.H{"ok": true, "deviceKey": deviceKey, "payload": payload}, nil } // sendGSTMB1Command 发送自定义 GSTMB1 命令(body 原样转发) func sendGSTMB1Command(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { if !requireObjectAccess(c, db, "device", c.Param("id")) { return } var body map[string]interface{} if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } result, err := publishGSTMB1Command(db, c.Param("id"), body) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } } // sendGSTMB1Info 获取设备信息 {"type":"info"} func sendGSTMB1Info(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { if !requireObjectAccess(c, db, "device", c.Param("id")) { return } result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{ "type": "info", "messageId": fmt.Sprintf("%d", time.Now().UnixMilli()), }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } } // sendGSTMB1Restart 重启设备 {"type":"setting","system":"restart"} func sendGSTMB1Restart(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { if !requireObjectAccess(c, db, "device", c.Param("id")) { return } result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{ "type": "setting", "system": "restart", }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } } // sendGSTMB1Interval 设置定时上报间隔 {"type":"setting","timerEnable":1,"timerInterval":N} func sendGSTMB1Interval(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { if !requireObjectAccess(c, db, "device", c.Param("id")) { return } var body struct { Interval int `json:"interval"` } c.ShouldBindJSON(&body) interval := body.Interval if interval < 5 { interval = 60 // 默认 60 秒 } if interval > 86400 { interval = 86400 } result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{ "type": "setting", "messageId": fmt.Sprintf("%d", time.Now().UnixMilli()), "timerEnable": 1, "timerInterval": interval, }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } }