chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
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())
|
||||
rg.POST("/control/batch", perm, batchControl())
|
||||
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() 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
|
||||
}
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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() 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 {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user