feat: DevTools 数据库监看面板 + 隧道控制 + 多项 Bug 修复
**DevTools 新增功能 (Tasks 13-14):** - 首页仪表盘添加数据库实时监看卡片 (5端口状态 + 记忆数) - 侧边栏新增数据库面板,支持自动 5 秒刷新 - 数据库面板显示 PostgreSQL/Redis/Qdrant/MinIO/NATS 端口状态 - 隧道控制按钮 (启动/停止/重启/查看状态) - 新增 API 端点: GET /api/database/status, POST /api/tunnel/:action - 更新 docs/api-reference/ API 文档 **Bug 修复 (Task 15):** - 修复 pgrep -f 自匹配导致隧道状态误判 (添加 ^ssh 锚点) - devtools/src/index.js (dashboard + database/status) - scripts/tunnel.sh (is_tunnel_running + show_status) - 修复数据库面板缺少自动刷新定时器 - 修复侧边栏数据库徽章永远 display:none - 修复僵尸进程场景下按钮死锁问题 **其他改进:** - .gitignore 添加 backend/cmd, backend/iot-debug-service/main - 前端多项改进 (登录/注册/会话/流式动画等)
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DeviceType 设备类型
|
||||
type DeviceType string
|
||||
|
||||
const (
|
||||
TypeLight DeviceType = "light"
|
||||
TypeAC DeviceType = "ac"
|
||||
TypeCurtain DeviceType = "curtain"
|
||||
TypeSensor DeviceType = "sensor"
|
||||
TypeLock DeviceType = "lock"
|
||||
)
|
||||
|
||||
// Device 设备状态
|
||||
type Device struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type DeviceType `json:"type"`
|
||||
Status string `json:"status"` // on/off/closed/open/locked/unlocked
|
||||
Brightness int `json:"brightness,omitempty"`
|
||||
Color string `json:"color,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
Mode string `json:"mode,omitempty"` // cool/heat/auto
|
||||
Position int `json:"position,omitempty"` // curtain position 0-100
|
||||
Value float64 `json:"value,omitempty"` // sensor value
|
||||
Unit string `json:"unit,omitempty"` // sensor unit
|
||||
Battery int `json:"battery,omitempty"` // lock battery
|
||||
LastUpdated string `json:"last_updated"`
|
||||
History []HistoryEntry `json:"history,omitempty"`
|
||||
}
|
||||
|
||||
// HistoryEntry 设备状态历史
|
||||
type HistoryEntry struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
Field string `json:"field"`
|
||||
OldValue string `json:"old_value"`
|
||||
NewValue string `json:"new_value"`
|
||||
}
|
||||
|
||||
// DeviceStore 设备存储(线程安全)
|
||||
type DeviceStore struct {
|
||||
mu sync.RWMutex
|
||||
devices map[string]*Device
|
||||
history map[string][]HistoryEntry
|
||||
}
|
||||
|
||||
func NewDeviceStore() *DeviceStore {
|
||||
ds := &DeviceStore{
|
||||
devices: make(map[string]*Device),
|
||||
history: make(map[string][]HistoryEntry),
|
||||
}
|
||||
ds.initDevices()
|
||||
return ds
|
||||
}
|
||||
|
||||
func (ds *DeviceStore) initDevices() {
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
|
||||
devices := []*Device{
|
||||
{ID: "light-livingroom", Name: "客厅灯", Type: TypeLight, Status: "on", Brightness: 80, Color: "warm_white", LastUpdated: now},
|
||||
{ID: "light-bedroom", Name: "卧室灯", Type: TypeLight, Status: "off", Brightness: 0, Color: "warm_white", LastUpdated: now},
|
||||
{ID: "ac-livingroom", Name: "客厅空调", Type: TypeAC, Status: "on", Temperature: 26, Mode: "cool", LastUpdated: now},
|
||||
{ID: "ac-bedroom", Name: "卧室空调", Type: TypeAC, Status: "off", Temperature: 24, Mode: "auto", LastUpdated: now},
|
||||
{ID: "curtain-livingroom", Name: "客厅窗帘", Type: TypeCurtain, Status: "closed", Position: 0, LastUpdated: now},
|
||||
{ID: "sensor-temperature", Name: "温度传感器", Type: TypeSensor, Value: 25.5, Unit: "celsius", LastUpdated: now},
|
||||
{ID: "sensor-humidity", Name: "湿度传感器", Type: TypeSensor, Value: 60, Unit: "percent", LastUpdated: now},
|
||||
{ID: "lock-door", Name: "智能门锁", Type: TypeLock, Status: "locked", Battery: 85, LastUpdated: now},
|
||||
}
|
||||
|
||||
for _, d := range devices {
|
||||
ds.devices[d.ID] = d
|
||||
ds.history[d.ID] = make([]HistoryEntry, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// GetAll 获取所有设备
|
||||
func (ds *DeviceStore) GetAll() []*Device {
|
||||
ds.mu.RLock()
|
||||
defer ds.mu.RUnlock()
|
||||
|
||||
result := make([]*Device, 0, len(ds.devices))
|
||||
for _, d := range ds.devices {
|
||||
cp := *d
|
||||
cp.History = nil // 列表不返回历史
|
||||
result = append(result, &cp)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Get 获取单个设备
|
||||
func (ds *DeviceStore) Get(id string) *Device {
|
||||
ds.mu.RLock()
|
||||
defer ds.mu.RUnlock()
|
||||
|
||||
d, ok := ds.devices[id]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
cp := *d
|
||||
// 包含最近10条历史(RLock 可重入)
|
||||
if h, ok := ds.history[id]; ok && len(h) > 0 {
|
||||
start := 0
|
||||
if len(h) > 10 {
|
||||
start = len(h) - 10
|
||||
}
|
||||
cp.History = make([]HistoryEntry, len(h)-start)
|
||||
copy(cp.History, h[start:])
|
||||
} else {
|
||||
cp.History = []HistoryEntry{}
|
||||
}
|
||||
return &cp
|
||||
}
|
||||
|
||||
// GetHistory 获取设备状态历史(最近10条)
|
||||
func (ds *DeviceStore) GetHistory(id string) []HistoryEntry {
|
||||
ds.mu.RLock()
|
||||
defer ds.mu.RUnlock()
|
||||
|
||||
h, ok := ds.history[id]
|
||||
if !ok || len(h) == 0 {
|
||||
return []HistoryEntry{}
|
||||
}
|
||||
|
||||
start := 0
|
||||
if len(h) > 10 {
|
||||
start = len(h) - 10
|
||||
}
|
||||
result := make([]HistoryEntry, len(h[start:]))
|
||||
copy(result, h[start:])
|
||||
return result
|
||||
}
|
||||
|
||||
// addHistory 添加历史记录
|
||||
func (ds *DeviceStore) addHistory(id, field, oldVal, newVal string) {
|
||||
entry := HistoryEntry{
|
||||
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||
Field: field,
|
||||
OldValue: oldVal,
|
||||
NewValue: newVal,
|
||||
}
|
||||
ds.history[id] = append(ds.history[id], entry)
|
||||
}
|
||||
|
||||
// Toggle 切换设备开关状态
|
||||
func (ds *DeviceStore) Toggle(id string) (*Device, error) {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
d, ok := ds.devices[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("设备 %s 不存在", id)
|
||||
}
|
||||
|
||||
switch d.Type {
|
||||
case TypeLight:
|
||||
oldStatus := d.Status
|
||||
if d.Status == "on" {
|
||||
d.Status = "off"
|
||||
d.Brightness = 0
|
||||
} else {
|
||||
d.Status = "on"
|
||||
d.Brightness = 80
|
||||
}
|
||||
d.LastUpdated = time.Now().UTC().Format(time.RFC3339)
|
||||
ds.addHistory(id, "status", oldStatus, d.Status)
|
||||
|
||||
case TypeAC:
|
||||
oldStatus := d.Status
|
||||
if d.Status == "on" {
|
||||
d.Status = "off"
|
||||
} else {
|
||||
d.Status = "on"
|
||||
}
|
||||
d.LastUpdated = time.Now().UTC().Format(time.RFC3339)
|
||||
ds.addHistory(id, "status", oldStatus, d.Status)
|
||||
|
||||
case TypeCurtain:
|
||||
oldStatus := d.Status
|
||||
if d.Status == "closed" {
|
||||
d.Status = "open"
|
||||
d.Position = 100
|
||||
} else {
|
||||
d.Status = "closed"
|
||||
d.Position = 0
|
||||
}
|
||||
d.LastUpdated = time.Now().UTC().Format(time.RFC3339)
|
||||
ds.addHistory(id, "status", oldStatus, d.Status)
|
||||
|
||||
case TypeLock:
|
||||
oldStatus := d.Status
|
||||
if d.Status == "locked" {
|
||||
d.Status = "unlocked"
|
||||
} else {
|
||||
d.Status = "locked"
|
||||
}
|
||||
d.LastUpdated = time.Now().UTC().Format(time.RFC3339)
|
||||
ds.addHistory(id, "status", oldStatus, d.Status)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("设备类型 %s 不支持切换", d.Type)
|
||||
}
|
||||
|
||||
cp := *d
|
||||
cp.History = ds.GetHistory(id)
|
||||
return &cp, nil
|
||||
}
|
||||
|
||||
// SimulateFluctuation 模拟传感器随机波动
|
||||
func (ds *DeviceStore) SimulateFluctuation() {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
|
||||
// 温度传感器: ±0.2°C
|
||||
if t, ok := ds.devices["sensor-temperature"]; ok {
|
||||
oldVal := t.Value
|
||||
t.Value += (rand.Float64()*0.4 - 0.2)
|
||||
t.Value = float64(int(t.Value*10)) / 10 // 保留一位小数
|
||||
t.LastUpdated = now
|
||||
ds.addHistory("sensor-temperature", "value", fmt.Sprintf("%.1f", oldVal), fmt.Sprintf("%.1f", t.Value))
|
||||
}
|
||||
|
||||
// 湿度传感器: ±1%
|
||||
if h, ok := ds.devices["sensor-humidity"]; ok {
|
||||
oldVal := h.Value
|
||||
h.Value += float64(rand.Intn(3) - 1) // -1, 0, +1
|
||||
if h.Value < 0 {
|
||||
h.Value = 0
|
||||
}
|
||||
if h.Value > 100 {
|
||||
h.Value = 100
|
||||
}
|
||||
h.LastUpdated = now
|
||||
ds.addHistory("sensor-humidity", "value", fmt.Sprintf("%.0f", oldVal), fmt.Sprintf("%.0f", h.Value))
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
port := getEnv("IOT_DEBUG_PORT", "8083")
|
||||
store := NewDeviceStore()
|
||||
|
||||
// 启动传感器波动模拟(每30秒)
|
||||
go func() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
store.SimulateFluctuation()
|
||||
}
|
||||
}()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// GET /api/v1/devices - 列出所有设备
|
||||
mux.HandleFunc("/api/v1/devices", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
devices := store.GetAll()
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"devices": devices,
|
||||
"total": len(devices),
|
||||
})
|
||||
})
|
||||
|
||||
// GET /api/v1/devices/{id} - 获取单个设备
|
||||
// POST /api/v1/devices/{id}/toggle - 切换设备
|
||||
mux.HandleFunc("/api/v1/devices/", func(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/v1/devices/")
|
||||
parts := strings.Split(path, "/")
|
||||
|
||||
if len(parts) == 0 || parts[0] == "" {
|
||||
http.Error(w, "缺少设备ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
deviceID := parts[0]
|
||||
|
||||
// POST /api/v1/devices/{id}/toggle
|
||||
if len(parts) == 2 && parts[1] == "toggle" {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
device, err := store.Toggle(deviceID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"device": device,
|
||||
"action": "toggled",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// GET /api/v1/devices/{id}/history
|
||||
if len(parts) == 2 && parts[1] == "history" {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
history := store.GetHistory(deviceID)
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"device_id": deviceID,
|
||||
"history": history,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// GET /api/v1/devices/{id}
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
device := store.Get(deviceID)
|
||||
if device == nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": fmt.Sprintf("设备 %s 不存在", deviceID)})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"device": device,
|
||||
})
|
||||
})
|
||||
|
||||
// 健康检查
|
||||
mux.HandleFunc("/api/v1/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"status": "ok",
|
||||
"service": "iot-debug-service",
|
||||
})
|
||||
})
|
||||
|
||||
log.Printf("🔌 IoT 调试服务启动在端口 %s", port)
|
||||
log.Printf(" 模拟设备数: %d", len(store.GetAll()))
|
||||
if err := http.ListenAndServe(":"+port, mux); err != nil {
|
||||
log.Fatalf("服务启动失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
Reference in New Issue
Block a user