Files
silk/server-go/internal/handler/control.go
T
2026-08-17 21:43:26 +08:00

444 lines
13 KiB
Go

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)
}
}